summaryrefslogtreecommitdiffstats
path: root/browser/base
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base')
-rw-r--r--browser/base/.eslintrc.js11
-rw-r--r--browser/base/content/aboutDialog-appUpdater.js428
-rw-r--r--browser/base/content/aboutDialog.css97
-rw-r--r--browser/base/content/aboutDialog.js80
-rw-r--r--browser/base/content/aboutDialog.xul157
-rw-r--r--browser/base/content/aboutNetError.xhtml699
-rw-r--r--browser/base/content/aboutProviderDirectory.xhtml60
-rw-r--r--browser/base/content/aboutRobots-icon.pngbin0 -> 9817 bytes
-rw-r--r--browser/base/content/aboutRobots-widget-left.pngbin0 -> 2224 bytes
-rw-r--r--browser/base/content/aboutRobots.xhtml108
-rw-r--r--browser/base/content/aboutSocialError.xhtml111
-rw-r--r--browser/base/content/aboutTabCrashed.css11
-rw-r--r--browser/base/content/aboutTabCrashed.js309
-rw-r--r--browser/base/content/aboutTabCrashed.xhtml97
-rw-r--r--browser/base/content/aboutaccounts/aboutaccounts.css24
-rw-r--r--browser/base/content/aboutaccounts/aboutaccounts.js543
-rw-r--r--browser/base/content/aboutaccounts/aboutaccounts.xhtml112
-rw-r--r--browser/base/content/aboutaccounts/images/fox.pngbin0 -> 1951 bytes
-rw-r--r--browser/base/content/aboutaccounts/images/graphic_sync_intro.pngbin0 -> 6441 bytes
-rw-r--r--browser/base/content/aboutaccounts/images/graphic_sync_intro@2x.pngbin0 -> 12852 bytes
-rw-r--r--browser/base/content/aboutaccounts/main.css166
-rw-r--r--browser/base/content/aboutaccounts/normalize.css402
-rw-r--r--browser/base/content/abouthealthreport/abouthealth.css15
-rw-r--r--browser/base/content/abouthealthreport/abouthealth.js180
-rw-r--r--browser/base/content/abouthealthreport/abouthealth.xhtml31
-rw-r--r--browser/base/content/abouthome/aboutHome.css454
-rw-r--r--browser/base/content/abouthome/aboutHome.js398
-rw-r--r--browser/base/content/abouthome/aboutHome.xhtml79
-rw-r--r--browser/base/content/abouthome/addons.pngbin0 -> 1444 bytes
-rw-r--r--browser/base/content/abouthome/addons@2x.pngbin0 -> 3783 bytes
-rw-r--r--browser/base/content/abouthome/bookmarks.pngbin0 -> 1276 bytes
-rw-r--r--browser/base/content/abouthome/bookmarks@2x.pngbin0 -> 2946 bytes
-rw-r--r--browser/base/content/abouthome/downloads.pngbin0 -> 898 bytes
-rw-r--r--browser/base/content/abouthome/downloads@2x.pngbin0 -> 2018 bytes
-rw-r--r--browser/base/content/abouthome/history.pngbin0 -> 1654 bytes
-rw-r--r--browser/base/content/abouthome/history@2x.pngbin0 -> 4629 bytes
-rw-r--r--browser/base/content/abouthome/mozilla.pngbin0 -> 2684 bytes
-rw-r--r--browser/base/content/abouthome/mozilla@2x.pngbin0 -> 5647 bytes
-rw-r--r--browser/base/content/abouthome/restore-large.pngbin0 -> 2841 bytes
-rw-r--r--browser/base/content/abouthome/restore-large@2x.pngbin0 -> 7267 bytes
-rw-r--r--browser/base/content/abouthome/restore.pngbin0 -> 1796 bytes
-rw-r--r--browser/base/content/abouthome/restore@2x.pngbin0 -> 4810 bytes
-rw-r--r--browser/base/content/abouthome/settings.pngbin0 -> 1557 bytes
-rw-r--r--browser/base/content/abouthome/settings@2x.pngbin0 -> 3836 bytes
-rw-r--r--browser/base/content/abouthome/snippet1.pngbin0 -> 1470 bytes
-rw-r--r--browser/base/content/abouthome/snippet1@2x.pngbin0 -> 3243 bytes
-rw-r--r--browser/base/content/abouthome/snippet2.pngbin0 -> 3287 bytes
-rw-r--r--browser/base/content/abouthome/snippet2@2x.pngbin0 -> 11027 bytes
-rw-r--r--browser/base/content/abouthome/sync.pngbin0 -> 1879 bytes
-rw-r--r--browser/base/content/abouthome/sync@2x.pngbin0 -> 4615 bytes
-rw-r--r--browser/base/content/baseMenuOverlay.xul118
-rw-r--r--browser/base/content/blockedSite.xhtml196
-rw-r--r--browser/base/content/browser-addons.js747
-rw-r--r--browser/base/content/browser-captivePortal.js257
-rw-r--r--browser/base/content/browser-charsetmenu.inc12
-rw-r--r--browser/base/content/browser-context.inc472
-rw-r--r--browser/base/content/browser-ctrlTab.js587
-rw-r--r--browser/base/content/browser-customization.js100
-rw-r--r--browser/base/content/browser-data-submission-info-bar.js127
-rw-r--r--browser/base/content/browser-devedition.js142
-rw-r--r--browser/base/content/browser-doctype.inc23
-rw-r--r--browser/base/content/browser-feeds.js646
-rw-r--r--browser/base/content/browser-fullScreenAndPointerLock.js673
-rw-r--r--browser/base/content/browser-fullZoom.js526
-rw-r--r--browser/base/content/browser-fxaccounts.js459
-rw-r--r--browser/base/content/browser-gestureSupport.js1244
-rw-r--r--browser/base/content/browser-media.js365
-rw-r--r--browser/base/content/browser-menubar.inc535
-rw-r--r--browser/base/content/browser-places.js2021
-rw-r--r--browser/base/content/browser-plugins.js548
-rw-r--r--browser/base/content/browser-refreshblocker.js84
-rw-r--r--browser/base/content/browser-safebrowsing.js48
-rw-r--r--browser/base/content/browser-sets.inc380
-rw-r--r--browser/base/content/browser-sidebar.js337
-rw-r--r--browser/base/content/browser-social.js503
-rw-r--r--browser/base/content/browser-syncui.js544
-rw-r--r--browser/base/content/browser-tabPreviews.xml37
-rw-r--r--browser/base/content/browser-tabsintitlebar-stub.js17
-rw-r--r--browser/base/content/browser-tabsintitlebar.js307
-rw-r--r--browser/base/content/browser-thumbnails.js142
-rw-r--r--browser/base/content/browser-trackingprotection.js239
-rw-r--r--browser/base/content/browser.css1244
-rwxr-xr-xbrowser/base/content/browser.js8281
-rw-r--r--browser/base/content/browser.xul1134
-rw-r--r--browser/base/content/browserMountPoints.inc12
-rw-r--r--browser/base/content/content.js1503
-rw-r--r--browser/base/content/contentSearchUI.css161
-rw-r--r--browser/base/content/contentSearchUI.js915
-rw-r--r--browser/base/content/defaultthemes/1.footer.jpgbin0 -> 151200 bytes
-rw-r--r--browser/base/content/defaultthemes/1.header.jpgbin0 -> 266398 bytes
-rw-r--r--browser/base/content/defaultthemes/1.icon.jpgbin0 -> 1093 bytes
-rw-r--r--browser/base/content/defaultthemes/1.preview.jpgbin0 -> 7953 bytes
-rw-r--r--browser/base/content/defaultthemes/2.footer.jpgbin0 -> 81134 bytes
-rw-r--r--browser/base/content/defaultthemes/2.header.jpgbin0 -> 173983 bytes
-rw-r--r--browser/base/content/defaultthemes/2.icon.jpgbin0 -> 509 bytes
-rw-r--r--browser/base/content/defaultthemes/2.preview.jpgbin0 -> 2877 bytes
-rw-r--r--browser/base/content/defaultthemes/3.footer.pngbin0 -> 180454 bytes
-rw-r--r--browser/base/content/defaultthemes/3.header.pngbin0 -> 293504 bytes
-rw-r--r--browser/base/content/defaultthemes/3.icon.pngbin0 -> 896 bytes
-rw-r--r--browser/base/content/defaultthemes/3.preview.pngbin0 -> 56585 bytes
-rw-r--r--browser/base/content/defaultthemes/4.footer.pngbin0 -> 384076 bytes
-rw-r--r--browser/base/content/defaultthemes/4.header.pngbin0 -> 769368 bytes
-rw-r--r--browser/base/content/defaultthemes/4.icon.pngbin0 -> 731 bytes
-rw-r--r--browser/base/content/defaultthemes/4.preview.pngbin0 -> 95328 bytes
-rw-r--r--browser/base/content/defaultthemes/5.footer.pngbin0 -> 9760 bytes
-rw-r--r--browser/base/content/defaultthemes/5.header.pngbin0 -> 9760 bytes
-rw-r--r--browser/base/content/defaultthemes/5.icon.jpgbin0 -> 267 bytes
-rw-r--r--browser/base/content/defaultthemes/5.preview.jpgbin0 -> 2837 bytes
-rw-r--r--browser/base/content/defaultthemes/devedition.header.pngbin0 -> 95 bytes
-rw-r--r--browser/base/content/defaultthemes/devedition.icon.pngbin0 -> 2402 bytes
-rw-r--r--browser/base/content/docs/sslerrorreport/dataformat.rst54
-rw-r--r--browser/base/content/docs/sslerrorreport/index.rst15
-rw-r--r--browser/base/content/docs/sslerrorreport/preferences.rst23
-rw-r--r--browser/base/content/downloadManagerOverlay.xul32
-rw-r--r--browser/base/content/gcli_sec_bad.svg7
-rw-r--r--browser/base/content/gcli_sec_good.svg4
-rw-r--r--browser/base/content/gcli_sec_moderate.svg4
-rwxr-xr-xbrowser/base/content/global-scripts.inc38
-rw-r--r--browser/base/content/hiddenWindow.xul20
-rw-r--r--browser/base/content/macBrowserOverlay.xul66
-rw-r--r--browser/base/content/newtab/alternativeDefaultSites.json50
-rw-r--r--browser/base/content/newtab/cells.js126
-rw-r--r--browser/base/content/newtab/customize.js133
-rw-r--r--browser/base/content/newtab/drag.js151
-rw-r--r--browser/base/content/newtab/dragDataHelper.js22
-rw-r--r--browser/base/content/newtab/drop.js150
-rw-r--r--browser/base/content/newtab/dropPreview.js222
-rw-r--r--browser/base/content/newtab/dropTargetShim.js232
-rw-r--r--browser/base/content/newtab/grid.js279
-rw-r--r--browser/base/content/newtab/newTab.css654
-rw-r--r--browser/base/content/newtab/newTab.inadjacent.json3209
-rw-r--r--browser/base/content/newtab/newTab.js71
-rw-r--r--browser/base/content/newtab/newTab.xhtml96
-rw-r--r--browser/base/content/newtab/page.js297
-rw-r--r--browser/base/content/newtab/search.js15
-rw-r--r--browser/base/content/newtab/sites.js440
-rw-r--r--browser/base/content/newtab/transformations.js270
-rw-r--r--browser/base/content/newtab/undo.js116
-rw-r--r--browser/base/content/newtab/updater.js177
-rw-r--r--browser/base/content/nsContextMenu.js1878
-rw-r--r--browser/base/content/overrides/app-license.html6
-rw-r--r--browser/base/content/pageinfo/feeds.js32
-rw-r--r--browser/base/content/pageinfo/feeds.xml40
-rw-r--r--browser/base/content/pageinfo/pageInfo.css26
-rw-r--r--browser/base/content/pageinfo/pageInfo.js1140
-rw-r--r--browser/base/content/pageinfo/pageInfo.xml20
-rw-r--r--browser/base/content/pageinfo/pageInfo.xul438
-rw-r--r--browser/base/content/pageinfo/permissions.js334
-rw-r--r--browser/base/content/pageinfo/security.js388
-rw-r--r--browser/base/content/popup-notifications.inc81
-rw-r--r--browser/base/content/report-phishing-overlay.xul35
-rw-r--r--browser/base/content/safeMode.css8
-rw-r--r--browser/base/content/safeMode.js82
-rw-r--r--browser/base/content/safeMode.xul51
-rw-r--r--browser/base/content/sanitize.js910
-rw-r--r--browser/base/content/sanitize.xul183
-rw-r--r--browser/base/content/sanitizeDialog.css23
-rw-r--r--browser/base/content/sanitizeDialog.js889
-rw-r--r--browser/base/content/social-content.js172
-rw-r--r--browser/base/content/softwareUpdateOverlay.xul18
-rw-r--r--browser/base/content/sync/aboutSyncTabs-bindings.xml46
-rw-r--r--browser/base/content/sync/aboutSyncTabs.css11
-rw-r--r--browser/base/content/sync/aboutSyncTabs.js361
-rw-r--r--browser/base/content/sync/aboutSyncTabs.xul68
-rw-r--r--browser/base/content/sync/addDevice.js157
-rw-r--r--browser/base/content/sync/addDevice.xul129
-rw-r--r--browser/base/content/sync/customize.css28
-rw-r--r--browser/base/content/sync/customize.js25
-rw-r--r--browser/base/content/sync/customize.xul67
-rw-r--r--browser/base/content/sync/genericChange.js233
-rw-r--r--browser/base/content/sync/genericChange.xul123
-rw-r--r--browser/base/content/sync/key.xhtml54
-rw-r--r--browser/base/content/sync/setup.js1060
-rw-r--r--browser/base/content/sync/setup.xul490
-rw-r--r--browser/base/content/sync/utils.js231
-rw-r--r--browser/base/content/tab-content.js947
-rw-r--r--browser/base/content/tab-shape.inc.svg11
-rw-r--r--browser/base/content/tabbrowser.css98
-rw-r--r--browser/base/content/tabbrowser.xml7417
-rw-r--r--browser/base/content/test/alerts/.eslintrc.js7
-rw-r--r--browser/base/content/test/alerts/browser.ini12
-rw-r--r--browser/base/content/test/alerts/browser_notification_close.js71
-rw-r--r--browser/base/content/test/alerts/browser_notification_do_not_disturb.js80
-rw-r--r--browser/base/content/test/alerts/browser_notification_open_settings.js58
-rw-r--r--browser/base/content/test/alerts/browser_notification_permission_migration.js45
-rw-r--r--browser/base/content/test/alerts/browser_notification_remove_permission.js72
-rw-r--r--browser/base/content/test/alerts/browser_notification_replace.js38
-rw-r--r--browser/base/content/test/alerts/browser_notification_tab_switching.js80
-rw-r--r--browser/base/content/test/alerts/file_dom_notifications.html39
-rw-r--r--browser/base/content/test/alerts/head.js71
-rw-r--r--browser/base/content/test/captivePortal/browser.ini9
-rw-r--r--browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js119
-rw-r--r--browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js91
-rw-r--r--browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js82
-rw-r--r--browser/base/content/test/captivePortal/head.js181
-rw-r--r--browser/base/content/test/chrome/.eslintrc.js7
-rw-r--r--browser/base/content/test/chrome/chrome.ini3
-rw-r--r--browser/base/content/test/chrome/test_aboutCrashed.xul86
-rw-r--r--browser/base/content/test/general/.eslintrc.js8
-rw-r--r--browser/base/content/test/general/POSTSearchEngine.xml6
-rw-r--r--browser/base/content/test/general/aboutHome_content_script.js6
-rw-r--r--browser/base/content/test/general/accounts_testRemoteCommands.html83
-rw-r--r--browser/base/content/test/general/alltabslistener.html8
-rw-r--r--browser/base/content/test/general/app_bug575561.html18
-rw-r--r--browser/base/content/test/general/app_subframe_bug575561.html12
-rw-r--r--browser/base/content/test/general/audio.oggbin0 -> 14293 bytes
-rw-r--r--browser/base/content/test/general/benignPage.html12
-rw-r--r--browser/base/content/test/general/browser.ini494
-rw-r--r--browser/base/content/test/general/browser_PageMetaData_pushstate.js29
-rw-r--r--browser/base/content/test/general/browser_aboutAccounts.js499
-rw-r--r--browser/base/content/test/general/browser_aboutCertError.js409
-rw-r--r--browser/base/content/test/general/browser_aboutHealthReport.js139
-rw-r--r--browser/base/content/test/general/browser_aboutHome.js668
-rw-r--r--browser/base/content/test/general/browser_aboutHome_wrapsCorrectly.js28
-rw-r--r--browser/base/content/test/general/browser_aboutNetError.js47
-rw-r--r--browser/base/content/test/general/browser_aboutSupport_newtab_security_state.js26
-rw-r--r--browser/base/content/test/general/browser_accesskeys.js82
-rw-r--r--browser/base/content/test/general/browser_addCertException.js50
-rw-r--r--browser/base/content/test/general/browser_addKeywordSearch.js81
-rw-r--r--browser/base/content/test/general/browser_alltabslistener.js206
-rw-r--r--browser/base/content/test/general/browser_audioTabIcon.js504
-rw-r--r--browser/base/content/test/general/browser_backButtonFitts.js42
-rw-r--r--browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js76
-rw-r--r--browser/base/content/test/general/browser_blob-channelname.js11
-rw-r--r--browser/base/content/test/general/browser_blockHPKP.js101
-rw-r--r--browser/base/content/test/general/browser_bookmark_popup.js431
-rw-r--r--browser/base/content/test/general/browser_bookmark_titles.js98
-rw-r--r--browser/base/content/test/general/browser_bug1015721.js54
-rw-r--r--browser/base/content/test/general/browser_bug1045809.js68
-rw-r--r--browser/base/content/test/general/browser_bug1064280_changeUrlInPinnedTab.js36
-rw-r--r--browser/base/content/test/general/browser_bug1261299.js73
-rw-r--r--browser/base/content/test/general/browser_bug1297539.js114
-rw-r--r--browser/base/content/test/general/browser_bug1299667.js71
-rw-r--r--browser/base/content/test/general/browser_bug321000.js80
-rw-r--r--browser/base/content/test/general/browser_bug356571.js93
-rw-r--r--browser/base/content/test/general/browser_bug380960.js11
-rw-r--r--browser/base/content/test/general/browser_bug386835.js89
-rw-r--r--browser/base/content/test/general/browser_bug406216.js54
-rw-r--r--browser/base/content/test/general/browser_bug408415.js45
-rw-r--r--browser/base/content/test/general/browser_bug409481.js83
-rw-r--r--browser/base/content/test/general/browser_bug409624.js57
-rw-r--r--browser/base/content/test/general/browser_bug413915.js62
-rw-r--r--browser/base/content/test/general/browser_bug416661.js43
-rw-r--r--browser/base/content/test/general/browser_bug417483.js30
-rw-r--r--browser/base/content/test/general/browser_bug419612.js32
-rw-r--r--browser/base/content/test/general/browser_bug422590.js50
-rw-r--r--browser/base/content/test/general/browser_bug423833.js138
-rw-r--r--browser/base/content/test/general/browser_bug424101.js52
-rw-r--r--browser/base/content/test/general/browser_bug427559.js38
-rw-r--r--browser/base/content/test/general/browser_bug431826.js50
-rw-r--r--browser/base/content/test/general/browser_bug432599.js127
-rw-r--r--browser/base/content/test/general/browser_bug435035.js17
-rw-r--r--browser/base/content/test/general/browser_bug435325.js69
-rw-r--r--browser/base/content/test/general/browser_bug441778.js46
-rw-r--r--browser/base/content/test/general/browser_bug455852.js20
-rw-r--r--browser/base/content/test/general/browser_bug460146.js51
-rw-r--r--browser/base/content/test/general/browser_bug462289.js81
-rw-r--r--browser/base/content/test/general/browser_bug462673.js36
-rw-r--r--browser/base/content/test/general/browser_bug477014.js25
-rw-r--r--browser/base/content/test/general/browser_bug479408.js17
-rw-r--r--browser/base/content/test/general/browser_bug479408_sample.html4
-rw-r--r--browser/base/content/test/general/browser_bug481560.js21
-rw-r--r--browser/base/content/test/general/browser_bug484315.js23
-rw-r--r--browser/base/content/test/general/browser_bug491431.js34
-rw-r--r--browser/base/content/test/general/browser_bug495058.js38
-rw-r--r--browser/base/content/test/general/browser_bug517902.js42
-rw-r--r--browser/base/content/test/general/browser_bug519216.js45
-rw-r--r--browser/base/content/test/general/browser_bug520538.js15
-rw-r--r--browser/base/content/test/general/browser_bug521216.js50
-rw-r--r--browser/base/content/test/general/browser_bug533232.js36
-rw-r--r--browser/base/content/test/general/browser_bug537013.js135
-rw-r--r--browser/base/content/test/general/browser_bug537474.js8
-rw-r--r--browser/base/content/test/general/browser_bug550565.js44
-rw-r--r--browser/base/content/test/general/browser_bug553455.js1200
-rw-r--r--browser/base/content/test/general/browser_bug555224.js40
-rw-r--r--browser/base/content/test/general/browser_bug555767.js54
-rw-r--r--browser/base/content/test/general/browser_bug559991.js42
-rw-r--r--browser/base/content/test/general/browser_bug561636.js370
-rw-r--r--browser/base/content/test/general/browser_bug563588.js30
-rw-r--r--browser/base/content/test/general/browser_bug565575.js14
-rw-r--r--browser/base/content/test/general/browser_bug567306.js50
-rw-r--r--browser/base/content/test/general/browser_bug575561.js97
-rw-r--r--browser/base/content/test/general/browser_bug575830.js33
-rw-r--r--browser/base/content/test/general/browser_bug577121.js29
-rw-r--r--browser/base/content/test/general/browser_bug578534.js23
-rw-r--r--browser/base/content/test/general/browser_bug579872.js28
-rw-r--r--browser/base/content/test/general/browser_bug580638.js60
-rw-r--r--browser/base/content/test/general/browser_bug580956.js26
-rw-r--r--browser/base/content/test/general/browser_bug581242.js21
-rw-r--r--browser/base/content/test/general/browser_bug581253.js86
-rw-r--r--browser/base/content/test/general/browser_bug585558.js153
-rw-r--r--browser/base/content/test/general/browser_bug585785.js35
-rw-r--r--browser/base/content/test/general/browser_bug585830.js25
-rw-r--r--browser/base/content/test/general/browser_bug590206.js163
-rw-r--r--browser/base/content/test/general/browser_bug592338.js163
-rw-r--r--browser/base/content/test/general/browser_bug594131.js21
-rw-r--r--browser/base/content/test/general/browser_bug595507.js36
-rw-r--r--browser/base/content/test/general/browser_bug596687.js25
-rw-r--r--browser/base/content/test/general/browser_bug597218.js38
-rw-r--r--browser/base/content/test/general/browser_bug609700.js20
-rw-r--r--browser/base/content/test/general/browser_bug623893.js37
-rw-r--r--browser/base/content/test/general/browser_bug624734.js29
-rw-r--r--browser/base/content/test/general/browser_bug633691.js28
-rw-r--r--browser/base/content/test/general/browser_bug647886.js40
-rw-r--r--browser/base/content/test/general/browser_bug655584.js23
-rw-r--r--browser/base/content/test/general/browser_bug664672.js19
-rw-r--r--browser/base/content/test/general/browser_bug676619.js124
-rw-r--r--browser/base/content/test/general/browser_bug678392-1.html12
-rw-r--r--browser/base/content/test/general/browser_bug678392-2.html12
-rw-r--r--browser/base/content/test/general/browser_bug678392.js191
-rw-r--r--browser/base/content/test/general/browser_bug710878.js34
-rw-r--r--browser/base/content/test/general/browser_bug719271.js95
-rw-r--r--browser/base/content/test/general/browser_bug724239.js11
-rw-r--r--browser/base/content/test/general/browser_bug734076.js114
-rw-r--r--browser/base/content/test/general/browser_bug735471.js23
-rw-r--r--browser/base/content/test/general/browser_bug749738.js29
-rw-r--r--browser/base/content/test/general/browser_bug763468_perwindowpb.js70
-rw-r--r--browser/base/content/test/general/browser_bug767836_perwindowpb.js90
-rw-r--r--browser/base/content/test/general/browser_bug817947.js55
-rw-r--r--browser/base/content/test/general/browser_bug822367.js187
-rw-r--r--browser/base/content/test/general/browser_bug832435.js23
-rw-r--r--browser/base/content/test/general/browser_bug839103.js120
-rw-r--r--browser/base/content/test/general/browser_bug882977.js29
-rw-r--r--browser/base/content/test/general/browser_bug902156.js174
-rw-r--r--browser/base/content/test/general/browser_bug906190.js240
-rw-r--r--browser/base/content/test/general/browser_bug963945.js23
-rw-r--r--browser/base/content/test/general/browser_bug970746.js121
-rw-r--r--browser/base/content/test/general/browser_bug970746.xhtml20
-rw-r--r--browser/base/content/test/general/browser_clipboard.js174
-rw-r--r--browser/base/content/test/general/browser_clipboard_pastefile.js62
-rw-r--r--browser/base/content/test/general/browser_contentAltClick.js107
-rw-r--r--browser/base/content/test/general/browser_contentAreaClick.js307
-rw-r--r--browser/base/content/test/general/browser_contentSearchUI.js771
-rw-r--r--browser/base/content/test/general/browser_contextmenu.js996
-rw-r--r--browser/base/content/test/general/browser_contextmenu_childprocess.js84
-rw-r--r--browser/base/content/test/general/browser_contextmenu_input.js243
-rw-r--r--browser/base/content/test/general/browser_csp_block_all_mixedcontent.js55
-rw-r--r--browser/base/content/test/general/browser_ctrlTab.js185
-rw-r--r--browser/base/content/test/general/browser_datachoices_notification.js221
-rw-r--r--browser/base/content/test/general/browser_decoderDoctor.js122
-rw-r--r--browser/base/content/test/general/browser_devedition.js129
-rw-r--r--browser/base/content/test/general/browser_discovery.js162
-rw-r--r--browser/base/content/test/general/browser_documentnavigation.js266
-rw-r--r--browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js221
-rw-r--r--browser/base/content/test/general/browser_double_close_tab.js80
-rw-r--r--browser/base/content/test/general/browser_drag.js45
-rw-r--r--browser/base/content/test/general/browser_duplicateIDs.js8
-rw-r--r--browser/base/content/test/general/browser_e10s_about_process.js114
-rw-r--r--browser/base/content/test/general/browser_e10s_chrome_process.js150
-rw-r--r--browser/base/content/test/general/browser_e10s_javascript.js11
-rw-r--r--browser/base/content/test/general/browser_e10s_switchbrowser.js261
-rw-r--r--browser/base/content/test/general/browser_favicon_change.js41
-rw-r--r--browser/base/content/test/general/browser_favicon_change_not_in_document.js34
-rw-r--r--browser/base/content/test/general/browser_feed_discovery.js33
-rw-r--r--browser/base/content/test/general/browser_findbarClose.js35
-rw-r--r--browser/base/content/test/general/browser_focusonkeydown.js26
-rw-r--r--browser/base/content/test/general/browser_fullscreen-window-open.js347
-rw-r--r--browser/base/content/test/general/browser_fxa_migrate.js18
-rw-r--r--browser/base/content/test/general/browser_fxa_oauth.html30
-rw-r--r--browser/base/content/test/general/browser_fxa_oauth.js327
-rw-r--r--browser/base/content/test/general/browser_fxa_oauth_with_keys.html33
-rw-r--r--browser/base/content/test/general/browser_fxa_web_channel.html138
-rw-r--r--browser/base/content/test/general/browser_fxa_web_channel.js210
-rw-r--r--browser/base/content/test/general/browser_fxaccounts.js261
-rw-r--r--browser/base/content/test/general/browser_gZipOfflineChild.js80
-rw-r--r--browser/base/content/test/general/browser_gestureSupport.js670
-rw-r--r--browser/base/content/test/general/browser_getshortcutoruri.js143
-rw-r--r--browser/base/content/test/general/browser_hide_removing.js39
-rw-r--r--browser/base/content/test/general/browser_homeDrop.js90
-rw-r--r--browser/base/content/test/general/browser_identity_UI.js146
-rw-r--r--browser/base/content/test/general/browser_insecureLoginForms.js162
-rw-r--r--browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js39
-rw-r--r--browser/base/content/test/general/browser_keywordBookmarklets.js54
-rw-r--r--browser/base/content/test/general/browser_keywordSearch.js88
-rw-r--r--browser/base/content/test/general/browser_keywordSearch_postData.js94
-rw-r--r--browser/base/content/test/general/browser_lastAccessedTab.js47
-rw-r--r--browser/base/content/test/general/browser_mcb_redirect.js314
-rw-r--r--browser/base/content/test/general/browser_menuButtonBadgeManager.js46
-rw-r--r--browser/base/content/test/general/browser_menuButtonFitts.js32
-rw-r--r--browser/base/content/test/general/browser_middleMouse_noJSPaste.js34
-rw-r--r--browser/base/content/test/general/browser_minimize.js18
-rw-r--r--browser/base/content/test/general/browser_misused_characters_in_strings.js244
-rw-r--r--browser/base/content/test/general/browser_mixedContentFramesOnHttp.js34
-rw-r--r--browser/base/content/test/general/browser_mixedContentFromOnunload.js49
-rw-r--r--browser/base/content/test/general/browser_mixed_content_cert_override.js54
-rw-r--r--browser/base/content/test/general/browser_mixedcontent_securityflags.js70
-rw-r--r--browser/base/content/test/general/browser_modifiedclick_inherit_principal.js30
-rw-r--r--browser/base/content/test/general/browser_newTabDrop.js99
-rw-r--r--browser/base/content/test/general/browser_newWindowDrop.js120
-rw-r--r--browser/base/content/test/general/browser_newwindow_focus.js96
-rw-r--r--browser/base/content/test/general/browser_no_mcb_on_http_site.js106
-rw-r--r--browser/base/content/test/general/browser_offlineQuotaNotification.js95
-rw-r--r--browser/base/content/test/general/browser_overflowScroll.js91
-rw-r--r--browser/base/content/test/general/browser_pageInfo.js38
-rw-r--r--browser/base/content/test/general/browser_page_style_menu.js97
-rw-r--r--browser/base/content/test/general/browser_page_style_menu_update.js67
-rw-r--r--browser/base/content/test/general/browser_pageinfo_svg_image.js38
-rw-r--r--browser/base/content/test/general/browser_parsable_css.js376
-rw-r--r--browser/base/content/test/general/browser_parsable_script.js132
-rw-r--r--browser/base/content/test/general/browser_permissions.js202
-rw-r--r--browser/base/content/test/general/browser_pinnedTabs.js75
-rw-r--r--browser/base/content/test/general/browser_plainTextLinks.js146
-rw-r--r--browser/base/content/test/general/browser_printpreview.js74
-rw-r--r--browser/base/content/test/general/browser_private_browsing_window.js65
-rw-r--r--browser/base/content/test/general/browser_private_no_prompt.js12
-rw-r--r--browser/base/content/test/general/browser_purgehistory_clears_sh.js60
-rw-r--r--browser/base/content/test/general/browser_refreshBlocker.js135
-rw-r--r--browser/base/content/test/general/browser_registerProtocolHandler_notification.html15
-rw-r--r--browser/base/content/test/general/browser_registerProtocolHandler_notification.js43
-rw-r--r--browser/base/content/test/general/browser_relatedTabs.js51
-rw-r--r--browser/base/content/test/general/browser_remoteTroubleshoot.js93
-rw-r--r--browser/base/content/test/general/browser_remoteWebNavigation_postdata.js50
-rw-r--r--browser/base/content/test/general/browser_removeTabsToTheEnd.js24
-rw-r--r--browser/base/content/test/general/browser_restore_isAppTab.js160
-rw-r--r--browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js39
-rw-r--r--browser/base/content/test/general/browser_sanitize-sitepermissions.js52
-rw-r--r--browser/base/content/test/general/browser_sanitize-timespans.js733
-rw-r--r--browser/base/content/test/general/browser_sanitizeDialog.js1027
-rw-r--r--browser/base/content/test/general/browser_save_link-perwindowpb.js199
-rw-r--r--browser/base/content/test/general/browser_save_link_when_window_navigates.js173
-rw-r--r--browser/base/content/test/general/browser_save_private_link_perwindowpb.js116
-rw-r--r--browser/base/content/test/general/browser_save_video.js87
-rw-r--r--browser/base/content/test/general/browser_save_video_frame.js125
-rw-r--r--browser/base/content/test/general/browser_scope.js10
-rw-r--r--browser/base/content/test/general/browser_selectTabAtIndex.js81
-rw-r--r--browser/base/content/test/general/browser_selectpopup.js563
-rw-r--r--browser/base/content/test/general/browser_ssl_error_reports.js174
-rw-r--r--browser/base/content/test/general/browser_star_hsts.js85
-rw-r--r--browser/base/content/test/general/browser_star_hsts.sjs13
-rw-r--r--browser/base/content/test/general/browser_subframe_favicons_not_used.js20
-rw-r--r--browser/base/content/test/general/browser_syncui.js205
-rw-r--r--browser/base/content/test/general/browser_tabDrop.js103
-rw-r--r--browser/base/content/test/general/browser_tabReorder.js49
-rw-r--r--browser/base/content/test/general/browser_tab_close_dependent_window.js24
-rw-r--r--browser/base/content/test/general/browser_tab_detach_restore.js34
-rw-r--r--browser/base/content/test/general/browser_tab_drag_drop_perwindow.js216
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop.js186
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop2.js57
-rw-r--r--browser/base/content/test/general/browser_tab_dragdrop2_frame1.xul169
-rw-r--r--browser/base/content/test/general/browser_tabbar_big_widgets.js29
-rw-r--r--browser/base/content/test/general/browser_tabfocus.js565
-rw-r--r--browser/base/content/test/general/browser_tabkeynavigation.js156
-rw-r--r--browser/base/content/test/general/browser_tabopen_reflows.js157
-rw-r--r--browser/base/content/test/general/browser_tabs_close_beforeunload.js49
-rw-r--r--browser/base/content/test/general/browser_tabs_isActive.js152
-rw-r--r--browser/base/content/test/general/browser_tabs_owner.js44
-rw-r--r--browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js126
-rw-r--r--browser/base/content/test/general/browser_trackingUI_1.js170
-rw-r--r--browser/base/content/test/general/browser_trackingUI_2.js96
-rw-r--r--browser/base/content/test/general/browser_trackingUI_3.js52
-rw-r--r--browser/base/content/test/general/browser_trackingUI_4.js109
-rw-r--r--browser/base/content/test/general/browser_trackingUI_5.js131
-rw-r--r--browser/base/content/test/general/browser_trackingUI_6.js46
-rw-r--r--browser/base/content/test/general/browser_trackingUI_telemetry.js145
-rw-r--r--browser/base/content/test/general/browser_typeAheadFind.js22
-rw-r--r--browser/base/content/test/general/browser_unknownContentType_title.js33
-rw-r--r--browser/base/content/test/general/browser_unloaddialogs.js41
-rw-r--r--browser/base/content/test/general/browser_utilityOverlay.js112
-rw-r--r--browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js55
-rw-r--r--browser/base/content/test/general/browser_visibleFindSelection.js52
-rw-r--r--browser/base/content/test/general/browser_visibleTabs.js97
-rw-r--r--browser/base/content/test/general/browser_visibleTabs_bookmarkAllPages.js34
-rw-r--r--browser/base/content/test/general/browser_visibleTabs_bookmarkAllTabs.js66
-rw-r--r--browser/base/content/test/general/browser_visibleTabs_contextMenu.js72
-rw-r--r--browser/base/content/test/general/browser_visibleTabs_tabPreview.js41
-rw-r--r--browser/base/content/test/general/browser_web_channel.html189
-rw-r--r--browser/base/content/test/general/browser_web_channel.js436
-rw-r--r--browser/base/content/test/general/browser_web_channel_iframe.html96
-rw-r--r--browser/base/content/test/general/browser_windowactivation.js183
-rw-r--r--browser/base/content/test/general/browser_windowopen_reflows.js117
-rw-r--r--browser/base/content/test/general/browser_zbug569342.js80
-rw-r--r--browser/base/content/test/general/bug1262648_string_with_newlines.dtd3
-rw-r--r--browser/base/content/test/general/bug364677-data.xml5
-rw-r--r--browser/base/content/test/general/bug364677-data.xml^headers^1
-rw-r--r--browser/base/content/test/general/bug395533-data.txt6
-rw-r--r--browser/base/content/test/general/bug592338.html24
-rw-r--r--browser/base/content/test/general/bug792517-2.html5
-rw-r--r--browser/base/content/test/general/bug792517.html5
-rw-r--r--browser/base/content/test/general/bug792517.sjs13
-rw-r--r--browser/base/content/test/general/bug839103.css1
-rw-r--r--browser/base/content/test/general/clipboard_pastefile.html37
-rw-r--r--browser/base/content/test/general/close_beforeunload.html8
-rw-r--r--browser/base/content/test/general/close_beforeunload_opens_second_tab.html3
-rw-r--r--browser/base/content/test/general/contentSearchUI.html21
-rw-r--r--browser/base/content/test/general/contentSearchUI.js209
-rw-r--r--browser/base/content/test/general/content_aboutAccounts.js87
-rw-r--r--browser/base/content/test/general/contextmenu_common.js324
-rw-r--r--browser/base/content/test/general/ctxmenu-image.pngbin0 -> 5401 bytes
-rw-r--r--browser/base/content/test/general/discovery.html8
-rw-r--r--browser/base/content/test/general/download_page.html47
-rw-r--r--browser/base/content/test/general/dummy_page.html9
-rw-r--r--browser/base/content/test/general/feed_discovery.html73
-rw-r--r--browser/base/content/test/general/feed_tab.html17
-rw-r--r--browser/base/content/test/general/file_bug1045809_1.html7
-rw-r--r--browser/base/content/test/general/file_bug1045809_2.html7
-rw-r--r--browser/base/content/test/general/file_bug822367_1.html18
-rw-r--r--browser/base/content/test/general/file_bug822367_1.js1
-rw-r--r--browser/base/content/test/general/file_bug822367_2.html16
-rw-r--r--browser/base/content/test/general/file_bug822367_3.html27
-rw-r--r--browser/base/content/test/general/file_bug822367_4.html18
-rw-r--r--browser/base/content/test/general/file_bug822367_4.js1
-rw-r--r--browser/base/content/test/general/file_bug822367_4B.html18
-rw-r--r--browser/base/content/test/general/file_bug822367_5.html24
-rw-r--r--browser/base/content/test/general/file_bug822367_6.html16
-rw-r--r--browser/base/content/test/general/file_bug902156.js5
-rw-r--r--browser/base/content/test/general/file_bug902156_1.html15
-rw-r--r--browser/base/content/test/general/file_bug902156_2.html17
-rw-r--r--browser/base/content/test/general/file_bug902156_3.html15
-rw-r--r--browser/base/content/test/general/file_bug906190.js5
-rw-r--r--browser/base/content/test/general/file_bug906190.sjs17
-rw-r--r--browser/base/content/test/general/file_bug906190_1.html15
-rw-r--r--browser/base/content/test/general/file_bug906190_2.html15
-rw-r--r--browser/base/content/test/general/file_bug906190_3_4.html14
-rw-r--r--browser/base/content/test/general/file_bug906190_redirected.html15
-rw-r--r--browser/base/content/test/general/file_bug970276_favicon1.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/general/file_bug970276_favicon2.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/general/file_bug970276_popup1.html14
-rw-r--r--browser/base/content/test/general/file_bug970276_popup2.html12
-rw-r--r--browser/base/content/test/general/file_csp_block_all_mixedcontent.html9
-rw-r--r--browser/base/content/test/general/file_csp_block_all_mixedcontent.js3
-rw-r--r--browser/base/content/test/general/file_documentnavigation_frameset.html12
-rw-r--r--browser/base/content/test/general/file_double_close_tab.html15
-rw-r--r--browser/base/content/test/general/file_favicon_change.html13
-rw-r--r--browser/base/content/test/general/file_favicon_change_not_in_document.html21
-rw-r--r--browser/base/content/test/general/file_fullscreen-window-open.html24
-rw-r--r--browser/base/content/test/general/file_generic_favicon.icobin0 -> 1406 bytes
-rw-r--r--browser/base/content/test/general/file_mediaPlayback.html2
-rw-r--r--browser/base/content/test/general/file_mixedContentFramesOnHttp.html14
-rw-r--r--browser/base/content/test/general/file_mixedContentFromOnunload.html18
-rw-r--r--browser/base/content/test/general/file_mixedContentFromOnunload_test1.html14
-rw-r--r--browser/base/content/test/general/file_mixedContentFromOnunload_test2.html15
-rw-r--r--browser/base/content/test/general/file_mixedPassiveContent.html13
-rw-r--r--browser/base/content/test/general/file_trackingUI_6.html16
-rw-r--r--browser/base/content/test/general/file_trackingUI_6.js2
-rw-r--r--browser/base/content/test/general/file_trackingUI_6.js^headers^1
-rw-r--r--browser/base/content/test/general/file_with_favicon.html12
-rw-r--r--browser/base/content/test/general/fxa_profile_handler.sjs34
-rw-r--r--browser/base/content/test/general/gZipOfflineChild.cacheManifest2
-rw-r--r--browser/base/content/test/general/gZipOfflineChild.cacheManifest^headers^1
-rw-r--r--browser/base/content/test/general/gZipOfflineChild.htmlbin0 -> 303 bytes
-rw-r--r--browser/base/content/test/general/gZipOfflineChild.html^headers^2
-rw-r--r--browser/base/content/test/general/gZipOfflineChild_uncompressed.html21
-rw-r--r--browser/base/content/test/general/head.js1069
-rw-r--r--browser/base/content/test/general/head_plain.js27
-rw-r--r--browser/base/content/test/general/healthreport_pingData.js17
-rw-r--r--browser/base/content/test/general/healthreport_testRemoteCommands.html243
-rw-r--r--browser/base/content/test/general/insecure_opener.html9
-rw-r--r--browser/base/content/test/general/mochitest.ini27
-rw-r--r--browser/base/content/test/general/moz.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/general/navigating_window_with_download.html7
-rw-r--r--browser/base/content/test/general/offlineByDefault.js17
-rw-r--r--browser/base/content/test/general/offlineChild.cacheManifest2
-rw-r--r--browser/base/content/test/general/offlineChild.cacheManifest^headers^1
-rw-r--r--browser/base/content/test/general/offlineChild.html20
-rw-r--r--browser/base/content/test/general/offlineChild2.cacheManifest2
-rw-r--r--browser/base/content/test/general/offlineChild2.cacheManifest^headers^1
-rw-r--r--browser/base/content/test/general/offlineChild2.html20
-rw-r--r--browser/base/content/test/general/offlineEvent.cacheManifest2
-rw-r--r--browser/base/content/test/general/offlineEvent.cacheManifest^headers^1
-rw-r--r--browser/base/content/test/general/offlineEvent.html9
-rw-r--r--browser/base/content/test/general/offlineQuotaNotification.cacheManifest7
-rw-r--r--browser/base/content/test/general/offlineQuotaNotification.html9
-rw-r--r--browser/base/content/test/general/page_style_sample.html41
-rw-r--r--browser/base/content/test/general/parsingTestHelpers.jsm131
-rw-r--r--browser/base/content/test/general/permissions.html14
-rw-r--r--browser/base/content/test/general/pinning_headers.sjs23
-rw-r--r--browser/base/content/test/general/print_postdata.sjs22
-rw-r--r--browser/base/content/test/general/refresh_header.sjs24
-rw-r--r--browser/base/content/test/general/refresh_meta.sjs36
-rw-r--r--browser/base/content/test/general/searchSuggestionEngine.sjs9
-rw-r--r--browser/base/content/test/general/searchSuggestionEngine.xml9
-rw-r--r--browser/base/content/test/general/searchSuggestionEngine2.xml9
-rw-r--r--browser/base/content/test/general/ssl_error_reports.sjs91
-rw-r--r--browser/base/content/test/general/subtst_contextmenu.html73
-rw-r--r--browser/base/content/test/general/subtst_contextmenu_input.html29
-rw-r--r--browser/base/content/test/general/subtst_contextmenu_xul.xul9
-rw-r--r--browser/base/content/test/general/svg_image.html11
-rw-r--r--browser/base/content/test/general/test-mixedcontent-securityerrors.html21
-rw-r--r--browser/base/content/test/general/test_bug364677.html32
-rw-r--r--browser/base/content/test/general/test_bug395533.html38
-rw-r--r--browser/base/content/test/general/test_bug435035.html1
-rw-r--r--browser/base/content/test/general/test_bug462673.html18
-rw-r--r--browser/base/content/test/general/test_bug628179.html10
-rw-r--r--browser/base/content/test/general/test_bug839103.html10
-rw-r--r--browser/base/content/test/general/test_bug959531.html9
-rw-r--r--browser/base/content/test/general/test_mcb_double_redirect_image.html23
-rw-r--r--browser/base/content/test/general/test_mcb_redirect.html15
-rw-r--r--browser/base/content/test/general/test_mcb_redirect.js5
-rw-r--r--browser/base/content/test/general/test_mcb_redirect.sjs22
-rw-r--r--browser/base/content/test/general/test_mcb_redirect_image.html23
-rw-r--r--browser/base/content/test/general/test_no_mcb_on_http_site_font.css10
-rw-r--r--browser/base/content/test/general/test_no_mcb_on_http_site_font.html47
-rw-r--r--browser/base/content/test/general/test_no_mcb_on_http_site_font2.css1
-rw-r--r--browser/base/content/test/general/test_no_mcb_on_http_site_font2.html48
-rw-r--r--browser/base/content/test/general/test_no_mcb_on_http_site_img.css3
-rw-r--r--browser/base/content/test/general/test_no_mcb_on_http_site_img.html47
-rw-r--r--browser/base/content/test/general/test_offlineNotification.html129
-rw-r--r--browser/base/content/test/general/test_offline_gzip.html21
-rw-r--r--browser/base/content/test/general/test_process_flags_chrome.html10
-rw-r--r--browser/base/content/test/general/test_remoteTroubleshoot.html50
-rw-r--r--browser/base/content/test/general/title_test.svg59
-rw-r--r--browser/base/content/test/general/trackingPage.html12
-rw-r--r--browser/base/content/test/general/unknownContentType_file.pif1
-rw-r--r--browser/base/content/test/general/unknownContentType_file.pif^headers^1
-rw-r--r--browser/base/content/test/general/video.oggbin0 -> 285310 bytes
-rw-r--r--browser/base/content/test/general/web_video.html10
-rw-r--r--browser/base/content/test/general/web_video1.ogvbin0 -> 28942 bytes
-rw-r--r--browser/base/content/test/general/web_video1.ogv^headers^3
-rw-r--r--browser/base/content/test/general/zoom_test.html14
-rw-r--r--browser/base/content/test/newtab/.eslintrc.js7
-rw-r--r--browser/base/content/test/newtab/browser.ini55
-rw-r--r--browser/base/content/test/newtab/browser_newtab_1188015.js26
-rw-r--r--browser/base/content/test/newtab/browser_newtab_background_captures.js64
-rw-r--r--browser/base/content/test/newtab/browser_newtab_block.js95
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug1145428.js87
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug1178586.js83
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug1194895.js146
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug1271075.js32
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug721442.js28
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug722273.js73
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug723102.js24
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug723121.js42
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug725996.js35
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug734043.js34
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug735987.js32
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug752841.js56
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug765628.js32
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug876313.js24
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug991111.js35
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug991210.js34
-rw-r--r--browser/base/content/test/newtab/browser_newtab_bug998387.js39
-rw-r--r--browser/base/content/test/newtab/browser_newtab_disable.js49
-rw-r--r--browser/base/content/test/newtab/browser_newtab_drag_drop.js95
-rw-r--r--browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js63
-rw-r--r--browser/base/content/test/newtab/browser_newtab_drop_preview.js41
-rw-r--r--browser/base/content/test/newtab/browser_newtab_enhanced.js228
-rw-r--r--browser/base/content/test/newtab/browser_newtab_focus.js48
-rw-r--r--browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js56
-rw-r--r--browser/base/content/test/newtab/browser_newtab_reflow_load.js37
-rw-r--r--browser/base/content/test/newtab/browser_newtab_reportLinkAction.js83
-rw-r--r--browser/base/content/test/newtab/browser_newtab_search.js247
-rw-r--r--browser/base/content/test/newtab/browser_newtab_sponsored_icon_click.js53
-rw-r--r--browser/base/content/test/newtab/browser_newtab_undo.js47
-rw-r--r--browser/base/content/test/newtab/browser_newtab_unpin.js56
-rw-r--r--browser/base/content/test/newtab/browser_newtab_update.js48
-rw-r--r--browser/base/content/test/newtab/content-reflows.js26
-rw-r--r--browser/base/content/test/newtab/head.js552
-rw-r--r--browser/base/content/test/newtab/searchEngine1x2xLogo.xml9
-rw-r--r--browser/base/content/test/newtab/searchEngine1xLogo.xml7
-rw-r--r--browser/base/content/test/newtab/searchEngine2xLogo.xml7
-rw-r--r--browser/base/content/test/newtab/searchEngineFavicon.xml6
-rw-r--r--browser/base/content/test/newtab/searchEngineNoLogo.xml5
-rw-r--r--browser/base/content/test/plugins/.eslintrc.js7
-rw-r--r--browser/base/content/test/plugins/blockNoPlugins.xml7
-rw-r--r--browser/base/content/test/plugins/blockPluginHard.xml11
-rw-r--r--browser/base/content/test/plugins/blockPluginInfoURL.xml12
-rw-r--r--browser/base/content/test/plugins/blockPluginVulnerableNoUpdate.xml11
-rw-r--r--browser/base/content/test/plugins/blockPluginVulnerableUpdatable.xml11
-rw-r--r--browser/base/content/test/plugins/blocklist_proxy.js78
-rw-r--r--browser/base/content/test/plugins/browser.ini78
-rw-r--r--browser/base/content/test/plugins/browser_CTP_context_menu.js69
-rw-r--r--browser/base/content/test/plugins/browser_CTP_crashreporting.js233
-rw-r--r--browser/base/content/test/plugins/browser_CTP_data_urls.js255
-rw-r--r--browser/base/content/test/plugins/browser_CTP_drag_drop.js96
-rw-r--r--browser/base/content/test/plugins/browser_CTP_hide_overlay.js88
-rw-r--r--browser/base/content/test/plugins/browser_CTP_iframe.js48
-rw-r--r--browser/base/content/test/plugins/browser_CTP_multi_allow.js99
-rw-r--r--browser/base/content/test/plugins/browser_CTP_nonplugins.js58
-rw-r--r--browser/base/content/test/plugins/browser_CTP_notificationBar.js151
-rw-r--r--browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js120
-rw-r--r--browser/base/content/test/plugins/browser_CTP_remove_navigate.js79
-rw-r--r--browser/base/content/test/plugins/browser_CTP_resize.js130
-rw-r--r--browser/base/content/test/plugins/browser_CTP_zoom.js62
-rw-r--r--browser/base/content/test/plugins/browser_blocking.js349
-rw-r--r--browser/base/content/test/plugins/browser_blocklist_content.js104
-rw-r--r--browser/base/content/test/plugins/browser_bug743421.js119
-rw-r--r--browser/base/content/test/plugins/browser_bug744745.js50
-rw-r--r--browser/base/content/test/plugins/browser_bug787619.js65
-rw-r--r--browser/base/content/test/plugins/browser_bug797677.js43
-rw-r--r--browser/base/content/test/plugins/browser_bug812562.js80
-rw-r--r--browser/base/content/test/plugins/browser_bug818118.js40
-rw-r--r--browser/base/content/test/plugins/browser_bug820497.js71
-rw-r--r--browser/base/content/test/plugins/browser_clearplugindata.html30
-rw-r--r--browser/base/content/test/plugins/browser_clearplugindata.js127
-rw-r--r--browser/base/content/test/plugins/browser_clearplugindata_noage.html30
-rw-r--r--browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js34
-rw-r--r--browser/base/content/test/plugins/browser_pageInfo_plugins.js191
-rw-r--r--browser/base/content/test/plugins/browser_pluginCrashCommentAndURL.js207
-rw-r--r--browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js254
-rw-r--r--browser/base/content/test/plugins/browser_plugin_reloading.js85
-rw-r--r--browser/base/content/test/plugins/browser_pluginnotification.js626
-rw-r--r--browser/base/content/test/plugins/browser_plugins_added_dynamically.js137
-rw-r--r--browser/base/content/test/plugins/browser_private_clicktoplay.js216
-rw-r--r--browser/base/content/test/plugins/head.js396
-rw-r--r--browser/base/content/test/plugins/plugin_add_dynamically.html18
-rw-r--r--browser/base/content/test/plugins/plugin_alternate_content.html9
-rw-r--r--browser/base/content/test/plugins/plugin_big.html9
-rw-r--r--browser/base/content/test/plugins/plugin_both.html10
-rw-r--r--browser/base/content/test/plugins/plugin_both2.html10
-rw-r--r--browser/base/content/test/plugins/plugin_bug744745.html12
-rw-r--r--browser/base/content/test/plugins/plugin_bug749455.html8
-rw-r--r--browser/base/content/test/plugins/plugin_bug787619.html9
-rw-r--r--browser/base/content/test/plugins/plugin_bug797677.html5
-rw-r--r--browser/base/content/test/plugins/plugin_bug820497.html17
-rw-r--r--browser/base/content/test/plugins/plugin_clickToPlayAllow.html9
-rw-r--r--browser/base/content/test/plugins/plugin_clickToPlayDeny.html9
-rw-r--r--browser/base/content/test/plugins/plugin_crashCommentAndURL.html27
-rw-r--r--browser/base/content/test/plugins/plugin_data_url.html11
-rw-r--r--browser/base/content/test/plugins/plugin_hidden_to_visible.html11
-rw-r--r--browser/base/content/test/plugins/plugin_iframe.html9
-rw-r--r--browser/base/content/test/plugins/plugin_outsideScrollArea.html25
-rw-r--r--browser/base/content/test/plugins/plugin_overlayed.html27
-rw-r--r--browser/base/content/test/plugins/plugin_positioned.html12
-rw-r--r--browser/base/content/test/plugins/plugin_small.html9
-rw-r--r--browser/base/content/test/plugins/plugin_small_2.html9
-rw-r--r--browser/base/content/test/plugins/plugin_syncRemoved.html15
-rw-r--r--browser/base/content/test/plugins/plugin_test.html9
-rw-r--r--browser/base/content/test/plugins/plugin_test2.html10
-rw-r--r--browser/base/content/test/plugins/plugin_test3.html9
-rw-r--r--browser/base/content/test/plugins/plugin_two_types.html9
-rw-r--r--browser/base/content/test/plugins/plugin_unknown.html9
-rw-r--r--browser/base/content/test/plugins/plugin_zoom.html10
-rw-r--r--browser/base/content/test/popupNotifications/.eslintrc.js7
-rw-r--r--browser/base/content/test/popupNotifications/browser.ini18
-rw-r--r--browser/base/content/test/popupNotifications/browser_displayURI.js28
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification.js203
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_2.js266
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_3.js305
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_4.js294
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js211
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js74
-rw-r--r--browser/base/content/test/popupNotifications/browser_reshow_in_background.js52
-rw-r--r--browser/base/content/test/popupNotifications/head.js303
-rw-r--r--browser/base/content/test/popups/browser.ini4
-rw-r--r--browser/base/content/test/popups/browser_popupUI.js37
-rw-r--r--browser/base/content/test/popups/browser_popup_blocker.js96
-rw-r--r--browser/base/content/test/popups/popup_blocker.html13
-rw-r--r--browser/base/content/test/referrer/.eslintrc.js7
-rw-r--r--browser/base/content/test/referrer/browser.ini24
-rw-r--r--browser/base/content/test/referrer/browser_referrer_middle_click.js20
-rw-r--r--browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js27
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab.js59
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab2.js31
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab3.js63
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_private.js22
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js21
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_window.js22
-rw-r--r--browser/base/content/test/referrer/browser_referrer_open_link_in_window_in_container.js32
-rw-r--r--browser/base/content/test/referrer/browser_referrer_simple_click.js20
-rw-r--r--browser/base/content/test/referrer/file_referrer_policyserver.sjs37
-rw-r--r--browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs36
-rw-r--r--browser/base/content/test/referrer/file_referrer_testserver.sjs31
-rw-r--r--browser/base/content/test/referrer/head.js265
-rw-r--r--browser/base/content/test/siteIdentity/browser.ini8
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityBlock_focus.js62
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_focus.js27
-rw-r--r--browser/base/content/test/siteIdentity/head.js6
-rw-r--r--browser/base/content/test/social/.eslintrc.js7
-rw-r--r--browser/base/content/test/social/blocklist.xml6
-rw-r--r--browser/base/content/test/social/browser.ini23
-rw-r--r--browser/base/content/test/social/browser_aboutHome_activation.js229
-rw-r--r--browser/base/content/test/social/browser_addons.js217
-rw-r--r--browser/base/content/test/social/browser_blocklist.js211
-rw-r--r--browser/base/content/test/social/browser_share.js396
-rw-r--r--browser/base/content/test/social/browser_social_activation.js270
-rw-r--r--browser/base/content/test/social/head.js273
-rw-r--r--browser/base/content/test/social/microformats.html18
-rw-r--r--browser/base/content/test/social/moz.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/social/opengraph/og_invalid_url.html11
-rw-r--r--browser/base/content/test/social/opengraph/opengraph.html13
-rw-r--r--browser/base/content/test/social/opengraph/shortlink_linkrel.html10
-rw-r--r--browser/base/content/test/social/opengraph/shorturl_link.html10
-rw-r--r--browser/base/content/test/social/opengraph/shorturl_linkrel.html25
-rw-r--r--browser/base/content/test/social/share.html9
-rw-r--r--browser/base/content/test/social/share_activate.html35
-rw-r--r--browser/base/content/test/social/social_activate.html41
-rw-r--r--browser/base/content/test/social/social_activate_basic.html41
-rw-r--r--browser/base/content/test/social/social_activate_iframe.html11
-rw-r--r--browser/base/content/test/social/social_crash_content_helper.js31
-rw-r--r--browser/base/content/test/social/social_postActivation.html12
-rw-r--r--browser/base/content/test/tabPrompts/.eslintrc.js7
-rw-r--r--browser/base/content/test/tabPrompts/browser.ini4
-rw-r--r--browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js41
-rw-r--r--browser/base/content/test/tabPrompts/browser_multiplePrompts.js72
-rw-r--r--browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js66
-rw-r--r--browser/base/content/test/tabPrompts/openPromptOffTimeout.html10
-rw-r--r--browser/base/content/test/tabcrashed/browser.ini13
-rw-r--r--browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js152
-rw-r--r--browser/base/content/test/tabcrashed/browser_clearEmail.js85
-rw-r--r--browser/base/content/test/tabcrashed/browser_showForm.js40
-rw-r--r--browser/base/content/test/tabcrashed/browser_shown.js203
-rw-r--r--browser/base/content/test/tabcrashed/browser_withoutDump.js36
-rw-r--r--browser/base/content/test/tabcrashed/head.js110
-rw-r--r--browser/base/content/test/tabs/.eslintrc.js7
-rw-r--r--browser/base/content/test/tabs/browser.ini4
-rw-r--r--browser/base/content/test/tabs/browser_tabSpinnerProbe.js93
-rw-r--r--browser/base/content/test/tabs/browser_tabSwitchPrintPreview.js29
-rw-r--r--browser/base/content/test/urlbar/.eslintrc.js7
-rw-r--r--browser/base/content/test/urlbar/authenticate.sjs220
-rw-r--r--browser/base/content/test/urlbar/browser.ini101
-rw-r--r--browser/base/content/test/urlbar/browser_URLBarSetURI.js100
-rw-r--r--browser/base/content/test/urlbar/browser_action_keyword.js119
-rw-r--r--browser/base/content/test/urlbar/browser_action_keyword_override.js40
-rw-r--r--browser/base/content/test/urlbar/browser_action_searchengine.js36
-rw-r--r--browser/base/content/test/urlbar/browser_action_searchengine_alias.js35
-rw-r--r--browser/base/content/test/urlbar/browser_autocomplete_a11y_label.js57
-rw-r--r--browser/base/content/test/urlbar/browser_autocomplete_autoselect.js92
-rw-r--r--browser/base/content/test/urlbar/browser_autocomplete_cursor.js17
-rw-r--r--browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js48
-rw-r--r--browser/base/content/test/urlbar/browser_autocomplete_enter_race.js122
-rw-r--r--browser/base/content/test/urlbar/browser_autocomplete_no_title.js15
-rw-r--r--browser/base/content/test/urlbar/browser_autocomplete_tag_star_visibility.js102
-rw-r--r--browser/base/content/test/urlbar/browser_bug1003461-switchtab-override.js61
-rw-r--r--browser/base/content/test/urlbar/browser_bug1024133-switchtab-override-keynav.js37
-rw-r--r--browser/base/content/test/urlbar/browser_bug1025195_switchToTabHavingURI_aOpenParams.js124
-rw-r--r--browser/base/content/test/urlbar/browser_bug1070778.js55
-rw-r--r--browser/base/content/test/urlbar/browser_bug1104165-switchtab-decodeuri.js29
-rw-r--r--browser/base/content/test/urlbar/browser_bug1225194-remotetab.js16
-rw-r--r--browser/base/content/test/urlbar/browser_bug304198.js109
-rw-r--r--browser/base/content/test/urlbar/browser_bug556061.js98
-rw-r--r--browser/base/content/test/urlbar/browser_bug562649.js24
-rw-r--r--browser/base/content/test/urlbar/browser_bug623155.js137
-rw-r--r--browser/base/content/test/urlbar/browser_bug783614.js13
-rw-r--r--browser/base/content/test/urlbar/browser_canonizeURL.js42
-rw-r--r--browser/base/content/test/urlbar/browser_dragdropURL.js15
-rw-r--r--browser/base/content/test/urlbar/browser_locationBarCommand.js218
-rw-r--r--browser/base/content/test/urlbar/browser_locationBarExternalLoad.js65
-rw-r--r--browser/base/content/test/urlbar/browser_moz_action_link.js31
-rw-r--r--browser/base/content/test/urlbar/browser_removeUnsafeProtocolsFromURLBarPaste.js49
-rw-r--r--browser/base/content/test/urlbar/browser_search_favicon.js52
-rw-r--r--browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar.js216
-rw-r--r--browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js84
-rw-r--r--browser/base/content/test/urlbar/browser_urlHighlight.js134
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarAboutHomeLoading.js104
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js49
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarCopying.js232
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarDecode.js97
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarDelete.js39
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarEnter.js45
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarEnterAfterMouseOver.js69
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarFocusedCmdK.js17
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarHashChangeProxyState.js111
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarKeepStateAcrossTabSwitches.js49
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarOneOffs.js232
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarPrivateBrowsingWindowChange.js41
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarRaceWithTabs.js57
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarRevert.js37
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js198
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js66
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarSearchSuggestionsNotification.js254
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarSearchTelemetry.js216
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarStop.js30
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarTrimURLs.js98
-rw-r--r--browser/base/content/test/urlbar/browser_urlbarUpdateForDomainCompletion.js17
-rw-r--r--browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js146
-rw-r--r--browser/base/content/test/urlbar/browser_urlbar_blanking.js35
-rw-r--r--browser/base/content/test/urlbar/browser_urlbar_locationchange_urlbar_edit_dos.js41
-rw-r--r--browser/base/content/test/urlbar/browser_urlbar_remoteness_switch.js39
-rw-r--r--browser/base/content/test/urlbar/browser_urlbar_searchsettings.js30
-rw-r--r--browser/base/content/test/urlbar/browser_urlbar_stop_pending.js138
-rw-r--r--browser/base/content/test/urlbar/browser_wyciwyg_urlbarCopying.js31
-rw-r--r--browser/base/content/test/urlbar/dummy_page.html9
-rw-r--r--browser/base/content/test/urlbar/file_blank_but_not_blank.html2
-rw-r--r--browser/base/content/test/urlbar/file_urlbar_edit_dos.html23
-rw-r--r--browser/base/content/test/urlbar/head.js205
-rw-r--r--browser/base/content/test/urlbar/moz.pngbin0 -> 580 bytes
-rw-r--r--browser/base/content/test/urlbar/print_postdata.sjs22
-rw-r--r--browser/base/content/test/urlbar/redirect_bug623155.sjs16
-rw-r--r--browser/base/content/test/urlbar/searchSuggestionEngine.sjs9
-rw-r--r--browser/base/content/test/urlbar/searchSuggestionEngine.xml9
-rw-r--r--browser/base/content/test/urlbar/slow-page.sjs22
-rw-r--r--browser/base/content/test/urlbar/test_wyciwyg_copying.html13
-rw-r--r--browser/base/content/test/webrtc/.eslintrc.js7
-rw-r--r--browser/base/content/test/webrtc/browser.ini11
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media.js554
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js109
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js266
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js109
-rw-r--r--browser/base/content/test/webrtc/get_user_media.html55
-rw-r--r--browser/base/content/test/webrtc/get_user_media_content_script.js85
-rw-r--r--browser/base/content/test/webrtc/head.js453
-rw-r--r--browser/base/content/urlbarBindings.xml2740
-rw-r--r--browser/base/content/usercontext.svg23
-rw-r--r--browser/base/content/utilityOverlay.js924
-rw-r--r--browser/base/content/viewSourceOverlay.xul26
-rw-r--r--browser/base/content/web-panels.js104
-rw-r--r--browser/base/content/web-panels.xul71
-rw-r--r--browser/base/content/webrtcIndicator.js194
-rw-r--r--browser/base/content/webrtcIndicator.xul35
-rw-r--r--browser/base/content/win6BrowserOverlay.xul12
-rw-r--r--browser/base/jar.mn197
-rw-r--r--browser/base/moz.build50
892 files changed, 117395 insertions, 0 deletions
diff --git a/browser/base/.eslintrc.js b/browser/base/.eslintrc.js
new file mode 100644
index 000000000..e6cf2032e
--- /dev/null
+++ b/browser/base/.eslintrc.js
@@ -0,0 +1,11 @@
+"use strict";
+
+module.exports = {
+ "rules": {
+ "no-unused-vars": ["error", {
+ "vars": "local",
+ "varsIgnorePattern": "^Cc|Ci|Cu|Cr|EXPORTED_SYMBOLS",
+ "args": "none",
+ }]
+ }
+};
diff --git a/browser/base/content/aboutDialog-appUpdater.js b/browser/base/content/aboutDialog-appUpdater.js
new file mode 100644
index 000000000..4b4fc6618
--- /dev/null
+++ b/browser/base/content/aboutDialog-appUpdater.js
@@ -0,0 +1,428 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Note: this file is included in aboutDialog.xul if MOZ_UPDATER is defined.
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
+const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
+
+var gAppUpdater;
+
+function onUnload(aEvent) {
+ if (gAppUpdater.isChecking)
+ gAppUpdater.checker.stopChecking(Components.interfaces.nsIUpdateChecker.CURRENT_CHECK);
+ // Safe to call even when there isn't a download in progress.
+ gAppUpdater.removeDownloadListener();
+ gAppUpdater = null;
+}
+
+
+function appUpdater()
+{
+ XPCOMUtils.defineLazyServiceGetter(this, "aus",
+ "@mozilla.org/updates/update-service;1",
+ "nsIApplicationUpdateService");
+ XPCOMUtils.defineLazyServiceGetter(this, "checker",
+ "@mozilla.org/updates/update-checker;1",
+ "nsIUpdateChecker");
+ XPCOMUtils.defineLazyServiceGetter(this, "um",
+ "@mozilla.org/updates/update-manager;1",
+ "nsIUpdateManager");
+
+ this.updateDeck = document.getElementById("updateDeck");
+
+ // Hide the update deck when the update window is already open and it's not
+ // already applied, to avoid syncing issues between them. Applied updates
+ // don't have any information to sync between the windows as they both just
+ // show the "Restart to continue"-type button.
+ if (Services.wm.getMostRecentWindow("Update:Wizard") &&
+ !this.isApplied) {
+ this.updateDeck.hidden = true;
+ return;
+ }
+
+ this.bundle = Services.strings.
+ createBundle("chrome://browser/locale/browser.properties");
+
+ let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual");
+ let manualLink = document.getElementById("manualLink");
+ manualLink.value = manualURL;
+ manualLink.href = manualURL;
+ document.getElementById("failedLink").href = manualURL;
+
+ if (this.updateDisabledAndLocked) {
+ this.selectPanel("adminDisabled");
+ return;
+ }
+
+ if (this.isPending || this.isApplied) {
+ this.selectPanel("apply");
+ return;
+ }
+
+ if (this.aus.isOtherInstanceHandlingUpdates) {
+ this.selectPanel("otherInstanceHandlingUpdates");
+ return;
+ }
+
+ if (this.isDownloading) {
+ this.startDownload();
+ // selectPanel("downloading") is called from setupDownloadingUI().
+ return;
+ }
+
+ // Honor the "Never check for updates" option by not only disabling background
+ // update checks, but also in the About dialog, by presenting a
+ // "Check for updates" button.
+ // If updates are found, the user is then asked if he wants to "Update to <version>".
+ if (!this.updateEnabled ||
+ Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
+ this.selectPanel("checkForUpdates");
+ return;
+ }
+
+ // That leaves the options
+ // "Check for updates, but let me choose whether to install them", and
+ // "Automatically install updates".
+ // In both cases, we check for updates without asking.
+ // In the "let me choose" case, we ask before downloading though, in onCheckComplete.
+ this.checkForUpdates();
+}
+
+appUpdater.prototype =
+{
+ // true when there is an update check in progress.
+ isChecking: false,
+
+ // true when there is an update already staged / ready to be applied.
+ get isPending() {
+ if (this.update) {
+ return this.update.state == "pending" ||
+ this.update.state == "pending-service" ||
+ this.update.state == "pending-elevate";
+ }
+ return this.um.activeUpdate &&
+ (this.um.activeUpdate.state == "pending" ||
+ this.um.activeUpdate.state == "pending-service" ||
+ this.um.activeUpdate.state == "pending-elevate");
+ },
+
+ // true when there is an update already installed in the background.
+ get isApplied() {
+ if (this.update)
+ return this.update.state == "applied" ||
+ this.update.state == "applied-service";
+ return this.um.activeUpdate &&
+ (this.um.activeUpdate.state == "applied" ||
+ this.um.activeUpdate.state == "applied-service");
+ },
+
+ // true when there is an update download in progress.
+ get isDownloading() {
+ if (this.update)
+ return this.update.state == "downloading";
+ return this.um.activeUpdate &&
+ this.um.activeUpdate.state == "downloading";
+ },
+
+ // true when updating is disabled by an administrator.
+ get updateDisabledAndLocked() {
+ return !this.updateEnabled &&
+ Services.prefs.prefIsLocked("app.update.enabled");
+ },
+
+ // true when updating is enabled.
+ get updateEnabled() {
+ try {
+ return Services.prefs.getBoolPref("app.update.enabled");
+ }
+ catch (e) { }
+ return true; // Firefox default is true
+ },
+
+ // true when updating in background is enabled.
+ get backgroundUpdateEnabled() {
+ return this.updateEnabled &&
+ gAppUpdater.aus.canStageUpdates;
+ },
+
+ // true when updating is automatic.
+ get updateAuto() {
+ try {
+ return Services.prefs.getBoolPref("app.update.auto");
+ }
+ catch (e) { }
+ return true; // Firefox default is true
+ },
+
+ /**
+ * Sets the panel of the updateDeck.
+ *
+ * @param aChildID
+ * The id of the deck's child to select, e.g. "apply".
+ */
+ selectPanel: function(aChildID) {
+ let panel = document.getElementById(aChildID);
+
+ let button = panel.querySelector("button");
+ if (button) {
+ if (aChildID == "downloadAndInstall") {
+ let updateVersion = gAppUpdater.update.displayVersion;
+ button.label = this.bundle.formatStringFromName("update.downloadAndInstallButton.label", [updateVersion], 1);
+ button.accessKey = this.bundle.GetStringFromName("update.downloadAndInstallButton.accesskey");
+ }
+ this.updateDeck.selectedPanel = panel;
+ if (!document.commandDispatcher.focusedElement || // don't steal the focus
+ document.commandDispatcher.focusedElement.localName == "button") // except from the other buttons
+ button.focus();
+
+ } else {
+ this.updateDeck.selectedPanel = panel;
+ }
+ },
+
+ /**
+ * Check for updates
+ */
+ checkForUpdates: function() {
+ // Clear prefs that could prevent a user from discovering available updates.
+ 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);
+ }
+ this.selectPanel("checkingForUpdates");
+ this.isChecking = true;
+ this.checker.checkForUpdates(this.updateCheckListener, true);
+ // after checking, onCheckComplete() is called
+ },
+
+ /**
+ * Handles oncommand for the "Restart to Update" button
+ * which is presented after the download has been downloaded.
+ */
+ buttonRestartAfterDownload: function() {
+ if (!this.isPending && !this.isApplied) {
+ return;
+ }
+
+ gAppUpdater.selectPanel("restarting");
+
+ // Notify all windows that an application quit has been requested.
+ let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
+ createInstance(Components.interfaces.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+ // Something aborted the quit process.
+ if (cancelQuit.data) {
+ gAppUpdater.selectPanel("apply");
+ return;
+ }
+
+ let appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"].
+ getService(Components.interfaces.nsIAppStartup);
+
+ // If already in safe mode restart in safe mode (bug 327119)
+ if (Services.appinfo.inSafeMode) {
+ appStartup.restartInSafeMode(Components.interfaces.nsIAppStartup.eAttemptQuit);
+ return;
+ }
+
+ appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit |
+ Components.interfaces.nsIAppStartup.eRestart);
+ },
+
+ /**
+ * Implements nsIUpdateCheckListener. The methods implemented by
+ * nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload
+ * to make it clear which are used by each interface.
+ */
+ updateCheckListener: {
+ /**
+ * See nsIUpdateService.idl
+ */
+ onCheckComplete: function(aRequest, aUpdates, aUpdateCount) {
+ gAppUpdater.isChecking = false;
+ gAppUpdater.update = gAppUpdater.aus.
+ selectUpdate(aUpdates, aUpdates.length);
+ if (!gAppUpdater.update) {
+ gAppUpdater.selectPanel("noUpdatesFound");
+ return;
+ }
+
+ if (gAppUpdater.update.unsupported) {
+ if (gAppUpdater.update.detailsURL) {
+ let unsupportedLink = document.getElementById("unsupportedLink");
+ unsupportedLink.href = gAppUpdater.update.detailsURL;
+ }
+ gAppUpdater.selectPanel("unsupportedSystem");
+ return;
+ }
+
+ if (!gAppUpdater.aus.canApplyUpdates) {
+ gAppUpdater.selectPanel("manualUpdate");
+ return;
+ }
+
+ if (gAppUpdater.updateAuto) // automatically download and install
+ gAppUpdater.startDownload();
+ else // ask
+ gAppUpdater.selectPanel("downloadAndInstall");
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ onError: function(aRequest, aUpdate) {
+ // Errors in the update check are treated as no updates found. If the
+ // update check fails repeatedly without a success the user will be
+ // notified with the normal app update user interface so this is safe.
+ gAppUpdater.isChecking = false;
+ gAppUpdater.selectPanel("noUpdatesFound");
+ },
+
+ /**
+ * See nsISupports.idl
+ */
+ QueryInterface: function(aIID) {
+ if (!aIID.equals(Components.interfaces.nsIUpdateCheckListener) &&
+ !aIID.equals(Components.interfaces.nsISupports))
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+ },
+
+ /**
+ * Starts the download of an update mar.
+ */
+ startDownload: function() {
+ if (!this.update)
+ this.update = this.um.activeUpdate;
+ this.update.QueryInterface(Components.interfaces.nsIWritablePropertyBag);
+ this.update.setProperty("foregroundDownload", "true");
+
+ this.aus.pauseDownload();
+ let state = this.aus.downloadUpdate(this.update, false);
+ if (state == "failed") {
+ this.selectPanel("downloadFailed");
+ return;
+ }
+
+ this.setupDownloadingUI();
+ },
+
+ /**
+ * Switches to the UI responsible for tracking the download.
+ */
+ setupDownloadingUI: function() {
+ this.downloadStatus = document.getElementById("downloadStatus");
+ this.downloadStatus.value =
+ DownloadUtils.getTransferTotal(0, this.update.selectedPatch.size);
+ this.selectPanel("downloading");
+ this.aus.addDownloadListener(this);
+ },
+
+ removeDownloadListener: function() {
+ if (this.aus) {
+ this.aus.removeDownloadListener(this);
+ }
+ },
+
+ /**
+ * See nsIRequestObserver.idl
+ */
+ onStartRequest: function(aRequest, aContext) {
+ },
+
+ /**
+ * See nsIRequestObserver.idl
+ */
+ onStopRequest: function(aRequest, aContext, aStatusCode) {
+ switch (aStatusCode) {
+ case Components.results.NS_ERROR_UNEXPECTED:
+ if (this.update.selectedPatch.state == "download-failed" &&
+ (this.update.isCompleteUpdate || this.update.patchCount != 2)) {
+ // Verification error of complete patch, informational text is held in
+ // the update object.
+ this.removeDownloadListener();
+ this.selectPanel("downloadFailed");
+ break;
+ }
+ // Verification failed for a partial patch, complete patch is now
+ // downloading so return early and do NOT remove the download listener!
+ break;
+ case Components.results.NS_BINDING_ABORTED:
+ // Do not remove UI listener since the user may resume downloading again.
+ break;
+ case Components.results.NS_OK:
+ this.removeDownloadListener();
+ if (this.backgroundUpdateEnabled) {
+ this.selectPanel("applying");
+ let self = this;
+ Services.obs.addObserver(function (aSubject, aTopic, aData) {
+ // Update the UI when the background updater is finished
+ let status = aData;
+ if (status == "applied" || status == "applied-service" ||
+ status == "pending" || status == "pending-service" ||
+ status == "pending-elevate") {
+ // If the update is successfully applied, or if the updater has
+ // fallen back to non-staged updates, show the "Restart to Update"
+ // button.
+ self.selectPanel("apply");
+ } else if (status == "failed") {
+ // Background update has failed, let's show the UI responsible for
+ // prompting the user to update manually.
+ self.selectPanel("downloadFailed");
+ } else if (status == "downloading") {
+ // We've fallen back to downloading the full update because the
+ // partial update failed to get staged in the background.
+ // Therefore we need to keep our observer.
+ self.setupDownloadingUI();
+ return;
+ }
+ Services.obs.removeObserver(arguments.callee, "update-staged");
+ }, "update-staged", false);
+ } else {
+ this.selectPanel("apply");
+ }
+ break;
+ default:
+ this.removeDownloadListener();
+ this.selectPanel("downloadFailed");
+ break;
+ }
+ },
+
+ /**
+ * See nsIProgressEventSink.idl
+ */
+ onStatus: function(aRequest, aContext, aStatus, aStatusArg) {
+ },
+
+ /**
+ * See nsIProgressEventSink.idl
+ */
+ onProgress: function(aRequest, aContext, aProgress, aProgressMax) {
+ this.downloadStatus.value =
+ DownloadUtils.getTransferTotal(aProgress, aProgressMax);
+ },
+
+ /**
+ * See nsISupports.idl
+ */
+ QueryInterface: function(aIID) {
+ if (!aIID.equals(Components.interfaces.nsIProgressEventSink) &&
+ !aIID.equals(Components.interfaces.nsIRequestObserver) &&
+ !aIID.equals(Components.interfaces.nsISupports))
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+};
diff --git a/browser/base/content/aboutDialog.css b/browser/base/content/aboutDialog.css
new file mode 100644
index 000000000..65830c8bb
--- /dev/null
+++ b/browser/base/content/aboutDialog.css
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#aboutDialog {
+ width: 620px;
+ /* Set an explicit line-height to avoid discrepancies in 'auto' spacing
+ across screens with different device DPI, which may cause font metrics
+ to round differently. */
+ line-height: 1.5;
+}
+
+#rightBox {
+ background-image: url("chrome://branding/content/about-wordmark.png");
+ background-repeat: no-repeat;
+ /* padding-top creates room for the wordmark */
+ padding-top: 38px;
+ margin-top:20px;
+}
+
+#rightBox:-moz-locale-dir(rtl) {
+ background-position: 100% 0;
+}
+
+#bottomBox {
+ padding: 15px 10px 0;
+}
+
+#version {
+ font-weight: bold;
+ margin-top: 10px;
+ margin-left: 0;
+ -moz-user-select: text;
+ -moz-user-focus: normal;
+ cursor: text;
+}
+
+#version:-moz-locale-dir(rtl) {
+ direction: ltr;
+ text-align: right;
+ margin-left: 5px;
+ margin-right: 0;
+}
+
+#releasenotes {
+ margin-top: 10px;
+}
+
+#distribution,
+#distributionId {
+ display: none;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.text-blurb {
+ margin-bottom: 10px;
+ margin-inline-start: 0;
+ padding-inline-start: 0;
+}
+
+#updateButton,
+#updateDeck > hbox > label {
+ margin-inline-start: 0;
+ padding-inline-start: 0;
+}
+
+.update-throbber {
+ width: 16px;
+ min-height: 16px;
+ margin-inline-end: 3px;
+ list-style-image: url("chrome://global/skin/icons/loading.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+ .update-throbber {
+ list-style-image: url("chrome://global/skin/icons/loading@2x.png");
+ }
+}
+
+description > .text-link,
+description > .text-link:focus {
+ margin: 0px;
+ padding: 0px;
+}
+
+.bottom-link,
+.bottom-link:focus {
+ text-align: center;
+ margin: 0 40px;
+}
+
+#currentChannel {
+ margin: 0;
+ padding: 0;
+ font-weight: bold;
+}
diff --git a/browser/base/content/aboutDialog.js b/browser/base/content/aboutDialog.js
new file mode 100644
index 000000000..569a65adb
--- /dev/null
+++ b/browser/base/content/aboutDialog.js
@@ -0,0 +1,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";
+
+// Services = object with smart getters for common XPCOM services
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+function init(aEvent)
+{
+ if (aEvent.target != document)
+ return;
+
+ try {
+ var distroId = Services.prefs.getCharPref("distribution.id");
+ if (distroId) {
+ var distroVersion = Services.prefs.getCharPref("distribution.version");
+
+ var distroIdField = document.getElementById("distributionId");
+ distroIdField.value = distroId + " - " + distroVersion;
+ distroIdField.style.display = "block";
+
+ try {
+ // This is in its own try catch due to bug 895473 and bug 900925.
+ var distroAbout = Services.prefs.getComplexValue("distribution.about",
+ Components.interfaces.nsISupportsString);
+ var distroField = document.getElementById("distribution");
+ distroField.value = distroAbout;
+ distroField.style.display = "block";
+ }
+ catch (ex) {
+ // Pref is unset
+ Components.utils.reportError(ex);
+ }
+ }
+ }
+ catch (e) {
+ // Pref is unset
+ }
+
+ // Include the build ID and display warning if this is an "a#" (nightly or aurora) build
+ let versionField = document.getElementById("version");
+ let version = Services.appinfo.version;
+ if (/a\d+$/.test(version)) {
+ let buildID = Services.appinfo.appBuildID;
+ let year = buildID.slice(0, 4);
+ let month = buildID.slice(4, 6);
+ let day = buildID.slice(6, 8);
+ versionField.textContent += ` (${year}-${month}-${day})`;
+
+ document.getElementById("experimental").hidden = false;
+ document.getElementById("communityDesc").hidden = true;
+ }
+
+ // Append "(32-bit)" or "(64-bit)" build architecture to the version number:
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let archResource = Services.appinfo.is64Bit
+ ? "aboutDialog.architecture.sixtyFourBit"
+ : "aboutDialog.architecture.thirtyTwoBit";
+ let arch = bundle.GetStringFromName(archResource);
+ versionField.textContent += ` (${arch})`;
+
+ if (AppConstants.MOZ_UPDATER) {
+ gAppUpdater = new appUpdater();
+
+ let channelLabel = document.getElementById("currentChannel");
+ let currentChannelText = document.getElementById("currentChannelText");
+ channelLabel.value = UpdateUtils.UpdateChannel;
+ if (/^release($|\-)/.test(channelLabel.value))
+ currentChannelText.hidden = true;
+ }
+
+ if (AppConstants.platform == "macosx") {
+ // it may not be sized at this point, and we need its width to calculate its position
+ window.sizeToContent();
+ window.moveTo((screen.availWidth / 2) - (window.outerWidth / 2), screen.availHeight / 5);
+ }
+}
diff --git a/browser/base/content/aboutDialog.xul b/browser/base/content/aboutDialog.xul
new file mode 100644
index 000000000..cbb07a5e1
--- /dev/null
+++ b/browser/base/content/aboutDialog.xul
@@ -0,0 +1,157 @@
+<?xml version="1.0"?> <!-- -*- Mode: HTML -*- -->
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/aboutDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://branding/content/aboutDialog.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % aboutDialogDTD SYSTEM "chrome://browser/locale/aboutDialog.dtd" >
+%aboutDialogDTD;
+]>
+
+#ifdef XP_MACOSX
+<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?>
+#endif
+
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="aboutDialog"
+ windowtype="Browser:About"
+ onload="init(event);"
+#ifdef MOZ_UPDATER
+ onunload="onUnload(event);"
+#endif
+#ifdef XP_MACOSX
+ inwindowmenu="false"
+#else
+ title="&aboutDialog.title;"
+#endif
+ role="dialog"
+ aria-describedby="version distribution distributionId communityDesc contributeDesc trademark"
+ >
+
+ <script type="application/javascript" src="chrome://browser/content/aboutDialog.js"/>
+#ifdef MOZ_UPDATER
+ <script type="application/javascript" src="chrome://browser/content/aboutDialog-appUpdater.js"/>
+#endif
+ <vbox id="aboutDialogContainer">
+ <hbox id="clientBox">
+ <vbox id="leftBox" flex="1"/>
+ <vbox id="rightBox" flex="1">
+ <hbox align="baseline">
+#expand <label id="version">__MOZ_APP_VERSION_DISPLAY__</label>
+#ifndef NIGHTLY_BUILD
+#expand <label id="releasenotes" class="text-link" href="https://www.mozilla.org/firefox/__MOZ_APP_VERSION__/releasenotes/">&releaseNotes.link;</label>
+#endif
+ </hbox>
+
+ <label id="distribution" class="text-blurb"/>
+ <label id="distributionId" class="text-blurb"/>
+
+ <vbox id="detailsBox">
+ <vbox id="updateBox">
+#ifdef MOZ_UPDATER
+ <deck id="updateDeck" orient="vertical">
+ <hbox id="checkForUpdates" align="center">
+ <button id="checkForUpdatesButton" align="start"
+ label="&update.checkForUpdatesButton.label;"
+ accesskey="&update.checkForUpdatesButton.accesskey;"
+ oncommand="gAppUpdater.checkForUpdates();"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox id="downloadAndInstall" align="center">
+ <button id="downloadAndInstallButton" align="start"
+ oncommand="gAppUpdater.startDownload();"/>
+ <!-- label and accesskey will be filled by JS -->
+ <spacer flex="1"/>
+ </hbox>
+ <hbox id="apply" align="center">
+ <button id="updateButton" align="start"
+ label="&update.updateButton.label2;"
+ accesskey="&update.updateButton.accesskey;"
+ oncommand="gAppUpdater.buttonRestartAfterDownload();"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox id="checkingForUpdates" align="center">
+ <image class="update-throbber"/><label>&update.checkingForUpdates;</label>
+ </hbox>
+ <hbox id="downloading" align="center">
+ <image class="update-throbber"/><label>&update.downloading.start;</label><label id="downloadStatus"/><label>&update.downloading.end;</label>
+ </hbox>
+ <hbox id="applying" align="center">
+ <image class="update-throbber"/><label>&update.applying;</label>
+ </hbox>
+ <hbox id="downloadFailed" align="center">
+ <label>&update.failed.start;</label><label id="failedLink" class="text-link">&update.failed.linkText;</label><label>&update.failed.end;</label>
+ </hbox>
+ <hbox id="adminDisabled" align="center">
+ <label>&update.adminDisabled;</label>
+ </hbox>
+ <hbox id="noUpdatesFound" align="center">
+ <label>&update.noUpdatesFound;</label>
+ </hbox>
+ <hbox id="otherInstanceHandlingUpdates" align="center">
+ <label>&update.otherInstanceHandlingUpdates;</label>
+ </hbox>
+ <hbox id="manualUpdate" align="center">
+ <label>&update.manual.start;</label><label id="manualLink" class="text-link"/><label>&update.manual.end;</label>
+ </hbox>
+ <hbox id="unsupportedSystem" align="center">
+ <label>&update.unsupported.start;</label><label id="unsupportedLink" class="text-link">&update.unsupported.linkText;</label><label>&update.unsupported.end;</label>
+ </hbox>
+ <hbox id="restarting" align="center">
+ <label>&update.restarting;</label>
+ </hbox>
+ </deck>
+#endif
+ </vbox>
+
+#ifdef MOZ_UPDATER
+ <description class="text-blurb" id="currentChannelText">
+ &channel.description.start;<label id="currentChannel"/>&channel.description.end;
+ </description>
+#endif
+ <vbox id="experimental" hidden="true">
+ <description class="text-blurb" id="warningDesc">
+ &warningDesc.version;
+#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
+ &warningDesc.telemetryDesc;
+#endif
+ </description>
+ <description class="text-blurb" id="communityExperimentalDesc">
+ &community.exp.start;<label class="text-link" href="http://www.mozilla.org/">&community.exp.mozillaLink;</label>&community.exp.middle;<label class="text-link" useoriginprincipal="true" href="about:credits">&community.exp.creditsLink;</label>&community.exp.end;
+ </description>
+ </vbox>
+ <description class="text-blurb" id="communityDesc">
+ &community.start2;<label class="text-link" href="http://www.mozilla.org/">&community.mozillaLink;</label>&community.middle2;<label class="text-link" useoriginprincipal="true" href="about:credits">&community.creditsLink;</label>&community.end3;
+ </description>
+ <description class="text-blurb" id="contributeDesc">
+ &helpus.start;<label class="text-link" href="https://sendto.mozilla.org/page/contribute/Give-Now?source=mozillaorg_default_footer&#38;ref=firefox_about&#38;utm_campaign=firefox_about&#38;tm_source=firefox&#38;tm_medium=referral&#38;utm_content=20140929_FireFoxAbout">&helpus.donateLink;</label>&helpus.middle;<label class="text-link" href="http://www.mozilla.org/contribute/">&helpus.getInvolvedLink;</label>&helpus.end;
+ </description>
+ </vbox>
+ </vbox>
+ </hbox>
+ <vbox id="bottomBox">
+ <hbox pack="center">
+ <label class="text-link bottom-link" useoriginprincipal="true" href="about:license">&bottomLinks.license;</label>
+ <label class="text-link bottom-link" useoriginprincipal="true" href="about:rights">&bottomLinks.rights;</label>
+ <label class="text-link bottom-link" href="https://www.mozilla.org/privacy/">&bottomLinks.privacy;</label>
+ </hbox>
+ <description id="trademark">&trademarkInfo.part1;</description>
+ </vbox>
+ </vbox>
+
+ <keyset>
+ <key keycode="VK_ESCAPE" oncommand="window.close();"/>
+ </keyset>
+
+#ifdef XP_MACOSX
+#include browserMountPoints.inc
+#endif
+</window>
diff --git a/browser/base/content/aboutNetError.xhtml b/browser/base/content/aboutNetError.xhtml
new file mode 100644
index 000000000..f2de106c2
--- /dev/null
+++ b/browser/base/content/aboutNetError.xhtml
@@ -0,0 +1,699 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD
+ SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&loadError.label;</title>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
+ <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link rel="icon" type="image/png" id="favicon" href="chrome://global/skin/icons/warning-16.png"/>
+
+ <script type="application/javascript"><![CDATA[
+ // The following parameters are parsed from the error URL:
+ // e - the error code
+ // s - custom CSS class to allow alternate styling/favicons
+ // d - error description
+ // captive - "true" to indicate we're behind a captive portal.
+ // Any other value is ignored.
+
+ // Note that this file uses document.documentURI to get
+ // the URL (with the format from above). This is because
+ // document.location.href gets the current URI off the docshell,
+ // which is the URL displayed in the location bar, i.e.
+ // the URI that the user attempted to load.
+
+ let searchParams = new URLSearchParams(document.documentURI.split("?")[1]);
+
+ // Set to true on init if the error code is nssBadCert.
+ let gIsCertError;
+
+ function getErrorCode()
+ {
+ return searchParams.get("e");
+ }
+
+ function getCSSClass()
+ {
+ return searchParams.get("s");
+ }
+
+ function getDescription()
+ {
+ return searchParams.get("d");
+ }
+
+ function isCaptive() {
+ return searchParams.get("captive") == "true";
+ }
+
+ function retryThis(buttonEl)
+ {
+ // Note: The application may wish to handle switching off "offline mode"
+ // before this event handler runs, but using a capturing event handler.
+
+ // Session history has the URL of the page that failed
+ // to load, not the one of the error page. So, just call
+ // reload(), which will also repost POST data correctly.
+ try {
+ location.reload();
+ } catch (e) {
+ // We probably tried to reload a URI that caused an exception to
+ // occur; e.g. a nonexistent file.
+ }
+
+ buttonEl.disabled = true;
+ }
+
+ function doOverride(buttonEl) {
+ var event = new CustomEvent("AboutNetErrorOverride", {bubbles:true});
+ document.dispatchEvent(event);
+ retryThis(buttonEl);
+ }
+
+ function toggleDisplay(node) {
+ const toggle = {
+ "": "block",
+ "none": "block",
+ "block": "none"
+ };
+ return (node.style.display = toggle[node.style.display]);
+ }
+
+ function showCertificateErrorReporting() {
+ // Display error reporting UI
+ document.getElementById("certificateErrorReporting").style.display = "block";
+ }
+
+ function showPrefChangeContainer() {
+ const panel = document.getElementById("prefChangeContainer");
+ panel.style.display = "block";
+ document.getElementById("netErrorButtonContainer").style.display = "none";
+ document.getElementById("prefResetButton").addEventListener("click", function resetPreferences(e) {
+ const event = new CustomEvent("AboutNetErrorResetPreferences", {bubbles:true});
+ document.dispatchEvent(event);
+ });
+ addAutofocus("prefResetButton", "beforeend");
+ }
+
+ function setupAdvancedButton(allowOverride) {
+ // Get the hostname and add it to the panel
+ var panelId = gIsCertError ? "badCertAdvancedPanel" : "weakCryptoAdvancedPanel";
+ var panel = document.getElementById(panelId);
+ for (var span of panel.querySelectorAll("span.hostname")) {
+ span.textContent = document.location.hostname;
+ }
+ if (!gIsCertError) {
+ panel.replaceChild(document.getElementById("errorLongDesc"),
+ document.getElementById("advancedLongDesc"));
+ }
+
+ // Register click handler for the weakCryptoAdvancedPanel
+ document.getElementById("advancedButton")
+ .addEventListener("click", function togglePanelVisibility() {
+ toggleDisplay(panel);
+ if (gIsCertError) {
+ // Toggling the advanced panel must ensure that the debugging
+ // information panel is hidden as well, since it's opened by the
+ // error code link in the advanced panel.
+ var div = document.getElementById("certificateErrorDebugInformation");
+ div.style.display = "none";
+ }
+
+ if (panel.style.display == "block") {
+ // send event to trigger telemetry ping
+ var event = new CustomEvent("AboutNetErrorUIExpanded", {bubbles:true});
+ document.dispatchEvent(event);
+ }
+ });
+
+ if (allowOverride) {
+ document.getElementById("overrideWeakCryptoPanel").style.display = "flex";
+ var overrideLink = document.getElementById("overrideWeakCrypto");
+ overrideLink.addEventListener("click", () => doOverride(overrideLink), false);
+ }
+ if (!gIsCertError) {
+ return;
+ }
+
+ if (getCSSClass() == "expertBadCert") {
+ toggleDisplay(document.getElementById("badCertAdvancedPanel"));
+ // Toggling the advanced panel must ensure that the debugging
+ // information panel is hidden as well, since it's opened by the
+ // error code link in the advanced panel.
+ var div = document.getElementById("certificateErrorDebugInformation");
+ div.style.display = "none";
+ }
+
+ disallowCertOverridesIfNeeded();
+
+ document.getElementById("badCertTechnicalInfo").textContent = getDescription();
+ }
+
+ function disallowCertOverridesIfNeeded() {
+ var cssClass = getCSSClass();
+ // Disallow overrides if this is a Strict-Transport-Security
+ // host and the cert is bad (STS Spec section 7.3) or if the
+ // certerror is in a frame (bug 633691).
+ if (cssClass == "badStsCert" || window != top) {
+ document.getElementById("exceptionDialogButton").setAttribute("hidden", "true");
+ }
+ if (cssClass == "badStsCert") {
+ document.getElementById("badStsCertExplanation").removeAttribute("hidden");
+ }
+ }
+
+ function initPage()
+ {
+ var err = getErrorCode();
+ gIsCertError = (err == "nssBadCert");
+ // Only worry about captive portals if this is a cert error.
+ let showCaptivePortalUI = isCaptive() && gIsCertError;
+ if (showCaptivePortalUI) {
+ err = "captivePortal";
+ }
+
+ // if it's an unknown error or there's no title or description
+ // defined, get the generic message
+ var errTitle = document.getElementById("et_" + err);
+ var errDesc = document.getElementById("ed_" + err);
+ if (!errTitle || !errDesc)
+ {
+ errTitle = document.getElementById("et_generic");
+ errDesc = document.getElementById("ed_generic");
+ }
+
+ document.querySelector(".title-text").innerHTML = errTitle.innerHTML;
+
+ var sd = document.getElementById("errorShortDescText");
+ if (sd) {
+ if (gIsCertError) {
+ sd.innerHTML = errDesc.innerHTML;
+ }
+ else {
+ sd.textContent = getDescription();
+ }
+ }
+ if (showCaptivePortalUI) {
+ initPageCaptivePortal();
+ return;
+ }
+ if (gIsCertError) {
+ initPageCertError();
+ return;
+ }
+ addAutofocus("errorTryAgain");
+
+ document.body.className = "neterror";
+
+ var ld = document.getElementById("errorLongDesc");
+ if (ld)
+ {
+ ld.innerHTML = errDesc.innerHTML;
+ }
+
+ if (err == "sslv3Used") {
+ document.getElementById("learnMoreContainer").style.display = "block";
+ var learnMoreLink = document.getElementById("learnMoreLink");
+ learnMoreLink.href = "https://support.mozilla.org/kb/how-resolve-sslv3-error-messages-firefox";
+ document.body.className = "certerror";
+ }
+
+ if (err == "weakCryptoUsed") {
+ document.body.className = "certerror";
+ }
+
+ // remove undisplayed errors to avoid bug 39098
+ var errContainer = document.getElementById("errorContainer");
+ errContainer.parentNode.removeChild(errContainer);
+
+ var className = getCSSClass();
+ if (className && className != "expertBadCert") {
+ // Associate a CSS class with the root of the page, if one was passed in,
+ // to allow custom styling.
+ // Not "expertBadCert" though, don't want to deal with the favicon
+ document.documentElement.className = className;
+
+ // Also, if they specified a CSS class, they must supply their own
+ // favicon. In order to trigger the browser to repaint though, we
+ // need to remove/add the link element.
+ var favicon = document.getElementById("favicon");
+ var faviconParent = favicon.parentNode;
+ faviconParent.removeChild(favicon);
+ favicon.setAttribute("href", "chrome://global/skin/icons/" + className + "_favicon.png");
+ faviconParent.appendChild(favicon);
+ }
+
+ if (err == "remoteXUL") {
+ // Remove the "Try again" button for remote XUL errors given that
+ // it is useless.
+ document.getElementById("netErrorButtonContainer").style.display = "none";
+ }
+
+ if (err == "cspBlocked") {
+ // Remove the "Try again" button for CSP violations, since it's
+ // almost certainly useless. (Bug 553180)
+ document.getElementById("netErrorButtonContainer").style.display = "none";
+ }
+
+ window.addEventListener("AboutNetErrorOptions", function(evt) {
+ // Pinning errors are of type nssFailure2
+ if (getErrorCode() == "nssFailure2" || getErrorCode() == "weakCryptoUsed") {
+ document.getElementById("learnMoreContainer").style.display = "block";
+ var learnMoreLink = document.getElementById("learnMoreLink");
+ // nssFailure2 also gets us other non-overrideable errors. Choose
+ // a "learn more" link based on description:
+ if (getDescription().includes("mozilla_pkix_error_key_pinning_failure")) {
+ learnMoreLink.href = "https://support.mozilla.org/kb/certificate-pinning-reports";
+ }
+ if (getErrorCode() == "weakCryptoUsed") {
+ learnMoreLink.href = "https://support.mozilla.org/kb/how-resolve-weak-crypto-error-messages-firefox";
+ }
+
+ var options = JSON.parse(evt.detail);
+ if (options && options.enabled) {
+ var checkbox = document.getElementById("automaticallyReportInFuture");
+ showCertificateErrorReporting();
+ if (options.automatic) {
+ // set the checkbox
+ checkbox.checked = true;
+ }
+
+ checkbox.addEventListener("change", function(evt) {
+ var event = new CustomEvent("AboutNetErrorSetAutomatic",
+ {bubbles:true, detail:evt.target.checked});
+ document.dispatchEvent(event);
+ }, false);
+ }
+ const hasPrefStyleError = [
+ "interrupted", // This happens with subresources that are above the max tls
+ "SSL_ERROR_PROTOCOL_VERSION_ALERT",
+ "SSL_ERROR_UNSUPPORTED_VERSION",
+ "SSL_ERROR_NO_CYPHER_OVERLAP",
+ "SSL_ERROR_NO_CIPHERS_SUPPORTED"
+ ].some((substring) => getDescription().includes(substring));
+ // If it looks like an error that is user config based
+ if (getErrorCode() == "nssFailure2" && hasPrefStyleError && options && options.changedCertPrefs) {
+ showPrefChangeContainer();
+ }
+ }
+ if (getErrorCode() == "weakCryptoUsed" || getErrorCode() == "sslv3Used") {
+ setupAdvancedButton(getErrorCode() == "weakCryptoUsed");
+ }
+ }.bind(this), true, true);
+
+ var event = new CustomEvent("AboutNetErrorLoad", {bubbles:true});
+ document.dispatchEvent(event);
+
+ if (err == "inadequateSecurityError") {
+ // Remove the "Try again" button for HTTP/2 inadequate security as it
+ // is useless.
+ document.getElementById("errorTryAgain").style.display = "none";
+
+ var container = document.getElementById("errorLongDesc");
+ for (var span of container.querySelectorAll("span.hostname")) {
+ span.textContent = document.location.hostname;
+ }
+ }
+
+ addDomainErrorLinks();
+ }
+
+ function initPageCaptivePortal()
+ {
+ document.body.className = "captiveportal";
+ document.title = document.getElementById("captivePortalPageTitle").textContent;
+
+ document.getElementById("openPortalLoginPageButton")
+ .addEventListener("click", () => {
+ let event = new CustomEvent("AboutNetErrorOpenCaptivePortal", {bubbles:true});
+ document.dispatchEvent(event);
+ });
+
+ addAutofocus("openPortalLoginPageButton");
+ setupAdvancedButton(true);
+
+ addDomainErrorLinks();
+
+ // When the portal is freed, an event is generated by the frame script
+ // that we can pick up and attempt to reload the original page.
+ window.addEventListener("AboutNetErrorCaptivePortalFreed", () => {
+ document.location.reload();
+ });
+ }
+
+ function initPageCertError() {
+ document.body.className = "certerror";
+ document.title = document.getElementById("certErrorPageTitle").textContent;
+ for (let host of document.querySelectorAll(".hostname")) {
+ host.textContent = document.location.hostname;
+ }
+
+ addAutofocus("returnButton");
+ setupAdvancedButton(true);
+
+ document.getElementById("learnMoreContainer").style.display = "block";
+
+ let checkbox = document.getElementById("automaticallyReportInFuture");
+ checkbox.addEventListener("change", function({target: {checked}}) {
+ document.dispatchEvent(new CustomEvent("AboutNetErrorSetAutomatic", {
+ detail: checked,
+ bubbles: true
+ }));
+ });
+
+ addEventListener("AboutNetErrorOptions", function(event) {
+ var options = JSON.parse(event.detail);
+ if (options && options.enabled) {
+ // Display error reporting UI
+ document.getElementById("certificateErrorReporting").style.display = "block";
+
+ // set the checkbox
+ checkbox.checked = !!options.automatic;
+ }
+ }, true, true);
+
+ let event = new CustomEvent("AboutNetErrorLoad", {bubbles:true});
+ document.getElementById("advancedButton").dispatchEvent(event);
+
+ addDomainErrorLinks();
+ }
+
+ /* Only do autofocus if we're the toplevel frame; otherwise we
+ don't want to call attention to ourselves! The key part is
+ that autofocus happens on insertion into the tree, so we
+ can remove the button, add @autofocus, and reinsert the
+ button.
+ */
+ function addAutofocus(buttonId, position = "afterbegin") {
+ if (window.top == window) {
+ var button = document.getElementById(buttonId);
+ var parent = button.parentNode;
+ button.remove();
+ button.setAttribute("autofocus", "true");
+ parent.insertAdjacentElement(position, button);
+ }
+ }
+
+ /* Try to preserve the links contained in the error description, like
+ the error code.
+
+ Also, in the case of SSL error pages about domain mismatch, see if
+ we can hyperlink the user to the correct site. We don't want
+ to do this generically since it allows MitM attacks to redirect
+ users to a site under attacker control, but in certain cases
+ it is safe (and helpful!) to do so. Bug 402210
+ */
+ function addDomainErrorLinks() {
+ // Rather than textContent, we need to treat description as HTML
+ var sdid = gIsCertError ? "badCertTechnicalInfo" : "errorShortDescText";
+ var sd = document.getElementById(sdid);
+ if (sd) {
+ var desc = getDescription();
+
+ // sanitize description text - see bug 441169
+
+ // First, find the index of the <a> tags we care about, being
+ // careful not to use an over-greedy regex.
+ var codeRe = /<a id="errorCode" title="([^"]+)">/;
+ var codeResult = codeRe.exec(desc);
+ var domainRe = /<a id="cert_domain_link" title="([^"]+)">/;
+ var domainResult = domainRe.exec(desc);
+
+ // The order of these links in the description is fixed in
+ // TransportSecurityInfo.cpp:formatOverridableCertErrorMessage.
+ var firstResult = domainResult;
+ if (!domainResult)
+ firstResult = codeResult;
+ if (!firstResult)
+ return;
+ // Remove sd's existing children
+ sd.textContent = "";
+
+ // Everything up to the first link should be text content.
+ sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index)));
+
+ // Now create the actual links.
+ if (domainResult) {
+ createLink(sd, "cert_domain_link", domainResult[1])
+ // Append text for anything between the two links.
+ sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index)));
+ }
+ createLink(sd, "errorCode", codeResult[1])
+
+ // Finally, append text for anything after the last closing </a>.
+ sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length)));
+ }
+
+ if (gIsCertError) {
+ // Initialize the error code link embedded in the error message to
+ // display debug information about the cert error.
+ var errorCode = document.getElementById("errorCode");
+ if (errorCode) {
+ errorCode.href = "javascript:void(0)";
+ errorCode.addEventListener("click", () => {
+ let debugInfo = document.getElementById("certificateErrorDebugInformation");
+ debugInfo.style.display = "block";
+ debugInfo.scrollIntoView({block: "start", behavior: "smooth"});
+ }, false);
+ }
+ }
+
+ // Initialize the cert domain link.
+ var link = document.getElementById("cert_domain_link");
+ if (!link)
+ return;
+
+ var okHost = link.getAttribute("title");
+ var thisHost = document.location.hostname;
+ var proto = document.location.protocol;
+
+ // If okHost is a wildcard domain ("*.example.com") let's
+ // use "www" instead. "*.example.com" isn't going to
+ // get anyone anywhere useful. bug 432491
+ okHost = okHost.replace(/^\*\./, "www.");
+
+ /* case #1:
+ * example.com uses an invalid security certificate.
+ *
+ * The certificate is only valid for www.example.com
+ *
+ * Make sure to include the "." ahead of thisHost so that
+ * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
+ *
+ * We'd normally just use a RegExp here except that we lack a
+ * library function to escape them properly (bug 248062), and
+ * domain names are famous for having '.' characters in them,
+ * which would allow spurious and possibly hostile matches.
+ */
+ if (okHost.endsWith("." + thisHost))
+ link.href = proto + okHost;
+
+ /* case #2:
+ * browser.garage.maemo.org uses an invalid security certificate.
+ *
+ * The certificate is only valid for garage.maemo.org
+ */
+ if (thisHost.endsWith("." + okHost))
+ link.href = proto + okHost;
+
+ // If we set a link, meaning there's something helpful for
+ // the user here, expand the section by default
+ if (link.href && getCSSClass() != "expertBadCert") {
+ var panelId = gIsCertError ? "badCertAdvancedPanel" : "weakCryptoAdvancedPanel"
+ toggleDisplay(document.getElementById(panelId));
+ if (gIsCertError) {
+ // Toggling the advanced panel must ensure that the debugging
+ // information panel is hidden as well, since it's opened by the
+ // error code link in the advanced panel.
+ var div = document.getElementById("certificateErrorDebugInformation");
+ div.style.display = "none";
+ }
+ }
+ }
+
+ function createLink(el, id, text) {
+ var anchorEl = document.createElement("a");
+ anchorEl.setAttribute("id", id);
+ anchorEl.setAttribute("title", text);
+ anchorEl.appendChild(document.createTextNode(text));
+ el.appendChild(anchorEl);
+ }
+ ]]></script>
+ </head>
+
+ <body dir="&locale.dir;">
+ <!-- Contains an alternate page title set on page init for cert errors. -->
+ <div id="certErrorPageTitle" style="display: none;">&certerror.pagetitle1;</div>
+ <div id="captivePortalPageTitle" style="display: none;">&captivePortal.title;</div>
+
+ <!-- ERROR ITEM CONTAINER (removed during loading to avoid bug 39098) -->
+ <div id="errorContainer">
+ <div id="errorTitlesContainer">
+ <h1 id="et_generic">&generic.title;</h1>
+ <h1 id="et_captivePortal">&captivePortal.title;</h1>
+ <h1 id="et_dnsNotFound">&dnsNotFound.title;</h1>
+ <h1 id="et_fileNotFound">&fileNotFound.title;</h1>
+ <h1 id="et_fileAccessDenied">&fileAccessDenied.title;</h1>
+ <h1 id="et_malformedURI">&malformedURI.title;</h1>
+ <h1 id="et_unknownProtocolFound">&unknownProtocolFound.title;</h1>
+ <h1 id="et_connectionFailure">&connectionFailure.title;</h1>
+ <h1 id="et_netTimeout">&netTimeout.title;</h1>
+ <h1 id="et_redirectLoop">&redirectLoop.title;</h1>
+ <h1 id="et_unknownSocketType">&unknownSocketType.title;</h1>
+ <h1 id="et_netReset">&netReset.title;</h1>
+ <h1 id="et_notCached">&notCached.title;</h1>
+ <h1 id="et_netOffline">&netOffline.title;</h1>
+ <h1 id="et_netInterrupt">&netInterrupt.title;</h1>
+ <h1 id="et_deniedPortAccess">&deniedPortAccess.title;</h1>
+ <h1 id="et_proxyResolveFailure">&proxyResolveFailure.title;</h1>
+ <h1 id="et_proxyConnectFailure">&proxyConnectFailure.title;</h1>
+ <h1 id="et_contentEncodingError">&contentEncodingError.title;</h1>
+ <h1 id="et_unsafeContentType">&unsafeContentType.title;</h1>
+ <h1 id="et_nssFailure2">&nssFailure2.title;</h1>
+ <h1 id="et_nssBadCert">&certerror.longpagetitle1;</h1>
+ <h1 id="et_cspBlocked">&cspBlocked.title;</h1>
+ <h1 id="et_remoteXUL">&remoteXUL.title;</h1>
+ <h1 id="et_corruptedContentErrorv2">&corruptedContentErrorv2.title;</h1>
+ <h1 id="et_sslv3Used">&sslv3Used.title;</h1>
+ <h1 id="et_weakCryptoUsed">&weakCryptoUsed.title;</h1>
+ <h1 id="et_inadequateSecurityError">&inadequateSecurityError.title;</h1>
+ </div>
+ <div id="errorDescriptionsContainer">
+ <div id="ed_generic">&generic.longDesc;</div>
+ <div id="ed_captivePortal">&captivePortal.longDesc;</div>
+ <div id="ed_dnsNotFound">&dnsNotFound.longDesc;</div>
+ <div id="ed_fileNotFound">&fileNotFound.longDesc;</div>
+ <div id="ed_fileAccessDenied">&fileAccessDenied.longDesc;</div>
+ <div id="ed_malformedURI">&malformedURI.longDesc;</div>
+ <div id="ed_unknownProtocolFound">&unknownProtocolFound.longDesc;</div>
+ <div id="ed_connectionFailure">&connectionFailure.longDesc;</div>
+ <div id="ed_netTimeout">&netTimeout.longDesc;</div>
+ <div id="ed_redirectLoop">&redirectLoop.longDesc;</div>
+ <div id="ed_unknownSocketType">&unknownSocketType.longDesc;</div>
+ <div id="ed_netReset">&netReset.longDesc;</div>
+ <div id="ed_notCached">&notCached.longDesc;</div>
+ <div id="ed_netOffline">&netOffline.longDesc2;</div>
+ <div id="ed_netInterrupt">&netInterrupt.longDesc;</div>
+ <div id="ed_deniedPortAccess">&deniedPortAccess.longDesc;</div>
+ <div id="ed_proxyResolveFailure">&proxyResolveFailure.longDesc;</div>
+ <div id="ed_proxyConnectFailure">&proxyConnectFailure.longDesc;</div>
+ <div id="ed_contentEncodingError">&contentEncodingError.longDesc;</div>
+ <div id="ed_unsafeContentType">&unsafeContentType.longDesc;</div>
+ <div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
+ <div id="ed_nssBadCert">&certerror.introPara;</div>
+ <div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
+ <div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
+ <div id="ed_corruptedContentErrorv2">&corruptedContentErrorv2.longDesc;</div>
+ <div id="ed_sslv3Used">&sslv3Used.longDesc2;</div>
+ <div id="ed_weakCryptoUsed">&weakCryptoUsed.longDesc2;</div>
+ <div id="ed_inadequateSecurityError">&inadequateSecurityError.longDesc;</div>
+ </div>
+ </div>
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer" class="container">
+
+ <!-- Error Title -->
+ <div class="title">
+ <h1 class="title-text"/>
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText" />
+ </div>
+ <p id="badStsCertExplanation" hidden="true">&certerror.whatShouldIDo.badStsCertExplanation;</p>
+
+ <div id="wrongSystemTimePanel" style="display: none;">
+ &certerror.wrongSystemTime;
+ </div>
+
+ <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
+ <div id="errorLongDesc" />
+
+ <div id="learnMoreContainer">
+ <p><a href="https://support.mozilla.org/kb/what-does-your-connection-is-not-secure-mean" id="learnMoreLink" target="new">&errorReporting.learnMore;</a></p>
+ </div>
+
+ <div id="prefChangeContainer" class="button-container">
+ <p>&prefReset.longDesc;</p>
+ <button id="prefResetButton" class="primary" autocomplete="off">&prefReset.label;</button>
+ </div>
+
+ <div id="certErrorAndCaptivePortalButtonContainer" class="button-container">
+ <button id="returnButton" class="primary" autocomplete="off">&returnToPreviousPage.label;</button>
+ <button id="openPortalLoginPageButton" class="primary" autocomplete="off">&openPortalLoginPage.label;</button>
+ <div class="button-spacer"></div>
+ <button id="advancedButton" autocomplete="off">&advanced.label;</button>
+ </div>
+ </div>
+
+ <div id="netErrorButtonContainer" class="button-container">
+ <button id="errorTryAgain" class="primary" autocomplete="off" onclick="retryThis(this);">&retry.label;</button>
+ </div>
+
+ <!-- UI for option to report certificate errors to Mozilla. Removed on
+ init for other error types .-->
+ <div id="certificateErrorReporting">
+ <p class="toggle-container-with-text">
+ <input type="checkbox" id="automaticallyReportInFuture" />
+ <label for="automaticallyReportInFuture" id="automaticallyReportInFuture">&errorReporting.automatic2;</label>
+ </p>
+ </div>
+
+ <div id="advancedPanelContainer">
+ <div id="weakCryptoAdvancedPanel" class="advanced-panel">
+ <div id="weakCryptoAdvancedDescription">
+ <p>&weakCryptoAdvanced.longDesc;</p>
+ </div>
+ <div id="advancedLongDesc" />
+ <div id="overrideWeakCryptoPanel">
+ <a id="overrideWeakCrypto" href="#">&weakCryptoAdvanced.override;</a>
+ </div>
+ </div>
+
+ <div id="badCertAdvancedPanel" class="advanced-panel">
+ <p id="badCertTechnicalInfo"/>
+ <button id="exceptionDialogButton">&securityOverride.exceptionButtonLabel;</button>
+ </div>
+ </div>
+
+ </div>
+
+ <div id="certificateErrorDebugInformation">
+ <button id="copyToClipboard">&certerror.copyToClipboard.label;</button>
+ <div id="certificateErrorText"/>
+ <button id="copyToClipboard">&certerror.copyToClipboard.label;</button>
+ </div>
+
+ <!--
+ - Note: It is important to run the script this way, instead of using
+ - an onload handler. This is because error pages are loaded as
+ - LOAD_BACKGROUND, which means that onload handlers will not be executed.
+ -->
+ <script type="application/javascript">
+ initPage();
+ </script>
+
+ </body>
+</html>
diff --git a/browser/base/content/aboutProviderDirectory.xhtml b/browser/base/content/aboutProviderDirectory.xhtml
new file mode 100644
index 000000000..596ede4b3
--- /dev/null
+++ b/browser/base/content/aboutProviderDirectory.xhtml
@@ -0,0 +1,60 @@
+<?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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+ %browserDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&social.directory.label;</title>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/skin/aboutProviderDirectory.css"/>
+ </head>
+
+ <body>
+ <div id="activation-link" hidden="true">
+ <div id="message-box">
+ <p>&social.directory.text;</p>
+ </div>
+ <div id="button-box">
+ <button onclick="openDirectory()">&social.directory.button;</button>
+ </div>
+ </div>
+ <div id="activation" hidden="true">
+ <p>&social.directory.introText;</p>
+ <div><iframe id="activation-frame"/></div>
+ <p><a class="link" onclick="openDirectory()">&social.directory.viewmore.text;</a></p>
+ </div>
+ </body>
+
+ <script type="text/javascript;version=1.8"><![CDATA[
+ const Cu = Components.utils;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ function openDirectory() {
+ let url = Services.prefs.getCharPref("social.directories").split(',')[0];
+ window.open(url);
+ window.close();
+ }
+
+ if (Services.prefs.getBoolPref("social.share.activationPanelEnabled")) {
+ let url = Services.prefs.getCharPref("social.shareDirectory");
+ document.getElementById("activation-frame").setAttribute("src", url);
+ document.getElementById("activation").removeAttribute("hidden");
+ } else {
+ document.getElementById("activation-link").removeAttribute("hidden");
+ }
+ ]]></script>
+</html>
diff --git a/browser/base/content/aboutRobots-icon.png b/browser/base/content/aboutRobots-icon.png
new file mode 100644
index 000000000..1c4899aaf
--- /dev/null
+++ b/browser/base/content/aboutRobots-icon.png
Binary files differ
diff --git a/browser/base/content/aboutRobots-widget-left.png b/browser/base/content/aboutRobots-widget-left.png
new file mode 100644
index 000000000..3a1e48d5f
--- /dev/null
+++ b/browser/base/content/aboutRobots-widget-left.png
Binary files differ
diff --git a/browser/base/content/aboutRobots.xhtml b/browser/base/content/aboutRobots.xhtml
new file mode 100644
index 000000000..23fe3ba17
--- /dev/null
+++ b/browser/base/content/aboutRobots.xhtml
@@ -0,0 +1,108 @@
+<?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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD
+ SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % aboutrobotsDTD
+ SYSTEM "chrome://browser/locale/aboutRobots.dtd">
+ %aboutrobotsDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&robots.pagetitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" />
+ <link rel="icon" type="image/png" id="favicon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8%2F9hAAAACGFjVEwAAAASAAAAAJNtBPIAAAAaZmNUTAAAAAAAAAAQAAAAEAAAAAAAAAAALuAD6AABhIDeugAAALhJREFUOI2Nk8sNxCAMRDlGohauXFOMpfTiAlxICqAELltHLqlgctg1InzMRhpFAc%2BLGWTnmoeZYamt78zXdZmaQtQMADlnU0OIAlbmJUBEcO4bRKQY2rUXIPmAGnDuG%2FBx3%2FfvOPVaDUg%2BoAPUf1PArIMCSD5glMEsUGaG%2BkyAFWIBaCsKuA%2BHGCNijLgP133XgOEtaPFMy2vUolEGJoCIzBmoRUR9%2B7rxj16DZaW%2FmgtmxnJ8V3oAnApQwNS5zpcAAAAaZmNUTAAAAAEAAAAQAAAAEAAAAAAAAAAAAB4D6AIB52fclgAAACpmZEFUAAAAAjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9WF%2Bu8QAAABpmY1RMAAAAAwAAABAAAAAQAAAAAAAAAAAAHgPoAgEK8Q9%2FAAAAFmZkQVQAAAAEOI1jYBgFo2AUjAIIAAAEEAAB0xIn4wAAABpmY1RMAAAABQAAABAAAAAQAAAAAAAAAAAAHgPoAgHnO30FAAAAQGZkQVQAAAAGOI1jYBieYKcaw39ixHCC%2F6cwFWMTw2rz%2F1MM%2F6Vu%2Ff%2F%2F%2FxTD%2F51qEIwuRjsXILuEGLFRMApgAADhNCsVfozYcAAAABpmY1RMAAAABwAAABAAAAAQAAAAAAAAAAAAHgPoAgEKra7sAAAAFmZkQVQAAAAIOI1jYBgFo2AUjAIIAAAEEAABM9s3hAAAABpmY1RMAAAACQAAABAAAAAQAAAAAAAAAAAAHgPoAgHn3p%2BwAAAAKmZkQVQAAAAKOI1jYBgFWEFzc%2FN%2FYsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF%2F1BhPl6AAAAGmZjVEwAAAALAAAAEAAAABAAAAAAAAAAAAAeA%2BgCAQpITFkAAAAWZmRBVAAAAAw4jWNgGAWjYBSMAggAAAQQAAHaszpmAAAAGmZjVEwAAAANAAAAEAAAABAAAAAAAAAAAAAeA%2BgCAeeCPiMAAABAZmRBVAAAAA44jWNgGJ5gpxrDf2LEcIL%2FpzAVYxPDavP%2FUwz%2FpW79%2F%2F%2F%2FFMP%2FnWoQjC5GOxcgu4QYsVEwCmAAAOE0KxUmBL0KAAAAGmZjVEwAAAAPAAAAEAAAABAAAAAAAAAAAAAeA%2BgCAQoU7coAAAAWZmRBVAAAABA4jWNgGAWjYBSMAggAAAQQAAEpOBELAAAAGmZjVEwAAAARAAAAEAAAABAAAAAAAAAAAAAeA%2BgCAeYVWtoAAAAqZmRBVAAAABI4jWNgGAVYQXNz839ixHBq3qnG8B9ZAzYx2rlgFIwCcgAA8psX%2FWvpAecAAAAaZmNUTAAAABMAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC4OJMwAAABZmZEFUAAAAFDiNY2AYBaNgFIwCCAAABBAAAcBQHOkAAAAaZmNUTAAAABUAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5kn7SQAAAEBmZEFUAAAAFjiNY2AYnmCnGsN%2FYsRwgv%2BnMBVjE8Nq8%2F9TDP%2Blbv3%2F%2F%2F8Uw%2F%2BdahCMLkY7FyC7hBixUTAKYAAA4TQrFc%2BcEoQAAAAaZmNUTAAAABcAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC98ooAAAABZmZEFUAAAAGDiNY2AYBaNgFIwCCAAABBAAASCZDI4AAAAaZmNUTAAAABkAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5qwZ%2FAAAACpmZEFUAAAAGjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9cjJWbAAAABpmY1RMAAAAGwAAABAAAAAQAAAAAAAAAAAAHgPoAgELOsoVAAAAFmZkQVQAAAAcOI1jYBgFo2AUjAIIAAAEEAAByfEBbAAAABpmY1RMAAAAHQAAABAAAAAQAAAAAAAAAAAAHgPoAgHm8LhvAAAAQGZkQVQAAAAeOI1jYBieYKcaw39ixHCC%2F6cwFWMTw2rz%2F1MM%2F6Vu%2Ff%2F%2F%2FxTD%2F51qEIwuRjsXILuEGLFRMApgAADhNCsVlxR3%2FgAAABpmY1RMAAAAHwAAABAAAAAQAAAAAAAAAAAAHgPoAgELZmuGAAAAFmZkQVQAAAAgOI1jYBgFo2AUjAIIAAAEEAABHP5cFQAAABpmY1RMAAAAIQAAABAAAAAQAAAAAAAAAAAAHgPoAgHlgtAOAAAAKmZkQVQAAAAiOI1jYBgFWEFzc%2FN%2FYsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF%2F0%2FMvDdAAAAAElFTkSuQmCC"/>
+
+ <script type="application/javascript"><![CDATA[
+ var buttonClicked = false;
+ function robotButton()
+ {
+ var button = document.getElementById('errorTryAgain');
+ if (buttonClicked) {
+ button.style.visibility = "hidden";
+ } else {
+ var newLabel = button.getAttribute("label2");
+ button.textContent = newLabel;
+ buttonClicked = true;
+ }
+ }
+ ]]></script>
+
+ <style type="text/css"><![CDATA[
+ #errorPageContainer {
+ background-image: none;
+ }
+
+ #errorPageContainer:before {
+ content: url('chrome://browser/content/aboutRobots-icon.png');
+ position: absolute;
+ }
+
+ body[dir=rtl] #icon,
+ body[dir=rtl] #errorPageContainer:before {
+ transform: scaleX(-1);
+ }
+ ]]></style>
+ </head>
+
+ <body dir="&locale.dir;">
+
+ <!-- PAGE CONTAINER (for styling purposes only) -->
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <h1 id="errorTitleText">&robots.errorTitleText;</h1>
+ </div>
+
+ <!-- LONG CONTENT (the section most likely to require scrolling) -->
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText">&robots.errorShortDescText;</p>
+ </div>
+
+ <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
+ <div id="errorLongDesc">
+ <ul>
+ <li>&robots.errorLongDesc1;</li>
+ <li>&robots.errorLongDesc2;</li>
+ <li>&robots.errorLongDesc3;</li>
+ <li>&robots.errorLongDesc4;</li>
+ </ul>
+ </div>
+
+ <!-- Short Description -->
+ <div id="errorTrailerDesc">
+ <p id="errorTrailerDescText">&robots.errorTrailerDescText;</p>
+ </div>
+
+ </div>
+
+ <!-- Button -->
+ <button id="errorTryAgain"
+ label2="&robots.dontpress;"
+ onclick="robotButton();">&retry.label;</button>
+
+ <img src="chrome://browser/content/aboutRobots-widget-left.png"
+ style="position: absolute; bottom: -12px; left: -10px;"/>
+ <img src="chrome://browser/content/aboutRobots-widget-left.png"
+ style="position: absolute; bottom: -12px; right: -10px; transform: scaleX(-1);"/>
+ </div>
+
+ </body>
+</html>
diff --git a/browser/base/content/aboutSocialError.xhtml b/browser/base/content/aboutSocialError.xhtml
new file mode 100644
index 000000000..94a4e3dbd
--- /dev/null
+++ b/browser/base/content/aboutSocialError.xhtml
@@ -0,0 +1,111 @@
+<?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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&loadError.label;</title>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
+ <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/aboutSocialError.css"/>
+ <link rel="icon" type="image/png" id="favicon" href="chrome://global/skin/icons/warning-16.png"/>
+ </head>
+
+ <body>
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <p id="errorShortDescText" >foo</p>
+ </div>
+
+ <div id="button-box">
+ <button id="btnTryAgain" onclick="tryAgainButton()"/>
+ </div>
+ </div>
+ </body>
+
+ <script type="text/javascript;version=1.8"><![CDATA[
+ const Cu = Components.utils;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource:///modules/Social.jsm");
+
+ let config = {
+ tryAgainCallback: reloadProvider
+ }
+
+ function parseQueryString() {
+ let searchParams = new URLSearchParams(document.documentURI.split("?")[1]);
+ let mode = searchParams.get("mode");
+ config.origin = searchParams.get("origin");
+ let encodedURL = searchParams.get("url");
+ let url = decodeURIComponent(encodedURL);
+ // directory does not have origin set, in that case use the url origin for
+ // the error message.
+ if (!config.origin) {
+ let URI = Services.io.newURI(url, null, null);
+ config.origin =
+ Services.scriptSecurityManager.createCodebasePrincipal(URI, {}).origin;
+ }
+
+ switch (mode) {
+ case "compactInfo":
+ document.getElementById("btnTryAgain").style.display = 'none';
+ break;
+ case "tryAgainOnly":
+ //intentional fall-through
+ case "tryAgain":
+ config.tryAgainCallback = loadQueryURL;
+ config.queryURL = url;
+ break;
+ default:
+ break;
+ }
+ }
+
+ function setUpStrings() {
+ let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+ let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let productName = brandBundle.GetStringFromName("brandShortName");
+ let provider = Social._getProviderFromOrigin(config.origin);
+ let providerName = provider ? provider.name : config.origin;
+
+ // Sets up the error message
+ let msg = browserBundle.formatStringFromName("social.error.message", [productName, providerName], 2);
+ document.getElementById("errorShortDescText").textContent = msg;
+
+ // Sets up the buttons' labels and accesskeys
+ let btnTryAgain = document.getElementById("btnTryAgain");
+ btnTryAgain.textContent = browserBundle.GetStringFromName("social.error.tryAgain.label");
+ btnTryAgain.accessKey = browserBundle.GetStringFromName("social.error.tryAgain.accesskey");
+ }
+
+ function tryAgainButton() {
+ config.tryAgainCallback();
+ }
+
+ function loadQueryURL() {
+ window.location.href = config.queryURL;
+ }
+
+ function reloadProvider() {
+ let provider = Social._getProviderFromOrigin(config.origin);
+ provider.reload();
+ }
+
+ parseQueryString();
+ setUpStrings();
+ ]]></script>
+</html>
diff --git a/browser/base/content/aboutTabCrashed.css b/browser/base/content/aboutTabCrashed.css
new file mode 100644
index 000000000..de0eabe8b
--- /dev/null
+++ b/browser/base/content/aboutTabCrashed.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/. */
+
+html:not(.crashDumpSubmitted) #reportSent,
+html:not(.crashDumpAvailable) #reportBox,
+.container[multiple="true"] > .offers > #offerHelpMessageSingle,
+.container[multiple="false"] > .offers > #offerHelpMessageMultiple,
+.container[multiple="false"] > .button-container > #restoreAll {
+ display: none;
+} \ No newline at end of file
diff --git a/browser/base/content/aboutTabCrashed.js b/browser/base/content/aboutTabCrashed.js
new file mode 100644
index 000000000..337add1d2
--- /dev/null
+++ b/browser/base/content/aboutTabCrashed.js
@@ -0,0 +1,309 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 AboutTabCrashed = {
+ /**
+ * This can be set to true once this page receives a message from the
+ * parent saying whether or not a crash report is available.
+ */
+ hasReport: false,
+
+ /**
+ * The messages that we might receive from the parent.
+ */
+ MESSAGES: [
+ "SetCrashReportAvailable",
+ "CrashReportSent",
+ "UpdateCount",
+ ],
+
+ /**
+ * Items for which we will listen for click events.
+ */
+ CLICK_TARGETS: [
+ "closeTab",
+ "restoreTab",
+ "restoreAll",
+ "sendReport",
+ ],
+
+ /**
+ * Returns information about this crashed tab.
+ *
+ * @return (Object) An object with the following properties:
+ * title (String):
+ * The title of the page that crashed.
+ * URL (String):
+ * The URL of the page that crashed.
+ */
+ get pageData() {
+ delete this.pageData;
+
+ let URL = document.documentURI;
+ let queryString = URL.replace(/^about:tabcrashed?e=tabcrashed/, "");
+
+ let titleMatch = queryString.match(/d=([^&]*)/);
+ let URLMatch = queryString.match(/u=([^&]*)/);
+
+ return this.pageData = {
+ title: titleMatch && titleMatch[1] ? decodeURIComponent(titleMatch[1]) : "",
+ URL: URLMatch && URLMatch[1] ? decodeURIComponent(URLMatch[1]) : "",
+ };
+ },
+
+ init() {
+ this.MESSAGES.forEach((msg) => addMessageListener(msg, this.receiveMessage.bind(this)));
+ addEventListener("DOMContentLoaded", this);
+
+ document.title = this.pageData.title;
+ },
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "UpdateCount": {
+ this.setMultiple(message.data.count > 1);
+ break;
+ }
+ case "SetCrashReportAvailable": {
+ this.onSetCrashReportAvailable(message);
+ break;
+ }
+ case "CrashReportSent": {
+ this.onCrashReportSent();
+ break;
+ }
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMContentLoaded": {
+ this.onDOMContentLoaded();
+ break;
+ }
+ case "click": {
+ this.onClick(event);
+ break;
+ }
+ case "input": {
+ this.onInput(event);
+ break;
+ }
+ }
+ },
+
+ onDOMContentLoaded() {
+ this.CLICK_TARGETS.forEach((targetID) => {
+ let el = document.getElementById(targetID);
+ el.addEventListener("click", this);
+ });
+
+ // For setting "emailMe" checkbox automatically on email value change.
+ document.getElementById("email").addEventListener("input", this);
+
+ // Error pages are loaded as LOAD_BACKGROUND, so they don't get load events.
+ let event = new CustomEvent("AboutTabCrashedLoad", {bubbles:true});
+ document.dispatchEvent(event);
+
+ sendAsyncMessage("Load");
+ },
+
+ onClick(event) {
+ switch (event.target.id) {
+ case "closeTab": {
+ this.sendMessage("closeTab");
+ break;
+ }
+
+ case "restoreTab": {
+ this.sendMessage("restoreTab");
+ break;
+ }
+
+ case "restoreAll": {
+ this.sendMessage("restoreAll");
+ break;
+ }
+
+ case "sendReport": {
+ this.showCrashReportUI(event.target.checked);
+ break;
+ }
+ }
+ },
+
+ onInput(event) {
+ switch (event.target.id) {
+ case "email": {
+ document.getElementById("emailMe").checked = !!event.target.value;
+ break;
+ }
+ }
+ },
+ /**
+ * After this page tells the parent that it has loaded, the parent
+ * will respond with whether or not a crash report is available. This
+ * method handles that message.
+ *
+ * @param message
+ * The message from the parent, which should contain a data
+ * Object property with the following properties:
+ *
+ * hasReport (bool):
+ * Whether or not there is a crash report.
+ *
+ * sendReport (bool):
+ * Whether or not the the user prefers to send the report
+ * by default.
+ *
+ * includeURL (bool):
+ * Whether or not the user prefers to send the URL of
+ * the tab that crashed.
+ *
+ * emailMe (bool):
+ * Whether or not to send the email address of the user
+ * in the report.
+ *
+ * email (String):
+ * The email address of the user (empty if emailMe is false).
+ *
+ * requestAutoSubmit (bool):
+ * Whether or not we should ask the user to automatically
+ * submit backlogged crash reports.
+ *
+ */
+ onSetCrashReportAvailable(message) {
+ let data = message.data;
+
+ if (data.hasReport) {
+ this.hasReport = true;
+ document.documentElement.classList.add("crashDumpAvailable");
+
+ document.getElementById("sendReport").checked = data.sendReport;
+ document.getElementById("includeURL").checked = data.includeURL;
+
+ if (data.requestEmail) {
+ document.getElementById("requestEmail").hidden = false;
+ document.getElementById("emailMe").checked = data.emailMe;
+ if (data.emailMe) {
+ document.getElementById("email").value = data.email;
+ }
+ }
+
+ this.showCrashReportUI(data.sendReport);
+ } else {
+ this.showCrashReportUI(false);
+ }
+
+ if (data.requestAutoSubmit) {
+ document.getElementById("requestAutoSubmit").hidden = false;
+ }
+
+ let event = new CustomEvent("AboutTabCrashedReady", {bubbles:true});
+ document.dispatchEvent(event);
+ },
+
+ /**
+ * Handler for when the parent reports that the crash report associated
+ * with this about:tabcrashed page has been sent.
+ */
+ onCrashReportSent() {
+ document.documentElement.classList.remove("crashDumpAvailable");
+ document.documentElement.classList.add("crashDumpSubmitted");
+ },
+
+ /**
+ * Toggles the display of the crash report form.
+ *
+ * @param shouldShow (bool)
+ * True if the crash report form should be shown
+ */
+ showCrashReportUI(shouldShow) {
+ let options = document.getElementById("options");
+ options.hidden = !shouldShow;
+ },
+
+ /**
+ * Toggles whether or not the page is one of several visible pages
+ * showing the crash reporter. This controls some of the language
+ * on the page, along with what the "primary" button is.
+ *
+ * @param hasMultiple (bool)
+ * True if there are multiple crash report pages being shown.
+ */
+ setMultiple(hasMultiple) {
+ let main = document.getElementById("main");
+ main.setAttribute("multiple", hasMultiple);
+
+ let restoreTab = document.getElementById("restoreTab");
+
+ // The "Restore All" button has the "primary" class by default, so
+ // we only need to modify the "Restore Tab" button.
+ if (hasMultiple) {
+ restoreTab.classList.remove("primary");
+ } else {
+ restoreTab.classList.add("primary");
+ }
+ },
+
+ /**
+ * Sends a message to the parent in response to the user choosing
+ * one of the actions available on the page. This might also send up
+ * crash report information if the user has chosen to submit a crash
+ * report.
+ *
+ * @param messageName (String)
+ * The message to send to the parent
+ */
+ sendMessage(messageName) {
+ let comments = "";
+ let email = "";
+ let URL = "";
+ let sendReport = false;
+ let emailMe = false;
+ let includeURL = false;
+ let autoSubmit = false;
+
+ if (this.hasReport) {
+ sendReport = document.getElementById("sendReport").checked;
+ if (sendReport) {
+ comments = document.getElementById("comments").value.trim();
+
+ includeURL = document.getElementById("includeURL").checked;
+ if (includeURL) {
+ URL = this.pageData.URL.trim();
+ }
+
+ if (!document.getElementById("requestEmail").hidden) {
+ emailMe = document.getElementById("emailMe").checked;
+ if (emailMe) {
+ email = document.getElementById("email").value.trim();
+ }
+ }
+ }
+ }
+
+ let requestAutoSubmit = document.getElementById("requestAutoSubmit");
+ if (requestAutoSubmit.hidden) {
+ // The checkbox is hidden if the user has already opted in to sending
+ // backlogged crash reports.
+ autoSubmit = true;
+ } else {
+ autoSubmit = document.getElementById("autoSubmit").checked;
+ }
+
+ sendAsyncMessage(messageName, {
+ sendReport,
+ comments,
+ email,
+ emailMe,
+ includeURL,
+ URL,
+ autoSubmit,
+ hasReport: this.hasReport,
+ });
+ },
+};
+
+AboutTabCrashed.init();
diff --git a/browser/base/content/aboutTabCrashed.xhtml b/browser/base/content/aboutTabCrashed.xhtml
new file mode 100644
index 000000000..8b18bee9c
--- /dev/null
+++ b/browser/base/content/aboutTabCrashed.xhtml
@@ -0,0 +1,97 @@
+<?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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD
+ SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % tabCrashedDTD
+ SYSTEM "chrome://browser/locale/aboutTabCrashed.dtd">
+ %tabCrashedDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://global/skin/in-content/info-pages.css"/>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/content/aboutTabCrashed.css"/>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/skin/aboutTabCrashed.css"/>
+ </head>
+
+ <body dir="&locale.dir;">
+ <div id="main" class="container" multiple="false">
+
+ <div class="title">
+ <h1 class="title-text">&tabCrashed.header2;</h1>
+ </div>
+
+ <div class="offers">
+ <h2>&tabCrashed.offerHelp;</h2>
+ <p id="offerHelpMessageSingle">&tabCrashed.single.offerHelpMessage;</p>
+ <p id="offerHelpMessageMultiple">&tabCrashed.multiple.offerHelpMessage;</p>
+ </div>
+
+ <div id="reportBox">
+ <h2>&tabCrashed.requestHelp;</h2>
+ <p>&tabCrashed.requestHelpMessage;</p>
+
+ <h2>&tabCrashed.requestReport;</h2>
+
+ <div class="checkbox-with-label">
+ <input type="checkbox" id="sendReport"/>
+ <label for="sendReport">&tabCrashed.sendReport2;</label>
+ </div>
+
+ <ul id="options">
+ <li>
+ <textarea id="comments" placeholder="&tabCrashed.commentPlaceholder2;" rows="4"></textarea>
+ </li>
+
+ <li class="checkbox-with-label">
+ <input type="checkbox" id="includeURL"/>
+ <label for="includeURL">&tabCrashed.includeURL2;</label>
+ </li>
+
+ <li id="requestEmail" hidden="true">
+ <div class="checkbox-with-label">
+ <input type="checkbox" id="emailMe"/>
+ <label for="emailMe">&tabCrashed.emailMe;</label>
+ </div>
+ <input type="text" id="email" placeholder="&tabCrashed.emailPlaceholder;"/>
+ </li>
+ </ul>
+
+ <div id="requestAutoSubmit" hidden="true">
+ <h2>&tabCrashed.requestAutoSubmit2;</h2>
+ <div class="checkbox-with-label">
+ <input type="checkbox" id="autoSubmit"/>
+ <label for="autoSubmit">&tabCrashed.autoSubmit;</label>
+ </div>
+ </div>
+ </div>
+
+ <p id="reportSent">&tabCrashed.reportSent;</p>
+
+ <div class="button-container">
+ <button id="closeTab">
+ &tabCrashed.closeTab;</button>
+ <button id="restoreTab" class="primary">
+ &tabCrashed.restoreTab;</button>
+ <button id="restoreAll" autofocus="true" class="primary">
+ &tabCrashed.restoreAll;</button>
+ </div>
+ </div>
+ </body>
+ <script type="text/javascript;version=1.8" src="chrome://browser/content/aboutTabCrashed.js"/>
+</html>
diff --git a/browser/base/content/aboutaccounts/aboutaccounts.css b/browser/base/content/aboutaccounts/aboutaccounts.css
new file mode 100644
index 000000000..a2c5cb8f0
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.css
@@ -0,0 +1,24 @@
+html, body {
+ height: 100%;
+}
+
+#remote {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ display: none;
+}
+
+#networkError, #manage, #intro, #stage, #configError {
+ display: none;
+}
+
+#oldsync {
+ background: none;
+ border: 0;
+ color: #0095dd;
+}
+
+#oldsync:focus {
+ outline: 1px dotted #0095dd;
+}
diff --git a/browser/base/content/aboutaccounts/aboutaccounts.js b/browser/base/content/aboutaccounts/aboutaccounts.js
new file mode 100644
index 000000000..a05c1ea75
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -0,0 +1,543 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+
+var fxAccountsCommon = {};
+Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+
+// for master-password utilities
+Cu.import("resource://services-sync/util.js");
+
+const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
+const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
+
+const ACTION_URL_PARAM = "action";
+
+const OBSERVER_TOPICS = [
+ fxAccountsCommon.ONVERIFIED_NOTIFICATION,
+ fxAccountsCommon.ONLOGOUT_NOTIFICATION,
+];
+
+function log(msg) {
+ // dump("FXA: " + msg + "\n");
+}
+
+function error(msg) {
+ console.log("Firefox Account Error: " + msg + "\n");
+}
+
+function getPreviousAccountNameHash() {
+ try {
+ return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
+ } catch (_) {
+ return "";
+ }
+}
+
+function setPreviousAccountNameHash(acctName) {
+ let string = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ string.data = sha256(acctName);
+ Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
+}
+
+function needRelinkWarning(acctName) {
+ let prevAcctHash = getPreviousAccountNameHash();
+ return prevAcctHash && prevAcctHash != sha256(acctName);
+}
+
+// Given a string, returns the SHA265 hash in base64
+function sha256(str) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ // Data is an array of bytes.
+ let data = converter.convertToByteArray(str, {});
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+
+ return hasher.finish(true);
+}
+
+function promptForRelink(acctName) {
+ let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
+ let continueLabel = sb.GetStringFromName("continue.label");
+ let title = sb.GetStringFromName("relinkVerify.title");
+ let description = sb.formatStringFromName("relinkVerify.description",
+ [acctName], 1);
+ let body = sb.GetStringFromName("relinkVerify.heading") +
+ "\n\n" + description;
+ let ps = Services.prompt;
+ let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) +
+ (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) +
+ ps.BUTTON_POS_1_DEFAULT;
+ let pressed = Services.prompt.confirmEx(window, title, body, buttonFlags,
+ continueLabel, null, null, null,
+ {});
+ return pressed == 0; // 0 is the "continue" button
+}
+
+// If the last fxa account used for sync isn't this account, we display
+// a modal dialog checking they really really want to do this...
+// (This is sync-specific, so ideally would be in sync's identity module,
+// but it's a little more seamless to do here, and sync is currently the
+// only fxa consumer, so...
+function shouldAllowRelink(acctName) {
+ return !needRelinkWarning(acctName) || promptForRelink(acctName);
+}
+
+function updateDisplayedEmail(user) {
+ let emailDiv = document.getElementById("email");
+ if (emailDiv && user) {
+ emailDiv.textContent = user.email;
+ }
+}
+
+var wrapper = {
+ iframe: null,
+
+ init: function (url, urlParams) {
+ // If a master-password is enabled, we want to encourage the user to
+ // unlock it. Things still work if not, but the user will probably need
+ // to re-auth next startup (in which case we will get here again and
+ // re-prompt)
+ Utils.ensureMPUnlocked();
+
+ let iframe = document.getElementById("remote");
+ this.iframe = iframe;
+ this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
+ let docShell = this.iframe.frameLoader.docShell;
+ docShell.QueryInterface(Ci.nsIWebProgress);
+ docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+ iframe.addEventListener("load", this);
+
+ // Ideally we'd just merge urlParams with new URL(url).searchParams, but our
+ // URLSearchParams implementation doesn't support iteration (bug 1085284).
+ let urlParamStr = urlParams.toString();
+ if (urlParamStr) {
+ url += (url.includes("?") ? "&" : "?") + urlParamStr;
+ }
+ this.url = url;
+ // Set the iframe's location with loadURI/LOAD_FLAGS_REPLACE_HISTORY to
+ // avoid having a new history entry being added. REPLACE_HISTORY is used
+ // to replace the current entry, which is `about:blank`.
+ let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, null, null, null);
+ },
+
+ retry: function () {
+ let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
+ },
+
+ iframeListener: {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+
+ onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
+ let failure = false;
+
+ // Captive portals sometimes redirect users
+ if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
+ failure = true;
+ } else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
+ if (aRequest instanceof Ci.nsIHttpChannel) {
+ try {
+ failure = aRequest.responseStatus != 200;
+ } catch (e) {
+ failure = aStatus != Components.results.NS_OK;
+ }
+ }
+ }
+
+ // Calling cancel() will raise some OnStateChange notifications by itself,
+ // so avoid doing that more than once
+ if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ setErrorPage("networkError");
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ setErrorPage("networkError");
+ }
+ },
+
+ onProgressChange: function() {},
+ onStatusChange: function() {},
+ onSecurityChange: function() {},
+ },
+
+ handleEvent: function (evt) {
+ switch (evt.type) {
+ case "load":
+ this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this);
+ this.iframe.removeEventListener("load", this);
+ break;
+ case "FirefoxAccountsCommand":
+ this.handleRemoteCommand(evt);
+ break;
+ }
+ },
+
+ /**
+ * onLogin handler receives user credentials from the jelly after a
+ * sucessful login and stores it in the fxaccounts service
+ *
+ * @param accountData the user's account data and credentials
+ */
+ onLogin: function (accountData) {
+ log("Received: 'login'. Data:" + JSON.stringify(accountData));
+
+ if (accountData.customizeSync) {
+ Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true);
+ }
+ delete accountData.customizeSync;
+ // sessionTokenContext is erroneously sent by the content server.
+ // https://github.com/mozilla/fxa-content-server/issues/2766
+ // To avoid having the FxA storage manager not knowing what to do with
+ // it we delete it here.
+ delete accountData.sessionTokenContext;
+
+ // We need to confirm a relink - see shouldAllowRelink for more
+ let newAccountEmail = accountData.email;
+ // The hosted code may have already checked for the relink situation
+ // by sending the can_link_account command. If it did, then
+ // it will indicate we don't need to ask twice.
+ if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) {
+ // we need to tell the page we successfully received the message, but
+ // then bail without telling fxAccounts
+ this.injectData("message", { status: "login" });
+ // after a successful login we return to preferences
+ openPrefs();
+ return;
+ }
+ delete accountData.verifiedCanLinkAccount;
+
+ // Remember who it was so we can log out next time.
+ setPreviousAccountNameHash(newAccountEmail);
+
+ // A sync-specific hack - we want to ensure sync has been initialized
+ // before we set the signed-in user.
+ let xps = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+ xps.whenLoaded().then(() => {
+ updateDisplayedEmail(accountData);
+ return fxAccounts.setSignedInUser(accountData);
+ }).then(() => {
+ // If the user data is verified, we want it to immediately look like
+ // they are signed in without waiting for messages to bounce around.
+ if (accountData.verified) {
+ openPrefs();
+ }
+ this.injectData("message", { status: "login" });
+ // until we sort out a better UX, just leave the jelly page in place.
+ // If the account email is not yet verified, it will tell the user to
+ // go check their email, but then it will *not* change state after
+ // the verification completes (the browser will begin syncing, but
+ // won't notify the user). If the email has already been verified,
+ // the jelly will say "Welcome! You are successfully signed in as
+ // EMAIL", but it won't then say "syncing started".
+ }, (err) => this.injectData("message", { status: "error", error: err })
+ );
+ },
+
+ onCanLinkAccount: function(accountData) {
+ // We need to confirm a relink - see shouldAllowRelink for more
+ let ok = shouldAllowRelink(accountData.email);
+ this.injectData("message", { status: "can_link_account", data: { ok: ok } });
+ },
+
+ /**
+ * onSignOut handler erases the current user's session from the fxaccounts service
+ */
+ onSignOut: function () {
+ log("Received: 'sign_out'.");
+
+ fxAccounts.signOut().then(
+ () => this.injectData("message", { status: "sign_out" }),
+ (err) => this.injectData("message", { status: "error", error: err })
+ );
+ },
+
+ handleRemoteCommand: function (evt) {
+ log('command: ' + evt.detail.command);
+ let data = evt.detail.data;
+
+ switch (evt.detail.command) {
+ case "login":
+ this.onLogin(data);
+ break;
+ case "can_link_account":
+ this.onCanLinkAccount(data);
+ break;
+ case "sign_out":
+ this.onSignOut(data);
+ break;
+ default:
+ log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
+ break;
+ }
+ },
+
+ injectData: function (type, content) {
+ return fxAccounts.promiseAccountsSignUpURI().then(authUrl => {
+ let data = {
+ type: type,
+ content: content
+ };
+ this.iframe.contentWindow.postMessage(data, authUrl);
+ })
+ .catch(e => {
+ console.log("Failed to inject data", e);
+ setErrorPage("configError");
+ });
+ },
+};
+
+
+// Button onclick handlers
+function handleOldSync() {
+ let chromeWin = window
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync";
+ chromeWin.switchToTabHavingURI(url, true);
+}
+
+function getStarted() {
+ show("remote");
+}
+
+function retry() {
+ show("remote");
+ wrapper.retry();
+}
+
+function openPrefs() {
+ // Bug 1199303 calls for this tab to always be replaced with Preferences
+ // rather than it opening in a different tab.
+ window.location = "about:preferences#sync";
+}
+
+function init() {
+ fxAccounts.getSignedInUser().then(user => {
+ // tests in particular might cause the window to start closing before
+ // getSignedInUser has returned.
+ if (window.closed) {
+ return Promise.resolve();
+ }
+
+ updateDisplayedEmail(user);
+
+ // Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
+ // searchParams is empty.
+ let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
+ let action = urlParams.get(ACTION_URL_PARAM);
+ urlParams.delete(ACTION_URL_PARAM);
+
+ switch (action) {
+ case "signin":
+ if (user) {
+ // asking to sign-in when already signed in just shows manage.
+ show("stage", "manage");
+ } else {
+ return fxAccounts.promiseAccountsSignInURI().then(url => {
+ show("remote");
+ wrapper.init(url, urlParams);
+ });
+ }
+ break;
+ case "signup":
+ if (user) {
+ // asking to sign-up when already signed in just shows manage.
+ show("stage", "manage");
+ } else {
+ return fxAccounts.promiseAccountsSignUpURI().then(url => {
+ show("remote");
+ wrapper.init(url, urlParams);
+ });
+ }
+ break;
+ case "reauth":
+ // ideally we would only show this when we know the user is in a
+ // "must reauthenticate" state - but we don't.
+ // As the email address will be included in the URL returned from
+ // promiseAccountsForceSigninURI, just always show it.
+ return fxAccounts.promiseAccountsForceSigninURI().then(url => {
+ show("remote");
+ wrapper.init(url, urlParams);
+ });
+ default:
+ // No action specified.
+ if (user) {
+ show("stage", "manage");
+ } else {
+ // Attempt a migration if enabled or show the introductory page
+ // otherwise.
+ return migrateToDevEdition(urlParams).then(migrated => {
+ if (!migrated) {
+ show("stage", "intro");
+ // load the remote frame in the background
+ return fxAccounts.promiseAccountsSignUpURI().then(uri =>
+ wrapper.init(uri, urlParams));
+ }
+ return Promise.resolve();
+ });
+ }
+ break;
+ }
+ return Promise.resolve();
+ }).catch(err => {
+ console.log("Configuration or sign in error", err);
+ setErrorPage("configError");
+ });
+}
+
+function setErrorPage(errorType) {
+ show("stage", errorType);
+}
+
+// Causes the "top-level" element with |id| to be shown - all other top-level
+// elements are hidden. Optionally, ensures that only 1 "second-level" element
+// inside the top-level one is shown.
+function show(id, childId) {
+ // top-level items are either <div> or <iframe>
+ let allTop = document.querySelectorAll("body > div, iframe");
+ for (let elt of allTop) {
+ if (elt.getAttribute("id") == id) {
+ elt.style.display = 'block';
+ } else {
+ elt.style.display = 'none';
+ }
+ }
+ if (childId) {
+ // child items are all <div>
+ let allSecond = document.querySelectorAll("#" + id + " > div");
+ for (let elt of allSecond) {
+ if (elt.getAttribute("id") == childId) {
+ elt.style.display = 'block';
+ } else {
+ elt.style.display = 'none';
+ }
+ }
+ }
+}
+
+// Migrate sync data from the default profile to the dev-edition profile.
+// Returns a promise of a true value if migration succeeded, or false if it
+// failed.
+function migrateToDevEdition(urlParams) {
+ let defaultProfilePath;
+ try {
+ defaultProfilePath = window.getDefaultProfilePath();
+ } catch (e) {} // no default profile.
+ let migrateSyncCreds = false;
+ if (defaultProfilePath) {
+ try {
+ migrateSyncCreds = Services.prefs.getBoolPref("identity.fxaccounts.migrateToDevEdition");
+ } catch (e) {}
+ }
+
+ if (!migrateSyncCreds) {
+ return Promise.resolve(false);
+ }
+
+ Cu.import("resource://gre/modules/osfile.jsm");
+ let fxAccountsStorage = OS.Path.join(defaultProfilePath, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
+ return OS.File.read(fxAccountsStorage, { encoding: "utf-8" }).then(text => {
+ let accountData = JSON.parse(text).accountData;
+ updateDisplayedEmail(accountData);
+ return fxAccounts.setSignedInUser(accountData);
+ }).then(() => {
+ return fxAccounts.promiseAccountsForceSigninURI().then(url => {
+ show("remote");
+ wrapper.init(url, urlParams);
+ });
+ }).then(null, error => {
+ log("Failed to migrate FX Account: " + error);
+ show("stage", "intro");
+ // load the remote frame in the background
+ fxAccounts.promiseAccountsSignUpURI().then(uri => {
+ wrapper.init(uri, urlParams)
+ }).catch(e => {
+ console.log("Failed to load signup page", e);
+ setErrorPage("configError");
+ });
+ }).then(() => {
+ // Reset the pref after migration.
+ Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false);
+ return true;
+ }).then(null, err => {
+ Cu.reportError("Failed to reset the migrateToDevEdition pref: " + err);
+ return false;
+ });
+}
+
+// Helper function that returns the path of the default profile on disk. Will be
+// overridden in tests.
+function getDefaultProfilePath() {
+ let defaultProfile = Cc["@mozilla.org/toolkit/profile-service;1"]
+ .getService(Ci.nsIToolkitProfileService)
+ .defaultProfile;
+ return defaultProfile.rootDir.path;
+}
+
+document.addEventListener("DOMContentLoaded", function onload() {
+ document.removeEventListener("DOMContentLoaded", onload, true);
+ init();
+ var buttonGetStarted = document.getElementById('buttonGetStarted');
+ buttonGetStarted.addEventListener('click', getStarted);
+
+ var buttonRetry = document.getElementById('buttonRetry');
+ buttonRetry.addEventListener('click', retry);
+
+ var oldsync = document.getElementById('oldsync');
+ oldsync.addEventListener('click', handleOldSync);
+
+ var buttonOpenPrefs = document.getElementById('buttonOpenPrefs')
+ buttonOpenPrefs.addEventListener('click', openPrefs);
+}, true);
+
+function initObservers() {
+ function observe(subject, topic, data) {
+ log("about:accounts observed " + topic);
+ if (topic == fxAccountsCommon.ONLOGOUT_NOTIFICATION) {
+ // All about:account windows get changed to action=signin on logout.
+ window.location = "about:accounts?action=signin";
+ return;
+ }
+
+ // must be onverified - we want to open preferences.
+ openPrefs();
+ }
+
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.addObserver(observe, topic, false);
+ }
+ window.addEventListener("unload", function(event) {
+ log("about:accounts unloading")
+ for (let topic of OBSERVER_TOPICS) {
+ Services.obs.removeObserver(observe, topic);
+ }
+ });
+}
+initObservers();
diff --git a/browser/base/content/aboutaccounts/aboutaccounts.xhtml b/browser/base/content/aboutaccounts/aboutaccounts.xhtml
new file mode 100644
index 000000000..475f0e86f
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.xhtml
@@ -0,0 +1,112 @@
+<?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/. -->
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % aboutAccountsDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
+ %aboutAccountsDTD;
+ <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
+ %syncBrandDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
+ <head>
+ <title>&syncBrand.fullName.label;</title>
+ <meta name="viewport" content="width=device-width"/>
+
+
+ <link rel="icon" type="image/png" id="favicon"
+ href="chrome://branding/content/icon32.png"/>
+ <link rel="stylesheet"
+ href="chrome://browser/content/aboutaccounts/normalize.css"
+ type="text/css" />
+ <link rel="stylesheet"
+ href="chrome://browser/content/aboutaccounts/main.css"
+ type="text/css" />
+ <link rel="stylesheet"
+ href="chrome://browser/content/aboutaccounts/aboutaccounts.css"
+ type="text/css" />
+ </head>
+ <body>
+ <div id="stage">
+
+ <div id="manage">
+ <header>
+ <h1>&aboutAccounts.connected;</h1>
+ <div id="email"></div>
+ </header>
+
+ <section>
+ <div class="graphic graphic-sync-intro"> </div>
+
+ <div class="button-row">
+ <button id="buttonOpenPrefs" class="button" href="#" tabindex="0">&aboutAccountsConfig.syncPreferences.label;</button>
+ </div>
+ </section>
+ </div>
+
+ <div id="intro">
+ <header>
+ <h1>&aboutAccounts.welcome;</h1>
+ </header>
+
+ <section>
+ <div class="graphic graphic-sync-intro"> </div>
+
+ <div class="description">&aboutAccountsConfig.description;</div>
+
+ <div class="button-row">
+ <button id="buttonGetStarted" class="button" tabindex="1">&aboutAccountsConfig.startButton.label;</button>
+ </div>
+
+ <div class="links">
+ <button id="oldsync" tabindex="2">&aboutAccountsConfig.useOldSync.label;</button>
+ </div>
+ </section>
+ </div>
+
+ <div id="networkError">
+ <header>
+ <h1>&aboutAccounts.noConnection.title;</h1>
+ </header>
+
+ <section>
+ <div class="graphic graphic-sync-intro"> </div>
+
+ <div class="description">&aboutAccounts.noConnection.description;</div>
+
+ <div class="button-row">
+ <button id="buttonRetry" class="button" tabindex="3">&aboutAccounts.noConnection.retry;</button>
+ </div>
+ </section>
+ </div>
+
+ <div id="configError">
+ <header>
+ <h1>&aboutAccounts.badConfig.title;</h1>
+ </header>
+
+ <section>
+ <div class="graphic graphic-sync-intro"> </div>
+
+ <div class="description">&aboutAccounts.badConfig.description;</div>
+
+ </section>
+ </div>
+
+ </div>
+
+ <iframe mozframetype="content" id="remote" />
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://browser/content/utilityOverlay.js"/>
+ <script type="text/javascript;version=1.8"
+ src="chrome://browser/content/aboutaccounts/aboutaccounts.js" />
+ </body>
+</html>
diff --git a/browser/base/content/aboutaccounts/images/fox.png b/browser/base/content/aboutaccounts/images/fox.png
new file mode 100644
index 000000000..83af78d6c
--- /dev/null
+++ b/browser/base/content/aboutaccounts/images/fox.png
Binary files differ
diff --git a/browser/base/content/aboutaccounts/images/graphic_sync_intro.png b/browser/base/content/aboutaccounts/images/graphic_sync_intro.png
new file mode 100644
index 000000000..ff5f482f0
--- /dev/null
+++ b/browser/base/content/aboutaccounts/images/graphic_sync_intro.png
Binary files differ
diff --git a/browser/base/content/aboutaccounts/images/graphic_sync_intro@2x.png b/browser/base/content/aboutaccounts/images/graphic_sync_intro@2x.png
new file mode 100644
index 000000000..89fda0681
--- /dev/null
+++ b/browser/base/content/aboutaccounts/images/graphic_sync_intro@2x.png
Binary files differ
diff --git a/browser/base/content/aboutaccounts/main.css b/browser/base/content/aboutaccounts/main.css
new file mode 100644
index 000000000..8f4c3b34e
--- /dev/null
+++ b/browser/base/content/aboutaccounts/main.css
@@ -0,0 +1,166 @@
+*,
+*:before,
+*:after {
+ box-sizing: border-box;
+}
+
+html {
+ background-color: #F2F2F2;
+ height: 100%;
+}
+
+body {
+ color: #424f59;
+ font: message-box;
+ font-size: 14px;
+ height: 100%;
+}
+
+a {
+ color: #0095dd;
+ cursor: pointer; /* Use the correct cursor for anchors without an href */
+}
+
+a:active {
+ outline: none;
+}
+
+a:focus {
+ outline: 1px dotted #0095dd;
+}
+
+
+a.no-underline {
+ text-decoration: none;
+}
+
+#stage {
+ background:#fff;
+ border-radius: 5px;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.25);
+ margin: 0 auto;
+ min-height: 300px;
+ padding: 60px 40px 40px 40px;
+ position: relative;
+ text-align: center;
+ top: 80px;
+ width: 420px;
+}
+
+header h1
+{
+ font-size: 24px;
+ font-weight: 200;
+ line-height: 1em;
+}
+
+#intro header h1 {
+ margin: 0 0 32px 0;
+}
+
+#manage header h1 {
+ margin: 0 0 12px 0;
+}
+
+#manage header #email {
+ margin-bottom: 23px;
+ color: rgb(138, 155, 168);
+ font-size: 19px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.description {
+ font-size: 18px;
+}
+
+.button-row {
+ margin-top: 45px;
+ margin-bottom:20px;
+}
+
+.button-row button,
+.button-row a.button {
+ background: #0095dd;
+ border: none;
+ border-radius: 5px;
+ color: #FFFFFF;
+ cursor: pointer;
+ font-size: 24px;
+ padding: 15px 0;
+ transition-duration: 150ms;
+ transition-property: background-color;
+ width: 100%;
+}
+
+.button-row a.button {
+ display: inline-block;
+ text-decoration: none;
+}
+
+.button-row a.button:active,
+.button-row a.button:hover,
+.button-row a.button:focus,
+.button-row button:active,
+.button-row button:hover,
+.button-row button:focus {
+ background: #08c;
+}
+
+
+.graphic-sync-intro {
+ background-image: url(images/graphic_sync_intro.png);
+ background-repeat: no-repeat;
+ background-size: 150px 195px;
+ height: 195px;
+ margin: 0 auto;
+ overflow: hidden;
+ text-indent: 100%;
+ white-space: nowrap;
+ width: 150px;
+}
+
+.description,
+.button-row {
+ margin-top: 30px;
+}
+
+.links {
+ margin: 20px 0;
+}
+
+@media only screen and (max-width: 500px) {
+ html {
+ background: #fff;
+ }
+
+ #stage {
+ box-shadow: none;
+ margin: 30px auto 0 auto;
+ min-height: none;
+ min-width: 320px;
+ padding: 0 10px;
+ width: 100%;
+ }
+
+ .button-row {
+ margin-top: 20px;
+ }
+
+ .button-row button,
+ .button-row a.button {
+ padding: 10px 0;
+ }
+
+}
+
+/* Retina */
+@media
+only screen and (min-device-pixel-ratio: 2),
+only screen and ( min-resolution: 192dpi),
+only screen and ( min-resolution: 2dppx) {
+ .graphic-sync-intro {
+ background-image: url(images/graphic_sync_intro@2x.png);
+ }
+}
diff --git a/browser/base/content/aboutaccounts/normalize.css b/browser/base/content/aboutaccounts/normalize.css
new file mode 100644
index 000000000..c02ab25de
--- /dev/null
+++ b/browser/base/content/aboutaccounts/normalize.css
@@ -0,0 +1,402 @@
+/*! normalize.css v2.1.3 | MIT License | git.io/normalize */
+
+/* ==========================================================================
+ HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined in IE 8/9.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * Correct `inline-block` display not defined in IE 8/9.
+ */
+
+audio,
+canvas,
+video {
+ display: inline-block;
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9.
+ * Hide the `template` element in IE, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* ==========================================================================
+ Base
+ ========================================================================== */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background: transparent;
+}
+
+/**
+ * Address `outline` inconsistency between Chrome and other browsers.
+ */
+
+a:focus {
+ outline: thin dotted;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* ==========================================================================
+ Typography
+ ========================================================================== */
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari 5, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9, Safari 5, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari 5 and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Correct font family set oddly in Safari 5 and Chrome.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, serif;
+ font-size: 1em;
+}
+
+/**
+ * Improve readability of pre-formatted text in all browsers.
+ */
+
+pre {
+ white-space: pre-wrap;
+}
+
+/**
+ * Set consistent quote types.
+ */
+
+q {
+ quotes: "\201C" "\201D" "\2018" "\2019";
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* ==========================================================================
+ Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow displayed oddly in IE 9.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* ==========================================================================
+ Figures
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari 5.
+ */
+
+figure {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Forms
+ ========================================================================== */
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * 1. Correct font family not being inherited in all browsers.
+ * 2. Correct font size not being inherited in all browsers.
+ * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.
+ */
+
+button,
+input,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+button,
+input {
+ line-height: normal;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
+ * Correct `select` style inheritance in Firefox 4+ and Opera.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome.
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ box-sizing: content-box; /* 2 */
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari 5 and Chrome
+ * on OS X.
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * 1. Remove default vertical scrollbar in IE 8/9.
+ * 2. Improve readability and alignment in all browsers.
+ */
+
+textarea {
+ overflow: auto; /* 1 */
+ vertical-align: top; /* 2 */
+}
+
+/* ==========================================================================
+ Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/browser/base/content/abouthealthreport/abouthealth.css b/browser/base/content/abouthealthreport/abouthealth.css
new file mode 100644
index 000000000..3dd40fc24
--- /dev/null
+++ b/browser/base/content/abouthealthreport/abouthealth.css
@@ -0,0 +1,15 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+html, body {
+ height: 100%;
+}
+
+#remote-report {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ display: flex;
+}
diff --git a/browser/base/content/abouthealthreport/abouthealth.js b/browser/base/content/abouthealthreport/abouthealth.js
new file mode 100644
index 000000000..66cbe16f5
--- /dev/null
+++ b/browser/base/content/abouthealthreport/abouthealth.js
@@ -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";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const prefs = new Preferences("datareporting.healthreport.");
+
+const PREF_UNIFIED = "toolkit.telemetry.unified";
+const PREF_REPORTING_URL = "datareporting.healthreport.about.reportUrl";
+
+var healthReportWrapper = {
+ init: function () {
+ let iframe = document.getElementById("remote-report");
+ iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
+ iframe.src = this._getReportURI().spec;
+ prefs.observe("uploadEnabled", this.updatePrefState, healthReportWrapper);
+ },
+
+ uninit: function () {
+ prefs.ignore("uploadEnabled", this.updatePrefState, healthReportWrapper);
+ },
+
+ _getReportURI: function () {
+ let url = Services.urlFormatter.formatURLPref(PREF_REPORTING_URL);
+ return Services.io.newURI(url, null, null);
+ },
+
+ setDataSubmission: function (enabled) {
+ MozSelfSupport.healthReportDataSubmissionEnabled = enabled;
+ this.updatePrefState();
+ },
+
+ updatePrefState: function () {
+ try {
+ let prefs = {
+ enabled: MozSelfSupport.healthReportDataSubmissionEnabled,
+ };
+ healthReportWrapper.injectData("prefs", prefs);
+ }
+ catch (ex) {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PREFS_FAILED);
+ }
+ },
+
+ sendTelemetryPingList: function () {
+ console.log("AboutHealthReport: Collecting Telemetry ping list.");
+ MozSelfSupport.getTelemetryPingList().then((list) => {
+ console.log("AboutHealthReport: Sending Telemetry ping list.");
+ this.injectData("telemetry-ping-list", list);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting ping list failed: " + ex);
+ });
+ },
+
+ sendTelemetryPingData: function (pingId) {
+ console.log("AboutHealthReport: Collecting Telemetry ping data.");
+ MozSelfSupport.getTelemetryPing(pingId).then((ping) => {
+ console.log("AboutHealthReport: Sending Telemetry ping data.");
+ this.injectData("telemetry-ping-data", {
+ id: pingId,
+ pingData: ping,
+ });
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Loading ping data failed: " + ex);
+ this.injectData("telemetry-ping-data", {
+ id: pingId,
+ error: "error-generic",
+ });
+ });
+ },
+
+ sendCurrentEnvironment: function () {
+ console.log("AboutHealthReport: Sending Telemetry environment data.");
+ MozSelfSupport.getCurrentTelemetryEnvironment().then((environment) => {
+ this.injectData("telemetry-current-environment-data", environment);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting current environment data failed: " + ex);
+ });
+ },
+
+ sendCurrentPingData: function () {
+ console.log("AboutHealthReport: Sending current Telemetry ping data.");
+ MozSelfSupport.getCurrentTelemetrySubsessionPing().then((ping) => {
+ this.injectData("telemetry-current-ping-data", ping);
+ }).catch((ex) => {
+ console.log("AboutHealthReport: Collecting current ping data failed: " + ex);
+ });
+ },
+
+ injectData: function (type, content) {
+ let report = this._getReportURI();
+
+ // file URIs can't be used for targetOrigin, so we use "*" for this special case
+ // in all other cases, pass in the URL to the report so we properly restrict the message dispatch
+ let reportUrl = report.scheme == "file" ? "*" : report.spec;
+
+ let data = {
+ type: type,
+ content: content
+ }
+
+ let iframe = document.getElementById("remote-report");
+ iframe.contentWindow.postMessage(data, reportUrl);
+ },
+
+ handleRemoteCommand: function (evt) {
+ // Do an origin check to harden against the frame content being loaded from unexpected locations.
+ let allowedPrincipal = Services.scriptSecurityManager.getCodebasePrincipal(this._getReportURI());
+ let targetPrincipal = evt.target.nodePrincipal;
+ if (!allowedPrincipal.equals(targetPrincipal)) {
+ Cu.reportError(`Origin check failed for message "${evt.detail.command}": ` +
+ `target origin is "${targetPrincipal.origin}", expected "${allowedPrincipal.origin}"`);
+ return;
+ }
+
+ switch (evt.detail.command) {
+ case "DisableDataSubmission":
+ this.setDataSubmission(false);
+ break;
+ case "EnableDataSubmission":
+ this.setDataSubmission(true);
+ break;
+ case "RequestCurrentPrefs":
+ this.updatePrefState();
+ break;
+ case "RequestTelemetryPingList":
+ this.sendTelemetryPingList();
+ break;
+ case "RequestTelemetryPingData":
+ this.sendTelemetryPingData(evt.detail.id);
+ break;
+ case "RequestCurrentEnvironment":
+ this.sendCurrentEnvironment();
+ break;
+ case "RequestCurrentPingData":
+ this.sendCurrentPingData();
+ break;
+ default:
+ Cu.reportError("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
+ break;
+ }
+ },
+
+ initRemotePage: function () {
+ let iframe = document.getElementById("remote-report").contentDocument;
+ iframe.addEventListener("RemoteHealthReportCommand",
+ function onCommand(e) { healthReportWrapper.handleRemoteCommand(e); },
+ false);
+ healthReportWrapper.updatePrefState();
+ },
+
+ // error handling
+ ERROR_INIT_FAILED: 1,
+ ERROR_PAYLOAD_FAILED: 2,
+ ERROR_PREFS_FAILED: 3,
+
+ reportFailure: function (error) {
+ let details = {
+ errorType: error,
+ }
+ healthReportWrapper.injectData("error", details);
+ },
+
+ handleInitFailure: function () {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED);
+ },
+
+ handlePayloadFailure: function () {
+ healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED);
+ },
+}
+
+window.addEventListener("load", function () { healthReportWrapper.init(); });
+window.addEventListener("unload", function () { healthReportWrapper.uninit(); });
diff --git a/browser/base/content/abouthealthreport/abouthealth.xhtml b/browser/base/content/abouthealthreport/abouthealth.xhtml
new file mode 100644
index 000000000..464635788
--- /dev/null
+++ b/browser/base/content/abouthealthreport/abouthealth.xhtml
@@ -0,0 +1,31 @@
+<?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/. -->
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % securityPrefsDTD SYSTEM "chrome://browser/locale/preferences/security.dtd">
+ %securityPrefsDTD;
+ <!ENTITY % aboutHealthReportDTD SYSTEM "chrome://browser/locale/aboutHealthReport.dtd">
+ %aboutHealthReportDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&abouthealth.pagetitle;</title>
+ <link rel="icon" type="image/png" id="favicon"
+ href="chrome://branding/content/icon32.png"/>
+ <link rel="stylesheet"
+ href="chrome://browser/content/abouthealthreport/abouthealth.css"
+ type="text/css" />
+ <script type="text/javascript;version=1.8"
+ src="chrome://browser/content/abouthealthreport/abouthealth.js" />
+ </head>
+ <body>
+ <iframe id="remote-report"/>
+ </body>
+</html>
+
diff --git a/browser/base/content/abouthome/aboutHome.css b/browser/base/content/abouthome/aboutHome.css
new file mode 100644
index 000000000..c0b02e257
--- /dev/null
+++ b/browser/base/content/abouthome/aboutHome.css
@@ -0,0 +1,454 @@
+%if 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/. */
+%endif
+
+html {
+ font: message-box;
+ font-size: 100%;
+ background-color: hsl(0,0%,95%);
+ color: #000;
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ width: 100%;
+ height: 100%;
+}
+
+input,
+button {
+ font-size: inherit;
+ font-family: inherit;
+}
+
+a {
+ color: -moz-nativehyperlinktext;
+ text-decoration: none;
+}
+
+.spacer {
+ -moz-box-flex: 1;
+}
+
+#topSection {
+ text-align: center;
+}
+
+#brandLogo {
+ height: 192px;
+ width: 192px;
+ margin: 22px auto 31px;
+ background-image: url("chrome://branding/content/about-logo.png");
+ background-size: 192px auto;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+#searchIconAndTextContainer,
+#snippets {
+ width: 470px;
+}
+
+#searchIconAndTextContainer {
+ display: -moz-box;
+ height: 36px;
+ position: relative;
+}
+
+#searchIcon {
+ border: 1px transparent;
+ padding: 0;
+ margin: 0;
+ width: 36px;
+ height: 36px;
+ background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat;
+ position: absolute;
+}
+
+#searchText {
+ margin-left: 0;
+ -moz-box-flex: 1;
+ padding-top: 6px;
+ padding-bottom: 6px;
+ padding-inline-start: 34px;
+ padding-inline-end: 8px;
+ background: hsla(0,0%,100%,.9) padding-box;
+ border: 1px solid;
+ border-radius: 2px 0 0 2px;
+ border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+ box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
+ 0 0 2px hsla(210,65%,9%,.1) inset,
+ 0 1px 0 hsla(0,0%,100%,.2);
+ color: inherit;
+ unicode-bidi: plaintext;
+}
+
+#searchText:dir(rtl) {
+ border-radius: 0 2px 2px 0;
+}
+
+#searchText[aria-expanded="true"] {
+ border-radius: 2px 0 0 0;
+}
+
+#searchText[aria-expanded="true"]:dir(rtl) {
+ border-radius: 0 2px 0 0;
+}
+
+#searchText[keepfocus],
+#searchText:focus,
+#searchText[autofocus] {
+ border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6);
+}
+
+#searchSubmit {
+ margin-inline-start: -1px;
+ color: transparent;
+ background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
+ padding: 0;
+ border: 1px solid;
+ border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+ border-radius: 0 2px 2px 0;
+ border-inline-start: 1px solid transparent;
+ box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
+ 0 1px 0 hsla(0,0%,100%,.2);
+ cursor: pointer;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 150ms;
+ width: 50px;
+}
+
+#searchSubmit:dir(rtl) {
+ border-radius: 2px 0 0 2px;
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl"), linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1));
+}
+
+#searchText:focus + #searchSubmit,
+#searchText[keepfocus] + #searchSubmit,
+#searchText + #searchSubmit:hover,
+#searchText[autofocus] + #searchSubmit {
+ border-color: #59b5fc #45a3e7 #3294d5;
+}
+
+#searchText:focus + #searchSubmit,
+#searchText[keepfocus] + #searchSubmit,
+#searchText[autofocus] + #searchSubmit {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#4cb1ff, #1793e5);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
+ 0 0 0 1px hsla(0,0%,100%,.1) inset,
+ 0 1px 0 hsla(210,54%,20%,.03);
+}
+
+#searchText:focus + #searchSubmit:dir(rtl),
+#searchText[keepfocus] + #searchSubmit:dir(rtl),
+#searchText[autofocus] + #searchSubmit:dir(rtl) {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl-inverted"), linear-gradient(#4cb1ff, #1793e5);
+}
+
+#searchText + #searchSubmit:hover {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#66bdff, #0d9eff);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
+ 0 0 0 1px hsla(0,0%,100%,.1) inset,
+ 0 1px 0 hsla(210,54%,20%,.03),
+ 0 0 4px hsla(206,100%,20%,.2);
+}
+
+#searchText + #searchSubmit:dir(rtl):hover {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl-inverted"), linear-gradient(#66bdff, #0d9eff);
+}
+
+#searchText + #searchSubmit:hover:active {
+ box-shadow: 0 1px 1px hsla(211,79%,6%,.1) inset,
+ 0 0 1px hsla(211,79%,6%,.2) inset;
+ transition-duration: 0ms;
+}
+
+#defaultSnippet1,
+#defaultSnippet2,
+#rightsSnippet {
+ display: block;
+ min-height: 38px;
+ background: 0 center no-repeat;
+ padding: 6px 0;
+ padding-inline-start: 49px;
+}
+
+#rightsSnippet[hidden] {
+ display: none;
+}
+
+#defaultSnippet1:dir(rtl),
+#defaultSnippet2:dir(rtl),
+#rightsSnippet:dir(rtl) {
+ background-position: right 0 center;
+}
+
+#defaultSnippet1 {
+ background-image: url("chrome://browser/content/abouthome/snippet1.png");
+}
+
+#defaultSnippet2 {
+ background-image: url("chrome://browser/content/abouthome/snippet2.png");
+}
+
+#snippets {
+ display: inline-block;
+ text-align: start;
+ margin: 12px 0;
+ color: #3c3c3c;
+ font-size: 75%;
+ /* 12px is the computed font size, 15px the computed line height of the snippets
+ with Segoe UI on a default Windows 7 setup. The 15/12 multiplier approximately
+ converts em from units of font-size to units of line-height. The goal is to
+ preset the height of a three-line snippet to avoid visual moving/flickering as
+ the snippets load. */
+ min-height: calc(15/12 * 3em);
+}
+
+#launcher {
+ display: -moz-box;
+ -moz-box-align: center;
+ -moz-box-pack: center;
+ width: 100%;
+ background-color: hsla(0,0%,0%,.03);
+ border-top: 1px solid hsla(0,0%,0%,.03);
+ box-shadow: 0 1px 2px hsla(0,0%,0%,.02) inset,
+ 0 -1px 0 hsla(0,0%,100%,.25);
+}
+
+#launcher:not([session]),
+body[narrow] #launcher[session] {
+ display: block; /* display separator and restore button on separate lines */
+ text-align: center;
+ white-space: nowrap; /* prevent navigational buttons from wrapping */
+}
+
+.launchButton {
+ display: -moz-box;
+ -moz-box-orient: vertical;
+ margin: 16px 1px;
+ padding: 14px 6px;
+ min-width: 88px;
+ max-width: 176px;
+ max-height: 85px;
+ vertical-align: top;
+ white-space: normal;
+ background: transparent padding-box;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ color: #525c66;
+ font-size: 75%;
+ cursor: pointer;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 150ms;
+}
+
+body[narrow] #launcher[session] > .launchButton {
+ margin: 4px 1px;
+}
+
+.launchButton:hover {
+ background-color: hsla(211,79%,6%,.03);
+ border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+}
+
+.launchButton:hover:active {
+ background-image: linear-gradient(hsla(211,79%,6%,.02), hsla(211,79%,6%,.05));
+ border-color: hsla(210,54%,20%,.2) hsla(210,54%,20%,.23) hsla(210,54%,20%,.25);
+ box-shadow: 0 1px 1px hsla(211,79%,6%,.05) inset,
+ 0 0 1px hsla(211,79%,6%,.1) inset;
+ transition-duration: 0ms;
+}
+
+.launchButton[hidden],
+#launcher:not([session]) > #restorePreviousSessionSeparator,
+#launcher:not([session]) > #restorePreviousSession {
+ display: none;
+}
+
+#restorePreviousSessionSeparator {
+ width: 3px;
+ height: 116px;
+ margin: 0 10px;
+ background-image: linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)),
+ linear-gradient(hsla(211,79%,6%,0), hsla(211,79%,6%,.2), hsla(211,79%,6%,0)),
+ linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0));
+ background-position: left top, center, right bottom;
+ background-size: 1px auto;
+ background-repeat: no-repeat;
+}
+
+body[narrow] #restorePreviousSessionSeparator {
+ margin: 0 auto;
+ width: 512px;
+ height: 3px;
+ background-image: linear-gradient(to right, hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)),
+ linear-gradient(to right, hsla(211,79%,6%,0), hsla(211,79%,6%,.2), hsla(211,79%,6%,0)),
+ linear-gradient(to right, hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0));
+ background-size: auto 1px;
+}
+
+#restorePreviousSession {
+ max-width: none;
+ font-size: 90%;
+}
+
+body[narrow] #restorePreviousSession {
+ font-size: 80%;
+}
+
+.launchButton::before {
+ display: block;
+ width: 32px;
+ height: 32px;
+ margin: 0 auto 6px;
+ line-height: 0; /* remove extra vertical space due to non-zero font-size */
+}
+
+#downloads::before {
+ content: url("chrome://browser/content/abouthome/downloads.png");
+}
+
+#bookmarks::before {
+ content: url("chrome://browser/content/abouthome/bookmarks.png");
+}
+
+#history::before {
+ content: url("chrome://browser/content/abouthome/history.png");
+}
+
+#addons::before {
+ content: url("chrome://browser/content/abouthome/addons.png");
+}
+
+#sync::before {
+ content: url("chrome://browser/content/abouthome/sync.png");
+}
+
+#settings::before {
+ content: url("chrome://browser/content/abouthome/settings.png");
+}
+
+#restorePreviousSession::before {
+ content: url("chrome://browser/content/abouthome/restore-large.png");
+ height: 48px;
+ width: 48px;
+ display: inline-block; /* display on same line as text label */
+ vertical-align: middle;
+ margin-bottom: 0;
+ margin-inline-end: 8px;
+}
+
+#restorePreviousSession:dir(rtl)::before {
+ transform: scaleX(-1);
+}
+
+body[narrow] #restorePreviousSession::before {
+ content: url("chrome://browser/content/abouthome/restore.png");
+ height: 32px;
+ width: 32px;
+}
+
+#aboutMozilla {
+ display: block;
+ position: relative; /* pin wordmark to edge of document, not of viewport */
+ -moz-box-ordinal-group: 0;
+ opacity: .5;
+ transition: opacity 150ms;
+}
+
+#aboutMozilla:hover {
+ opacity: 1;
+}
+
+#aboutMozilla::before {
+ content: url("chrome://browser/content/abouthome/mozilla.png");
+ display: block;
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ width: 69px;
+ height: 19px;
+}
+
+/* [HiDPI]
+ * At resolutions above 1dppx, prefer downscaling the 2x Retina graphics
+ * rather than upscaling the original-size ones (bug 818940).
+ */
+@media not all and (max-resolution: 1dppx) {
+ #brandLogo {
+ background-image: url("chrome://branding/content/about-logo@2x.png");
+ }
+
+ #defaultSnippet1,
+ #defaultSnippet2,
+ #rightsSnippet {
+ background-size: 40px;
+ }
+
+ #defaultSnippet1 {
+ background-image: url("chrome://browser/content/abouthome/snippet1@2x.png");
+ }
+
+ #defaultSnippet2 {
+ background-image: url("chrome://browser/content/abouthome/snippet2@2x.png");
+ }
+
+ .launchButton::before,
+ #aboutMozilla::before {
+ transform: scale(.5);
+ transform-origin: 0 0;
+ }
+
+ .launchButton:dir(rtl)::before,
+ #aboutMozilla:dir(rtl)::before {
+ transform: scale(.5) translateX(32px);
+ }
+
+ #downloads::before {
+ content: url("chrome://browser/content/abouthome/downloads@2x.png");
+ }
+
+ #bookmarks::before {
+ content: url("chrome://browser/content/abouthome/bookmarks@2x.png");
+ }
+
+ #history::before {
+ content: url("chrome://browser/content/abouthome/history@2x.png");
+ }
+
+ #addons::before {
+ content: url("chrome://browser/content/abouthome/addons@2x.png");
+ }
+
+ #sync::before {
+ content: url("chrome://browser/content/abouthome/sync@2x.png");
+ }
+
+ #settings::before {
+ content: url("chrome://browser/content/abouthome/settings@2x.png");
+ }
+
+ #restorePreviousSession::before {
+ content: url("chrome://browser/content/abouthome/restore-large@2x.png");
+ }
+
+ body[narrow] #restorePreviousSession::before {
+ content: url("chrome://browser/content/abouthome/restore@2x.png");
+ }
+
+ #restorePreviousSession:dir(rtl)::before {
+ transform: scale(-0.5, 0.5) translateX(24px);
+ transform-origin: top center;
+ }
+
+ #aboutMozilla::before {
+ content: url("chrome://browser/content/abouthome/mozilla@2x.png");
+ }
+}
+
diff --git a/browser/base/content/abouthome/aboutHome.js b/browser/base/content/abouthome/aboutHome.js
new file mode 100644
index 000000000..50f3e01cd
--- /dev/null
+++ b/browser/base/content/abouthome/aboutHome.js
@@ -0,0 +1,398 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ../contentSearchUI.js */
+
+// The process of adding a new default snippet involves:
+// * add a new entity to aboutHome.dtd
+// * add a <span/> for it in aboutHome.xhtml
+// * add an entry here in the proper ordering (based on spans)
+// The <a/> part of the snippet will be linked to the corresponding url.
+const DEFAULT_SNIPPETS_URLS = [
+ "https://www.mozilla.org/firefox/features/?utm_source=snippet&utm_medium=snippet&utm_campaign=default+feature+snippet"
+, "https://addons.mozilla.org/firefox/?utm_source=snippet&utm_medium=snippet&utm_campaign=addons"
+];
+
+const SNIPPETS_UPDATE_INTERVAL_MS = 14400000; // 4 hours.
+
+// IndexedDB storage constants.
+const DATABASE_NAME = "abouthome";
+const DATABASE_VERSION = 1;
+const DATABASE_STORAGE = "persistent";
+const SNIPPETS_OBJECTSTORE_NAME = "snippets";
+var searchText;
+
+// This global tracks if the page has been set up before, to prevent double inits
+var gInitialized = false;
+var gObserver = new MutationObserver(function (mutations) {
+ for (let mutation of mutations) {
+ // The addition of the restore session button changes our width:
+ if (mutation.attributeName == "session") {
+ fitToWidth();
+ }
+ if (mutation.attributeName == "snippetsVersion") {
+ if (!gInitialized) {
+ ensureSnippetsMapThen(loadSnippets);
+ gInitialized = true;
+ }
+ return;
+ }
+ }
+});
+
+window.addEventListener("pageshow", function () {
+ // Delay search engine setup, cause browser.js::BrowserOnAboutPageLoad runs
+ // later and may use asynchronous getters.
+ window.gObserver.observe(document.documentElement, { attributes: true });
+ window.gObserver.observe(document.getElementById("launcher"), { attributes: true });
+ fitToWidth();
+ setupSearch();
+ window.addEventListener("resize", fitToWidth);
+
+ // Ask chrome to update snippets.
+ var event = new CustomEvent("AboutHomeLoad", {bubbles:true});
+ document.dispatchEvent(event);
+});
+
+window.addEventListener("pagehide", function() {
+ window.gObserver.disconnect();
+ window.removeEventListener("resize", fitToWidth);
+});
+
+window.addEventListener("keypress", ev => {
+ if (ev.defaultPrevented) {
+ return;
+ }
+
+ // don't focus the search-box on keypress if something other than the
+ // body or document element has focus - don't want to steal input from other elements
+ // Make an exception for <a> and <button> elements (and input[type=button|submit])
+ // which don't usefully take keypresses anyway.
+ // (except space, which is handled below)
+ if (document.activeElement && document.activeElement != document.body &&
+ document.activeElement != document.documentElement &&
+ !["a", "button"].includes(document.activeElement.localName) &&
+ !document.activeElement.matches("input:-moz-any([type=button],[type=submit])")) {
+ return;
+ }
+
+ let modifiers = ev.ctrlKey + ev.altKey + ev.metaKey;
+ // ignore Ctrl/Cmd/Alt, but not Shift
+ // also ignore Tab, Insert, PageUp, etc., and Space
+ if (modifiers != 0 || ev.charCode == 0 || ev.charCode == 32)
+ return;
+
+ searchText.focus();
+ // need to send the first keypress outside the search-box manually to it
+ searchText.value += ev.key;
+});
+
+// This object has the same interface as Map and is used to store and retrieve
+// the snippets data. It is lazily initialized by ensureSnippetsMapThen(), so
+// be sure its callback returned before trying to use it.
+var gSnippetsMap;
+var gSnippetsMapCallbacks = [];
+
+/**
+ * Ensure the snippets map is properly initialized.
+ *
+ * @param aCallback
+ * Invoked once the map has been initialized, gets the map as argument.
+ * @note Snippets should never directly manage the underlying storage, since
+ * it may change inadvertently.
+ */
+function ensureSnippetsMapThen(aCallback)
+{
+ if (gSnippetsMap) {
+ aCallback(gSnippetsMap);
+ return;
+ }
+
+ // Handle multiple requests during the async initialization.
+ gSnippetsMapCallbacks.push(aCallback);
+ if (gSnippetsMapCallbacks.length > 1) {
+ // We are already updating, the callbacks will be invoked when done.
+ return;
+ }
+
+ let invokeCallbacks = function () {
+ if (!gSnippetsMap) {
+ gSnippetsMap = Object.freeze(new Map());
+ }
+
+ for (let callback of gSnippetsMapCallbacks) {
+ callback(gSnippetsMap);
+ }
+ gSnippetsMapCallbacks.length = 0;
+ }
+
+ let openRequest = indexedDB.open(DATABASE_NAME, {version: DATABASE_VERSION,
+ storage: DATABASE_STORAGE});
+
+ openRequest.onerror = function (event) {
+ // Try to delete the old database so that we can start this process over
+ // next time.
+ indexedDB.deleteDatabase(DATABASE_NAME);
+ invokeCallbacks();
+ };
+
+ openRequest.onupgradeneeded = function (event) {
+ let db = event.target.result;
+ if (!db.objectStoreNames.contains(SNIPPETS_OBJECTSTORE_NAME)) {
+ db.createObjectStore(SNIPPETS_OBJECTSTORE_NAME);
+ }
+ }
+
+ openRequest.onsuccess = function (event) {
+ let db = event.target.result;
+
+ db.onerror = function (event) {
+ invokeCallbacks();
+ }
+
+ db.onversionchange = function (event) {
+ event.target.close();
+ invokeCallbacks();
+ }
+
+ let cache = new Map();
+ let cursorRequest;
+ try {
+ cursorRequest = db.transaction(SNIPPETS_OBJECTSTORE_NAME)
+ .objectStore(SNIPPETS_OBJECTSTORE_NAME).openCursor();
+ } catch (ex) {
+ console.error(ex);
+ invokeCallbacks();
+ return;
+ }
+
+ cursorRequest.onerror = function (event) {
+ invokeCallbacks();
+ }
+
+ cursorRequest.onsuccess = function(event) {
+ let cursor = event.target.result;
+
+ // Populate the cache from the persistent storage.
+ if (cursor) {
+ cache.set(cursor.key, cursor.value);
+ cursor.continue();
+ return;
+ }
+
+ // The cache has been filled up, create the snippets map.
+ gSnippetsMap = Object.freeze({
+ get: (aKey) => cache.get(aKey),
+ set: function (aKey, aValue) {
+ db.transaction(SNIPPETS_OBJECTSTORE_NAME, "readwrite")
+ .objectStore(SNIPPETS_OBJECTSTORE_NAME).put(aValue, aKey);
+ return cache.set(aKey, aValue);
+ },
+ has: (aKey) => cache.has(aKey),
+ delete: function (aKey) {
+ db.transaction(SNIPPETS_OBJECTSTORE_NAME, "readwrite")
+ .objectStore(SNIPPETS_OBJECTSTORE_NAME).delete(aKey);
+ return cache.delete(aKey);
+ },
+ clear: function () {
+ db.transaction(SNIPPETS_OBJECTSTORE_NAME, "readwrite")
+ .objectStore(SNIPPETS_OBJECTSTORE_NAME).clear();
+ return cache.clear();
+ },
+ get size() { return cache.size; },
+ });
+
+ setTimeout(invokeCallbacks, 0);
+ }
+ }
+}
+
+function onSearchSubmit(aEvent)
+{
+ gContentSearchController.search(aEvent);
+}
+
+
+var gContentSearchController;
+
+function setupSearch()
+{
+ // Set submit button label for when CSS background are disabled (e.g.
+ // high contrast mode).
+ document.getElementById("searchSubmit").value =
+ document.body.getAttribute("dir") == "ltr" ? "\u25B6" : "\u25C0";
+
+ // The "autofocus" attribute doesn't focus the form element
+ // immediately when the element is first drawn, so the
+ // attribute is also used for styling when the page first loads.
+ searchText = document.getElementById("searchText");
+ searchText.addEventListener("blur", function searchText_onBlur() {
+ searchText.removeEventListener("blur", searchText_onBlur);
+ searchText.removeAttribute("autofocus");
+ });
+
+ if (!gContentSearchController) {
+ gContentSearchController =
+ new ContentSearchUIController(searchText, searchText.parentNode,
+ "abouthome", "homepage");
+ }
+}
+
+/**
+ * Inform the test harness that we're done loading the page.
+ */
+function loadCompleted()
+{
+ var event = new CustomEvent("AboutHomeLoadSnippetsCompleted", {bubbles:true});
+ document.dispatchEvent(event);
+}
+
+/**
+ * Update the local snippets from the remote storage, then show them through
+ * showSnippets.
+ */
+function loadSnippets()
+{
+ if (!gSnippetsMap)
+ throw new Error("Snippets map has not properly been initialized");
+
+ // Allow tests to modify the snippets map before using it.
+ var event = new CustomEvent("AboutHomeLoadSnippets", {bubbles:true});
+ document.dispatchEvent(event);
+
+ // Check cached snippets version.
+ let cachedVersion = gSnippetsMap.get("snippets-cached-version") || 0;
+ let currentVersion = document.documentElement.getAttribute("snippetsVersion");
+ if (cachedVersion < currentVersion) {
+ // The cached snippets are old and unsupported, restart from scratch.
+ gSnippetsMap.clear();
+ }
+
+ // Check last snippets update.
+ let lastUpdate = gSnippetsMap.get("snippets-last-update");
+ let updateURL = document.documentElement.getAttribute("snippetsURL");
+ let shouldUpdate = !lastUpdate ||
+ Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS;
+ if (updateURL && shouldUpdate) {
+ // Try to update from network.
+ let xhr = new XMLHttpRequest();
+ xhr.timeout = 5000;
+ // Even if fetching should fail we don't want to spam the server, thus
+ // set the last update time regardless its results. Will retry tomorrow.
+ gSnippetsMap.set("snippets-last-update", Date.now());
+ xhr.onloadend = function (event) {
+ if (xhr.status == 200) {
+ gSnippetsMap.set("snippets", xhr.responseText);
+ gSnippetsMap.set("snippets-cached-version", currentVersion);
+ }
+ showSnippets();
+ loadCompleted();
+ };
+ try {
+ xhr.open("GET", updateURL, true);
+ xhr.send(null);
+ } catch (ex) {
+ showSnippets();
+ loadCompleted();
+ return;
+ }
+ } else {
+ showSnippets();
+ loadCompleted();
+ }
+}
+
+/**
+ * Shows locally cached remote snippets, or default ones when not available.
+ *
+ * @note: snippets should never invoke showSnippets(), or they may cause
+ * a "too much recursion" exception.
+ */
+var _snippetsShown = false;
+function showSnippets()
+{
+ let snippetsElt = document.getElementById("snippets");
+
+ // Show about:rights notification, if needed.
+ let showRights = document.documentElement.getAttribute("showKnowYourRights");
+ if (showRights) {
+ let rightsElt = document.getElementById("rightsSnippet");
+ let anchor = rightsElt.getElementsByTagName("a")[0];
+ anchor.href = "about:rights";
+ snippetsElt.appendChild(rightsElt);
+ rightsElt.removeAttribute("hidden");
+ return;
+ }
+
+ if (!gSnippetsMap)
+ throw new Error("Snippets map has not properly been initialized");
+ if (_snippetsShown) {
+ // There's something wrong with the remote snippets, just in case fall back
+ // to the default snippets.
+ showDefaultSnippets();
+ throw new Error("showSnippets should never be invoked multiple times");
+ }
+ _snippetsShown = true;
+
+ let snippets = gSnippetsMap.get("snippets");
+ // If there are remotely fetched snippets, try to to show them.
+ if (snippets) {
+ // Injecting snippets can throw if they're invalid XML.
+ try {
+ snippetsElt.innerHTML = snippets;
+ // Scripts injected by innerHTML are inactive, so we have to relocate them
+ // through DOM manipulation to activate their contents.
+ Array.forEach(snippetsElt.getElementsByTagName("script"), function(elt) {
+ let relocatedScript = document.createElement("script");
+ relocatedScript.type = "text/javascript;version=1.8";
+ relocatedScript.text = elt.text;
+ elt.parentNode.replaceChild(relocatedScript, elt);
+ });
+ return;
+ } catch (ex) {
+ // Bad content, continue to show default snippets.
+ }
+ }
+
+ showDefaultSnippets();
+}
+
+/**
+ * Clear snippets element contents and show default snippets.
+ */
+function showDefaultSnippets()
+{
+ // Clear eventual contents...
+ let snippetsElt = document.getElementById("snippets");
+ snippetsElt.innerHTML = "";
+
+ // ...then show default snippets.
+ let defaultSnippetsElt = document.getElementById("defaultSnippets");
+ let entries = defaultSnippetsElt.querySelectorAll("span");
+ // Choose a random snippet. Assume there is always at least one.
+ let randIndex = Math.floor(Math.random() * entries.length);
+ let entry = entries[randIndex];
+ // Inject url in the eventual link.
+ if (DEFAULT_SNIPPETS_URLS[randIndex]) {
+ let links = entry.getElementsByTagName("a");
+ // Default snippets can have only one link, otherwise something is messed
+ // up in the translation.
+ if (links.length == 1) {
+ links[0].href = DEFAULT_SNIPPETS_URLS[randIndex];
+ }
+ }
+ // Move the default snippet to the snippets element.
+ snippetsElt.appendChild(entry);
+}
+
+function fitToWidth() {
+ if (document.documentElement.scrollWidth > window.innerWidth) {
+ document.body.setAttribute("narrow", "true");
+ } else if (document.body.hasAttribute("narrow")) {
+ document.body.removeAttribute("narrow");
+ fitToWidth();
+ }
+}
diff --git a/browser/base/content/abouthome/aboutHome.xhtml b/browser/base/content/abouthome/aboutHome.xhtml
new file mode 100644
index 000000000..c288e732e
--- /dev/null
+++ b/browser/base/content/abouthome/aboutHome.xhtml
@@ -0,0 +1,79 @@
+<?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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd">
+ %aboutHomeDTD;
+ <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" >
+ %browserDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&abouthome.pageTitle;</title>
+
+ <link rel="icon" type="image/png" id="favicon"
+ href="chrome://branding/content/icon32.png"/>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/content/contentSearchUI.css"/>
+ <link rel="stylesheet" type="text/css" media="all" defer="defer"
+ href="chrome://browser/content/abouthome/aboutHome.css"/>
+
+ <script type="text/javascript;version=1.8"
+ src="chrome://browser/content/abouthome/aboutHome.js"/>
+ <script type="text/javascript;version=1.8"
+ src="chrome://browser/content/contentSearchUI.js"/>
+ </head>
+
+ <body dir="&locale.dir;">
+ <div class="spacer"/>
+ <div id="topSection">
+ <div id="brandLogo"></div>
+
+ <div id="searchIconAndTextContainer">
+ <div id="searchIcon"/>
+ <input type="text" name="q" value="" id="searchText" maxlength="256"
+ aria-label="&contentSearchInput.label;" autofocus="autofocus"/>
+ <input id="searchSubmit" type="button" onclick="onSearchSubmit(event)"
+ title="&contentSearchSubmit.tooltip;"/>
+ </div>
+
+ <div id="snippetContainer">
+ <div id="defaultSnippets" hidden="true">
+ <span id="defaultSnippet1">&abouthome.defaultSnippet1.v1;</span>
+ <span id="defaultSnippet2">&abouthome.defaultSnippet2.v1;</span>
+ </div>
+ <span id="rightsSnippet" hidden="true">&abouthome.rightsSnippet;</span>
+ <div id="snippets"/>
+ </div>
+ </div>
+ <div class="spacer"/>
+
+ <div id="launcher">
+ <button class="launchButton" id="downloads">&abouthome.downloadsButton.label;</button>
+ <button class="launchButton" id="bookmarks">&abouthome.bookmarksButton.label;</button>
+ <button class="launchButton" id="history">&abouthome.historyButton.label;</button>
+ <button class="launchButton" id="addons">&abouthome.addonsButton.label;</button>
+ <button class="launchButton" id="sync">&abouthome.syncButton.label;</button>
+#ifdef XP_WIN
+ <button class="launchButton" id="settings">&abouthome.preferencesButtonWin.label;</button>
+#else
+ <button class="launchButton" id="settings">&abouthome.preferencesButtonUnix.label;</button>
+#endif
+ <div id="restorePreviousSessionSeparator"/>
+ <button class="launchButton" id="restorePreviousSession">&historyRestoreLastSession.label;</button>
+ </div>
+
+ <a id="aboutMozilla" href="https://www.mozilla.org/about/?utm_source=about-home&amp;utm_medium=Referral"
+ aria-label="&abouthome.aboutMozilla.label;"/>
+ </body>
+</html>
diff --git a/browser/base/content/abouthome/addons.png b/browser/base/content/abouthome/addons.png
new file mode 100644
index 000000000..41519ce49
--- /dev/null
+++ b/browser/base/content/abouthome/addons.png
Binary files differ
diff --git a/browser/base/content/abouthome/addons@2x.png b/browser/base/content/abouthome/addons@2x.png
new file mode 100644
index 000000000..d4d04ee8c
--- /dev/null
+++ b/browser/base/content/abouthome/addons@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/bookmarks.png b/browser/base/content/abouthome/bookmarks.png
new file mode 100644
index 000000000..5c7e194a6
--- /dev/null
+++ b/browser/base/content/abouthome/bookmarks.png
Binary files differ
diff --git a/browser/base/content/abouthome/bookmarks@2x.png b/browser/base/content/abouthome/bookmarks@2x.png
new file mode 100644
index 000000000..7ede00744
--- /dev/null
+++ b/browser/base/content/abouthome/bookmarks@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/downloads.png b/browser/base/content/abouthome/downloads.png
new file mode 100644
index 000000000..3d4d10e7a
--- /dev/null
+++ b/browser/base/content/abouthome/downloads.png
Binary files differ
diff --git a/browser/base/content/abouthome/downloads@2x.png b/browser/base/content/abouthome/downloads@2x.png
new file mode 100644
index 000000000..d384a22c6
--- /dev/null
+++ b/browser/base/content/abouthome/downloads@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/history.png b/browser/base/content/abouthome/history.png
new file mode 100644
index 000000000..ae742b1aa
--- /dev/null
+++ b/browser/base/content/abouthome/history.png
Binary files differ
diff --git a/browser/base/content/abouthome/history@2x.png b/browser/base/content/abouthome/history@2x.png
new file mode 100644
index 000000000..696902e7c
--- /dev/null
+++ b/browser/base/content/abouthome/history@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/mozilla.png b/browser/base/content/abouthome/mozilla.png
new file mode 100644
index 000000000..f2c348d13
--- /dev/null
+++ b/browser/base/content/abouthome/mozilla.png
Binary files differ
diff --git a/browser/base/content/abouthome/mozilla@2x.png b/browser/base/content/abouthome/mozilla@2x.png
new file mode 100644
index 000000000..f8fc622d0
--- /dev/null
+++ b/browser/base/content/abouthome/mozilla@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/restore-large.png b/browser/base/content/abouthome/restore-large.png
new file mode 100644
index 000000000..ef593e6e1
--- /dev/null
+++ b/browser/base/content/abouthome/restore-large.png
Binary files differ
diff --git a/browser/base/content/abouthome/restore-large@2x.png b/browser/base/content/abouthome/restore-large@2x.png
new file mode 100644
index 000000000..d5c71d0b0
--- /dev/null
+++ b/browser/base/content/abouthome/restore-large@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/restore.png b/browser/base/content/abouthome/restore.png
new file mode 100644
index 000000000..5c3d6f437
--- /dev/null
+++ b/browser/base/content/abouthome/restore.png
Binary files differ
diff --git a/browser/base/content/abouthome/restore@2x.png b/browser/base/content/abouthome/restore@2x.png
new file mode 100644
index 000000000..5acb63052
--- /dev/null
+++ b/browser/base/content/abouthome/restore@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/settings.png b/browser/base/content/abouthome/settings.png
new file mode 100644
index 000000000..4b0c30990
--- /dev/null
+++ b/browser/base/content/abouthome/settings.png
Binary files differ
diff --git a/browser/base/content/abouthome/settings@2x.png b/browser/base/content/abouthome/settings@2x.png
new file mode 100644
index 000000000..c77cb9a92
--- /dev/null
+++ b/browser/base/content/abouthome/settings@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/snippet1.png b/browser/base/content/abouthome/snippet1.png
new file mode 100644
index 000000000..ce2ec55c2
--- /dev/null
+++ b/browser/base/content/abouthome/snippet1.png
Binary files differ
diff --git a/browser/base/content/abouthome/snippet1@2x.png b/browser/base/content/abouthome/snippet1@2x.png
new file mode 100644
index 000000000..f57cd0a82
--- /dev/null
+++ b/browser/base/content/abouthome/snippet1@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/snippet2.png b/browser/base/content/abouthome/snippet2.png
new file mode 100644
index 000000000..e0724fb6d
--- /dev/null
+++ b/browser/base/content/abouthome/snippet2.png
Binary files differ
diff --git a/browser/base/content/abouthome/snippet2@2x.png b/browser/base/content/abouthome/snippet2@2x.png
new file mode 100644
index 000000000..40577f52f
--- /dev/null
+++ b/browser/base/content/abouthome/snippet2@2x.png
Binary files differ
diff --git a/browser/base/content/abouthome/sync.png b/browser/base/content/abouthome/sync.png
new file mode 100644
index 000000000..11e40cc93
--- /dev/null
+++ b/browser/base/content/abouthome/sync.png
Binary files differ
diff --git a/browser/base/content/abouthome/sync@2x.png b/browser/base/content/abouthome/sync@2x.png
new file mode 100644
index 000000000..6354f5bf9
--- /dev/null
+++ b/browser/base/content/abouthome/sync@2x.png
Binary files differ
diff --git a/browser/base/content/baseMenuOverlay.xul b/browser/base/content/baseMenuOverlay.xul
new file mode 100644
index 000000000..da74ca077
--- /dev/null
+++ b/browser/base/content/baseMenuOverlay.xul
@@ -0,0 +1,118 @@
+<?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 overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % baseMenuOverlayDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd">
+%baseMenuOverlayDTD;
+]>
+<overlay id="baseMenuOverlay"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
+
+#ifdef XP_MACOSX
+<!-- nsMenuBarX hides these and uses them to build the Application menu.
+ When using Carbon widgets for Mac OS X widgets, some of these are not
+ used as they only apply to Cocoa widget builds. All version of Firefox
+ through Firefox 2 will use Carbon widgets. -->
+ <menupopup id="menu_ToolsPopup">
+ <menuitem id="menu_preferences" label="&preferencesCmdMac.label;" key="key_preferencesCmdMac" oncommand="openPreferences();"/>
+ <menuitem id="menu_mac_services" label="&servicesMenuMac.label;"/>
+ <menuitem id="menu_mac_hide_app" label="&hideThisAppCmdMac2.label;" key="key_hideThisAppCmdMac"/>
+ <menuitem id="menu_mac_hide_others" label="&hideOtherAppsCmdMac.label;" key="key_hideOtherAppsCmdMac"/>
+ <menuitem id="menu_mac_show_all" label="&showAllAppsCmdMac.label;"/>
+ </menupopup>
+<!-- Mac window menu -->
+#include ../../../toolkit/content/macWindowMenu.inc
+#endif
+
+#ifdef XP_WIN
+ <menu id="helpMenu"
+ label="&helpMenuWin.label;"
+ accesskey="&helpMenuWin.accesskey;">
+#else
+ <menu id="helpMenu"
+ label="&helpMenu.label;"
+ accesskey="&helpMenu.accesskey;">
+#endif
+ <menupopup id="menu_HelpPopup" onpopupshowing="buildHelpMenu();">
+ <menuitem id="menu_openHelp"
+ oncommand="openHelpLink('firefox-help')"
+ onclick="checkForMiddleClick(this, event);"
+ label="&productHelp2.label;"
+ accesskey="&productHelp2.accesskey;"
+#ifdef XP_MACOSX
+ key="key_openHelpMac"/>
+#else
+ />
+#endif
+ <menuitem id="menu_openTour"
+ oncommand="openTourPage();"
+ label="&helpShowTour2.label;"
+ accesskey="&helpShowTour2.accesskey;"/>
+ <menuitem id="menu_keyboardShortcuts"
+ oncommand="openHelpLink('keyboard-shortcuts')"
+ onclick="checkForMiddleClick(this, event);"
+ label="&helpKeyboardShortcuts.label;"
+ accesskey="&helpKeyboardShortcuts.accesskey;"/>
+#ifdef MOZ_TELEMETRY_REPORTING
+ <menuitem id="healthReport"
+ label="&healthReport2.label;"
+ accesskey="&healthReport2.accesskey;"
+ oncommand="openHealthReport()"
+ onclick="checkForMiddleClick(this, event);"/>
+#endif
+ <menuitem id="troubleShooting"
+ accesskey="&helpTroubleshootingInfo.accesskey;"
+ label="&helpTroubleshootingInfo.label;"
+ oncommand="openTroubleshootingPage()"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="feedbackPage"
+ accesskey="&helpFeedbackPage.accesskey;"
+ label="&helpFeedbackPage.label;"
+ oncommand="openFeedbackPage()"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="helpSafeMode"
+ accesskey="&helpSafeMode.accesskey;"
+ label="&helpSafeMode.label;"
+ stopaccesskey="&helpSafeMode.stop.accesskey;"
+ stoplabel="&helpSafeMode.stop.label;"
+ oncommand="safeModeRestart();"/>
+ <menuseparator id="aboutSeparator"/>
+ <menuitem id="aboutName"
+ accesskey="&aboutProduct2.accesskey;"
+ label="&aboutProduct2.label;"
+ oncommand="openAboutDialog();"/>
+ </menupopup>
+ </menu>
+
+ <keyset id="baseMenuKeyset">
+#ifdef XP_MACOSX
+ <key id="key_openHelpMac"
+ oncommand="openHelpLink('firefox-osxkey');"
+ key="&helpMac.commandkey;"
+ modifiers="accel"/>
+<!-- These are used to build the Application menu under Cocoa widgets -->
+ <key id="key_preferencesCmdMac"
+ key="&preferencesCmdMac.commandkey;"
+ modifiers="accel"/>
+ <key id="key_hideThisAppCmdMac"
+ key="&hideThisAppCmdMac2.commandkey;"
+ modifiers="accel"/>
+ <key id="key_hideOtherAppsCmdMac"
+ key="&hideOtherAppsCmdMac.commandkey;"
+ modifiers="accel,alt"/>
+#endif
+ </keyset>
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
+ <stringbundle id="bundle_browser_region" src="chrome://browser-region/locale/region.properties"/>
+ </stringbundleset>
+</overlay>
diff --git a/browser/base/content/blockedSite.xhtml b/browser/base/content/blockedSite.xhtml
new file mode 100644
index 000000000..10a4b33e8
--- /dev/null
+++ b/browser/base/content/blockedSite.xhtml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % blockedSiteDTD SYSTEM "chrome://browser/locale/safebrowsing/phishing-afterload-warning-message.dtd">
+ %blockedSiteDTD;
+]>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml" class="blacklist">
+ <head>
+ <link rel="stylesheet" href="chrome://browser/skin/blockedSite.css" type="text/css" media="all" />
+ <link rel="icon" type="image/png" id="favicon" href="chrome://global/skin/icons/blacklist_favicon.png"/>
+
+ <script type="application/javascript"><![CDATA[
+ // Error url MUST be formatted like this:
+ // about:blocked?e=error_code&u=url(&o=1)?
+ // (o=1 when user overrides are allowed)
+
+ // Note that this file uses document.documentURI to get
+ // the URL (with the format from above). This is because
+ // document.location.href gets the current URI off the docshell,
+ // which is the URL displayed in the location bar, i.e.
+ // the URI that the user attempted to load.
+
+ function getErrorCode()
+ {
+ var url = document.documentURI;
+ var error = url.search(/e\=/);
+ var duffUrl = url.search(/\&u\=/);
+ return decodeURIComponent(url.slice(error + 2, duffUrl));
+ }
+
+ function getURL()
+ {
+ var url = document.documentURI;
+ var match = url.match(/&u=([^&]+)&/);
+
+ // match == null if not found; if so, return an empty string
+ // instead of what would turn out to be portions of the URI
+ if (!match)
+ return "";
+
+ url = decodeURIComponent(match[1]);
+
+ // If this is a view-source page, then get then real URI of the page
+ if (url.startsWith("view-source:"))
+ url = url.slice(12);
+ return url;
+ }
+
+ /**
+ * Check whether this warning page should be overridable or whether
+ * the "ignore warning" button should be hidden.
+ */
+ function getOverride()
+ {
+ var url = document.documentURI;
+ var match = url.match(/&o=1&/);
+ return !!match;
+ }
+
+ /**
+ * Attempt to get the hostname via document.location. Fail back
+ * to getURL so that we always return something meaningful.
+ */
+ function getHostString()
+ {
+ try {
+ return document.location.hostname;
+ } catch (e) {
+ return getURL();
+ }
+ }
+
+ function initPage()
+ {
+ var error = "";
+ switch (getErrorCode()) {
+ case "malwareBlocked" :
+ error = "malware";
+ break;
+ case "deceptiveBlocked" :
+ error = "phishing";
+ break;
+ case "unwantedBlocked" :
+ error = "unwanted";
+ break;
+ default:
+ return;
+ }
+
+ var el;
+
+ if (error !== "malware") {
+ el = document.getElementById("errorTitleText_malware");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_malware");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_malware");
+ el.parentNode.removeChild(el);
+ }
+
+ if (error !== "phishing") {
+ el = document.getElementById("errorTitleText_phishing");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_phishing");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_phishing");
+ el.parentNode.removeChild(el);
+ }
+
+ if (error !== "unwanted") {
+ el = document.getElementById("errorTitleText_unwanted");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorShortDescText_unwanted");
+ el.parentNode.removeChild(el);
+ el = document.getElementById("errorLongDescText_unwanted");
+ el.parentNode.removeChild(el);
+ }
+
+ // Set sitename
+ document.getElementById(error + "_sitename").textContent = getHostString();
+ document.title = document.getElementById("errorTitleText_" + error)
+ .innerHTML;
+
+ if (!getOverride()) {
+ var btn = document.getElementById("ignoreWarningButton");
+ if (btn) {
+ btn.parentNode.removeChild(btn);
+ }
+ }
+
+ // Inform the test harness that we're done loading the page
+ var event = new CustomEvent("AboutBlockedLoaded");
+ document.dispatchEvent(event);
+ }
+ ]]></script>
+ </head>
+
+ <body dir="&locale.dir;">
+ <div id="errorPageContainer" class="container">
+
+ <!-- Error Title -->
+ <div id="errorTitle" class="title">
+ <h1 class="title-text" id="errorTitleText_phishing">&safeb.blocked.phishingPage.title2;</h1>
+ <h1 class="title-text" id="errorTitleText_malware">&safeb.blocked.malwarePage.title;</h1>
+ <h1 class="title-text" id="errorTitleText_unwanted">&safeb.blocked.unwantedPage.title;</h1>
+ </div>
+
+ <div id="errorLongContent">
+
+ <!-- Short Description -->
+ <div id="errorShortDesc">
+ <p id="errorShortDescText_phishing">&safeb.blocked.phishingPage.shortDesc2;</p>
+ <p id="errorShortDescText_malware">&safeb.blocked.malwarePage.shortDesc;</p>
+ <p id="errorShortDescText_unwanted">&safeb.blocked.unwantedPage.shortDesc;</p>
+ </div>
+
+ <!-- Long Description -->
+ <div id="errorLongDesc">
+ <p id="errorLongDescText_phishing">&safeb.blocked.phishingPage.longDesc2;</p>
+ <p id="errorLongDescText_malware">&safeb.blocked.malwarePage.longDesc;</p>
+ <p id="errorLongDescText_unwanted">&safeb.blocked.unwantedPage.longDesc;</p>
+ </div>
+
+ <!-- Action buttons -->
+ <div id="buttons" class="button-container">
+ <!-- Commands handled in browser.js -->
+ <button id="getMeOutButton" class="primary">&safeb.palm.accept.label;</button>
+ <div class="button-spacer"></div>
+ <button id="reportButton">&safeb.palm.reportPage.label;</button>
+ </div>
+ </div>
+ <div id="ignoreWarning">
+ <button id="ignoreWarningButton">&safeb.palm.decline.label;</button>
+ </div>
+ </div>
+ <!--
+ - Note: It is important to run the script this way, instead of using
+ - an onload handler. This is because error pages are loaded as
+ - LOAD_BACKGROUND, which means that onload handlers will not be executed.
+ -->
+ <script type="application/javascript">
+ initPage();
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js
new file mode 100644
index 000000000..1f81d1fb0
--- /dev/null
+++ b/browser/base/content/browser-addons.js
@@ -0,0 +1,747 @@
+/* -*- 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/. */
+
+// Removes a doorhanger notification if all of the installs it was notifying
+// about have ended in some way.
+function removeNotificationOnEnd(notification, installs) {
+ let count = installs.length;
+
+ function maybeRemove(install) {
+ install.removeListener(this);
+
+ if (--count == 0) {
+ // Check that the notification is still showing
+ let current = PopupNotifications.getNotification(notification.id, notification.browser);
+ if (current === notification)
+ notification.remove();
+ }
+ }
+
+ for (let install of installs) {
+ install.addListener({
+ onDownloadCancelled: maybeRemove,
+ onDownloadFailed: maybeRemove,
+ onInstallFailed: maybeRemove,
+ onInstallEnded: maybeRemove
+ });
+ }
+}
+
+const gXPInstallObserver = {
+ _findChildShell: function (aDocShell, aSoughtShell)
+ {
+ if (aDocShell == aSoughtShell)
+ return aDocShell;
+
+ var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem);
+ for (var i = 0; i < node.childCount; ++i) {
+ var docShell = node.getChildAt(i);
+ docShell = this._findChildShell(docShell, aSoughtShell);
+ if (docShell == aSoughtShell)
+ return docShell;
+ }
+ return null;
+ },
+
+ _getBrowser: function (aDocShell)
+ {
+ for (let browser of gBrowser.browsers) {
+ if (this._findChildShell(browser.docShell, aDocShell))
+ return browser;
+ }
+ return null;
+ },
+
+ pendingInstalls: new WeakMap(),
+
+ showInstallConfirmation: function(browser, installInfo, height = undefined) {
+ // If the confirmation notification is already open cache the installInfo
+ // and the new confirmation will be shown later
+ if (PopupNotifications.getNotification("addon-install-confirmation", browser)) {
+ let pending = this.pendingInstalls.get(browser);
+ if (pending) {
+ pending.push(installInfo);
+ } else {
+ this.pendingInstalls.set(browser, [installInfo]);
+ }
+ return;
+ }
+
+ let showNextConfirmation = () => {
+ // Make sure the browser is still alive.
+ if (gBrowser.browsers.indexOf(browser) == -1)
+ return;
+
+ let pending = this.pendingInstalls.get(browser);
+ if (pending && pending.length)
+ this.showInstallConfirmation(browser, pending.shift());
+ }
+
+ // If all installs have already been cancelled in some way then just show
+ // the next confirmation
+ if (installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) {
+ showNextConfirmation();
+ return;
+ }
+
+ const anchorID = "addons-notification-icon";
+
+ // Make notifications persist a minimum of 30 seconds
+ var options = {
+ displayURI: installInfo.originatingURI,
+ timeout: Date.now() + 30000,
+ };
+
+ let cancelInstallation = () => {
+ if (installInfo) {
+ for (let install of installInfo.installs) {
+ // The notification may have been closed because the add-ons got
+ // cancelled elsewhere, only try to cancel those that are still
+ // pending install.
+ if (install.state != AddonManager.STATE_CANCELLED)
+ install.cancel();
+ }
+ }
+
+ this.acceptInstallation = null;
+
+ showNextConfirmation();
+ };
+
+ let unsigned = installInfo.installs.filter(i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING);
+ let someUnsigned = unsigned.length > 0 && unsigned.length < installInfo.installs.length;
+
+ options.eventCallback = (aEvent) => {
+ switch (aEvent) {
+ case "removed":
+ cancelInstallation();
+ break;
+ case "shown":
+ let addonList = document.getElementById("addon-install-confirmation-content");
+ while (addonList.firstChild)
+ addonList.firstChild.remove();
+
+ for (let install of installInfo.installs) {
+ let container = document.createElement("hbox");
+
+ let name = document.createElement("label");
+ name.setAttribute("value", install.addon.name);
+ name.setAttribute("class", "addon-install-confirmation-name");
+ container.appendChild(name);
+
+ if (someUnsigned && install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+ let unsigned = document.createElement("label");
+ unsigned.setAttribute("value", gNavigatorBundle.getString("addonInstall.unsigned"));
+ unsigned.setAttribute("class", "addon-install-confirmation-unsigned");
+ container.appendChild(unsigned);
+ }
+
+ addonList.appendChild(container);
+ }
+
+ this.acceptInstallation = () => {
+ for (let install of installInfo.installs)
+ install.install();
+ installInfo = null;
+
+ Services.telemetry
+ .getHistogramById("SECURITY_UI")
+ .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH);
+ };
+ break;
+ }
+ };
+
+ options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+
+ let messageString;
+ let notification = document.getElementById("addon-install-confirmation-notification");
+ if (unsigned.length == installInfo.installs.length) {
+ // None of the add-ons are verified
+ messageString = gNavigatorBundle.getString("addonConfirmInstallUnsigned.message");
+ notification.setAttribute("warning", "true");
+ options.learnMoreURL += "unsigned-addons";
+ }
+ else if (unsigned.length == 0) {
+ // All add-ons are verified or don't need to be verified
+ messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
+ notification.removeAttribute("warning");
+ options.learnMoreURL += "find-and-install-add-ons";
+ }
+ else {
+ // Some of the add-ons are unverified, the list of names will indicate
+ // which
+ messageString = gNavigatorBundle.getString("addonConfirmInstallSomeUnsigned.message");
+ notification.setAttribute("warning", "true");
+ options.learnMoreURL += "unsigned-addons";
+ }
+
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+
+ messageString = PluralForm.get(installInfo.installs.length, messageString);
+ messageString = messageString.replace("#1", brandShortName);
+ messageString = messageString.replace("#2", installInfo.installs.length);
+
+ let cancelButton = document.getElementById("addon-install-confirmation-cancel");
+ cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label");
+ cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey");
+
+ let acceptButton = document.getElementById("addon-install-confirmation-accept");
+ acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label");
+ acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey");
+
+ if (height) {
+ let notification = document.getElementById("addon-install-confirmation-notification");
+ notification.style.minHeight = height + "px";
+ }
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ if (tab) {
+ gBrowser.selectedTab = tab;
+ }
+
+ let popup = PopupNotifications.show(browser, "addon-install-confirmation",
+ messageString, anchorID, null, null,
+ options);
+
+ removeNotificationOnEnd(popup, installInfo.installs);
+
+ Services.telemetry
+ .getHistogramById("SECURITY_UI")
+ .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
+ },
+
+ observe: function (aSubject, aTopic, aData)
+ {
+ var brandBundle = document.getElementById("bundle_brand");
+ var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
+ var browser = installInfo.browser;
+
+ // Make sure the browser is still alive.
+ if (!browser || gBrowser.browsers.indexOf(browser) == -1)
+ return;
+
+ const anchorID = "addons-notification-icon";
+ var messageString, action;
+ var brandShortName = brandBundle.getString("brandShortName");
+
+ var notificationID = aTopic;
+ // Make notifications persist a minimum of 30 seconds
+ var options = {
+ displayURI: installInfo.originatingURI,
+ timeout: Date.now() + 30000,
+ };
+
+ switch (aTopic) {
+ case "addon-install-disabled": {
+ notificationID = "xpinstall-disabled";
+
+ if (gPrefService.prefIsLocked("xpinstall.enabled")) {
+ messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked");
+ buttons = [];
+ }
+ else {
+ messageString = gNavigatorBundle.getString("xpinstallDisabledMessage");
+
+ action = {
+ label: gNavigatorBundle.getString("xpinstallDisabledButton"),
+ accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"),
+ callback: function editPrefs() {
+ gPrefService.setBoolPref("xpinstall.enabled", true);
+ }
+ };
+ }
+
+ PopupNotifications.show(browser, notificationID, messageString, anchorID,
+ action, null, options);
+ break; }
+ case "addon-install-origin-blocked": {
+ messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage",
+ [brandShortName]);
+
+ let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI");
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
+ let popup = PopupNotifications.show(browser, notificationID,
+ messageString, anchorID,
+ null, null, options);
+ removeNotificationOnEnd(popup, installInfo.installs);
+ break; }
+ case "addon-install-blocked": {
+ messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage",
+ [brandShortName]);
+
+ let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI");
+ action = {
+ label: gNavigatorBundle.getString("xpinstallPromptAllowButton"),
+ accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"),
+ callback: function() {
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH);
+ installInfo.install();
+ }
+ };
+
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
+ let popup = PopupNotifications.show(browser, notificationID,
+ messageString, anchorID,
+ action, null, options);
+ removeNotificationOnEnd(popup, installInfo.installs);
+ break; }
+ case "addon-install-started": {
+ let needsDownload = function needsDownload(aInstall) {
+ return aInstall.state != AddonManager.STATE_DOWNLOADED;
+ }
+ // If all installs have already been downloaded then there is no need to
+ // show the download progress
+ if (!installInfo.installs.some(needsDownload))
+ return;
+ notificationID = "addon-progress";
+ messageString = gNavigatorBundle.getString("addonDownloadingAndVerifying");
+ messageString = PluralForm.get(installInfo.installs.length, messageString);
+ messageString = messageString.replace("#1", installInfo.installs.length);
+ options.installs = installInfo.installs;
+ options.contentWindow = browser.contentWindow;
+ options.sourceURI = browser.currentURI;
+ options.eventCallback = (aEvent) => {
+ switch (aEvent) {
+ case "removed":
+ options.contentWindow = null;
+ options.sourceURI = null;
+ break;
+ }
+ };
+ let notification = PopupNotifications.show(browser, notificationID, messageString,
+ anchorID, null, null, options);
+ notification._startTime = Date.now();
+
+ let cancelButton = document.getElementById("addon-progress-cancel");
+ cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label");
+ cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey");
+
+ let acceptButton = document.getElementById("addon-progress-accept");
+ if (Preferences.get("xpinstall.customConfirmationUI", false)) {
+ acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label");
+ acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey");
+ } else {
+ acceptButton.hidden = true;
+ }
+ break; }
+ case "addon-install-failed": {
+ // TODO This isn't terribly ideal for the multiple failure case
+ for (let install of installInfo.installs) {
+ let host;
+ try {
+ host = options.displayURI.host;
+ } catch (e) {
+ // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
+ }
+
+ if (!host)
+ host = (install.sourceURI instanceof Ci.nsIStandardURL) &&
+ install.sourceURI.host;
+
+ let error = (host || install.error == 0) ? "addonInstallError" : "addonLocalInstallError";
+ let args;
+ if (install.error < 0) {
+ error += install.error;
+ args = [brandShortName, install.name];
+ } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ error += "Blocklisted";
+ args = [install.name];
+ } else {
+ error += "Incompatible";
+ args = [brandShortName, Services.appinfo.version, install.name];
+ }
+
+ // Add Learn More link when refusing to install an unsigned add-on
+ if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
+ options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
+ }
+
+ messageString = gNavigatorBundle.getFormattedString(error, args);
+
+ PopupNotifications.show(browser, notificationID, messageString, anchorID,
+ action, null, options);
+
+ // Can't have multiple notifications with the same ID, so stop here.
+ break;
+ }
+ this._removeProgressNotification(browser);
+ break; }
+ case "addon-install-confirmation": {
+ let showNotification = () => {
+ let height = undefined;
+
+ if (PopupNotifications.isPanelOpen) {
+ let rect = document.getElementById("addon-progress-notification").getBoundingClientRect();
+ height = rect.height;
+ }
+
+ this._removeProgressNotification(browser);
+ this.showInstallConfirmation(browser, installInfo, height);
+ };
+
+ let progressNotification = PopupNotifications.getNotification("addon-progress", browser);
+ if (progressNotification) {
+ let downloadDuration = Date.now() - progressNotification._startTime;
+ let securityDelay = Services.prefs.getIntPref("security.dialog_enable_delay") - downloadDuration;
+ if (securityDelay > 0) {
+ setTimeout(() => {
+ // The download may have been cancelled during the security delay
+ if (PopupNotifications.getNotification("addon-progress", browser))
+ showNotification();
+ }, securityDelay);
+ break;
+ }
+ }
+ showNotification();
+ break; }
+ case "addon-install-complete": {
+ let needsRestart = installInfo.installs.some(function(i) {
+ return i.addon.pendingOperations != AddonManager.PENDING_NONE;
+ });
+
+ if (needsRestart) {
+ notificationID = "addon-install-restart";
+ messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart");
+ action = {
+ label: gNavigatorBundle.getString("addonInstallRestartButton"),
+ accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"),
+ callback: function() {
+ BrowserUtils.restartApplication();
+ }
+ };
+ }
+ else {
+ messageString = gNavigatorBundle.getString("addonsInstalled");
+ action = null;
+ }
+
+ messageString = PluralForm.get(installInfo.installs.length, messageString);
+ messageString = messageString.replace("#1", installInfo.installs[0].name);
+ messageString = messageString.replace("#2", installInfo.installs.length);
+ messageString = messageString.replace("#3", brandShortName);
+
+ // Remove notificaion on dismissal, since it's possible to cancel the
+ // install through the addons manager UI, making the "restart" prompt
+ // irrelevant.
+ options.removeOnDismissal = true;
+
+ PopupNotifications.show(browser, notificationID, messageString, anchorID,
+ action, null, options);
+ break; }
+ }
+ },
+ _removeProgressNotification(aBrowser) {
+ let notification = PopupNotifications.getNotification("addon-progress", aBrowser);
+ if (notification)
+ notification.remove();
+ }
+};
+
+var LightWeightThemeWebInstaller = {
+ init: function () {
+ let mm = window.messageManager;
+ mm.addMessageListener("LightWeightThemeWebInstaller:Install", this);
+ mm.addMessageListener("LightWeightThemeWebInstaller:Preview", this);
+ mm.addMessageListener("LightWeightThemeWebInstaller:ResetPreview", this);
+ },
+
+ receiveMessage: function (message) {
+ // ignore requests from background tabs
+ if (message.target != gBrowser.selectedBrowser) {
+ return;
+ }
+
+ let data = message.data;
+
+ switch (message.name) {
+ case "LightWeightThemeWebInstaller:Install": {
+ this._installRequest(data.themeData, data.baseURI);
+ break;
+ }
+ case "LightWeightThemeWebInstaller:Preview": {
+ this._preview(data.themeData, data.baseURI);
+ break;
+ }
+ case "LightWeightThemeWebInstaller:ResetPreview": {
+ this._resetPreview(data && data.baseURI);
+ break;
+ }
+ }
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "TabSelect": {
+ this._resetPreview();
+ break;
+ }
+ }
+ },
+
+ get _manager () {
+ let temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
+ delete this._manager;
+ return this._manager = temp.LightweightThemeManager;
+ },
+
+ _installRequest: function (dataString, baseURI) {
+ let data = this._manager.parseTheme(dataString, baseURI);
+
+ if (!data) {
+ return;
+ }
+
+ let uri = makeURI(baseURI);
+
+ // A notification bar with the option to undo is normally shown after a
+ // theme is installed. But the discovery pane served from the url(s)
+ // below has its own toggle switch for quick undos, so don't show the
+ // notification in that case.
+ let notify = uri.prePath != "https://discovery.addons.mozilla.org";
+ if (notify) {
+ try {
+ if (Services.prefs.getBoolPref("extensions.webapi.testing")
+ && (uri.prePath == "https://discovery.addons.allizom.org"
+ || uri.prePath == "https://discovery.addons-dev.allizom.org")) {
+ notify = false;
+ }
+ } catch (e) {
+ // getBoolPref() throws if the testing pref isn't set. ignore it.
+ }
+ }
+
+ if (this._isAllowed(baseURI)) {
+ this._install(data, notify);
+ return;
+ }
+
+ let allowButtonText =
+ gNavigatorBundle.getString("lwthemeInstallRequest.allowButton");
+ let allowButtonAccesskey =
+ gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey");
+ let message =
+ gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message",
+ [uri.host]);
+ let buttons = [{
+ label: allowButtonText,
+ accessKey: allowButtonAccesskey,
+ callback: function () {
+ LightWeightThemeWebInstaller._install(data, notify);
+ }
+ }];
+
+ this._removePreviousNotifications();
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let notificationBar =
+ notificationBox.appendNotification(message, "lwtheme-install-request", "",
+ notificationBox.PRIORITY_INFO_MEDIUM,
+ buttons);
+ notificationBar.persistence = 1;
+ },
+
+ _install: function (newLWTheme, notify) {
+ let previousLWTheme = this._manager.currentTheme;
+
+ let listener = {
+ onEnabling: function(aAddon, aRequiresRestart) {
+ if (!aRequiresRestart) {
+ return;
+ }
+
+ let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message",
+ [aAddon.name], 1);
+
+ let action = {
+ label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"),
+ accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"),
+ callback: function () {
+ BrowserUtils.restartApplication();
+ }
+ };
+
+ let options = {
+ timeout: Date.now() + 30000
+ };
+
+ PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change",
+ messageString, "addons-notification-icon",
+ action, null, options);
+ },
+
+ onEnabled: function(aAddon) {
+ if (notify) {
+ LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme);
+ }
+ }
+ };
+
+ AddonManager.addAddonListener(listener);
+ this._manager.currentTheme = newLWTheme;
+ AddonManager.removeAddonListener(listener);
+ },
+
+ _postInstallNotification: function (newTheme, previousTheme) {
+ function text(id) {
+ return gNavigatorBundle.getString("lwthemePostInstallNotification." + id);
+ }
+
+ let buttons = [{
+ label: text("undoButton"),
+ accessKey: text("undoButton.accesskey"),
+ callback: function () {
+ LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id);
+ LightWeightThemeWebInstaller._manager.currentTheme = previousTheme;
+ }
+ }, {
+ label: text("manageButton"),
+ accessKey: text("manageButton.accesskey"),
+ callback: function () {
+ BrowserOpenAddonsMgr("addons://list/theme");
+ }
+ }];
+
+ this._removePreviousNotifications();
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let notificationBar =
+ notificationBox.appendNotification(text("message"),
+ "lwtheme-install-notification", "",
+ notificationBox.PRIORITY_INFO_MEDIUM,
+ buttons);
+ notificationBar.persistence = 1;
+ notificationBar.timeout = Date.now() + 20000; // 20 seconds
+ },
+
+ _removePreviousNotifications: function () {
+ let box = gBrowser.getNotificationBox();
+
+ ["lwtheme-install-request",
+ "lwtheme-install-notification"].forEach(function (value) {
+ let notification = box.getNotificationWithValue(value);
+ if (notification)
+ box.removeNotification(notification);
+ });
+ },
+
+ _preview: function (dataString, baseURI) {
+ if (!this._isAllowed(baseURI))
+ return;
+
+ let data = this._manager.parseTheme(dataString, baseURI);
+ if (!data)
+ return;
+
+ this._resetPreview();
+ gBrowser.tabContainer.addEventListener("TabSelect", this, false);
+ this._manager.previewTheme(data);
+ },
+
+ _resetPreview: function (baseURI) {
+ if (baseURI && !this._isAllowed(baseURI))
+ return;
+ gBrowser.tabContainer.removeEventListener("TabSelect", this, false);
+ this._manager.resetPreview();
+ },
+
+ _isAllowed: function (srcURIString) {
+ let uri;
+ try {
+ uri = makeURI(srcURIString);
+ }
+ catch (e) {
+ // makeURI fails if srcURIString is a nonsense URI
+ return false;
+ }
+
+ if (!uri.schemeIs("https")) {
+ return false;
+ }
+
+ let pm = Services.perms;
+ return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
+ }
+};
+
+/*
+ * Listen for Lightweight Theme styling changes and update the browser's theme accordingly.
+ */
+var LightweightThemeListener = {
+ _modifiedStyles: [],
+
+ init: function () {
+ XPCOMUtils.defineLazyGetter(this, "styleSheet", function() {
+ for (let i = document.styleSheets.length - 1; i >= 0; i--) {
+ let sheet = document.styleSheets[i];
+ if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css")
+ return sheet;
+ }
+ return undefined;
+ });
+
+ Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
+ Services.obs.addObserver(this, "lightweight-theme-optimized", false);
+ if (document.documentElement.hasAttribute("lwtheme"))
+ this.updateStyleSheet(document.documentElement.style.backgroundImage);
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update");
+ Services.obs.removeObserver(this, "lightweight-theme-optimized");
+ },
+
+ /**
+ * Append the headerImage to the background-image property of all rulesets in
+ * browser-lightweightTheme.css.
+ *
+ * @param headerImage - a string containing a CSS image for the lightweight theme header.
+ */
+ updateStyleSheet: function(headerImage) {
+ if (!this.styleSheet)
+ return;
+ this.substituteRules(this.styleSheet.cssRules, headerImage);
+ },
+
+ substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) {
+ let styleRulesModified = 0;
+ for (let i = 0; i < ruleList.length; i++) {
+ let rule = ruleList[i];
+ if (rule instanceof Ci.nsIDOMCSSGroupingRule) {
+ // Add the number of modified sub-rules to the modified count
+ styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified);
+ } else if (rule instanceof Ci.nsIDOMCSSStyleRule) {
+ if (!rule.style.backgroundImage)
+ continue;
+ let modifiedIndex = existingStyleRulesModified + styleRulesModified;
+ if (!this._modifiedStyles[modifiedIndex])
+ this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage };
+
+ rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage;
+ styleRulesModified++;
+ } else {
+ Cu.reportError("Unsupported rule encountered");
+ }
+ }
+ return styleRulesModified;
+ },
+
+ // nsIObserver
+ observe: function (aSubject, aTopic, aData) {
+ if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") ||
+ !this.styleSheet)
+ return;
+
+ if (aTopic == "lightweight-theme-optimized" && aSubject != window)
+ return;
+
+ let themeData = JSON.parse(aData);
+ if (!themeData)
+ return;
+ this.updateStyleSheet("url(" + themeData.headerURL + ")");
+ },
+};
diff --git a/browser/base/content/browser-captivePortal.js b/browser/base/content/browser-captivePortal.js
new file mode 100644
index 000000000..c2e45c4ed
--- /dev/null
+++ b/browser/base/content/browser-captivePortal.js
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+XPCOMUtils.defineLazyServiceGetter(this, "cps",
+ "@mozilla.org/network/captive-portal-service;1",
+ "nsICaptivePortalService");
+
+var CaptivePortalWatcher = {
+ /**
+ * This constant is chosen to be large enough for a portal recheck to complete,
+ * and small enough that the delay in opening a tab isn't too noticeable.
+ * Please see comments for _delayedCaptivePortalDetected for more details.
+ */
+ PORTAL_RECHECK_DELAY_MS: Preferences.get("captivedetect.portalRecheckDelayMS", 500),
+
+ // This is the value used to identify the captive portal notification.
+ PORTAL_NOTIFICATION_VALUE: "captive-portal-detected",
+
+ // This holds a weak reference to the captive portal tab so that we
+ // don't leak it if the user closes it.
+ _captivePortalTab: null,
+
+ /**
+ * If a portal is detected when we don't have focus, we first wait for focus
+ * and then add the tab if, after a recheck, the portal is still active. This
+ * is set to true while we wait so that in the unlikely event that we receive
+ * another notification while waiting, we don't do things twice.
+ */
+ _delayedCaptivePortalDetectedInProgress: false,
+
+ // In the situation above, this is set to true while we wait for the recheck.
+ // This flag exists so that tests can appropriately simulate a recheck.
+ _waitingForRecheck: false,
+
+ get _captivePortalNotification() {
+ let nb = document.getElementById("high-priority-global-notificationbox");
+ return nb.getNotificationWithValue(this.PORTAL_NOTIFICATION_VALUE);
+ },
+
+ get canonicalURL() {
+ return Services.prefs.getCharPref("captivedetect.canonicalURL");
+ },
+
+ get _browserBundle() {
+ delete this._browserBundle;
+ return this._browserBundle =
+ Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ },
+
+ init() {
+ Services.obs.addObserver(this, "captive-portal-login", false);
+ Services.obs.addObserver(this, "captive-portal-login-abort", false);
+ Services.obs.addObserver(this, "captive-portal-login-success", false);
+
+ if (cps.state == cps.LOCKED_PORTAL) {
+ // A captive portal has already been detected.
+ this._captivePortalDetected();
+
+ // Automatically open a captive portal tab if there's no other browser window.
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ if (windows.getNext() == window && !windows.hasMoreElements()) {
+ this.ensureCaptivePortalTab();
+ }
+ }
+
+ cps.recheckCaptivePortal();
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "captive-portal-login");
+ Services.obs.removeObserver(this, "captive-portal-login-abort");
+ Services.obs.removeObserver(this, "captive-portal-login-success");
+
+
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ Services.obs.removeObserver(this, "xul-window-visible");
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "captive-portal-login":
+ this._captivePortalDetected();
+ break;
+ case "captive-portal-login-abort":
+ case "captive-portal-login-success":
+ this._captivePortalGone();
+ break;
+ case "xul-window-visible":
+ this._delayedCaptivePortalDetected();
+ break;
+ }
+ },
+
+ _captivePortalDetected() {
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ return;
+ }
+
+ let win = RecentWindow.getMostRecentBrowserWindow();
+ // If no browser window has focus, open and show the tab when we regain focus.
+ // This is so that if a different application was focused, when the user
+ // (re-)focuses a browser window, we open the tab immediately in that window
+ // so they can log in before continuing to browse.
+ if (win != Services.ww.activeWindow) {
+ this._delayedCaptivePortalDetectedInProgress = true;
+ Services.obs.addObserver(this, "xul-window-visible", false);
+ }
+
+ this._showNotification();
+ },
+
+ /**
+ * Called after we regain focus if we detect a portal while a browser window
+ * doesn't have focus. Triggers a portal recheck to reaffirm state, and adds
+ * the tab if needed after a short delay to allow the recheck to complete.
+ */
+ _delayedCaptivePortalDetected() {
+ if (!this._delayedCaptivePortalDetectedInProgress) {
+ return;
+ }
+
+ let win = RecentWindow.getMostRecentBrowserWindow();
+ if (win != Services.ww.activeWindow) {
+ // The window that got focused was not a browser window.
+ return;
+ }
+ Services.obs.removeObserver(this, "xul-window-visible");
+ this._delayedCaptivePortalDetectedInProgress = false;
+
+ if (win != window) {
+ // Some other browser window got focus, we don't have to do anything.
+ return;
+ }
+ // Trigger a portal recheck. The user may have logged into the portal via
+ // another client, or changed networks.
+ cps.recheckCaptivePortal();
+ this._waitingForRecheck = true;
+ let requestTime = Date.now();
+
+ let self = this;
+ Services.obs.addObserver(function observer() {
+ let time = Date.now() - requestTime;
+ Services.obs.removeObserver(observer, "captive-portal-check-complete");
+ self._waitingForRecheck = false;
+ if (cps.state != cps.LOCKED_PORTAL) {
+ // We're free of the portal!
+ return;
+ }
+
+ if (time <= self.PORTAL_RECHECK_DELAY_MS) {
+ // The amount of time elapsed since we requested a recheck (i.e. since
+ // the browser window was focused) was small enough that we can add and
+ // focus a tab with the login page with no noticeable delay.
+ self.ensureCaptivePortalTab();
+ }
+ }, "captive-portal-check-complete", false);
+ },
+
+ _captivePortalGone() {
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ Services.obs.removeObserver(this, "xul-window-visible");
+ this._delayedCaptivePortalDetectedInProgress = false;
+ }
+
+ this._removeNotification();
+ },
+
+ handleEvent(aEvent) {
+ if (aEvent.type != "TabSelect" || !this._captivePortalTab || !this._captivePortalNotification) {
+ return;
+ }
+
+ let tab = this._captivePortalTab.get();
+ let n = this._captivePortalNotification;
+ if (!tab || !n) {
+ return;
+ }
+
+ let doc = tab.ownerDocument;
+ let button = n.querySelector("button.notification-button");
+ if (doc.defaultView.gBrowser.selectedTab == tab) {
+ button.style.visibility = "hidden";
+ } else {
+ button.style.visibility = "visible";
+ }
+ },
+
+ _showNotification() {
+ let buttons = [
+ {
+ label: this._browserBundle.GetStringFromName("captivePortal.showLoginPage"),
+ callback: () => {
+ this.ensureCaptivePortalTab();
+
+ // Returning true prevents the notification from closing.
+ return true;
+ },
+ isDefault: true,
+ },
+ ];
+
+ let message = this._browserBundle.GetStringFromName("captivePortal.infoMessage2");
+
+ let closeHandler = (aEventName) => {
+ if (aEventName != "removed") {
+ return;
+ }
+ gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ };
+
+ let nb = document.getElementById("high-priority-global-notificationbox");
+ nb.appendNotification(message, this.PORTAL_NOTIFICATION_VALUE, "",
+ nb.PRIORITY_INFO_MEDIUM, buttons, closeHandler);
+
+ gBrowser.tabContainer.addEventListener("TabSelect", this);
+ },
+
+ _removeNotification() {
+ let n = this._captivePortalNotification;
+ if (!n || !n.parentNode) {
+ return;
+ }
+ n.close();
+ },
+
+ ensureCaptivePortalTab() {
+ let tab;
+ if (this._captivePortalTab) {
+ tab = this._captivePortalTab.get();
+ }
+
+ // If the tab is gone or going, we need to open a new one.
+ if (!tab || tab.closing || !tab.parentNode) {
+ tab = gBrowser.addTab(this.canonicalURL, { ownerTab: gBrowser.selectedTab });
+ this._captivePortalTab = Cu.getWeakReference(tab);
+ }
+
+ gBrowser.selectedTab = tab;
+
+ let canonicalURI = makeURI(this.canonicalURL);
+
+ // When we are no longer captive, close the tab if it's at the canonical URL.
+ let tabCloser = () => {
+ Services.obs.removeObserver(tabCloser, "captive-portal-login-abort");
+ Services.obs.removeObserver(tabCloser, "captive-portal-login-success");
+ if (!tab || tab.closing || !tab.parentNode || !tab.linkedBrowser ||
+ !tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI)) {
+ return;
+ }
+ gBrowser.removeTab(tab);
+ }
+ Services.obs.addObserver(tabCloser, "captive-portal-login-abort", false);
+ Services.obs.addObserver(tabCloser, "captive-portal-login-success", false);
+ },
+};
diff --git a/browser/base/content/browser-charsetmenu.inc b/browser/base/content/browser-charsetmenu.inc
new file mode 100644
index 000000000..806b1cf03
--- /dev/null
+++ b/browser/base/content/browser-charsetmenu.inc
@@ -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/.
+
+<menu id="charsetMenu"
+ label="&charsetMenu2.label;"
+ accesskey="&charsetMenu2.accesskey;"
+ oncommand="BrowserSetForcedCharacterSet(event.target.getAttribute('charset'));"
+ onpopupshowing="CharsetMenu.build(event.target); UpdateCurrentCharset(this);">
+ <menupopup>
+ </menupopup>
+</menu>
diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc
new file mode 100644
index 000000000..51b14d152
--- /dev/null
+++ b/browser/base/content/browser-context.inc
@@ -0,0 +1,472 @@
+# -*- 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/.
+
+# NB: IF YOU ADD ITEMS TO THIS FILE, PLEASE UPDATE THE WHITELIST IN
+# BrowserUITelemetry.jsm. SEE BUG 991757 FOR DETAILS.
+
+ <menugroup id="context-navigation">
+ <menuitem id="context-back"
+ class="menuitem-iconic"
+ tooltiptext="&backButton.tooltip;"
+ aria-label="&backCmd.label;"
+ command="Browser:BackOrBackDuplicate"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-forward"
+ class="menuitem-iconic"
+ tooltiptext="&forwardButton.tooltip;"
+ aria-label="&forwardCmd.label;"
+ command="Browser:ForwardOrForwardDuplicate"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-reload"
+ class="menuitem-iconic"
+ tooltiptext="&reloadButton.tooltip;"
+ aria-label="&reloadCmd.label;"
+ oncommand="gContextMenu.reload(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-stop"
+ class="menuitem-iconic"
+ tooltiptext="&stopButton.tooltip;"
+ aria-label="&stopCmd.label;"
+ command="Browser:Stop"/>
+ <menuitem id="context-bookmarkpage"
+ class="menuitem-iconic"
+ observes="bookmarkThisPageBroadcaster"
+ aria-label="&bookmarkPageCmd2.label;"
+ oncommand="gContextMenu.bookmarkThisPage();"/>
+ </menugroup>
+ <menuseparator id="context-sep-navigation"/>
+ <menuseparator id="page-menu-separator"/>
+ <menuitem id="spell-no-suggestions"
+ disabled="true"
+ label="&spellNoSuggestions.label;"/>
+ <menuitem id="spell-add-to-dictionary"
+ label="&spellAddToDictionary.label;"
+ accesskey="&spellAddToDictionary.accesskey;"
+ oncommand="InlineSpellCheckerUI.addToDictionary();"/>
+ <menuitem id="spell-undo-add-to-dictionary"
+ label="&spellUndoAddToDictionary.label;"
+ accesskey="&spellUndoAddToDictionary.accesskey;"
+ oncommand="InlineSpellCheckerUI.undoAddToDictionary();" />
+ <menuseparator id="spell-suggestions-separator"/>
+ <menuitem id="context-openlinkincurrent"
+ label="&openLinkCmdInCurrent.label;"
+ accesskey="&openLinkCmdInCurrent.accesskey;"
+ oncommand="gContextMenu.openLinkInCurrent();"/>
+# label and data-usercontextid are dynamically set.
+ <menuitem id="context-openlinkincontainertab"
+ accesskey="&openLinkCmdInTab.accesskey;"
+ oncommand="gContextMenu.openLinkInTab(event);"/>
+ <menuitem id="context-openlinkintab"
+ label="&openLinkCmdInTab.label;"
+ accesskey="&openLinkCmdInTab.accesskey;"
+ data-usercontextid="0"
+ oncommand="gContextMenu.openLinkInTab(event);"/>
+
+ <menu id="context-openlinkinusercontext-menu"
+ label="&openLinkCmdInContainerTab.label;"
+ accesskey="&openLinkCmdInContainerTab.accesskey;"
+ hidden="true">
+ <menupopup oncommand="gContextMenu.openLinkInTab(event);"
+ onpopupshowing="return gContextMenu.createContainerMenu(event);" />
+ </menu>
+
+ <menuitem id="context-openlink"
+ label="&openLinkCmd.label;"
+ accesskey="&openLinkCmd.accesskey;"
+ oncommand="gContextMenu.openLink();"/>
+ <menuitem id="context-openlinkprivate"
+ label="&openLinkInPrivateWindowCmd.label;"
+ accesskey="&openLinkInPrivateWindowCmd.accesskey;"
+ oncommand="gContextMenu.openLinkInPrivateWindow();"/>
+ <menuseparator id="context-sep-open"/>
+ <menuitem id="context-bookmarklink"
+ label="&bookmarkThisLinkCmd.label;"
+ accesskey="&bookmarkThisLinkCmd.accesskey;"
+ oncommand="gContextMenu.bookmarkLink();"/>
+ <menuitem id="context-sharelink"
+ label="&shareLink.label;"
+ accesskey="&shareLink.accesskey;"
+ oncommand="gContextMenu.shareLink();"/>
+ <menuitem id="context-savelink"
+ label="&saveLinkCmd.label;"
+ accesskey="&saveLinkCmd.accesskey;"
+ oncommand="gContextMenu.saveLink();"/>
+ <menuitem id="context-copyemail"
+ label="&copyEmailCmd.label;"
+ accesskey="&copyEmailCmd.accesskey;"
+ oncommand="gContextMenu.copyEmail();"/>
+ <menuitem id="context-copylink"
+ label="&copyLinkCmd.label;"
+ accesskey="&copyLinkCmd.accesskey;"
+ oncommand="gContextMenu.copyLink();"/>
+ <menuseparator id="context-sep-copylink"/>
+ <menuitem id="context-media-play"
+ label="&mediaPlay.label;"
+ accesskey="&mediaPlay.accesskey;"
+ oncommand="gContextMenu.mediaCommand('play');"/>
+ <menuitem id="context-media-pause"
+ label="&mediaPause.label;"
+ accesskey="&mediaPause.accesskey;"
+ oncommand="gContextMenu.mediaCommand('pause');"/>
+ <menuitem id="context-media-mute"
+ label="&mediaMute.label;"
+ accesskey="&mediaMute.accesskey;"
+ oncommand="gContextMenu.mediaCommand('mute');"/>
+ <menuitem id="context-media-unmute"
+ label="&mediaUnmute.label;"
+ accesskey="&mediaUnmute.accesskey;"
+ oncommand="gContextMenu.mediaCommand('unmute');"/>
+ <menu id="context-media-playbackrate" label="&mediaPlaybackRate2.label;" accesskey="&mediaPlaybackRate2.accesskey;">
+ <menupopup>
+ <menuitem id="context-media-playbackrate-050x"
+ label="&mediaPlaybackRate050x2.label;"
+ accesskey="&mediaPlaybackRate050x2.accesskey;"
+ type="radio"
+ name="playbackrate"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 0.5);"/>
+ <menuitem id="context-media-playbackrate-100x"
+ label="&mediaPlaybackRate100x2.label;"
+ accesskey="&mediaPlaybackRate100x2.accesskey;"
+ type="radio"
+ name="playbackrate"
+ checked="true"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 1.0);"/>
+ <menuitem id="context-media-playbackrate-125x"
+ label="&mediaPlaybackRate125x2.label;"
+ accesskey="&mediaPlaybackRate125x2.accesskey;"
+ type="radio"
+ name="playbackrate"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 1.25);"/>
+ <menuitem id="context-media-playbackrate-150x"
+ label="&mediaPlaybackRate150x2.label;"
+ accesskey="&mediaPlaybackRate150x2.accesskey;"
+ type="radio"
+ name="playbackrate"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 1.5);"/>
+ <menuitem id="context-media-playbackrate-200x"
+ label="&mediaPlaybackRate200x2.label;"
+ accesskey="&mediaPlaybackRate200x2.accesskey;"
+ type="radio"
+ name="playbackrate"
+ oncommand="gContextMenu.mediaCommand('playbackRate', 2.0);"/>
+ </menupopup>
+ </menu>
+ <menuitem id="context-media-loop"
+ label="&mediaLoop.label;"
+ accesskey="&mediaLoop.accesskey;"
+ type="checkbox"
+ oncommand="gContextMenu.mediaCommand('loop');"/>
+ <menuitem id="context-media-showcontrols"
+ label="&mediaShowControls.label;"
+ accesskey="&mediaShowControls.accesskey;"
+ oncommand="gContextMenu.mediaCommand('showcontrols');"/>
+ <menuitem id="context-media-hidecontrols"
+ label="&mediaHideControls.label;"
+ accesskey="&mediaHideControls.accesskey;"
+ oncommand="gContextMenu.mediaCommand('hidecontrols');"/>
+ <menuitem id="context-video-fullscreen"
+ accesskey="&videoFullScreen.accesskey;"
+ label="&videoFullScreen.label;"
+ oncommand="gContextMenu.mediaCommand('fullscreen');"/>
+ <menuitem id="context-leave-dom-fullscreen"
+ accesskey="&leaveDOMFullScreen.accesskey;"
+ label="&leaveDOMFullScreen.label;"
+ oncommand="gContextMenu.leaveDOMFullScreen();"/>
+ <menuseparator id="context-media-sep-commands"/>
+ <menuitem id="context-reloadimage"
+ label="&reloadImageCmd.label;"
+ accesskey="&reloadImageCmd.accesskey;"
+ oncommand="gContextMenu.reloadImage();"/>
+ <menuitem id="context-viewimage"
+ label="&viewImageCmd.label;"
+ accesskey="&viewImageCmd.accesskey;"
+ oncommand="gContextMenu.viewMedia(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-viewvideo"
+ label="&viewVideoCmd.label;"
+ accesskey="&viewVideoCmd.accesskey;"
+ oncommand="gContextMenu.viewMedia(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+#ifdef CONTEXT_COPY_IMAGE_CONTENTS
+ <menuitem id="context-copyimage-contents"
+ label="&copyImageContentsCmd.label;"
+ accesskey="&copyImageContentsCmd.accesskey;"
+ oncommand="goDoCommand('cmd_copyImage');"/>
+#endif
+ <menuitem id="context-copyimage"
+ label="&copyImageCmd.label;"
+ accesskey="&copyImageCmd.accesskey;"
+ oncommand="gContextMenu.copyMediaLocation();"/>
+ <menuitem id="context-copyvideourl"
+ label="&copyVideoURLCmd.label;"
+ accesskey="&copyVideoURLCmd.accesskey;"
+ oncommand="gContextMenu.copyMediaLocation();"/>
+ <menuitem id="context-copyaudiourl"
+ label="&copyAudioURLCmd.label;"
+ accesskey="&copyAudioURLCmd.accesskey;"
+ oncommand="gContextMenu.copyMediaLocation();"/>
+ <menuseparator id="context-sep-copyimage"/>
+ <menuitem id="context-saveimage"
+ label="&saveImageCmd.label;"
+ accesskey="&saveImageCmd.accesskey;"
+ oncommand="gContextMenu.saveMedia();"/>
+ <menuitem id="context-shareimage"
+ label="&shareImage.label;"
+ accesskey="&shareImage.accesskey;"
+ oncommand="gContextMenu.shareImage();"/>
+ <menuitem id="context-sendimage"
+ label="&emailImageCmd.label;"
+ accesskey="&emailImageCmd.accesskey;"
+ oncommand="gContextMenu.sendMedia();"/>
+ <menuitem id="context-setDesktopBackground"
+ label="&setDesktopBackgroundCmd.label;"
+ accesskey="&setDesktopBackgroundCmd.accesskey;"
+ oncommand="gContextMenu.setDesktopBackground();"/>
+ <menuitem id="context-viewimageinfo"
+ label="&viewImageInfoCmd.label;"
+ accesskey="&viewImageInfoCmd.accesskey;"
+ oncommand="gContextMenu.viewImageInfo();"/>
+ <menuitem id="context-viewimagedesc"
+ label="&viewImageDescCmd.label;"
+ accesskey="&viewImageDescCmd.accesskey;"
+ oncommand="gContextMenu.viewImageDesc(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-savevideo"
+ label="&saveVideoCmd.label;"
+ accesskey="&saveVideoCmd.accesskey;"
+ oncommand="gContextMenu.saveMedia();"/>
+ <menuitem id="context-sharevideo"
+ label="&shareVideo.label;"
+ accesskey="&shareVideo.accesskey;"
+ oncommand="gContextMenu.shareVideo();"/>
+ <menuitem id="context-saveaudio"
+ label="&saveAudioCmd.label;"
+ accesskey="&saveAudioCmd.accesskey;"
+ oncommand="gContextMenu.saveMedia();"/>
+ <menuitem id="context-video-saveimage"
+ accesskey="&videoSaveImage.accesskey;"
+ label="&videoSaveImage.label;"
+ oncommand="gContextMenu.saveVideoFrameAsImage();"/>
+ <menuitem id="context-sendvideo"
+ label="&emailVideoCmd.label;"
+ accesskey="&emailVideoCmd.accesskey;"
+ oncommand="gContextMenu.sendMedia();"/>
+ <menu id="context-castvideo"
+ label="&castVideoCmd.label;"
+ accesskey="&castVideoCmd.accesskey;">
+ <menupopup id="context-castvideo-popup" onpopupshowing="gContextMenu.populateCastVideoMenu(this)"/>
+ </menu>
+ <menuitem id="context-sendaudio"
+ label="&emailAudioCmd.label;"
+ accesskey="&emailAudioCmd.accesskey;"
+ oncommand="gContextMenu.sendMedia();"/>
+ <menuitem id="context-ctp-play"
+ label="&playPluginCmd.label;"
+ accesskey="&playPluginCmd.accesskey;"
+ oncommand="gContextMenu.playPlugin();"/>
+ <menuitem id="context-ctp-hide"
+ label="&hidePluginCmd.label;"
+ accesskey="&hidePluginCmd.accesskey;"
+ oncommand="gContextMenu.hidePlugin();"/>
+ <menuseparator id="context-sep-ctp"/>
+ <menuitem id="context-sharepage"
+ label="&sharePageCmd.label;"
+ accesskey="&sharePageCmd.accesskey;"
+ oncommand="SocialShare.sharePage();"/>
+ <menuitem id="context-savepage"
+ label="&savePageCmd.label;"
+ accesskey="&savePageCmd.accesskey2;"
+ oncommand="gContextMenu.savePageAs();"/>
+ <menuseparator id="context-sep-sendpagetodevice" hidden="true"/>
+ <menu id="context-sendpagetodevice"
+ label="&sendPageToDevice.label;"
+ accesskey="&sendPageToDevice.accesskey;"
+ hidden="true">
+ <menupopup id="context-sendpagetodevice-popup"
+ onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
+ </menu>
+ <menuseparator id="context-sep-viewbgimage"/>
+ <menuitem id="context-viewbgimage"
+ label="&viewBGImageCmd.label;"
+ accesskey="&viewBGImageCmd.accesskey;"
+ oncommand="gContextMenu.viewBGImage(event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="context-undo"
+ label="&undoCmd.label;"
+ accesskey="&undoCmd.accesskey;"
+ command="cmd_undo"/>
+ <menuseparator id="context-sep-undo"/>
+ <menuitem id="context-cut"
+ label="&cutCmd.label;"
+ accesskey="&cutCmd.accesskey;"
+ command="cmd_cut"/>
+ <menuitem id="context-copy"
+ label="&copyCmd.label;"
+ accesskey="&copyCmd.accesskey;"
+ command="cmd_copy"/>
+ <menuitem id="context-paste"
+ label="&pasteCmd.label;"
+ accesskey="&pasteCmd.accesskey;"
+ command="cmd_paste"/>
+ <menuitem id="context-delete"
+ label="&deleteCmd.label;"
+ accesskey="&deleteCmd.accesskey;"
+ command="cmd_delete"/>
+ <menuseparator id="context-sep-paste"/>
+ <menuitem id="context-selectall"
+ label="&selectAllCmd.label;"
+ accesskey="&selectAllCmd.accesskey;"
+ command="cmd_selectAll"/>
+ <menuseparator id="context-sep-selectall"/>
+ <menuitem id="context-keywordfield"
+ label="&keywordfield.label;"
+ accesskey="&keywordfield.accesskey;"
+ oncommand="AddKeywordForSearchField();"/>
+ <menuitem id="context-searchselect"
+ oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
+ <menuseparator id="context-sep-sendlinktodevice" hidden="true"/>
+ <menu id="context-sendlinktodevice"
+ label="&sendLinkToDevice.label;"
+ accesskey="&sendLinkToDevice.accesskey;"
+ hidden="true">
+ <menupopup id="context-sendlinktodevice-popup"
+ onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
+ </menu>
+ <menuitem id="context-shareselect"
+ label="&shareSelect.label;"
+ accesskey="&shareSelect.accesskey;"
+ oncommand="gContextMenu.shareSelect();"/>
+ <menuseparator id="frame-sep"/>
+ <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
+ <menupopup>
+ <menuitem id="context-showonlythisframe"
+ label="&showOnlyThisFrameCmd.label;"
+ accesskey="&showOnlyThisFrameCmd.accesskey;"
+ oncommand="gContextMenu.showOnlyThisFrame();"/>
+ <menuitem id="context-openframeintab"
+ label="&openFrameCmdInTab.label;"
+ accesskey="&openFrameCmdInTab.accesskey;"
+ oncommand="gContextMenu.openFrameInTab();"/>
+ <menuitem id="context-openframe"
+ label="&openFrameCmd.label;"
+ accesskey="&openFrameCmd.accesskey;"
+ oncommand="gContextMenu.openFrame();"/>
+ <menuseparator id="open-frame-sep"/>
+ <menuitem id="context-reloadframe"
+ label="&reloadFrameCmd.label;"
+ accesskey="&reloadFrameCmd.accesskey;"
+ oncommand="gContextMenu.reloadFrame();"/>
+ <menuseparator/>
+ <menuitem id="context-bookmarkframe"
+ label="&bookmarkThisFrameCmd.label;"
+ accesskey="&bookmarkThisFrameCmd.accesskey;"
+ oncommand="gContextMenu.addBookmarkForFrame();"/>
+ <menuitem id="context-saveframe"
+ label="&saveFrameCmd.label;"
+ accesskey="&saveFrameCmd.accesskey;"
+ oncommand="gContextMenu.saveFrame();"/>
+ <menuseparator/>
+ <menuitem id="context-printframe"
+ label="&printFrameCmd.label;"
+ accesskey="&printFrameCmd.accesskey;"
+ oncommand="gContextMenu.printFrame();"/>
+ <menuseparator/>
+ <menuitem id="context-viewframesource"
+ label="&viewFrameSourceCmd.label;"
+ accesskey="&viewFrameSourceCmd.accesskey;"
+ oncommand="gContextMenu.viewFrameSource();"
+ observes="isFrameImage"/>
+ <menuitem id="context-viewframeinfo"
+ label="&viewFrameInfoCmd.label;"
+ accesskey="&viewFrameInfoCmd.accesskey;"
+ oncommand="gContextMenu.viewFrameInfo();"/>
+ </menupopup>
+ </menu>
+ <menuitem id="context-viewpartialsource-selection"
+ label="&viewPartialSourceForSelectionCmd.label;"
+ accesskey="&viewPartialSourceCmd.accesskey;"
+ oncommand="gContextMenu.viewPartialSource('selection');"
+ observes="isImage"/>
+ <menuitem id="context-viewpartialsource-mathml"
+ label="&viewPartialSourceForMathMLCmd.label;"
+ accesskey="&viewPartialSourceCmd.accesskey;"
+ oncommand="gContextMenu.viewPartialSource('mathml');"
+ observes="isImage"/>
+ <menuseparator id="context-sep-viewsource"/>
+ <menuitem id="context-viewsource"
+ label="&viewPageSourceCmd.label;"
+ accesskey="&viewPageSourceCmd.accesskey;"
+ oncommand="BrowserViewSource(gContextMenu.browser);"
+ observes="canViewSource"/>
+ <menuitem id="context-viewinfo"
+ label="&viewPageInfoCmd.label;"
+ accesskey="&viewPageInfoCmd.accesskey;"
+ oncommand="gContextMenu.viewInfo();"/>
+ <menuseparator id="spell-separator"/>
+ <menuitem id="spell-check-enabled"
+ label="&spellCheckToggle.label;"
+ type="checkbox"
+ accesskey="&spellCheckToggle.accesskey;"
+ oncommand="InlineSpellCheckerUI.toggleEnabled(window);"/>
+ <menuitem id="spell-add-dictionaries-main"
+ label="&spellAddDictionaries.label;"
+ accesskey="&spellAddDictionaries.accesskey;"
+ oncommand="gContextMenu.addDictionaries();"/>
+ <menu id="spell-dictionaries"
+ label="&spellDictionaries.label;"
+ accesskey="&spellDictionaries.accesskey;">
+ <menupopup id="spell-dictionaries-menu">
+ <menuseparator id="spell-language-separator"/>
+ <menuitem id="spell-add-dictionaries"
+ label="&spellAddDictionaries.label;"
+ accesskey="&spellAddDictionaries.accesskey;"
+ oncommand="gContextMenu.addDictionaries();"/>
+ </menupopup>
+ </menu>
+ <menuseparator hidden="true" id="context-sep-bidi"/>
+ <menuitem hidden="true" id="context-bidi-text-direction-toggle"
+ label="&bidiSwitchTextDirectionItem.label;"
+ accesskey="&bidiSwitchTextDirectionItem.accesskey;"
+ command="cmd_switchTextDirection"/>
+ <menuitem hidden="true" id="context-bidi-page-direction-toggle"
+ label="&bidiSwitchPageDirectionItem.label;"
+ accesskey="&bidiSwitchPageDirectionItem.accesskey;"
+ oncommand="gContextMenu.switchPageDirection();"/>
+ <menuseparator id="fill-login-separator" hidden="true"/>
+ <menu id="fill-login"
+ label="&fillLoginMenu.label;"
+ label-login="&fillLoginMenu.label;"
+ label-password="&fillPasswordMenu.label;"
+ label-username="&fillUsernameMenu.label;"
+ accesskey="&fillLoginMenu.accesskey;"
+ accesskey-login="&fillLoginMenu.accesskey;"
+ accesskey-password="&fillPasswordMenu.accesskey;"
+ accesskey-username="&fillUsernameMenu.accesskey;"
+ hidden="true">
+ <menupopup id="fill-login-popup">
+ <menuitem id="fill-login-no-logins"
+ label="&noLoginSuggestions.label;"
+ disabled="true"
+ hidden="true"/>
+ <menuseparator id="saved-logins-separator"/>
+ <menuitem id="fill-login-saved-passwords"
+ label="&viewSavedLogins.label;"
+ oncommand="gContextMenu.openPasswordManager();"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="inspect-separator" hidden="true"/>
+ <menuitem id="context-inspect"
+ hidden="true"
+ label="&inspectContextMenu.label;"
+ accesskey="&inspectContextMenu.accesskey;"
+ oncommand="gContextMenu.inspectNode();"/>
+ <menuseparator id="context-media-eme-separator" hidden="true"/>
+ <menuitem id="context-media-eme-learnmore"
+ class="menuitem-iconic"
+ hidden="true"
+ label="&emeLearnMoreContextMenu.label;"
+ accesskey="&emeLearnMoreContextMenu.accesskey;"
+ oncommand="gContextMenu.drmLearnMore(event);"
+ onclick="checkForMiddleClick(this, event);"/>
diff --git a/browser/base/content/browser-ctrlTab.js b/browser/base/content/browser-ctrlTab.js
new file mode 100644
index 000000000..c761ea095
--- /dev/null
+++ b/browser/base/content/browser-ctrlTab.js
@@ -0,0 +1,587 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tab previews utility, produces thumbnails
+ */
+var tabPreviews = {
+ init: function tabPreviews_init() {
+ if (this._selectedTab)
+ return;
+ this._selectedTab = gBrowser.selectedTab;
+
+ gBrowser.tabContainer.addEventListener("TabSelect", this, false);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", this, false);
+
+ let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
+ .getService(Ci.nsIScreenManager);
+ let left = {}, top = {}, width = {}, height = {};
+ screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height);
+ this.aspectRatio = height.value / width.value;
+ },
+
+ get: function tabPreviews_get(aTab) {
+ let uri = aTab.linkedBrowser.currentURI.spec;
+
+ if (aTab.__thumbnail_lastURI &&
+ aTab.__thumbnail_lastURI != uri) {
+ aTab.__thumbnail = null;
+ aTab.__thumbnail_lastURI = null;
+ }
+
+ if (aTab.__thumbnail)
+ return aTab.__thumbnail;
+
+ if (aTab.getAttribute("pending") == "true") {
+ let img = new Image;
+ img.src = PageThumbs.getThumbnailURL(uri);
+ return img;
+ }
+
+ return this.capture(aTab, !aTab.hasAttribute("busy"));
+ },
+
+ capture: function tabPreviews_capture(aTab, aShouldCache) {
+ let browser = aTab.linkedBrowser;
+ let uri = browser.currentURI.spec;
+ let canvas = PageThumbs.createCanvas(window);
+ PageThumbs.shouldStoreThumbnail(browser, (aDoStore) => {
+ if (aDoStore && aShouldCache) {
+ PageThumbs.captureAndStore(browser, function () {
+ let img = new Image;
+ img.src = PageThumbs.getThumbnailURL(uri);
+ aTab.__thumbnail = img;
+ aTab.__thumbnail_lastURI = uri;
+ canvas.getContext("2d").drawImage(img, 0, 0);
+ });
+ } else {
+ PageThumbs.captureToCanvas(browser, canvas, () => {
+ if (aShouldCache) {
+ aTab.__thumbnail = canvas;
+ aTab.__thumbnail_lastURI = uri;
+ }
+ });
+ }
+ });
+ return canvas;
+ },
+
+ handleEvent: function tabPreviews_handleEvent(event) {
+ switch (event.type) {
+ case "TabSelect":
+ if (this._selectedTab &&
+ this._selectedTab.parentNode &&
+ !this._pendingUpdate) {
+ // Generate a thumbnail for the tab that was selected.
+ // The timeout keeps the UI snappy and prevents us from generating thumbnails
+ // for tabs that will be closed. During that timeout, don't generate other
+ // thumbnails in case multiple TabSelect events occur fast in succession.
+ this._pendingUpdate = true;
+ setTimeout(function (self, aTab) {
+ self._pendingUpdate = false;
+ if (aTab.parentNode &&
+ !aTab.hasAttribute("busy") &&
+ !aTab.hasAttribute("pending"))
+ self.capture(aTab, true);
+ }, 2000, this, this._selectedTab);
+ }
+ this._selectedTab = event.target;
+ break;
+ case "SSTabRestored":
+ this.capture(event.target, true);
+ break;
+ }
+ }
+};
+
+var tabPreviewPanelHelper = {
+ opening: function (host) {
+ host.panel.hidden = false;
+
+ var handler = this._generateHandler(host);
+ host.panel.addEventListener("popupshown", handler, false);
+ host.panel.addEventListener("popuphiding", handler, false);
+
+ host._prevFocus = document.commandDispatcher.focusedElement;
+ },
+ _generateHandler: function (host) {
+ var self = this;
+ return function (event) {
+ if (event.target == host.panel) {
+ host.panel.removeEventListener(event.type, arguments.callee, false);
+ self["_" + event.type](host);
+ }
+ };
+ },
+ _popupshown: function (host) {
+ if ("setupGUI" in host)
+ host.setupGUI();
+ },
+ _popuphiding: function (host) {
+ if ("suspendGUI" in host)
+ host.suspendGUI();
+
+ if (host._prevFocus) {
+ Services.focus.setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL);
+ host._prevFocus = null;
+ } else
+ gBrowser.selectedBrowser.focus();
+
+ if (host.tabToSelect) {
+ gBrowser.selectedTab = host.tabToSelect;
+ host.tabToSelect = null;
+ }
+ }
+};
+
+/**
+ * Ctrl-Tab panel
+ */
+var ctrlTab = {
+ get panel () {
+ delete this.panel;
+ return this.panel = document.getElementById("ctrlTab-panel");
+ },
+ get showAllButton () {
+ delete this.showAllButton;
+ return this.showAllButton = document.getElementById("ctrlTab-showAll");
+ },
+ get previews () {
+ delete this.previews;
+ return this.previews = this.panel.getElementsByClassName("ctrlTab-preview");
+ },
+ get maxTabPreviews () {
+ delete this.maxTabPreviews;
+ return this.maxTabPreviews = this.previews.length - 1;
+ },
+ get canvasWidth () {
+ delete this.canvasWidth;
+ return this.canvasWidth = Math.ceil(screen.availWidth * .85 / this.maxTabPreviews);
+ },
+ get canvasHeight () {
+ delete this.canvasHeight;
+ return this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio);
+ },
+ get keys () {
+ var keys = {};
+ ["close", "find", "selectAll"].forEach(function (key) {
+ keys[key] = document.getElementById("key_" + key)
+ .getAttribute("key")
+ .toLocaleLowerCase().charCodeAt(0);
+ });
+ delete this.keys;
+ return this.keys = keys;
+ },
+ _selectedIndex: 0,
+ get selected () {
+ return this._selectedIndex < 0 ?
+ document.activeElement :
+ this.previews.item(this._selectedIndex);
+ },
+ get isOpen () {
+ return this.panel.state == "open" || this.panel.state == "showing" || this._timer;
+ },
+ get tabCount () {
+ return this.tabList.length;
+ },
+ get tabPreviewCount () {
+ return Math.min(this.maxTabPreviews, this.tabCount);
+ },
+
+ get tabList () {
+ return this._recentlyUsedTabs;
+ },
+
+ init: function ctrlTab_init() {
+ if (!this._recentlyUsedTabs) {
+ tabPreviews.init();
+
+ this._initRecentlyUsedTabs();
+ this._init(true);
+ }
+ },
+
+ uninit: function ctrlTab_uninit() {
+ this._recentlyUsedTabs = null;
+ this._init(false);
+ },
+
+ prefName: "browser.ctrlTab.previews",
+ readPref: function ctrlTab_readPref() {
+ var enable =
+ gPrefService.getBoolPref(this.prefName) &&
+ (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") ||
+ !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders"));
+
+ if (enable)
+ this.init();
+ else
+ this.uninit();
+ },
+ observe: function (aSubject, aTopic, aPrefName) {
+ this.readPref();
+ },
+
+ updatePreviews: function ctrlTab_updatePreviews() {
+ for (let i = 0; i < this.previews.length; i++)
+ this.updatePreview(this.previews[i], this.tabList[i]);
+
+ var showAllLabel = gNavigatorBundle.getString("ctrlTab.listAllTabs.label");
+ this.showAllButton.label =
+ PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount);
+ this.showAllButton.hidden = !allTabs.canOpen;
+ },
+
+ updatePreview: function ctrlTab_updatePreview(aPreview, aTab) {
+ if (aPreview == this.showAllButton)
+ return;
+
+ aPreview._tab = aTab;
+
+ if (aPreview.firstChild)
+ aPreview.removeChild(aPreview.firstChild);
+ if (aTab) {
+ let canvasWidth = this.canvasWidth;
+ let canvasHeight = this.canvasHeight;
+ aPreview.appendChild(tabPreviews.get(aTab));
+ aPreview.setAttribute("label", aTab.label);
+ aPreview.setAttribute("tooltiptext", aTab.label);
+ aPreview.setAttribute("crop", aTab.crop);
+ aPreview.setAttribute("canvaswidth", canvasWidth);
+ aPreview.setAttribute("canvasstyle",
+ "max-width:" + canvasWidth + "px;" +
+ "min-width:" + canvasWidth + "px;" +
+ "max-height:" + canvasHeight + "px;" +
+ "min-height:" + canvasHeight + "px;");
+ if (aTab.image)
+ aPreview.setAttribute("image", aTab.image);
+ else
+ aPreview.removeAttribute("image");
+ aPreview.hidden = false;
+ } else {
+ aPreview.hidden = true;
+ aPreview.removeAttribute("label");
+ aPreview.removeAttribute("tooltiptext");
+ aPreview.removeAttribute("image");
+ }
+ },
+
+ advanceFocus: function ctrlTab_advanceFocus(aForward) {
+ let selectedIndex = Array.indexOf(this.previews, this.selected);
+ do {
+ selectedIndex += aForward ? 1 : -1;
+ if (selectedIndex < 0)
+ selectedIndex = this.previews.length - 1;
+ else if (selectedIndex >= this.previews.length)
+ selectedIndex = 0;
+ } while (this.previews[selectedIndex].hidden);
+
+ if (this._selectedIndex == -1) {
+ // Focus is already in the panel.
+ this.previews[selectedIndex].focus();
+ } else {
+ this._selectedIndex = selectedIndex;
+ }
+
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ this._openPanel();
+ }
+ },
+
+ _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) {
+ if (this._trackMouseOver)
+ aPreview.focus();
+ },
+
+ pick: function ctrlTab_pick(aPreview) {
+ if (!this.tabCount)
+ return;
+
+ var select = (aPreview || this.selected);
+
+ if (select == this.showAllButton)
+ this.showAllTabs();
+ else
+ this.close(select._tab);
+ },
+
+ showAllTabs: function ctrlTab_showAllTabs(aPreview) {
+ this.close();
+ document.getElementById("Browser:ShowAllTabs").doCommand();
+ },
+
+ remove: function ctrlTab_remove(aPreview) {
+ if (aPreview._tab)
+ gBrowser.removeTab(aPreview._tab);
+ },
+
+ attachTab: function ctrlTab_attachTab(aTab, aPos) {
+ if (aTab.closing)
+ return;
+
+ if (aPos == 0)
+ this._recentlyUsedTabs.unshift(aTab);
+ else if (aPos)
+ this._recentlyUsedTabs.splice(aPos, 0, aTab);
+ else
+ this._recentlyUsedTabs.push(aTab);
+ },
+
+ detachTab: function ctrlTab_detachTab(aTab) {
+ var i = this._recentlyUsedTabs.indexOf(aTab);
+ if (i >= 0)
+ this._recentlyUsedTabs.splice(i, 1);
+ },
+
+ open: function ctrlTab_open() {
+ if (this.isOpen)
+ return;
+
+ document.addEventListener("keyup", this, true);
+
+ this.updatePreviews();
+ this._selectedIndex = 1;
+
+ // Add a slight delay before showing the UI, so that a quick
+ // "ctrl-tab" keypress just flips back to the MRU tab.
+ this._timer = setTimeout(function (self) {
+ self._timer = null;
+ self._openPanel();
+ }, 200, this);
+ },
+
+ _openPanel: function ctrlTab_openPanel() {
+ tabPreviewPanelHelper.opening(this);
+
+ this.panel.width = Math.min(screen.availWidth * .99,
+ this.canvasWidth * 1.25 * this.tabPreviewCount);
+ var estimateHeight = this.canvasHeight * 1.25 + 75;
+ this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2,
+ screen.availTop + (screen.availHeight - estimateHeight) / 2,
+ false);
+ },
+
+ close: function ctrlTab_close(aTabToSelect) {
+ if (!this.isOpen)
+ return;
+
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = null;
+ this.suspendGUI();
+ if (aTabToSelect)
+ gBrowser.selectedTab = aTabToSelect;
+ return;
+ }
+
+ this.tabToSelect = aTabToSelect;
+ this.panel.hidePopup();
+ },
+
+ setupGUI: function ctrlTab_setupGUI() {
+ this.selected.focus();
+ this._selectedIndex = -1;
+
+ // Track mouse movement after a brief delay so that the item that happens
+ // to be under the mouse pointer initially won't be selected unintentionally.
+ this._trackMouseOver = false;
+ setTimeout(function (self) {
+ if (self.isOpen)
+ self._trackMouseOver = true;
+ }, 0, this);
+ },
+
+ suspendGUI: function ctrlTab_suspendGUI() {
+ document.removeEventListener("keyup", this, true);
+
+ for (let preview of this.previews) {
+ this.updatePreview(preview, null);
+ }
+ },
+
+ onKeyPress: function ctrlTab_onKeyPress(event) {
+ var isOpen = this.isOpen;
+
+ if (isOpen) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ switch (event.keyCode) {
+ case event.DOM_VK_TAB:
+ if (event.ctrlKey && !event.altKey && !event.metaKey) {
+ if (isOpen) {
+ this.advanceFocus(!event.shiftKey);
+ } else if (!event.shiftKey) {
+ event.preventDefault();
+ event.stopPropagation();
+ let tabs = gBrowser.visibleTabs;
+ if (tabs.length > 2) {
+ this.open();
+ } else if (tabs.length == 2) {
+ let index = tabs[0].selected ? 1 : 0;
+ gBrowser.selectedTab = tabs[index];
+ }
+ }
+ }
+ break;
+ default:
+ if (isOpen && event.ctrlKey) {
+ if (event.keyCode == event.DOM_VK_DELETE) {
+ this.remove(this.selected);
+ break;
+ }
+ switch (event.charCode) {
+ case this.keys.close:
+ this.remove(this.selected);
+ break;
+ case this.keys.find:
+ case this.keys.selectAll:
+ this.showAllTabs();
+ break;
+ }
+ }
+ }
+ },
+
+ removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) {
+ if (this.tabCount == 2) {
+ this.close();
+ return;
+ }
+
+ this.updatePreviews();
+
+ if (this.selected.hidden)
+ this.advanceFocus(false);
+ if (this.selected == this.showAllButton)
+ this.advanceFocus(false);
+
+ // If the current tab is removed, another tab can steal our focus.
+ if (aTab.selected && this.panel.state == "open") {
+ setTimeout(function (selected) {
+ selected.focus();
+ }, 0, this.selected);
+ }
+ },
+
+ handleEvent: function ctrlTab_handleEvent(event) {
+ switch (event.type) {
+ case "SSWindowRestored":
+ this._initRecentlyUsedTabs();
+ break;
+ case "TabAttrModified":
+ // tab attribute modified (e.g. label, crop, busy, image, selected)
+ for (let i = this.previews.length - 1; i >= 0; i--) {
+ if (this.previews[i]._tab && this.previews[i]._tab == event.target) {
+ this.updatePreview(this.previews[i], event.target);
+ break;
+ }
+ }
+ break;
+ case "TabSelect":
+ this.detachTab(event.target);
+ this.attachTab(event.target, 0);
+ break;
+ case "TabOpen":
+ this.attachTab(event.target, 1);
+ break;
+ case "TabClose":
+ this.detachTab(event.target);
+ if (this.isOpen)
+ this.removeClosingTabFromUI(event.target);
+ break;
+ case "keypress":
+ this.onKeyPress(event);
+ break;
+ case "keyup":
+ if (event.keyCode == event.DOM_VK_CONTROL)
+ this.pick();
+ break;
+ case "popupshowing":
+ if (event.target.id == "menu_viewPopup")
+ document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen;
+ break;
+ }
+ },
+
+ filterForThumbnailExpiration: function (aCallback) {
+ // Save a few more thumbnails than we actually display, so that when tabs
+ // are closed, the previews we add instead still get thumbnails.
+ const extraThumbnails = 3;
+ const thumbnailCount = Math.min(this.tabPreviewCount + extraThumbnails,
+ this.tabCount);
+
+ let urls = [];
+ for (let i = 0; i < thumbnailCount; i++)
+ urls.push(this.tabList[i].linkedBrowser.currentURI.spec);
+
+ aCallback(urls);
+ },
+
+ _initRecentlyUsedTabs: function () {
+ this._recentlyUsedTabs =
+ Array.filter(gBrowser.tabs, tab => !tab.closing)
+ .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed);
+ },
+
+ _init: function ctrlTab__init(enable) {
+ var toggleEventListener = enable ? "addEventListener" : "removeEventListener";
+
+ window[toggleEventListener]("SSWindowRestored", this, false);
+
+ var tabContainer = gBrowser.tabContainer;
+ tabContainer[toggleEventListener]("TabOpen", this, false);
+ tabContainer[toggleEventListener]("TabAttrModified", this, false);
+ tabContainer[toggleEventListener]("TabSelect", this, false);
+ tabContainer[toggleEventListener]("TabClose", this, false);
+
+ document[toggleEventListener]("keypress", this, false);
+ gBrowser.mTabBox.handleCtrlTab = !enable;
+
+ if (enable)
+ PageThumbs.addExpirationFilter(this);
+ else
+ PageThumbs.removeExpirationFilter(this);
+
+ // If we're not running, hide the "Show All Tabs" menu item,
+ // as Shift+Ctrl+Tab will be handled by the tab bar.
+ document.getElementById("menu_showAllTabs").hidden = !enable;
+ document.getElementById("menu_viewPopup")[toggleEventListener]("popupshowing", this);
+
+ // Also disable the <key> to ensure Shift+Ctrl+Tab never triggers
+ // Show All Tabs.
+ var key_showAllTabs = document.getElementById("key_showAllTabs");
+ if (enable)
+ key_showAllTabs.removeAttribute("disabled");
+ else
+ key_showAllTabs.setAttribute("disabled", "true");
+ }
+};
+
+
+/**
+ * All Tabs menu
+ */
+var allTabs = {
+ get toolbarButton() {
+ return document.getElementById("alltabs-button");
+ },
+
+ get canOpen() {
+ return isElementVisible(this.toolbarButton);
+ },
+
+ open: function allTabs_open() {
+ if (this.canOpen) {
+ // Without setTimeout, the menupopup won't stay open when invoking
+ // "View > Show All Tabs" and the menu bar auto-hides.
+ setTimeout(() => {
+ this.toolbarButton.open = true;
+ }, 0);
+ }
+ }
+};
diff --git a/browser/base/content/browser-customization.js b/browser/base/content/browser-customization.js
new file mode 100644
index 000000000..d5d51b893
--- /dev/null
+++ b/browser/base/content/browser-customization.js
@@ -0,0 +1,100 @@
+/* -*- 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/. */
+
+/**
+ * Customization handler prepares this browser window for entering and exiting
+ * customization mode by handling customizationstarting and customizationending
+ * events.
+ */
+var CustomizationHandler = {
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "customizationstarting":
+ this._customizationStarting();
+ break;
+ case "customizationchange":
+ this._customizationChange();
+ break;
+ case "customizationending":
+ this._customizationEnding(aEvent.detail);
+ break;
+ }
+ },
+
+ isCustomizing: function() {
+ return document.documentElement.hasAttribute("customizing");
+ },
+
+ _customizationStarting: function() {
+ // Disable the toolbar context menu items
+ let menubar = document.getElementById("main-menubar");
+ for (let childNode of menubar.childNodes)
+ childNode.setAttribute("disabled", true);
+
+ let cmd = document.getElementById("cmd_CustomizeToolbars");
+ cmd.setAttribute("disabled", "true");
+
+ UpdateUrlbarSearchSplitterState();
+
+ CombinedStopReload.uninit();
+ PlacesToolbarHelper.customizeStart();
+ DownloadsButton.customizeStart();
+
+ // The additional padding on the sides of the browser
+ // can cause the customize tab to get clipped.
+ let tabContainer = gBrowser.tabContainer;
+ if (tabContainer.getAttribute("overflow") == "true") {
+ let tabstrip = tabContainer.mTabstrip;
+ tabstrip.ensureElementIsVisible(gBrowser.selectedTab, true);
+ }
+ },
+
+ _customizationChange: function() {
+ PlacesToolbarHelper.customizeChange();
+ },
+
+ _customizationEnding: function(aDetails) {
+ // Update global UI elements that may have been added or removed
+ if (aDetails.changed) {
+ gURLBar = document.getElementById("urlbar");
+
+ gHomeButton.updateTooltip();
+ XULBrowserWindow.init();
+
+ if (AppConstants.platform != "macosx")
+ updateEditUIVisibility();
+
+ // Hacky: update the PopupNotifications' object's reference to the iconBox,
+ // if it already exists, since it may have changed if the URL bar was
+ // added/removed.
+ if (!window.__lookupGetter__("PopupNotifications")) {
+ PopupNotifications.iconBox =
+ document.getElementById("notification-popup-box");
+ }
+
+ }
+
+ PlacesToolbarHelper.customizeDone();
+ DownloadsButton.customizeDone();
+
+ // The url bar splitter state is dependent on whether stop/reload
+ // and the location bar are combined, so we need this ordering
+ CombinedStopReload.init();
+ UpdateUrlbarSearchSplitterState();
+
+ // Update the urlbar
+ URLBarSetURI();
+ XULBrowserWindow.asyncUpdateUI();
+
+ // Re-enable parts of the UI we disabled during the dialog
+ let menubar = document.getElementById("main-menubar");
+ for (let childNode of menubar.childNodes)
+ childNode.setAttribute("disabled", false);
+ let cmd = document.getElementById("cmd_CustomizeToolbars");
+ cmd.removeAttribute("disabled");
+
+ gBrowser.selectedBrowser.focus();
+ }
+}
diff --git a/browser/base/content/browser-data-submission-info-bar.js b/browser/base/content/browser-data-submission-info-bar.js
new file mode 100644
index 000000000..0c87d199f
--- /dev/null
+++ b/browser/base/content/browser-data-submission-info-bar.js
@@ -0,0 +1,127 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "DataNotificationInfoBar::";
+
+/**
+ * Represents an info bar that shows a data submission notification.
+ */
+var gDataNotificationInfoBar = {
+ _OBSERVERS: [
+ "datareporting:notify-data-policy:request",
+ "datareporting:notify-data-policy:close",
+ ],
+
+ _DATA_REPORTING_NOTIFICATION: "data-reporting",
+
+ get _notificationBox() {
+ delete this._notificationBox;
+ return this._notificationBox = document.getElementById("global-notificationbox");
+ },
+
+ get _log() {
+ let Log = Cu.import("resource://gre/modules/Log.jsm", {}).Log;
+ delete this._log;
+ return this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ },
+
+ init: function() {
+ window.addEventListener("unload", () => {
+ for (let o of this._OBSERVERS) {
+ Services.obs.removeObserver(this, o);
+ }
+ }, false);
+
+ for (let o of this._OBSERVERS) {
+ Services.obs.addObserver(this, o, true);
+ }
+ },
+
+ _getDataReportingNotification: function (name=this._DATA_REPORTING_NOTIFICATION) {
+ return this._notificationBox.getNotificationWithValue(name);
+ },
+
+ _displayDataPolicyInfoBar: function (request) {
+ if (this._getDataReportingNotification()) {
+ return;
+ }
+
+ let brandBundle = document.getElementById("bundle_brand");
+ let appName = brandBundle.getString("brandShortName");
+ let vendorName = brandBundle.getString("vendorShortName");
+
+ let message = gNavigatorBundle.getFormattedString(
+ "dataReportingNotification.message",
+ [appName, vendorName]);
+
+ this._actionTaken = false;
+
+ let buttons = [{
+ label: gNavigatorBundle.getString("dataReportingNotification.button.label"),
+ accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"),
+ popup: null,
+ callback: () => {
+ this._actionTaken = true;
+ window.openAdvancedPreferences("dataChoicesTab");
+ },
+ }];
+
+ this._log.info("Creating data reporting policy notification.");
+ this._notificationBox.appendNotification(
+ message,
+ this._DATA_REPORTING_NOTIFICATION,
+ null,
+ this._notificationBox.PRIORITY_INFO_HIGH,
+ buttons,
+ event => {
+ if (event == "removed") {
+ Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null);
+ }
+ }
+ );
+ // It is important to defer calling onUserNotifyComplete() until we're
+ // actually sure the notification was displayed. If we ever called
+ // onUserNotifyComplete() without showing anything to the user, that
+ // would be very good for user choice. It may also have legal impact.
+ request.onUserNotifyComplete();
+ },
+
+ _clearPolicyNotification: function () {
+ let notification = this._getDataReportingNotification();
+ if (notification) {
+ this._log.debug("Closing notification.");
+ notification.close();
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case "datareporting:notify-data-policy:request":
+ let request = subject.wrappedJSObject.object;
+ try {
+ this._displayDataPolicyInfoBar(request);
+ } catch (ex) {
+ request.onUserNotifyFailed(ex);
+ return;
+ }
+ break;
+
+ case "datareporting:notify-data-policy:close":
+ // If this observer fires, it means something else took care of
+ // responding. Therefore, we don't need to do anything. So, we
+ // act like we took action and clear state.
+ this._actionTaken = true;
+ this._clearPolicyNotification();
+ break;
+
+ default:
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ]),
+};
diff --git a/browser/base/content/browser-devedition.js b/browser/base/content/browser-devedition.js
new file mode 100644
index 000000000..0dc1e94da
--- /dev/null
+++ b/browser/base/content/browser-devedition.js
@@ -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/. */
+
+/**
+ * Listeners for the DevEdition theme. This adds an extra stylesheet
+ * to browser.xul if a pref is set and no other themes are applied.
+ */
+var DevEdition = {
+ _devtoolsThemePrefName: "devtools.theme",
+ styleSheetLocation: "chrome://browser/skin/devedition.css",
+ styleSheet: null,
+ initialized: false,
+
+ get isStyleSheetEnabled() {
+ return this.styleSheet && !this.styleSheet.sheet.disabled;
+ },
+
+ get isThemeCurrentlyApplied() {
+ let theme = LightweightThemeManager.currentTheme;
+ return theme && theme.id == "firefox-devedition@mozilla.org";
+ },
+
+ init: function () {
+ this.initialized = true;
+ Services.prefs.addObserver(this._devtoolsThemePrefName, this, false);
+ Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
+ Services.obs.addObserver(this, "lightweight-theme-window-updated", false);
+ this._updateDevtoolsThemeAttribute();
+
+ if (this.isThemeCurrentlyApplied) {
+ this._toggleStyleSheet(true);
+ }
+ },
+
+ createStyleSheet: function() {
+ let styleSheetAttr = `href="${this.styleSheetLocation}" type="text/css"`;
+ this.styleSheet = document.createProcessingInstruction(
+ "xml-stylesheet", styleSheetAttr);
+ this.styleSheet.addEventListener("load", this);
+ document.insertBefore(this.styleSheet, document.documentElement);
+ this.styleSheet.sheet.disabled = true;
+ },
+
+ observe: function (subject, topic, data) {
+ if (topic == "lightweight-theme-styling-update") {
+ let newTheme = JSON.parse(data);
+ if (newTheme && newTheme.id == "firefox-devedition@mozilla.org") {
+ this._toggleStyleSheet(true);
+ } else {
+ this._toggleStyleSheet(false);
+ }
+ } else if (topic == "lightweight-theme-window-updated" && subject == window) {
+ this._updateLWTBrightness();
+ }
+
+ if (topic == "nsPref:changed" && data == this._devtoolsThemePrefName) {
+ this._updateDevtoolsThemeAttribute();
+ }
+ },
+
+ _inferBrightness: function() {
+ ToolbarIconColor.inferFromText();
+ // Get an inverted full screen button if the dark theme is applied.
+ if (this.isStyleSheetEnabled &&
+ document.documentElement.getAttribute("devtoolstheme") == "dark") {
+ document.documentElement.setAttribute("brighttitlebarforeground", "true");
+ } else {
+ document.documentElement.removeAttribute("brighttitlebarforeground");
+ }
+ },
+
+ _updateLWTBrightness() {
+ if (this.isThemeCurrentlyApplied) {
+ let devtoolsTheme = Services.prefs.getCharPref(this._devtoolsThemePrefName);
+ let textColor = devtoolsTheme == "dark" ? "bright" : "dark";
+ document.documentElement.setAttribute("lwthemetextcolor", textColor);
+ }
+ },
+
+ _updateDevtoolsThemeAttribute: function() {
+ // Set an attribute on root element to make it possible
+ // to change colors based on the selected devtools theme.
+ let devtoolsTheme = Services.prefs.getCharPref(this._devtoolsThemePrefName);
+ if (devtoolsTheme != "dark") {
+ devtoolsTheme = "light";
+ }
+ document.documentElement.setAttribute("devtoolstheme", devtoolsTheme);
+ this._updateLWTBrightness();
+ this._inferBrightness();
+ },
+
+ handleEvent: function(e) {
+ if (e.type === "load") {
+ this.styleSheet.removeEventListener("load", this);
+ this.refreshBrowserDisplay();
+ }
+ },
+
+ refreshBrowserDisplay: function() {
+ // Don't touch things on the browser if gBrowserInit.onLoad hasn't
+ // yet fired.
+ if (this.initialized) {
+ gBrowser.tabContainer._positionPinnedTabs();
+ this._inferBrightness();
+ }
+ },
+
+ _toggleStyleSheet: function(deveditionThemeEnabled) {
+ let wasEnabled = this.isStyleSheetEnabled;
+ if (deveditionThemeEnabled && !wasEnabled) {
+ // The stylesheet may not have been created yet if it wasn't
+ // needed on initial load. Make it now.
+ if (!this.styleSheet) {
+ this.createStyleSheet();
+ }
+ this.styleSheet.sheet.disabled = false;
+ this.refreshBrowserDisplay();
+ } else if (!deveditionThemeEnabled && wasEnabled) {
+ this.styleSheet.sheet.disabled = true;
+ this.refreshBrowserDisplay();
+ }
+ },
+
+ uninit: function () {
+ Services.prefs.removeObserver(this._devtoolsThemePrefName, this);
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update", false);
+ Services.obs.removeObserver(this, "lightweight-theme-window-updated", false);
+ if (this.styleSheet) {
+ this.styleSheet.removeEventListener("load", this);
+ }
+ this.styleSheet = null;
+ }
+};
+
+// If the DevEdition theme is going to be applied in gBrowserInit.onLoad,
+// then preload it now. This prevents a flash of unstyled content where the
+// normal theme is applied while the DevEdition stylesheet is loading.
+if (!AppConstants.RELEASE_OR_BETA &&
+ this != Services.appShell.hiddenDOMWindow && DevEdition.isThemeCurrentlyApplied) {
+ DevEdition.createStyleSheet();
+}
diff --git a/browser/base/content/browser-doctype.inc b/browser/base/content/browser-doctype.inc
new file mode 100644
index 000000000..10015d898
--- /dev/null
+++ b/browser/base/content/browser-doctype.inc
@@ -0,0 +1,23 @@
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" >
+%browserDTD;
+<!ENTITY % baseMenuDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd" >
+%baseMenuDTD;
+<!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetMenu.dtd" >
+%charsetDTD;
+<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" >
+%textcontextDTD;
+<!ENTITY % customizeToolbarDTD SYSTEM "chrome://global/locale/customizeToolbar.dtd">
+ %customizeToolbarDTD;
+<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd">
+%placesDTD;
+<!ENTITY % safebrowsingDTD SYSTEM "chrome://browser/locale/safebrowsing/phishing-afterload-warning-message.dtd">
+%safebrowsingDTD;
+<!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd">
+%aboutHomeDTD;
+<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
+%syncBrandDTD;
+]>
+
diff --git a/browser/base/content/browser-feeds.js b/browser/base/content/browser-feeds.js
new file mode 100644
index 000000000..6f29d8915
--- /dev/null
+++ b/browser/base/content/browser-feeds.js
@@ -0,0 +1,646 @@
+/* -*- 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/. */
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+
+const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
+const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed";
+const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed";
+
+const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI";
+
+const PREF_SELECTED_APP = "browser.feeds.handlers.application";
+const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice";
+const PREF_SELECTED_ACTION = "browser.feeds.handler";
+const PREF_SELECTED_READER = "browser.feeds.handler.default";
+
+const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application";
+const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice";
+const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler";
+const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default";
+
+const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application";
+const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice";
+const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler";
+const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default";
+
+const PREF_UPDATE_DELAY = 2000;
+
+const SETTABLE_PREFS = new Set([
+ PREF_VIDEO_SELECTED_ACTION,
+ PREF_AUDIO_SELECTED_ACTION,
+ PREF_SELECTED_ACTION,
+ PREF_VIDEO_SELECTED_READER,
+ PREF_AUDIO_SELECTED_READER,
+ PREF_SELECTED_READER,
+ PREF_VIDEO_SELECTED_WEB,
+ PREF_AUDIO_SELECTED_WEB,
+ PREF_SELECTED_WEB
+]);
+
+const EXECUTABLE_PREFS = new Set([
+ PREF_SELECTED_APP,
+ PREF_VIDEO_SELECTED_APP,
+ PREF_AUDIO_SELECTED_APP
+]);
+
+const VALID_ACTIONS = new Set(["ask", "reader", "bookmarks"]);
+const VALID_READERS = new Set(["web", "client", "default", "bookmarks"]);
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "SHOULD_LOG",
+ "feeds.log", false);
+
+function LOG(str) {
+ if (SHOULD_LOG)
+ dump("*** Feeds: " + str + "\n");
+}
+
+function getPrefActionForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_ACTION;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_ACTION;
+
+ default:
+ return PREF_SELECTED_ACTION;
+ }
+}
+
+function getPrefReaderForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_READER;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_READER;
+
+ default:
+ return PREF_SELECTED_READER;
+ }
+}
+
+function getPrefWebForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_WEB;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_WEB;
+
+ default:
+ return PREF_SELECTED_WEB;
+ }
+}
+
+function getPrefAppForType(t) {
+ switch (t) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return PREF_VIDEO_SELECTED_APP;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return PREF_AUDIO_SELECTED_APP;
+
+ default:
+ return PREF_SELECTED_APP;
+ }
+}
+
+/**
+ * Maps a feed type to a maybe-feed mimetype.
+ */
+function getMimeTypeForFeedType(aFeedType) {
+ switch (aFeedType) {
+ case Ci.nsIFeed.TYPE_VIDEO:
+ return TYPE_MAYBE_VIDEO_FEED;
+
+ case Ci.nsIFeed.TYPE_AUDIO:
+ return TYPE_MAYBE_AUDIO_FEED;
+
+ default:
+ return TYPE_MAYBE_FEED;
+ }
+}
+
+/**
+ * The Feed Handler object manages discovery of RSS/ATOM feeds in web pages
+ * and shows UI when they are discovered.
+ */
+var FeedHandler = {
+ _prefChangeCallback: null,
+
+ /** Called when the user clicks on the Subscribe to This Page... menu item,
+ * or when the user clicks the feed button when the page contains multiple
+ * feeds.
+ * Builds a menu of unique feeds associated with the page, and if there
+ * is only one, shows the feed inline in the browser window.
+ * @param container
+ * The feed list container (menupopup or subview) to be populated.
+ * @param isSubview
+ * Whether we're creating a subview (true) or menu (false/undefined)
+ * @return true if the menu/subview should be shown, false if there was only
+ * one feed and the feed should be shown inline in the browser
+ * window (do not show the menupopup/subview).
+ */
+ buildFeedList(container, isSubview) {
+ let feeds = gBrowser.selectedBrowser.feeds;
+ if (!isSubview && feeds == null) {
+ // XXX hack -- menu opening depends on setting of an "open"
+ // attribute, and the menu refuses to open if that attribute is
+ // set (because it thinks it's already open). onpopupshowing gets
+ // called after the attribute is unset, and it doesn't get unset
+ // if we return false. so we unset it here; otherwise, the menu
+ // refuses to work past this point.
+ container.parentNode.removeAttribute("open");
+ return false;
+ }
+
+ for (let i = container.childNodes.length - 1; i >= 0; --i) {
+ let node = container.childNodes[i];
+ if (isSubview && node.localName == "label")
+ continue;
+ container.removeChild(node);
+ }
+
+ if (!feeds || feeds.length <= 1)
+ return false;
+
+ // Build the menu showing the available feed choices for viewing.
+ let itemNodeType = isSubview ? "toolbarbutton" : "menuitem";
+ for (let feedInfo of feeds) {
+ let item = document.createElement(itemNodeType);
+ let baseTitle = feedInfo.title || feedInfo.href;
+ item.setAttribute("label", baseTitle);
+ item.setAttribute("feed", feedInfo.href);
+ item.setAttribute("tooltiptext", feedInfo.href);
+ item.setAttribute("crop", "center");
+ let className = "feed-" + itemNodeType;
+ if (isSubview) {
+ className += " subviewbutton";
+ }
+ item.setAttribute("class", className);
+ container.appendChild(item);
+ }
+ return true;
+ },
+
+ /**
+ * Subscribe to a given feed. Called when
+ * 1. Page has a single feed and user clicks feed icon in location bar
+ * 2. Page has a single feed and user selects Subscribe menu item
+ * 3. Page has multiple feeds and user selects from feed icon popup (or subview)
+ * 4. Page has multiple feeds and user selects from Subscribe submenu
+ * @param href
+ * The feed to subscribe to. May be null, in which case the
+ * event target's feed attribute is examined.
+ * @param event
+ * The event this method is handling. Used to decide where
+ * to open the preview UI. (Optional, unless href is null)
+ */
+ subscribeToFeed(href, event) {
+ // Just load the feed in the content area to either subscribe or show the
+ // preview UI
+ if (!href)
+ href = event.target.getAttribute("feed");
+ urlSecurityCheck(href, gBrowser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+ let feedURI = makeURI(href, document.characterSet);
+ // Use the feed scheme so X-Moz-Is-Feed will be set
+ // The value doesn't matter
+ if (/^https?$/.test(feedURI.scheme))
+ href = "feed:" + href;
+ this.loadFeed(href, event);
+ },
+
+ loadFeed(href, event) {
+ let feeds = gBrowser.selectedBrowser.feeds;
+ try {
+ openUILink(href, event, { ignoreAlt: true });
+ }
+ finally {
+ // We might default to a livebookmarks modal dialog,
+ // so reset that if the user happens to click it again
+ gBrowser.selectedBrowser.feeds = feeds;
+ }
+ },
+
+ get _feedMenuitem() {
+ delete this._feedMenuitem;
+ return this._feedMenuitem = document.getElementById("singleFeedMenuitemState");
+ },
+
+ get _feedMenupopup() {
+ delete this._feedMenupopup;
+ return this._feedMenupopup = document.getElementById("multipleFeedsMenuState");
+ },
+
+ /**
+ * Update the browser UI to show whether or not feeds are available when
+ * a page is loaded or the user switches tabs to a page that has feeds.
+ */
+ updateFeeds() {
+ if (this._updateFeedTimeout)
+ clearTimeout(this._updateFeedTimeout);
+
+ let feeds = gBrowser.selectedBrowser.feeds;
+ let haveFeeds = feeds && feeds.length > 0;
+
+ let feedButton = document.getElementById("feed-button");
+ if (feedButton) {
+ if (haveFeeds) {
+ feedButton.removeAttribute("disabled");
+ } else {
+ feedButton.setAttribute("disabled", "true");
+ }
+ }
+
+ if (!haveFeeds) {
+ this._feedMenuitem.setAttribute("disabled", "true");
+ this._feedMenuitem.removeAttribute("hidden");
+ this._feedMenupopup.setAttribute("hidden", "true");
+ return;
+ }
+
+ if (feeds.length > 1) {
+ this._feedMenuitem.setAttribute("hidden", "true");
+ this._feedMenupopup.removeAttribute("hidden");
+ } else {
+ this._feedMenuitem.setAttribute("feed", feeds[0].href);
+ this._feedMenuitem.removeAttribute("disabled");
+ this._feedMenuitem.removeAttribute("hidden");
+ this._feedMenupopup.setAttribute("hidden", "true");
+ }
+ },
+
+ addFeed(link, browserForLink) {
+ if (!browserForLink.feeds)
+ browserForLink.feeds = [];
+
+ browserForLink.feeds.push({ href: link.href, title: link.title });
+
+ // If this addition was for the current browser, update the UI. For
+ // background browsers, we'll update on tab switch.
+ if (browserForLink == gBrowser.selectedBrowser) {
+ // Batch updates to avoid updating the UI for multiple onLinkAdded events
+ // fired within 100ms of each other.
+ if (this._updateFeedTimeout)
+ clearTimeout(this._updateFeedTimeout);
+ this._updateFeedTimeout = setTimeout(this.updateFeeds.bind(this), 100);
+ }
+ },
+
+ /**
+ * Get the human-readable display name of a file. This could be the
+ * application name.
+ * @param file
+ * A nsIFile to look up the name of
+ * @return The display name of the application represented by the file.
+ */
+ _getFileDisplayName(file) {
+ switch (AppConstants.platform) {
+ case "win":
+ if (file instanceof Ci.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ }
+ break;
+ case "macosx":
+ if (file instanceof Ci.nsILocalFileMac) {
+ try {
+ return file.bundleDisplayName;
+ } catch (e) {}
+ }
+ break;
+ }
+
+ return file.leafName;
+ },
+
+ _chooseClientApp(aTitle, aTypeName, aBrowser) {
+ const prefName = getPrefAppForType(aTypeName);
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ fp.init(window, aTitle, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterApps);
+
+ fp.open((aResult) => {
+ if (aResult == Ci.nsIFilePicker.returnOK) {
+ let selectedApp = fp.file;
+ if (selectedApp) {
+ // XXXben - we need to compare this with the running instance
+ // executable just don't know how to do that via script
+ // XXXmano TBD: can probably add this to nsIShellService
+ let appName = "";
+ switch (AppConstants.platform) {
+ case "win":
+ appName = AppConstants.MOZ_APP_NAME + ".exe";
+ break;
+ case "macosx":
+ appName = AppConstants.MOZ_MACBUNDLE_NAME;
+ break;
+ default:
+ appName = AppConstants.MOZ_APP_NAME + "-bin";
+ break;
+ }
+
+ if (fp.file.leafName != appName) {
+ Services.prefs.setComplexValue(prefName, Ci.nsILocalFile, selectedApp);
+ aBrowser.messageManager.sendAsyncMessage("FeedWriter:SetApplicationLauncherMenuItem",
+ { name: this._getFileDisplayName(selectedApp),
+ type: "SelectedAppMenuItem" });
+ }
+ }
+ }
+ });
+
+ },
+
+ executeClientApp(aSpec, aTitle, aSubtitle, aFeedHandler) {
+ // aFeedHandler is either "default", indicating the system default reader, or a pref-name containing
+ // an nsILocalFile pointing to the feed handler's executable.
+
+ let clientApp = null;
+ if (aFeedHandler == "default") {
+ clientApp = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService)
+ .defaultFeedReader;
+ } else {
+ clientApp = Services.prefs.getComplexValue(aFeedHandler, Ci.nsILocalFile);
+ }
+
+ // For the benefit of applications that might know how to deal with more
+ // URLs than just feeds, send feed: URLs in the following format:
+ //
+ // http urls: replace scheme with feed, e.g.
+ // http://foo.com/index.rdf -> feed://foo.com/index.rdf
+ // other urls: prepend feed: scheme, e.g.
+ // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf
+ let feedURI = NetUtil.newURI(aSpec);
+ if (feedURI.schemeIs("http")) {
+ feedURI.scheme = "feed";
+ aSpec = feedURI.spec;
+ } else {
+ aSpec = "feed:" + aSpec;
+ }
+
+ // Retrieving the shell service might fail on some systems, most
+ // notably systems where GNOME is not installed.
+ try {
+ let ss = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ ss.openApplicationWithURI(clientApp, aSpec);
+ } catch (e) {
+ // If we couldn't use the shell service, fallback to using a
+ // nsIProcess instance
+ let p = Cc["@mozilla.org/process/util;1"]
+ .createInstance(Ci.nsIProcess);
+ p.init(clientApp);
+ p.run(false, [aSpec], 1);
+ }
+ },
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ init() {
+ window.messageManager.addMessageListener("FeedWriter:ChooseClientApp", this);
+ window.messageManager.addMessageListener("FeedWriter:GetSubscriptionUI", this);
+ window.messageManager.addMessageListener("FeedWriter:SetFeedPrefsAndSubscribe", this);
+ window.messageManager.addMessageListener("FeedWriter:ShownFirstRun", this);
+
+ Services.ppmm.addMessageListener("FeedConverter:ExecuteClientApp", this);
+
+ const prefs = Services.prefs;
+ prefs.addObserver(PREF_SELECTED_ACTION, this, true);
+ prefs.addObserver(PREF_SELECTED_READER, this, true);
+ prefs.addObserver(PREF_SELECTED_WEB, this, true);
+ prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this, true);
+ prefs.addObserver(PREF_VIDEO_SELECTED_READER, this, true);
+ prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this, true);
+ prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this, true);
+ prefs.addObserver(PREF_AUDIO_SELECTED_READER, this, true);
+ prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this, true);
+ },
+
+ uninit() {
+ Services.ppmm.removeMessageListener("FeedConverter:ExecuteClientApp", this);
+
+ this._prefChangeCallback = null;
+ },
+
+ // nsIObserver
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ LOG(`Pref changed ${data}`)
+ if (this._prefChangeCallback) {
+ this._prefChangeCallback.disarm();
+ }
+ // Multiple prefs are set at the same time, debounce to reduce noise
+ // This can happen in one feed and we want to message all feed pages
+ this._prefChangeCallback = new DeferredTask(() => {
+ this._prefChanged(data);
+ }, PREF_UPDATE_DELAY);
+ this._prefChangeCallback.arm();
+ }
+ },
+
+ _prefChanged(prefName) {
+ // Don't observe for PREF_*SELECTED_APP as user likely just picked one
+ // That is also handled by SetApplicationLauncherMenuItem call
+ // Rather than the others which happen on subscription
+ switch (prefName) {
+ case PREF_SELECTED_READER:
+ case PREF_SELECTED_WEB:
+ case PREF_VIDEO_SELECTED_READER:
+ case PREF_VIDEO_SELECTED_WEB:
+ case PREF_AUDIO_SELECTED_READER:
+ case PREF_AUDIO_SELECTED_WEB:
+ case PREF_SELECTED_ACTION:
+ case PREF_VIDEO_SELECTED_ACTION:
+ case PREF_AUDIO_SELECTED_ACTION:
+ const response = {
+ default: this._getReaderForType(Ci.nsIFeed.TYPE_FEED),
+ [Ci.nsIFeed.TYPE_AUDIO]: this._getReaderForType(Ci.nsIFeed.TYPE_AUDIO),
+ [Ci.nsIFeed.TYPE_VIDEO]: this._getReaderForType(Ci.nsIFeed.TYPE_VIDEO)
+ };
+ Services.mm.broadcastAsyncMessage("FeedWriter:PreferenceUpdated",
+ response);
+ break;
+ }
+ },
+
+ _initSubscriptionUIResponse(feedType) {
+ const wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
+ getService(Ci.nsIWebContentConverterService);
+ const handlersRaw = wccr.getContentHandlers(getMimeTypeForFeedType(feedType));
+ const handlers = [];
+ for (let handler of handlersRaw) {
+ LOG(`Handler found: ${handler}`);
+ handlers.push({
+ name: handler.name,
+ uri: handler.uri
+ });
+ }
+ let showFirstRunUI = true;
+ // eslint-disable-next-line mozilla/use-default-preference-values
+ try {
+ showFirstRunUI = Services.prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI);
+ } catch (ex) { }
+ const response = { handlers, showFirstRunUI };
+ let selectedClientApp;
+ const feedTypePref = getPrefAppForType(feedType);
+ try {
+ selectedClientApp = Services.prefs.getComplexValue(feedTypePref, Ci.nsILocalFile);
+ } catch (ex) {
+ // Just do nothing, then we won't bother populating
+ }
+
+ let defaultClientApp = null;
+ try {
+ // This can sometimes not exist
+ defaultClientApp = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService)
+ .defaultFeedReader;
+ } catch (ex) {
+ // Just do nothing, then we don't bother populating
+ }
+
+ if (selectedClientApp && selectedClientApp.exists()) {
+ if (defaultClientApp && selectedClientApp.path != defaultClientApp.path) {
+ // Only set the default menu item if it differs from the selected one
+ response.defaultMenuItem = this._getFileDisplayName(defaultClientApp);
+ }
+ response.selectedMenuItem = this._getFileDisplayName(selectedClientApp);
+ }
+ response.reader = this._getReaderForType(feedType);
+ return response;
+ },
+
+ _setPref(aPrefName, aPrefValue, aIsComplex = false) {
+ LOG(`FeedWriter._setPref ${aPrefName}`);
+ // Ensure we have a pref that is settable
+ if (aPrefName && SETTABLE_PREFS.has(aPrefName)) {
+ if (aIsComplex) {
+ const supportsString = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ supportsString.data = aPrefValue;
+ Services.prefs.setComplexValue(aPrefName, Ci.nsISupportsString, supportsString);
+ } else {
+ Services.prefs.setCharPref(aPrefName, aPrefValue);
+ }
+ } else {
+ LOG(`FeedWriter._setPref ${aPrefName} not allowed`);
+ }
+ },
+
+ _getReaderForType(feedType) {
+ let prefs = Services.prefs;
+ let handler = "bookmarks";
+ let url;
+ // eslint-disable-next-line mozilla/use-default-preference-values
+ try {
+ handler = prefs.getCharPref(getPrefReaderForType(feedType));
+ } catch (ex) { }
+
+ if (handler === "web") {
+ try {
+ url = prefs.getComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString).data;
+ } catch (ex) {
+ LOG("FeedWriter._setSelectedHandler: invalid or no handler in prefs");
+ url = null;
+ }
+ }
+ const alwaysUse = this._getAlwaysUseState(feedType);
+ const action = prefs.getCharPref(getPrefActionForType(feedType));
+ return { handler, url, alwaysUse, action };
+ },
+
+ _getAlwaysUseState(feedType) {
+ try {
+ return Services.prefs.getCharPref(getPrefActionForType(feedType)) != "ask";
+ } catch (ex) { }
+ return false;
+ },
+
+ receiveMessage(msg) {
+ let handler;
+ switch (msg.name) {
+ case "FeedWriter:GetSubscriptionUI":
+ const response = this._initSubscriptionUIResponse(msg.data.feedType);
+ msg.target.messageManager
+ .sendAsyncMessage("FeedWriter:GetSubscriptionUIResponse",
+ response);
+ break;
+ case "FeedWriter:ChooseClientApp":
+ this._chooseClientApp(msg.data.title, msg.data.feedType, msg.target);
+ break;
+ case "FeedWriter:ShownFirstRun":
+ Services.prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false);
+ break;
+ case "FeedWriter:SetFeedPrefsAndSubscribe":
+ const settings = msg.data;
+ if (!settings.action || !VALID_ACTIONS.has(settings.action)) {
+ LOG(`Invalid action ${settings.action}`);
+ return;
+ }
+ if (!settings.reader || !VALID_READERS.has(settings.reader)) {
+ LOG(`Invalid reader ${settings.reader}`);
+ return;
+ }
+ const actionPref = getPrefActionForType(settings.feedType);
+ this._setPref(actionPref, settings.action);
+ const readerPref = getPrefReaderForType(settings.feedType);
+ this._setPref(readerPref, settings.reader);
+ handler = null;
+
+ switch (settings.reader) {
+ case "web":
+ // This is a web set URI by content using window.registerContentHandler()
+ // Lets make sure we know about it before setting it
+ const webPref = getPrefWebForType(settings.feedType);
+ let wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
+ getService(Ci.nsIWebContentConverterService);
+ // If the user provided an invalid web URL this function won't give us a reference
+ handler = wccr.getWebContentHandlerByURI(getMimeTypeForFeedType(settings.feedType), settings.uri);
+ if (handler) {
+ this._setPref(webPref, settings.uri, true);
+ if (settings.useAsDefault) {
+ wccr.setAutoHandler(getMimeTypeForFeedType(settings.feedType), handler);
+ }
+ msg.target.messageManager
+ .sendAsyncMessage("FeedWriter:SetFeedPrefsAndSubscribeResponse",
+ { redirect: handler.getHandlerURI(settings.feedLocation) });
+ } else {
+ LOG(`No handler found for web ${settings.feedType} ${settings.uri}`);
+ }
+ break;
+ default:
+ const feedService = Cc["@mozilla.org/browser/feeds/result-service;1"].
+ getService(Ci.nsIFeedResultService);
+
+ feedService.addToClientReader(settings.feedLocation,
+ settings.feedTitle,
+ settings.feedSubtitle,
+ settings.feedType,
+ settings.reader);
+ }
+ break;
+ case "FeedConverter:ExecuteClientApp":
+ // Always check feedHandler is from a set array of executable prefs
+ if (EXECUTABLE_PREFS.has(msg.data.feedHandler)) {
+ this.executeClientApp(msg.data.spec, msg.data.title,
+ msg.data.subtitle, msg.data.feedHandler);
+ } else {
+ LOG(`FeedConverter:ExecuteClientApp - Will not exec ${msg.data.feedHandler}`);
+ }
+ break;
+ }
+ },
+};
diff --git a/browser/base/content/browser-fullScreenAndPointerLock.js b/browser/base/content/browser-fullScreenAndPointerLock.js
new file mode 100644
index 000000000..497e51121
--- /dev/null
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -0,0 +1,673 @@
+/* -*- 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 PointerlockFsWarning = {
+
+ _element: null,
+ _origin: null,
+
+ init: function() {
+ this.Timeout.prototype = {
+ start: function() {
+ this.cancel();
+ this._id = setTimeout(() => this._handle(), this._delay);
+ },
+ cancel: function() {
+ if (this._id) {
+ clearTimeout(this._id);
+ this._id = 0;
+ }
+ },
+ _handle: function() {
+ this._id = 0;
+ this._func();
+ },
+ get delay() {
+ return this._delay;
+ }
+ };
+ },
+
+ /**
+ * Timeout object for managing timeout request. If it is started when
+ * the previous call hasn't finished, it would automatically cancelled
+ * the previous one.
+ */
+ Timeout: function(func, delay) {
+ this._id = 0;
+ this._func = func;
+ this._delay = delay;
+ },
+
+ showPointerLock: function(aOrigin) {
+ if (!document.fullscreen) {
+ let timeout = gPrefService.getIntPref("pointer-lock-api.warning.timeout");
+ this.show(aOrigin, "pointerlock-warning", timeout, 0);
+ }
+ },
+
+ showFullScreen: function(aOrigin) {
+ let timeout = gPrefService.getIntPref("full-screen-api.warning.timeout");
+ let delay = gPrefService.getIntPref("full-screen-api.warning.delay");
+ this.show(aOrigin, "fullscreen-warning", timeout, delay);
+ },
+
+ // Shows a warning that the site has entered fullscreen or
+ // pointer lock for a short duration.
+ show: function(aOrigin, elementId, timeout, delay) {
+
+ if (!this._element) {
+ this._element = document.getElementById(elementId);
+ // Setup event listeners
+ this._element.addEventListener("transitionend", this);
+ window.addEventListener("mousemove", this, true);
+ // The timeout to hide the warning box after a while.
+ this._timeoutHide = new this.Timeout(() => {
+ this._state = "hidden";
+ }, timeout);
+ // The timeout to show the warning box when the pointer is at the top
+ this._timeoutShow = new this.Timeout(() => {
+ this._state = "ontop";
+ this._timeoutHide.start();
+ }, delay);
+ }
+
+ // Set the strings on the warning UI.
+ if (aOrigin) {
+ this._origin = aOrigin;
+ }
+ let uri = BrowserUtils.makeURI(this._origin);
+ let host = null;
+ try {
+ host = uri.host;
+ } catch (e) { }
+ let textElem = this._element.querySelector(".pointerlockfswarning-domain-text");
+ if (!host) {
+ textElem.setAttribute("hidden", true);
+ } else {
+ textElem.removeAttribute("hidden");
+ let hostElem = this._element.querySelector(".pointerlockfswarning-domain");
+ // Document's principal's URI has a host. Display a warning including it.
+ let utils = {};
+ Cu.import("resource://gre/modules/DownloadUtils.jsm", utils);
+ hostElem.textContent = utils.DownloadUtils.getURIHost(uri.spec)[0];
+ }
+
+ this._element.dataset.identity =
+ gIdentityHandler.pointerlockFsWarningClassName;
+
+ // User should be allowed to explicitly disable
+ // the prompt if they really want.
+ if (this._timeoutHide.delay <= 0) {
+ return;
+ }
+
+ // Explicitly set the last state to hidden to avoid the warning
+ // box being hidden immediately because of mousemove.
+ this._state = "onscreen";
+ this._lastState = "hidden";
+ this._timeoutHide.start();
+ },
+
+ close: function() {
+ if (!this._element) {
+ return;
+ }
+ // Cancel any pending timeout
+ this._timeoutHide.cancel();
+ this._timeoutShow.cancel();
+ // Reset state of the warning box
+ this._state = "hidden";
+ this._element.setAttribute("hidden", true);
+ // Remove all event listeners
+ this._element.removeEventListener("transitionend", this);
+ window.removeEventListener("mousemove", this, true);
+ // Clear fields
+ this._element = null;
+ this._timeoutHide = null;
+ this._timeoutShow = null;
+
+ // Ensure focus switches away from the (now hidden) warning box.
+ // If the user clicked buttons in the warning box, it would have
+ // been focused, and any key events would be directed at the (now
+ // hidden) chrome document instead of the target document.
+ gBrowser.selectedBrowser.focus();
+ },
+
+ // State could be one of "onscreen", "ontop", "hiding", and
+ // "hidden". Setting the state to "onscreen" and "ontop" takes
+ // effect immediately, while setting it to "hidden" actually
+ // turns the state to "hiding" before the transition finishes.
+ _lastState: null,
+ _STATES: ["hidden", "ontop", "onscreen"],
+ get _state() {
+ for (let state of this._STATES) {
+ if (this._element.hasAttribute(state)) {
+ return state;
+ }
+ }
+ return "hiding";
+ },
+ set _state(newState) {
+ let currentState = this._state;
+ if (currentState == newState) {
+ return;
+ }
+ if (currentState != "hiding") {
+ this._lastState = currentState;
+ this._element.removeAttribute(currentState);
+ }
+ if (newState != "hidden") {
+ if (currentState != "hidden") {
+ this._element.setAttribute(newState, true);
+ } else {
+ // When the previous state is hidden, the display was none,
+ // thus no box was constructed. We need to wait for the new
+ // display value taking effect first, otherwise, there won't
+ // be any transition. Since requestAnimationFrame callback is
+ // generally triggered before any style flush and layout, we
+ // should wait for the second animation frame.
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ if (this._element) {
+ this._element.setAttribute(newState, true);
+ }
+ });
+ });
+ }
+ }
+ },
+
+ handleEvent: function(event) {
+ switch (event.type) {
+ case "mousemove": {
+ let state = this._state;
+ if (state == "hidden") {
+ // If the warning box is currently hidden, show it after
+ // a short delay if the pointer is at the top.
+ if (event.clientY != 0) {
+ this._timeoutShow.cancel();
+ } else if (this._timeoutShow.delay >= 0) {
+ this._timeoutShow.start();
+ }
+ } else {
+ let elemRect = this._element.getBoundingClientRect();
+ if (state == "hiding" && this._lastState != "hidden") {
+ // If we are on the hiding transition, and the pointer
+ // moved near the box, restore to the previous state.
+ if (event.clientY <= elemRect.bottom + 50) {
+ this._state = this._lastState;
+ this._timeoutHide.start();
+ }
+ } else if (state == "ontop" || this._lastState != "hidden") {
+ // State being "ontop" or the previous state not being
+ // "hidden" indicates this current warning box is shown
+ // in response to user's action. Hide it immediately when
+ // the pointer leaves that area.
+ if (event.clientY > elemRect.bottom + 50) {
+ this._state = "hidden";
+ this._timeoutHide.cancel();
+ }
+ }
+ }
+ break;
+ }
+ case "transitionend": {
+ if (this._state == "hiding") {
+ this._element.setAttribute("hidden", true);
+ }
+ break;
+ }
+ }
+ }
+};
+
+var PointerLock = {
+
+ init: function() {
+ window.messageManager.addMessageListener("PointerLock:Entered", this);
+ window.messageManager.addMessageListener("PointerLock:Exited", this);
+ },
+
+ receiveMessage: function(aMessage) {
+ switch (aMessage.name) {
+ case "PointerLock:Entered": {
+ PointerlockFsWarning.showPointerLock(aMessage.data.originNoSuffix);
+ break;
+ }
+ case "PointerLock:Exited": {
+ PointerlockFsWarning.close();
+ break;
+ }
+ }
+ }
+};
+
+var FullScreen = {
+ _MESSAGES: [
+ "DOMFullscreen:Request",
+ "DOMFullscreen:NewOrigin",
+ "DOMFullscreen:Exit",
+ "DOMFullscreen:Painted",
+ ],
+
+ init: function() {
+ // called when we go into full screen, even if initiated by a web page script
+ window.addEventListener("fullscreen", this, true);
+ window.addEventListener("MozDOMFullscreen:Entered", this,
+ /* useCapture */ true,
+ /* wantsUntrusted */ false);
+ window.addEventListener("MozDOMFullscreen:Exited", this,
+ /* useCapture */ true,
+ /* wantsUntrusted */ false);
+ for (let type of this._MESSAGES) {
+ window.messageManager.addMessageListener(type, this);
+ }
+
+ if (window.fullScreen)
+ this.toggle();
+ },
+
+ uninit: function() {
+ for (let type of this._MESSAGES) {
+ window.messageManager.removeMessageListener(type, this);
+ }
+ this.cleanup();
+ },
+
+ toggle: function () {
+ var enterFS = window.fullScreen;
+
+ // Toggle the View:FullScreen command, which controls elements like the
+ // fullscreen menuitem, and menubars.
+ let fullscreenCommand = document.getElementById("View:FullScreen");
+ if (enterFS) {
+ fullscreenCommand.setAttribute("checked", enterFS);
+ } else {
+ fullscreenCommand.removeAttribute("checked");
+ }
+
+ if (AppConstants.platform == "macosx") {
+ // Make sure the menu items are adjusted.
+ document.getElementById("enterFullScreenItem").hidden = enterFS;
+ document.getElementById("exitFullScreenItem").hidden = !enterFS;
+ }
+
+ if (!this._fullScrToggler) {
+ this._fullScrToggler = document.getElementById("fullscr-toggler");
+ this._fullScrToggler.addEventListener("mouseover", this._expandCallback, false);
+ this._fullScrToggler.addEventListener("dragenter", this._expandCallback, false);
+ this._fullScrToggler.addEventListener("touchmove", this._expandCallback, {passive: true});
+ }
+
+ if (enterFS) {
+ gNavToolbox.setAttribute("inFullscreen", true);
+ document.documentElement.setAttribute("inFullscreen", true);
+ if (!document.fullscreenElement && this.useLionFullScreen)
+ document.documentElement.setAttribute("OSXLionFullscreen", true);
+ } else {
+ gNavToolbox.removeAttribute("inFullscreen");
+ document.documentElement.removeAttribute("inFullscreen");
+ document.documentElement.removeAttribute("OSXLionFullscreen");
+ }
+
+ if (!document.fullscreenElement)
+ this._updateToolbars(enterFS);
+
+ if (enterFS) {
+ document.addEventListener("keypress", this._keyToggleCallback, false);
+ document.addEventListener("popupshown", this._setPopupOpen, false);
+ document.addEventListener("popuphidden", this._setPopupOpen, false);
+ // In DOM fullscreen mode, we hide toolbars with CSS
+ if (!document.fullscreenElement)
+ this.hideNavToolbox(true);
+ }
+ else {
+ this.showNavToolbox(false);
+ // This is needed if they use the context menu to quit fullscreen
+ this._isPopupOpen = false;
+ this.cleanup();
+ // In TabsInTitlebar._update(), we cancel the appearance update on
+ // resize event for exiting fullscreen, since that happens before we
+ // change the UI here in the "fullscreen" event. Hence we need to
+ // call it here to ensure the appearance is properly updated. See
+ // TabsInTitlebar._update() and bug 1173768.
+ TabsInTitlebar.updateAppearance(true);
+ }
+
+ if (enterFS && !document.fullscreenElement) {
+ Services.telemetry.getHistogramById("FX_BROWSER_FULLSCREEN_USED")
+ .add(1);
+ }
+ },
+
+ exitDomFullScreen : function() {
+ document.exitFullscreen();
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "fullscreen":
+ this.toggle();
+ break;
+ case "MozDOMFullscreen:Entered": {
+ // The event target is the element which requested the DOM
+ // fullscreen. If we were entering DOM fullscreen for a remote
+ // browser, the target would be `gBrowser` and the original
+ // target would be the browser which was the parameter of
+ // `remoteFrameFullscreenChanged` call. If the fullscreen
+ // request was initiated from an in-process browser, we need
+ // to get its corresponding browser here.
+ let browser;
+ if (event.target == gBrowser) {
+ browser = event.originalTarget;
+ } else {
+ let topWin = event.target.ownerGlobal.top;
+ browser = gBrowser.getBrowserForContentWindow(topWin);
+ }
+ TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
+ this.enterDomFullscreen(browser);
+ break;
+ }
+ case "MozDOMFullscreen:Exited":
+ TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
+ this.cleanupDomFullscreen();
+ break;
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ let browser = aMessage.target;
+ switch (aMessage.name) {
+ case "DOMFullscreen:Request": {
+ this._windowUtils.remoteFrameFullscreenChanged(browser);
+ break;
+ }
+ case "DOMFullscreen:NewOrigin": {
+ // Don't show the warning if we've already exited fullscreen.
+ if (document.fullscreen) {
+ PointerlockFsWarning.showFullScreen(aMessage.data.originNoSuffix);
+ }
+ break;
+ }
+ case "DOMFullscreen:Exit": {
+ this._windowUtils.remoteFrameFullscreenReverted();
+ break;
+ }
+ case "DOMFullscreen:Painted": {
+ Services.obs.notifyObservers(window, "fullscreen-painted", "");
+ TelemetryStopwatch.finish("FULLSCREEN_CHANGE_MS");
+ break;
+ }
+ }
+ },
+
+ enterDomFullscreen : function(aBrowser) {
+
+ if (!document.fullscreenElement) {
+ return;
+ }
+
+ // If we have a current pointerlock warning shown then hide it
+ // before transition.
+ PointerlockFsWarning.close();
+
+ // If it is a remote browser, send a message to ask the content
+ // to enter fullscreen state. We don't need to do so if it is an
+ // in-process browser, since all related document should have
+ // entered fullscreen state at this point.
+ // This should be done before the active tab check below to ensure
+ // that the content document handles the pending request. Doing so
+ // before the check is fine since we also check the activeness of
+ // the requesting document in content-side handling code.
+ if (this._isRemoteBrowser(aBrowser)) {
+ aBrowser.messageManager.sendAsyncMessage("DOMFullscreen:Entered");
+ }
+
+ // If we've received a fullscreen notification, we have to ensure that the
+ // element that's requesting fullscreen belongs to the browser that's currently
+ // active. If not, we exit fullscreen since the "full-screen document" isn't
+ // actually visible now.
+ if (!aBrowser || gBrowser.selectedBrowser != aBrowser ||
+ // The top-level window has lost focus since the request to enter
+ // full-screen was made. Cancel full-screen.
+ Services.focus.activeWindow != window) {
+ // This function is called synchronously in fullscreen change, so
+ // we have to avoid calling exitFullscreen synchronously here.
+ setTimeout(() => document.exitFullscreen(), 0);
+ return;
+ }
+
+ document.documentElement.setAttribute("inDOMFullscreen", true);
+
+ if (gFindBarInitialized) {
+ gFindBar.close(true);
+ }
+
+ // Exit DOM full-screen mode upon open, close, or change tab.
+ gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen);
+ gBrowser.tabContainer.addEventListener("TabClose", this.exitDomFullScreen);
+ gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen);
+
+ // Add listener to detect when the fullscreen window is re-focused.
+ // If a fullscreen window loses focus, we show a warning when the
+ // fullscreen window is refocused.
+ window.addEventListener("activate", this);
+ },
+
+ cleanup: function () {
+ if (!window.fullScreen) {
+ MousePosTracker.removeListener(this);
+ document.removeEventListener("keypress", this._keyToggleCallback, false);
+ document.removeEventListener("popupshown", this._setPopupOpen, false);
+ document.removeEventListener("popuphidden", this._setPopupOpen, false);
+ }
+ },
+
+ cleanupDomFullscreen: function () {
+ window.messageManager
+ .broadcastAsyncMessage("DOMFullscreen:CleanUp");
+
+ PointerlockFsWarning.close();
+ gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen);
+ gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen);
+ gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen);
+ window.removeEventListener("activate", this);
+
+ document.documentElement.removeAttribute("inDOMFullscreen");
+ },
+
+ _isRemoteBrowser: function (aBrowser) {
+ return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true";
+ },
+
+ get _windowUtils() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ },
+
+ getMouseTargetRect: function()
+ {
+ return this._mouseTargetRect;
+ },
+
+ // Event callbacks
+ _expandCallback: function()
+ {
+ FullScreen.showNavToolbox();
+ },
+ onMouseEnter: function()
+ {
+ FullScreen.hideNavToolbox();
+ },
+ _keyToggleCallback: function(aEvent)
+ {
+ // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we
+ // should provide a way to collapse them too.
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ FullScreen.hideNavToolbox();
+ }
+ // F6 is another shortcut to the address bar, but its not covered in OpenLocation()
+ else if (aEvent.keyCode == aEvent.DOM_VK_F6)
+ FullScreen.showNavToolbox();
+ },
+
+ // Checks whether we are allowed to collapse the chrome
+ _isPopupOpen: false,
+ _isChromeCollapsed: false,
+ _safeToCollapse: function () {
+ if (!gPrefService.getBoolPref("browser.fullscreen.autohide"))
+ return false;
+
+ // a popup menu is open in chrome: don't collapse chrome
+ if (this._isPopupOpen)
+ return false;
+
+ // On OS X Lion we don't want to hide toolbars.
+ if (this.useLionFullScreen)
+ return false;
+
+ // a textbox in chrome is focused (location bar anyone?): don't collapse chrome
+ if (document.commandDispatcher.focusedElement &&
+ document.commandDispatcher.focusedElement.ownerDocument == document &&
+ document.commandDispatcher.focusedElement.localName == "input") {
+ return false;
+ }
+
+ return true;
+ },
+
+ _setPopupOpen: function(aEvent)
+ {
+ // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed.
+ // Otherwise, they would not affect chrome and the user would expect the chrome to go away.
+ // e.g. we wouldn't want the autoscroll icon firing this event, so when the user
+ // toggles chrome when moving mouse to the top, it doesn't go away again.
+ if (aEvent.type == "popupshown" && !FullScreen._isChromeCollapsed &&
+ aEvent.target.localName != "tooltip" && aEvent.target.localName != "window")
+ FullScreen._isPopupOpen = true;
+ else if (aEvent.type == "popuphidden" && aEvent.target.localName != "tooltip" &&
+ aEvent.target.localName != "window") {
+ FullScreen._isPopupOpen = false;
+ // Try again to hide toolbar when we close the popup.
+ FullScreen.hideNavToolbox(true);
+ }
+ },
+
+ // Autohide helpers for the context menu item
+ getAutohide: function(aItem)
+ {
+ aItem.setAttribute("checked", gPrefService.getBoolPref("browser.fullscreen.autohide"));
+ },
+ setAutohide: function()
+ {
+ gPrefService.setBoolPref("browser.fullscreen.autohide", !gPrefService.getBoolPref("browser.fullscreen.autohide"));
+ // Try again to hide toolbar when we change the pref.
+ FullScreen.hideNavToolbox(true);
+ },
+
+ showNavToolbox: function(trackMouse = true) {
+ this._fullScrToggler.hidden = true;
+ gNavToolbox.removeAttribute("fullscreenShouldAnimate");
+ gNavToolbox.style.marginTop = "";
+
+ if (!this._isChromeCollapsed) {
+ return;
+ }
+
+ // Track whether mouse is near the toolbox
+ if (trackMouse && !this.useLionFullScreen) {
+ let rect = gBrowser.mPanelContainer.getBoundingClientRect();
+ this._mouseTargetRect = {
+ top: rect.top + 50,
+ bottom: rect.bottom,
+ left: rect.left,
+ right: rect.right
+ };
+ MousePosTracker.addListener(this);
+ }
+
+ this._isChromeCollapsed = false;
+ },
+
+ hideNavToolbox: function (aAnimate = false) {
+ if (this._isChromeCollapsed || !this._safeToCollapse())
+ return;
+
+ this._fullScrToggler.hidden = false;
+
+ if (aAnimate && gPrefService.getBoolPref("browser.fullscreen.animate")) {
+ gNavToolbox.setAttribute("fullscreenShouldAnimate", true);
+ // Hide the fullscreen toggler until the transition ends.
+ let listener = () => {
+ gNavToolbox.removeEventListener("transitionend", listener, true);
+ if (this._isChromeCollapsed)
+ this._fullScrToggler.hidden = false;
+ };
+ gNavToolbox.addEventListener("transitionend", listener, true);
+ this._fullScrToggler.hidden = true;
+ }
+
+ gNavToolbox.style.marginTop =
+ -gNavToolbox.getBoundingClientRect().height + "px";
+ this._isChromeCollapsed = true;
+ MousePosTracker.removeListener(this);
+ },
+
+ _updateToolbars: function (aEnterFS) {
+ for (let el of document.querySelectorAll("toolbar[fullscreentoolbar=true]")) {
+ if (aEnterFS) {
+ // Give the main nav bar and the tab bar the fullscreen context menu,
+ // otherwise remove context menu to prevent breakage
+ el.setAttribute("saved-context", el.getAttribute("context"));
+ if (el.id == "nav-bar" || el.id == "TabsToolbar")
+ el.setAttribute("context", "autohide-context");
+ else
+ el.removeAttribute("context");
+
+ // Set the inFullscreen attribute to allow specific styling
+ // in fullscreen mode
+ el.setAttribute("inFullscreen", true);
+ } else {
+ if (el.hasAttribute("saved-context")) {
+ el.setAttribute("context", el.getAttribute("saved-context"));
+ el.removeAttribute("saved-context");
+ }
+ el.removeAttribute("inFullscreen");
+ }
+ }
+
+ ToolbarIconColor.inferFromText();
+
+ // For Lion fullscreen, all fullscreen controls are hidden, don't
+ // bother to touch them. If we don't stop here, the following code
+ // could cause the native fullscreen button be shown unexpectedly.
+ // See bug 1165570.
+ if (this.useLionFullScreen) {
+ return;
+ }
+
+ var fullscreenctls = document.getElementById("window-controls");
+ var navbar = document.getElementById("nav-bar");
+ var ctlsOnTabbar = window.toolbar.visible;
+ if (fullscreenctls.parentNode == navbar && ctlsOnTabbar) {
+ fullscreenctls.removeAttribute("flex");
+ document.getElementById("TabsToolbar").appendChild(fullscreenctls);
+ }
+ else if (fullscreenctls.parentNode.id == "TabsToolbar" && !ctlsOnTabbar) {
+ fullscreenctls.setAttribute("flex", "1");
+ navbar.appendChild(fullscreenctls);
+ }
+ fullscreenctls.hidden = !aEnterFS;
+ }
+};
+XPCOMUtils.defineLazyGetter(FullScreen, "useLionFullScreen", function() {
+ // We'll only use OS X Lion full screen if we're
+ // * on OS X
+ // * on Lion or higher (Darwin 11+)
+ // * have fullscreenbutton="true"
+ return AppConstants.isPlatformAndVersionAtLeast("macosx", 11) &&
+ document.documentElement.getAttribute("fullscreenbutton") == "true";
+});
diff --git a/browser/base/content/browser-fullZoom.js b/browser/base/content/browser-fullZoom.js
new file mode 100644
index 000000000..890cd8440
--- /dev/null
+++ b/browser/base/content/browser-fullZoom.js
@@ -0,0 +1,526 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Controls the "full zoom" setting and its site-specific preferences.
+ */
+var FullZoom = {
+ // Identifies the setting in the content prefs database.
+ name: "browser.content.full-zoom",
+
+ // browser.zoom.siteSpecific preference cache
+ _siteSpecificPref: undefined,
+
+ // browser.zoom.updateBackgroundTabs preference cache
+ updateBackgroundTabs: undefined,
+
+ // This maps the browser to monotonically increasing integer
+ // tokens. _browserTokenMap[browser] is increased each time the zoom is
+ // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses.
+ _browserTokenMap: new WeakMap(),
+
+ // Stores initial locations if we receive onLocationChange
+ // events before we're initialized.
+ _initialLocations: new WeakMap(),
+
+ get siteSpecific() {
+ return this._siteSpecificPref;
+ },
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
+ Ci.nsIObserver,
+ Ci.nsIContentPrefObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+
+ // Initialization & Destruction
+
+ init: function FullZoom_init() {
+ gBrowser.addEventListener("ZoomChangeUsingMouseWheel", this);
+
+ // Register ourselves with the service so we know when our pref changes.
+ this._cps2 = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService2);
+ this._cps2.addObserverForName(this.name, this);
+
+ this._siteSpecificPref =
+ gPrefService.getBoolPref("browser.zoom.siteSpecific");
+ this.updateBackgroundTabs =
+ gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
+ // Listen for changes to the browser.zoom branch so we can enable/disable
+ // updating background tabs and per-site saving and restoring of zoom levels.
+ gPrefService.addObserver("browser.zoom.", this, true);
+
+ // If we received onLocationChange events for any of the current browsers
+ // before we were initialized we want to replay those upon initialization.
+ for (let browser of gBrowser.browsers) {
+ if (this._initialLocations.has(browser)) {
+ this.onLocationChange(...this._initialLocations.get(browser), browser);
+ }
+ }
+
+ // This should be nulled after initialization.
+ this._initialLocations = null;
+ },
+
+ destroy: function FullZoom_destroy() {
+ gPrefService.removeObserver("browser.zoom.", this);
+ this._cps2.removeObserverForName(this.name, this);
+ gBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this);
+ },
+
+
+ // Event Handlers
+
+ // nsIDOMEventListener
+
+ handleEvent: function FullZoom_handleEvent(event) {
+ switch (event.type) {
+ case "ZoomChangeUsingMouseWheel":
+ let browser = this._getTargetedBrowser(event);
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ break;
+ }
+ },
+
+ // nsIObserver
+
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "nsPref:changed":
+ switch (aData) {
+ case "browser.zoom.siteSpecific":
+ this._siteSpecificPref =
+ gPrefService.getBoolPref("browser.zoom.siteSpecific");
+ break;
+ case "browser.zoom.updateBackgroundTabs":
+ this.updateBackgroundTabs =
+ gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
+ break;
+ }
+ break;
+ }
+ },
+
+ // nsIContentPrefObserver
+
+ onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue, aIsPrivate) {
+ this._onContentPrefChanged(aGroup, aValue, aIsPrivate);
+ },
+
+ onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName, aIsPrivate) {
+ this._onContentPrefChanged(aGroup, undefined, aIsPrivate);
+ },
+
+ /**
+ * Appropriately updates the zoom level after a content preference has
+ * changed.
+ *
+ * @param aGroup The group of the changed preference.
+ * @param aValue The new value of the changed preference. Pass undefined to
+ * indicate the preference's removal.
+ */
+ _onContentPrefChanged: function FullZoom__onContentPrefChanged(aGroup, aValue, aIsPrivate) {
+ if (this._isNextContentPrefChangeInternal) {
+ // Ignore changes that FullZoom itself makes. This works because the
+ // content pref service calls callbacks before notifying observers, and it
+ // does both in the same turn of the event loop.
+ delete this._isNextContentPrefChangeInternal;
+ return;
+ }
+
+ let browser = gBrowser.selectedBrowser;
+ if (!browser.currentURI)
+ return;
+
+ let ctxt = this._loadContextFromBrowser(browser);
+ let domain = this._cps2.extractDomain(browser.currentURI.spec);
+ if (aGroup) {
+ if (aGroup == domain && ctxt.usePrivateBrowsing == aIsPrivate)
+ this._applyPrefToZoom(aValue, browser);
+ return;
+ }
+
+ this._globalValue = aValue === undefined ? aValue :
+ this._ensureValid(aValue);
+
+ // If the current page doesn't have a site-specific preference, then its
+ // zoom should be set to the new global preference now that the global
+ // preference has changed.
+ let hasPref = false;
+ let token = this._getBrowserToken(browser);
+ this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
+ handleResult: function () { hasPref = true; },
+ handleCompletion: function () {
+ if (!hasPref && token.isCurrent)
+ this._applyPrefToZoom(undefined, browser);
+ }.bind(this)
+ });
+ },
+
+ // location change observer
+
+ /**
+ * Called when the location of a tab changes.
+ * When that happens, we need to update the current zoom level if appropriate.
+ *
+ * @param aURI
+ * A URI object representing the new location.
+ * @param aIsTabSwitch
+ * Whether this location change has happened because of a tab switch.
+ * @param aBrowser
+ * (optional) browser object displaying the document
+ */
+ onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) {
+ let browser = aBrowser || gBrowser.selectedBrowser;
+
+ // If we haven't been initialized yet but receive an onLocationChange
+ // notification then let's store and replay it upon initialization.
+ if (this._initialLocations) {
+ this._initialLocations.set(browser, [aURI, aIsTabSwitch]);
+ return;
+ }
+
+ // Ignore all pending async zoom accesses in the browser. Pending accesses
+ // that started before the location change will be prevented from applying
+ // to the new location.
+ this._ignorePendingZoomAccesses(browser);
+
+ if (!aURI || (aIsTabSwitch && !this.siteSpecific)) {
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+
+ // Avoid the cps roundtrip and apply the default/global pref.
+ if (aURI.spec == "about:blank") {
+ this._applyPrefToZoom(undefined, browser,
+ this._notifyOnLocationChange.bind(this, browser));
+ return;
+ }
+
+ // Media documents should always start at 1, and are not affected by prefs.
+ if (!aIsTabSwitch && browser.isSyntheticDocument) {
+ ZoomManager.setZoomForBrowser(browser, 1);
+ // _ignorePendingZoomAccesses already called above, so no need here.
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+
+ // See if the zoom pref is cached.
+ let ctxt = this._loadContextFromBrowser(browser);
+ let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
+ if (pref) {
+ this._applyPrefToZoom(pref.value, browser,
+ this._notifyOnLocationChange.bind(this, browser));
+ return;
+ }
+
+ // It's not cached, so we have to asynchronously fetch it.
+ let value = undefined;
+ let token = this._getBrowserToken(browser);
+ this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, {
+ handleResult: function (resultPref) { value = resultPref.value; },
+ handleCompletion: function () {
+ if (!token.isCurrent) {
+ this._notifyOnLocationChange(browser);
+ return;
+ }
+ this._applyPrefToZoom(value, browser,
+ this._notifyOnLocationChange.bind(this, browser));
+ }.bind(this)
+ });
+ },
+
+ // update state of zoom type menu item
+
+ updateMenu: function FullZoom_updateMenu() {
+ var menuItem = document.getElementById("toggle_zoom");
+
+ menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
+ },
+
+ // Setting & Pref Manipulation
+
+ /**
+ * Reduces the zoom level of the page in the current browser.
+ */
+ reduce: function FullZoom_reduce() {
+ ZoomManager.reduce();
+ let browser = gBrowser.selectedBrowser;
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ },
+
+ /**
+ * Enlarges the zoom level of the page in the current browser.
+ */
+ enlarge: function FullZoom_enlarge() {
+ ZoomManager.enlarge();
+ let browser = gBrowser.selectedBrowser;
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ },
+
+ /**
+ * Sets the zoom level for the given browser to the given floating
+ * point value, where 1 is the default zoom level.
+ */
+ setZoom: function (value, browser = gBrowser.selectedBrowser) {
+ ZoomManager.setZoomForBrowser(browser, value);
+ this._ignorePendingZoomAccesses(browser);
+ this._applyZoomToPref(browser);
+ },
+
+ /**
+ * Sets the zoom level of the page in the given browser to the global zoom
+ * level.
+ *
+ * @return A promise which resolves when the zoom reset has been applied.
+ */
+ reset: function FullZoom_reset(browser = gBrowser.selectedBrowser) {
+ let token = this._getBrowserToken(browser);
+ let result = this._getGlobalValue(browser).then(value => {
+ if (token.isCurrent) {
+ ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value);
+ this._ignorePendingZoomAccesses(browser);
+ Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", "");
+ }
+ });
+ this._removePref(browser);
+ return result;
+ },
+
+ /**
+ * Set the zoom level for a given browser.
+ *
+ * Per nsPresContext::setFullZoom, we can set the zoom to its current value
+ * without significant impact on performance, as the setting is only applied
+ * if it differs from the current setting. In fact getting the zoom and then
+ * checking ourselves if it differs costs more.
+ *
+ * And perhaps we should always set the zoom even if it was more expensive,
+ * since nsDocumentViewer::SetTextZoom claims that child documents can have
+ * a different text zoom (although it would be unusual), and it implies that
+ * those child text zooms should get updated when the parent zoom gets set,
+ * and perhaps the same is true for full zoom
+ * (although nsDocumentViewer::SetFullZoom doesn't mention it).
+ *
+ * So when we apply new zoom values to the browser, we simply set the zoom.
+ * We don't check first to see if the new value is the same as the current
+ * one.
+ *
+ * @param aValue The zoom level value.
+ * @param aBrowser The zoom is set in this browser. Required.
+ * @param aCallback If given, it's asynchronously called when complete.
+ */
+ _applyPrefToZoom: function FullZoom__applyPrefToZoom(aValue, aBrowser, aCallback) {
+ if (!this.siteSpecific || gInPrintPreviewMode) {
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ // The browser is sometimes half-destroyed because this method is called
+ // by content pref service callbacks, which themselves can be called at any
+ // time, even after browsers are closed.
+ if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) {
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ if (aValue !== undefined) {
+ ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
+ this._ignorePendingZoomAccesses(aBrowser);
+ this._executeSoon(aCallback);
+ return;
+ }
+
+ let token = this._getBrowserToken(aBrowser);
+ this._getGlobalValue(aBrowser).then(value => {
+ if (token.isCurrent) {
+ ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value);
+ this._ignorePendingZoomAccesses(aBrowser);
+ }
+ this._executeSoon(aCallback);
+ });
+ },
+
+ /**
+ * Saves the zoom level of the page in the given browser to the content
+ * prefs store.
+ *
+ * @param browser The zoom of this browser will be saved. Required.
+ */
+ _applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
+ Services.obs.notifyObservers(browser, "browser-fullZoom:zoomChange", "");
+ if (!this.siteSpecific ||
+ gInPrintPreviewMode ||
+ browser.isSyntheticDocument)
+ return;
+
+ this._cps2.set(browser.currentURI.spec, this.name,
+ ZoomManager.getZoomForBrowser(browser),
+ this._loadContextFromBrowser(browser), {
+ handleCompletion: function () {
+ this._isNextContentPrefChangeInternal = true;
+ }.bind(this),
+ });
+ },
+
+ /**
+ * Removes from the content prefs store the zoom level of the given browser.
+ *
+ * @param browser The zoom of this browser will be removed. Required.
+ */
+ _removePref: function FullZoom__removePref(browser) {
+ Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", "");
+ if (browser.isSyntheticDocument)
+ return;
+ let ctxt = this._loadContextFromBrowser(browser);
+ this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
+ handleCompletion: function () {
+ this._isNextContentPrefChangeInternal = true;
+ }.bind(this),
+ });
+ },
+
+ // Utilities
+
+ /**
+ * Returns the zoom change token of the given browser. Asynchronous
+ * operations that access the given browser's zoom should use this method to
+ * capture the token before starting and use token.isCurrent to determine if
+ * it's safe to access the zoom when done. If token.isCurrent is false, then
+ * after the async operation started, either the browser's zoom was changed or
+ * the browser was destroyed, and depending on what the operation is doing, it
+ * may no longer be safe to set and get its zoom.
+ *
+ * @param browser The token of this browser will be returned.
+ * @return An object with an "isCurrent" getter.
+ */
+ _getBrowserToken: function FullZoom__getBrowserToken(browser) {
+ let map = this._browserTokenMap;
+ if (!map.has(browser))
+ map.set(browser, 0);
+ return {
+ token: map.get(browser),
+ get isCurrent() {
+ // At this point, the browser may have been destructed and unbound but
+ // its outer ID not removed from the map because outer-window-destroyed
+ // hasn't been received yet. In that case, the browser is unusable, it
+ // has no properties, so return false. Check for this case by getting a
+ // property, say, docShell.
+ return map.get(browser) === this.token && browser.parentNode;
+ },
+ };
+ },
+
+ /**
+ * Returns the browser that the supplied zoom event is associated with.
+ * @param event The ZoomChangeUsingMouseWheel event.
+ * @return The associated browser element, if one exists, otherwise null.
+ */
+ _getTargetedBrowser: function FullZoom__getTargetedBrowser(event) {
+ let target = event.originalTarget;
+
+ // With remote content browsers, the event's target is the browser
+ // we're looking for.
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ if (target instanceof window.XULElement &&
+ target.localName == "browser" &&
+ target.namespaceURI == XUL_NS)
+ return target;
+
+ // With in-process content browsers, the event's target is the content
+ // document.
+ if (target.nodeType == Node.DOCUMENT_NODE)
+ return gBrowser.getBrowserForDocument(target);
+
+ throw new Error("Unexpected ZoomChangeUsingMouseWheel event source");
+ },
+
+ /**
+ * Increments the zoom change token for the given browser so that pending
+ * async operations know that it may be unsafe to access they zoom when they
+ * finish.
+ *
+ * @param browser Pending accesses in this browser will be ignored.
+ */
+ _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) {
+ let map = this._browserTokenMap;
+ map.set(browser, (map.get(browser) || 0) + 1);
+ },
+
+ _ensureValid: function FullZoom__ensureValid(aValue) {
+ // Note that undefined is a valid value for aValue that indicates a known-
+ // not-to-exist value.
+ if (isNaN(aValue))
+ return 1;
+
+ if (aValue < ZoomManager.MIN)
+ return ZoomManager.MIN;
+
+ if (aValue > ZoomManager.MAX)
+ return ZoomManager.MAX;
+
+ return aValue;
+ },
+
+ /**
+ * Gets the global browser.content.full-zoom content preference.
+ *
+ * @param browser The browser pertaining to the zoom.
+ * @returns Promise<prefValue>
+ * Resolves to the preference value when done.
+ */
+ _getGlobalValue: function FullZoom__getGlobalValue(browser) {
+ // * !("_globalValue" in this) => global value not yet cached.
+ // * this._globalValue === undefined => global value known not to exist.
+ // * Otherwise, this._globalValue is a number, the global value.
+ return new Promise(resolve => {
+ if ("_globalValue" in this) {
+ resolve(this._globalValue);
+ return;
+ }
+ let value = undefined;
+ this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), {
+ handleResult: function (pref) { value = pref.value; },
+ handleCompletion: (reason) => {
+ this._globalValue = this._ensureValid(value);
+ resolve(this._globalValue);
+ }
+ });
+ });
+ },
+
+ /**
+ * Gets the load context from the given Browser.
+ *
+ * @param Browser The Browser whose load context will be returned.
+ * @return The nsILoadContext of the given Browser.
+ */
+ _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) {
+ return browser.loadContext;
+ },
+
+ /**
+ * Asynchronously broadcasts "browser-fullZoom:location-change" so that
+ * listeners can be notified when the zoom levels on those pages change.
+ * The notification is always asynchronous so that observers are guaranteed a
+ * consistent behavior.
+ */
+ _notifyOnLocationChange: function FullZoom__notifyOnLocationChange(browser) {
+ this._executeSoon(function () {
+ Services.obs.notifyObservers(browser, "browser-fullZoom:location-change", "");
+ });
+ },
+
+ _executeSoon: function FullZoom__executeSoon(callback) {
+ if (!callback)
+ return;
+ Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+};
diff --git a/browser/base/content/browser-fxaccounts.js b/browser/base/content/browser-fxaccounts.js
new file mode 100644
index 000000000..0bbce3e26
--- /dev/null
+++ b/browser/base/content/browser-fxaccounts.js
@@ -0,0 +1,459 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 gFxAccounts = {
+
+ SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration",
+
+ _initialized: false,
+ _inCustomizationMode: false,
+ _cachedProfile: null,
+
+ get weave() {
+ delete this.weave;
+ return this.weave = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+ },
+
+ get topics() {
+ // Do all this dance to lazy-load FxAccountsCommon.
+ delete this.topics;
+ return this.topics = [
+ "weave:service:ready",
+ "weave:service:login:change",
+ "weave:service:setup-complete",
+ "weave:service:sync:error",
+ "weave:ui:login:error",
+ "fxa-migration:state-changed",
+ this.FxAccountsCommon.ONLOGIN_NOTIFICATION,
+ this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
+ this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
+ ];
+ },
+
+ get panelUIFooter() {
+ delete this.panelUIFooter;
+ return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
+ },
+
+ get panelUIStatus() {
+ delete this.panelUIStatus;
+ return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
+ },
+
+ get panelUIAvatar() {
+ delete this.panelUIAvatar;
+ return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
+ },
+
+ get panelUILabel() {
+ delete this.panelUILabel;
+ return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
+ },
+
+ get panelUIIcon() {
+ delete this.panelUIIcon;
+ return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
+ },
+
+ get strings() {
+ delete this.strings;
+ return this.strings = Services.strings.createBundle(
+ "chrome://browser/locale/accounts.properties"
+ );
+ },
+
+ get loginFailed() {
+ // Referencing Weave.Service will implicitly initialize sync, and we don't
+ // want to force that - so first check if it is ready.
+ let service = Cc["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ if (!service.ready) {
+ return false;
+ }
+ // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
+ // All other login failures are assumed to be transient and should go
+ // away by themselves, so aren't reflected here.
+ return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ },
+
+ get sendTabToDeviceEnabled() {
+ return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
+ },
+
+ get remoteClients() {
+ return Weave.Service.clientsEngine.remoteClients
+ .sort((a, b) => a.name.localeCompare(b.name));
+ },
+
+ init: function () {
+ // Bail out if we're already initialized and for pop-up windows.
+ if (this._initialized || !window.toolbar.visible) {
+ return;
+ }
+
+ for (let topic of this.topics) {
+ Services.obs.addObserver(this, topic, false);
+ }
+
+ gNavToolbox.addEventListener("customizationstarting", this);
+ gNavToolbox.addEventListener("customizationending", this);
+
+ EnsureFxAccountsWebChannel();
+ this._initialized = true;
+
+ this.updateUI();
+ },
+
+ uninit: function () {
+ if (!this._initialized) {
+ return;
+ }
+
+ for (let topic of this.topics) {
+ Services.obs.removeObserver(this, topic);
+ }
+
+ this._initialized = false;
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "fxa-migration:state-changed":
+ this.onMigrationStateChanged(data, subject);
+ break;
+ case this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION:
+ this._cachedProfile = null;
+ // Fallthrough intended
+ default:
+ this.updateUI();
+ break;
+ }
+ },
+
+ onMigrationStateChanged: function () {
+ // Since we nuked most of the migration code, this notification will fire
+ // once after legacy Sync has been disconnected (and should never fire
+ // again)
+ let nb = window.document.getElementById("global-notificationbox");
+
+ let msg = this.strings.GetStringFromName("autoDisconnectDescription")
+ let signInLabel = this.strings.GetStringFromName("autoDisconnectSignIn.label");
+ let signInAccessKey = this.strings.GetStringFromName("autoDisconnectSignIn.accessKey");
+ let learnMoreLink = this.fxaMigrator.learnMoreLink;
+
+ let buttons = [
+ {
+ label: signInLabel,
+ accessKey: signInAccessKey,
+ callback: () => {
+ this.openPreferences();
+ }
+ }
+ ];
+
+ let fragment = document.createDocumentFragment();
+ let msgNode = document.createTextNode(msg);
+ fragment.appendChild(msgNode);
+ if (learnMoreLink) {
+ let link = document.createElement("label");
+ link.className = "text-link";
+ link.setAttribute("value", learnMoreLink.text);
+ link.href = learnMoreLink.href;
+ fragment.appendChild(link);
+ }
+
+ nb.appendNotification(fragment,
+ this.SYNC_MIGRATION_NOTIFICATION_TITLE,
+ undefined,
+ nb.PRIORITY_WARNING_LOW,
+ buttons);
+
+ // ensure the hamburger menu reflects the newly disconnected state.
+ this.updateAppMenuItem();
+ },
+
+ handleEvent: function (event) {
+ this._inCustomizationMode = event.type == "customizationstarting";
+ this.updateAppMenuItem();
+ },
+
+ updateUI: function () {
+ // It's possible someone signed in to FxA after seeing our notification
+ // about "Legacy Sync migration" (which now is actually "Legacy Sync
+ // auto-disconnect") so kill that notification if it still exists.
+ let nb = window.document.getElementById("global-notificationbox");
+ let n = nb.getNotificationWithValue(this.SYNC_MIGRATION_NOTIFICATION_TITLE);
+ if (n) {
+ nb.removeNotification(n, true);
+ }
+
+ this.updateAppMenuItem();
+ },
+
+ // Note that updateAppMenuItem() returns a Promise that's only used by tests.
+ updateAppMenuItem: function () {
+ let profileInfoEnabled = false;
+ try {
+ profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled");
+ } catch (e) { }
+
+ // Bail out if FxA is disabled.
+ if (!this.weave.fxAccountsEnabled) {
+ return Promise.resolve();
+ }
+
+ this.panelUIFooter.hidden = false;
+
+ // Make sure the button is disabled in customization mode.
+ if (this._inCustomizationMode) {
+ this.panelUIStatus.setAttribute("disabled", "true");
+ this.panelUILabel.setAttribute("disabled", "true");
+ this.panelUIAvatar.setAttribute("disabled", "true");
+ this.panelUIIcon.setAttribute("disabled", "true");
+ } else {
+ this.panelUIStatus.removeAttribute("disabled");
+ this.panelUILabel.removeAttribute("disabled");
+ this.panelUIAvatar.removeAttribute("disabled");
+ this.panelUIIcon.removeAttribute("disabled");
+ }
+
+ let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
+ let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
+ let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
+ // The localization string is for the signed in text, but it's the default text as well
+ let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
+
+ let updateWithUserData = (userData) => {
+ // Window might have been closed while fetching data.
+ if (window.closed) {
+ return;
+ }
+
+ // Reset the button to its original state.
+ this.panelUILabel.setAttribute("label", defaultLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
+ this.panelUIFooter.removeAttribute("fxastatus");
+ this.panelUIFooter.removeAttribute("fxaprofileimage");
+ this.panelUIAvatar.style.removeProperty("list-style-image");
+ let showErrorBadge = false;
+ if (userData) {
+ // At this point we consider the user as logged-in (but still can be in an error state)
+ if (this.loginFailed) {
+ let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1);
+ this.panelUIFooter.setAttribute("fxastatus", "error");
+ this.panelUILabel.setAttribute("label", errorLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
+ showErrorBadge = true;
+ } else if (!userData.verified) {
+ let tooltipDescription = this.strings.formatStringFromName("verifyDescription", [userData.email], 1);
+ this.panelUIFooter.setAttribute("fxastatus", "error");
+ this.panelUIFooter.setAttribute("unverified", "true");
+ this.panelUILabel.setAttribute("label", unverifiedLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
+ showErrorBadge = true;
+ } else {
+ this.panelUIFooter.setAttribute("fxastatus", "signedin");
+ this.panelUILabel.setAttribute("label", userData.email);
+ }
+ if (profileInfoEnabled) {
+ this.panelUIFooter.setAttribute("fxaprofileimage", "enabled");
+ }
+ }
+ if (showErrorBadge) {
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
+ } else {
+ gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
+ }
+ }
+
+ let updateWithProfile = (profile) => {
+ if (profileInfoEnabled) {
+ if (profile.displayName) {
+ this.panelUILabel.setAttribute("label", profile.displayName);
+ }
+ if (profile.avatar) {
+ this.panelUIFooter.setAttribute("fxaprofileimage", "set");
+ let bgImage = "url(\"" + profile.avatar + "\")";
+ this.panelUIAvatar.style.listStyleImage = bgImage;
+
+ let img = new Image();
+ img.onerror = () => {
+ // Clear the image if it has trouble loading. Since this callback is asynchronous
+ // we check to make sure the image is still the same before we clear it.
+ if (this.panelUIAvatar.style.listStyleImage === bgImage) {
+ this.panelUIFooter.removeAttribute("fxaprofileimage");
+ this.panelUIAvatar.style.removeProperty("list-style-image");
+ }
+ };
+ img.src = profile.avatar;
+ }
+ }
+ }
+
+ return fxAccounts.getSignedInUser().then(userData => {
+ // userData may be null here when the user is not signed-in, but that's expected
+ updateWithUserData(userData);
+ // unverified users cause us to spew log errors fetching an OAuth token
+ // to fetch the profile, so don't even try in that case.
+ if (!userData || !userData.verified || !profileInfoEnabled) {
+ return null; // don't even try to grab the profile.
+ }
+ if (this._cachedProfile) {
+ return this._cachedProfile;
+ }
+ return fxAccounts.getSignedInUserProfile().catch(err => {
+ // Not fetching the profile is sad but the FxA logs will already have noise.
+ return null;
+ });
+ }).then(profile => {
+ if (!profile) {
+ return;
+ }
+ updateWithProfile(profile);
+ this._cachedProfile = profile; // Try to avoid fetching the profile on every UI update
+ }).catch(error => {
+ // This is most likely in tests, were we quickly log users in and out.
+ // The most likely scenario is a user logged out, so reflect that.
+ // Bug 995134 calls for better errors so we could retry if we were
+ // sure this was the failure reason.
+ this.FxAccountsCommon.log.error("Error updating FxA account info", error);
+ updateWithUserData(null);
+ });
+ },
+
+ onMenuPanelCommand: function () {
+
+ switch (this.panelUIFooter.getAttribute("fxastatus")) {
+ case "signedin":
+ this.openPreferences();
+ break;
+ case "error":
+ if (this.panelUIFooter.getAttribute("unverified")) {
+ this.openPreferences();
+ } else {
+ this.openSignInAgainPage("menupanel");
+ }
+ break;
+ default:
+ this.openPreferences();
+ break;
+ }
+
+ PanelUI.hide();
+ },
+
+ openPreferences: function () {
+ openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } });
+ },
+
+ openAccountsPage: function (action, urlParams={}) {
+ let params = new URLSearchParams();
+ if (action) {
+ params.set("action", action);
+ }
+ for (let name in urlParams) {
+ if (urlParams[name] !== undefined) {
+ params.set(name, urlParams[name]);
+ }
+ }
+ let url = "about:accounts?" + params;
+ switchToTabHavingURI(url, true, {
+ replaceQueryString: true
+ });
+ },
+
+ openSignInAgainPage: function (entryPoint) {
+ this.openAccountsPage("reauth", { entrypoint: entryPoint });
+ },
+
+ sendTabToDevice: function (url, clientId, title) {
+ Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
+ },
+
+ populateSendTabToDevicesMenu: function (devicesPopup, url, title) {
+ // remove existing menu items
+ while (devicesPopup.hasChildNodes()) {
+ devicesPopup.removeChild(devicesPopup.firstChild);
+ }
+
+ const fragment = document.createDocumentFragment();
+
+ const onTargetDeviceCommand = (event) => {
+ const clientId = event.target.getAttribute("clientId");
+ const clients = clientId
+ ? [clientId]
+ : this.remoteClients.map(client => client.id);
+
+ clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
+ }
+
+ function addTargetDevice(clientId, name) {
+ const targetDevice = document.createElement("menuitem");
+ targetDevice.addEventListener("command", onTargetDeviceCommand, true);
+ targetDevice.setAttribute("class", "sendtab-target");
+ targetDevice.setAttribute("clientId", clientId);
+ targetDevice.setAttribute("label", name);
+ fragment.appendChild(targetDevice);
+ }
+
+ const clients = this.remoteClients;
+ for (let client of clients) {
+ addTargetDevice(client.id, client.name);
+ }
+
+ // "All devices" menu item
+ if (clients.length > 1) {
+ const separator = document.createElement("menuseparator");
+ fragment.appendChild(separator);
+ const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
+ addTargetDevice("", allDevicesLabel);
+ }
+
+ devicesPopup.appendChild(fragment);
+ },
+
+ updateTabContextMenu: function (aPopupMenu) {
+ if (!this.sendTabToDeviceEnabled) {
+ return;
+ }
+
+ const remoteClientPresent = this.remoteClients.length > 0;
+ ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
+ .forEach(id => { document.getElementById(id).hidden = !remoteClientPresent });
+ },
+
+ initPageContextMenu: function (contextMenu) {
+ if (!this.sendTabToDeviceEnabled) {
+ return;
+ }
+
+ const remoteClientPresent = this.remoteClients.length > 0;
+ // showSendLink and showSendPage are mutually exclusive
+ const showSendLink = remoteClientPresent
+ && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
+ const showSendPage = !showSendLink && remoteClientPresent
+ && !(contextMenu.isContentSelected ||
+ contextMenu.onImage || contextMenu.onCanvas ||
+ contextMenu.onVideo || contextMenu.onAudio ||
+ contextMenu.onLink || contextMenu.onTextInput);
+
+ ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
+ .forEach(id => contextMenu.showItem(id, showSendPage));
+ ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
+ .forEach(id => contextMenu.showItem(id, showSendLink));
+ }
+};
+
+XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () {
+ return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
+});
+
+XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator",
+ "resource://services-sync/FxaMigrator.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
+ "resource://gre/modules/FxAccountsWebChannel.jsm");
diff --git a/browser/base/content/browser-gestureSupport.js b/browser/base/content/browser-gestureSupport.js
new file mode 100644
index 000000000..f472e5c9a
--- /dev/null
+++ b/browser/base/content/browser-gestureSupport.js
@@ -0,0 +1,1244 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Simple gestures support
+//
+// As per bug #412486, web content must not be allowed to receive any
+// simple gesture events. Multi-touch gesture APIs are in their
+// infancy and we do NOT want to be forced into supporting an API that
+// will probably have to change in the future. (The current Mac OS X
+// API is undocumented and was reverse-engineered.) Until support is
+// implemented in the event dispatcher to keep these events as
+// chrome-only, we must listen for the simple gesture events during
+// the capturing phase and call stopPropagation on every event.
+
+var gGestureSupport = {
+ _currentRotation: 0,
+ _lastRotateDelta: 0,
+ _rotateMomentumThreshold: .75,
+
+ /**
+ * Add or remove mouse gesture event listeners
+ *
+ * @param aAddListener
+ * True to add/init listeners and false to remove/uninit
+ */
+ init: function GS_init(aAddListener) {
+ const gestureEvents = ["SwipeGestureMayStart", "SwipeGestureStart",
+ "SwipeGestureUpdate", "SwipeGestureEnd", "SwipeGesture",
+ "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture",
+ "RotateGestureStart", "RotateGestureUpdate", "RotateGesture",
+ "TapGesture", "PressTapGesture"];
+
+ let addRemove = aAddListener ? window.addEventListener :
+ window.removeEventListener;
+
+ for (let event of gestureEvents) {
+ addRemove("Moz" + event, this, true);
+ }
+ },
+
+ /**
+ * Dispatch events based on the type of mouse gesture event. For now, make
+ * sure to stop propagation of every gesture event so that web content cannot
+ * receive gesture events.
+ *
+ * @param aEvent
+ * The gesture event to handle
+ */
+ handleEvent: function GS_handleEvent(aEvent) {
+ if (!Services.prefs.getBoolPref(
+ "dom.debug.propagate_gesture_events_through_content")) {
+ aEvent.stopPropagation();
+ }
+
+ // Create a preference object with some defaults
+ let def = (aThreshold, aLatched) =>
+ ({ threshold: aThreshold, latched: !!aLatched });
+
+ switch (aEvent.type) {
+ case "MozSwipeGestureMayStart":
+ if (this._shouldDoSwipeGesture(aEvent)) {
+ aEvent.preventDefault();
+ }
+ break;
+ case "MozSwipeGestureStart":
+ aEvent.preventDefault();
+ this._setupSwipeGesture();
+ break;
+ case "MozSwipeGestureUpdate":
+ aEvent.preventDefault();
+ this._doUpdate(aEvent);
+ break;
+ case "MozSwipeGestureEnd":
+ aEvent.preventDefault();
+ this._doEnd(aEvent);
+ break;
+ case "MozSwipeGesture":
+ aEvent.preventDefault();
+ this.onSwipe(aEvent);
+ break;
+ case "MozMagnifyGestureStart":
+ aEvent.preventDefault();
+ let pinchPref = AppConstants.platform == "win"
+ ? def(25, 0)
+ : def(150, 1);
+ this._setupGesture(aEvent, "pinch", pinchPref, "out", "in");
+ break;
+ case "MozRotateGestureStart":
+ aEvent.preventDefault();
+ this._setupGesture(aEvent, "twist", def(25, 0), "right", "left");
+ break;
+ case "MozMagnifyGestureUpdate":
+ case "MozRotateGestureUpdate":
+ aEvent.preventDefault();
+ this._doUpdate(aEvent);
+ break;
+ case "MozTapGesture":
+ aEvent.preventDefault();
+ this._doAction(aEvent, ["tap"]);
+ break;
+ case "MozRotateGesture":
+ aEvent.preventDefault();
+ this._doAction(aEvent, ["twist", "end"]);
+ break;
+ /* case "MozPressTapGesture":
+ break; */
+ }
+ },
+
+ /**
+ * Called at the start of "pinch" and "twist" gestures to setup all of the
+ * information needed to process the gesture
+ *
+ * @param aEvent
+ * The continual motion start event to handle
+ * @param aGesture
+ * Name of the gesture to handle
+ * @param aPref
+ * Preference object with the names of preferences and defaults
+ * @param aInc
+ * Command to trigger for increasing motion (without gesture name)
+ * @param aDec
+ * Command to trigger for decreasing motion (without gesture name)
+ */
+ _setupGesture: function GS__setupGesture(aEvent, aGesture, aPref, aInc, aDec) {
+ // Try to load user-set values from preferences
+ for (let [pref, def] of Object.entries(aPref))
+ aPref[pref] = this._getPref(aGesture + "." + pref, def);
+
+ // Keep track of the total deltas and latching behavior
+ let offset = 0;
+ let latchDir = aEvent.delta > 0 ? 1 : -1;
+ let isLatched = false;
+
+ // Create the update function here to capture closure state
+ this._doUpdate = function GS__doUpdate(aEvent) {
+ // Update the offset with new event data
+ offset += aEvent.delta;
+
+ // Check if the cumulative deltas exceed the threshold
+ if (Math.abs(offset) > aPref["threshold"]) {
+ // Trigger the action if we don't care about latching; otherwise, make
+ // sure either we're not latched and going the same direction of the
+ // initial motion; or we're latched and going the opposite way
+ let sameDir = (latchDir ^ offset) >= 0;
+ if (!aPref["latched"] || (isLatched ^ sameDir)) {
+ this._doAction(aEvent, [aGesture, offset > 0 ? aInc : aDec]);
+
+ // We must be getting latched or leaving it, so just toggle
+ isLatched = !isLatched;
+ }
+
+ // Reset motion counter to prepare for more of the same gesture
+ offset = 0;
+ }
+ };
+
+ // The start event also contains deltas, so handle an update right away
+ this._doUpdate(aEvent);
+ },
+
+ /**
+ * Checks whether a swipe gesture event can navigate the browser history or
+ * not.
+ *
+ * @param aEvent
+ * The swipe gesture event.
+ * @return true if the swipe event may navigate the history, false othwerwise.
+ */
+ _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) {
+ return this._getCommand(aEvent, ["swipe", "left"])
+ == "Browser:BackOrBackDuplicate" &&
+ this._getCommand(aEvent, ["swipe", "right"])
+ == "Browser:ForwardOrForwardDuplicate";
+ },
+
+ /**
+ * Checks whether we want to start a swipe for aEvent and sets
+ * aEvent.allowedDirections to the right values.
+ *
+ * @param aEvent
+ * The swipe gesture "MayStart" event.
+ * @return true if we're willing to start a swipe for this event, false
+ * otherwise.
+ */
+ _shouldDoSwipeGesture: function GS__shouldDoSwipeGesture(aEvent) {
+ if (!this._swipeNavigatesHistory(aEvent)) {
+ return false;
+ }
+
+ let isVerticalSwipe = false;
+ if (aEvent.direction == aEvent.DIRECTION_UP) {
+ if (gMultiProcessBrowser || content.pageYOffset > 0) {
+ return false;
+ }
+ isVerticalSwipe = true;
+ } else if (aEvent.direction == aEvent.DIRECTION_DOWN) {
+ if (gMultiProcessBrowser || content.pageYOffset < content.scrollMaxY) {
+ return false;
+ }
+ isVerticalSwipe = true;
+ }
+ if (isVerticalSwipe) {
+ // Vertical overscroll has been temporarily disabled until bug 939480 is
+ // fixed.
+ return false;
+ }
+
+ let canGoBack = gHistorySwipeAnimation.canGoBack();
+ let canGoForward = gHistorySwipeAnimation.canGoForward();
+ let isLTR = gHistorySwipeAnimation.isLTR;
+
+ if (canGoBack) {
+ aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_LEFT :
+ aEvent.DIRECTION_RIGHT;
+ }
+ if (canGoForward) {
+ aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_RIGHT :
+ aEvent.DIRECTION_LEFT;
+ }
+
+ return true;
+ },
+
+ /**
+ * Sets up swipe gestures. This includes setting up swipe animations for the
+ * gesture, if enabled.
+ *
+ * @param aEvent
+ * The swipe gesture start event.
+ * @return true if swipe gestures could successfully be set up, false
+ * othwerwise.
+ */
+ _setupSwipeGesture: function GS__setupSwipeGesture() {
+ gHistorySwipeAnimation.startAnimation(false);
+
+ this._doUpdate = function GS__doUpdate(aEvent) {
+ gHistorySwipeAnimation.updateAnimation(aEvent.delta);
+ };
+
+ this._doEnd = function GS__doEnd(aEvent) {
+ gHistorySwipeAnimation.swipeEndEventReceived();
+
+ this._doUpdate = function (aEvent) {};
+ this._doEnd = function (aEvent) {};
+ }
+ },
+
+ /**
+ * Generator producing the powerset of the input array where the first result
+ * is the complete set and the last result (before StopIteration) is empty.
+ *
+ * @param aArray
+ * Source array containing any number of elements
+ * @yield Array that is a subset of the input array from full set to empty
+ */
+ _power: function* GS__power(aArray) {
+ // Create a bitmask based on the length of the array
+ let num = 1 << aArray.length;
+ while (--num >= 0) {
+ // Only select array elements where the current bit is set
+ yield aArray.reduce(function (aPrev, aCurr, aIndex) {
+ if (num & 1 << aIndex)
+ aPrev.push(aCurr);
+ return aPrev;
+ }, []);
+ }
+ },
+
+ /**
+ * Determine what action to do for the gesture based on which keys are
+ * pressed and which commands are set, and execute the command.
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aGesture
+ * Array of gesture name parts (to be joined by periods)
+ * @return Name of the executed command. Returns null if no command is
+ * found.
+ */
+ _doAction: function GS__doAction(aEvent, aGesture) {
+ let command = this._getCommand(aEvent, aGesture);
+ return command && this._doCommand(aEvent, command);
+ },
+
+ /**
+ * Determine what action to do for the gesture based on which keys are
+ * pressed and which commands are set
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aGesture
+ * Array of gesture name parts (to be joined by periods)
+ */
+ _getCommand: function GS__getCommand(aEvent, aGesture) {
+ // Create an array of pressed keys in a fixed order so that a command for
+ // "meta" is preferred over "ctrl" when both buttons are pressed (and a
+ // command for both don't exist)
+ let keyCombos = [];
+ for (let key of ["shift", "alt", "ctrl", "meta"]) {
+ if (aEvent[key + "Key"])
+ keyCombos.push(key);
+ }
+
+ // Try each combination of key presses in decreasing order for commands
+ for (let subCombo of this._power(keyCombos)) {
+ // Convert a gesture and pressed keys into the corresponding command
+ // action where the preference has the gesture before "shift" before
+ // "alt" before "ctrl" before "meta" all separated by periods
+ let command;
+ try {
+ command = this._getPref(aGesture.concat(subCombo).join("."));
+ } catch (e) {}
+
+ if (command)
+ return command;
+ }
+ return null;
+ },
+
+ /**
+ * Execute the specified command.
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aCommand
+ * Name of the command found for the event's keys and gesture.
+ */
+ _doCommand: function GS__doCommand(aEvent, aCommand) {
+ let node = document.getElementById(aCommand);
+ if (node) {
+ if (node.getAttribute("disabled") != "true") {
+ let cmdEvent = document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent("command", true, true, window, 0,
+ aEvent.ctrlKey, aEvent.altKey,
+ aEvent.shiftKey, aEvent.metaKey, aEvent);
+ node.dispatchEvent(cmdEvent);
+ }
+
+ }
+ else {
+ goDoCommand(aCommand);
+ }
+ },
+
+ /**
+ * Handle continual motion events. This function will be set by
+ * _setupGesture or _setupSwipe.
+ *
+ * @param aEvent
+ * The continual motion update event to handle
+ */
+ _doUpdate: function(aEvent) {},
+
+ /**
+ * Handle gesture end events. This function will be set by _setupSwipe.
+ *
+ * @param aEvent
+ * The gesture end event to handle
+ */
+ _doEnd: function(aEvent) {},
+
+ /**
+ * Convert the swipe gesture into a browser action based on the direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ */
+ onSwipe: function GS_onSwipe(aEvent) {
+ // Figure out which one (and only one) direction was triggered
+ for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) {
+ if (aEvent.direction == aEvent["DIRECTION_" + dir]) {
+ this._coordinateSwipeEventWithAnimation(aEvent, dir);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Process a swipe event based on the given direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) {
+ this._doAction(aEvent, ["swipe", aDir.toLowerCase()]);
+ },
+
+ /**
+ * Coordinates the swipe event with the swipe animation, if any.
+ * If an animation is currently running, the swipe event will be
+ * processed once the animation stops. This will guarantee a fluid
+ * motion of the animation.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ _coordinateSwipeEventWithAnimation:
+ function GS__coordinateSwipeEventWithAnimation(aEvent, aDir) {
+ if ((gHistorySwipeAnimation.isAnimationRunning()) &&
+ (aDir == "RIGHT" || aDir == "LEFT")) {
+ gHistorySwipeAnimation.processSwipeEvent(aEvent, aDir);
+ }
+ else {
+ this.processSwipeEvent(aEvent, aDir);
+ }
+ },
+
+ /**
+ * Get a gesture preference or use a default if it doesn't exist
+ *
+ * @param aPref
+ * Name of the preference to load under the gesture branch
+ * @param aDef
+ * Default value if the preference doesn't exist
+ */
+ _getPref: function GS__getPref(aPref, aDef) {
+ // Preferences branch under which all gestures preferences are stored
+ const branch = "browser.gesture.";
+
+ try {
+ // Determine what type of data to load based on default value's type
+ let type = typeof aDef;
+ let getFunc = "Char";
+ if (type == "boolean")
+ getFunc = "Bool";
+ else if (type == "number")
+ getFunc = "Int";
+ return gPrefService["get" + getFunc + "Pref"](branch + aPref);
+ }
+ catch (e) {
+ return aDef;
+ }
+ },
+
+ /**
+ * Perform rotation for ImageDocuments
+ *
+ * @param aEvent
+ * The MozRotateGestureUpdate event triggering this call
+ */
+ rotate: function(aEvent) {
+ if (!(content.document instanceof ImageDocument))
+ return;
+
+ let contentElement = content.document.body.firstElementChild;
+ if (!contentElement)
+ return;
+ // If we're currently snapping, cancel that snap
+ if (contentElement.classList.contains("completeRotation"))
+ this._clearCompleteRotation();
+
+ this.rotation = Math.round(this.rotation + aEvent.delta);
+ contentElement.style.transform = "rotate(" + this.rotation + "deg)";
+ this._lastRotateDelta = aEvent.delta;
+ },
+
+ /**
+ * Perform a rotation end for ImageDocuments
+ */
+ rotateEnd: function() {
+ if (!(content.document instanceof ImageDocument))
+ return;
+
+ let contentElement = content.document.body.firstElementChild;
+ if (!contentElement)
+ return;
+
+ let transitionRotation = 0;
+
+ // The reason that 360 is allowed here is because when rotating between
+ // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong
+ // direction around--spinning wildly.
+ if (this.rotation <= 45)
+ transitionRotation = 0;
+ else if (this.rotation > 45 && this.rotation <= 135)
+ transitionRotation = 90;
+ else if (this.rotation > 135 && this.rotation <= 225)
+ transitionRotation = 180;
+ else if (this.rotation > 225 && this.rotation <= 315)
+ transitionRotation = 270;
+ else
+ transitionRotation = 360;
+
+ // If we're going fast enough, and we didn't already snap ahead of rotation,
+ // then snap ahead of rotation to simulate momentum
+ if (this._lastRotateDelta > this._rotateMomentumThreshold &&
+ this.rotation > transitionRotation)
+ transitionRotation += 90;
+ else if (this._lastRotateDelta < -1 * this._rotateMomentumThreshold &&
+ this.rotation < transitionRotation)
+ transitionRotation -= 90;
+
+ // Only add the completeRotation class if it is is necessary
+ if (transitionRotation != this.rotation) {
+ contentElement.classList.add("completeRotation");
+ contentElement.addEventListener("transitionend", this._clearCompleteRotation);
+ }
+
+ contentElement.style.transform = "rotate(" + transitionRotation + "deg)";
+ this.rotation = transitionRotation;
+ },
+
+ /**
+ * Gets the current rotation for the ImageDocument
+ */
+ get rotation() {
+ return this._currentRotation;
+ },
+
+ /**
+ * Sets the current rotation for the ImageDocument
+ *
+ * @param aVal
+ * The new value to take. Can be any value, but it will be bounded to
+ * 0 inclusive to 360 exclusive.
+ */
+ set rotation(aVal) {
+ this._currentRotation = aVal % 360;
+ if (this._currentRotation < 0)
+ this._currentRotation += 360;
+ return this._currentRotation;
+ },
+
+ /**
+ * When the location/tab changes, need to reload the current rotation for the
+ * image
+ */
+ restoreRotationState: function() {
+ // Bug 863514 - Make gesture support work in electrolysis
+ if (gMultiProcessBrowser)
+ return;
+
+ if (!(content.document instanceof ImageDocument))
+ return;
+
+ let contentElement = content.document.body.firstElementChild;
+ let transformValue = content.window.getComputedStyle(contentElement, null)
+ .transform;
+
+ if (transformValue == "none") {
+ this.rotation = 0;
+ return;
+ }
+
+ // transformValue is a rotation matrix--split it and do mathemagic to
+ // obtain the real rotation value
+ transformValue = transformValue.split("(")[1]
+ .split(")")[0]
+ .split(",");
+ this.rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) *
+ (180 / Math.PI));
+ },
+
+ /**
+ * Removes the transition rule by removing the completeRotation class
+ */
+ _clearCompleteRotation: function() {
+ let contentElement = content.document &&
+ content.document instanceof ImageDocument &&
+ content.document.body &&
+ content.document.body.firstElementChild;
+ if (!contentElement)
+ return;
+ contentElement.classList.remove("completeRotation");
+ contentElement.removeEventListener("transitionend", this._clearCompleteRotation);
+ },
+};
+
+// History Swipe Animation Support (bug 678392)
+var gHistorySwipeAnimation = {
+
+ active: false,
+ isLTR: false,
+
+ /**
+ * Initializes the support for history swipe animations, if it is supported
+ * by the platform/configuration.
+ */
+ init: function HSA_init() {
+ if (!this._isSupported())
+ return;
+
+ this.active = false;
+ this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)");
+ this._trackedSnapshots = [];
+ this._startingIndex = -1;
+ this._historyIndex = -1;
+ this._boxWidth = -1;
+ this._boxHeight = -1;
+ this._maxSnapshots = this._getMaxSnapshots();
+ this._lastSwipeDir = "";
+ this._direction = "horizontal";
+
+ // We only want to activate history swipe animations if we store snapshots.
+ // If we don't store any, we handle horizontal swipes without animations.
+ if (this._maxSnapshots > 0) {
+ this.active = true;
+ gBrowser.addEventListener("pagehide", this, false);
+ gBrowser.addEventListener("pageshow", this, false);
+ gBrowser.addEventListener("popstate", this, false);
+ gBrowser.addEventListener("DOMModalDialogClosed", this, false);
+ gBrowser.tabContainer.addEventListener("TabClose", this, false);
+ }
+ },
+
+ /**
+ * Uninitializes the support for history swipe animations.
+ */
+ uninit: function HSA_uninit() {
+ gBrowser.removeEventListener("pagehide", this, false);
+ gBrowser.removeEventListener("pageshow", this, false);
+ gBrowser.removeEventListener("popstate", this, false);
+ gBrowser.removeEventListener("DOMModalDialogClosed", this, false);
+ gBrowser.tabContainer.removeEventListener("TabClose", this, false);
+
+ this.active = false;
+ this.isLTR = false;
+ },
+
+ /**
+ * Starts the swipe animation and handles fast swiping (i.e. a swipe animation
+ * is already in progress when a new one is initiated).
+ *
+ * @param aIsVerticalSwipe
+ * Whether we're dealing with a vertical swipe or not.
+ */
+ startAnimation: function HSA_startAnimation(aIsVerticalSwipe) {
+ this._direction = aIsVerticalSwipe ? "vertical" : "horizontal";
+
+ if (this.isAnimationRunning()) {
+ // If this is a horizontal scroll, or if this is a vertical scroll that
+ // was started while a horizontal scroll was still running, handle it as
+ // as a fast swipe. In the case of the latter scenario, this allows us to
+ // start the vertical animation without first loading the final page, or
+ // taking another snapshot. If vertical scrolls are initiated repeatedly
+ // without prior horizontal scroll we skip this and restart the animation
+ // from 0.
+ if (this._direction == "horizontal" || this._lastSwipeDir != "") {
+ gBrowser.stop();
+ this._lastSwipeDir = "RELOAD"; // just ensure that != ""
+ this._canGoBack = this.canGoBack();
+ this._canGoForward = this.canGoForward();
+ this._handleFastSwiping();
+ }
+ this.updateAnimation(0);
+ }
+ else {
+ // Get the session history from SessionStore.
+ let updateSessionHistory = sessionHistory => {
+ this._startingIndex = sessionHistory.index;
+ this._historyIndex = this._startingIndex;
+ this._canGoBack = this.canGoBack();
+ this._canGoForward = this.canGoForward();
+ if (this.active) {
+ this._addBoxes();
+ this._takeSnapshot();
+ this._installPrevAndNextSnapshots();
+ this._lastSwipeDir = "";
+ }
+ this.updateAnimation(0);
+ }
+ SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory);
+ }
+ },
+
+ /**
+ * Stops the swipe animation.
+ */
+ stopAnimation: function HSA_stopAnimation() {
+ gHistorySwipeAnimation._removeBoxes();
+ this._historyIndex = this._getCurrentHistoryIndex();
+ },
+
+ /**
+ * Updates the animation between two pages in history.
+ *
+ * @param aVal
+ * A floating point value that represents the progress of the
+ * swipe gesture.
+ */
+ updateAnimation: function HSA_updateAnimation(aVal) {
+ if (!this.isAnimationRunning()) {
+ return;
+ }
+
+ // We use the following value to decrease the bounce effect when scrolling
+ // to the top or bottom of the page, or when swiping back/forward past the
+ // browsing history. This value was determined experimentally.
+ let dampValue = 4;
+ if (this._direction == "vertical") {
+ this._prevBox.collapsed = true;
+ this._nextBox.collapsed = true;
+ this._positionBox(this._curBox, -1 * aVal / dampValue);
+ } else if ((aVal >= 0 && this.isLTR) ||
+ (aVal <= 0 && !this.isLTR)) {
+ let tempDampValue = 1;
+ if (this._canGoBack) {
+ this._prevBox.collapsed = false;
+ } else {
+ tempDampValue = dampValue;
+ this._prevBox.collapsed = true;
+ }
+
+ // The current page is pushed to the right (LTR) or left (RTL),
+ // the intention is to go back.
+ // If there is a page to go back to, it should show in the background.
+ this._positionBox(this._curBox, aVal / tempDampValue);
+
+ // The forward page should be pushed offscreen all the way to the right.
+ this._positionBox(this._nextBox, 1);
+ } else if (this._canGoForward) {
+ // The intention is to go forward. If there is a page to go forward to,
+ // it should slide in from the right (LTR) or left (RTL).
+ // Otherwise, the current page should slide to the left (LTR) or
+ // right (RTL) and the backdrop should appear in the background.
+ // For the backdrop to be visible in that case, the previous page needs
+ // to be hidden (if it exists).
+ this._nextBox.collapsed = false;
+ let offset = this.isLTR ? 1 : -1;
+ this._positionBox(this._curBox, 0);
+ this._positionBox(this._nextBox, offset + aVal);
+ } else {
+ this._prevBox.collapsed = true;
+ this._positionBox(this._curBox, aVal / dampValue);
+ }
+ },
+
+ _getCurrentHistoryIndex: function() {
+ return SessionStore.getSessionHistory(gBrowser.selectedTab).index;
+ },
+
+ /**
+ * Event handler for events relevant to the history swipe animation.
+ *
+ * @param aEvent
+ * An event to process.
+ */
+ handleEvent: function HSA_handleEvent(aEvent) {
+ let browser = gBrowser.selectedBrowser;
+ switch (aEvent.type) {
+ case "TabClose":
+ let browserForTab = gBrowser.getBrowserForTab(aEvent.target);
+ this._removeTrackedSnapshot(-1, browserForTab);
+ break;
+ case "DOMModalDialogClosed":
+ this.stopAnimation();
+ break;
+ case "pageshow":
+ if (aEvent.target == browser.contentDocument) {
+ this.stopAnimation();
+ }
+ break;
+ case "popstate":
+ if (aEvent.target == browser.contentDocument.defaultView) {
+ this.stopAnimation();
+ }
+ break;
+ case "pagehide":
+ if (aEvent.target == browser.contentDocument) {
+ // Take and compress a snapshot of a page whenever it's about to be
+ // navigated away from. We already have a snapshot of the page if an
+ // animation is running, so we're left with compressing it.
+ if (!this.isAnimationRunning()) {
+ this._takeSnapshot();
+ }
+ this._compressSnapshotAtCurrentIndex();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Checks whether the history swipe animation is currently running or not.
+ *
+ * @return true if the animation is currently running, false otherwise.
+ */
+ isAnimationRunning: function HSA_isAnimationRunning() {
+ return !!this._container;
+ },
+
+ /**
+ * Process a swipe event based on the given direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ processSwipeEvent: function HSA_processSwipeEvent(aEvent, aDir) {
+ if (aDir == "RIGHT")
+ this._historyIndex += this.isLTR ? 1 : -1;
+ else if (aDir == "LEFT")
+ this._historyIndex += this.isLTR ? -1 : 1;
+ else
+ return;
+ this._lastSwipeDir = aDir;
+ },
+
+ /**
+ * Checks if there is a page in the browser history to go back to.
+ *
+ * @return true if there is a previous page in history, false otherwise.
+ */
+ canGoBack: function HSA_canGoBack() {
+ if (this.isAnimationRunning())
+ return this._doesIndexExistInHistory(this._historyIndex - 1);
+ return gBrowser.webNavigation.canGoBack;
+ },
+
+ /**
+ * Checks if there is a page in the browser history to go forward to.
+ *
+ * @return true if there is a next page in history, false otherwise.
+ */
+ canGoForward: function HSA_canGoForward() {
+ if (this.isAnimationRunning())
+ return this._doesIndexExistInHistory(this._historyIndex + 1);
+ return gBrowser.webNavigation.canGoForward;
+ },
+
+ /**
+ * Used to notify the history swipe animation that the OS sent a swipe end
+ * event and that we should navigate to the page that the user swiped to, if
+ * any. This will also result in the animation overlay to be torn down.
+ */
+ swipeEndEventReceived: function HSA_swipeEndEventReceived() {
+ // Update the session history before continuing.
+ let updateSessionHistory = sessionHistory => {
+ if (this._lastSwipeDir != "" && this._historyIndex != this._startingIndex)
+ this._navigateToHistoryIndex();
+ else
+ this.stopAnimation();
+ }
+ SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory);
+ },
+
+ /**
+ * Checks whether a particular index exists in the browser history or not.
+ *
+ * @param aIndex
+ * The index to check for availability for in the history.
+ * @return true if the index exists in the browser history, false otherwise.
+ */
+ _doesIndexExistInHistory: function HSA__doesIndexExistInHistory(aIndex) {
+ try {
+ return SessionStore.getSessionHistory(gBrowser.selectedTab).entries[aIndex] != null;
+ }
+ catch (ex) {
+ return false;
+ }
+ },
+
+ /**
+ * Navigates to the index in history that is currently being tracked by
+ * |this|.
+ */
+ _navigateToHistoryIndex: function HSA__navigateToHistoryIndex() {
+ if (this._doesIndexExistInHistory(this._historyIndex))
+ gBrowser.webNavigation.gotoIndex(this._historyIndex);
+ else
+ this.stopAnimation();
+ },
+
+ /**
+ * Checks to see if history swipe animations are supported by this
+ * platform/configuration.
+ *
+ * return true if supported, false otherwise.
+ */
+ _isSupported: function HSA__isSupported() {
+ return window.matchMedia("(-moz-swipe-animation-enabled)").matches;
+ },
+
+ /**
+ * Handle fast swiping (i.e. a swipe animation is already in
+ * progress when a new one is initiated). This will swap out the snapshots
+ * used in the previous animation with the appropriate new ones.
+ */
+ _handleFastSwiping: function HSA__handleFastSwiping() {
+ this._installCurrentPageSnapshot(null);
+ this._installPrevAndNextSnapshots();
+ },
+
+ /**
+ * Adds the boxes that contain the snapshots used during the swipe animation.
+ */
+ _addBoxes: function HSA__addBoxes() {
+ let browserStack =
+ document.getAnonymousElementByAttribute(gBrowser.getNotificationBox(),
+ "class", "browserStack");
+ this._container = this._createElement("historySwipeAnimationContainer",
+ "stack");
+ browserStack.appendChild(this._container);
+
+ this._prevBox = this._createElement("historySwipeAnimationPreviousPage",
+ "box");
+ this._container.appendChild(this._prevBox);
+
+ this._curBox = this._createElement("historySwipeAnimationCurrentPage",
+ "box");
+ this._container.appendChild(this._curBox);
+
+ this._nextBox = this._createElement("historySwipeAnimationNextPage",
+ "box");
+ this._container.appendChild(this._nextBox);
+
+ // Cache width and height.
+ this._boxWidth = this._curBox.getBoundingClientRect().width;
+ this._boxHeight = this._curBox.getBoundingClientRect().height;
+ },
+
+ /**
+ * Removes the boxes.
+ */
+ _removeBoxes: function HSA__removeBoxes() {
+ this._curBox = null;
+ this._prevBox = null;
+ this._nextBox = null;
+ if (this._container)
+ this._container.parentNode.removeChild(this._container);
+ this._container = null;
+ this._boxWidth = -1;
+ this._boxHeight = -1;
+ },
+
+ /**
+ * Creates an element with a given identifier and tag name.
+ *
+ * @param aID
+ * An identifier to create the element with.
+ * @param aTagName
+ * The name of the tag to create the element for.
+ * @return the newly created element.
+ */
+ _createElement: function HSA__createElement(aID, aTagName) {
+ let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let element = document.createElementNS(XULNS, aTagName);
+ element.id = aID;
+ return element;
+ },
+
+ /**
+ * Moves a given box to a given X coordinate position.
+ *
+ * @param aBox
+ * The box element to position.
+ * @param aPosition
+ * The position (in X coordinates) to move the box element to.
+ */
+ _positionBox: function HSA__positionBox(aBox, aPosition) {
+ let transform = "";
+
+ if (this._direction == "vertical")
+ transform = "translateY(" + this._boxHeight * aPosition + "px)";
+ else
+ transform = "translateX(" + this._boxWidth * aPosition + "px)";
+
+ aBox.style.transform = transform;
+ },
+
+ /**
+ * Verifies that we're ready to take snapshots based on the global pref and
+ * the current index in history.
+ *
+ * @return true if we're ready to take snapshots, false otherwise.
+ */
+ _readyToTakeSnapshots: function HSA__readyToTakeSnapshots() {
+ return (this._maxSnapshots >= 1 && this._getCurrentHistoryIndex() >= 0);
+ },
+
+ /**
+ * Takes a snapshot of the page the browser is currently on.
+ */
+ _takeSnapshot: function HSA__takeSnapshot() {
+ if (!this._readyToTakeSnapshots()) {
+ return;
+ }
+
+ let canvas = null;
+
+ let browser = gBrowser.selectedBrowser;
+ let r = browser.getBoundingClientRect();
+ canvas = document.createElementNS("http://www.w3.org/1999/xhtml",
+ "canvas");
+ canvas.mozOpaque = true;
+ let scale = window.devicePixelRatio;
+ canvas.width = r.width * scale;
+ canvas.height = r.height * scale;
+ let ctx = canvas.getContext("2d");
+ let zoom = browser.markupDocumentViewer.fullZoom * scale;
+ ctx.scale(zoom, zoom);
+ ctx.drawWindow(browser.contentWindow,
+ 0, 0, canvas.width / zoom, canvas.height / zoom, "white",
+ ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW |
+ ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
+
+ TelemetryStopwatch.start("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
+ try {
+ this._installCurrentPageSnapshot(canvas);
+ this._assignSnapshotToCurrentBrowser(canvas);
+ } finally {
+ TelemetryStopwatch.finish("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE");
+ }
+ },
+
+ /**
+ * Retrieves the maximum number of snapshots that should be kept in memory.
+ * This limit is a global limit and is valid across all open tabs.
+ */
+ _getMaxSnapshots: function HSA__getMaxSnapshots() {
+ return gPrefService.getIntPref("browser.snapshots.limit");
+ },
+
+ /**
+ * Adds a snapshot to the list and initiates the compression of said snapshot.
+ * Once the compression is completed, it will replace the uncompressed
+ * snapshot in the list.
+ *
+ * @param aCanvas
+ * The snapshot to add to the list and compress.
+ */
+ _assignSnapshotToCurrentBrowser:
+ function HSA__assignSnapshotToCurrentBrowser(aCanvas) {
+ let browser = gBrowser.selectedBrowser;
+ let currIndex = this._getCurrentHistoryIndex();
+
+ this._removeTrackedSnapshot(currIndex, browser);
+ this._addSnapshotRefToArray(currIndex, browser);
+
+ if (!("snapshots" in browser))
+ browser.snapshots = [];
+ let snapshots = browser.snapshots;
+ // Temporarily store the canvas as the compressed snapshot.
+ // This avoids a blank page if the user swipes quickly
+ // between pages before the compression could complete.
+ snapshots[currIndex] = {
+ image: aCanvas,
+ scale: window.devicePixelRatio
+ };
+ },
+
+ /**
+ * Compresses the HTMLCanvasElement that's stored at the current history
+ * index in the snapshot array and stores the compressed image in its place.
+ */
+ _compressSnapshotAtCurrentIndex:
+ function HSA__compressSnapshotAtCurrentIndex() {
+ if (!this._readyToTakeSnapshots()) {
+ // We didn't take a snapshot earlier because we weren't ready to, so
+ // there's nothing to compress.
+ return;
+ }
+
+ TelemetryStopwatch.start("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
+ try {
+ let browser = gBrowser.selectedBrowser;
+ let snapshots = browser.snapshots;
+ let currIndex = _getCurrentHistoryIndex();
+
+ // Kick off snapshot compression.
+ let canvas = snapshots[currIndex].image;
+ canvas.toBlob(function(aBlob) {
+ if (snapshots[currIndex]) {
+ snapshots[currIndex].image = aBlob;
+ }
+ }, "image/png"
+ );
+ } finally {
+ TelemetryStopwatch.finish("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE");
+ }
+ },
+
+ /**
+ * Removes a snapshot identified by the browser and index in the array of
+ * snapshots for that browser, if present. If no snapshot could be identified
+ * the method simply returns without taking any action. If aIndex is negative,
+ * all snapshots for a particular browser will be removed.
+ *
+ * @param aIndex
+ * The index in history of the new snapshot, or negative value if all
+ * snapshots for a browser should be removed.
+ * @param aBrowser
+ * The browser the new snapshot was taken in.
+ */
+ _removeTrackedSnapshot: function HSA__removeTrackedSnapshot(aIndex, aBrowser) {
+ let arr = this._trackedSnapshots;
+ let requiresExactIndexMatch = aIndex >= 0;
+ for (let i = 0; i < arr.length; i++) {
+ if ((arr[i].browser == aBrowser) &&
+ (aIndex < 0 || aIndex == arr[i].index)) {
+ delete aBrowser.snapshots[arr[i].index];
+ arr.splice(i, 1);
+ if (requiresExactIndexMatch)
+ return; // Found and removed the only element.
+ i--; // Make sure to revisit the index that we just removed an
+ // element at.
+ }
+ }
+ },
+
+ /**
+ * Adds a new snapshot reference for a given index and browser to the array
+ * of references to tracked snapshots.
+ *
+ * @param aIndex
+ * The index in history of the new snapshot.
+ * @param aBrowser
+ * The browser the new snapshot was taken in.
+ */
+ _addSnapshotRefToArray:
+ function HSA__addSnapshotRefToArray(aIndex, aBrowser) {
+ let id = { index: aIndex,
+ browser: aBrowser };
+ let arr = this._trackedSnapshots;
+ arr.unshift(id);
+
+ while (arr.length > this._maxSnapshots) {
+ let lastElem = arr[arr.length - 1];
+ delete lastElem.browser.snapshots[lastElem.index].image;
+ delete lastElem.browser.snapshots[lastElem.index];
+ arr.splice(-1, 1);
+ }
+ },
+
+ /**
+ * Converts a compressed blob to an Image object. In some situations
+ * (especially during fast swiping) aBlob may still be a canvas, not a
+ * compressed blob. In this case, we simply return the canvas.
+ *
+ * @param aBlob
+ * The compressed blob to convert, or a canvas if a blob compression
+ * couldn't complete before this method was called.
+ * @return A new Image object representing the converted blob.
+ */
+ _convertToImg: function HSA__convertToImg(aBlob) {
+ if (!aBlob)
+ return null;
+
+ // Return aBlob if it's still a canvas and not a compressed blob yet.
+ if (aBlob instanceof HTMLCanvasElement)
+ return aBlob;
+
+ let img = new Image();
+ let url = "";
+ try {
+ url = URL.createObjectURL(aBlob);
+ img.onload = function() {
+ URL.revokeObjectURL(url);
+ };
+ }
+ finally {
+ img.src = url;
+ return img;
+ }
+ },
+
+ /**
+ * Scales the background of a given box element (which uses a given snapshot
+ * as background) based on a given scale factor.
+ * @param aSnapshot
+ * The snapshot that is used as background of aBox.
+ * @param aScale
+ * The scale factor to use.
+ * @param aBox
+ * The box element that uses aSnapshot as background.
+ */
+ _scaleSnapshot: function HSA__scaleSnapshot(aSnapshot, aScale, aBox) {
+ if (aSnapshot && aScale != 1 && aBox) {
+ if (aSnapshot instanceof HTMLCanvasElement) {
+ aBox.style.backgroundSize =
+ aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
+ } else {
+ // snapshot is instanceof HTMLImageElement
+ aSnapshot.addEventListener("load", function() {
+ aBox.style.backgroundSize =
+ aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px";
+ });
+ }
+ }
+ },
+
+ /**
+ * Sets the snapshot of the current page to the snapshot passed as parameter,
+ * or to the one previously stored for the current index in history if the
+ * parameter is null.
+ *
+ * @param aCanvas
+ * The snapshot to set the current page to. If this parameter is null,
+ * the previously stored snapshot for this index (if any) will be used.
+ */
+ _installCurrentPageSnapshot:
+ function HSA__installCurrentPageSnapshot(aCanvas) {
+ let currSnapshot = aCanvas;
+ let scale = window.devicePixelRatio;
+ if (!currSnapshot) {
+ let snapshots = gBrowser.selectedBrowser.snapshots || {};
+ let currIndex = this._historyIndex;
+ if (currIndex in snapshots) {
+ currSnapshot = this._convertToImg(snapshots[currIndex].image);
+ scale = snapshots[currIndex].scale;
+ }
+ }
+ this._scaleSnapshot(currSnapshot, scale, this._curBox ? this._curBox :
+ null);
+ document.mozSetImageElement("historySwipeAnimationCurrentPageSnapshot",
+ currSnapshot);
+ },
+
+ /**
+ * Sets the snapshots of the previous and next pages to the snapshots
+ * previously stored for their respective indeces.
+ */
+ _installPrevAndNextSnapshots:
+ function HSA__installPrevAndNextSnapshots() {
+ let snapshots = gBrowser.selectedBrowser.snapshots || [];
+ let currIndex = this._historyIndex;
+ let prevIndex = currIndex - 1;
+ let prevSnapshot = null;
+ if (prevIndex in snapshots) {
+ prevSnapshot = this._convertToImg(snapshots[prevIndex].image);
+ this._scaleSnapshot(prevSnapshot, snapshots[prevIndex].scale,
+ this._prevBox);
+ }
+ document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot",
+ prevSnapshot);
+
+ let nextIndex = currIndex + 1;
+ let nextSnapshot = null;
+ if (nextIndex in snapshots) {
+ nextSnapshot = this._convertToImg(snapshots[nextIndex].image);
+ this._scaleSnapshot(nextSnapshot, snapshots[nextIndex].scale,
+ this._nextBox);
+ }
+ document.mozSetImageElement("historySwipeAnimationNextPageSnapshot",
+ nextSnapshot);
+ },
+};
diff --git a/browser/base/content/browser-media.js b/browser/base/content/browser-media.js
new file mode 100644
index 000000000..81e7faf17
--- /dev/null
+++ b/browser/base/content/browser-media.js
@@ -0,0 +1,365 @@
+/* -*- 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 gEMEHandler = {
+ get uiEnabled() {
+ let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled");
+ // Force-disable on WinXP:
+ if (navigator.platform.toLowerCase().startsWith("win")) {
+ emeUIEnabled = emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6;
+ }
+ return emeUIEnabled;
+ },
+ ensureEMEEnabled: function(browser, keySystem) {
+ Services.prefs.setBoolPref("media.eme.enabled", true);
+ if (keySystem) {
+ if (keySystem.startsWith("com.adobe") &&
+ Services.prefs.getPrefType("media.gmp-eme-adobe.enabled") &&
+ !Services.prefs.getBoolPref("media.gmp-eme-adobe.enabled")) {
+ Services.prefs.setBoolPref("media.gmp-eme-adobe.enabled", true);
+ } else if (keySystem == "com.widevine.alpha" &&
+ Services.prefs.getPrefType("media.gmp-widevinecdm.enabled") &&
+ !Services.prefs.getBoolPref("media.gmp-widevinecdm.enabled")) {
+ Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", true);
+ }
+ }
+ browser.reload();
+ },
+ isKeySystemVisible: function(keySystem) {
+ if (!keySystem) {
+ return false;
+ }
+ if (keySystem.startsWith("com.adobe") &&
+ Services.prefs.getPrefType("media.gmp-eme-adobe.visible")) {
+ return Services.prefs.getBoolPref("media.gmp-eme-adobe.visible");
+ }
+ if (keySystem == "com.widevine.alpha" &&
+ Services.prefs.getPrefType("media.gmp-widevinecdm.visible")) {
+ return Services.prefs.getBoolPref("media.gmp-widevinecdm.visible");
+ }
+ return true;
+ },
+ getLearnMoreLink: function(msgId) {
+ let text = gNavigatorBundle.getString("emeNotifications." + msgId + ".learnMoreLabel");
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ return "<label class='text-link' href='" + baseURL + "drm-content'>" +
+ text + "</label>";
+ },
+ receiveMessage: function({target: browser, data: data}) {
+ let parsedData;
+ try {
+ parsedData = JSON.parse(data);
+ } catch (ex) {
+ Cu.reportError("Malformed EME video message with data: " + data);
+ return;
+ }
+ let {status: status, keySystem: keySystem} = parsedData;
+ // Don't need to show if disabled or keysystem not visible.
+ if (!this.uiEnabled || !this.isKeySystemVisible(keySystem)) {
+ return;
+ }
+
+ let notificationId;
+ let buttonCallback;
+ let params = [];
+ switch (status) {
+ case "available":
+ case "cdm-created":
+ // Only show the chain icon for proprietary CDMs. Clearkey is not one.
+ if (keySystem != "org.w3.clearkey") {
+ this.showPopupNotificationForSuccess(browser, keySystem);
+ }
+ // ... and bail!
+ return;
+
+ case "api-disabled":
+ case "cdm-disabled":
+ notificationId = "drmContentDisabled";
+ buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem)
+ params = [this.getLearnMoreLink(notificationId)];
+ break;
+
+ case "cdm-insufficient-version":
+ notificationId = "drmContentCDMInsufficientVersion";
+ params = [this._brandShortName];
+ break;
+
+ case "cdm-not-installed":
+ notificationId = "drmContentCDMInstalling";
+ params = [this._brandShortName];
+ break;
+
+ case "cdm-not-supported":
+ // Not to pop up user-level notification because they cannot do anything
+ // about it.
+ return;
+ default:
+ Cu.reportError(new Error("Unknown message ('" + status + "') dealing with EME key request: " + data));
+ return;
+ }
+
+ this.showNotificationBar(browser, notificationId, keySystem, params, buttonCallback);
+ },
+ showNotificationBar: function(browser, notificationId, keySystem, labelParams, callback) {
+ let box = gBrowser.getNotificationBox(browser);
+ if (box.getNotificationWithValue(notificationId)) {
+ return;
+ }
+
+ let msgPrefix = "emeNotifications." + notificationId + ".";
+ let msgId = msgPrefix + "message";
+
+ let message = labelParams.length ?
+ gNavigatorBundle.getFormattedString(msgId, labelParams) :
+ gNavigatorBundle.getString(msgId);
+
+ let buttons = [];
+ if (callback) {
+ let btnLabelId = msgPrefix + "button.label";
+ let btnAccessKeyId = msgPrefix + "button.accesskey";
+ buttons.push({
+ label: gNavigatorBundle.getString(btnLabelId),
+ accessKey: gNavigatorBundle.getString(btnAccessKeyId),
+ callback: callback
+ });
+ }
+
+ let iconURL = "chrome://browser/skin/drm-icon.svg#chains-black";
+
+ // Do a little dance to get rich content into the notification:
+ let fragment = document.createDocumentFragment();
+ let descriptionContainer = document.createElement("description");
+ descriptionContainer.innerHTML = message;
+ while (descriptionContainer.childNodes.length) {
+ fragment.appendChild(descriptionContainer.childNodes[0]);
+ }
+
+ box.appendNotification(fragment, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM,
+ buttons);
+ },
+ showPopupNotificationForSuccess: function(browser, keySystem) {
+ // We're playing EME content! Remove any "we can't play because..." messages.
+ var box = gBrowser.getNotificationBox(browser);
+ ["drmContentDisabled",
+ "drmContentCDMInstalling"
+ ].forEach(function (value) {
+ var notification = box.getNotificationWithValue(value);
+ if (notification)
+ box.removeNotification(notification);
+ });
+
+ // Don't bother creating it if it's already there:
+ if (PopupNotifications.getNotification("drmContentPlaying", browser)) {
+ return;
+ }
+
+ let msgPrefix = "emeNotifications.drmContentPlaying.";
+ let msgId = msgPrefix + "message2";
+ let btnLabelId = msgPrefix + "button.label";
+ let btnAccessKeyId = msgPrefix + "button.accesskey";
+
+ let message = gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]);
+ let anchorId = "eme-notification-icon";
+ let firstPlayPref = "browser.eme.ui.firstContentShown";
+ if (!Services.prefs.getPrefType(firstPlayPref) ||
+ !Services.prefs.getBoolPref(firstPlayPref)) {
+ document.getElementById(anchorId).setAttribute("firstplay", "true");
+ Services.prefs.setBoolPref(firstPlayPref, true);
+ } else {
+ document.getElementById(anchorId).removeAttribute("firstplay");
+ }
+
+ let mainAction = {
+ label: gNavigatorBundle.getString(btnLabelId),
+ accessKey: gNavigatorBundle.getString(btnAccessKeyId),
+ callback: function() { openPreferences("paneContent"); },
+ dismiss: true
+ };
+ let options = {
+ dismissed: true,
+ eventCallback: aTopic => aTopic == "swapping",
+ learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content",
+ };
+ PopupNotifications.show(browser, "drmContentPlaying", message, anchorId, mainAction, null, options);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener])
+};
+
+XPCOMUtils.defineLazyGetter(gEMEHandler, "_brandShortName", function() {
+ return document.getElementById("bundle_brand").getString("brandShortName");
+});
+
+const TELEMETRY_DDSTAT_SHOWN = 0;
+const TELEMETRY_DDSTAT_SHOWN_FIRST = 1;
+const TELEMETRY_DDSTAT_CLICKED = 2;
+const TELEMETRY_DDSTAT_CLICKED_FIRST = 3;
+const TELEMETRY_DDSTAT_SOLVED = 4;
+
+let gDecoderDoctorHandler = {
+ getLabelForNotificationBox(type) {
+ if (type == "adobe-cdm-not-found" &&
+ AppConstants.platform == "win") {
+ if (AppConstants.isPlatformAndVersionAtMost("win", "5.9")) {
+ // We supply our own Learn More button so we don't need to populate the message here.
+ return gNavigatorBundle.getFormattedString("emeNotifications.drmContentDisabled.message", [""]);
+ }
+ return gNavigatorBundle.getString("decoder.noCodecs.message");
+ }
+ if (type == "adobe-cdm-not-activated" &&
+ AppConstants.platform == "win") {
+ if (AppConstants.isPlatformAndVersionAtMost("win", "5.9")) {
+ return gNavigatorBundle.getString("decoder.noCodecsXP.message");
+ }
+ if (!AppConstants.isPlatformAndVersionAtLeast("win", "6.1")) {
+ return gNavigatorBundle.getString("decoder.noCodecsVista.message");
+ }
+ return gNavigatorBundle.getString("decoder.noCodecs.message");
+ }
+ if (type == "platform-decoder-not-found") {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.1")) {
+ return gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+ }
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6")) {
+ return gNavigatorBundle.getString("decoder.noHWAccelerationVista.message");
+ }
+ if (AppConstants.platform == "linux") {
+ return gNavigatorBundle.getString("decoder.noCodecsLinux.message");
+ }
+ }
+ if (type == "cannot-initialize-pulseaudio") {
+ return gNavigatorBundle.getString("decoder.noPulseAudio.message");
+ }
+ if (type == "unsupported-libavcodec" &&
+ AppConstants.platform == "linux") {
+ return gNavigatorBundle.getString("decoder.unsupportedLibavcodec.message");
+ }
+ return "";
+ },
+
+ getSumoForLearnHowButton(type) {
+ if (AppConstants.platform == "win") {
+ return "fix-video-audio-problems-firefox-windows";
+ }
+ if (type == "cannot-initialize-pulseaudio") {
+ return "fix-common-audio-and-video-issues";
+ }
+ return "";
+ },
+
+ receiveMessage({target: browser, data: data}) {
+ let box = gBrowser.getNotificationBox(browser);
+ let notificationId = "decoder-doctor-notification";
+ if (box.getNotificationWithValue(notificationId)) {
+ return;
+ }
+
+ let parsedData;
+ try {
+ parsedData = JSON.parse(data);
+ } catch (ex) {
+ Cu.reportError("Malformed Decoder Doctor message with data: " + data);
+ return;
+ }
+ // parsedData (the result of parsing the incoming 'data' json string)
+ // contains analysis information from Decoder Doctor:
+ // - 'type' is the type of issue, it determines which text to show in the
+ // infobar.
+ // - 'decoderDoctorReportId' is the Decoder Doctor issue identifier, to be
+ // used here as key for the telemetry (counting infobar displays,
+ // "Learn how" buttons clicks, and resolutions) and for the prefs used
+ // to store at-issue formats.
+ // - 'formats' contains a comma-separated list of formats (or key systems)
+ // that suffer the issue. These are kept in a pref, which the backend
+ // uses to later find when an issue is resolved.
+ // - 'isSolved' is true when the notification actually indicates the
+ // resolution of that issue, to be reported as telemetry.
+ let {type, isSolved, decoderDoctorReportId, formats} = parsedData;
+ type = type.toLowerCase();
+ // Error out early on invalid ReportId
+ if (!(/^\w+$/mi).test(decoderDoctorReportId)) {
+ return
+ }
+ let title = gDecoderDoctorHandler.getLabelForNotificationBox(type);
+ if (!title) {
+ return;
+ }
+
+ // We keep the list of formats in prefs for the sake of the decoder itself,
+ // which reads it to determine when issues get solved for these formats.
+ // (Writing prefs from e10s content is now allowed.)
+ let formatsPref = "media.decoder-doctor." + decoderDoctorReportId + ".formats";
+ let buttonClickedPref = "media.decoder-doctor." + decoderDoctorReportId + ".button-clicked";
+ let histogram =
+ Services.telemetry.getKeyedHistogramById("DECODER_DOCTOR_INFOBAR_STATS");
+
+ let formatsInPref = Services.prefs.getPrefType(formatsPref) &&
+ Services.prefs.getCharPref(formatsPref);
+
+ if (!isSolved) {
+ if (!formats) {
+ Cu.reportError("Malformed Decoder Doctor unsolved message with no formats");
+ return;
+ }
+ if (!formatsInPref) {
+ Services.prefs.setCharPref(formatsPref, formats);
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN_FIRST);
+ } else {
+ // Split existing formats into an array of strings.
+ let existing = formatsInPref.split(",").map(String.trim);
+ // Keep given formats that were not already recorded.
+ let newbies = formats.split(",").map(String.trim)
+ .filter(x => !existing.includes(x));
+ // And rewrite pref with the added new formats (if any).
+ if (newbies.length) {
+ Services.prefs.setCharPref(formatsPref,
+ existing.concat(newbies).join(", "));
+ }
+ }
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN);
+
+ let buttons = [];
+ let sumo = gDecoderDoctorHandler.getSumoForLearnHowButton(type);
+ if (sumo) {
+ buttons.push({
+ label: gNavigatorBundle.getString("decoder.noCodecs.button"),
+ accessKey: gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ callback() {
+ let clickedInPref = Services.prefs.getPrefType(buttonClickedPref) &&
+ Services.prefs.getBoolPref(buttonClickedPref);
+ if (!clickedInPref) {
+ Services.prefs.setBoolPref(buttonClickedPref, true);
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED_FIRST);
+ }
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED);
+
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ openUILinkIn(baseURL + sumo, "tab");
+ }
+ });
+ }
+
+ box.appendNotification(
+ title,
+ notificationId,
+ "", // This uses the info icon as specified below.
+ box.PRIORITY_INFO_LOW,
+ buttons
+ );
+ } else if (formatsInPref) {
+ // Issue is solved, and prefs haven't been cleared yet, meaning it's the
+ // first time we get this resolution -> Clear prefs and report telemetry.
+ Services.prefs.clearUserPref(formatsPref);
+ Services.prefs.clearUserPref(buttonClickedPref);
+ histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SOLVED);
+ }
+ },
+}
+
+window.getGroupMessageManager("browsers").addMessageListener("DecoderDoctor:Notification", gDecoderDoctorHandler);
+window.getGroupMessageManager("browsers").addMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler);
+window.addEventListener("unload", function() {
+ window.getGroupMessageManager("browsers").removeMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler);
+ window.getGroupMessageManager("browsers").removeMessageListener("DecoderDoctor:Notification", gDecoderDoctorHandler);
+}, false);
diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc
new file mode 100644
index 000000000..8f77ebafe
--- /dev/null
+++ b/browser/base/content/browser-menubar.inc
@@ -0,0 +1,535 @@
+# -*- 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/.
+
+ <menubar id="main-menubar"
+ onpopupshowing="if (event.target.parentNode.parentNode == this &amp;&amp;
+ !('@mozilla.org/widget/nativemenuservice;1' in Cc))
+ this.setAttribute('openedwithkey',
+ event.target.parentNode.openedWithKey);"
+ style="border:0px;padding:0px;margin:0px;-moz-appearance:none">
+ <menu id="file-menu" label="&fileMenu.label;"
+ accesskey="&fileMenu.accesskey;">
+ <menupopup id="menu_FilePopup"
+ onpopupshowing="updateUserContextUIVisibility();">
+ <menuitem id="menu_newNavigatorTab"
+ label="&tabCmd.label;"
+ command="cmd_newNavigatorTab"
+ key="key_newNavigatorTab"
+ accesskey="&tabCmd.accesskey;"/>
+ <menu id="menu_newUserContext"
+ label="&newUserContext.label;"
+ accesskey="&newUserContext.accesskey;"
+ hidden="true">
+ <menupopup onpopupshowing="return createUserContextMenu(event);" />
+ </menu>
+ <menuitem id="menu_newNavigator"
+ label="&newNavigatorCmd.label;"
+ accesskey="&newNavigatorCmd.accesskey;"
+ key="key_newNavigator"
+ command="cmd_newNavigator"/>
+ <menuitem id="menu_newPrivateWindow"
+ label="&newPrivateWindow.label;"
+ accesskey="&newPrivateWindow.accesskey;"
+ command="Tools:PrivateBrowsing"
+ key="key_privatebrowsing"/>
+#ifdef E10S_TESTING_ONLY
+ <menuitem id="menu_newNonRemoteWindow"
+ label="&newNonRemoteWindow.label;"
+ hidden="true"
+ command="Tools:NonRemoteWindow"/>
+#endif
+#ifdef MAC_NON_BROWSER_WINDOW
+ <menuitem id="menu_openLocation"
+ label="&openLocationCmd.label;"
+ command="Browser:OpenLocation"
+ key="focusURLBar"/>
+#endif
+ <menuitem id="menu_openFile"
+ label="&openFileCmd.label;"
+ command="Browser:OpenFile"
+ key="openFileKb"
+ accesskey="&openFileCmd.accesskey;"/>
+ <menuitem id="menu_close"
+ class="show-only-for-keyboard"
+ label="&closeCmd.label;"
+ key="key_close"
+ accesskey="&closeCmd.accesskey;"
+ command="cmd_close"/>
+ <menuitem id="menu_closeWindow"
+ class="show-only-for-keyboard"
+ hidden="true"
+ command="cmd_closeWindow"
+ key="key_closeWindow"
+ label="&closeWindow.label;"
+ accesskey="&closeWindow.accesskey;"/>
+ <menuseparator/>
+ <menuitem id="menu_savePage"
+ label="&savePageCmd.label;"
+ accesskey="&savePageCmd.accesskey;"
+ key="key_savePage"
+ command="Browser:SavePage"/>
+ <menuitem id="menu_sendLink"
+ label="&emailPageCmd.label;"
+ accesskey="&emailPageCmd.accesskey;"
+ command="Browser:SendLink"/>
+ <menuseparator/>
+#if !defined(MOZ_WIDGET_GTK)
+ <menuitem id="menu_printSetup"
+ label="&printSetupCmd.label;"
+ accesskey="&printSetupCmd.accesskey;"
+ command="cmd_pageSetup"/>
+#endif
+#ifndef XP_MACOSX
+ <menuitem id="menu_printPreview"
+ label="&printPreviewCmd.label;"
+ accesskey="&printPreviewCmd.accesskey;"
+ command="cmd_printPreview"/>
+#endif
+ <menuitem id="menu_print"
+ label="&printCmd.label;"
+ accesskey="&printCmd.accesskey;"
+ key="printKb"
+ command="cmd_print"/>
+ <menuseparator/>
+ <menuitem id="goOfflineMenuitem"
+ label="&goOfflineCmd.label;"
+ accesskey="&goOfflineCmd.accesskey;"
+ type="checkbox"
+ observes="workOfflineMenuitemState"
+ oncommand="BrowserOffline.toggleOfflineStatus();"/>
+ <menuitem id="menu_FileQuitItem"
+#ifdef XP_WIN
+ label="&quitApplicationCmdWin2.label;"
+ accesskey="&quitApplicationCmdWin2.accesskey;"
+#else
+#ifdef XP_MACOSX
+ label="&quitApplicationCmdMac2.label;"
+#else
+ label="&quitApplicationCmd.label;"
+ accesskey="&quitApplicationCmd.accesskey;"
+#endif
+#ifdef XP_UNIX
+ key="key_quitApplication"
+#endif
+#endif
+ command="cmd_quitApplication"/>
+ </menupopup>
+ </menu>
+
+ <menu id="edit-menu" label="&editMenu.label;"
+ accesskey="&editMenu.accesskey;">
+ <menupopup id="menu_EditPopup"
+ onpopupshowing="updateEditUIVisibility()"
+ onpopuphidden="updateEditUIVisibility()">
+ <menuitem id="menu_undo"
+ label="&undoCmd.label;"
+ key="key_undo"
+ accesskey="&undoCmd.accesskey;"
+ command="cmd_undo"/>
+ <menuitem id="menu_redo"
+ label="&redoCmd.label;"
+ key="key_redo"
+ accesskey="&redoCmd.accesskey;"
+ command="cmd_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"
+ label="&cutCmd.label;"
+ key="key_cut"
+ accesskey="&cutCmd.accesskey;"
+ command="cmd_cut"/>
+ <menuitem id="menu_copy"
+ label="&copyCmd.label;"
+ key="key_copy"
+ accesskey="&copyCmd.accesskey;"
+ command="cmd_copy"/>
+ <menuitem id="menu_paste"
+ label="&pasteCmd.label;"
+ key="key_paste"
+ accesskey="&pasteCmd.accesskey;"
+ command="cmd_paste"/>
+ <menuitem id="menu_delete"
+ label="&deleteCmd.label;"
+ key="key_delete"
+ accesskey="&deleteCmd.accesskey;"
+ command="cmd_delete"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"
+ label="&selectAllCmd.label;"
+ key="key_selectAll"
+ accesskey="&selectAllCmd.accesskey;"
+ command="cmd_selectAll"/>
+ <menuseparator/>
+ <menuitem id="menu_find"
+ label="&findOnCmd.label;"
+ accesskey="&findOnCmd.accesskey;"
+ key="key_find"
+ command="cmd_find"/>
+ <menuitem id="menu_findAgain"
+ class="show-only-for-keyboard"
+ label="&findAgainCmd.label;"
+ accesskey="&findAgainCmd.accesskey;"
+ key="key_findAgain"
+ command="cmd_findAgain"/>
+ <menuseparator hidden="true" id="textfieldDirection-separator"/>
+ <menuitem id="textfieldDirection-swap"
+ command="cmd_switchTextDirection"
+ key="key_switchTextDirection"
+ label="&bidiSwitchTextDirectionItem.label;"
+ accesskey="&bidiSwitchTextDirectionItem.accesskey;"
+ hidden="true"/>
+#ifdef XP_UNIX
+#ifndef XP_MACOSX
+ <menuseparator/>
+ <menuitem id="menu_preferences"
+ label="&preferencesCmdUnix.label;"
+ accesskey="&preferencesCmdUnix.accesskey;"
+ oncommand="openPreferences();"/>
+#endif
+#endif
+ </menupopup>
+ </menu>
+
+ <menu id="view-menu" label="&viewMenu.label;"
+ accesskey="&viewMenu.accesskey;">
+ <menupopup id="menu_viewPopup"
+ onpopupshowing="updateCharacterEncodingMenuState();">
+ <menu id="viewToolbarsMenu"
+ label="&viewToolbarsMenu.label;"
+ accesskey="&viewToolbarsMenu.accesskey;">
+ <menupopup onpopupshowing="onViewToolbarsPopupShowing(event);">
+ <menuseparator/>
+ <menuitem id="menu_customizeToolbars"
+ label="&viewCustomizeToolbar.label;"
+ accesskey="&viewCustomizeToolbar.accesskey;"
+ command="cmd_CustomizeToolbars"/>
+ </menupopup>
+ </menu>
+ <menu id="viewSidebarMenuMenu"
+ label="&viewSidebarMenu.label;"
+ accesskey="&viewSidebarMenu.accesskey;">
+ <menupopup id="viewSidebarMenu">
+ <menuitem id="menu_bookmarksSidebar"
+ key="viewBookmarksSidebarKb"
+ observes="viewBookmarksSidebar"/>
+ <menuitem id="menu_historySidebar"
+ key="key_gotoHistory"
+ observes="viewHistorySidebar"
+ label="&historyButton.label;"/>
+ <menuitem id="menu_tabsSidebar"
+ observes="viewTabsSidebar"
+ label="&syncedTabs.sidebar.label;"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menu id="viewFullZoomMenu" label="&fullZoom.label;"
+ accesskey="&fullZoom.accesskey;"
+ onpopupshowing="FullZoom.updateMenu();">
+ <menupopup>
+ <menuitem id="menu_zoomEnlarge"
+ key="key_fullZoomEnlarge"
+ label="&fullZoomEnlargeCmd.label;"
+ accesskey="&fullZoomEnlargeCmd.accesskey;"
+ command="cmd_fullZoomEnlarge"/>
+ <menuitem id="menu_zoomReduce"
+ key="key_fullZoomReduce"
+ label="&fullZoomReduceCmd.label;"
+ accesskey="&fullZoomReduceCmd.accesskey;"
+ command="cmd_fullZoomReduce"/>
+ <menuseparator/>
+ <menuitem id="menu_zoomReset"
+ key="key_fullZoomReset"
+ label="&fullZoomResetCmd.label;"
+ accesskey="&fullZoomResetCmd.accesskey;"
+ command="cmd_fullZoomReset"/>
+ <menuseparator/>
+ <menuitem id="toggle_zoom"
+ label="&fullZoomToggleCmd.label;"
+ accesskey="&fullZoomToggleCmd.accesskey;"
+ type="checkbox"
+ command="cmd_fullZoomToggle"
+ checked="false"/>
+ </menupopup>
+ </menu>
+ <menu id="pageStyleMenu" label="&pageStyleMenu.label;"
+ accesskey="&pageStyleMenu.accesskey;" observes="isImage">
+ <menupopup onpopupshowing="gPageStyleMenu.fillPopup(this);">
+ <menuitem id="menu_pageStyleNoStyle"
+ label="&pageStyleNoStyle.label;"
+ accesskey="&pageStyleNoStyle.accesskey;"
+ oncommand="gPageStyleMenu.disableStyle();"
+ type="radio"/>
+ <menuitem id="menu_pageStylePersistentOnly"
+ label="&pageStylePersistentOnly.label;"
+ accesskey="&pageStylePersistentOnly.accesskey;"
+ oncommand="gPageStyleMenu.switchStyleSheet('');"
+ type="radio"
+ checked="true"/>
+ <menuseparator/>
+ </menupopup>
+ </menu>
+#include browser-charsetmenu.inc
+ <menuseparator/>
+#ifdef XP_MACOSX
+ <menuitem id="enterFullScreenItem"
+ accesskey="&enterFullScreenCmd.accesskey;"
+ label="&enterFullScreenCmd.label;"
+ key="key_fullScreen">
+ <observes element="View:FullScreen" attribute="oncommand"/>
+ <observes element="View:FullScreen" attribute="disabled"/>
+ </menuitem>
+ <menuitem id="exitFullScreenItem"
+ accesskey="&exitFullScreenCmd.accesskey;"
+ label="&exitFullScreenCmd.label;"
+ key="key_fullScreen"
+ hidden="true">
+ <observes element="View:FullScreen" attribute="oncommand"/>
+ <observes element="View:FullScreen" attribute="disabled"/>
+ </menuitem>
+#else
+ <menuitem id="fullScreenItem"
+ accesskey="&fullScreenCmd.accesskey;"
+ label="&fullScreenCmd.label;"
+ key="key_fullScreen"
+ type="checkbox"
+ observes="View:FullScreen"/>
+#endif
+ <menuitem id="menu_readerModeItem"
+ observes="View:ReaderView"
+ hidden="true"/>
+ <menuitem id="menu_showAllTabs"
+ hidden="true"
+ accesskey="&showAllTabsCmd.accesskey;"
+ label="&showAllTabsCmd.label;"
+ command="Browser:ShowAllTabs"
+ key="key_showAllTabs"/>
+ <menuseparator hidden="true" id="documentDirection-separator"/>
+ <menuitem id="documentDirection-swap"
+ hidden="true"
+ label="&bidiSwitchPageDirectionItem.label;"
+ accesskey="&bidiSwitchPageDirectionItem.accesskey;"
+ oncommand="gBrowser.selectedBrowser
+ .messageManager
+ .sendAsyncMessage('SwitchDocumentDirection');"/>
+ </menupopup>
+ </menu>
+
+ <menu id="history-menu"
+ label="&historyMenu.label;"
+ accesskey="&historyMenu.accesskey;">
+ <menupopup id="goPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+#endif
+ oncommand="this.parentNode._placesView._onCommand(event);"
+ onclick="checkForMiddleClick(this, event);"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new HistoryMenu(event);"
+ tooltip="bhTooltip"
+ popupsinherittooltip="true">
+ <menuitem id="menu_showAllHistory"
+ label="&showAllHistoryCmd2.label;"
+#ifndef XP_MACOSX
+ key="showAllHistoryKb"
+#endif
+ command="Browser:ShowAllHistory"/>
+ <menuitem id="sanitizeItem"
+ label="&clearRecentHistory.label;"
+ key="key_sanitize"
+ command="Tools:Sanitize"/>
+ <menuseparator id="sanitizeSeparator"/>
+ <menuitem id="sync-tabs-menuitem"
+ class="syncTabsMenuItem"
+ label="&syncTabsMenu3.label;"
+ oncommand="BrowserOpenSyncTabs();"
+ hidden="true"/>
+ <menuitem id="historyRestoreLastSession"
+ label="&historyRestoreLastSession.label;"
+ command="Browser:RestoreLastSession"/>
+ <menu id="historyUndoMenu"
+ class="recentlyClosedTabsMenu"
+ label="&historyUndoMenu.label;"
+ disabled="true">
+ <menupopup id="historyUndoPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+#endif
+ onpopupshowing="document.getElementById('history-menu')._placesView.populateUndoSubmenu();"/>
+ </menu>
+ <menu id="historyUndoWindowMenu"
+ class="recentlyClosedWindowsMenu"
+ label="&historyUndoWindowMenu.label;"
+ disabled="true">
+ <menupopup id="historyUndoWindowPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+#endif
+ onpopupshowing="document.getElementById('history-menu')._placesView.populateUndoWindowSubmenu();"/>
+ </menu>
+ <menuseparator id="startHistorySeparator"
+ class="hide-if-empty-places-result"/>
+ </menupopup>
+ </menu>
+
+ <menu id="bookmarksMenu"
+ label="&bookmarksMenu.label;"
+ accesskey="&bookmarksMenu.accesskey;"
+ ondragenter="PlacesMenuDNDHandler.onDragEnter(event);"
+ ondragover="PlacesMenuDNDHandler.onDragOver(event);"
+ ondrop="PlacesMenuDNDHandler.onDrop(event);">
+ <menupopup id="bookmarksMenuPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+#endif
+ context="placesContext"
+ openInTabs="children"
+ oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);"
+ onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);"
+ onpopupshowing="BookmarkingUI.onMainMenuPopupShowing(event);
+ if (!this.parentNode._placesView)
+ new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');"
+ tooltip="bhTooltip" popupsinherittooltip="true">
+ <menuitem id="bookmarksShowAll"
+ label="&showAllBookmarks2.label;"
+ command="Browser:ShowAllBookmarks"
+ key="manBookmarkKb"/>
+ <menuseparator id="organizeBookmarksSeparator"/>
+ <menuitem id="menu_bookmarkThisPage"
+ command="Browser:AddBookmarkAs"
+ observes="bookmarkThisPageBroadcaster"
+ key="addBookmarkAsKb"/>
+ <menuitem id="subscribeToPageMenuitem"
+#ifndef XP_MACOSX
+ class="menuitem-iconic"
+#endif
+ label="&subscribeToPageMenuitem.label;"
+ oncommand="return FeedHandler.subscribeToFeed(null, event);"
+ onclick="checkForMiddleClick(this, event);"
+ observes="singleFeedMenuitemState"/>
+ <menu id="subscribeToPageMenupopup"
+#ifndef XP_MACOSX
+ class="menu-iconic"
+#endif
+ label="&subscribeToPageMenupopup.label;"
+ observes="multipleFeedsMenuState">
+ <menupopup id="subscribeToPageSubmenuMenupopup"
+ onpopupshowing="return FeedHandler.buildFeedList(event.target);"
+ oncommand="return FeedHandler.subscribeToFeed(null, event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ </menu>
+ <menuitem id="menu_bookmarkAllTabs"
+ label="&addCurPagesCmd.label;"
+ class="show-only-for-keyboard"
+ command="Browser:BookmarkAllTabs"
+ key="bookmarkAllTabsKb"/>
+ <menuseparator/>
+ <menuitem label="&recentBookmarks.label;"
+ id="menu_recentBookmarks"
+ disabled="true"/>
+ <menuseparator id="bookmarksToolbarSeparator"/>
+ <menu id="bookmarksToolbarFolderMenu"
+ class="menu-iconic bookmark-item"
+ label="&personalbarCmd.label;"
+ container="true">
+ <menupopup id="bookmarksToolbarFolderPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+#endif
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, 'place:folder=TOOLBAR');"/>
+ </menu>
+ <menu id="menu_unsortedBookmarks"
+ class="menu-iconic bookmark-item"
+ label="&otherBookmarksCmd.label;"
+ container="true">
+ <menupopup id="otherBookmarksFolderPopup"
+#ifndef XP_MACOSX
+ placespopup="true"
+#endif
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS');"/>
+ </menu>
+ <menuseparator id="bookmarksMenuItemsSeparator"/>
+ <!-- Bookmarks menu items -->
+ </menupopup>
+ </menu>
+
+ <menu id="tools-menu"
+ label="&toolsMenu.label;"
+ accesskey="&toolsMenu.accesskey;"
+ onpopupshowing="mirrorShow(this)">
+ <menupopup id="menu_ToolsPopup"
+# We have to use setTimeout() here to avoid a flickering menu bar when opening
+# the Tools menu, see bug 970769. This can be removed once we got rid of the
+# event loop spinning in Weave.Status._authManager.
+ onpopupshowing="setTimeout(() => gSyncUI.updateUI());"
+ >
+ <menuitem id="menu_openDownloads"
+ label="&downloads.label;"
+ accesskey="&downloads.accesskey;"
+ key="key_openDownloads"
+ command="Tools:Downloads"/>
+ <menuitem id="menu_openAddons"
+ label="&addons.label;"
+ accesskey="&addons.accesskey;"
+ key="key_openAddons"
+ command="Tools:Addons"/>
+
+ <!-- only one of sync-setup, sync-syncnowitem or sync-reauthitem will be showing at once -->
+ <menuitem id="sync-setup"
+ label="&syncSignIn.label;"
+ accesskey="&syncSignIn.accesskey;"
+ observes="sync-setup-state"
+ oncommand="gSyncUI.openSetup(null, 'menubar')"/>
+ <menuitem id="sync-syncnowitem"
+ label="&syncSyncNowItem.label;"
+ accesskey="&syncSyncNowItem.accesskey;"
+ observes="sync-syncnow-state"
+ oncommand="gSyncUI.doSync(event);"/>
+ <menuitem id="sync-reauthitem"
+ label="&syncReAuthItem.label;"
+ accesskey="&syncReAuthItem.accesskey;"
+ observes="sync-reauth-state"
+ oncommand="gSyncUI.openSignInAgainPage('menubar');"/>
+ <menuseparator id="devToolsSeparator"/>
+ <menu id="webDeveloperMenu"
+ label="&webDeveloperMenu.label;"
+ accesskey="&webDeveloperMenu.accesskey;">
+ <menupopup id="menuWebDeveloperPopup">
+ <menuitem id="menu_pageSource"
+ observes="devtoolsMenuBroadcaster_PageSource"
+ accesskey="&pageSourceCmd.accesskey;"/>
+ </menupopup>
+ </menu>
+ <menuitem id="menu_pageInfo"
+ accesskey="&pageInfoCmd.accesskey;"
+ label="&pageInfoCmd.label;"
+#ifndef XP_WIN
+ key="key_viewInfo"
+#endif
+ command="View:PageInfo"/>
+ <menu id="menu_mirrorTabCmd"
+ hidden="true"
+ accesskey="&mirrorTabCmd.accesskey;"
+ label="&mirrorTabCmd.label;">
+ <menupopup id="menu_mirrorTab-popup"
+ onpopupshowing="populateMirrorTabMenu(this)"/>
+ </menu>
+#ifndef XP_UNIX
+ <menuseparator id="prefSep"/>
+ <menuitem id="menu_preferences"
+ label="&preferencesCmd2.label;"
+ accesskey="&preferencesCmd2.accesskey;"
+ oncommand="openPreferences();"/>
+#endif
+ </menupopup>
+ </menu>
+
+#ifdef XP_MACOSX
+ <menu id="windowMenu" />
+#endif
+ <menu id="helpMenu" />
+ </menubar>
diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js
new file mode 100644
index 000000000..14e90cde2
--- /dev/null
+++ b/browser/base/content/browser-places.js
@@ -0,0 +1,2021 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 StarUI = {
+ _itemId: -1,
+ uri: null,
+ _batching: false,
+ _isNewBookmark: false,
+ _isComposing: false,
+ _autoCloseTimer: 0,
+ // The autoclose timer is diasbled if the user interacts with the
+ // popup, such as making a change through typing or clicking on
+ // the popup.
+ _autoCloseTimerEnabled: true,
+
+ _element: function(aID) {
+ return document.getElementById(aID);
+ },
+
+ // Edit-bookmark panel
+ get panel() {
+ delete this.panel;
+ var element = this._element("editBookmarkPanel");
+ // initially the panel is hidden
+ // to avoid impacting startup / new window performance
+ element.hidden = false;
+ element.addEventListener("keypress", this, false);
+ element.addEventListener("mousedown", this);
+ element.addEventListener("mouseout", this, false);
+ element.addEventListener("mousemove", this, false);
+ element.addEventListener("compositionstart", this, false);
+ element.addEventListener("compositionend", this, false);
+ element.addEventListener("input", this, false);
+ element.addEventListener("popuphidden", this, false);
+ element.addEventListener("popupshown", this, false);
+ return this.panel = element;
+ },
+
+ // Array of command elements to disable when the panel is opened.
+ get _blockedCommands() {
+ delete this._blockedCommands;
+ return this._blockedCommands =
+ ["cmd_close", "cmd_closeWindow"].map(id => this._element(id));
+ },
+
+ _blockCommands: function SU__blockCommands() {
+ this._blockedCommands.forEach(function (elt) {
+ // make sure not to permanently disable this item (see bug 409155)
+ if (elt.hasAttribute("wasDisabled"))
+ return;
+ if (elt.getAttribute("disabled") == "true") {
+ elt.setAttribute("wasDisabled", "true");
+ } else {
+ elt.setAttribute("wasDisabled", "false");
+ elt.setAttribute("disabled", "true");
+ }
+ });
+ },
+
+ _restoreCommandsState: function SU__restoreCommandsState() {
+ this._blockedCommands.forEach(function (elt) {
+ if (elt.getAttribute("wasDisabled") != "true")
+ elt.removeAttribute("disabled");
+ elt.removeAttribute("wasDisabled");
+ });
+ },
+
+ // nsIDOMEventListener
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "mousemove":
+ clearTimeout(this._autoCloseTimer);
+ // The autoclose timer is not disabled on generic mouseout
+ // because the user may not have actually interacted with the popup.
+ break;
+ case "popuphidden":
+ clearTimeout(this._autoCloseTimer);
+ if (aEvent.originalTarget == this.panel) {
+ if (!this._element("editBookmarkPanelContent").hidden)
+ this.quitEditMode();
+
+ if (this._anchorToolbarButton) {
+ this._anchorToolbarButton.removeAttribute("open");
+ this._anchorToolbarButton = null;
+ }
+ this._restoreCommandsState();
+ this._itemId = -1;
+ if (this._batching)
+ this.endBatch();
+
+ if (this._uriForRemoval) {
+ if (this._isNewBookmark) {
+ if (!PlacesUtils.useAsyncTransactions) {
+ PlacesUtils.transactionManager.undoTransaction();
+ break;
+ }
+ PlacesTransactions().undo().catch(Cu.reportError);
+ break;
+ }
+ // Remove all bookmarks for the bookmark's url, this also removes
+ // the tags for the url.
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
+ for (let itemId of itemIds) {
+ let txn = new PlacesRemoveItemTransaction(itemId);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ break;
+ }
+
+ PlacesTransactions.RemoveBookmarksForUrls([this._uriForRemoval])
+ .transact().catch(Cu.reportError);
+ }
+ }
+ break;
+ case "keypress":
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimerEnabled = false;
+
+ if (aEvent.defaultPrevented) {
+ // The event has already been consumed inside of the panel.
+ break;
+ }
+
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.panel.hidePopup();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ if (aEvent.target.classList.contains("expander-up") ||
+ aEvent.target.classList.contains("expander-down") ||
+ aEvent.target.id == "editBMPanel_newFolderButton" ||
+ aEvent.target.id == "editBookmarkPanelRemoveButton") {
+ // XXX Why is this necessary? The defaultPrevented check should
+ // be enough.
+ break;
+ }
+ this.panel.hidePopup();
+ break;
+ // This case is for catching character-generating keypresses
+ case 0:
+ let accessKey = document.getElementById("key_close");
+ if (eventMatchesKey(aEvent, accessKey)) {
+ this.panel.hidePopup();
+ }
+ break;
+ }
+ break;
+ case "compositionend":
+ // After composition is committed, "mouseout" or something can set
+ // auto close timer.
+ this._isComposing = false;
+ break;
+ case "compositionstart":
+ if (aEvent.defaultPrevented) {
+ // If the composition was canceled, nothing to do here.
+ break;
+ }
+ this._isComposing = true;
+ // Explicit fall-through, during composition, panel shouldn't be
+ // hidden automatically.
+ case "input":
+ // Might have edited some text without keyboard events nor composition
+ // events. Fall-through to cancel auto close in such case.
+ case "mousedown":
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimerEnabled = false;
+ break;
+ case "mouseout":
+ if (!this._autoCloseTimerEnabled) {
+ // Don't autoclose the popup if the user has made a selection
+ // or keypress and then subsequently mouseout.
+ break;
+ }
+ // Explicit fall-through
+ case "popupshown":
+ // Don't handle events for descendent elements.
+ if (aEvent.target != aEvent.currentTarget) {
+ break;
+ }
+ // auto-close if new and not interacted with
+ if (this._isNewBookmark && !this._isComposing) {
+ // 3500ms matches the timeout that Pocket uses in
+ // browser/extensions/pocket/content/panels/js/saved.js
+ let delay = 3500;
+ if (this._closePanelQuickForTesting) {
+ delay /= 10;
+ }
+ clearTimeout(this._autoCloseTimer);
+ this._autoCloseTimer = setTimeout(() => {
+ if (!this.panel.mozMatchesSelector(":hover")) {
+ this.panel.hidePopup();
+ }
+ }, delay);
+ this._autoCloseTimerEnabled = true;
+ }
+ break;
+ }
+ },
+
+ _overlayLoaded: false,
+ _overlayLoading: false,
+ showEditBookmarkPopup: Task.async(function* (aNode, aAnchorElement, aPosition, aIsNewBookmark) {
+ // Slow double-clicks (not true double-clicks) shouldn't
+ // cause the panel to flicker.
+ if (this.panel.state == "showing" ||
+ this.panel.state == "open") {
+ return;
+ }
+
+ this._isNewBookmark = aIsNewBookmark;
+ this._uriForRemoval = "";
+ // TODO: Deprecate this once async transactions are enabled and the legacy
+ // transactions code is gone (bug 1131491) - we don't want addons to to use
+ // the completeNodeLikeObjectForItemId, so it's better if they keep passing
+ // the item-id for now).
+ if (typeof(aNode) == "number") {
+ let itemId = aNode;
+ if (PlacesUIUtils.useAsyncTransactions) {
+ let guid = yield PlacesUtils.promiseItemGuid(itemId);
+ aNode = yield PlacesUIUtils.promiseNodeLike(guid);
+ }
+ else {
+ aNode = { itemId };
+ yield PlacesUIUtils.completeNodeLikeObjectForItemId(aNode);
+ }
+ }
+
+ // Performance: load the overlay the first time the panel is opened
+ // (see bug 392443).
+ if (this._overlayLoading)
+ return;
+
+ if (this._overlayLoaded) {
+ this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
+ return;
+ }
+
+ this._overlayLoading = true;
+ document.loadOverlay(
+ "chrome://browser/content/places/editBookmarkOverlay.xul",
+ (function (aSubject, aTopic, aData) {
+ // Move the header (star, title, button) into the grid,
+ // so that it aligns nicely with the other items (bug 484022).
+ let header = this._element("editBookmarkPanelHeader");
+ let rows = this._element("editBookmarkPanelGrid").lastChild;
+ rows.insertBefore(header, rows.firstChild);
+ header.hidden = false;
+
+ this._overlayLoading = false;
+ this._overlayLoaded = true;
+ this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
+ }).bind(this)
+ );
+ }),
+
+ _doShowEditBookmarkPanel: Task.async(function* (aNode, aAnchorElement, aPosition) {
+ if (this.panel.state != "closed")
+ return;
+
+ this._blockCommands(); // un-done in the popuphidden handler
+
+ this._element("editBookmarkPanelTitle").value =
+ this._isNewBookmark ?
+ gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
+ gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle");
+
+ // No description; show the Done, Remove;
+ this._element("editBookmarkPanelDescription").textContent = "";
+ this._element("editBookmarkPanelBottomButtons").hidden = false;
+ this._element("editBookmarkPanelContent").hidden = false;
+
+ // The label of the remove button differs if the URI is bookmarked
+ // multiple times.
+ let bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
+ let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
+ let label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
+ this._element("editBookmarkPanelRemoveButton").label = label;
+
+ // unset the unstarred state, if set
+ this._element("editBookmarkPanelStarIcon").removeAttribute("unstarred");
+
+ this._itemId = aNode.itemId;
+ this.beginBatch();
+
+ if (aAnchorElement) {
+ // Set the open=true attribute if the anchor is a
+ // descendent of a toolbarbutton.
+ let parent = aAnchorElement.parentNode;
+ while (parent) {
+ if (parent.localName == "toolbarbutton") {
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (parent) {
+ this._anchorToolbarButton = parent;
+ parent.setAttribute("open", "true");
+ }
+ }
+ let panel = this.panel;
+ let target = panel;
+ if (target.parentNode) {
+ // By targeting the panel's parent and using a capturing listener, we
+ // can have our listener called before others waiting for the panel to
+ // be shown (which probably expect the panel to be fully initialized)
+ target = target.parentNode;
+ }
+ target.addEventListener("popupshown", function shownListener(event) {
+ if (event.target == panel) {
+ target.removeEventListener("popupshown", shownListener, true);
+
+ gEditItemOverlay.initPanel({ node: aNode
+ , hiddenRows: ["description", "location",
+ "loadInSidebar", "keyword"]
+ , focusedElement: "preferred"});
+ }
+ }, true);
+
+ this.panel.openPopup(aAnchorElement, aPosition);
+ }),
+
+ panelShown:
+ function SU_panelShown(aEvent) {
+ if (aEvent.target == this.panel) {
+ if (this._element("editBookmarkPanelContent").hidden) {
+ // Note this isn't actually used anymore, we should remove this
+ // once we decide not to bring back the page bookmarked notification
+ this.panel.focus();
+ }
+ }
+ },
+
+ quitEditMode: function SU_quitEditMode() {
+ this._element("editBookmarkPanelContent").hidden = true;
+ this._element("editBookmarkPanelBottomButtons").hidden = true;
+ gEditItemOverlay.uninitPanel(true);
+ },
+
+ removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
+ this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
+ this.panel.hidePopup();
+ },
+
+ // Matching the way it is used in the Library, editBookmarkOverlay implements
+ // an instant-apply UI, having no batched-Undo/Redo support.
+ // However, in this context (the Star UI) we have a Cancel button whose
+ // expected behavior is to undo all the operations done in the panel.
+ // Sometime in the future this needs to be reimplemented using a
+ // non-instant apply code path, but for the time being, we patch-around
+ // editBookmarkOverlay so that all of the actions done in the panel
+ // are treated by PlacesTransactions as a single batch. To do so,
+ // we start a PlacesTransactions batch when the star UI panel is shown, and
+ // we keep the batch ongoing until the panel is hidden.
+ _batchBlockingDeferred: null,
+ beginBatch() {
+ if (this._batching)
+ return;
+ if (PlacesUIUtils.useAsyncTransactions) {
+ this._batchBlockingDeferred = PromiseUtils.defer();
+ PlacesTransactions.batch(function* () {
+ yield this._batchBlockingDeferred.promise;
+ }.bind(this));
+ }
+ else {
+ PlacesUtils.transactionManager.beginBatch(null);
+ }
+ this._batching = true;
+ },
+
+ endBatch() {
+ if (!this._batching)
+ return;
+
+ if (PlacesUIUtils.useAsyncTransactions) {
+ this._batchBlockingDeferred.resolve();
+ this._batchBlockingDeferred = null;
+ }
+ else {
+ PlacesUtils.transactionManager.endBatch(false);
+ }
+ this._batching = false;
+ }
+};
+
+var PlacesCommandHook = {
+ /**
+ * Adds a bookmark to the page loaded in the given browser.
+ *
+ * @param aBrowser
+ * a <browser> element.
+ * @param [optional] aParent
+ * The folder in which to create a new bookmark if the page loaded in
+ * aBrowser isn't bookmarked yet, defaults to the unfiled root.
+ * @param [optional] aShowEditUI
+ * whether or not to show the edit-bookmark UI for the bookmark item
+ */
+ bookmarkPage: Task.async(function* (aBrowser, aParent, aShowEditUI) {
+ if (PlacesUIUtils.useAsyncTransactions) {
+ yield this._bookmarkPagePT(aBrowser, aParent, aShowEditUI);
+ return;
+ }
+
+ var uri = aBrowser.currentURI;
+ var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
+ let isNewBookmark = itemId == -1;
+ if (isNewBookmark) {
+ // Bug 1148838 - Make this code work for full page plugins.
+ var title;
+ var description;
+ var charset;
+
+ let docInfo = yield this._getPageDetails(aBrowser);
+
+ try {
+ title = docInfo.isErrorPage ? PlacesUtils.history.getPageTitle(uri)
+ : aBrowser.contentTitle;
+ title = title || uri.spec;
+ description = docInfo.description;
+ charset = aBrowser.characterSet;
+ }
+ catch (e) { }
+
+ if (aShowEditUI && isNewBookmark) {
+ // If we bookmark the page here but open right into a cancelable
+ // state (i.e. new bookmark in Library), start batching here so
+ // all of the actions can be undone in a single undo step.
+ StarUI.beginBatch();
+ }
+
+ var parent = aParent !== undefined ?
+ aParent : PlacesUtils.unfiledBookmarksFolderId;
+ var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
+ var txn = new PlacesCreateBookmarkTransaction(uri, parent,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title, null, [descAnno]);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ itemId = txn.item.id;
+ // Set the character-set.
+ if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+ PlacesUtils.setCharsetForURI(uri, charset);
+ }
+
+ // Revert the contents of the location bar
+ gURLBar.handleRevert();
+
+ // If it was not requested to open directly in "edit" mode, we are done.
+ if (!aShowEditUI)
+ return;
+
+ // Try to dock the panel to:
+ // 1. the bookmarks menu button
+ // 2. the identity icon
+ // 3. the content area
+ if (BookmarkingUI.anchor) {
+ StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor,
+ "bottomcenter topright", isNewBookmark);
+ return;
+ }
+
+ let identityIcon = document.getElementById("identity-icon");
+ if (isElementVisible(identityIcon)) {
+ StarUI.showEditBookmarkPopup(itemId, identityIcon,
+ "bottomcenter topright", isNewBookmark);
+ } else {
+ StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap", isNewBookmark);
+ }
+ }),
+
+ // TODO: Replace bookmarkPage code with this function once legacy
+ // transactions are removed.
+ _bookmarkPagePT: Task.async(function* (aBrowser, aParentId, aShowEditUI) {
+ let url = new URL(aBrowser.currentURI.spec);
+ let info = yield PlacesUtils.bookmarks.fetch({ url });
+ let isNewBookmark = !info;
+ if (!info) {
+ let parentGuid = aParentId !== undefined ?
+ yield PlacesUtils.promiseItemGuid(aParentId) :
+ PlacesUtils.bookmarks.unfiledGuid;
+ info = { url, parentGuid };
+ // Bug 1148838 - Make this code work for full page plugins.
+ let description = null;
+ let charset = null;
+
+ let docInfo = yield this._getPageDetails(aBrowser);
+
+ try {
+ info.title = docInfo.isErrorPage ?
+ (yield PlacesUtils.promisePlaceInfo(aBrowser.currentURI)).title :
+ aBrowser.contentTitle;
+ info.title = info.title || url.href;
+ description = docInfo.description;
+ charset = aBrowser.characterSet;
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ if (aShowEditUI && isNewBookmark) {
+ // If we bookmark the page here but open right into a cancelable
+ // state (i.e. new bookmark in Library), start batching here so
+ // all of the actions can be undone in a single undo step.
+ StarUI.beginBatch();
+ }
+
+ if (description) {
+ info.annotations = [{ name: PlacesUIUtils.DESCRIPTION_ANNO
+ , value: description }];
+ }
+
+ info.guid = yield PlacesTransactions.NewBookmark(info).transact();
+
+ // Set the character-set
+ if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+ PlacesUtils.setCharsetForURI(makeURI(url.href), charset);
+ }
+
+ // Revert the contents of the location bar
+ gURLBar.handleRevert();
+
+ // If it was not requested to open directly in "edit" mode, we are done.
+ if (!aShowEditUI)
+ return;
+
+ let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
+
+ // Try to dock the panel to:
+ // 1. the bookmarks menu button
+ // 2. the identity icon
+ // 3. the content area
+ if (BookmarkingUI.anchor) {
+ StarUI.showEditBookmarkPopup(node, BookmarkingUI.anchor,
+ "bottomcenter topright", isNewBookmark);
+ return;
+ }
+
+ let identityIcon = document.getElementById("identity-icon");
+ if (isElementVisible(identityIcon)) {
+ StarUI.showEditBookmarkPopup(node, identityIcon,
+ "bottomcenter topright", isNewBookmark);
+ } else {
+ StarUI.showEditBookmarkPopup(node, aBrowser, "overlap", isNewBookmark);
+ }
+ }),
+
+ _getPageDetails(browser) {
+ return new Promise(resolve => {
+ let mm = browser.messageManager;
+ mm.addMessageListener("Bookmarks:GetPageDetails:Result", function listener(msg) {
+ mm.removeMessageListener("Bookmarks:GetPageDetails:Result", listener);
+ resolve(msg.data);
+ });
+
+ mm.sendAsyncMessage("Bookmarks:GetPageDetails", { })
+ });
+ },
+
+ /**
+ * Adds a bookmark to the page loaded in the current tab.
+ */
+ bookmarkCurrentPage: function PCH_bookmarkCurrentPage(aShowEditUI, aParent) {
+ this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI);
+ },
+
+ /**
+ * Adds a bookmark to the page targeted by a link.
+ * @param aParent
+ * The folder in which to create a new bookmark if aURL isn't
+ * bookmarked.
+ * @param aURL (string)
+ * the address of the link target
+ * @param aTitle
+ * The link text
+ * @param [optional] aDescription
+ * The linked page description, if available
+ */
+ bookmarkLink: Task.async(function* (aParentId, aURL, aTitle, aDescription="") {
+ let node = yield PlacesUIUtils.fetchNodeLike({ url: aURL });
+ if (node) {
+ PlacesUIUtils.showBookmarkDialog({ action: "edit"
+ , node
+ }, window.top);
+ return;
+ }
+
+ let ip = new InsertionPoint(aParentId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "bookmark"
+ , uri: makeURI(aURL)
+ , title: aTitle
+ , description: aDescription
+ , defaultInsertionPoint: ip
+ , hiddenRows: [ "description"
+ , "location"
+ , "loadInSidebar"
+ , "keyword" ]
+ }, window.top);
+ }),
+
+ /**
+ * List of nsIURI objects characterizing the tabs currently open in the
+ * browser, modulo pinned tabs. The URIs will be in the order in which their
+ * corresponding tabs appeared and duplicates are discarded.
+ */
+ get uniqueCurrentPages() {
+ let uniquePages = {};
+ let URIs = [];
+
+ gBrowser.visibleTabs.forEach(tab => {
+ let browser = tab.linkedBrowser;
+ let uri = browser.currentURI;
+ let title = browser.contentTitle || tab.label;
+ let spec = uri.spec;
+ if (!tab.pinned && !(spec in uniquePages)) {
+ uniquePages[spec] = null;
+ URIs.push({ uri, title });
+ }
+ });
+ return URIs;
+ },
+
+ /**
+ * Adds a folder with bookmarks to all of the currently open tabs in this
+ * window.
+ */
+ bookmarkCurrentPages: function PCH_bookmarkCurrentPages() {
+ let pages = this.uniqueCurrentPages;
+ if (pages.length > 1) {
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "folder"
+ , URIList: pages
+ , hiddenRows: [ "description" ]
+ }, window);
+ }
+ },
+
+ /**
+ * Updates disabled state for the "Bookmark All Tabs" command.
+ */
+ updateBookmarkAllTabsCommand:
+ function PCH_updateBookmarkAllTabsCommand() {
+ // There's nothing to do in non-browser windows.
+ if (window.location.href != getBrowserURL())
+ return;
+
+ // Disable "Bookmark All Tabs" if there are less than two
+ // "unique current pages".
+ goSetCommandEnabled("Browser:BookmarkAllTabs",
+ this.uniqueCurrentPages.length >= 2);
+ },
+
+ /**
+ * Adds a Live Bookmark to a feed associated with the current page.
+ * @param url
+ * The nsIURI of the page the feed was attached to
+ * @title title
+ * The title of the feed. Optional.
+ * @subtitle subtitle
+ * A short description of the feed. Optional.
+ */
+ addLiveBookmark: Task.async(function *(url, feedTitle, feedSubtitle) {
+ let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+
+ let feedURI = makeURI(url);
+ let title = feedTitle || gBrowser.contentTitle;
+ let description = feedSubtitle;
+ if (!description) {
+ description = (yield this._getPageDetails(gBrowser.selectedBrowser)).description;
+ }
+
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "livemark"
+ , feedURI: feedURI
+ , siteURI: gBrowser.currentURI
+ , title: title
+ , description: description
+ , defaultInsertionPoint: toolbarIP
+ , hiddenRows: [ "feedLocation"
+ , "siteLocation"
+ , "description" ]
+ }, window);
+ }),
+
+ /**
+ * Opens the Places Organizer.
+ * @param aLeftPaneRoot
+ * The query to select in the organizer window - options
+ * are: History, AllBookmarks, BookmarksMenu, BookmarksToolbar,
+ * UnfiledBookmarks, Tags and Downloads.
+ */
+ showPlacesOrganizer: function PCH_showPlacesOrganizer(aLeftPaneRoot) {
+ var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
+ // Due to bug 528706, getMostRecentWindow can return closed windows.
+ if (!organizer || organizer.closed) {
+ // No currently open places window, so open one with the specified mode.
+ openDialog("chrome://browser/content/places/places.xul",
+ "", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot);
+ }
+ else {
+ organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
+ organizer.focus();
+ }
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils",
+ "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm");
+
+// View for the history menu.
+function HistoryMenu(aPopupShowingEvent) {
+ // Workaround for Bug 610187. The sidebar does not include all the Places
+ // views definitions, and we don't need them there.
+ // Defining the prototype inheritance in the prototype itself would cause
+ // browser.js to halt on "PlacesMenu is not defined" error.
+ this.__proto__.__proto__ = PlacesMenu.prototype;
+ PlacesMenu.call(this, aPopupShowingEvent,
+ "place:sort=4&maxResults=15");
+}
+
+HistoryMenu.prototype = {
+ _getClosedTabCount() {
+ // SessionStore doesn't track the hidden window, so just return zero then.
+ if (window == Services.appShell.hiddenDOMWindow) {
+ return 0;
+ }
+
+ return SessionStore.getClosedTabCount(window);
+ },
+
+ toggleRecentlyClosedTabs: function HM_toggleRecentlyClosedTabs() {
+ // enable/disable the Recently Closed Tabs sub menu
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0];
+
+ // no restorable tabs, so disable menu
+ if (this._getClosedTabCount() == 0)
+ undoMenu.setAttribute("disabled", true);
+ else
+ undoMenu.removeAttribute("disabled");
+ },
+
+ /**
+ * Populate when the history menu is opened
+ */
+ populateUndoSubmenu: function PHM_populateUndoSubmenu() {
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0];
+ var undoPopup = undoMenu.firstChild;
+
+ // remove existing menu items
+ while (undoPopup.hasChildNodes())
+ undoPopup.removeChild(undoPopup.firstChild);
+
+ // no restorable tabs, so make sure menu is disabled, and return
+ if (this._getClosedTabCount() == 0) {
+ undoMenu.setAttribute("disabled", true);
+ return;
+ }
+
+ // enable menu
+ undoMenu.removeAttribute("disabled");
+
+ // populate menu
+ let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(window, "menuitem");
+ undoPopup.appendChild(tabsFragment);
+ },
+
+ toggleRecentlyClosedWindows: function PHM_toggleRecentlyClosedWindows() {
+ // enable/disable the Recently Closed Windows sub menu
+ var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0];
+
+ // no restorable windows, so disable menu
+ if (SessionStore.getClosedWindowCount() == 0)
+ undoMenu.setAttribute("disabled", true);
+ else
+ undoMenu.removeAttribute("disabled");
+ },
+
+ /**
+ * Populate when the history menu is opened
+ */
+ populateUndoWindowSubmenu: function PHM_populateUndoWindowSubmenu() {
+ let undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0];
+ let undoPopup = undoMenu.firstChild;
+
+ // remove existing menu items
+ while (undoPopup.hasChildNodes())
+ undoPopup.removeChild(undoPopup.firstChild);
+
+ // no restorable windows, so make sure menu is disabled, and return
+ if (SessionStore.getClosedWindowCount() == 0) {
+ undoMenu.setAttribute("disabled", true);
+ return;
+ }
+
+ // enable menu
+ undoMenu.removeAttribute("disabled");
+
+ // populate menu
+ let windowsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(window, "menuitem");
+ undoPopup.appendChild(windowsFragment);
+ },
+
+ toggleTabsFromOtherComputers: function PHM_toggleTabsFromOtherComputers() {
+ // Enable/disable the Tabs From Other Computers menu. Some of the menus handled
+ // by HistoryMenu do not have this menuitem.
+ let menuitem = this._rootElt.getElementsByClassName("syncTabsMenuItem")[0];
+ if (!menuitem)
+ return;
+
+ if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) {
+ menuitem.setAttribute("hidden", true);
+ return;
+ }
+
+ menuitem.setAttribute("hidden", false);
+ },
+
+ _onPopupShowing: function HM__onPopupShowing(aEvent) {
+ PlacesMenu.prototype._onPopupShowing.apply(this, arguments);
+
+ // Don't handle events for submenus.
+ if (aEvent.target != aEvent.currentTarget)
+ return;
+
+ this.toggleRecentlyClosedTabs();
+ this.toggleRecentlyClosedWindows();
+ this.toggleTabsFromOtherComputers();
+ },
+
+ _onCommand: function HM__onCommand(aEvent) {
+ let placesNode = aEvent.target._placesNode;
+ if (placesNode) {
+ if (!PrivateBrowsingUtils.isWindowPrivate(window))
+ PlacesUIUtils.markPageAsTyped(placesNode.uri);
+ openUILink(placesNode.uri, aEvent, { ignoreAlt: true });
+ }
+ }
+};
+
+/**
+ * Functions for handling events in the Bookmarks Toolbar and menu.
+ */
+var BookmarksEventHandler = {
+ /**
+ * Handler for click event for an item in the bookmarks toolbar or menu.
+ * Menus and submenus from the folder buttons bubble up to this handler.
+ * Left-click is handled in the onCommand function.
+ * When items are middle-clicked (or clicked with modifier), open in tabs.
+ * If the click came through a menu, close the menu.
+ * @param aEvent
+ * DOMEvent for the click
+ * @param aView
+ * The places view which aEvent should be associated with.
+ */
+ onClick: function BEH_onClick(aEvent, aView) {
+ // Only handle middle-click or left-click with modifiers.
+ let modifKey;
+ if (AppConstants.platform == "macosx") {
+ modifKey = aEvent.metaKey || aEvent.shiftKey;
+ } else {
+ modifKey = aEvent.ctrlKey || aEvent.shiftKey;
+ }
+
+ if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey))
+ return;
+
+ var target = aEvent.originalTarget;
+ // If this event bubbled up from a menu or menuitem, close the menus.
+ // Do this before opening tabs, to avoid hiding the open tabs confirm-dialog.
+ if (target.localName == "menu" || target.localName == "menuitem") {
+ for (let node = target.parentNode; node; node = node.parentNode) {
+ if (node.localName == "menupopup")
+ node.hidePopup();
+ else if (node.localName != "menu" &&
+ node.localName != "splitmenu" &&
+ node.localName != "hbox" &&
+ node.localName != "vbox" )
+ break;
+ }
+ }
+
+ if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) {
+ // Don't open the root folder in tabs when the empty area on the toolbar
+ // is middle-clicked or when a non-bookmark item except for Open in Tabs)
+ // in a bookmarks menupopup is middle-clicked.
+ if (target.localName == "menu" || target.localName == "toolbarbutton")
+ PlacesUIUtils.openContainerNodeInTabs(target._placesNode, aEvent, aView);
+ }
+ else if (aEvent.button == 1) {
+ // left-clicks with modifier are already served by onCommand
+ this.onCommand(aEvent, aView);
+ }
+ },
+
+ /**
+ * Handler for command event for an item in the bookmarks toolbar.
+ * Menus and submenus from the folder buttons bubble up to this handler.
+ * Opens the item.
+ * @param aEvent
+ * DOMEvent for the command
+ * @param aView
+ * The places view which aEvent should be associated with.
+ */
+ onCommand: function BEH_onCommand(aEvent, aView) {
+ var target = aEvent.originalTarget;
+ if (target._placesNode)
+ PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView);
+ },
+
+ fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) {
+ var node;
+ var cropped = false;
+ var targetURI;
+
+ if (aDocument.tooltipNode.localName == "treechildren") {
+ var tree = aDocument.tooltipNode.parentNode;
+ var tbo = tree.treeBoxObject;
+ var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.row == -1)
+ return false;
+ node = tree.view.nodeForTreeIndex(cell.row);
+ cropped = tbo.isCellCropped(cell.row, cell.col);
+ }
+ else {
+ // Check whether the tooltipNode is a Places node.
+ // In such a case use it, otherwise check for targetURI attribute.
+ var tooltipNode = aDocument.tooltipNode;
+ if (tooltipNode._placesNode)
+ node = tooltipNode._placesNode;
+ else {
+ // This is a static non-Places node.
+ targetURI = tooltipNode.getAttribute("targetURI");
+ }
+ }
+
+ if (!node && !targetURI)
+ return false;
+
+ // Show node.label as tooltip's title for non-Places nodes.
+ var title = node ? node.title : tooltipNode.label;
+
+ // Show URL only for Places URI-nodes or nodes with a targetURI attribute.
+ var url;
+ if (targetURI || PlacesUtils.nodeIsURI(node))
+ url = targetURI || node.uri;
+
+ // Show tooltip for containers only if their title is cropped.
+ if (!cropped && !url)
+ return false;
+
+ var tooltipTitle = aDocument.getElementById("bhtTitleText");
+ tooltipTitle.hidden = (!title || (title == url));
+ if (!tooltipTitle.hidden)
+ tooltipTitle.textContent = title;
+
+ var tooltipUrl = aDocument.getElementById("bhtUrlText");
+ tooltipUrl.hidden = !url;
+ if (!tooltipUrl.hidden)
+ tooltipUrl.value = url;
+
+ // Show tooltip.
+ return true;
+ }
+};
+
+// Handles special drag and drop functionality for Places menus that are not
+// part of a Places view (e.g. the bookmarks menu in the menubar).
+var PlacesMenuDNDHandler = {
+ _springLoadDelayMs: 350,
+ _closeDelayMs: 500,
+ _loadTimer: null,
+ _closeTimer: null,
+ _closingTimerNode: null,
+
+ /**
+ * Called when the user enters the <menu> element during a drag.
+ * @param event
+ * The DragEnter event that spawned the opening.
+ */
+ onDragEnter: function PMDH_onDragEnter(event) {
+ // Opening menus in a Places popup is handled by the view itself.
+ if (!this._isStaticContainer(event.target))
+ return;
+
+ // If we re-enter the same menu or anchor before the close timer runs out,
+ // we should ensure that we do not close:
+ if (this._closeTimer && this._closingTimerNode === event.currentTarget) {
+ this._closeTimer.cancel();
+ this._closingTimerNode = null;
+ this._closeTimer = null;
+ }
+
+ PlacesControllerDragHelper.currentDropTarget = event.target;
+ let popup = event.target.lastChild;
+ if (this._loadTimer || popup.state === "showing" || popup.state === "open")
+ return;
+
+ this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._loadTimer.initWithCallback(() => {
+ this._loadTimer = null;
+ popup.setAttribute("autoopened", "true");
+ popup.showPopup(popup);
+ }, this._springLoadDelayMs, Ci.nsITimer.TYPE_ONE_SHOT);
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Handles dragleave on the <menu> element.
+ */
+ onDragLeave: function PMDH_onDragLeave(event) {
+ // Handle menu-button separate targets.
+ if (event.relatedTarget === event.currentTarget ||
+ (event.relatedTarget &&
+ event.relatedTarget.parentNode === event.currentTarget))
+ return;
+
+ // Closing menus in a Places popup is handled by the view itself.
+ if (!this._isStaticContainer(event.target))
+ return;
+
+ PlacesControllerDragHelper.currentDropTarget = null;
+ let popup = event.target.lastChild;
+
+ if (this._loadTimer) {
+ this._loadTimer.cancel();
+ this._loadTimer = null;
+ }
+ this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._closingTimerNode = event.currentTarget;
+ this._closeTimer.initWithCallback(function() {
+ this._closeTimer = null;
+ this._closingTimerNode = null;
+ let node = PlacesControllerDragHelper.currentDropTarget;
+ let inHierarchy = false;
+ while (node && !inHierarchy) {
+ inHierarchy = node == event.target;
+ node = node.parentNode;
+ }
+ if (!inHierarchy && popup && popup.hasAttribute("autoopened")) {
+ popup.removeAttribute("autoopened");
+ popup.hidePopup();
+ }
+ }, this._closeDelayMs, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Determines if a XUL element represents a static container.
+ * @returns true if the element is a container element (menu or
+ *` menu-toolbarbutton), false otherwise.
+ */
+ _isStaticContainer: function PMDH__isContainer(node) {
+ let isMenu = node.localName == "menu" ||
+ (node.localName == "toolbarbutton" &&
+ (node.getAttribute("type") == "menu" ||
+ node.getAttribute("type") == "menu-button"));
+ let isStatic = !("_placesNode" in node) && node.lastChild &&
+ node.lastChild.hasAttribute("placespopup") &&
+ !node.parentNode.hasAttribute("placespopup");
+ return isMenu && isStatic;
+ },
+
+ /**
+ * Called when the user drags over the <menu> element.
+ * @param event
+ * The DragOver event.
+ */
+ onDragOver: function PMDH_onDragOver(event) {
+ let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer))
+ event.preventDefault();
+
+ event.stopPropagation();
+ },
+
+ /**
+ * Called when the user drops on the <menu> element.
+ * @param event
+ * The Drop event.
+ */
+ onDrop: function PMDH_onDrop(event) {
+ // Put the item at the end of bookmark menu.
+ let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ Components.interfaces.nsITreeView.DROP_ON);
+ PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
+ PlacesControllerDragHelper.currentDropTarget = null;
+ event.stopPropagation();
+ }
+};
+
+/**
+ * This object handles the initialization and uninitialization of the bookmarks
+ * toolbar.
+ */
+var PlacesToolbarHelper = {
+ _place: "place:folder=TOOLBAR",
+
+ get _viewElt() {
+ return document.getElementById("PlacesToolbar");
+ },
+
+ get _placeholder() {
+ return document.getElementById("bookmarks-toolbar-placeholder");
+ },
+
+ init: function PTH_init(forceToolbarOverflowCheck) {
+ let viewElt = this._viewElt;
+ if (!viewElt || viewElt._placesView)
+ return;
+
+ // CustomizableUI.addListener is idempotent, so we can safely
+ // call this multiple times.
+ CustomizableUI.addListener(this);
+
+ // If the bookmarks toolbar item is:
+ // - not in a toolbar, or;
+ // - the toolbar is collapsed, or;
+ // - the toolbar is hidden some other way:
+ // don't initialize. Also, there is no need to initialize the toolbar if
+ // customizing, because that will happen when the customization is done.
+ let toolbar = this._getParentToolbar(viewElt);
+ if (!toolbar || toolbar.collapsed || this._isCustomizing ||
+ getComputedStyle(toolbar, "").display == "none")
+ return;
+
+ new PlacesToolbar(this._place);
+ if (forceToolbarOverflowCheck) {
+ viewElt._placesView.updateOverflowStatus();
+ }
+ this._shouldWrap = false;
+ this._setupPlaceholder();
+ },
+
+ uninit: function PTH_uninit() {
+ CustomizableUI.removeListener(this);
+ },
+
+ customizeStart: function PTH_customizeStart() {
+ try {
+ let viewElt = this._viewElt;
+ if (viewElt && viewElt._placesView)
+ viewElt._placesView.uninit();
+ } finally {
+ this._isCustomizing = true;
+ }
+ this._shouldWrap = this._getShouldWrap();
+ },
+
+ customizeChange: function PTH_customizeChange() {
+ this._setupPlaceholder();
+ },
+
+ _setupPlaceholder: function PTH_setupPlaceholder() {
+ let placeholder = this._placeholder;
+ if (!placeholder) {
+ return;
+ }
+
+ let shouldWrapNow = this._getShouldWrap();
+ if (this._shouldWrap != shouldWrapNow) {
+ if (shouldWrapNow) {
+ placeholder.setAttribute("wrap", "true");
+ } else {
+ placeholder.removeAttribute("wrap");
+ }
+ this._shouldWrap = shouldWrapNow;
+ }
+ },
+
+ customizeDone: function PTH_customizeDone() {
+ this._isCustomizing = false;
+ this.init(true);
+ },
+
+ _getShouldWrap: function PTH_getShouldWrap() {
+ let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
+ let area = placement && placement.area;
+ let areaType = area && CustomizableUI.getAreaType(area);
+ return !area || CustomizableUI.TYPE_MENU_PANEL == areaType;
+ },
+
+ onPlaceholderCommand: function () {
+ let widgetGroup = CustomizableUI.getWidget("personal-bookmarks");
+ let widget = widgetGroup.forWindow(window);
+ if (widget.overflowed ||
+ widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
+ PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar");
+ }
+ },
+
+ _getParentToolbar: function(element) {
+ while (element) {
+ if (element.localName == "toolbar") {
+ return element;
+ }
+ element = element.parentNode;
+ }
+ return null;
+ },
+
+ onWidgetUnderflow: function(aNode, aContainer) {
+ // The view gets broken by being removed and reinserted by the overflowable
+ // toolbar, so we have to force an uninit and reinit.
+ let win = aNode.ownerGlobal;
+ if (aNode.id == "personal-bookmarks" && win == window) {
+ this._resetView();
+ }
+ },
+
+ onWidgetAdded: function(aWidgetId, aArea, aPosition) {
+ if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) {
+ // It's possible (with the "Add to Menu", "Add to Toolbar" context
+ // options) that the Places Toolbar Items have been moved without
+ // letting us prepare and handle it with with customizeStart and
+ // customizeDone. If that's the case, we need to reset the views
+ // since they're probably broken from the DOM reparenting.
+ this._resetView();
+ }
+ },
+
+ _resetView: function() {
+ if (this._viewElt) {
+ // It's possible that the placesView might not exist, and we need to
+ // do a full init. This could happen if the Bookmarks Toolbar Items are
+ // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar"
+ // context menu option, outside of customize mode.
+ if (this._viewElt._placesView) {
+ this._viewElt._placesView.uninit();
+ }
+ this.init(true);
+ }
+ },
+};
+
+/**
+ * Handles the bookmarks menu-button in the toolbar.
+ */
+
+var BookmarkingUI = {
+ BOOKMARK_BUTTON_ID: "bookmarks-menu-button",
+ BOOKMARK_BUTTON_SHORTCUT: "addBookmarkAsKb",
+ get button() {
+ delete this.button;
+ let widgetGroup = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID);
+ return this.button = widgetGroup.forWindow(window).node;
+ },
+
+ /* Can't make this a self-deleting getter because it's anonymous content
+ * and might lose/regain bindings at some point. */
+ get star() {
+ return document.getAnonymousElementByAttribute(this.button, "anonid",
+ "button");
+ },
+
+ get anchor() {
+ if (!this._shouldUpdateStarState()) {
+ return null;
+ }
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed)
+ return widget.anchor;
+
+ let star = this.star;
+ return star ? document.getAnonymousElementByAttribute(star, "class",
+ "toolbarbutton-icon")
+ : null;
+ },
+
+ get notifier() {
+ delete this.notifier;
+ return this.notifier = document.getElementById("bookmarked-notification-anchor");
+ },
+
+ get dropmarkerNotifier() {
+ delete this.dropmarkerNotifier;
+ return this.dropmarkerNotifier = document.getElementById("bookmarked-notification-dropmarker-anchor");
+ },
+
+ get broadcaster() {
+ delete this.broadcaster;
+ let broadcaster = document.getElementById("bookmarkThisPageBroadcaster");
+ return this.broadcaster = broadcaster;
+ },
+
+ STATUS_UPDATING: -1,
+ STATUS_UNSTARRED: 0,
+ STATUS_STARRED: 1,
+ get status() {
+ if (!this._shouldUpdateStarState()) {
+ return this.STATUS_UNSTARRED;
+ }
+ if (this._pendingStmt)
+ return this.STATUS_UPDATING;
+ return this.button.hasAttribute("starred") ? this.STATUS_STARRED
+ : this.STATUS_UNSTARRED;
+ },
+
+ get _starredTooltip()
+ {
+ delete this._starredTooltip;
+ return this._starredTooltip =
+ this._getFormattedTooltip("starButtonOn.tooltip2");
+ },
+
+ get _unstarredTooltip()
+ {
+ delete this._unstarredTooltip;
+ return this._unstarredTooltip =
+ this._getFormattedTooltip("starButtonOff.tooltip2");
+ },
+
+ _getFormattedTooltip: function(strId) {
+ let args = [];
+ let shortcut = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT);
+ if (shortcut)
+ args.push(ShortcutUtils.prettifyShortcut(shortcut));
+ return gNavigatorBundle.getFormattedString(strId, args);
+ },
+
+ /**
+ * The type of the area in which the button is currently located.
+ * When in the panel, we don't update the button's icon.
+ */
+ _currentAreaType: null,
+ _shouldUpdateStarState: function() {
+ return this._currentAreaType == CustomizableUI.TYPE_TOOLBAR;
+ },
+
+ /**
+ * The popup contents must be updated when the user customizes the UI, or
+ * changes the personal toolbar collapsed status. In such a case, any needed
+ * change should be handled in the popupshowing helper, for performance
+ * reasons.
+ */
+ _popupNeedsUpdate: true,
+ onToolbarVisibilityChange: function BUI_onToolbarVisibilityChange() {
+ this._popupNeedsUpdate = true;
+ },
+
+ onPopupShowing: function BUI_onPopupShowing(event) {
+ // Don't handle events for submenus.
+ if (event.target != event.currentTarget)
+ return;
+
+ // Ideally this code would never be reached, but if you click the outer
+ // button's border, some cpp code for the menu button's so-called XBL binding
+ // decides to open the popup even though the dropmarker is invisible.
+ if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
+ this._showSubview();
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed) {
+ // Don't open a popup in the overflow popup, rather just open the Library.
+ event.preventDefault();
+ widget.node.removeAttribute("closemenu");
+ PlacesCommandHook.showPlacesOrganizer("BookmarksMenu");
+ return;
+ }
+
+ this._initRecentBookmarks(document.getElementById("BMB_recentBookmarks"),
+ "subviewbutton");
+
+ if (!this._popupNeedsUpdate)
+ return;
+ this._popupNeedsUpdate = false;
+
+ let popup = event.target;
+ let getPlacesAnonymousElement =
+ aAnonId => document.getAnonymousElementByAttribute(popup.parentNode,
+ "placesanonid",
+ aAnonId);
+
+ let viewToolbarMenuitem = getPlacesAnonymousElement("view-toolbar");
+ if (viewToolbarMenuitem) {
+ // Update View bookmarks toolbar checkbox menuitem.
+ viewToolbarMenuitem.classList.add("subviewbutton");
+ let personalToolbar = document.getElementById("PersonalToolbar");
+ viewToolbarMenuitem.setAttribute("checked", !personalToolbar.collapsed);
+ }
+ },
+
+ attachPlacesView: function(event, node) {
+ // If the view is already there, bail out early.
+ if (node.parentNode._placesView)
+ return;
+
+ new PlacesMenu(event, "place:folder=BOOKMARKS_MENU", {
+ extraClasses: {
+ entry: "subviewbutton",
+ footer: "panel-subview-footer"
+ },
+ insertionPoint: ".panel-subview-footer"
+ });
+ },
+
+ RECENTLY_BOOKMARKED_PREF: "browser.bookmarks.showRecentlyBookmarked",
+
+ _initRecentBookmarks(aHeaderItem, aExtraCSSClass) {
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+
+ // Add observers and listeners and remove them again when the menupopup closes.
+
+ let bookmarksMenu = aHeaderItem.parentNode;
+ let placesContextMenu = document.getElementById("placesContext");
+
+ let prefObserver = () => {
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+ };
+
+ this._recentlyBookmarkedObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ Ci.nsISupportsWeakReference
+ ])
+ };
+ this._recentlyBookmarkedObserver.onItemRemoved = () => {
+ // Update the menu when a bookmark has been removed.
+ // The native menubar on Mac doesn't support live update, so this won't
+ // work there.
+ this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass);
+ };
+
+ let updatePlacesContextMenu = (shouldHidePrefUI = false) => {
+ let prefEnabled = !shouldHidePrefUI && Services.prefs.getBoolPref(this.RECENTLY_BOOKMARKED_PREF);
+ let showItem = document.getElementById("placesContext_showRecentlyBookmarked");
+ let hideItem = document.getElementById("placesContext_hideRecentlyBookmarked");
+ let separator = document.getElementById("placesContext_recentlyBookmarkedSeparator");
+ showItem.hidden = shouldHidePrefUI || prefEnabled;
+ hideItem.hidden = shouldHidePrefUI || !prefEnabled;
+ separator.hidden = shouldHidePrefUI;
+ if (!shouldHidePrefUI) {
+ // Move to the bottom of the menu.
+ separator.parentNode.appendChild(separator);
+ showItem.parentNode.appendChild(showItem);
+ hideItem.parentNode.appendChild(hideItem);
+ }
+ };
+
+ let onPlacesContextMenuShowing = event => {
+ if (event.target == event.currentTarget) {
+ let triggerPopup = event.target.triggerNode;
+ while (triggerPopup && triggerPopup.localName != "menupopup") {
+ triggerPopup = triggerPopup.parentNode;
+ }
+ let shouldHidePrefUI = triggerPopup != bookmarksMenu;
+ updatePlacesContextMenu(shouldHidePrefUI);
+ }
+ };
+
+ let onBookmarksMenuHidden = event => {
+ if (event.target == event.currentTarget) {
+ updatePlacesContextMenu(true);
+
+ Services.prefs.removeObserver(this.RECENTLY_BOOKMARKED_PREF, prefObserver, false);
+ PlacesUtils.bookmarks.removeObserver(this._recentlyBookmarkedObserver);
+ this._recentlyBookmarkedObserver = null;
+ if (placesContextMenu) {
+ placesContextMenu.removeEventListener("popupshowing", onPlacesContextMenuShowing);
+ }
+ bookmarksMenu.removeEventListener("popuphidden", onBookmarksMenuHidden);
+ }
+ };
+
+ Services.prefs.addObserver(this.RECENTLY_BOOKMARKED_PREF, prefObserver, false);
+ PlacesUtils.bookmarks.addObserver(this._recentlyBookmarkedObserver, true);
+
+ // The context menu doesn't exist in non-browser windows on Mac
+ if (placesContextMenu) {
+ placesContextMenu.addEventListener("popupshowing", onPlacesContextMenuShowing);
+ }
+
+ bookmarksMenu.addEventListener("popuphidden", onBookmarksMenuHidden);
+ },
+
+ _populateRecentBookmarks(aHeaderItem, aExtraCSSClass = "") {
+ while (aHeaderItem.nextSibling &&
+ aHeaderItem.nextSibling.localName == "menuitem") {
+ aHeaderItem.nextSibling.remove();
+ }
+
+ let shouldShow = Services.prefs.getBoolPref(this.RECENTLY_BOOKMARKED_PREF);
+ let separator = aHeaderItem.previousSibling;
+ aHeaderItem.hidden = !shouldShow;
+ separator.hidden = !shouldShow;
+
+ if (!shouldShow) {
+ return;
+ }
+
+ const kMaxResults = 5;
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.excludeQueries = true;
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ options.sortingMode = options.SORT_BY_DATEADDED_DESCENDING;
+ options.maxResults = kMaxResults;
+ let query = PlacesUtils.history.getNewQuery();
+
+ let fragment = document.createDocumentFragment();
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = node.uri;
+ let title = node.title;
+ let icon = node.icon;
+
+ let item =
+ document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "menuitem");
+ item.setAttribute("label", title || uri);
+ item.setAttribute("targetURI", uri);
+ item.setAttribute("simulated-places-node", true);
+ item.setAttribute("class", "menuitem-iconic menuitem-with-favicon bookmark-item " +
+ aExtraCSSClass);
+ if (icon) {
+ item.setAttribute("image", icon);
+ }
+ item._placesNode = node;
+ fragment.appendChild(item);
+ }
+ root.containerOpen = false;
+ aHeaderItem.parentNode.insertBefore(fragment, aHeaderItem.nextSibling);
+ },
+
+ showRecentlyBookmarked() {
+ Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, true);
+ },
+
+ hideRecentlyBookmarked() {
+ Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, false);
+ },
+
+ _updateCustomizationState: function BUI__updateCustomizationState() {
+ let placement = CustomizableUI.getPlacementOfWidget(this.BOOKMARK_BUTTON_ID);
+ this._currentAreaType = placement && CustomizableUI.getAreaType(placement.area);
+ },
+
+ _uninitView: function BUI__uninitView() {
+ // When an element with a placesView attached is removed and re-inserted,
+ // XBL reapplies the binding causing any kind of issues and possible leaks,
+ // so kill current view and let popupshowing generate a new one.
+ if (this.button._placesView)
+ this.button._placesView.uninit();
+
+ // We have to do the same thing for the "special" views underneath the
+ // the bookmarks menu.
+ const kSpecialViewNodeIDs = ["BMB_bookmarksToolbar", "BMB_unsortedBookmarks"];
+ for (let viewNodeID of kSpecialViewNodeIDs) {
+ let elem = document.getElementById(viewNodeID);
+ if (elem && elem._placesView) {
+ elem._placesView.uninit();
+ }
+ }
+ },
+
+ onCustomizeStart: function BUI_customizeStart(aWindow) {
+ if (aWindow == window) {
+ this._uninitView();
+ this._isCustomizing = true;
+ }
+ },
+
+ onWidgetAdded: function BUI_widgetAdded(aWidgetId) {
+ if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetRemoved: function BUI_widgetRemoved(aWidgetId) {
+ if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetReset: function BUI_widgetReset(aNode, aContainer) {
+ if (aNode == this.button) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode, aContainer) {
+ if (aNode == this.button) {
+ this._onWidgetWasMoved();
+ }
+ },
+
+ _onWidgetWasMoved: function BUI_widgetWasMoved() {
+ let usedToUpdateStarState = this._shouldUpdateStarState();
+ this._updateCustomizationState();
+ if (!usedToUpdateStarState && this._shouldUpdateStarState()) {
+ this.updateStarState();
+ } else if (usedToUpdateStarState && !this._shouldUpdateStarState()) {
+ this._updateStar();
+ }
+ // If we're moved outside of customize mode, we need to uninit
+ // our view so it gets reconstructed.
+ if (!this._isCustomizing) {
+ this._uninitView();
+ }
+ },
+
+ onCustomizeEnd: function BUI_customizeEnd(aWindow) {
+ if (aWindow == window) {
+ this._isCustomizing = false;
+ this.onToolbarVisibilityChange();
+ }
+ },
+
+ init: function() {
+ CustomizableUI.addListener(this);
+ this._updateCustomizationState();
+ },
+
+ _hasBookmarksObserver: false,
+ _itemIds: [],
+ uninit: function BUI_uninit() {
+ this._updateBookmarkPageMenuItem(true);
+ CustomizableUI.removeListener(this);
+
+ this._uninitView();
+
+ if (this._hasBookmarksObserver) {
+ PlacesUtils.removeLazyBookmarkObserver(this);
+ }
+
+ if (this._pendingStmt) {
+ this._pendingStmt.cancel();
+ delete this._pendingStmt;
+ }
+ },
+
+ onLocationChange: function BUI_onLocationChange() {
+ if (this._uri && gBrowser.currentURI.equals(this._uri)) {
+ return;
+ }
+ this.updateStarState();
+ },
+
+ updateStarState: function BUI_updateStarState() {
+ // Reset tracked values.
+ this._uri = gBrowser.currentURI;
+ this._itemIds = [];
+
+ if (this._pendingStmt) {
+ this._pendingStmt.cancel();
+ delete this._pendingStmt;
+ }
+
+ this._pendingStmt = PlacesUtils.asyncGetBookmarkIds(this._uri, (aItemIds, aURI) => {
+ // Safety check that the bookmarked URI equals the tracked one.
+ if (!aURI.equals(this._uri)) {
+ Components.utils.reportError("BookmarkingUI did not receive current URI");
+ return;
+ }
+
+ // It's possible that onItemAdded gets called before the async statement
+ // calls back. For such an edge case, retain all unique entries from both
+ // arrays.
+ this._itemIds = this._itemIds.filter(
+ id => !aItemIds.includes(id)
+ ).concat(aItemIds);
+
+ this._updateStar();
+
+ // Start observing bookmarks if needed.
+ if (!this._hasBookmarksObserver) {
+ try {
+ PlacesUtils.addLazyBookmarkObserver(this);
+ this._hasBookmarksObserver = true;
+ } catch (ex) {
+ Components.utils.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
+ }
+ }
+
+ delete this._pendingStmt;
+ });
+ },
+
+ _updateStar: function BUI__updateStar() {
+ if (!this._shouldUpdateStarState()) {
+ if (this.broadcaster.hasAttribute("starred")) {
+ this.broadcaster.removeAttribute("starred");
+ this.broadcaster.removeAttribute("buttontooltiptext");
+ }
+ return;
+ }
+
+ if (this._itemIds.length > 0) {
+ this.broadcaster.setAttribute("starred", "true");
+ this.broadcaster.setAttribute("buttontooltiptext", this._starredTooltip);
+ if (this.button.getAttribute("overflowedItem") == "true") {
+ this.button.setAttribute("label", this._starButtonOverflowedStarredLabel);
+ }
+ }
+ else {
+ this.broadcaster.removeAttribute("starred");
+ this.broadcaster.setAttribute("buttontooltiptext", this._unstarredTooltip);
+ if (this.button.getAttribute("overflowedItem") == "true") {
+ this.button.setAttribute("label", this._starButtonOverflowedLabel);
+ }
+ }
+ },
+
+ /**
+ * forceReset is passed when we're destroyed and the label should go back
+ * to the default (Bookmark This Page) for OS X.
+ */
+ _updateBookmarkPageMenuItem: function BUI__updateBookmarkPageMenuItem(forceReset) {
+ let isStarred = !forceReset && this._itemIds.length > 0;
+ let label = isStarred ? "editlabel" : "bookmarklabel";
+ if (this.broadcaster) {
+ this.broadcaster.setAttribute("label", this.broadcaster.getAttribute(label));
+ }
+ },
+
+ onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) {
+ // Don't handle events for submenus.
+ if (event.target != event.currentTarget)
+ return;
+
+ this._updateBookmarkPageMenuItem();
+ PlacesCommandHook.updateBookmarkAllTabsCommand();
+ this._initRecentBookmarks(document.getElementById("menu_recentBookmarks"));
+ },
+
+ _showBookmarkedNotification: function BUI_showBookmarkedNotification() {
+ function getCenteringTransformForRects(rectToPosition, referenceRect) {
+ let topDiff = referenceRect.top - rectToPosition.top;
+ let leftDiff = referenceRect.left - rectToPosition.left;
+ let heightDiff = referenceRect.height - rectToPosition.height;
+ let widthDiff = referenceRect.width - rectToPosition.width;
+ return [(leftDiff + .5 * widthDiff) + "px", (topDiff + .5 * heightDiff) + "px"];
+ }
+
+ if (this._notificationTimeout) {
+ clearTimeout(this._notificationTimeout);
+ }
+
+ if (this.notifier.style.transform == '') {
+ // Get all the relevant nodes and computed style objects
+ let dropmarker = document.getAnonymousElementByAttribute(this.button, "anonid", "dropmarker");
+ let dropmarkerIcon = document.getAnonymousElementByAttribute(dropmarker, "class", "dropmarker-icon");
+ let dropmarkerStyle = getComputedStyle(dropmarkerIcon);
+
+ // Check for RTL and get bounds
+ let isRTL = getComputedStyle(this.button).direction == "rtl";
+ let buttonRect = this.button.getBoundingClientRect();
+ let notifierRect = this.notifier.getBoundingClientRect();
+ let dropmarkerRect = dropmarkerIcon.getBoundingClientRect();
+ let dropmarkerNotifierRect = this.dropmarkerNotifier.getBoundingClientRect();
+
+ // Compute, but do not set, transform for star icon
+ let [translateX, translateY] = getCenteringTransformForRects(notifierRect, buttonRect);
+ let starIconTransform = "translate(" + translateX + ", " + translateY + ")";
+ if (isRTL) {
+ starIconTransform += " scaleX(-1)";
+ }
+
+ // Compute, but do not set, transform for dropmarker
+ [translateX, translateY] = getCenteringTransformForRects(dropmarkerNotifierRect, dropmarkerRect);
+ let dropmarkerTransform = "translate(" + translateX + ", " + translateY + ")";
+
+ // Do all layout invalidation in one go:
+ this.notifier.style.transform = starIconTransform;
+ this.dropmarkerNotifier.style.transform = dropmarkerTransform;
+
+ let dropmarkerAnimationNode = this.dropmarkerNotifier.firstChild;
+ dropmarkerAnimationNode.style.MozImageRegion = dropmarkerStyle.MozImageRegion;
+ dropmarkerAnimationNode.style.listStyleImage = dropmarkerStyle.listStyleImage;
+ }
+
+ let isInOverflowPanel = this.button.getAttribute("overflowedItem") == "true";
+ if (!isInOverflowPanel) {
+ this.notifier.setAttribute("notification", "finish");
+ this.button.setAttribute("notification", "finish");
+ this.dropmarkerNotifier.setAttribute("notification", "finish");
+ }
+
+ this._notificationTimeout = setTimeout( () => {
+ this.notifier.removeAttribute("notification");
+ this.dropmarkerNotifier.removeAttribute("notification");
+ this.button.removeAttribute("notification");
+
+ this.dropmarkerNotifier.style.transform = '';
+ this.notifier.style.transform = '';
+ }, 1000);
+ },
+
+ _showSubview: function() {
+ let view = document.getElementById("PanelUI-bookmarks");
+ view.addEventListener("ViewShowing", this);
+ view.addEventListener("ViewHiding", this);
+ let anchor = document.getElementById(this.BOOKMARK_BUTTON_ID);
+ anchor.setAttribute("closemenu", "none");
+ PanelUI.showSubView("PanelUI-bookmarks", anchor,
+ CustomizableUI.AREA_PANEL);
+ },
+
+ onCommand: function BUI_onCommand(aEvent) {
+ if (aEvent.target != aEvent.currentTarget) {
+ return;
+ }
+
+ // Handle special case when the button is in the panel.
+ let isBookmarked = this._itemIds.length > 0;
+
+ if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
+ this._showSubview();
+ return;
+ }
+ let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
+ .forWindow(window);
+ if (widget.overflowed) {
+ // Close the overflow panel because the Edit Bookmark panel will appear.
+ widget.node.removeAttribute("closemenu");
+ }
+
+ // Ignore clicks on the star if we are updating its state.
+ if (!this._pendingStmt) {
+ if (!isBookmarked)
+ this._showBookmarkedNotification();
+ PlacesCommandHook.bookmarkCurrentPage(true);
+ }
+ },
+
+ onCurrentPageContextPopupShowing() {
+ this._updateBookmarkPageMenuItem();
+ },
+
+ handleEvent: function BUI_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "ViewShowing":
+ this.onPanelMenuViewShowing(aEvent);
+ break;
+ case "ViewHiding":
+ this.onPanelMenuViewHiding(aEvent);
+ break;
+ }
+ },
+
+ onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) {
+ this._updateBookmarkPageMenuItem();
+ // Update checked status of the toolbar toggle.
+ let viewToolbar = document.getElementById("panelMenu_viewBookmarksToolbar");
+ let personalToolbar = document.getElementById("PersonalToolbar");
+ if (personalToolbar.collapsed)
+ viewToolbar.removeAttribute("checked");
+ else
+ viewToolbar.setAttribute("checked", "true");
+ // Get all statically placed buttons to supply them with keyboard shortcuts.
+ let staticButtons = viewToolbar.parentNode.getElementsByTagName("toolbarbutton");
+ for (let i = 0, l = staticButtons.length; i < l; ++i)
+ CustomizableUI.addShortcut(staticButtons[i]);
+ // Setup the Places view.
+ this._panelMenuView = new PlacesPanelMenuView("place:folder=BOOKMARKS_MENU",
+ "panelMenu_bookmarksMenu",
+ "panelMenu_bookmarksMenu", {
+ extraClasses: {
+ entry: "subviewbutton",
+ footer: "panel-subview-footer"
+ }
+ });
+ aEvent.target.removeEventListener("ViewShowing", this);
+ },
+
+ onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) {
+ this._panelMenuView.uninit();
+ delete this._panelMenuView;
+ aEvent.target.removeEventListener("ViewHiding", this);
+ },
+
+ onPanelMenuViewCommand: function BUI_onPanelMenuViewCommand(aEvent, aView) {
+ let target = aEvent.originalTarget;
+ if (!target._placesNode)
+ return;
+ if (PlacesUtils.nodeIsContainer(target._placesNode))
+ PlacesCommandHook.showPlacesOrganizer([ "BookmarksMenu", target._placesNode.itemId ]);
+ else
+ PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView);
+ PanelUI.hide();
+ },
+
+ // nsINavBookmarkObserver
+ onItemAdded: function BUI_onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI) {
+ if (aURI && aURI.equals(this._uri)) {
+ // If a new bookmark has been added to the tracked uri, register it.
+ if (!this._itemIds.includes(aItemId)) {
+ this._itemIds.push(aItemId);
+ // Only need to update the UI if it wasn't marked as starred before:
+ if (this._itemIds.length == 1) {
+ this._updateStar();
+ }
+ }
+ }
+ },
+
+ onItemRemoved: function BUI_onItemRemoved(aItemId) {
+ let index = this._itemIds.indexOf(aItemId);
+ // If one of the tracked bookmarks has been removed, unregister it.
+ if (index != -1) {
+ this._itemIds.splice(index, 1);
+ // Only need to update the UI if the page is no longer starred
+ if (this._itemIds.length == 0) {
+ this._updateStar();
+ }
+ }
+ },
+
+ onItemChanged: function BUI_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue) {
+ if (aProperty == "uri") {
+ let index = this._itemIds.indexOf(aItemId);
+ // If the changed bookmark was tracked, check if it is now pointing to
+ // a different uri and unregister it.
+ if (index != -1 && aNewValue != this._uri.spec) {
+ this._itemIds.splice(index, 1);
+ // Only need to update the UI if the page is no longer starred
+ if (this._itemIds.length == 0) {
+ this._updateStar();
+ }
+ }
+ // If another bookmark is now pointing to the tracked uri, register it.
+ else if (index == -1 && aNewValue == this._uri.spec) {
+ this._itemIds.push(aItemId);
+ // Only need to update the UI if it wasn't marked as starred before:
+ if (this._itemIds.length == 1) {
+ this._updateStar();
+ }
+ }
+ }
+ },
+
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onBeforeItemRemoved: function () {},
+ onItemVisited: function () {},
+ onItemMoved: function () {},
+
+ // CustomizableUI events:
+ _starButtonLabel: null,
+ get _starButtonOverflowedLabel() {
+ delete this._starButtonOverflowedLabel;
+ return this._starButtonOverflowedLabel =
+ gNavigatorBundle.getString("starButtonOverflowed.label");
+ },
+ get _starButtonOverflowedStarredLabel() {
+ delete this._starButtonOverflowedStarredLabel;
+ return this._starButtonOverflowedStarredLabel =
+ gNavigatorBundle.getString("starButtonOverflowedStarred.label");
+ },
+ onWidgetOverflow: function(aNode, aContainer) {
+ let win = aNode.ownerGlobal;
+ if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window)
+ return;
+
+ let currentLabel = aNode.getAttribute("label");
+ if (!this._starButtonLabel)
+ this._starButtonLabel = currentLabel;
+
+ if (currentLabel == this._starButtonLabel) {
+ let desiredLabel = this._itemIds.length > 0 ? this._starButtonOverflowedStarredLabel
+ : this._starButtonOverflowedLabel;
+ aNode.setAttribute("label", desiredLabel);
+ }
+ },
+
+ onWidgetUnderflow: function(aNode, aContainer) {
+ let win = aNode.ownerGlobal;
+ if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window)
+ return;
+
+ // The view gets broken by being removed and reinserted. Uninit
+ // here so popupshowing will generate a new one:
+ this._uninitView();
+
+ if (aNode.getAttribute("label") != this._starButtonLabel)
+ aNode.setAttribute("label", this._starButtonLabel);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ ])
+};
+
+var AutoShowBookmarksToolbar = {
+ init() {
+ Services.obs.addObserver(this, "autoshow-bookmarks-toolbar", false);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "autoshow-bookmarks-toolbar");
+ },
+
+ observe(subject, topic, data) {
+ let toolbar = document.getElementById("PersonalToolbar");
+ if (!toolbar.collapsed)
+ return;
+
+ let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
+ let area = placement && placement.area;
+ if (area != CustomizableUI.AREA_BOOKMARKS)
+ return;
+
+ setToolbarVisibility(toolbar, true);
+ }
+};
diff --git a/browser/base/content/browser-plugins.js b/browser/base/content/browser-plugins.js
new file mode 100644
index 000000000..ad070df12
--- /dev/null
+++ b/browser/base/content/browser-plugins.js
@@ -0,0 +1,548 @@
+/* -*- 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 gPluginHandler = {
+ PREF_SESSION_PERSIST_MINUTES: "plugin.sessionPermissionNow.intervalInMinutes",
+ PREF_PERSISTENT_DAYS: "plugin.persistentPermissionAlways.intervalInDays",
+ MESSAGES: [
+ "PluginContent:ShowClickToPlayNotification",
+ "PluginContent:RemoveNotification",
+ "PluginContent:UpdateHiddenPluginUI",
+ "PluginContent:HideNotificationBar",
+ "PluginContent:InstallSinglePlugin",
+ "PluginContent:ShowPluginCrashedNotification",
+ "PluginContent:SubmitReport",
+ "PluginContent:LinkClickCallback",
+ ],
+
+ init: function () {
+ const mm = window.messageManager;
+ for (let msg of this.MESSAGES) {
+ mm.addMessageListener(msg, this);
+ }
+ window.addEventListener("unload", this);
+ },
+
+ uninit: function () {
+ const mm = window.messageManager;
+ for (let msg of this.MESSAGES) {
+ mm.removeMessageListener(msg, this);
+ }
+ window.removeEventListener("unload", this);
+ },
+
+ handleEvent: function (event) {
+ if (event.type == "unload") {
+ this.uninit();
+ }
+ },
+
+ receiveMessage: function (msg) {
+ switch (msg.name) {
+ case "PluginContent:ShowClickToPlayNotification":
+ this.showClickToPlayNotification(msg.target, msg.data.plugins, msg.data.showNow,
+ msg.principal, msg.data.location);
+ break;
+ case "PluginContent:RemoveNotification":
+ this.removeNotification(msg.target, msg.data.name);
+ break;
+ case "PluginContent:UpdateHiddenPluginUI":
+ this.updateHiddenPluginUI(msg.target, msg.data.haveInsecure, msg.data.actions,
+ msg.principal, msg.data.location);
+ break;
+ case "PluginContent:HideNotificationBar":
+ this.hideNotificationBar(msg.target, msg.data.name);
+ break;
+ case "PluginContent:InstallSinglePlugin":
+ this.installSinglePlugin(msg.data.pluginInfo);
+ break;
+ case "PluginContent:ShowPluginCrashedNotification":
+ this.showPluginCrashedNotification(msg.target, msg.data.messageString,
+ msg.data.pluginID);
+ break;
+ case "PluginContent:SubmitReport":
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ this.submitReport(msg.data.runID, msg.data.keyVals, msg.data.submitURLOptIn);
+ }
+ break;
+ case "PluginContent:LinkClickCallback":
+ switch (msg.data.name) {
+ case "managePlugins":
+ case "openHelpPage":
+ case "openPluginUpdatePage":
+ this[msg.data.name].call(this, msg.data.pluginTag);
+ break;
+ }
+ break;
+ default:
+ Cu.reportError("gPluginHandler did not expect to handle message " + msg.name);
+ break;
+ }
+ },
+
+ // Callback for user clicking on a disabled plugin
+ managePlugins: function () {
+ BrowserOpenAddonsMgr("addons://list/plugin");
+ },
+
+ // Callback for user clicking on the link in a click-to-play plugin
+ // (where the plugin has an update)
+ openPluginUpdatePage: function(pluginTag) {
+ let url = Services.blocklist.getPluginInfoURL(pluginTag);
+ if (!url) {
+ url = Services.blocklist.getPluginBlocklistURL(pluginTag);
+ }
+ openUILinkIn(url, "tab");
+ },
+
+ submitReport: function submitReport(runID, keyVals, submitURLOptIn) {
+ if (!AppConstants.MOZ_CRASHREPORTER) {
+ return;
+ }
+ Services.prefs.setBoolPref("dom.ipc.plugins.reportCrashURL", submitURLOptIn);
+ PluginCrashReporter.submitCrashReport(runID, keyVals);
+ },
+
+ // Callback for user clicking a "reload page" link
+ reloadPage: function (browser) {
+ browser.reload();
+ },
+
+ // Callback for user clicking the help icon
+ openHelpPage: function () {
+ openHelpLink("plugin-crashed", false);
+ },
+
+ _clickToPlayNotificationEventCallback: function PH_ctpEventCallback(event) {
+ if (event == "showing") {
+ Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_SHOWN")
+ .add(!this.options.primaryPlugin);
+ // Histograms always start at 0, even though our data starts at 1
+ let histogramCount = this.options.pluginData.size - 1;
+ if (histogramCount > 4) {
+ histogramCount = 4;
+ }
+ Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_PLUGIN_COUNT")
+ .add(histogramCount);
+ }
+ else if (event == "dismissed") {
+ // Once the popup is dismissed, clicking the icon should show the full
+ // list again
+ this.options.primaryPlugin = null;
+ }
+ },
+
+ /**
+ * Called from the plugin doorhanger to set the new permissions for a plugin
+ * and activate plugins if necessary.
+ * aNewState should be either "allownow" "allowalways" or "block"
+ */
+ _updatePluginPermission: function (aNotification, aPluginInfo, aNewState) {
+ let permission;
+ let expireType;
+ let expireTime;
+ let histogram =
+ Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_USER_ACTION");
+
+ // Update the permission manager.
+ // Also update the current state of pluginInfo.fallbackType so that
+ // subsequent opening of the notification shows the current state.
+ switch (aNewState) {
+ case "allownow":
+ permission = Ci.nsIPermissionManager.ALLOW_ACTION;
+ expireType = Ci.nsIPermissionManager.EXPIRE_SESSION;
+ expireTime = Date.now() + Services.prefs.getIntPref(this.PREF_SESSION_PERSIST_MINUTES) * 60 * 1000;
+ histogram.add(0);
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
+ break;
+
+ case "allowalways":
+ permission = Ci.nsIPermissionManager.ALLOW_ACTION;
+ expireType = Ci.nsIPermissionManager.EXPIRE_TIME;
+ expireTime = Date.now() +
+ Services.prefs.getIntPref(this.PREF_PERSISTENT_DAYS) * 24 * 60 * 60 * 1000;
+ histogram.add(1);
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
+ break;
+
+ case "block":
+ permission = Ci.nsIPermissionManager.PROMPT_ACTION;
+ expireType = Ci.nsIPermissionManager.EXPIRE_NEVER;
+ expireTime = 0;
+ histogram.add(2);
+ switch (aPluginInfo.blocklistState) {
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE;
+ break;
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE;
+ break;
+ default:
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY;
+ }
+ break;
+
+ // In case a plugin has already been allowed in another tab, the "continue allowing" button
+ // shouldn't change any permissions but should run the plugin-enablement code below.
+ case "continue":
+ aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
+ break;
+ default:
+ Cu.reportError(Error("Unexpected plugin state: " + aNewState));
+ return;
+ }
+
+ let browser = aNotification.browser;
+ if (aNewState != "continue") {
+ let principal = aNotification.options.principal;
+ Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString,
+ permission, expireType, expireTime);
+ aPluginInfo.pluginPermissionType = expireType;
+ }
+
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:ActivatePlugins", {
+ pluginInfo: aPluginInfo,
+ newState: aNewState,
+ });
+ },
+
+ showClickToPlayNotification: function (browser, plugins, showNow,
+ principal, location) {
+ // It is possible that we've received a message from the frame script to show
+ // a click to play notification for a principal that no longer matches the one
+ // that the browser's content now has assigned (ie, the browser has browsed away
+ // after the message was sent, but before the message was received). In that case,
+ // we should just ignore the message.
+ if (!principal.equals(browser.contentPrincipal)) {
+ return;
+ }
+
+ // Data URIs, when linked to from some page, inherit the principal of that
+ // page. That means that we also need to compare the actual locations to
+ // ensure we aren't getting a message from a Data URI that we're no longer
+ // looking at.
+ let receivedURI = BrowserUtils.makeURI(location);
+ if (!browser.documentURI.equalsExceptRef(receivedURI)) {
+ return;
+ }
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", browser);
+
+ // If this is a new notification, create a pluginData map, otherwise append
+ let pluginData;
+ if (notification) {
+ pluginData = notification.options.pluginData;
+ } else {
+ pluginData = new Map();
+ }
+
+ for (var pluginInfo of plugins) {
+ if (pluginData.has(pluginInfo.permissionString)) {
+ continue;
+ }
+
+ // If a block contains an infoURL, we should always prefer that to the default
+ // URL that we construct in-product, even for other blocklist types.
+ let url = Services.blocklist.getPluginInfoURL(pluginInfo.pluginTag);
+
+ if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
+ if (!url) {
+ url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag);
+ }
+ }
+ else {
+ url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "clicktoplay";
+ }
+ pluginInfo.detailsLink = url;
+
+ pluginData.set(pluginInfo.permissionString, pluginInfo);
+ }
+
+ let primaryPluginPermission = null;
+ if (showNow) {
+ primaryPluginPermission = plugins[0].permissionString;
+ }
+
+ if (notification) {
+ // Don't modify the notification UI while it's on the screen, that would be
+ // jumpy and might allow clickjacking.
+ if (showNow) {
+ notification.options.primaryPlugin = primaryPluginPermission;
+ notification.reshow();
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
+ }
+ return;
+ }
+
+ let options = {
+ dismissed: !showNow,
+ eventCallback: this._clickToPlayNotificationEventCallback,
+ primaryPlugin: primaryPluginPermission,
+ pluginData: pluginData,
+ principal: principal,
+ };
+ PopupNotifications.show(browser, "click-to-play-plugins",
+ "", "plugins-notification-icon",
+ null, null, options);
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
+ },
+
+ removeNotification: function (browser, name) {
+ let notification = PopupNotifications.getNotification(name, browser);
+ if (notification)
+ PopupNotifications.remove(notification);
+ },
+
+ hideNotificationBar: function (browser, name) {
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.getNotificationWithValue(name);
+ if (notification)
+ notificationBox.removeNotification(notification, true);
+ },
+
+ updateHiddenPluginUI: function (browser, haveInsecure, actions,
+ principal, location) {
+ let origin = principal.originNoSuffix;
+
+ // It is possible that we've received a message from the frame script to show
+ // the hidden plugin notification for a principal that no longer matches the one
+ // that the browser's content now has assigned (ie, the browser has browsed away
+ // after the message was sent, but before the message was received). In that case,
+ // we should just ignore the message.
+ if (!principal.equals(browser.contentPrincipal)) {
+ return;
+ }
+
+ // Data URIs, when linked to from some page, inherit the principal of that
+ // page. That means that we also need to compare the actual locations to
+ // ensure we aren't getting a message from a Data URI that we're no longer
+ // looking at.
+ let receivedURI = BrowserUtils.makeURI(location);
+ if (!browser.documentURI.equalsExceptRef(receivedURI)) {
+ return;
+ }
+
+ // Set up the icon
+ document.getElementById("plugins-notification-icon").classList.
+ toggle("plugin-blocked", haveInsecure);
+
+ // Now configure the notification bar
+ let notificationBox = gBrowser.getNotificationBox(browser);
+
+ function hideNotification() {
+ let n = notificationBox.getNotificationWithValue("plugin-hidden");
+ if (n) {
+ notificationBox.removeNotification(n, true);
+ }
+ }
+
+ // There are three different cases when showing an infobar:
+ // 1. A single type of plugin is hidden on the page. Show the UI for that
+ // plugin.
+ // 2a. Multiple types of plugins are hidden on the page. Show the multi-UI
+ // with the vulnerable styling.
+ // 2b. Multiple types of plugins are hidden on the page, but none are
+ // vulnerable. Show the nonvulnerable multi-UI.
+ function showNotification() {
+ let n = notificationBox.getNotificationWithValue("plugin-hidden");
+ if (n) {
+ // If something is already shown, just keep it
+ return;
+ }
+
+ Services.telemetry.getHistogramById("PLUGINS_INFOBAR_SHOWN").
+ add(true);
+
+ let message;
+ // Icons set directly cannot be manipulated using moz-image-region, so
+ // we use CSS classes instead.
+ let brand = document.getElementById("bundle_brand").getString("brandShortName");
+
+ if (actions.length == 1) {
+ let pluginInfo = actions[0];
+ let pluginName = pluginInfo.pluginName;
+
+ switch (pluginInfo.fallbackType) {
+ case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
+ message = gNavigatorBundle.getFormattedString(
+ "pluginActivateNew.message",
+ [pluginName, origin]);
+ break;
+ case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
+ message = gNavigatorBundle.getFormattedString(
+ "pluginActivateOutdated.message",
+ [pluginName, origin, brand]);
+ break;
+ case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
+ message = gNavigatorBundle.getFormattedString(
+ "pluginActivateVulnerable.message",
+ [pluginName, origin, brand]);
+ }
+ } else {
+ // Multi-plugin
+ message = gNavigatorBundle.getFormattedString(
+ "pluginActivateMultiple.message", [origin]);
+ }
+
+ let buttons = [
+ {
+ label: gNavigatorBundle.getString("pluginContinueBlocking.label"),
+ accessKey: gNavigatorBundle.getString("pluginContinueBlocking.accesskey"),
+ callback: function() {
+ Services.telemetry.getHistogramById("PLUGINS_INFOBAR_BLOCK").
+ add(true);
+
+ Services.perms.addFromPrincipal(principal,
+ "plugin-hidden-notification",
+ Services.perms.DENY_ACTION);
+ }
+ },
+ {
+ label: gNavigatorBundle.getString("pluginActivateTrigger.label"),
+ accessKey: gNavigatorBundle.getString("pluginActivateTrigger.accesskey"),
+ callback: function() {
+ Services.telemetry.getHistogramById("PLUGINS_INFOBAR_ALLOW").
+ add(true);
+
+ let curNotification =
+ PopupNotifications.getNotification("click-to-play-plugins",
+ browser);
+ if (curNotification) {
+ curNotification.reshow();
+ }
+ }
+ }
+ ];
+ n = notificationBox.
+ appendNotification(message, "plugin-hidden", null,
+ notificationBox.PRIORITY_INFO_HIGH, buttons);
+ if (haveInsecure) {
+ n.classList.add('pluginVulnerable');
+ }
+ }
+
+ if (actions.length == 0) {
+ hideNotification();
+ } else {
+ let notificationPermission = Services.perms.testPermissionFromPrincipal(
+ principal, "plugin-hidden-notification");
+ if (notificationPermission == Ci.nsIPermissionManager.DENY_ACTION) {
+ hideNotification();
+ } else {
+ showNotification();
+ }
+ }
+ },
+
+ contextMenuCommand: function (browser, plugin, command) {
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:ContextMenuCommand",
+ { command: command }, { plugin: plugin });
+ },
+
+ // Crashed-plugin observer. Notified once per plugin crash, before events
+ // are dispatched to individual plugin instances.
+ NPAPIPluginCrashed : function(subject, topic, data) {
+ let propertyBag = subject;
+ if (!(propertyBag instanceof Ci.nsIPropertyBag2) ||
+ !(propertyBag instanceof Ci.nsIWritablePropertyBag2) ||
+ !propertyBag.hasKey("runID") ||
+ !propertyBag.hasKey("pluginName")) {
+ Cu.reportError("A NPAPI plugin crashed, but the properties of this plugin " +
+ "cannot be read.");
+ return;
+ }
+
+ let runID = propertyBag.getPropertyAsUint32("runID");
+ let uglyPluginName = propertyBag.getPropertyAsAString("pluginName");
+ let pluginName = BrowserUtils.makeNicePluginName(uglyPluginName);
+ let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID");
+
+ // If we don't have a minidumpID, we can't (or didn't) submit anything.
+ // This can happen if the plugin is killed from the task manager.
+ let state;
+ if (!AppConstants.MOZ_CRASHREPORTER || !gCrashReporter.enabled) {
+ // This state tells the user that crash reporting is disabled, so we
+ // cannot send a report.
+ state = "noSubmit";
+ } else if (!pluginDumpID) {
+ // This state tells the user that there is no crash report available.
+ state = "noReport";
+ } else {
+ // This state asks the user to submit a crash report.
+ state = "please";
+ }
+
+ let mm = window.getGroupMessageManager("browsers");
+ mm.broadcastAsyncMessage("BrowserPlugins:NPAPIPluginProcessCrashed",
+ { pluginName, runID, state });
+ },
+
+ /**
+ * Shows a plugin-crashed notification bar for a browser that has had an
+ * invisiable NPAPI plugin crash, or a GMP plugin crash.
+ *
+ * @param browser
+ * The browser to show the notification for.
+ * @param messageString
+ * The string to put in the notification bar
+ * @param pluginID
+ * The unique-per-process identifier for the NPAPI plugin or GMP.
+ * For a GMP, this is the pluginID. For NPAPI plugins (where "pluginID"
+ * means something different), this is the runID.
+ */
+ showPluginCrashedNotification: function (browser, messageString, pluginID) {
+ // If there's already an existing notification bar, don't do anything.
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.getNotificationWithValue("plugin-crashed");
+ if (notification) {
+ return;
+ }
+
+ // Configure the notification bar
+ let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
+ let iconURL = "chrome://mozapps/skin/plugins/notifyPluginCrashed.png";
+ let reloadLabel = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.label");
+ let reloadKey = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.accesskey");
+
+ let buttons = [{
+ label: reloadLabel,
+ accessKey: reloadKey,
+ popup: null,
+ callback: function() { browser.reload(); },
+ }];
+
+ if (AppConstants.MOZ_CRASHREPORTER &&
+ PluginCrashReporter.hasCrashReport(pluginID)) {
+ let submitLabel = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.label");
+ let submitKey = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.accesskey");
+ let submitButton = {
+ label: submitLabel,
+ accessKey: submitKey,
+ popup: null,
+ callback: () => {
+ PluginCrashReporter.submitCrashReport(pluginID);
+ },
+ };
+
+ buttons.push(submitButton);
+ }
+
+ notification = notificationBox.appendNotification(messageString, "plugin-crashed",
+ iconURL, priority, buttons);
+
+ // Add the "learn more" link.
+ let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let link = notification.ownerDocument.createElementNS(XULNS, "label");
+ link.className = "text-link";
+ link.setAttribute("value", gNavigatorBundle.getString("crashedpluginsMessage.learnMore"));
+ let crashurl = formatURL("app.support.baseURL", true);
+ crashurl += "plugin-crashed-notificationbar";
+ link.href = crashurl;
+ let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText");
+ description.appendChild(link);
+ },
+};
+
+gPluginHandler.init();
diff --git a/browser/base/content/browser-refreshblocker.js b/browser/base/content/browser-refreshblocker.js
new file mode 100644
index 000000000..025d45421
--- /dev/null
+++ b/browser/base/content/browser-refreshblocker.js
@@ -0,0 +1,84 @@
+/* -*- 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/. */
+
+/**
+ * If the user has opted into blocking refresh and redirect attempts by
+ * default, this handles showing the notification to the user which
+ * gives them the option to let the refresh or redirect proceed.
+ */
+var RefreshBlocker = {
+ init() {
+ gBrowser.addEventListener("RefreshBlocked", this);
+ },
+
+ uninit() {
+ gBrowser.removeEventListener("RefreshBlocked", this);
+ },
+
+ handleEvent: function(event) {
+ if (event.type == "RefreshBlocked") {
+ this.block(event.originalTarget, event.detail);
+ }
+ },
+
+ /**
+ * Shows the blocked refresh / redirect notification for some browser.
+ *
+ * @param browser (<xul:browser>)
+ * The browser that had the refresh blocked. This will be the browser
+ * for which we'll show the notification on.
+ * @param data (object)
+ * An object with the following properties:
+ *
+ * URI (string)
+ * The URI that a page is attempting to refresh or redirect to.
+ *
+ * delay (int)
+ * The delay (in milliseconds) before the page was going to reload
+ * or redirect.
+ *
+ * sameURI (bool)
+ * true if we're refreshing the page. false if we're redirecting.
+ *
+ * outerWindowID (int)
+ * The outerWindowID of the frame that requested the refresh or
+ * redirect.
+ */
+ block(browser, data) {
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+ let message =
+ gNavigatorBundle.getFormattedString(data.sameURI ? "refreshBlocked.refreshLabel"
+ : "refreshBlocked.redirectLabel",
+ [brandShortName]);
+
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.getNotificationWithValue("refresh-blocked");
+
+ if (notification) {
+ notification.label = message;
+ } else {
+ let refreshButtonText =
+ gNavigatorBundle.getString("refreshBlocked.goButton");
+ let refreshButtonAccesskey =
+ gNavigatorBundle.getString("refreshBlocked.goButton.accesskey");
+
+ let buttons = [{
+ label: refreshButtonText,
+ accessKey: refreshButtonAccesskey,
+ callback: function (notification, button) {
+ if (browser.messageManager) {
+ browser.messageManager.sendAsyncMessage("RefreshBlocker:Refresh", data);
+ }
+ }
+ }];
+
+ notificationBox.appendNotification(message, "refresh-blocked",
+ "chrome://browser/skin/Info.png",
+ notificationBox.PRIORITY_INFO_MEDIUM,
+ buttons);
+ }
+ }
+};
diff --git a/browser/base/content/browser-safebrowsing.js b/browser/base/content/browser-safebrowsing.js
new file mode 100644
index 000000000..430d84f13
--- /dev/null
+++ b/browser/base/content/browser-safebrowsing.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 gSafeBrowsing = {
+
+ setReportPhishingMenu: function() {
+ // In order to detect whether or not we're at the phishing warning
+ // page, we have to check the documentURI instead of the currentURI.
+ // This is because when the DocShell loads an error page, the
+ // currentURI stays at the original target, while the documentURI
+ // will point to the internal error page we loaded instead.
+ var docURI = gBrowser.selectedBrowser.documentURI;
+ var isPhishingPage =
+ docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked");
+
+ // Show/hide the appropriate menu item.
+ document.getElementById("menu_HelpPopup_reportPhishingtoolmenu")
+ .hidden = isPhishingPage;
+ document.getElementById("menu_HelpPopup_reportPhishingErrortoolmenu")
+ .hidden = !isPhishingPage;
+
+ var broadcasterId = isPhishingPage
+ ? "reportPhishingErrorBroadcaster"
+ : "reportPhishingBroadcaster";
+
+ var broadcaster = document.getElementById(broadcasterId);
+ if (!broadcaster)
+ return;
+
+ // Now look at the currentURI to learn which page we were trying
+ // to browse to.
+ let uri = gBrowser.currentURI;
+ if (uri && (uri.schemeIs("http") || uri.schemeIs("https")))
+ broadcaster.removeAttribute("disabled");
+ else
+ broadcaster.setAttribute("disabled", true);
+ },
+
+ /**
+ * Used to report a phishing page or a false positive
+ * @param name String One of "Phish", "Error", "Malware" or "MalwareError"
+ * @return String the report phishing URL.
+ */
+ getReportURL: function(name) {
+ return SafeBrowsing.getReportURL(name, gBrowser.currentURI);
+ }
+}
diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc
new file mode 100644
index 000000000..fc5bfeb7e
--- /dev/null
+++ b/browser/base/content/browser-sets.inc
@@ -0,0 +1,380 @@
+# -*- 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
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="bundle_shell" src="chrome://browser/locale/shellservice.properties"/>
+ <stringbundle id="bundle_preferences" src="chrome://browser/locale/preferences/preferences.properties"/>
+ </stringbundleset>
+
+ <commandset id="mainCommandSet">
+ <command id="cmd_newNavigator" oncommand="OpenBrowserWindow()" reserved="true"/>
+ <command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" />
+ <command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" />
+
+ <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab(event);" reserved="true"/>
+ <command id="cmd_newNavigatorTabNoEvent" oncommand="BrowserOpenTab();" reserved="true"/>
+ <command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/>
+ <command id="Browser:SavePage" oncommand="saveBrowser(gBrowser.selectedBrowser);"/>
+
+ <command id="Browser:SendLink"
+ oncommand="MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);"/>
+
+ <command id="cmd_pageSetup" oncommand="PrintUtils.showPageSetup();"/>
+ <command id="cmd_print" oncommand="PrintUtils.printWindow(window.gBrowser.selectedBrowser.outerWindowID, window.gBrowser.selectedBrowser);"/>
+ <command id="cmd_printPreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/>
+ <command id="cmd_close" oncommand="BrowserCloseTabOrWindow()" reserved="true"/>
+ <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow()" reserved="true"/>
+ <command id="cmd_toggleMute" oncommand="gBrowser.selectedTab.toggleMuteAudio()"/>
+ <command id="cmd_CustomizeToolbars" oncommand="BrowserCustomizeToolbar()"/>
+ <command id="cmd_quitApplication" oncommand="goQuitApplication()" reserved="true"/>
+
+
+ <commandset id="editMenuCommands"/>
+
+ <command id="View:PageSource" oncommand="BrowserViewSource(window.gBrowser.selectedBrowser);" observes="canViewSource"/>
+ <command id="View:PageInfo" oncommand="BrowserPageInfo();"/>
+ <command id="View:FullScreen" oncommand="BrowserFullScreen();"/>
+ <command id="View:ReaderView" oncommand="ReaderParent.toggleReaderMode(event);"/>
+ <command id="cmd_find"
+ oncommand="gFindBar.onFindCommand();"
+ observes="isImage"/>
+ <command id="cmd_findAgain"
+ oncommand="gFindBar.onFindAgainCommand(false);"
+ observes="isImage"/>
+ <command id="cmd_findPrevious"
+ oncommand="gFindBar.onFindAgainCommand(true);"
+ observes="isImage"/>
+#ifdef XP_MACOSX
+ <command id="cmd_findSelection" oncommand="gFindBar.onFindSelectionCommand();"/>
+#endif
+ <!-- work-around bug 392512 -->
+ <command id="Browser:AddBookmarkAs"
+ oncommand="PlacesCommandHook.bookmarkCurrentPage(true, PlacesUtils.bookmarksMenuFolderId);"/>
+ <!-- The command disabled state must be manually updated through
+ PlacesCommandHook.updateBookmarkAllTabsCommand() -->
+ <command id="Browser:BookmarkAllTabs"
+ oncommand="PlacesCommandHook.bookmarkCurrentPages();"/>
+ <command id="Browser:Home" oncommand="BrowserHome();"/>
+ <command id="Browser:Back" oncommand="BrowserBack();" disabled="true"/>
+ <command id="Browser:BackOrBackDuplicate" oncommand="BrowserBack(event);" disabled="true">
+ <observes element="Browser:Back" attribute="disabled"/>
+ </command>
+ <command id="Browser:Forward" oncommand="BrowserForward();" disabled="true"/>
+ <command id="Browser:ForwardOrForwardDuplicate" oncommand="BrowserForward(event);" disabled="true">
+ <observes element="Browser:Forward" attribute="disabled"/>
+ </command>
+ <command id="Browser:Stop" oncommand="BrowserStop();" disabled="true"/>
+ <command id="Browser:Reload" oncommand="if (event.shiftKey) BrowserReloadSkipCache(); else BrowserReload()" disabled="true"/>
+ <command id="Browser:ReloadOrDuplicate" oncommand="BrowserReloadOrDuplicate(event)" disabled="true">
+ <observes element="Browser:Reload" attribute="disabled"/>
+ </command>
+ <command id="Browser:ReloadSkipCache" oncommand="BrowserReloadSkipCache()" disabled="true">
+ <observes element="Browser:Reload" attribute="disabled"/>
+ </command>
+ <command id="Browser:NextTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(1, true);" reserved="true"/>
+ <command id="Browser:PrevTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(-1, true);" reserved="true"/>
+ <command id="Browser:ShowAllTabs" oncommand="allTabs.open();"/>
+ <command id="cmd_fullZoomReduce" oncommand="FullZoom.reduce()"/>
+ <command id="cmd_fullZoomEnlarge" oncommand="FullZoom.enlarge()"/>
+ <command id="cmd_fullZoomReset" oncommand="FullZoom.reset()"/>
+ <command id="cmd_fullZoomToggle" oncommand="ZoomManager.toggleZoom();"/>
+ <command id="cmd_gestureRotateLeft" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
+ <command id="cmd_gestureRotateRight" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
+ <command id="cmd_gestureRotateEnd" oncommand="gGestureSupport.rotateEnd()"/>
+ <command id="Browser:OpenLocation" oncommand="openLocation();"/>
+ <command id="Browser:RestoreLastSession" oncommand="restoreLastSession();" disabled="true"/>
+ <command id="Browser:NewUserContextTab" oncommand="openNewUserContextTab(event.sourceEvent);" reserved="true"/>
+ <command id="Browser:OpenAboutContainers" oncommand="openPreferences('paneContainers');"/>
+
+ <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
+ <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
+ <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/>
+ <command id="Tools:Sanitize"
+ oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/>
+ <command id="Tools:PrivateBrowsing"
+ oncommand="OpenBrowserWindow({private: true});" reserved="true"/>
+#ifdef E10S_TESTING_ONLY
+ <command id="Tools:NonRemoteWindow"
+ oncommand="OpenBrowserWindow({remote: false});"/>
+#endif
+ <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
+ <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
+ <command id="Social:SharePage" oncommand="SocialShare.sharePage();"/>
+ <command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/>
+ </commandset>
+
+ <commandset id="placesCommands">
+ <command id="Browser:ShowAllBookmarks"
+ oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/>
+ <command id="Browser:ShowAllHistory"
+ oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/>
+ </commandset>
+
+ <broadcasterset id="mainBroadcasterSet">
+ <broadcaster id="Social:PageShareable" disabled="true"/>
+ <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
+ type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
+ oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/>
+
+ <!-- for both places and non-places, the sidebar lives at
+ chrome://browser/content/history/history-panel.xul so there are no
+ problems when switching between versions -->
+ <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;"
+ type="checkbox" group="sidebar"
+ sidebarurl="chrome://browser/content/history/history-panel.xul"
+ oncommand="SidebarUI.toggle('viewHistorySidebar');"/>
+
+ <broadcaster id="viewWebPanelsSidebar" autoCheck="false"
+ type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul"
+ oncommand="SidebarUI.toggle('viewWebPanelsSidebar');"/>
+
+ <broadcaster id="bookmarkThisPageBroadcaster"
+ label="&bookmarkThisPageCmd.label;"
+ bookmarklabel="&bookmarkThisPageCmd.label;"
+ editlabel="&editThisBookmarkCmd.label;"/>
+
+ <!-- popup blocking menu items -->
+ <broadcaster id="blockedPopupAllowSite"
+ accesskey="&allowPopups.accesskey;"
+ oncommand="gPopupBlockerObserver.toggleAllowPopupsForSite(event);"/>
+ <broadcaster id="blockedPopupEditSettings"
+#ifdef XP_WIN
+ label="&editPopupSettings.label;"
+#else
+ label="&editPopupSettingsUnix.label;"
+#endif
+ accesskey="&editPopupSettings.accesskey;"
+ oncommand="gPopupBlockerObserver.editPopupSettings();"/>
+ <broadcaster id="blockedPopupDontShowMessage"
+ accesskey="&dontShowMessage.accesskey;"
+ type="checkbox"
+ oncommand="gPopupBlockerObserver.dontShowMessage();"/>
+ <broadcaster id="blockedPopupsSeparator"/>
+ <broadcaster id="isImage"/>
+ <broadcaster id="canViewSource"/>
+ <broadcaster id="isFrameImage"/>
+ <broadcaster id="singleFeedMenuitemState" disabled="true"/>
+ <broadcaster id="multipleFeedsMenuState" hidden="true"/>
+
+ <!-- Sync broadcasters -->
+ <!-- A broadcaster of a number of attributes suitable for "sync now" UI -
+ A 'syncstatus' attribute is set while actively syncing, and the label
+ attribute which changes from "sync now" to "syncing" etc. -->
+ <broadcaster id="sync-status"/>
+ <!-- broadcasters of the "hidden" attribute to reflect setup state for
+ menus -->
+ <broadcaster id="sync-setup-state"/>
+ <broadcaster id="sync-syncnow-state" hidden="true"/>
+ <broadcaster id="sync-reauth-state" hidden="true"/>
+ <broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;"
+ type="checkbox" group="sidebar"
+ sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml"
+ oncommand="SidebarUI.toggle('viewTabsSidebar');"/>
+ <broadcaster id="workOfflineMenuitemState"/>
+
+ <broadcaster id="devtoolsMenuBroadcaster_PageSource"
+ label="&pageSourceCmd.label;"
+ key="key_viewSource"
+ command="View:PageSource">
+ <observes element="canViewSource" attribute="disabled"/>
+ </broadcaster>
+ </broadcasterset>
+
+ <keyset id="mainKeyset">
+ <key id="key_newNavigator"
+ key="&newNavigatorCmd.key;"
+ command="cmd_newNavigator"
+ modifiers="accel"/>
+ <key id="key_newNavigatorTab" key="&tabCmd.commandkey;" modifiers="accel" command="cmd_newNavigatorTabNoEvent"/>
+ <key id="focusURLBar" key="&openCmd.commandkey;" command="Browser:OpenLocation"
+ modifiers="accel"/>
+#ifndef XP_MACOSX
+ <key id="focusURLBar2" key="&urlbar.accesskey;" command="Browser:OpenLocation"
+ modifiers="alt"/>
+#endif
+
+#
+# Search Command Key Logic works like this:
+#
+# Unix: Ctrl+K (cross platform binding)
+# Ctrl+J (in case of emacs Ctrl-K conflict)
+# Mac: Cmd+K (cross platform binding)
+# Cmd+Opt+F (platform convention)
+# Win: Ctrl+K (cross platform binding)
+# Ctrl+E (IE compat)
+#
+# We support Ctrl+K on all platforms now and advertise it in the menu since it is
+# our standard - it is a "safe" choice since it is near no harmful keys like "W" as
+# "E" is. People mourning the loss of Ctrl+K for emacs compat can switch their GTK
+# system setting to use emacs emulation, and we should respect it. Focus-Search-Box
+# is a fundamental keybinding and we are maintaining a XP binding so that it is easy
+# for people to switch to Linux.
+#
+ <key id="key_search" key="&searchFocus.commandkey;" command="Tools:Search" modifiers="accel"/>
+#ifdef XP_MACOSX
+ <key id="key_search2" key="&findOnCmd.commandkey;" command="Tools:Search" modifiers="accel,alt"/>
+#endif
+#ifdef XP_WIN
+ <key id="key_search2" key="&searchFocus.commandkey2;" command="Tools:Search" modifiers="accel"/>
+#endif
+#ifdef XP_GNOME
+ <key id="key_search2" key="&searchFocusUnix.commandkey;" command="Tools:Search" modifiers="accel"/>
+ <key id="key_openDownloads" key="&downloadsUnix.commandkey;" command="Tools:Downloads" modifiers="accel,shift"/>
+#else
+ <key id="key_openDownloads" key="&downloads.commandkey;" command="Tools:Downloads" modifiers="accel"/>
+#endif
+ <key id="key_openAddons" key="&addons.commandkey;" command="Tools:Addons" modifiers="accel,shift"/>
+ <key id="openFileKb" key="&openFileCmd.commandkey;" command="Browser:OpenFile" modifiers="accel"/>
+ <key id="key_savePage" key="&savePageCmd.commandkey;" command="Browser:SavePage" modifiers="accel"/>
+ <key id="printKb" key="&printCmd.commandkey;" command="cmd_print" modifiers="accel"/>
+ <key id="key_close" key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
+ <key id="key_closeWindow" key="&closeCmd.key;" command="cmd_closeWindow" modifiers="accel,shift"/>
+ <key id="key_toggleMute" key="&toggleMuteCmd.key;" command="cmd_toggleMute" modifiers="control"/>
+ <key id="key_undo"
+ key="&undoCmd.key;"
+ modifiers="accel"/>
+#ifdef XP_UNIX
+ <key id="key_redo" key="&undoCmd.key;" modifiers="accel,shift"/>
+#else
+ <key id="key_redo" key="&redoCmd.key;" modifiers="accel"/>
+#endif
+ <key id="key_cut"
+ key="&cutCmd.key;"
+ modifiers="accel"/>
+ <key id="key_copy"
+ key="&copyCmd.key;"
+ modifiers="accel"/>
+ <key id="key_paste"
+ key="&pasteCmd.key;"
+ modifiers="accel"/>
+ <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/>
+ <key id="key_selectAll" key="&selectAllCmd.key;" modifiers="accel"/>
+
+ <key keycode="VK_BACK" command="cmd_handleBackspace"/>
+ <key keycode="VK_BACK" command="cmd_handleShiftBackspace" modifiers="shift"/>
+#ifndef XP_MACOSX
+ <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="alt"/>
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="alt"/>
+#else
+ <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="accel" />
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="accel" />
+#endif
+#ifdef XP_UNIX
+ <key id="goBackKb2" key="&goBackCmd.commandKey;" command="Browser:Back" modifiers="accel"/>
+ <key id="goForwardKb2" key="&goForwardCmd.commandKey;" command="Browser:Forward" modifiers="accel"/>
+#endif
+ <key id="goHome" keycode="VK_HOME" command="Browser:Home" modifiers="alt"/>
+ <key keycode="VK_F5" command="Browser:Reload"/>
+#ifndef XP_MACOSX
+ <key id="showAllHistoryKb" key="&showAllHistoryCmd.commandkey;" command="Browser:ShowAllHistory" modifiers="accel,shift"/>
+ <key keycode="VK_F5" command="Browser:ReloadSkipCache" modifiers="accel"/>
+ <key id="key_fullScreen" keycode="VK_F11" command="View:FullScreen"/>
+#else
+ <key id="key_fullScreen" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,control"/>
+ <key id="key_fullScreen_old" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,shift"/>
+ <key keycode="VK_F11" command="View:FullScreen"/>
+#endif
+ <key id="toggleReaderMode" key="&toggleReaderMode.key;" command="View:ReaderView" modifiers="accel,alt" disabled="true"/>
+ <key key="&reloadCmd.commandkey;" command="Browser:Reload" modifiers="accel" id="key_reload"/>
+ <key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/>
+ <key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/>
+#ifndef XP_WIN
+ <key id="key_viewInfo" key="&pageInfoCmd.commandkey;" command="View:PageInfo" modifiers="accel"/>
+#endif
+ <key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/>
+ <key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/>
+ <key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/>
+#ifdef XP_MACOSX
+ <key id="key_findSelection" key="&findSelectionCmd.commandkey;" command="cmd_findSelection" modifiers="accel"/>
+#endif
+ <key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/>
+ <key keycode="&findAgainCmd.commandkey2;" command="cmd_findPrevious" modifiers="shift"/>
+
+ <key id="addBookmarkAsKb" key="&bookmarkThisPageCmd.commandkey;" command="Browser:AddBookmarkAs" modifiers="accel"/>
+# Accel+Shift+A-F are reserved on GTK
+#ifndef MOZ_WIDGET_GTK
+ <key id="bookmarkAllTabsKb" key="&bookmarkThisPageCmd.commandkey;" oncommand="PlacesCommandHook.bookmarkCurrentPages();" modifiers="accel,shift"/>
+ <key id="manBookmarkKb" key="&bookmarksCmd.commandkey;" command="Browser:ShowAllBookmarks" modifiers="accel,shift"/>
+#else
+ <key id="manBookmarkKb" key="&bookmarksGtkCmd.commandkey;" command="Browser:ShowAllBookmarks" modifiers="accel,shift"/>
+#endif
+ <key id="viewBookmarksSidebarKb" key="&bookmarksCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/>
+#ifdef XP_WIN
+# Cmd+I is conventially mapped to Info on MacOS X, thus it should not be
+# overridden for other purposes there.
+ <key id="viewBookmarksSidebarWinKb" key="&bookmarksWinCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/>
+#endif
+
+ <key id="key_stop" keycode="VK_ESCAPE" command="Browser:Stop"/>
+
+#ifdef XP_MACOSX
+ <key id="key_stop_mac" modifiers="accel" key="&stopCmd.macCommandKey;" command="Browser:Stop"/>
+#endif
+
+ <key id="key_gotoHistory"
+ key="&historySidebarCmd.commandKey;"
+#ifdef XP_MACOSX
+ modifiers="accel,shift"
+#else
+ modifiers="accel"
+#endif
+ command="viewHistorySidebar"/>
+
+ <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/>
+ <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
+ <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/>
+ <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/>
+
+ <key id="key_showAllTabs" command="Browser:ShowAllTabs" keycode="VK_TAB" modifiers="control,shift"/>
+
+ <key id="key_switchTextDirection" key="&bidiSwitchTextDirectionItem.commandkey;" command="cmd_switchTextDirection" modifiers="accel,shift" />
+
+ <key id="key_privatebrowsing" command="Tools:PrivateBrowsing" key="&privateBrowsingCmd.commandkey;" modifiers="accel,shift"/>
+ <key id="key_sanitize" command="Tools:Sanitize" keycode="VK_DELETE" modifiers="accel,shift"/>
+#ifdef XP_MACOSX
+ <key id="key_sanitize_mac" command="Tools:Sanitize" keycode="VK_BACK" modifiers="accel,shift"/>
+ <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" modifiers="accel" reserved="true"/>
+#elifdef XP_UNIX
+ <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" command="cmd_quitApplication" modifiers="accel"/>
+#endif
+
+#ifdef FULL_BROWSER_WINDOW
+ <key id="key_undoCloseTab" command="History:UndoCloseTab" key="&tabCmd.commandkey;" modifiers="accel,shift"/>
+#endif
+ <key id="key_undoCloseWindow" command="History:UndoCloseWindow" key="&newNavigatorCmd.key;" modifiers="accel,shift"/>
+
+#ifdef XP_GNOME
+#define NUM_SELECT_TAB_MODIFIER alt
+#else
+#define NUM_SELECT_TAB_MODIFIER accel
+#endif
+
+#expand <key id="key_selectTab1" oncommand="gBrowser.selectTabAtIndex(0, event);" key="1" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab2" oncommand="gBrowser.selectTabAtIndex(1, event);" key="2" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab3" oncommand="gBrowser.selectTabAtIndex(2, event);" key="3" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab4" oncommand="gBrowser.selectTabAtIndex(3, event);" key="4" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab5" oncommand="gBrowser.selectTabAtIndex(4, event);" key="5" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab6" oncommand="gBrowser.selectTabAtIndex(5, event);" key="6" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab7" oncommand="gBrowser.selectTabAtIndex(6, event);" key="7" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectTab8" oncommand="gBrowser.selectTabAtIndex(7, event);" key="8" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand <key id="key_selectLastTab" oncommand="gBrowser.selectTabAtIndex(-1, event);" key="9" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+
+ </keyset>
+
+# Used by baseMenuOverlay
+#ifdef XP_MACOSX
+ <commandset id="baseMenuCommandSet" />
+#endif
+ <keyset id="baseMenuKeyset" />
diff --git a/browser/base/content/browser-sidebar.js b/browser/base/content/browser-sidebar.js
new file mode 100644
index 000000000..5893e6015
--- /dev/null
+++ b/browser/base/content/browser-sidebar.js
@@ -0,0 +1,337 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * SidebarUI controls showing and hiding the browser sidebar.
+ *
+ * @note
+ * Some of these methods take a commandID argument - we expect to find a
+ * xul:broadcaster element with the specified ID.
+ * The following attributes on that element may be used and/or modified:
+ * - id (required) the string to match commandID. The convention
+ * is to use this naming scheme: 'view<sidebar-name>Sidebar'.
+ * - sidebarurl (required) specifies the URL to load in this sidebar.
+ * - sidebartitle or label (in that order) specify the title to
+ * display on the sidebar.
+ * - checked indicates whether the sidebar is currently displayed.
+ * Note that toggleSidebar updates this attribute when
+ * it changes the sidebar's visibility.
+ * - group this attribute must be set to "sidebar".
+ */
+var SidebarUI = {
+ browser: null,
+
+ _box: null,
+ _title: null,
+ _splitter: null,
+
+ init() {
+ this._box = document.getElementById("sidebar-box");
+ this.browser = document.getElementById("sidebar");
+ this._title = document.getElementById("sidebar-title");
+ this._splitter = document.getElementById("sidebar-splitter");
+
+ if (!this.adoptFromWindow(window.opener)) {
+ let commandID = this._box.getAttribute("sidebarcommand");
+ if (commandID) {
+ let command = document.getElementById(commandID);
+ if (command) {
+ this._delayedLoad = true;
+ this._box.hidden = false;
+ this._splitter.hidden = false;
+ command.setAttribute("checked", "true");
+ } else {
+ // Remove the |sidebarcommand| attribute, because the element it
+ // refers to no longer exists, so we should assume this sidebar
+ // panel has been uninstalled. (249883)
+ this._box.removeAttribute("sidebarcommand");
+ }
+ }
+ }
+ },
+
+ uninit() {
+ let enumerator = Services.wm.getEnumerator(null);
+ enumerator.getNext();
+ if (!enumerator.hasMoreElements()) {
+ document.persist("sidebar-box", "sidebarcommand");
+ document.persist("sidebar-box", "width");
+ document.persist("sidebar-box", "src");
+ document.persist("sidebar-title", "value");
+ }
+ },
+
+ /**
+ * Try and adopt the status of the sidebar from another window.
+ * @param {Window} sourceWindow - Window to use as a source for sidebar status.
+ * @return true if we adopted the state, or false if the caller should
+ * initialize the state itself.
+ */
+ adoptFromWindow(sourceWindow) {
+ // No source window, or it being closed, or not chrome, or in a different
+ // private-browsing context means we can't adopt.
+ if (!sourceWindow || sourceWindow.closed ||
+ !sourceWindow.document.documentURIObject.schemeIs("chrome") ||
+ PrivateBrowsingUtils.isWindowPrivate(window) != PrivateBrowsingUtils.isWindowPrivate(sourceWindow)) {
+ return false;
+ }
+
+ // If the opener had a sidebar, open the same sidebar in our window.
+ // The opener can be the hidden window too, if we're coming from the state
+ // where no windows are open, and the hidden window has no sidebar box.
+ let sourceUI = sourceWindow.SidebarUI;
+ if (!sourceUI || !sourceUI._box) {
+ // no source UI or no _box means we also can't adopt the state.
+ return false;
+ }
+ if (sourceUI._box.hidden) {
+ // just hidden means we have adopted the hidden state.
+ return true;
+ }
+
+ let commandID = sourceUI._box.getAttribute("sidebarcommand");
+ let commandElem = document.getElementById(commandID);
+
+ // dynamically generated sidebars will fail this check, but we still
+ // consider it adopted.
+ if (!commandElem) {
+ return true;
+ }
+
+ this._title.setAttribute("value",
+ sourceUI._title.getAttribute("value"));
+ this._box.setAttribute("width", sourceUI._box.boxObject.width);
+
+ this._box.setAttribute("sidebarcommand", commandID);
+ // Note: we're setting 'src' on this._box, which is a <vbox>, not on
+ // the <browser id="sidebar">. This lets us delay the actual load until
+ // delayedStartup().
+ this._box.setAttribute("src", sourceUI.browser.getAttribute("src"));
+ this._delayedLoad = true;
+
+ this._box.hidden = false;
+ this._splitter.hidden = false;
+ commandElem.setAttribute("checked", "true");
+ return true;
+ },
+
+ /**
+ * If loading a sidebar was delayed on startup, start the load now.
+ */
+ startDelayedLoad() {
+ if (!this._delayedLoad) {
+ return;
+ }
+
+ this.browser.setAttribute("src", this._box.getAttribute("src"));
+ },
+
+ /**
+ * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
+ * a chance to adjust focus as needed. An additional event is needed, because
+ * we don't want to focus the sidebar when it's opened on startup or in a new
+ * window, only when the user opens the sidebar.
+ */
+ _fireFocusedEvent() {
+ let event = new CustomEvent("SidebarFocused", {bubbles: true});
+ this.browser.contentWindow.dispatchEvent(event);
+
+ // Run the original function for backwards compatibility.
+ fireSidebarFocusedEvent();
+ },
+
+ /**
+ * True if the sidebar is currently open.
+ */
+ get isOpen() {
+ return !this._box.hidden;
+ },
+
+ /**
+ * The ID of the current sidebar (ie, the ID of the broadcaster being used).
+ * This can be set even if the sidebar is hidden.
+ */
+ get currentID() {
+ return this._box.getAttribute("sidebarcommand");
+ },
+
+ get title() {
+ return this._title.value;
+ },
+
+ set title(value) {
+ this._title.value = value;
+ },
+
+ /**
+ * Toggle the visibility of the sidebar. If the sidebar is hidden or is open
+ * with a different commandID, then the sidebar will be opened using the
+ * specified commandID. Otherwise the sidebar will be hidden.
+ *
+ * @param {string} commandID ID of the xul:broadcaster element to use.
+ * @return {Promise}
+ */
+ toggle(commandID = this.currentID) {
+ if (this.isOpen && commandID == this.currentID) {
+ this.hide();
+ return Promise.resolve();
+ }
+ return this.show(commandID);
+ },
+
+ /**
+ * Show the sidebar, using the parameters from the specified broadcaster.
+ * @see SidebarUI note.
+ *
+ * @param {string} commandID ID of the xul:broadcaster element to use.
+ */
+ show(commandID) {
+ return new Promise((resolve, reject) => {
+ let sidebarBroadcaster = document.getElementById(commandID);
+ if (!sidebarBroadcaster || sidebarBroadcaster.localName != "broadcaster") {
+ reject(new Error("Invalid sidebar broadcaster specified: " + commandID));
+ return;
+ }
+
+ if (this.isOpen && commandID != this.currentID) {
+ BrowserUITelemetry.countSidebarEvent(this.currentID, "hide");
+ }
+
+ let broadcasters = document.getElementsByAttribute("group", "sidebar");
+ for (let broadcaster of broadcasters) {
+ // skip elements that observe sidebar broadcasters and random
+ // other elements
+ if (broadcaster.localName != "broadcaster") {
+ continue;
+ }
+
+ if (broadcaster != sidebarBroadcaster) {
+ broadcaster.removeAttribute("checked");
+ } else {
+ sidebarBroadcaster.setAttribute("checked", "true");
+ }
+ }
+
+ this._box.hidden = false;
+ this._splitter.hidden = false;
+
+ this._box.setAttribute("sidebarcommand", sidebarBroadcaster.id);
+
+ let title = sidebarBroadcaster.getAttribute("sidebartitle");
+ if (!title) {
+ title = sidebarBroadcaster.getAttribute("label");
+ }
+ this._title.value = title;
+
+ let url = sidebarBroadcaster.getAttribute("sidebarurl");
+ this.browser.setAttribute("src", url); // kick off async load
+
+ // We set this attribute here in addition to setting it on the <browser>
+ // element itself, because the code in SidebarUI.uninit() persists this
+ // attribute, not the "src" of the <browser id="sidebar">. The reason it
+ // does that is that we want to delay sidebar load a bit when a browser
+ // window opens. See delayedStartup() and SidebarUI.startDelayedLoad().
+ this._box.setAttribute("src", url);
+
+ if (this.browser.contentDocument.location.href != url) {
+ let onLoad = event => {
+ this.browser.removeEventListener("load", onLoad, true);
+
+ // We're handling the 'load' event before it bubbles up to the usual
+ // (non-capturing) event handlers. Let it bubble up before firing the
+ // SidebarFocused event.
+ setTimeout(() => this._fireFocusedEvent(), 0);
+
+ // Run the original function for backwards compatibility.
+ sidebarOnLoad(event);
+
+ resolve();
+ };
+
+ this.browser.addEventListener("load", onLoad, true);
+ } else {
+ // Older code handled this case, so we do it too.
+ this._fireFocusedEvent();
+ resolve();
+ }
+
+ let selBrowser = gBrowser.selectedBrowser;
+ selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
+ {commandID: commandID, isOpen: true}
+ );
+ BrowserUITelemetry.countSidebarEvent(commandID, "show");
+ });
+ },
+
+ /**
+ * Hide the sidebar.
+ */
+ hide() {
+ if (!this.isOpen) {
+ return;
+ }
+
+ let commandID = this._box.getAttribute("sidebarcommand");
+ let sidebarBroadcaster = document.getElementById(commandID);
+
+ if (sidebarBroadcaster.getAttribute("checked") != "true") {
+ return;
+ }
+
+ // Replace the document currently displayed in the sidebar with about:blank
+ // so that we can free memory by unloading the page. We need to explicitly
+ // create a new content viewer because the old one doesn't get destroyed
+ // until about:blank has loaded (which does not happen as long as the
+ // element is hidden).
+ this.browser.setAttribute("src", "about:blank");
+ this.browser.docShell.createAboutBlankContentViewer(null);
+
+ sidebarBroadcaster.removeAttribute("checked");
+ this._box.setAttribute("sidebarcommand", "");
+ this._title.value = "";
+ this._box.hidden = true;
+ this._splitter.hidden = true;
+
+ let selBrowser = gBrowser.selectedBrowser;
+ selBrowser.focus();
+ selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
+ {commandID: commandID, isOpen: false}
+ );
+ BrowserUITelemetry.countSidebarEvent(commandID, "hide");
+ },
+};
+
+/**
+ * This exists for backards compatibility - it will be called once a sidebar is
+ * ready, following any request to show it.
+ *
+ * @deprecated
+ */
+function fireSidebarFocusedEvent() {}
+
+/**
+ * This exists for backards compatibility - it gets called when a sidebar has
+ * been loaded.
+ *
+ * @deprecated
+ */
+function sidebarOnLoad(event) {}
+
+/**
+ * This exists for backards compatibility, and is equivilent to
+ * SidebarUI.toggle() without the forceOpen param. With forceOpen set to true,
+ * it is equalivent to SidebarUI.show().
+ *
+ * @deprecated
+ */
+function toggleSidebar(commandID, forceOpen = false) {
+ Deprecated.warning("toggleSidebar() is deprecated, please use SidebarUI.toggle() or SidebarUI.show() instead",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Sidebar");
+
+ if (forceOpen) {
+ SidebarUI.show(commandID);
+ } else {
+ SidebarUI.toggle(commandID);
+ }
+}
diff --git a/browser/base/content/browser-social.js b/browser/base/content/browser-social.js
new file mode 100644
index 000000000..b470efd3d
--- /dev/null
+++ b/browser/base/content/browser-social.js
@@ -0,0 +1,503 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "exported" symbols
+var SocialUI,
+ SocialShare,
+ SocialActivationListener;
+
+(function() {
+
+XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() {
+ let tmp = {};
+ Cu.import("resource:///modules/Social.jsm", tmp);
+ return tmp.OpenGraphBuilder;
+});
+
+XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() {
+ let tmp = {};
+ Cu.import("resource:///modules/Social.jsm", tmp);
+ return tmp.DynamicResizeWatcher;
+});
+
+SocialUI = {
+ _initialized: false,
+
+ // Called on delayed startup to initialize the UI
+ init: function SocialUI_init() {
+ if (this._initialized) {
+ return;
+ }
+ let mm = window.getGroupMessageManager("social");
+ mm.loadFrameScript("chrome://browser/content/content.js", true);
+ mm.loadFrameScript("chrome://browser/content/social-content.js", true);
+
+ Services.obs.addObserver(this, "social:providers-changed", false);
+
+ CustomizableUI.addListener(this);
+ SocialActivationListener.init();
+
+ Social.init().then((update) => {
+ if (update)
+ this._providersChanged();
+ });
+
+ this._initialized = true;
+ },
+
+ // Called on window unload
+ uninit: function SocialUI_uninit() {
+ if (!this._initialized) {
+ return;
+ }
+ Services.obs.removeObserver(this, "social:providers-changed");
+
+ CustomizableUI.removeListener(this);
+ SocialActivationListener.uninit();
+
+ this._initialized = false;
+ },
+
+ observe: function SocialUI_observe(subject, topic, data) {
+ switch (topic) {
+ case "social:providers-changed":
+ this._providersChanged();
+ break;
+ }
+ },
+
+ _providersChanged: function() {
+ SocialShare.populateProviderMenu();
+ },
+
+ showLearnMore: function() {
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
+ openUILinkIn(url, "tab");
+ },
+
+ closeSocialPanelForLinkTraversal: function (target, linkNode) {
+ // No need to close the panel if this traversal was not retargeted
+ if (target == "" || target == "_self")
+ return;
+
+ // Check to see whether this link traversal was in a social panel
+ let win = linkNode.ownerGlobal;
+ let container = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ let containerParent = container.parentNode;
+ if (containerParent.classList.contains("social-panel") &&
+ containerParent instanceof Ci.nsIDOMXULPopupElement) {
+ // allow the link traversal to finish before closing the panel
+ setTimeout(() => {
+ containerParent.hidePopup();
+ }, 0);
+ }
+ },
+
+ get _chromeless() {
+ // Is this a popup window that doesn't want chrome shown?
+ let docElem = document.documentElement;
+ // extrachrome is not restored during session restore, so we need
+ // to check for the toolbar as well.
+ let chromeless = docElem.getAttribute("chromehidden").includes("extrachrome") ||
+ docElem.getAttribute('chromehidden').includes("toolbar");
+ // This property is "fixed" for a window, so avoid doing the check above
+ // multiple times...
+ delete this._chromeless;
+ this._chromeless = chromeless;
+ return chromeless;
+ },
+
+ get enabled() {
+ // Returns whether social is enabled *for this window*.
+ if (this._chromeless)
+ return false;
+ return Social.providers.length > 0;
+ },
+
+ canSharePage: function(aURI) {
+ return (aURI && (aURI.schemeIs('http') || aURI.schemeIs('https')));
+ },
+
+ onCustomizeEnd: function(aWindow) {
+ if (aWindow != window)
+ return;
+ // customization mode gets buttons out of sync with command updating, fix
+ // the disabled state
+ let canShare = this.canSharePage(gBrowser.currentURI);
+ let shareButton = SocialShare.shareButton;
+ if (shareButton) {
+ if (canShare) {
+ shareButton.removeAttribute("disabled")
+ } else {
+ shareButton.setAttribute("disabled", "true")
+ }
+ }
+ },
+
+ // called on tab/urlbar/location changes and after customization. Update
+ // anything that is tab specific.
+ updateState: function() {
+ goSetCommandEnabled("Social:PageShareable", this.canSharePage(gBrowser.currentURI));
+ }
+}
+
+// message manager handlers
+SocialActivationListener = {
+ init: function() {
+ messageManager.addMessageListener("Social:Activation", this);
+ },
+ uninit: function() {
+ messageManager.removeMessageListener("Social:Activation", this);
+ },
+ receiveMessage: function(aMessage) {
+ let data = aMessage.json;
+ let browser = aMessage.target;
+ data.window = window;
+ // if the source if the message is the share panel, we do a one-click
+ // installation. The source of activations is controlled by the
+ // social.directories preference
+ let options;
+ if (browser == SocialShare.iframe && Services.prefs.getBoolPref("social.share.activationPanelEnabled")) {
+ options = { bypassContentCheck: true, bypassInstallPanel: true };
+ }
+
+ Social.installProvider(data, function(manifest) {
+ Social.activateFromOrigin(manifest.origin, function(provider) {
+ if (provider.shareURL) {
+ // Ensure that the share button is somewhere usable.
+ // SocialShare.shareButton may return null if it is in the menu-panel
+ // and has never been visible, so we check the widget directly. If
+ // there is no area for the widget we move it into the toolbar.
+ let widget = CustomizableUI.getWidget("social-share-button");
+ // If the panel is already open, we can be sure that the provider can
+ // already be accessed, possibly anchored to another toolbar button.
+ // In that case we don't move the widget.
+ if (!widget.areaType && SocialShare.panel.state != "open") {
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // Ensure correct state.
+ SocialUI.onCustomizeEnd(window);
+ }
+
+ // make this new provider the selected provider. If the panel hasn't
+ // been opened, we need to make the frame first.
+ SocialShare._createFrame();
+ SocialShare.iframe.setAttribute('src', 'data:text/plain;charset=utf8,');
+ SocialShare.iframe.setAttribute('origin', provider.origin);
+ // get the right button selected
+ SocialShare.populateProviderMenu();
+ if (SocialShare.panel.state == "open") {
+ SocialShare.sharePage(provider.origin);
+ }
+ }
+ if (provider.postActivationURL) {
+ // if activated from an open share panel, we load the landing page in
+ // a background tab
+ gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"});
+ }
+ });
+ }, options);
+ }
+}
+
+SocialShare = {
+ get _dynamicResizer() {
+ delete this._dynamicResizer;
+ this._dynamicResizer = new DynamicResizeWatcher();
+ return this._dynamicResizer;
+ },
+
+ // Share panel may be attached to the overflow or menu button depending on
+ // customization, we need to manage open state of the anchor.
+ get anchor() {
+ let widget = CustomizableUI.getWidget("social-share-button");
+ return widget.forWindow(window).anchor;
+ },
+ // Holds the anchor node in use whilst the panel is open, because it may vary.
+ _currentAnchor: null,
+
+ get panel() {
+ return document.getElementById("social-share-panel");
+ },
+
+ get iframe() {
+ // panel.firstChild is our toolbar hbox, panel.lastChild is the iframe
+ // container hbox used for an interstitial "loading" graphic
+ return this.panel.lastChild.firstChild;
+ },
+
+ uninit: function () {
+ if (this.iframe) {
+ let mm = this.messageManager;
+ mm.removeMessageListener("PageVisibility:Show", this);
+ mm.removeMessageListener("PageVisibility:Hide", this);
+ mm.removeMessageListener("Social:DOMWindowClose", this);
+ this.iframe.removeEventListener("load", this);
+ this.iframe.remove();
+ }
+ },
+
+ _createFrame: function() {
+ let panel = this.panel;
+ if (this.iframe)
+ return;
+ this.panel.hidden = false;
+ // create and initialize the panel for this window
+ let iframe = document.createElement("browser");
+ iframe.setAttribute("type", "content");
+ iframe.setAttribute("class", "social-share-frame");
+ iframe.setAttribute("context", "contentAreaContextMenu");
+ iframe.setAttribute("tooltip", "aHTMLTooltip");
+ iframe.setAttribute("disableglobalhistory", "true");
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("message", "true");
+ iframe.setAttribute("messagemanagergroup", "social");
+ panel.lastChild.appendChild(iframe);
+ let mm = this.messageManager;
+ mm.addMessageListener("PageVisibility:Show", this);
+ mm.addMessageListener("PageVisibility:Hide", this);
+ mm.sendAsyncMessage("Social:SetErrorURL",
+ { template: "about:socialerror?mode=compactInfo&origin=%{origin}&url=%{url}" });
+ iframe.addEventListener("load", this, true);
+ mm.addMessageListener("Social:DOMWindowClose", this);
+
+ this.populateProviderMenu();
+ },
+
+ get messageManager() {
+ // The xbl bindings for the iframe may not exist yet, so we can't
+ // access iframe.messageManager directly - but can get at it with this dance.
+ return this.iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager;
+ },
+
+ receiveMessage: function(aMessage) {
+ let iframe = this.iframe;
+ switch(aMessage.name) {
+ case "PageVisibility:Show":
+ SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
+ break;
+ case "PageVisibility:Hide":
+ SocialShare._dynamicResizer.stop();
+ break;
+ case "Social:DOMWindowClose":
+ this.panel.hidePopup();
+ break;
+ }
+ },
+
+ handleEvent: function(event) {
+ switch (event.type) {
+ case "load": {
+ this.iframe.parentNode.removeAttribute("loading");
+ if (this.currentShare)
+ SocialShare.messageManager.sendAsyncMessage("Social:OpenGraphData", this.currentShare);
+ }
+ }
+ },
+
+ getSelectedProvider: function() {
+ let provider;
+ let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin");
+ if (lastProviderOrigin) {
+ provider = Social._getProviderFromOrigin(lastProviderOrigin);
+ }
+ return provider;
+ },
+
+ createTooltip: function(event) {
+ let tt = event.target;
+ let provider = Social._getProviderFromOrigin(tt.triggerNode.getAttribute("origin"));
+ tt.firstChild.setAttribute("value", provider.name);
+ tt.lastChild.setAttribute("value", provider.origin);
+ },
+
+ populateProviderMenu: function() {
+ if (!this.iframe)
+ return;
+ let providers = Social.providers.filter(p => p.shareURL);
+ let hbox = document.getElementById("social-share-provider-buttons");
+ // remove everything before the add-share-provider button (which should also
+ // be lastChild if any share providers were added)
+ let addButton = document.getElementById("add-share-provider");
+ while (hbox.lastChild != addButton) {
+ hbox.removeChild(hbox.lastChild);
+ }
+ let selectedProvider = this.getSelectedProvider();
+ for (let provider of providers) {
+ let button = document.createElement("toolbarbutton");
+ button.setAttribute("class", "toolbarbutton-1 share-provider-button");
+ button.setAttribute("type", "radio");
+ button.setAttribute("group", "share-providers");
+ button.setAttribute("image", provider.iconURL);
+ button.setAttribute("tooltip", "share-button-tooltip");
+ button.setAttribute("origin", provider.origin);
+ button.setAttribute("label", provider.name);
+ button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
+ if (provider == selectedProvider) {
+ this.defaultButton = button;
+ }
+ hbox.appendChild(button);
+ }
+ if (!this.defaultButton) {
+ this.defaultButton = addButton;
+ }
+ this.defaultButton.setAttribute("checked", "true");
+ },
+
+ get shareButton() {
+ // web-panels (bookmark/sidebar) don't include customizableui, so
+ // nsContextMenu fails when accessing shareButton, breaking
+ // browser_bug409481.js.
+ if (!window.CustomizableUI)
+ return null;
+ let widget = CustomizableUI.getWidget("social-share-button");
+ if (!widget || !widget.areaType)
+ return null;
+ return widget.forWindow(window).node;
+ },
+
+ _onclick: function() {
+ Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(0);
+ },
+
+ onShowing: function() {
+ (this._currentAnchor || this.anchor).setAttribute("open", "true");
+ this.iframe.addEventListener("click", this._onclick, true);
+ },
+
+ onHidden: function() {
+ (this._currentAnchor || this.anchor).removeAttribute("open");
+ this._currentAnchor = null;
+ this.iframe.docShellIsActive = false;
+ this.iframe.removeEventListener("click", this._onclick, true);
+ this.iframe.setAttribute("src", "data:text/plain;charset=utf8,");
+ // make sure that the frame is unloaded after it is hidden
+ this.messageManager.sendAsyncMessage("Social:ClearFrame");
+ this.currentShare = null;
+ // share panel use is over, purge any history
+ this.iframe.purgeSessionHistory();
+ },
+
+ sharePage: function(providerOrigin, graphData, target, anchor) {
+ // if providerOrigin is undefined, we use the last-used provider, or the
+ // current/default provider. The provider selection in the share panel
+ // will call sharePage with an origin for us to switch to.
+ this._createFrame();
+ let iframe = this.iframe;
+
+ // graphData is an optional param that either defines the full set of data
+ // to be shared, or partial data about the current page. It is set by a call
+ // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST
+ // define at least url. If it is undefined, we're sharing the current url in
+ // the browser tab.
+ let pageData = graphData ? graphData : this.currentShare;
+ let sharedURI = pageData ? Services.io.newURI(pageData.url, null, null) :
+ gBrowser.currentURI;
+ if (!SocialUI.canSharePage(sharedURI))
+ return;
+
+ let browserMM = gBrowser.selectedBrowser.messageManager;
+
+ // the point of this action type is that we can use existing share
+ // endpoints (e.g. oexchange) that do not support additional
+ // socialapi functionality. One tweak is that we shoot an event
+ // containing the open graph data.
+ let _dataFn;
+ if (!pageData || sharedURI == gBrowser.currentURI) {
+ browserMM.addMessageListener("PageMetadata:PageDataResult", _dataFn = (msg) => {
+ browserMM.removeMessageListener("PageMetadata:PageDataResult", _dataFn);
+ let pageData = msg.json;
+ if (graphData) {
+ // overwrite data retreived from page with data given to us as a param
+ for (let p in graphData) {
+ pageData[p] = graphData[p];
+ }
+ }
+ this.sharePage(providerOrigin, pageData, target, anchor);
+ });
+ browserMM.sendAsyncMessage("PageMetadata:GetPageData", null, { target });
+ return;
+ }
+ // if this is a share of a selected item, get any microformats
+ if (!pageData.microformats && target) {
+ browserMM.addMessageListener("PageMetadata:MicroformatsResult", _dataFn = (msg) => {
+ browserMM.removeMessageListener("PageMetadata:MicroformatsResult", _dataFn);
+ pageData.microformats = msg.data;
+ this.sharePage(providerOrigin, pageData, target, anchor);
+ });
+ browserMM.sendAsyncMessage("PageMetadata:GetMicroformats", null, { target });
+ return;
+ }
+ this.currentShare = pageData;
+
+ let provider;
+ if (providerOrigin)
+ provider = Social._getProviderFromOrigin(providerOrigin);
+ else
+ provider = this.getSelectedProvider();
+ if (!provider || !provider.shareURL) {
+ this.showDirectory(anchor);
+ return;
+ }
+ // check the menu button
+ let hbox = document.getElementById("social-share-provider-buttons");
+ let btn = hbox.querySelector("[origin='" + provider.origin + "']");
+ if (btn)
+ btn.checked = true;
+
+ let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData);
+
+ this._dynamicResizer.stop();
+ let size = provider.getPageSize("share");
+ if (size) {
+ // let the css on the share panel define width, but height
+ // calculations dont work on all sites, so we allow that to be
+ // defined.
+ delete size.width;
+ }
+
+ // if we've already loaded this provider/page share endpoint, we don't want
+ // to add another load event listener.
+ let endpointMatch = shareEndpoint == iframe.getAttribute("src");
+ if (endpointMatch) {
+ this._dynamicResizer.start(iframe.parentNode, iframe, size);
+ iframe.docShellIsActive = true;
+ SocialShare.messageManager.sendAsyncMessage("Social:OpenGraphData", this.currentShare);
+ } else {
+ iframe.parentNode.setAttribute("loading", "true");
+ }
+ // if the user switched between share providers we do not want that history
+ // available.
+ iframe.purgeSessionHistory();
+
+ // always ensure that origin belongs to the endpoint
+ let uri = Services.io.newURI(shareEndpoint, null, null);
+ iframe.setAttribute("origin", provider.origin);
+ iframe.setAttribute("src", shareEndpoint);
+ this._openPanel(anchor);
+ },
+
+ showDirectory: function(anchor) {
+ this._createFrame();
+ let iframe = this.iframe;
+ if (iframe.getAttribute("src") == "about:providerdirectory")
+ return;
+ iframe.removeAttribute("origin");
+ iframe.parentNode.setAttribute("loading", "true");
+
+ iframe.setAttribute("src", "about:providerdirectory");
+ this._openPanel(anchor);
+ },
+
+ _openPanel: function(anchor) {
+ this._currentAnchor = anchor || this.anchor;
+ anchor = document.getAnonymousElementByAttribute(this._currentAnchor, "class", "toolbarbutton-icon");
+ this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
+ Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0);
+ }
+};
+
+})();
diff --git a/browser/base/content/browser-syncui.js b/browser/base/content/browser-syncui.js
new file mode 100644
index 000000000..c5c2995c8
--- /dev/null
+++ b/browser/base/content/browser-syncui.js
@@ -0,0 +1,544 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
+ XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
+ "resource://gre/modules/CloudSync.jsm");
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+
+const MIN_STATUS_ANIMATION_DURATION = 1600;
+
+// gSyncUI handles updating the tools menu and displaying notifications.
+var gSyncUI = {
+ _obs: ["weave:service:sync:start",
+ "weave:service:sync:finish",
+ "weave:service:sync:error",
+ "weave:service:setup-complete",
+ "weave:service:login:start",
+ "weave:service:login:finish",
+ "weave:service:login:error",
+ "weave:service:logout:finish",
+ "weave:service:start-over",
+ "weave:service:start-over:finish",
+ "weave:ui:login:error",
+ "weave:ui:sync:error",
+ "weave:ui:sync:finish",
+ "weave:ui:clear-error",
+ "weave:engine:sync:finish"
+ ],
+
+ _unloaded: false,
+ // The last sync start time. Used to calculate the leftover animation time
+ // once syncing completes (bug 1239042).
+ _syncStartTime: 0,
+ _syncAnimationTimer: 0,
+
+ init: function () {
+ Cu.import("resource://services-common/stringbundle.js");
+
+ // Proceed to set up the UI if Sync has already started up.
+ // Otherwise we'll do it when Sync is firing up.
+ if (this.weaveService.ready) {
+ this.initUI();
+ return;
+ }
+
+ // Sync isn't ready yet, but we can still update the UI with an initial
+ // state - we haven't called initUI() yet, but that's OK - that's more
+ // about observers for state changes, and will be called once Sync is
+ // ready to start sending notifications.
+ this.updateUI();
+
+ Services.obs.addObserver(this, "weave:service:ready", true);
+ Services.obs.addObserver(this, "quit-application", true);
+
+ // Remove the observer if the window is closed before the observer
+ // was triggered.
+ window.addEventListener("unload", function onUnload() {
+ gSyncUI._unloaded = true;
+ window.removeEventListener("unload", onUnload, false);
+ Services.obs.removeObserver(gSyncUI, "weave:service:ready");
+ Services.obs.removeObserver(gSyncUI, "quit-application");
+
+ if (Weave.Status.ready) {
+ gSyncUI._obs.forEach(function(topic) {
+ Services.obs.removeObserver(gSyncUI, topic);
+ });
+ }
+ }, false);
+ },
+
+ initUI: function SUI_initUI() {
+ // If this is a browser window?
+ if (gBrowser) {
+ this._obs.push("weave:notification:added");
+ }
+
+ this._obs.forEach(function(topic) {
+ Services.obs.addObserver(this, topic, true);
+ }, this);
+
+ // initial label for the sync buttons.
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
+
+ this.maybeMoveSyncedTabsButton();
+
+ this.updateUI();
+ },
+
+
+ // Returns a promise that resolves with true if Sync needs to be configured,
+ // false otherwise.
+ _needsSetup() {
+ // If Sync is configured for FxAccounts then we do that promise-dance.
+ if (this.weaveService.fxAccountsEnabled) {
+ return fxAccounts.getSignedInUser().then(user => {
+ // We want to treat "account needs verification" as "needs setup".
+ return !(user && user.verified);
+ });
+ }
+ // We are using legacy sync - check that.
+ let firstSync = "";
+ try {
+ firstSync = Services.prefs.getCharPref("services.sync.firstSync");
+ } catch (e) { }
+
+ return Promise.resolve(Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED ||
+ firstSync == "notReady");
+ },
+
+ // Returns a promise that resolves with true if the user currently signed in
+ // to Sync needs to be verified, false otherwise.
+ _needsVerification() {
+ // For callers who care about the distinction between "needs setup" and
+ // "needs verification"
+ if (this.weaveService.fxAccountsEnabled) {
+ return fxAccounts.getSignedInUser().then(user => {
+ // If there is no user, they can't be in a "needs verification" state.
+ if (!user) {
+ return false;
+ }
+ return !user.verified;
+ });
+ }
+
+ // Otherwise we are configured for legacy Sync, which has no verification
+ // concept.
+ return Promise.resolve(false);
+ },
+
+ // Note that we don't show login errors in a notification bar here, but do
+ // still need to track a login-failed state so the "Tools" menu updates
+ // with the correct state.
+ _loginFailed: function () {
+ // If Sync isn't already ready, we don't want to force it to initialize
+ // by referencing Weave.Status - and it isn't going to be accurate before
+ // Sync is ready anyway.
+ if (!this.weaveService.ready) {
+ this.log.debug("_loginFailed has sync not ready, so returning false");
+ return false;
+ }
+ this.log.debug("_loginFailed has sync state=${sync}",
+ { sync: Weave.Status.login});
+ return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ },
+
+ // Kick off an update of the UI - does *not* return a promise.
+ updateUI() {
+ this._promiseUpdateUI().catch(err => {
+ this.log.error("updateUI failed", err);
+ })
+ },
+
+ // Updates the UI - returns a promise.
+ _promiseUpdateUI() {
+ return this._needsSetup().then(needsSetup => {
+ if (!gBrowser)
+ return Promise.resolve();
+
+ let loginFailed = this._loginFailed();
+
+ // Start off with a clean slate
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = true;
+ document.getElementById("sync-syncnow-state").hidden = true;
+
+ if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
+ document.getElementById("sync-syncnow-state").hidden = false;
+ } else if (loginFailed) {
+ // unhiding this element makes the menubar show the login failure state.
+ document.getElementById("sync-reauth-state").hidden = false;
+ } else if (needsSetup) {
+ document.getElementById("sync-setup-state").hidden = false;
+ } else {
+ document.getElementById("sync-syncnow-state").hidden = false;
+ }
+
+ return this._updateSyncButtonsTooltip();
+ });
+ },
+
+ // Functions called by observers
+ onActivityStart() {
+ if (!gBrowser)
+ return;
+
+ this.log.debug("onActivityStart");
+
+ clearTimeout(this._syncAnimationTimer);
+ this._syncStartTime = Date.now();
+
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.setAttribute("syncstatus", "active");
+ broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncing2.label"));
+ broadcaster.setAttribute("disabled", "true");
+
+ this.updateUI();
+ },
+
+ _updateSyncStatus() {
+ if (!gBrowser)
+ return;
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.removeAttribute("syncstatus");
+ broadcaster.removeAttribute("disabled");
+ broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
+ this.updateUI();
+ },
+
+ onActivityStop() {
+ if (!gBrowser)
+ return;
+ this.log.debug("onActivityStop");
+
+ let now = Date.now();
+ let syncDuration = now - this._syncStartTime;
+
+ if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
+ let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
+ clearTimeout(this._syncAnimationTimer);
+ this._syncAnimationTimer = setTimeout(() => this._updateSyncStatus(), animationTime);
+ } else {
+ this._updateSyncStatus();
+ }
+ },
+
+ onLoginError: function SUI_onLoginError() {
+ this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status);
+
+ // We don't show any login errors here; browser-fxaccounts shows them in
+ // the hamburger menu.
+ this.updateUI();
+ },
+
+ onLogout: function SUI_onLogout() {
+ this.updateUI();
+ },
+
+ _getAppName: function () {
+ let brand = new StringBundle("chrome://branding/locale/brand.properties");
+ return brand.get("brandShortName");
+ },
+
+ // Commands
+ // doSync forces a sync - it *does not* return a promise as it is called
+ // via the various UI components.
+ doSync() {
+ this._needsSetup().then(needsSetup => {
+ if (!needsSetup) {
+ setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
+ }
+ Services.obs.notifyObservers(null, "cloudsync:user-sync", null);
+ }).catch(err => {
+ this.log.error("Failed to force a sync", err);
+ });
+ },
+
+ // Handle clicking the toolbar button - which either opens the Sync setup
+ // pages or forces a sync now. Does *not* return a promise as it is called
+ // via the UI.
+ handleToolbarButton() {
+ this._needsSetup().then(needsSetup => {
+ if (needsSetup || this._loginFailed()) {
+ this.openSetup();
+ } else {
+ this.doSync();
+ }
+ }).catch(err => {
+ this.log.error("Failed to handle toolbar button command", err);
+ });
+ },
+
+ /**
+ * Invoke the Sync setup wizard.
+ *
+ * @param wizardType
+ * Indicates type of wizard to launch:
+ * null -- regular set up wizard
+ * "pair" -- pair a device first
+ * "reset" -- reset sync
+ * @param entryPoint
+ * Indicates the entrypoint from where this method was called.
+ */
+
+ openSetup: function SUI_openSetup(wizardType, entryPoint = "syncbutton") {
+ if (this.weaveService.fxAccountsEnabled) {
+ this.openPrefs(entryPoint);
+ } else {
+ let win = Services.wm.getMostRecentWindow("Weave:AccountSetup");
+ if (win)
+ win.focus();
+ else {
+ window.openDialog("chrome://browser/content/sync/setup.xul",
+ "weaveSetup", "centerscreen,chrome,resizable=no",
+ wizardType);
+ }
+ }
+ },
+
+ // Open the legacy-sync device pairing UI. Note used for FxA Sync.
+ openAddDevice: function () {
+ if (!Weave.Utils.ensureMPUnlocked())
+ return;
+
+ let win = Services.wm.getMostRecentWindow("Sync:AddDevice");
+ if (win)
+ win.focus();
+ else
+ window.openDialog("chrome://browser/content/sync/addDevice.xul",
+ "syncAddDevice", "centerscreen,chrome,resizable=no");
+ },
+
+ openPrefs: function (entryPoint) {
+ openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
+ },
+
+ openSignInAgainPage: function (entryPoint = "syncbutton") {
+ gFxAccounts.openSignInAgainPage(entryPoint);
+ },
+
+ openSyncedTabsPanel() {
+ let placement = CustomizableUI.getPlacementOfWidget("sync-button");
+ let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
+ let anchor = document.getElementById("sync-button") ||
+ document.getElementById("PanelUI-menu-button");
+ if (area == CustomizableUI.AREA_PANEL) {
+ // The button is in the panel, so we need to show the panel UI, then our
+ // subview.
+ PanelUI.show().then(() => {
+ PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
+ }).catch(Cu.reportError);
+ } else {
+ // It is placed somewhere else - just try and show it.
+ PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
+ }
+ },
+
+ /* After Sync is initialized we perform a once-only check for the sync
+ button being in "customize purgatory" and if so, move it to the panel.
+ This is done primarily for profiles created before SyncedTabs landed,
+ where the button defaulted to being in that purgatory.
+ We use a preference to ensure we only do it once, so people can still
+ customize it away and have it stick.
+ */
+ maybeMoveSyncedTabsButton() {
+ const prefName = "browser.migrated-sync-button";
+ let migrated = false;
+ try {
+ migrated = Services.prefs.getBoolPref(prefName);
+ } catch (_) {}
+ if (migrated) {
+ return;
+ }
+ if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
+ CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+ }
+ Services.prefs.setBoolPref(prefName, true);
+ },
+
+ /* Update the tooltip for the sync-status broadcaster (which will update the
+ Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
+ If Sync is configured, the tooltip is when the last sync occurred,
+ otherwise the tooltip reflects the fact that Sync needs to be
+ (re-)configured.
+ */
+ _updateSyncButtonsTooltip: Task.async(function* () {
+ if (!gBrowser)
+ return;
+
+ let email;
+ try {
+ email = Services.prefs.getCharPref("services.sync.username");
+ } catch (ex) {}
+
+ let needsSetup = yield this._needsSetup();
+ let needsVerification = yield this._needsVerification();
+ let loginFailed = this._loginFailed();
+ // This is a little messy as the Sync buttons are 1/2 Sync related and
+ // 1/2 FxA related - so for some strings we use Sync strings, but for
+ // others we reach into gFxAccounts for strings.
+ let tooltiptext;
+ if (needsVerification) {
+ // "needs verification"
+ tooltiptext = gFxAccounts.strings.formatStringFromName("verifyDescription", [email], 1);
+ } else if (needsSetup) {
+ // "needs setup".
+ tooltiptext = this._stringBundle.GetStringFromName("signInToSync.description");
+ } else if (loginFailed) {
+ // "need to reconnect/re-enter your password"
+ tooltiptext = gFxAccounts.strings.formatStringFromName("reconnectDescription", [email], 1);
+ } else {
+ // Sync appears configured - format the "last synced at" time.
+ try {
+ let lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
+ tooltiptext = this.formatLastSyncDate(lastSync);
+ }
+ catch (e) {
+ // pref doesn't exist (which will be the case until we've seen the
+ // first successful sync) or is invalid (which should be impossible!)
+ // Just leave tooltiptext as the empty string in these cases, which
+ // will cause the tooltip to be removed below.
+ }
+ }
+
+ // We've done all our promise-y work and ready to update the UI - make
+ // sure it hasn't been torn down since we started.
+ if (!gBrowser)
+ return;
+
+ let broadcaster = document.getElementById("sync-status");
+ if (broadcaster) {
+ if (tooltiptext) {
+ broadcaster.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ broadcaster.removeAttribute("tooltiptext");
+ }
+ }
+ }),
+
+ formatLastSyncDate: function(date) {
+ let dateFormat;
+ let sixDaysAgo = (() => {
+ let date = new Date();
+ date.setDate(date.getDate() - 6);
+ date.setHours(0, 0, 0, 0);
+ return date;
+ })();
+ // It may be confusing for the user to see "Last Sync: Monday" when the last sync was a indeed a Monday but 3 weeks ago
+ if (date < sixDaysAgo) {
+ dateFormat = {month: 'long', day: 'numeric'};
+ } else {
+ dateFormat = {weekday: 'long', hour: 'numeric', minute: 'numeric'};
+ }
+ let lastSyncDateString = date.toLocaleDateString(undefined, dateFormat);
+ return this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
+ },
+
+ onClientsSynced: function() {
+ let broadcaster = document.getElementById("sync-syncnow-state");
+ if (broadcaster) {
+ if (Weave.Service.clientsEngine.stats.numClients > 1) {
+ broadcaster.setAttribute("devices-status", "multi");
+ } else {
+ broadcaster.setAttribute("devices-status", "single");
+ }
+ }
+ },
+
+ observe: function SUI_observe(subject, topic, data) {
+ this.log.debug("observed", topic);
+ if (this._unloaded) {
+ Cu.reportError("SyncUI observer called after unload: " + topic);
+ return;
+ }
+
+ // Unwrap, just like Svc.Obs, but without pulling in that dependency.
+ if (subject && typeof subject == "object" &&
+ ("wrappedJSObject" in subject) &&
+ ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) {
+ subject = subject.wrappedJSObject.object;
+ }
+
+ // First handle "activity" only.
+ switch (topic) {
+ case "weave:service:sync:start":
+ this.onActivityStart();
+ break;
+ case "weave:service:sync:finish":
+ case "weave:service:sync:error":
+ this.onActivityStop();
+ break;
+ }
+ // Now non-activity state (eg, enabled, errors, etc)
+ // Note that sync uses the ":ui:" notifications for errors because sync.
+ switch (topic) {
+ case "weave:ui:sync:finish":
+ // Do nothing.
+ break;
+ case "weave:ui:sync:error":
+ case "weave:service:setup-complete":
+ case "weave:service:login:finish":
+ case "weave:service:login:start":
+ case "weave:service:start-over":
+ this.updateUI();
+ break;
+ case "weave:ui:login:error":
+ case "weave:service:login:error":
+ this.onLoginError();
+ break;
+ case "weave:service:logout:finish":
+ this.onLogout();
+ break;
+ case "weave:service:start-over:finish":
+ this.updateUI();
+ break;
+ case "weave:service:ready":
+ this.initUI();
+ break;
+ case "weave:notification:added":
+ this.initNotifications();
+ break;
+ case "weave:engine:sync:finish":
+ if (data != "clients") {
+ return;
+ }
+ this.onClientsSynced();
+ break;
+ case "quit-application":
+ // Stop the animation timer on shutdown, since we can't update the UI
+ // after this.
+ clearTimeout(this._syncAnimationTimer);
+ break;
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ])
+};
+
+XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
+ // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
+ // but for now just make it work
+ return Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle("chrome://weave/locale/services/sync.properties");
+});
+
+XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() {
+ return Log.repository.getLogger("browserwindow.syncui");
+});
+
+XPCOMUtils.defineLazyGetter(gSyncUI, "weaveService", function() {
+ return Components.classes["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+});
diff --git a/browser/base/content/browser-tabPreviews.xml b/browser/base/content/browser-tabPreviews.xml
new file mode 100644
index 000000000..f3f2ad180
--- /dev/null
+++ b/browser/base/content/browser-tabPreviews.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+# -*- Mode: HTML -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<bindings id="tabPreviews"
+ 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="ctrlTab-preview" extends="chrome://global/content/bindings/button.xml#button-base">
+ <content pack="center">
+ <xul:stack>
+ <xul:vbox class="ctrlTab-preview-inner" align="center" pack="center"
+ xbl:inherits="width=canvaswidth">
+ <xul:hbox class="tabPreview-canvas" xbl:inherits="style=canvasstyle">
+ <children/>
+ </xul:hbox>
+ <xul:label xbl:inherits="value=label,crop" class="plain"/>
+ </xul:vbox>
+ <xul:hbox class="ctrlTab-favicon-container" xbl:inherits="hidden=noicon">
+ <xul:image class="ctrlTab-favicon" xbl:inherits="src=image"/>
+ </xul:hbox>
+ </xul:stack>
+ </content>
+ <handlers>
+ <handler event="mouseover" action="ctrlTab._mouseOverFocus(this);"/>
+ <handler event="command" action="ctrlTab.pick(this);"/>
+ <handler event="click" button="1" action="ctrlTab.remove(this);"/>
+#ifdef XP_MACOSX
+# Control+click is a right click on OS X
+ <handler event="click" button="2" action="ctrlTab.pick(this);"/>
+#endif
+ </handlers>
+ </binding>
+</bindings>
diff --git a/browser/base/content/browser-tabsintitlebar-stub.js b/browser/base/content/browser-tabsintitlebar-stub.js
new file mode 100644
index 000000000..1e45b17dd
--- /dev/null
+++ b/browser/base/content/browser-tabsintitlebar-stub.js
@@ -0,0 +1,17 @@
+/* -*- 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 file is used as a stub object for platforms which
+// don't have CAN_DRAW_IN_TITLEBAR defined.
+
+var TabsInTitlebar = {
+ init: function () {},
+ uninit: function () {},
+ allowedBy: function (condition, allow) {},
+ updateAppearance: function updateAppearance(aForce) {},
+ get enabled() {
+ return document.documentElement.getAttribute("tabsintitlebar") == "true";
+ },
+};
diff --git a/browser/base/content/browser-tabsintitlebar.js b/browser/base/content/browser-tabsintitlebar.js
new file mode 100644
index 000000000..5c0d94514
--- /dev/null
+++ b/browser/base/content/browser-tabsintitlebar.js
@@ -0,0 +1,307 @@
+/* -*- 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/. */
+
+// Note: the file browser-tabsintitlebar-stub.js is used instead of
+// this one on platforms which don't have CAN_DRAW_IN_TITLEBAR defined.
+
+var TabsInTitlebar = {
+ init: function () {
+ if (this._initialized) {
+ return;
+ }
+ this._readPref();
+ Services.prefs.addObserver(this._prefName, this, false);
+
+ // We need to update the appearance of the titlebar when the menu changes
+ // from the active to the inactive state. We can't, however, rely on
+ // DOMMenuBarInactive, because the menu fires this event and then removes
+ // the inactive attribute after an event-loop spin.
+ //
+ // Because updating the appearance involves sampling the heights and margins
+ // of various elements, it's important that the layout be more or less
+ // settled before updating the titlebar. So instead of listening to
+ // DOMMenuBarActive and DOMMenuBarInactive, we use a MutationObserver to
+ // watch the "invalid" attribute directly.
+ let menu = document.getElementById("toolbar-menubar");
+ this._menuObserver = new MutationObserver(this._onMenuMutate);
+ this._menuObserver.observe(menu, {attributes: true});
+
+ this.onAreaReset = function(aArea) {
+ if (aArea == CustomizableUI.AREA_TABSTRIP || aArea == CustomizableUI.AREA_MENUBAR)
+ this._update(true);
+ };
+ this.onWidgetAdded = this.onWidgetRemoved = function(aWidgetId, aArea) {
+ if (aArea == CustomizableUI.AREA_TABSTRIP || aArea == CustomizableUI.AREA_MENUBAR)
+ this._update(true);
+ };
+ CustomizableUI.addListener(this);
+
+ addEventListener("resolutionchange", this, false);
+
+ this._initialized = true;
+ if (this._updateOnInit) {
+ // We don't need to call this with 'true', even if original calls
+ // (before init()) did, because this will be the first call and so
+ // we will update anyway.
+ this._update();
+ }
+ },
+
+ allowedBy: function (condition, allow) {
+ if (allow) {
+ if (condition in this._disallowed) {
+ delete this._disallowed[condition];
+ this._update(true);
+ }
+ } else if (!(condition in this._disallowed)) {
+ this._disallowed[condition] = null;
+ this._update(true);
+ }
+ },
+
+ updateAppearance: function updateAppearance(aForce) {
+ this._update(aForce);
+ },
+
+ get enabled() {
+ return document.documentElement.getAttribute("tabsintitlebar") == "true";
+ },
+
+ observe: function (subject, topic, data) {
+ if (topic == "nsPref:changed")
+ this._readPref();
+ },
+
+ handleEvent: function (aEvent) {
+ if (aEvent.type == "resolutionchange" && aEvent.target == window) {
+ this._update(true);
+ }
+ },
+
+ _onMenuMutate: function (aMutations) {
+ for (let mutation of aMutations) {
+ if (mutation.attributeName == "inactive" ||
+ mutation.attributeName == "autohide") {
+ TabsInTitlebar._update(true);
+ return;
+ }
+ }
+ },
+
+ _initialized: false,
+ _updateOnInit: false,
+ _disallowed: {},
+ _prefName: "browser.tabs.drawInTitlebar",
+ _lastSizeMode: null,
+
+ _readPref: function () {
+ this.allowedBy("pref",
+ Services.prefs.getBoolPref(this._prefName));
+ },
+
+ _update: function (aForce=false) {
+ let $ = id => document.getElementById(id);
+ let rect = ele => ele.getBoundingClientRect();
+ let verticalMargins = cstyle => parseFloat(cstyle.marginBottom) + parseFloat(cstyle.marginTop);
+
+ if (window.fullScreen)
+ return;
+
+ // In some edgecases it is possible for this to fire before we've initialized.
+ // Don't run now, but don't forget to run it when we do initialize.
+ if (!this._initialized) {
+ this._updateOnInit = true;
+ return;
+ }
+
+ if (!aForce) {
+ // _update is called on resize events, because the window is not ready
+ // after sizemode events. However, we only care about the event when the
+ // sizemode is different from the last time we updated the appearance of
+ // the tabs in the titlebar.
+ let sizemode = document.documentElement.getAttribute("sizemode");
+ if (this._lastSizeMode == sizemode) {
+ return;
+ }
+ let oldSizeMode = this._lastSizeMode;
+ this._lastSizeMode = sizemode;
+ // Don't update right now if we are leaving fullscreen, since the UI is
+ // still changing in the consequent "fullscreen" event. Code there will
+ // call this function again when everything is ready.
+ // See browser-fullScreen.js: FullScreen.toggle and bug 1173768.
+ if (oldSizeMode == "fullscreen") {
+ return;
+ }
+ }
+
+ let allowed = (Object.keys(this._disallowed)).length == 0;
+
+ let titlebar = $("titlebar");
+ let titlebarContent = $("titlebar-content");
+ let menubar = $("toolbar-menubar");
+
+ if (allowed) {
+ // We set the tabsintitlebar attribute first so that our CSS for
+ // tabsintitlebar manifests before we do our measurements.
+ document.documentElement.setAttribute("tabsintitlebar", "true");
+ updateTitlebarDisplay();
+
+ // Try to avoid reflows in this code by calculating dimensions first and
+ // then later set the properties affecting layout together in a batch.
+
+ // Get the full height of the tabs toolbar:
+ let tabsToolbar = $("TabsToolbar");
+ let tabsStyles = window.getComputedStyle(tabsToolbar);
+ let fullTabsHeight = rect(tabsToolbar).height + verticalMargins(tabsStyles);
+ // Buttons first:
+ let captionButtonsBoxWidth = rect($("titlebar-buttonbox-container")).width;
+
+ let secondaryButtonsWidth, menuHeight, fullMenuHeight, menuStyles;
+ if (AppConstants.platform == "macosx") {
+ secondaryButtonsWidth = rect($("titlebar-secondary-buttonbox")).width;
+ // No need to look up the menubar stuff on OS X:
+ menuHeight = 0;
+ fullMenuHeight = 0;
+ } else {
+ // Otherwise, get the height and margins separately for the menubar
+ menuHeight = rect(menubar).height;
+ menuStyles = window.getComputedStyle(menubar);
+ fullMenuHeight = verticalMargins(menuStyles) + menuHeight;
+ }
+
+ // And get the height of what's in the titlebar:
+ let titlebarContentHeight = rect(titlebarContent).height;
+
+ // Begin setting CSS properties which will cause a reflow
+
+ // If the menubar is around (menuHeight is non-zero), try to adjust
+ // its full height (i.e. including margins) to match the titlebar,
+ // by changing the menubar's bottom padding
+ if (menuHeight) {
+ // Calculate the difference between the titlebar's height and that of the menubar
+ let menuTitlebarDelta = titlebarContentHeight - fullMenuHeight;
+ let paddingBottom;
+ // The titlebar is bigger:
+ if (menuTitlebarDelta > 0) {
+ fullMenuHeight += menuTitlebarDelta;
+ // If there is already padding on the menubar, we need to add that
+ // to the difference so the total padding is correct:
+ if ((paddingBottom = menuStyles.paddingBottom)) {
+ menuTitlebarDelta += parseFloat(paddingBottom);
+ }
+ menubar.style.paddingBottom = menuTitlebarDelta + "px";
+ // The menubar is bigger, but has bottom padding we can remove:
+ } else if (menuTitlebarDelta < 0 && (paddingBottom = menuStyles.paddingBottom)) {
+ let existingPadding = parseFloat(paddingBottom);
+ // menuTitlebarDelta is negative; work out what's left, but don't set negative padding:
+ let desiredPadding = Math.max(0, existingPadding + menuTitlebarDelta);
+ menubar.style.paddingBottom = desiredPadding + "px";
+ // We've changed the menu height now:
+ fullMenuHeight += desiredPadding - existingPadding;
+ }
+ }
+
+ // Next, we calculate how much we need to stretch the titlebar down to
+ // go all the way to the bottom of the tab strip, if necessary.
+ let tabAndMenuHeight = fullTabsHeight + fullMenuHeight;
+
+ if (tabAndMenuHeight > titlebarContentHeight) {
+ // We need to increase the titlebar content's outer height (ie including margins)
+ // to match the tab and menu height:
+ let extraMargin = tabAndMenuHeight - titlebarContentHeight;
+ if (AppConstants.platform != "macosx") {
+ titlebarContent.style.marginBottom = extraMargin + "px";
+ }
+
+ titlebarContentHeight += extraMargin;
+ } else {
+ titlebarContent.style.removeProperty("margin-bottom");
+ }
+
+ // Then add a negative margin to the titlebar, so that the following elements
+ // will overlap it by the lesser of the titlebar height or the tabstrip+menu.
+ let minTitlebarOrTabsHeight = Math.min(titlebarContentHeight, tabAndMenuHeight);
+ titlebar.style.marginBottom = "-" + minTitlebarOrTabsHeight + "px";
+
+ // Finally, size the placeholders:
+ if (AppConstants.platform == "macosx") {
+ this._sizePlaceholder("fullscreen-button", secondaryButtonsWidth);
+ }
+ this._sizePlaceholder("caption-buttons", captionButtonsBoxWidth);
+
+ } else {
+ document.documentElement.removeAttribute("tabsintitlebar");
+ updateTitlebarDisplay();
+
+ if (AppConstants.platform == "macosx") {
+ let secondaryButtonsWidth = rect($("titlebar-secondary-buttonbox")).width;
+ this._sizePlaceholder("fullscreen-button", secondaryButtonsWidth);
+ }
+
+ // Reset the margins and padding that might have been modified:
+ titlebarContent.style.marginTop = "";
+ titlebarContent.style.marginBottom = "";
+ titlebar.style.marginBottom = "";
+ menubar.style.paddingBottom = "";
+ }
+
+ ToolbarIconColor.inferFromText();
+ if (CustomizationHandler.isCustomizing()) {
+ gCustomizeMode.updateLWTStyling();
+ }
+ },
+
+ _sizePlaceholder: function (type, width) {
+ Array.forEach(document.querySelectorAll(".titlebar-placeholder[type='"+ type +"']"),
+ function (node) { node.width = width; });
+ },
+
+ uninit: function () {
+ this._initialized = false;
+ removeEventListener("resolutionchange", this);
+ Services.prefs.removeObserver(this._prefName, this);
+ this._menuObserver.disconnect();
+ CustomizableUI.removeListener(this);
+ }
+};
+
+function updateTitlebarDisplay() {
+ if (AppConstants.platform == "macosx") {
+ // OS X and the other platforms differ enough to necessitate this kind of
+ // special-casing. Like the other platforms where we CAN_DRAW_IN_TITLEBAR,
+ // we draw in the OS X titlebar when putting the tabs up there. However, OS X
+ // also draws in the titlebar when a lightweight theme is applied, regardless
+ // of whether or not the tabs are drawn in the titlebar.
+ if (TabsInTitlebar.enabled) {
+ document.documentElement.setAttribute("chromemargin-nonlwtheme", "0,-1,-1,-1");
+ document.documentElement.setAttribute("chromemargin", "0,-1,-1,-1");
+ document.documentElement.removeAttribute("drawtitle");
+ } else {
+ // We set chromemargin-nonlwtheme to "" instead of removing it as a way of
+ // making sure that LightweightThemeConsumer doesn't take it upon itself to
+ // detect this value again if and when we do a lwtheme state change.
+ document.documentElement.setAttribute("chromemargin-nonlwtheme", "");
+ let isCustomizing = document.documentElement.hasAttribute("customizing");
+ let hasLWTheme = document.documentElement.hasAttribute("lwtheme");
+ let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
+ if ((!hasLWTheme || isCustomizing) && !isPrivate) {
+ document.documentElement.removeAttribute("chromemargin");
+ }
+ document.documentElement.setAttribute("drawtitle", "true");
+ }
+ } else if (TabsInTitlebar.enabled) {
+ // not OS X
+ document.documentElement.setAttribute("chromemargin", "0,2,2,2");
+ } else {
+ document.documentElement.removeAttribute("chromemargin");
+ }
+}
+
+function onTitlebarMaxClick() {
+ if (window.windowState == window.STATE_MAXIMIZED)
+ window.restore();
+ else
+ window.maximize();
+}
diff --git a/browser/base/content/browser-thumbnails.js b/browser/base/content/browser-thumbnails.js
new file mode 100644
index 000000000..ebefb193e
--- /dev/null
+++ b/browser/base/content/browser-thumbnails.js
@@ -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/. */
+
+/**
+ * Keeps thumbnails of open web pages up-to-date.
+ */
+var gBrowserThumbnails = {
+ /**
+ * Pref that controls whether we can store SSL content on disk
+ */
+ PREF_DISK_CACHE_SSL: "browser.cache.disk_cache_ssl",
+
+ _captureDelayMS: 1000,
+
+ /**
+ * Used to keep track of disk_cache_ssl preference
+ */
+ _sslDiskCacheEnabled: null,
+
+ /**
+ * Map of capture() timeouts assigned to their browsers.
+ */
+ _timeouts: null,
+
+ /**
+ * List of tab events we want to listen for.
+ */
+ _tabEvents: ["TabClose", "TabSelect"],
+
+ init: function Thumbnails_init() {
+ PageThumbs.addExpirationFilter(this);
+ gBrowser.addTabsProgressListener(this);
+ Services.prefs.addObserver(this.PREF_DISK_CACHE_SSL, this, false);
+
+ this._sslDiskCacheEnabled =
+ Services.prefs.getBoolPref(this.PREF_DISK_CACHE_SSL);
+
+ this._tabEvents.forEach(function (aEvent) {
+ gBrowser.tabContainer.addEventListener(aEvent, this, false);
+ }, this);
+
+ this._timeouts = new WeakMap();
+ },
+
+ uninit: function Thumbnails_uninit() {
+ PageThumbs.removeExpirationFilter(this);
+ gBrowser.removeTabsProgressListener(this);
+ Services.prefs.removeObserver(this.PREF_DISK_CACHE_SSL, this);
+
+ this._tabEvents.forEach(function (aEvent) {
+ gBrowser.tabContainer.removeEventListener(aEvent, this, false);
+ }, this);
+ },
+
+ handleEvent: function Thumbnails_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "scroll":
+ let browser = aEvent.currentTarget;
+ if (this._timeouts.has(browser))
+ this._delayedCapture(browser);
+ break;
+ case "TabSelect":
+ this._delayedCapture(aEvent.target.linkedBrowser);
+ break;
+ case "TabClose": {
+ this._clearTimeout(aEvent.target.linkedBrowser);
+ break;
+ }
+ }
+ },
+
+ observe: function Thumbnails_observe() {
+ this._sslDiskCacheEnabled =
+ Services.prefs.getBoolPref(this.PREF_DISK_CACHE_SSL);
+ },
+
+ filterForThumbnailExpiration:
+ function Thumbnails_filterForThumbnailExpiration(aCallback) {
+ aCallback(this._topSiteURLs);
+ },
+
+ /**
+ * State change progress listener for all tabs.
+ */
+ onStateChange: function Thumbnails_onStateChange(aBrowser, aWebProgress,
+ aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)
+ this._delayedCapture(aBrowser);
+ },
+
+ _capture: function Thumbnails_capture(aBrowser) {
+ // Only capture about:newtab top sites.
+ if (this._topSiteURLs.indexOf(aBrowser.currentURI.spec) == -1)
+ return;
+ this._shouldCapture(aBrowser, function (aResult) {
+ if (aResult) {
+ PageThumbs.captureAndStoreIfStale(aBrowser);
+ }
+ });
+ },
+
+ _delayedCapture: function Thumbnails_delayedCapture(aBrowser) {
+ if (this._timeouts.has(aBrowser))
+ clearTimeout(this._timeouts.get(aBrowser));
+ else
+ aBrowser.addEventListener("scroll", this, true);
+
+ let timeout = setTimeout(function () {
+ this._clearTimeout(aBrowser);
+ this._capture(aBrowser);
+ }.bind(this), this._captureDelayMS);
+
+ this._timeouts.set(aBrowser, timeout);
+ },
+
+ _shouldCapture: function Thumbnails_shouldCapture(aBrowser, aCallback) {
+ // Capture only if it's the currently selected tab.
+ if (aBrowser != gBrowser.selectedBrowser) {
+ aCallback(false);
+ return;
+ }
+ PageThumbs.shouldStoreThumbnail(aBrowser, aCallback);
+ },
+
+ get _topSiteURLs() {
+ return NewTabUtils.links.getLinks().reduce((urls, link) => {
+ if (link)
+ urls.push(link.url);
+ return urls;
+ }, []);
+ },
+
+ _clearTimeout: function Thumbnails_clearTimeout(aBrowser) {
+ if (this._timeouts.has(aBrowser)) {
+ aBrowser.removeEventListener("scroll", this, false);
+ clearTimeout(this._timeouts.get(aBrowser));
+ this._timeouts.delete(aBrowser);
+ }
+ }
+};
diff --git a/browser/base/content/browser-trackingprotection.js b/browser/base/content/browser-trackingprotection.js
new file mode 100644
index 000000000..cacb0522c
--- /dev/null
+++ b/browser/base/content/browser-trackingprotection.js
@@ -0,0 +1,239 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var TrackingProtection = {
+ // If the user ignores the doorhanger, we stop showing it after some time.
+ MAX_INTROS: 20,
+ PREF_ENABLED_GLOBALLY: "privacy.trackingprotection.enabled",
+ PREF_ENABLED_IN_PRIVATE_WINDOWS: "privacy.trackingprotection.pbmode.enabled",
+ enabledGlobally: false,
+ enabledInPrivateWindows: false,
+ container: null,
+ content: null,
+ icon: null,
+ activeTooltipText: null,
+ disabledTooltipText: null,
+
+ init() {
+ let $ = selector => document.querySelector(selector);
+ this.container = $("#tracking-protection-container");
+ this.content = $("#tracking-protection-content");
+ this.icon = $("#tracking-protection-icon");
+
+ this.updateEnabled();
+ Services.prefs.addObserver(this.PREF_ENABLED_GLOBALLY, this, false);
+ Services.prefs.addObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this, false);
+
+ this.activeTooltipText =
+ gNavigatorBundle.getString("trackingProtection.icon.activeTooltip");
+ this.disabledTooltipText =
+ gNavigatorBundle.getString("trackingProtection.icon.disabledTooltip");
+
+ this.enabledHistogramAdd(this.enabledGlobally);
+ this.disabledPBMHistogramAdd(!this.enabledInPrivateWindows);
+ },
+
+ uninit() {
+ Services.prefs.removeObserver(this.PREF_ENABLED_GLOBALLY, this);
+ Services.prefs.removeObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this);
+ },
+
+ observe() {
+ this.updateEnabled();
+ },
+
+ get enabled() {
+ return this.enabledGlobally ||
+ (this.enabledInPrivateWindows &&
+ PrivateBrowsingUtils.isWindowPrivate(window));
+ },
+
+ updateEnabled() {
+ this.enabledGlobally =
+ Services.prefs.getBoolPref(this.PREF_ENABLED_GLOBALLY);
+ this.enabledInPrivateWindows =
+ Services.prefs.getBoolPref(this.PREF_ENABLED_IN_PRIVATE_WINDOWS);
+ this.container.hidden = !this.enabled;
+ },
+
+ enabledHistogramAdd(value) {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+ Services.telemetry.getHistogramById("TRACKING_PROTECTION_ENABLED").add(value);
+ },
+
+ disabledPBMHistogramAdd(value) {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+ Services.telemetry.getHistogramById("TRACKING_PROTECTION_PBM_DISABLED").add(value);
+ },
+
+ eventsHistogramAdd(value) {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+ Services.telemetry.getHistogramById("TRACKING_PROTECTION_EVENTS").add(value);
+ },
+
+ shieldHistogramAdd(value) {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+ Services.telemetry.getHistogramById("TRACKING_PROTECTION_SHIELD").add(value);
+ },
+
+ onSecurityChange(state, isSimulated) {
+ if (!this.enabled) {
+ return;
+ }
+
+ // Only animate the shield if the event was not fired directly from
+ // the tabbrowser (due to a browser change).
+ if (isSimulated) {
+ this.icon.removeAttribute("animate");
+ } else {
+ this.icon.setAttribute("animate", "true");
+ }
+
+ let isBlocking = state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT;
+ let isAllowing = state & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT;
+
+ if (isBlocking) {
+ this.icon.setAttribute("tooltiptext", this.activeTooltipText);
+ this.icon.setAttribute("state", "blocked-tracking-content");
+ this.content.setAttribute("state", "blocked-tracking-content");
+
+ // Open the tracking protection introduction panel, if applicable.
+ if (this.enabledGlobally) {
+ let introCount = gPrefService.getIntPref("privacy.trackingprotection.introCount");
+ if (introCount < TrackingProtection.MAX_INTROS) {
+ gPrefService.setIntPref("privacy.trackingprotection.introCount", ++introCount);
+ gPrefService.savePrefFile(null);
+ this.showIntroPanel();
+ }
+ }
+
+ this.shieldHistogramAdd(2);
+ } else if (isAllowing) {
+ this.icon.setAttribute("tooltiptext", this.disabledTooltipText);
+ this.icon.setAttribute("state", "loaded-tracking-content");
+ this.content.setAttribute("state", "loaded-tracking-content");
+
+ this.shieldHistogramAdd(1);
+ } else {
+ this.icon.removeAttribute("tooltiptext");
+ this.icon.removeAttribute("state");
+ this.content.removeAttribute("state");
+
+ // We didn't show the shield
+ this.shieldHistogramAdd(0);
+ }
+
+ // Telemetry for state change.
+ this.eventsHistogramAdd(0);
+ },
+
+ disableForCurrentPage() {
+ // Convert document URI into the format used by
+ // nsChannelClassifier::ShouldEnableTrackingProtection.
+ // Any scheme turned into https is correct.
+ let normalizedUrl = Services.io.newURI(
+ "https://" + gBrowser.selectedBrowser.currentURI.hostPort,
+ null, null);
+
+ // Add the current host in the 'trackingprotection' consumer of
+ // the permission manager using a normalized URI. This effectively
+ // places this host on the tracking protection allowlist.
+ if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
+ PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
+ } else {
+ Services.perms.add(normalizedUrl,
+ "trackingprotection", Services.perms.ALLOW_ACTION);
+ }
+
+ // Telemetry for disable protection.
+ this.eventsHistogramAdd(1);
+
+ // Hide the control center.
+ document.getElementById("identity-popup").hidePopup();
+
+ BrowserReload();
+ },
+
+ enableForCurrentPage() {
+ // Remove the current host from the 'trackingprotection' consumer
+ // of the permission manager. This effectively removes this host
+ // from the tracking protection allowlist.
+ let normalizedUrl = Services.io.newURI(
+ "https://" + gBrowser.selectedBrowser.currentURI.hostPort,
+ null, null);
+
+ if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
+ PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
+ } else {
+ Services.perms.remove(normalizedUrl, "trackingprotection");
+ }
+
+ // Telemetry for enable protection.
+ this.eventsHistogramAdd(2);
+
+ // Hide the control center.
+ document.getElementById("identity-popup").hidePopup();
+
+ BrowserReload();
+ },
+
+ dontShowIntroPanelAgain() {
+ // This function may be called in private windows, but it does not change
+ // any preference unless Tracking Protection is enabled globally.
+ if (this.enabledGlobally) {
+ gPrefService.setIntPref("privacy.trackingprotection.introCount",
+ this.MAX_INTROS);
+ gPrefService.savePrefFile(null);
+ }
+ },
+
+ showIntroPanel: Task.async(function*() {
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+
+ let openStep2 = () => {
+ // When the user proceeds in the tour, adjust the counter to indicate that
+ // the user doesn't need to see the intro anymore.
+ this.dontShowIntroPanelAgain();
+
+ let nextURL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
+ "?step=2&newtab=true";
+ switchToTabHavingURI(nextURL, true, {
+ // Ignore the fragment in case the intro is shown on the tour page
+ // (e.g. if the user manually visited the tour or clicked the link from
+ // about:privatebrowsing) so we can avoid a reload.
+ ignoreFragment: "whenComparingAndReplace",
+ });
+ };
+
+ let buttons = [
+ {
+ label: gNavigatorBundle.getString("trackingProtection.intro.step1of3"),
+ style: "text",
+ },
+ {
+ callback: openStep2,
+ label: gNavigatorBundle.getString("trackingProtection.intro.nextButton.label"),
+ style: "primary",
+ },
+ ];
+
+ let panelTarget = yield UITour.getTarget(window, "trackingProtection");
+ UITour.initForBrowser(gBrowser.selectedBrowser, window);
+ UITour.showInfo(window, panelTarget,
+ gNavigatorBundle.getString("trackingProtection.intro.title"),
+ gNavigatorBundle.getFormattedString("trackingProtection.intro.description2",
+ [brandShortName]),
+ undefined, buttons,
+ { closeButtonCallback: () => this.dontShowIntroPanelAgain() });
+ }),
+};
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
new file mode 100644
index 000000000..a05b031b2
--- /dev/null
+++ b/browser/base/content/browser.css
@@ -0,0 +1,1244 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+@namespace html url("http://www.w3.org/1999/xhtml");
+@namespace svg url("http://www.w3.org/2000/svg");
+
+:root {
+ --identity-popup-expander-width: 38px;
+ --panelui-subview-transition-duration: 150ms;
+}
+
+#main-window:not([chromehidden~="toolbar"]) {
+%ifdef XP_MACOSX
+ min-width: 335px;
+%else
+ min-width: 300px;
+%endif
+}
+
+#main-window[customize-entered] {
+ min-width: -moz-fit-content;
+}
+
+searchbar {
+ -moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
+}
+
+/* Prevent shrinking the page content to 0 height and width */
+.browserStack > browser {
+ min-height: 25px;
+ min-width: 25px;
+}
+
+.browserStack > browser {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-browser");
+}
+
+.browserStack > browser[remote="true"] {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-remote-browser");
+}
+
+toolbar[customizable="true"] {
+ -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar");
+}
+
+%ifdef XP_MACOSX
+#toolbar-menubar {
+ -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar-menubar-stub");
+}
+%endif
+
+#toolbar-menubar[autohide="true"] {
+ -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar-menubar-autohide");
+}
+
+#addon-bar {
+ -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#addonbar-delegating");
+ visibility: visible;
+ margin: 0;
+ height: 0 !important;
+ overflow: hidden;
+ padding: 0;
+ border: 0 none;
+}
+
+#addonbar-closebutton {
+ visibility: visible;
+ height: 0 !important;
+}
+
+#status-bar {
+ height: 0 !important;
+ -moz-binding: none;
+ padding: 0;
+ margin: 0;
+}
+
+panelmultiview {
+ -moz-binding: url("chrome://browser/content/customizableui/panelUI.xml#panelmultiview");
+}
+
+panelview {
+ -moz-binding: url("chrome://browser/content/customizableui/panelUI.xml#panelview");
+ -moz-box-orient: vertical;
+}
+
+.panel-mainview {
+ transition: transform var(--panelui-subview-transition-duration);
+}
+
+panelview:not([mainview]):not([current]) {
+ transition: visibility 0s linear var(--panelui-subview-transition-duration);
+ visibility: collapse;
+}
+
+browser[frameType="social"][remote="true"] {
+ -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser");
+}
+
+tabbrowser {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser");
+}
+
+.tabbrowser-tabs {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabs");
+}
+
+#tabbrowser-tabs:not([overflow="true"]) ~ #alltabs-button,
+#tabbrowser-tabs:not([overflow="true"]) + #new-tab-button,
+#tabbrowser-tabs[overflow="true"] > .tabbrowser-arrowscrollbox > .tabs-newtab-button,
+#TabsToolbar[currentset]:not([currentset*="tabbrowser-tabs,new-tab-button"]) > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .tabs-newtab-button,
+#TabsToolbar[customizing="true"] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .tabs-newtab-button {
+ visibility: collapse;
+}
+
+#tabbrowser-tabs:not([overflow="true"])[using-closing-tabs-spacer] ~ #alltabs-button {
+ visibility: hidden; /* temporary space to keep a tab's close button under the cursor */
+}
+
+.tabs-newtab-button > .toolbarbutton-menu-dropmarker,
+#new-tab-button > .toolbarbutton-menu-dropmarker {
+ display: none;
+}
+
+/* override drop marker image padding */
+.tabs-newtab-button > .toolbarbutton-icon {
+ margin-inline-end: 0;
+}
+
+.tabbrowser-tab {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tab");
+}
+
+.tabbrowser-tab:not([pinned]) {
+ -moz-box-flex: 100;
+ max-width: 210px;
+ min-width: 100px;
+ width: 0;
+ transition: min-width 100ms ease-out,
+ max-width 100ms ease-out;
+}
+
+.tabbrowser-tab:not([pinned]):not([fadein]) {
+ max-width: 0.1px;
+ min-width: 0.1px;
+ visibility: hidden;
+}
+
+.tab-close-button,
+.tab-background {
+ /* Explicitly set the visibility to override the value (collapsed)
+ * we inherit from #TabsToolbar[collapsed] upon opening a browser window. */
+ visibility: visible;
+}
+
+.tab-close-button[fadein],
+.tab-background[fadein] {
+ /* This transition is only wanted for opening tabs. */
+ transition: visibility 0ms 25ms;
+}
+
+.tab-close-button:not([fadein]),
+.tab-background:not([fadein]) {
+ visibility: hidden;
+}
+
+.tab-label:not([fadein]),
+.tab-throbber:not([fadein]),
+.tab-icon-image:not([fadein]) {
+ display: none;
+}
+
+.tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] {
+ position: fixed !important;
+ display: block; /* position:fixed already does this (bug 579776), but let's be explicit */
+}
+
+.tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected] {
+ position: relative;
+ z-index: 2;
+ pointer-events: none; /* avoid blocking dragover events on scroll buttons */
+}
+
+.tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]) {
+ transition: transform 200ms ease-out;
+}
+
+.new-tab-popup,
+#alltabs-popup {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-alltabs-popup");
+}
+
+toolbar[printpreview="true"] {
+ -moz-binding: url("chrome://global/content/printPreviewBindings.xml#printpreviewtoolbar");
+}
+
+toolbar[overflowable] > .customization-target {
+ overflow: hidden;
+}
+
+toolbar:not([overflowing]) > .overflow-button,
+toolbar[customizing] > .overflow-button {
+ display: none;
+}
+
+%ifdef CAN_DRAW_IN_TITLEBAR
+#main-window:not([chromemargin]) > #titlebar,
+#main-window[inFullscreen] > #titlebar,
+#main-window[inFullscreen] .titlebar-placeholder,
+#main-window:not([tabsintitlebar]) .titlebar-placeholder {
+ display: none;
+}
+
+#titlebar {
+ -moz-binding: url("chrome://global/content/bindings/general.xml#windowdragbox");
+ -moz-window-dragging: drag;
+}
+
+#titlebar-spacer {
+ pointer-events: none;
+}
+
+#main-window[tabsintitlebar] #titlebar-buttonbox {
+ position: relative;
+}
+
+#titlebar-buttonbox {
+ -moz-appearance: -moz-window-button-box;
+}
+
+#personal-bookmarks {
+ -moz-window-dragging: inherit;
+}
+
+%ifdef XP_MACOSX
+#titlebar-fullscreen-button {
+ -moz-appearance: -moz-mac-fullscreen-button;
+}
+
+/* Fullscreen and caption buttons don't move with RTL on OS X so override the automatic ordering. */
+#titlebar-secondary-buttonbox:-moz-locale-dir(ltr),
+#titlebar-buttonbox-container:-moz-locale-dir(rtl),
+.titlebar-placeholder[type="fullscreen-button"]:-moz-locale-dir(ltr),
+.titlebar-placeholder[type="caption-buttons"]:-moz-locale-dir(rtl) {
+ -moz-box-ordinal-group: 1000;
+}
+
+#titlebar-secondary-buttonbox:-moz-locale-dir(rtl),
+#titlebar-buttonbox-container:-moz-locale-dir(ltr),
+.titlebar-placeholder[type="caption-buttons"]:-moz-locale-dir(ltr),
+.titlebar-placeholder[type="fullscreen-button"]:-moz-locale-dir(rtl) {
+ -moz-box-ordinal-group: 0;
+}
+
+%else
+/* On non-OSX, these should be start-aligned */
+#titlebar-buttonbox-container {
+ -moz-box-align: start;
+}
+%endif
+
+%if !defined(MOZ_WIDGET_GTK)
+#TabsToolbar > .private-browsing-indicator {
+ -moz-box-ordinal-group: 1000;
+}
+%endif
+
+%ifdef XP_WIN
+#main-window[sizemode="maximized"] #titlebar-buttonbox {
+ -moz-appearance: -moz-window-button-box-maximized;
+}
+
+#main-window[tabletmode] #titlebar-min,
+#main-window[tabletmode] #titlebar-max {
+ display: none !important;
+}
+
+#main-window[tabsintitlebar] #TabsToolbar,
+#main-window[tabsintitlebar] #toolbar-menubar,
+#main-window[tabsintitlebar] #navigator-toolbox > toolbar:-moz-lwtheme {
+ -moz-window-dragging: drag;
+}
+%endif
+
+%endif
+
+#main-window[inFullscreen][inDOMFullscreen] #navigator-toolbox,
+#main-window[inFullscreen][inDOMFullscreen] #fullscr-toggler,
+#main-window[inFullscreen][inDOMFullscreen] #sidebar-box,
+#main-window[inFullscreen][inDOMFullscreen] #sidebar-splitter,
+#main-window[inFullscreen]:not([OSXLionFullscreen]) toolbar:not([fullscreentoolbar=true]),
+#main-window[inFullscreen] #global-notificationbox,
+#main-window[inFullscreen] #high-priority-global-notificationbox {
+ visibility: collapse;
+}
+
+#navigator-toolbox[fullscreenShouldAnimate] {
+ transition: 1.5s margin-top ease-out;
+}
+
+/* Rules to help integrate SDK widgets */
+toolbaritem[sdkstylewidget="true"] > toolbarbutton,
+toolbarpaletteitem > toolbaritem[sdkstylewidget="true"] > iframe,
+toolbarpaletteitem > toolbaritem[sdkstylewidget="true"] > .toolbarbutton-text {
+ display: none;
+}
+
+toolbarpaletteitem:-moz-any([place="palette"], [place="panel"]) > toolbaritem[sdkstylewidget="true"] > toolbarbutton {
+ display: -moz-box;
+}
+
+toolbarpaletteitem > toolbaritem[sdkstylewidget="true"][cui-areatype="toolbar"] > .toolbarbutton-text {
+ display: -moz-box;
+}
+
+@media not all and (min-resolution: 1.1dppx) {
+ .webextension-browser-action {
+ list-style-image: var(--webextension-toolbar-image);
+ }
+
+ .webextension-browser-action[cui-areatype="menu-panel"],
+ toolbarpaletteitem[place="palette"] > .webextension-browser-action {
+ list-style-image: var(--webextension-menupanel-image);
+ }
+
+ .webextension-page-action {
+ list-style-image: var(--webextension-urlbar-image);
+ }
+}
+
+@media (min-resolution: 1.1dppx) {
+ .webextension-browser-action {
+ list-style-image: var(--webextension-toolbar-image-2x);
+ }
+
+ .webextension-browser-action[cui-areatype="menu-panel"],
+ toolbarpaletteitem[place="palette"] > .webextension-browser-action {
+ list-style-image: var(--webextension-menupanel-image-2x);
+ }
+
+ .webextension-page-action {
+ list-style-image: var(--webextension-urlbar-image-2x);
+ }
+}
+
+toolbarpaletteitem[removable="false"] {
+ opacity: 0.5;
+ cursor: default;
+}
+
+%ifndef XP_MACOSX
+toolbarpaletteitem[place="palette"],
+toolbarpaletteitem[place="panel"],
+toolbarpaletteitem[place="toolbar"] {
+ -moz-user-focus: normal;
+}
+%endif
+
+#bookmarks-toolbar-placeholder,
+toolbarpaletteitem > #personal-bookmarks > #PlacesToolbar,
+#personal-bookmarks[cui-areatype="menu-panel"] > #PlacesToolbar,
+#personal-bookmarks[cui-areatype="toolbar"][overflowedItem=true] > #PlacesToolbar {
+ display: none;
+}
+
+#PlacesToolbarDropIndicatorHolder {
+ position: absolute;
+ top: 25%;
+}
+
+toolbarpaletteitem > #personal-bookmarks > #bookmarks-toolbar-placeholder,
+#personal-bookmarks[cui-areatype="menu-panel"] > #bookmarks-toolbar-placeholder,
+#personal-bookmarks[cui-areatype="toolbar"][overflowedItem=true] > #bookmarks-toolbar-placeholder {
+ display: -moz-box;
+}
+
+#nav-bar-customization-target > #personal-bookmarks,
+toolbar:not(#TabsToolbar) > #wrapper-personal-bookmarks,
+toolbar:not(#TabsToolbar) > #personal-bookmarks {
+ -moz-box-flex: 1;
+}
+
+#zoom-controls[cui-areatype="toolbar"]:not([overflowedItem=true]) > #zoom-reset-button > .toolbarbutton-text {
+ display: -moz-box;
+}
+
+#urlbar-reload-button:not([displaystop]) + #urlbar-stop-button,
+#urlbar-reload-button[displaystop] {
+ visibility: collapse;
+}
+
+#PanelUI-feeds > .feed-toolbarbutton:-moz-locale-dir(rtl) {
+ direction: rtl;
+}
+
+#panelMenu_bookmarksMenu > .bookmark-item {
+ max-width: none;
+}
+
+#urlbar-container {
+ min-width: 50ch;
+}
+
+#search-container {
+ min-width: 25ch;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ .searchbar-engine-image {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+#urlbar,
+.searchbar-textbox {
+ /* Setting a width and min-width to let the location & search bars maintain
+ a constant width in case they haven't be resized manually. (bug 965772) */
+ width: 1px;
+ min-width: 1px;
+}
+
+#main-window:-moz-lwtheme {
+ background-repeat: no-repeat;
+ background-position: top right;
+}
+
+%ifdef XP_MACOSX
+#main-window[inFullscreen="true"] {
+ padding-top: 0; /* override drawintitlebar="true" */
+}
+%endif
+
+#browser-bottombox[lwthemefooter="true"] {
+ background-repeat: no-repeat;
+ background-position: bottom left;
+}
+
+.menuitem-tooltip {
+ -moz-binding: url("chrome://browser/content/urlbarBindings.xml#menuitem-tooltip");
+}
+
+.menuitem-iconic-tooltip,
+.menuitem-tooltip[type="checkbox"],
+.menuitem-tooltip[type="radio"] {
+ -moz-binding: url("chrome://browser/content/urlbarBindings.xml#menuitem-iconic-tooltip");
+}
+
+/* Hide menu elements intended for keyboard access support */
+#main-menubar[openedwithkey=false] .show-only-for-keyboard {
+ display: none;
+}
+
+/* ::::: location bar ::::: */
+#urlbar {
+ -moz-binding: url(chrome://browser/content/urlbarBindings.xml#urlbar);
+}
+
+/* Always show URLs LTR. */
+.ac-url-text:-moz-locale-dir(rtl),
+.ac-title-text[lookslikeurl]:-moz-locale-dir(rtl) {
+ direction: ltr !important;
+}
+
+/* For non-action items, hide the action text; for action items, hide the URL
+ text. */
+.ac-url[actiontype],
+.ac-action:not([actiontype]) {
+ display: none;
+}
+
+/* For action items in a noactions popup, show the URL text and hide the action
+ text and type icon. */
+#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-url {
+ display: -moz-box;
+}
+#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-action {
+ display: none;
+}
+#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-type-icon {
+ list-style-image: none;
+}
+
+#urlbar:not([actiontype="switchtab"]):not([actiontype="extension"]) > #urlbar-display-box {
+ display: none;
+}
+
+#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box > #switchtab {
+ display: none;
+}
+
+#urlbar:not([actiontype="extension"]) > #urlbar-display-box > #extension {
+ display: none;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem > .ac-type-icon,
+#PopupAutoComplete > richlistbox > richlistitem > .ac-site-icon,
+#PopupAutoComplete > richlistbox > richlistitem > .ac-tags,
+#PopupAutoComplete > richlistbox > richlistitem > .ac-separator,
+#PopupAutoComplete > richlistbox > richlistitem > .ac-url {
+ display: none;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] {
+ -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem-insecure-field");
+ height: auto;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-site-icon {
+ display: initial;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > .ac-text-overflow-container > .ac-title-text {
+ text-overflow: initial;
+ white-space: initial;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > label {
+ margin-inline-start: 0;
+}
+
+#PopupSearchAutoComplete {
+ -moz-binding: url("chrome://browser/content/search/search.xml#browser-search-autocomplete-result-popup");
+}
+
+/* Overlay a badge on top of the icon of additional open search providers
+ in the search panel. */
+.addengine-item > .button-box > .button-icon {
+ -moz-binding: url("chrome://browser/content/search/search.xml#addengine-icon");
+ display: -moz-stack;
+}
+
+#PopupAutoCompleteRichResult {
+ -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup");
+}
+
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification {
+ transition: height 100ms;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] {
+ visibility: collapse;
+ transition: margin-top 100ms;
+}
+
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > hbox[anonid="search-suggestions-notification"] {
+ visibility: visible;
+}
+
+#PopupAutoCompleteRichResult > richlistbox {
+ transition: height 100ms;
+}
+
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > richlistbox {
+ transition: none;
+}
+
+#DateTimePickerPanel {
+ -moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup");
+}
+
+#urlbar[pageproxystate="invalid"] > #urlbar-icons > .urlbar-icon,
+#urlbar[pageproxystate="invalid"][focused="true"] > #urlbar-go-button ~ toolbarbutton,
+#urlbar[pageproxystate="valid"] > #urlbar-go-button,
+#urlbar:not([focused="true"]) > #urlbar-go-button {
+ visibility: collapse;
+}
+
+#urlbar[pageproxystate="invalid"] > #identity-box > #identity-icon-labels {
+ visibility: collapse;
+}
+
+#identity-box {
+ -moz-user-focus: normal;
+}
+
+#urlbar[pageproxystate="invalid"] > #identity-box {
+ pointer-events: none;
+ -moz-user-focus: ignore;
+}
+
+#urlbar[pageproxystate="invalid"] > #identity-box > #notification-popup-box {
+ pointer-events: auto;
+}
+
+#identity-icon-labels {
+ max-width: 18em;
+}
+@media (max-width: 700px) {
+ #urlbar-container {
+ min-width: 45ch;
+ }
+ #identity-icon-labels {
+ max-width: 70px;
+ }
+}
+@media (max-width: 600px) {
+ #urlbar-container {
+ min-width: 40ch;
+ }
+ #identity-icon-labels {
+ max-width: 60px;
+ }
+}
+@media (max-width: 500px) {
+ #urlbar-container {
+ min-width: 35ch;
+ }
+ #identity-icon-labels {
+ max-width: 50px;
+ }
+}
+@media (max-width: 400px) {
+ #urlbar-container {
+ min-width: 28ch;
+ }
+ #identity-icon-labels {
+ max-width: 40px;
+ }
+}
+
+#identity-icon-country-label {
+ direction: ltr;
+}
+
+#identity-box.verifiedIdentity > #identity-icon-labels > #identity-icon-label {
+ margin-inline-end: 0.25em !important;
+}
+
+#main-window[customizing] :-moz-any(#urlbar, .searchbar-textbox) > .autocomplete-textbox-container > .textbox-input-box {
+ visibility: hidden;
+}
+
+/* ::::: Unified Back-/Forward Button ::::: */
+#back-button > .toolbarbutton-menu-dropmarker,
+#forward-button > .toolbarbutton-menu-dropmarker {
+ display: none;
+}
+.unified-nav-current {
+ font-weight: bold;
+}
+
+toolbarbutton.bookmark-item {
+ max-width: 13em;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ #alltabs-popup > .menuitem-iconic > .menu-iconic-left > .menu-iconic-icon,
+ .menuitem-with-favicon > .menu-iconic-left > .menu-iconic-icon {
+ image-rendering: -moz-crisp-edges;
+ }
+
+ .bookmark-item > .toolbarbutton-icon,
+ .bookmark-item > .menu-iconic-left > .menu-iconic-icon,
+ #personal-bookmarks[cui-areatype="toolbar"] > #bookmarks-toolbar-placeholder > .toolbarbutton-icon {
+ image-rendering: -moz-crisp-edges;
+ }
+ /* Synced Tabs sidebar */
+ html|*.tabs-container html|*.item-tabs-list html|*.item-icon-container {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+#editBMPanel_tagsSelector {
+ /* override default listbox width from xul.css */
+ width: auto;
+}
+
+menupopup[emptyplacesresult="true"] > .hide-if-empty-places-result {
+ display: none;
+}
+menuitem.spell-suggestion {
+ font-weight: bold;
+}
+
+/* Hide extension toolbars that neglected to set the proper class */
+window[chromehidden~="location"][chromehidden~="toolbar"] toolbar:not(.chromeclass-menubar),
+window[chromehidden~="toolbar"] toolbar:not(#nav-bar):not(#TabsToolbar):not(#print-preview-toolbar):not(.chromeclass-menubar) {
+ display: none;
+}
+
+#navigator-toolbox ,
+#mainPopupSet {
+ min-width: 1px;
+}
+
+/* History Swipe Animation */
+
+#historySwipeAnimationContainer {
+ overflow: hidden;
+}
+
+#historySwipeAnimationPreviousPage,
+#historySwipeAnimationCurrentPage,
+#historySwipeAnimationNextPage {
+ background: none top left no-repeat white;
+}
+
+#historySwipeAnimationPreviousPage {
+ background-image: -moz-element(#historySwipeAnimationPreviousPageSnapshot);
+}
+
+#historySwipeAnimationCurrentPage {
+ background-image: -moz-element(#historySwipeAnimationCurrentPageSnapshot);
+}
+
+#historySwipeAnimationNextPage {
+ background-image: -moz-element(#historySwipeAnimationNextPageSnapshot);
+}
+
+/* Full Screen UI */
+
+#fullscr-toggler {
+ height: 1px;
+ background: black;
+}
+
+html|*.pointerlockfswarning {
+ position: fixed;
+ z-index: 2147483647 !important;
+ visibility: visible;
+ transition: transform 300ms ease-in;
+ /* To center the warning box horizontally,
+ we use left: 50% with translateX(-50%). */
+ top: 0; left: 50%;
+ transform: translate(-50%, -100%);
+ box-sizing: border-box;
+ width: -moz-max-content;
+ max-width: 95%;
+ pointer-events: none;
+}
+html|*.pointerlockfswarning:not([hidden]) {
+ display: flex;
+ will-change: transform;
+}
+html|*.pointerlockfswarning[onscreen] {
+ transform: translate(-50%, 50px);
+}
+html|*.pointerlockfswarning[ontop] {
+ /* Use -10px to hide the border and border-radius on the top */
+ transform: translate(-50%, -10px);
+}
+#main-window[OSXLionFullscreen] html|*.pointerlockfswarning[ontop] {
+ transform: translate(-50%, 80px);
+}
+
+html|*.pointerlockfswarning-domain-text,
+html|*.pointerlockfswarning-generic-text {
+ word-wrap: break-word;
+ /* We must specify a min-width, otherwise word-wrap:break-word doesn't work. Bug 630864. */
+ min-width: 1px
+}
+html|*.pointerlockfswarning-domain-text:not([hidden]) + html|*.pointerlockfswarning-generic-text {
+ display: none;
+}
+
+html|*#fullscreen-exit-button {
+ pointer-events: auto;
+}
+
+/* ::::: Ctrl-Tab Panel ::::: */
+
+.ctrlTab-preview > html|img,
+.ctrlTab-preview > html|canvas {
+ min-width: inherit;
+ max-width: inherit;
+ min-height: inherit;
+ max-height: inherit;
+}
+
+.ctrlTab-favicon-container {
+ -moz-box-align: start;
+%ifdef XP_MACOSX
+ -moz-box-pack: end;
+%else
+ -moz-box-pack: start;
+%endif
+}
+
+.ctrlTab-favicon {
+ width: 16px;
+ height: 16px;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ .ctrlTab-favicon {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+.ctrlTab-preview {
+ -moz-binding: url("chrome://browser/content/browser-tabPreviews.xml#ctrlTab-preview");
+}
+
+
+/* notification anchors should only be visible when their associated
+ notifications are */
+.notification-anchor-icon {
+ -moz-user-focus: normal;
+}
+
+#blocked-permissions-container > .blocked-permission-icon:not([showing]),
+.notification-anchor-icon:not([showing]) {
+ display: none;
+}
+
+#invalid-form-popup > description {
+ max-width: 280px;
+}
+
+.popup-anchor {
+ /* should occupy space but not be visible */
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ -moz-stack-sizing: ignore;
+}
+
+#addon-progress-notification {
+ -moz-binding: url("chrome://browser/content/urlbarBindings.xml#addon-progress-notification");
+}
+
+#click-to-play-plugins-notification {
+ -moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification");
+}
+
+
+.plugin-popupnotification-centeritem {
+ -moz-binding: url("chrome://browser/content/urlbarBindings.xml#plugin-popupnotification-center-item");
+}
+
+browser[tabmodalPromptShowing] {
+ -moz-user-focus: none !important;
+}
+
+/* Status panel */
+
+statuspanel {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#statuspanel");
+ position: fixed;
+ margin-top: -3em;
+ max-width: calc(100% - 5px);
+ pointer-events: none;
+}
+
+statuspanel:-moz-locale-dir(ltr)[mirror],
+statuspanel:-moz-locale-dir(rtl):not([mirror]) {
+ left: auto;
+ right: 0;
+}
+
+statuspanel[sizelimit] {
+ max-width: 50%;
+}
+
+statuspanel[type=status] {
+ min-width: 23em;
+}
+
+@media all and (max-width: 800px) {
+ statuspanel[type=status] {
+ min-width: 33%;
+ }
+}
+
+statuspanel[type=overLink] {
+ transition: opacity 120ms ease-out;
+ direction: ltr;
+}
+
+statuspanel[inactive] {
+ transition: none;
+ opacity: 0;
+}
+
+statuspanel[inactive][previoustype=overLink] {
+ transition: opacity 200ms ease-out;
+}
+
+.statuspanel-inner {
+ height: 3em;
+ width: 100%;
+ -moz-box-align: end;
+}
+
+/* gcli */
+
+html|*#gcli-tooltip-frame,
+html|*#gcli-output-frame,
+#gcli-output,
+#gcli-tooltip {
+ overflow-x: hidden;
+}
+
+.gclitoolbar-input-node,
+.gclitoolbar-complete-node {
+ direction: ltr;
+}
+
+#developer-toolbar-toolbox-button[error-count] > .toolbarbutton-icon {
+ display: none;
+}
+
+#developer-toolbar-toolbox-button[error-count]:before {
+ content: attr(error-count);
+ display: -moz-box;
+ -moz-box-pack: center;
+}
+
+/* Responsive Mode */
+
+.browserContainer[responsivemode] {
+ overflow: auto;
+}
+
+.devtools-responsiveui-toolbar:-moz-locale-dir(rtl) {
+ -moz-box-pack: end;
+}
+
+.browserStack[responsivemode] {
+ transition-duration: 200ms;
+ transition-timing-function: linear;
+}
+
+.browserStack[responsivemode] {
+ transition-property: min-width, max-width, min-height, max-height;
+}
+
+.browserStack[responsivemode][notransition] {
+ transition: none;
+}
+
+panelview > .social-panel-frame {
+ width: auto;
+ height: auto;
+}
+
+/* Translation */
+notification[value="translation"] {
+ -moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar");
+}
+
+/** See bug 872317 for why the following rule is necessary. */
+
+#downloads-button {
+ -moz-binding: url("chrome://browser/content/downloads/download.xml#download-toolbarbutton");
+}
+
+/*** Visibility of downloads indicator controls ***/
+
+/* Bug 924050: If we've loaded the indicator, for now we hide it in the menu panel,
+ and just show the icon. This is a hack to side-step very weird layout bugs that
+ seem to be caused by the indicator stack interacting with the menu panel. */
+#downloads-button[indicator]:not([cui-areatype="menu-panel"]) > .toolbarbutton-badge-stack > image.toolbarbutton-icon,
+#downloads-button[indicator][cui-areatype="menu-panel"] > #downloads-indicator-anchor {
+ display: none;
+}
+
+toolbarpaletteitem[place="palette"] > #downloads-button[indicator] > .toolbarbutton-badge-stack > image.toolbarbutton-icon {
+ display: -moz-box;
+}
+
+toolbarpaletteitem[place="palette"] > #downloads-button[indicator] > #downloads-indicator-anchor {
+ display: none;
+}
+
+#downloads-button:-moz-any([progress], [counter], [paused]) #downloads-indicator-icon,
+#downloads-button:not(:-moz-any([progress], [counter], [paused]))
+ #downloads-indicator-progress-area
+{
+ visibility: hidden;
+}
+
+/* Combobox dropdown renderer */
+#ContentSelectDropdown > menupopup {
+ /* The menupopup itself should always be rendered LTR to ensure the scrollbar aligns with
+ * the dropdown arrow on the dropdown widget. If a menuitem is RTL, its style will be set accordingly */
+ direction: ltr;
+}
+
+/* Indent options in optgroups */
+.contentSelectDropdown-ingroup .menu-iconic-text {
+ padding-inline-start: 2em;
+}
+
+/* Give this menupopup an arrow panel styling */
+#BMB_bookmarksPopup {
+ -moz-appearance: none;
+ -moz-binding: url("chrome://browser/content/places/menu.xml#places-popup-arrow");
+ background: transparent;
+ border: none;
+ /* The popup inherits -moz-image-region from the button, must reset it */
+ -moz-image-region: auto;
+}
+
+%ifndef MOZ_WIDGET_GTK
+
+#BMB_bookmarksPopup {
+ transform: scale(.4);
+ opacity: 0;
+ transition-property: transform, opacity;
+ transition-duration: 0.15s;
+ transition-timing-function: ease-out;
+}
+
+#BMB_bookmarksPopup[animate="open"] {
+ transform: none;
+ opacity: 1.0;
+}
+
+#BMB_bookmarksPopup[animate="cancel"] {
+ transform: none;
+}
+
+#BMB_bookmarksPopup[arrowposition="after_start"]:-moz-locale-dir(ltr),
+#BMB_bookmarksPopup[arrowposition="after_end"]:-moz-locale-dir(rtl) {
+ transform-origin: 20px top;
+}
+
+#BMB_bookmarksPopup[arrowposition="after_end"]:-moz-locale-dir(ltr),
+#BMB_bookmarksPopup[arrowposition="after_start"]:-moz-locale-dir(rtl) {
+ transform-origin: calc(100% - 20px) top;
+}
+
+#BMB_bookmarksPopup[arrowposition="before_start"]:-moz-locale-dir(ltr),
+#BMB_bookmarksPopup[arrowposition="before_end"]:-moz-locale-dir(rtl) {
+ transform-origin: 20px bottom;
+}
+
+#BMB_bookmarksPopup[arrowposition="before_end"]:-moz-locale-dir(ltr),
+#BMB_bookmarksPopup[arrowposition="before_start"]:-moz-locale-dir(rtl) {
+ transform-origin: calc(100% - 20px) bottom;
+}
+
+%endif
+
+/* Customize mode */
+#navigator-toolbox,
+#browser-bottombox,
+#content-deck {
+ transition-property: margin-left, margin-right;
+ transition-duration: 200ms;
+ transition-timing-function: linear;
+}
+
+#tab-view-deck[fastcustomizeanimation] #navigator-toolbox,
+#tab-view-deck[fastcustomizeanimation] #content-deck {
+ transition-duration: 1ms;
+ transition-timing-function: linear;
+}
+
+#PanelUI-contents > .panel-customization-placeholder > .panel-customization-placeholder-child {
+ list-style-image: none;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ #PanelUI-remotetabs-tabslist > toolbarbutton > .toolbarbutton-icon,
+ #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
+ #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
+ #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+#customization-panelHolder {
+ overflow-y: hidden;
+}
+
+#customization-panelWrapper,
+#customization-panelWrapper > .panel-arrowcontent {
+ -moz-box-flex: 1;
+}
+
+#customization-panelWrapper > .panel-arrowcontent {
+ padding: 0 !important;
+ overflow: hidden;
+}
+
+#customization-panelHolder > #PanelUI-mainView {
+ display: flex;
+ flex-direction: column;
+ /* Hack alert - by manually setting the preferred height to 0, we convince
+ #PanelUI-mainView to shrink when the window gets smaller in customization
+ mode. Not sure why that is - might have to do with our intermingling of
+ XUL flex, and CSS3 Flexbox. */
+ height: 0;
+}
+
+#customization-panelHolder > #PanelUI-mainView > #PanelUI-contents-scroller {
+ display: flex;
+ flex: auto;
+ flex-direction: column;
+}
+
+#customization-panel-container {
+ overflow-y: auto;
+}
+
+toolbarpaletteitem[dragover] {
+ border-left-color: transparent;
+ border-right-color: transparent;
+}
+
+#customization-palette-container {
+ display: flex;
+ flex-direction: column;
+}
+
+#customization-palette:not([hidden]) {
+ display: block;
+ flex: 1 1 auto;
+ overflow: auto;
+ min-height: 3em;
+}
+
+#customization-footer-spacer,
+#customization-spacer {
+ flex: 1 1 auto;
+}
+
+#customization-footer {
+ display: flex;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+
+#customization-toolbar-visibility-button > .box-inherit > .button-menu-dropmarker {
+ display: -moz-box;
+}
+
+toolbarpaletteitem[place="palette"] {
+ width: 10em;
+ /* icon (32) + margin (2 * 4) + button padding/border (2 * 4) + label margin (~2) + label
+ * line-height (1.5em): */
+ height: calc(50px + 1.5em);
+ margin-bottom: 5px;
+ overflow: hidden;
+ display: inline-block;
+}
+
+toolbarpaletteitem[place="palette"][hidden] {
+ display: none;
+}
+
+#customization-palette .toolbarpaletteitem-box {
+ -moz-box-pack: center;
+ -moz-box-flex: 1;
+ width: 10em;
+ max-width: 10em;
+}
+
+#main-window[customizing=true] #PanelUI-update-status {
+ display: none;
+}
+
+/* UI Tour */
+
+@keyframes uitour-wobble {
+ from {
+ transform: rotate(0deg) translateX(3px) rotate(0deg);
+ }
+ 50% {
+ transform: rotate(360deg) translateX(3px) rotate(-360deg);
+ }
+ to {
+ transform: rotate(720deg) translateX(0px) rotate(-720deg);
+ }
+}
+
+@keyframes uitour-zoom {
+ from {
+ transform: scale(0.8);
+ }
+ 50% {
+ transform: scale(1.0);
+ }
+ to {
+ transform: scale(0.8);
+ }
+}
+
+@keyframes uitour-color {
+ from {
+ border-color: #5B9CD9;
+ }
+ 50% {
+ border-color: #FF0000;
+ }
+ to {
+ border-color: #5B9CD9;
+ }
+}
+
+#UITourHighlightContainer,
+#UITourHighlight {
+ pointer-events: none;
+}
+
+#UITourHighlight[active] {
+ animation-delay: 2s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+}
+
+#UITourHighlight[active="wobble"] {
+ animation-name: uitour-wobble;
+ animation-delay: 0s;
+ animation-duration: 1.5s;
+ animation-iteration-count: 1;
+}
+#UITourHighlight[active="zoom"] {
+ animation-name: uitour-zoom;
+ animation-duration: 1s;
+}
+#UITourHighlight[active="color"] {
+ animation-name: uitour-color;
+ animation-duration: 2s;
+}
+
+/* Combined context-menu items */
+#context-navigation > .menuitem-iconic > .menu-iconic-text,
+#context-navigation > .menuitem-iconic > .menu-accel-container {
+ display: none;
+}
+
+.popup-notification-invalid-input {
+ box-shadow: 0 0 1.5px 1px red;
+}
+
+.popup-notification-invalid-input[focused] {
+ box-shadow: 0 0 2px 2px rgba(255,0,0,0.4);
+}
+
+.dragfeedback-tab {
+ -moz-appearance: none;
+ opacity: 0.65;
+ -moz-window-shadow: none;
+}
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
new file mode 100755
index 000000000..b794386f7
--- /dev/null
+++ b/browser/base/content/browser.js
@@ -0,0 +1,8281 @@
+/* -*- 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 Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ContextualIdentityService.jsm");
+Cu.import("resource://gre/modules/NotificationDB.jsm");
+
+// lazy module getters
+[
+ ["AboutHome", "resource:///modules/AboutHome.jsm"],
+ ["AddonWatcher", "resource://gre/modules/AddonWatcher.jsm"],
+ ["AppConstants", "resource://gre/modules/AppConstants.jsm"],
+ ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"],
+ ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"],
+ ["BrowserUtils", "resource://gre/modules/BrowserUtils.jsm"],
+ ["CastingApps", "resource:///modules/CastingApps.jsm"],
+ ["CharsetMenu", "resource://gre/modules/CharsetMenu.jsm"],
+ ["Color", "resource://gre/modules/Color.jsm"],
+ ["ContentSearch", "resource:///modules/ContentSearch.jsm"],
+ ["Deprecated", "resource://gre/modules/Deprecated.jsm"],
+ ["E10SUtils", "resource:///modules/E10SUtils.jsm"],
+ ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
+ ["GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"],
+ ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
+ ["Log", "resource://gre/modules/Log.jsm"],
+ ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"],
+ ["NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"],
+ ["PageThumbs", "resource://gre/modules/PageThumbs.jsm"],
+ ["PluralForm", "resource://gre/modules/PluralForm.jsm"],
+ ["Preferences", "resource://gre/modules/Preferences.jsm"],
+ ["PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"],
+ ["ProcessHangMonitor", "resource:///modules/ProcessHangMonitor.jsm"],
+ ["PromiseUtils", "resource://gre/modules/PromiseUtils.jsm"],
+ ["ReaderMode", "resource://gre/modules/ReaderMode.jsm"],
+ ["ReaderParent", "resource:///modules/ReaderParent.jsm"],
+ ["RecentWindow", "resource:///modules/RecentWindow.jsm"],
+ ["SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"],
+ ["ShortcutUtils", "resource://gre/modules/ShortcutUtils.jsm"],
+ ["SimpleServiceDiscovery", "resource://gre/modules/SimpleServiceDiscovery.jsm"],
+ ["SitePermissions", "resource:///modules/SitePermissions.jsm"],
+ ["Social", "resource:///modules/Social.jsm"],
+ ["TabCrashHandler", "resource:///modules/ContentCrashHandlers.jsm"],
+ ["Task", "resource://gre/modules/Task.jsm"],
+ ["TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm"],
+ ["Translation", "resource:///modules/translation/Translation.jsm"],
+ ["UITour", "resource:///modules/UITour.jsm"],
+ ["UpdateUtils", "resource://gre/modules/UpdateUtils.jsm"],
+ ["Weave", "resource://services-sync/main.js"],
+ ["fxAccounts", "resource://gre/modules/FxAccounts.jsm"],
+ ["gDevTools", "resource://devtools/client/framework/gDevTools.jsm"],
+ ["gDevToolsBrowser", "resource://devtools/client/framework/gDevTools.jsm"],
+ ["webrtcUI", "resource:///modules/webrtcUI.jsm", ]
+].forEach(([name, resource]) => XPCOMUtils.defineLazyModuleGetter(this, name, resource));
+
+XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
+ "resource://gre/modules/SafeBrowsing.jsm");
+
+if (AppConstants.MOZ_CRASHREPORTER) {
+ XPCOMUtils.defineLazyModuleGetter(this, "PluginCrashReporter",
+ "resource:///modules/ContentCrashHandlers.jsm");
+}
+
+// lazy service getters
+[
+ ["Favicons", "@mozilla.org/browser/favicon-service;1", "mozIAsyncFavicons"],
+ ["WindowsUIUtils", "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
+ ["gAboutNewTabService", "@mozilla.org/browser/aboutnewtab-service;1", "nsIAboutNewTabService"],
+ ["gDNSService", "@mozilla.org/network/dns-service;1", "nsIDNSService"],
+].forEach(([name, cc, ci]) => XPCOMUtils.defineLazyServiceGetter(this, name, cc, ci));
+
+if (AppConstants.MOZ_CRASHREPORTER) {
+ XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
+ "@mozilla.org/xre/app-info;1",
+ "nsICrashReporter");
+}
+
+
+XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function() {
+ let tmp = {};
+ Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", tmp);
+ return tmp.BrowserToolboxProcess;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
+ return Services.strings.createBundle('chrome://browser/locale/browser.properties');
+});
+
+XPCOMUtils.defineLazyGetter(this, "gCustomizeMode", function() {
+ let scope = {};
+ Cu.import("resource:///modules/CustomizeMode.jsm", scope);
+ return new scope.CustomizeMode(window);
+});
+
+XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function () {
+ // Only show resizers on Windows 2000 and XP
+ return AppConstants.isPlatformAndVersionAtMost("win", "5.9");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gPrefService", function() {
+ return Services.prefs;
+});
+
+XPCOMUtils.defineLazyGetter(this, "InlineSpellCheckerUI", function() {
+ let tmp = {};
+ Cu.import("resource://gre/modules/InlineSpellChecker.jsm", tmp);
+ return new tmp.InlineSpellChecker();
+});
+
+XPCOMUtils.defineLazyGetter(this, "PageMenuParent", function() {
+ let tmp = {};
+ Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
+ return new tmp.PageMenuParent();
+});
+
+XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () {
+ let tmp = {};
+ Cu.import("resource://gre/modules/PopupNotifications.jsm", tmp);
+ try {
+ return new tmp.PopupNotifications(gBrowser,
+ document.getElementById("notification-popup"),
+ document.getElementById("notification-popup-box"));
+ } catch (ex) {
+ Cu.reportError(ex);
+ return null;
+ }
+});
+
+XPCOMUtils.defineLazyGetter(this, "Win7Features", function () {
+ if (AppConstants.platform != "win")
+ return null;
+
+ const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
+ if (WINTASKBAR_CONTRACTID in Cc &&
+ Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) {
+ let AeroPeek = Cu.import("resource:///modules/WindowsPreviewPerTab.jsm", {}).AeroPeek;
+ return {
+ onOpenWindow: function () {
+ AeroPeek.onOpenWindow(window);
+ },
+ onCloseWindow: function () {
+ AeroPeek.onCloseWindow(window);
+ }
+ };
+ }
+ return null;
+});
+
+const nsIWebNavigation = Ci.nsIWebNavigation;
+
+var gLastBrowserCharset = null;
+var gLastValidURLStr = "";
+var gInPrintPreviewMode = false;
+var gContextMenu = null; // nsContextMenu instance
+var gMultiProcessBrowser =
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext)
+ .useRemoteTabs;
+var gAppInfo = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULAppInfo)
+ .QueryInterface(Ci.nsIXULRuntime);
+
+if (AppConstants.platform != "macosx") {
+ var gEditUIVisible = true;
+}
+
+/* globals gBrowser, gNavToolbox, gURLBar, gNavigatorBundle*/
+[
+ ["gBrowser", "content"],
+ ["gNavToolbox", "navigator-toolbox"],
+ ["gURLBar", "urlbar"],
+ ["gNavigatorBundle", "bundle_browser"]
+].forEach(function (elementGlobal) {
+ var [name, id] = elementGlobal;
+ window.__defineGetter__(name, function () {
+ var element = document.getElementById(id);
+ if (!element)
+ return null;
+ delete window[name];
+ return window[name] = element;
+ });
+ window.__defineSetter__(name, function (val) {
+ delete window[name];
+ return window[name] = val;
+ });
+});
+
+// Smart getter for the findbar. If you don't wish to force the creation of
+// the findbar, check gFindBarInitialized first.
+
+this.__defineGetter__("gFindBar", function() {
+ return window.gBrowser.getFindBar();
+});
+
+this.__defineGetter__("gFindBarInitialized", function() {
+ return window.gBrowser.isFindBarInitialized();
+});
+
+this.__defineGetter__("AddonManager", function() {
+ let tmp = {};
+ Cu.import("resource://gre/modules/AddonManager.jsm", tmp);
+ return this.AddonManager = tmp.AddonManager;
+});
+this.__defineSetter__("AddonManager", function (val) {
+ delete this.AddonManager;
+ return this.AddonManager = val;
+});
+
+
+var gInitialPages = [
+ "about:blank",
+ "about:newtab",
+ "about:home",
+ "about:privatebrowsing",
+ "about:welcomeback",
+ "about:sessionrestore"
+];
+
+function* browserWindows() {
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements())
+ yield windows.getNext();
+}
+
+/**
+* We can avoid adding multiple load event listeners and save some time by adding
+* one listener that calls all real handlers.
+*/
+function pageShowEventHandlers(persisted) {
+ XULBrowserWindow.asyncUpdateUI();
+}
+
+function UpdateBackForwardCommands(aWebNavigation) {
+ var backBroadcaster = document.getElementById("Browser:Back");
+ var forwardBroadcaster = document.getElementById("Browser:Forward");
+
+ // Avoid setting attributes on broadcasters if the value hasn't changed!
+ // Remember, guys, setting attributes on elements is expensive! They
+ // get inherited into anonymous content, broadcast to other widgets, etc.!
+ // Don't do it if the value hasn't changed! - dwh
+
+ var backDisabled = backBroadcaster.hasAttribute("disabled");
+ var forwardDisabled = forwardBroadcaster.hasAttribute("disabled");
+ if (backDisabled == aWebNavigation.canGoBack) {
+ if (backDisabled)
+ backBroadcaster.removeAttribute("disabled");
+ else
+ backBroadcaster.setAttribute("disabled", true);
+ }
+
+ if (forwardDisabled == aWebNavigation.canGoForward) {
+ if (forwardDisabled)
+ forwardBroadcaster.removeAttribute("disabled");
+ else
+ forwardBroadcaster.setAttribute("disabled", true);
+ }
+}
+
+/**
+ * Click-and-Hold implementation for the Back and Forward buttons
+ * XXXmano: should this live in toolbarbutton.xml?
+ */
+function SetClickAndHoldHandlers() {
+ // Bug 414797: Clone the back/forward buttons' context menu into both buttons.
+ let popup = document.getElementById("backForwardMenu").cloneNode(true);
+ popup.removeAttribute("id");
+ // Prevent the back/forward buttons' context attributes from being inherited.
+ popup.setAttribute("context", "");
+
+ let backButton = document.getElementById("back-button");
+ backButton.setAttribute("type", "menu");
+ backButton.appendChild(popup);
+ gClickAndHoldListenersOnElement.add(backButton);
+
+ let forwardButton = document.getElementById("forward-button");
+ popup = popup.cloneNode(true);
+ forwardButton.setAttribute("type", "menu");
+ forwardButton.appendChild(popup);
+ gClickAndHoldListenersOnElement.add(forwardButton);
+}
+
+
+const gClickAndHoldListenersOnElement = {
+ _timers: new Map(),
+
+ _mousedownHandler(aEvent) {
+ if (aEvent.button != 0 ||
+ aEvent.currentTarget.open ||
+ aEvent.currentTarget.disabled)
+ return;
+
+ // Prevent the menupopup from opening immediately
+ aEvent.currentTarget.firstChild.hidden = true;
+
+ aEvent.currentTarget.addEventListener("mouseout", this, false);
+ aEvent.currentTarget.addEventListener("mouseup", this, false);
+ this._timers.set(aEvent.currentTarget, setTimeout((b) => this._openMenu(b), 500, aEvent.currentTarget));
+ },
+
+ _clickHandler(aEvent) {
+ if (aEvent.button == 0 &&
+ aEvent.target == aEvent.currentTarget &&
+ !aEvent.currentTarget.open &&
+ !aEvent.currentTarget.disabled) {
+ let cmdEvent = document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent("command", true, true, window, 0,
+ aEvent.ctrlKey, aEvent.altKey, aEvent.shiftKey,
+ aEvent.metaKey, null);
+ aEvent.currentTarget.dispatchEvent(cmdEvent);
+
+ // This is here to cancel the XUL default event
+ // dom.click() triggers a command even if there is a click handler
+ // however this can now be prevented with preventDefault().
+ aEvent.preventDefault();
+ }
+ },
+
+ _openMenu(aButton) {
+ this._cancelHold(aButton);
+ aButton.firstChild.hidden = false;
+ aButton.open = true;
+ },
+
+ _mouseoutHandler(aEvent) {
+ let buttonRect = aEvent.currentTarget.getBoundingClientRect();
+ if (aEvent.clientX >= buttonRect.left &&
+ aEvent.clientX <= buttonRect.right &&
+ aEvent.clientY >= buttonRect.bottom)
+ this._openMenu(aEvent.currentTarget);
+ else
+ this._cancelHold(aEvent.currentTarget);
+ },
+
+ _mouseupHandler(aEvent) {
+ this._cancelHold(aEvent.currentTarget);
+ },
+
+ _cancelHold(aButton) {
+ clearTimeout(this._timers.get(aButton));
+ aButton.removeEventListener("mouseout", this, false);
+ aButton.removeEventListener("mouseup", this, false);
+ },
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "mouseout":
+ this._mouseoutHandler(e);
+ break;
+ case "mousedown":
+ this._mousedownHandler(e);
+ break;
+ case "click":
+ this._clickHandler(e);
+ break;
+ case "mouseup":
+ this._mouseupHandler(e);
+ break;
+ }
+ },
+
+ remove(aButton) {
+ aButton.removeEventListener("mousedown", this, true);
+ aButton.removeEventListener("click", this, true);
+ },
+
+ add(aElm) {
+ this._timers.delete(aElm);
+
+ aElm.addEventListener("mousedown", this, true);
+ aElm.addEventListener("click", this, true);
+ }
+};
+
+const gSessionHistoryObserver = {
+ observe: function(subject, topic, data)
+ {
+ if (topic != "browser:purge-session-history")
+ return;
+
+ var backCommand = document.getElementById("Browser:Back");
+ backCommand.setAttribute("disabled", "true");
+ var fwdCommand = document.getElementById("Browser:Forward");
+ fwdCommand.setAttribute("disabled", "true");
+
+ // Hide session restore button on about:home
+ window.messageManager.broadcastAsyncMessage("Browser:HideSessionRestoreButton");
+
+ // Clear undo history of the URL bar
+ gURLBar.editor.transactionManager.clear()
+ }
+};
+
+/**
+ * Given a starting docshell and a URI to look up, find the docshell the URI
+ * is loaded in.
+ * @param aDocument
+ * A document to find instead of using just a URI - this is more specific.
+ * @param aDocShell
+ * The doc shell to start at
+ * @param aSoughtURI
+ * The URI that we're looking for
+ * @returns The doc shell that the sought URI is loaded in. Can be in
+ * subframes.
+ */
+function findChildShell(aDocument, aDocShell, aSoughtURI) {
+ aDocShell.QueryInterface(Components.interfaces.nsIWebNavigation);
+ aDocShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
+ var doc = aDocShell.getInterface(Components.interfaces.nsIDOMDocument);
+ if ((aDocument && doc == aDocument) ||
+ (aSoughtURI && aSoughtURI.spec == aDocShell.currentURI.spec))
+ return aDocShell;
+
+ var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem);
+ for (var i = 0; i < node.childCount; ++i) {
+ var docShell = node.getChildAt(i);
+ docShell = findChildShell(aDocument, docShell, aSoughtURI);
+ if (docShell)
+ return docShell;
+ }
+ return null;
+}
+
+var gPopupBlockerObserver = {
+ _reportButton: null,
+
+ onReportButtonMousedown: function (aEvent)
+ {
+ // If this method is called on the same event tick as the popup gets
+ // hidden, do nothing to avoid re-opening the popup.
+ if (aEvent.button != 0 || aEvent.target != this._reportButton || this.isPopupHidingTick)
+ return;
+
+ document.getElementById("blockedPopupOptions")
+ .openPopup(this._reportButton, "after_end", 0, 2, false, false, aEvent);
+ },
+
+ handleEvent: function (aEvent)
+ {
+ if (aEvent.originalTarget != gBrowser.selectedBrowser)
+ return;
+
+ if (!this._reportButton)
+ this._reportButton = document.getElementById("page-report-button");
+
+ if (!gBrowser.selectedBrowser.blockedPopups ||
+ !gBrowser.selectedBrowser.blockedPopups.length) {
+ // Hide the icon in the location bar (if the location bar exists)
+ this._reportButton.hidden = true;
+
+ // Hide the notification box (if it's visible).
+ let notificationBox = gBrowser.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("popup-blocked");
+ if (notification) {
+ notificationBox.removeNotification(notification, false);
+ }
+ return;
+ }
+
+ this._reportButton.hidden = false;
+
+ // Only show the notification again if we've not already shown it. Since
+ // notifications are per-browser, we don't need to worry about re-adding
+ // it.
+ if (!gBrowser.selectedBrowser.blockedPopups.reported) {
+ if (gPrefService.getBoolPref("privacy.popups.showBrowserMessage")) {
+ var brandBundle = document.getElementById("bundle_brand");
+ var brandShortName = brandBundle.getString("brandShortName");
+ var popupCount = gBrowser.selectedBrowser.blockedPopups.length;
+
+ var stringKey = AppConstants.platform == "win"
+ ? "popupWarningButton"
+ : "popupWarningButtonUnix";
+
+ var popupButtonText = gNavigatorBundle.getString(stringKey);
+ var popupButtonAccesskey = gNavigatorBundle.getString(stringKey + ".accesskey");
+
+ var messageBase = gNavigatorBundle.getString("popupWarning.message");
+ var message = PluralForm.get(popupCount, messageBase)
+ .replace("#1", brandShortName)
+ .replace("#2", popupCount);
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("popup-blocked");
+ if (notification) {
+ notification.label = message;
+ }
+ else {
+ var buttons = [{
+ label: popupButtonText,
+ accessKey: popupButtonAccesskey,
+ popup: "blockedPopupOptions",
+ callback: null
+ }];
+
+ const priority = notificationBox.PRIORITY_WARNING_MEDIUM;
+ notificationBox.appendNotification(message, "popup-blocked",
+ "chrome://browser/skin/Info.png",
+ priority, buttons);
+ }
+ }
+
+ // Record the fact that we've reported this blocked popup, so we don't
+ // show it again.
+ gBrowser.selectedBrowser.blockedPopups.reported = true;
+ }
+ },
+
+ toggleAllowPopupsForSite: function (aEvent)
+ {
+ var pm = Services.perms;
+ var shouldBlock = aEvent.target.getAttribute("block") == "true";
+ var perm = shouldBlock ? pm.DENY_ACTION : pm.ALLOW_ACTION;
+ pm.add(gBrowser.currentURI, "popup", perm);
+
+ if (!shouldBlock)
+ this.showAllBlockedPopups(gBrowser.selectedBrowser);
+
+ gBrowser.getNotificationBox().removeCurrentNotification();
+ },
+
+ fillPopupList: function (aEvent)
+ {
+ // XXXben - rather than using |currentURI| here, which breaks down on multi-framed sites
+ // we should really walk the blockedPopups and create a list of "allow for <host>"
+ // menuitems for the common subset of hosts present in the report, this will
+ // make us frame-safe.
+ //
+ // XXXjst - Note that when this is fixed to work with multi-framed sites,
+ // also back out the fix for bug 343772 where
+ // nsGlobalWindow::CheckOpenAllow() was changed to also
+ // check if the top window's location is whitelisted.
+ let browser = gBrowser.selectedBrowser;
+ var uri = browser.currentURI;
+ var blockedPopupAllowSite = document.getElementById("blockedPopupAllowSite");
+ try {
+ blockedPopupAllowSite.removeAttribute("hidden");
+
+ var pm = Services.perms;
+ if (pm.testPermission(uri, "popup") == pm.ALLOW_ACTION) {
+ // Offer an item to block popups for this site, if a whitelist entry exists
+ // already for it.
+ let blockString = gNavigatorBundle.getFormattedString("popupBlock", [uri.host || uri.spec]);
+ blockedPopupAllowSite.setAttribute("label", blockString);
+ blockedPopupAllowSite.setAttribute("block", "true");
+ }
+ else {
+ // Offer an item to allow popups for this site
+ let allowString = gNavigatorBundle.getFormattedString("popupAllow", [uri.host || uri.spec]);
+ blockedPopupAllowSite.setAttribute("label", allowString);
+ blockedPopupAllowSite.removeAttribute("block");
+ }
+ }
+ catch (e) {
+ blockedPopupAllowSite.setAttribute("hidden", "true");
+ }
+
+ if (PrivateBrowsingUtils.isWindowPrivate(window))
+ blockedPopupAllowSite.setAttribute("disabled", "true");
+ else
+ blockedPopupAllowSite.removeAttribute("disabled");
+
+ let blockedPopupDontShowMessage = document.getElementById("blockedPopupDontShowMessage");
+ let showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage");
+ blockedPopupDontShowMessage.setAttribute("checked", !showMessage);
+ if (aEvent.target.anchorNode.id == "page-report-button") {
+ aEvent.target.anchorNode.setAttribute("open", "true");
+ blockedPopupDontShowMessage.setAttribute("label", gNavigatorBundle.getString("popupWarningDontShowFromLocationbar"));
+ } else {
+ blockedPopupDontShowMessage.setAttribute("label", gNavigatorBundle.getString("popupWarningDontShowFromMessage"));
+ }
+
+ let blockedPopupsSeparator =
+ document.getElementById("blockedPopupsSeparator");
+ blockedPopupsSeparator.setAttribute("hidden", true);
+
+ gBrowser.selectedBrowser.retrieveListOfBlockedPopups().then(blockedPopups => {
+ let foundUsablePopupURI = false;
+ if (blockedPopups) {
+ for (let i = 0; i < blockedPopups.length; i++) {
+ let blockedPopup = blockedPopups[i];
+
+ // popupWindowURI will be null if the file picker popup is blocked.
+ // xxxdz this should make the option say "Show file picker" and do it (Bug 590306)
+ if (!blockedPopup.popupWindowURIspec)
+ continue;
+
+ var popupURIspec = blockedPopup.popupWindowURIspec;
+
+ // Sometimes the popup URI that we get back from the blockedPopup
+ // isn't useful (for instance, netscape.com's popup URI ends up
+ // being "http://www.netscape.com", which isn't really the URI of
+ // the popup they're trying to show). This isn't going to be
+ // useful to the user, so we won't create a menu item for it.
+ if (popupURIspec == "" || popupURIspec == "about:blank" ||
+ popupURIspec == "<self>" ||
+ popupURIspec == uri.spec)
+ continue;
+
+ // Because of the short-circuit above, we may end up in a situation
+ // in which we don't have any usable popup addresses to show in
+ // the menu, and therefore we shouldn't show the separator. However,
+ // since we got past the short-circuit, we must've found at least
+ // one usable popup URI and thus we'll turn on the separator later.
+ foundUsablePopupURI = true;
+
+ var menuitem = document.createElement("menuitem");
+ var label = gNavigatorBundle.getFormattedString("popupShowPopupPrefix",
+ [popupURIspec]);
+ menuitem.setAttribute("label", label);
+ menuitem.setAttribute("oncommand", "gPopupBlockerObserver.showBlockedPopup(event);");
+ menuitem.setAttribute("popupReportIndex", i);
+ menuitem.popupReportBrowser = browser;
+ aEvent.target.appendChild(menuitem);
+ }
+ }
+
+ // Show the separator if we added any
+ // showable popup addresses to the menu.
+ if (foundUsablePopupURI)
+ blockedPopupsSeparator.removeAttribute("hidden");
+ }, null);
+ },
+
+ onPopupHiding: function (aEvent) {
+ if (aEvent.target.anchorNode.id == "page-report-button")
+ aEvent.target.anchorNode.removeAttribute("open");
+
+ this.isPopupHidingTick = true;
+ setTimeout(() => this.isPopupHidingTick = false, 0);
+
+ let item = aEvent.target.lastChild;
+ while (item && item.getAttribute("observes") != "blockedPopupsSeparator") {
+ let next = item.previousSibling;
+ item.parentNode.removeChild(item);
+ item = next;
+ }
+ },
+
+ showBlockedPopup: function (aEvent)
+ {
+ var target = aEvent.target;
+ var popupReportIndex = target.getAttribute("popupReportIndex");
+ let browser = target.popupReportBrowser;
+ browser.unblockPopup(popupReportIndex);
+ },
+
+ showAllBlockedPopups: function (aBrowser)
+ {
+ aBrowser.retrieveListOfBlockedPopups().then(popups => {
+ for (let i = 0; i < popups.length; i++) {
+ if (popups[i].popupWindowURIspec)
+ aBrowser.unblockPopup(i);
+ }
+ }, null);
+ },
+
+ editPopupSettings: function ()
+ {
+ var host = "";
+ try {
+ host = gBrowser.currentURI.host;
+ }
+ catch (e) { }
+
+ var bundlePreferences = document.getElementById("bundle_preferences");
+ var params = { blockVisible : false,
+ sessionVisible : false,
+ allowVisible : true,
+ prefilledHost : host,
+ permissionType : "popup",
+ windowTitle : bundlePreferences.getString("popuppermissionstitle"),
+ introText : bundlePreferences.getString("popuppermissionstext") };
+ var existingWindow = Services.wm.getMostRecentWindow("Browser:Permissions");
+ if (existingWindow) {
+ existingWindow.initWithParams(params);
+ existingWindow.focus();
+ }
+ else
+ window.openDialog("chrome://browser/content/preferences/permissions.xul",
+ "_blank", "resizable,dialog=no,centerscreen", params);
+ },
+
+ dontShowMessage: function ()
+ {
+ var showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage");
+ gPrefService.setBoolPref("privacy.popups.showBrowserMessage", !showMessage);
+ gBrowser.getNotificationBox().removeCurrentNotification();
+ }
+};
+
+function gKeywordURIFixup({ target: browser, data: fixupInfo }) {
+ let deserializeURI = (spec) => spec ? makeURI(spec) : null;
+
+ // We get called irrespective of whether we did a keyword search, or
+ // whether the original input would be vaguely interpretable as a URL,
+ // so figure that out first.
+ let alternativeURI = deserializeURI(fixupInfo.fixedURI);
+ if (!fixupInfo.keywordProviderName || !alternativeURI || !alternativeURI.host) {
+ return;
+ }
+
+ // At this point we're still only just about to load this URI.
+ // When the async DNS lookup comes back, we may be in any of these states:
+ // 1) still on the previous URI, waiting for the preferredURI (keyword
+ // search) to respond;
+ // 2) at the keyword search URI (preferredURI)
+ // 3) at some other page because the user stopped navigation.
+ // We keep track of the currentURI to detect case (1) in the DNS lookup
+ // callback.
+ let previousURI = browser.currentURI;
+ let preferredURI = deserializeURI(fixupInfo.preferredURI);
+
+ // now swap for a weak ref so we don't hang on to browser needlessly
+ // even if the DNS query takes forever
+ let weakBrowser = Cu.getWeakReference(browser);
+ browser = null;
+
+ // Additionally, we need the host of the parsed url
+ let hostName = alternativeURI.host;
+ // and the ascii-only host for the pref:
+ let asciiHost = alternativeURI.asciiHost;
+ // Normalize out a single trailing dot - NB: not using endsWith/lastIndexOf
+ // because we need to be sure this last dot is the *only* dot, too.
+ // More generally, this is used for the pref and should stay in sync with
+ // the code in nsDefaultURIFixup::KeywordURIFixup .
+ if (asciiHost.indexOf('.') == asciiHost.length - 1) {
+ asciiHost = asciiHost.slice(0, -1);
+ }
+
+ let isIPv4Address = host => {
+ let parts = host.split(".");
+ if (parts.length != 4) {
+ return false;
+ }
+ return parts.every(part => {
+ let n = parseInt(part, 10);
+ return n >= 0 && n <= 255;
+ });
+ };
+ // Avoid showing fixup information if we're suggesting an IP. Note that
+ // decimal representations of IPs are normalized to a 'regular'
+ // dot-separated IP address by network code, but that only happens for
+ // numbers that don't overflow. Longer numbers do not get normalized,
+ // but still work to access IP addresses. So for instance,
+ // 1097347366913 (ff7f000001) gets resolved by using the final bytes,
+ // making it the same as 7f000001, which is 127.0.0.1 aka localhost.
+ // While 2130706433 would get normalized by network, 1097347366913
+ // does not, and we have to deal with both cases here:
+ if (isIPv4Address(asciiHost) || /^(?:\d+|0x[a-f0-9]+)$/i.test(asciiHost))
+ return;
+
+ let onLookupComplete = (request, record, status) => {
+ let browser = weakBrowser.get();
+ if (!Components.isSuccessCode(status) || !browser)
+ return;
+
+ let currentURI = browser.currentURI;
+ // If we're in case (3) (see above), don't show an info bar.
+ if (!currentURI.equals(previousURI) &&
+ !currentURI.equals(preferredURI)) {
+ return;
+ }
+
+ // show infobar offering to visit the host
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ if (notificationBox.getNotificationWithValue("keyword-uri-fixup"))
+ return;
+
+ let message = gNavigatorBundle.getFormattedString(
+ "keywordURIFixup.message", [hostName]);
+ let yesMessage = gNavigatorBundle.getFormattedString(
+ "keywordURIFixup.goTo", [hostName])
+
+ let buttons = [
+ {
+ label: yesMessage,
+ accessKey: gNavigatorBundle.getString("keywordURIFixup.goTo.accesskey"),
+ callback: function() {
+ // Do not set this preference while in private browsing.
+ if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
+ let pref = "browser.fixup.domainwhitelist." + asciiHost;
+ Services.prefs.setBoolPref(pref, true);
+ }
+ openUILinkIn(alternativeURI.spec, "current");
+ }
+ },
+ {
+ label: gNavigatorBundle.getString("keywordURIFixup.dismiss"),
+ accessKey: gNavigatorBundle.getString("keywordURIFixup.dismiss.accesskey"),
+ callback: function() {
+ let notification = notificationBox.getNotificationWithValue("keyword-uri-fixup");
+ notificationBox.removeNotification(notification, true);
+ }
+ }
+ ];
+ let notification =
+ notificationBox.appendNotification(message, "keyword-uri-fixup", null,
+ notificationBox.PRIORITY_INFO_HIGH,
+ buttons);
+ notification.persistence = 1;
+ };
+
+ try {
+ gDNSService.asyncResolve(hostName, 0, onLookupComplete, Services.tm.mainThread);
+ } catch (ex) {
+ // Do nothing if the URL is invalid (we don't want to show a notification in that case).
+ if (ex.result != Cr.NS_ERROR_UNKNOWN_HOST) {
+ // ... otherwise, report:
+ Cu.reportError(ex);
+ }
+ }
+}
+
+// A shared function used by both remote and non-remote browser XBL bindings to
+// load a URI or redirect it to the correct process.
+function _loadURIWithFlags(browser, uri, params) {
+ if (!uri) {
+ uri = "about:blank";
+ }
+ let flags = params.flags || 0;
+ let referrer = params.referrerURI;
+ let referrerPolicy = ('referrerPolicy' in params ? params.referrerPolicy :
+ Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT);
+ let postData = params.postData;
+
+ let wasRemote = browser.isRemoteBrowser;
+
+ let process = browser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
+ : Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+ let mustChangeProcess = gMultiProcessBrowser &&
+ !E10SUtils.canLoadURIInProcess(uri, process);
+ if ((!wasRemote && !mustChangeProcess) ||
+ (wasRemote && mustChangeProcess)) {
+ browser.inLoadURI = true;
+ }
+ try {
+ if (!mustChangeProcess) {
+ if (params.userContextId) {
+ browser.webNavigation.setOriginAttributesBeforeLoading({ userContextId: params.userContextId });
+ }
+
+ browser.webNavigation.loadURIWithOptions(uri, flags,
+ referrer, referrerPolicy,
+ postData, null, null);
+ } else {
+ // Check if the current browser is allowed to unload.
+ let {permitUnload, timedOut} = browser.permitUnload();
+ if (!timedOut && !permitUnload) {
+ return;
+ }
+
+ if (postData) {
+ postData = NetUtil.readInputStreamToString(postData, postData.available());
+ }
+
+ let loadParams = {
+ uri: uri,
+ flags: flags,
+ referrer: referrer ? referrer.spec : null,
+ referrerPolicy: referrerPolicy,
+ postData: postData
+ }
+
+ if (params.userContextId) {
+ loadParams.userContextId = params.userContextId;
+ }
+
+ LoadInOtherProcess(browser, loadParams);
+ }
+ } catch (e) {
+ // If anything goes wrong when switching remoteness, just switch remoteness
+ // manually and load the URI.
+ // We might lose history that way but at least the browser loaded a page.
+ // This might be necessary if SessionStore wasn't initialized yet i.e.
+ // when the homepage is a non-remote page.
+ if (mustChangeProcess) {
+ Cu.reportError(e);
+ gBrowser.updateBrowserRemotenessByURL(browser, uri);
+
+ if (params.userContextId) {
+ browser.webNavigation.setOriginAttributesBeforeLoading({ userContextId: params.userContextId });
+ }
+
+ browser.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy,
+ postData, null, null);
+ } else {
+ throw e;
+ }
+ } finally {
+ if ((!wasRemote && !mustChangeProcess) ||
+ (wasRemote && mustChangeProcess)) {
+ browser.inLoadURI = false;
+ }
+ }
+}
+
+// Starts a new load in the browser first switching the browser to the correct
+// process
+function LoadInOtherProcess(browser, loadOptions, historyIndex = -1) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ SessionStore.navigateAndRestore(tab, loadOptions, historyIndex);
+}
+
+// Called when a docshell has attempted to load a page in an incorrect process.
+// This function is responsible for loading the page in the correct process.
+function RedirectLoad({ target: browser, data }) {
+ // We should only start the redirection if the browser window has finished
+ // starting up. Otherwise, we should wait until the startup is done.
+ if (gBrowserInit.delayedStartupFinished) {
+ LoadInOtherProcess(browser, data.loadOptions, data.historyIndex);
+ } else {
+ let delayedStartupFinished = (subject, topic) => {
+ if (topic == "browser-delayed-startup-finished" &&
+ subject == window) {
+ Services.obs.removeObserver(delayedStartupFinished, topic);
+ LoadInOtherProcess(browser, data.loadOptions, data.historyIndex);
+ }
+ };
+ Services.obs.addObserver(delayedStartupFinished,
+ "browser-delayed-startup-finished",
+ false);
+ }
+}
+
+addEventListener("DOMContentLoaded", function onDCL() {
+ removeEventListener("DOMContentLoaded", onDCL);
+
+ // There are some windows, like macBrowserOverlay.xul, that
+ // load browser.js, but never load tabbrowser.xml. We can ignore
+ // those cases.
+ if (!gBrowser || !gBrowser.updateBrowserRemoteness) {
+ return;
+ }
+
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIXULWindow)
+ .XULBrowserWindow = window.XULBrowserWindow;
+ window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
+ new nsBrowserAccess();
+
+ let initBrowser =
+ document.getAnonymousElementByAttribute(gBrowser, "anonid", "initialBrowser");
+
+ // The window's first argument is a tab if and only if we are swapping tabs.
+ // We must set the browser's usercontextid before updateBrowserRemoteness(),
+ // so that the newly created remote tab child has the correct usercontextid.
+ if (window.arguments) {
+ let tabToOpen = window.arguments[0];
+ if (tabToOpen instanceof XULElement && tabToOpen.hasAttribute("usercontextid")) {
+ initBrowser.setAttribute("usercontextid", tabToOpen.getAttribute("usercontextid"));
+ }
+ }
+
+ gBrowser.updateBrowserRemoteness(initBrowser, gMultiProcessBrowser);
+});
+
+var gBrowserInit = {
+ delayedStartupFinished: false,
+
+ onLoad: function() {
+ gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false);
+
+ Services.obs.addObserver(gPluginHandler.NPAPIPluginCrashed, "plugin-crashed", false);
+
+ window.addEventListener("AppCommand", HandleAppCommandEvent, true);
+
+ // These routines add message listeners. They must run before
+ // loading the frame script to ensure that we don't miss any
+ // message sent between when the frame script is loaded and when
+ // the listener is registered.
+ DOMLinkHandler.init();
+ gPageStyleMenu.init();
+ LanguageDetectionListener.init();
+ BrowserOnClick.init();
+ FeedHandler.init();
+ DevEdition.init();
+ AboutPrivateBrowsingListener.init();
+ TrackingProtection.init();
+ RefreshBlocker.init();
+ CaptivePortalWatcher.init();
+
+ let mm = window.getGroupMessageManager("browsers");
+ mm.loadFrameScript("chrome://browser/content/tab-content.js", true);
+ mm.loadFrameScript("chrome://browser/content/content.js", true);
+ mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
+ mm.loadFrameScript("chrome://global/content/manifestMessages.js", true);
+
+ // initialize observers and listeners
+ // and give C++ access to gBrowser
+ XULBrowserWindow.init();
+
+ window.messageManager.addMessageListener("Browser:LoadURI", RedirectLoad);
+
+ if (!gMultiProcessBrowser) {
+ // There is a Content:Click message manually sent from content.
+ Cc["@mozilla.org/eventlistenerservice;1"]
+ .getService(Ci.nsIEventListenerService)
+ .addSystemEventListener(gBrowser, "click", contentAreaClick, true);
+ }
+
+ // hook up UI through progress listener
+ gBrowser.addProgressListener(window.XULBrowserWindow);
+ gBrowser.addTabsProgressListener(window.TabsProgressListener);
+
+ // setup simple gestures support
+ gGestureSupport.init(true);
+
+ // setup history swipe animation
+ gHistorySwipeAnimation.init();
+
+ SidebarUI.init();
+
+ // Certain kinds of automigration rely on this notification to complete
+ // their tasks BEFORE the browser window is shown. SessionStore uses it to
+ // restore tabs into windows AFTER important parts like gMultiProcessBrowser
+ // have been initialized.
+ Services.obs.notifyObservers(window, "browser-window-before-show", "");
+
+ // Set a sane starting width/height for all resolutions on new profiles.
+ if (!document.documentElement.hasAttribute("width")) {
+ const TARGET_WIDTH = 1280;
+ const TARGET_HEIGHT = 1040;
+ let width = Math.min(screen.availWidth * .9, TARGET_WIDTH);
+ let height = Math.min(screen.availHeight * .9, TARGET_HEIGHT);
+
+ document.documentElement.setAttribute("width", width);
+ document.documentElement.setAttribute("height", height);
+
+ if (width < TARGET_WIDTH && height < TARGET_HEIGHT) {
+ document.documentElement.setAttribute("sizemode", "maximized");
+ }
+ }
+
+ if (!window.toolbar.visible) {
+ // adjust browser UI for popups
+ gURLBar.setAttribute("readonly", "true");
+ gURLBar.setAttribute("enablehistory", "false");
+ }
+
+ // Misc. inits.
+ TabletModeUpdater.init();
+ CombinedStopReload.init();
+ gPrivateBrowsingUI.init();
+
+ if (window.matchMedia("(-moz-os-version: windows-win8)").matches &&
+ window.matchMedia("(-moz-windows-default-theme)").matches) {
+ let windowFrameColor = new Color(...Cu.import("resource:///modules/Windows8WindowFrameColor.jsm", {})
+ .Windows8WindowFrameColor.get());
+ // Default to black for foreground text.
+ if (!windowFrameColor.isContrastRatioAcceptable(new Color(0, 0, 0))) {
+ document.documentElement.setAttribute("darkwindowframe", "true");
+ }
+ }
+
+ ToolbarIconColor.init();
+
+ // Wait until chrome is painted before executing code not critical to making the window visible
+ this._boundDelayedStartup = this._delayedStartup.bind(this);
+ window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
+
+ this._loadHandled = true;
+ },
+
+ _cancelDelayedStartup: function () {
+ window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
+ this._boundDelayedStartup = null;
+ },
+
+ _delayedStartup: function() {
+ let tmp = {};
+ Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
+ let TelemetryTimestamps = tmp.TelemetryTimestamps;
+ TelemetryTimestamps.add("delayedStartupStarted");
+
+ this._cancelDelayedStartup();
+
+ // We need to set the OfflineApps message listeners up before we
+ // load homepages, which might need them.
+ OfflineApps.init();
+
+ // This pageshow listener needs to be registered before we may call
+ // swapBrowsersAndCloseOther() to receive pageshow events fired by that.
+ let mm = window.messageManager;
+ mm.addMessageListener("PageVisibility:Show", function(message) {
+ if (message.target == gBrowser.selectedBrowser) {
+ setTimeout(pageShowEventHandlers, 0, message.data.persisted);
+ }
+ });
+
+ gBrowser.addEventListener("AboutTabCrashedLoad", function(event) {
+ let ownerDoc = event.originalTarget;
+
+ if (!ownerDoc.documentURI.startsWith("about:tabcrashed")) {
+ return;
+ }
+
+ let browser = gBrowser.getBrowserForDocument(event.target);
+ // Reset the zoom for the tabcrashed page.
+ ZoomManager.setZoomForBrowser(browser, 1);
+ }, false, true);
+
+ gBrowser.addEventListener("InsecureLoginFormsStateChange", function() {
+ gIdentityHandler.refreshForInsecureLoginForms();
+ });
+
+ let uriToLoad = this._getUriToLoad();
+ if (uriToLoad && uriToLoad != "about:blank") {
+ if (uriToLoad instanceof Ci.nsIArray) {
+ let count = uriToLoad.length;
+ let specs = [];
+ for (let i = 0; i < count; i++) {
+ let urisstring = uriToLoad.queryElementAt(i, Ci.nsISupportsString);
+ specs.push(urisstring.data);
+ }
+
+ // This function throws for certain malformed URIs, so use exception handling
+ // so that we don't disrupt startup
+ try {
+ gBrowser.loadTabs(specs, false, true);
+ } catch (e) {}
+ }
+ else if (uriToLoad instanceof XULElement) {
+ // swap the given tab with the default about:blank tab and then close
+ // the original tab in the other window.
+ let tabToOpen = uriToLoad;
+
+ // If this tab was passed as a window argument, clear the
+ // reference to it from the arguments array.
+ if (window.arguments[0] == tabToOpen) {
+ window.arguments[0] = null;
+ }
+
+ // Stop the about:blank load
+ gBrowser.stop();
+ // make sure it has a docshell
+ gBrowser.docShell;
+
+ // We must set usercontextid before updateBrowserRemoteness()
+ // so that the newly created remote tab child has correct usercontextid
+ if (tabToOpen.hasAttribute("usercontextid")) {
+ let usercontextid = tabToOpen.getAttribute("usercontextid");
+ gBrowser.selectedBrowser.setAttribute("usercontextid", usercontextid);
+ }
+
+ // If the browser that we're swapping in was remote, then we'd better
+ // be able to support remote browsers, and then make our selectedTab
+ // remote.
+ try {
+ if (tabToOpen.linkedBrowser.isRemoteBrowser) {
+ if (!gMultiProcessBrowser) {
+ throw new Error("Cannot drag a remote browser into a window " +
+ "without the remote tabs load context.");
+ }
+ gBrowser.updateBrowserRemoteness(gBrowser.selectedBrowser, true);
+ } else if (gBrowser.selectedBrowser.isRemoteBrowser) {
+ // If the browser is remote, then it's implied that
+ // gMultiProcessBrowser is true. We need to flip the remoteness
+ // of this tab to false in order for the tab drag to work.
+ gBrowser.updateBrowserRemoteness(gBrowser.selectedBrowser, false);
+ }
+ gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, tabToOpen);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ // window.arguments[2]: referrer (nsIURI | string)
+ // [3]: postData (nsIInputStream)
+ // [4]: allowThirdPartyFixup (bool)
+ // [5]: referrerPolicy (int)
+ // [6]: userContextId (int)
+ // [7]: originPrincipal (nsIPrincipal)
+ else if (window.arguments.length >= 3) {
+ let referrerURI = window.arguments[2];
+ if (typeof(referrerURI) == "string") {
+ try {
+ referrerURI = makeURI(referrerURI);
+ } catch (e) {
+ referrerURI = null;
+ }
+ }
+ let referrerPolicy = (window.arguments[5] != undefined ?
+ window.arguments[5] : Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT);
+ let userContextId = (window.arguments[6] != undefined ?
+ window.arguments[6] : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID);
+ loadURI(uriToLoad, referrerURI, window.arguments[3] || null,
+ window.arguments[4] || false, referrerPolicy, userContextId,
+ // pass the origin principal (if any) and force its use to create
+ // an initial about:blank viewer if present:
+ window.arguments[7], !!window.arguments[7]);
+ window.focus();
+ }
+ // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3.
+ // Such callers expect that window.arguments[0] is handled as a single URI.
+ else {
+ loadOneOrMoreURIs(uriToLoad);
+ }
+ }
+
+ // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
+ setTimeout(function() { SafeBrowsing.init(); }, 2000);
+
+ Services.obs.addObserver(gIdentityHandler, "perm-changed", false);
+ Services.obs.addObserver(gSessionHistoryObserver, "browser:purge-session-history", false);
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled", false);
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-started", false);
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked", false);
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-origin-blocked", false);
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false);
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation", false);
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false);
+ window.messageManager.addMessageListener("Browser:URIFixup", gKeywordURIFixup);
+
+ BrowserOffline.init();
+ IndexedDBPromptHelper.init();
+
+ if (AppConstants.E10S_TESTING_ONLY)
+ gRemoteTabsUI.init();
+
+ // Initialize the full zoom setting.
+ // We do this before the session restore service gets initialized so we can
+ // apply full zoom settings to tabs restored by the session restore service.
+ FullZoom.init();
+ PanelUI.init();
+ LightweightThemeListener.init();
+
+ Services.telemetry.getHistogramById("E10S_WINDOW").add(gMultiProcessBrowser);
+
+ SidebarUI.startDelayedLoad();
+
+ UpdateUrlbarSearchSplitterState();
+
+ if (!(isBlankPageURL(uriToLoad) || uriToLoad == "about:privatebrowsing") ||
+ !focusAndSelectUrlBar()) {
+ if (gBrowser.selectedBrowser.isRemoteBrowser) {
+ // If the initial browser is remote, in order to optimize for first paint,
+ // we'll defer switching focus to that browser until it has painted.
+ let focusedElement = document.commandDispatcher.focusedElement;
+ let mm = window.messageManager;
+ mm.addMessageListener("Browser:FirstPaint", function onFirstPaint() {
+ mm.removeMessageListener("Browser:FirstPaint", onFirstPaint);
+ // If focus didn't move while we were waiting for first paint, we're okay
+ // to move to the browser.
+ if (document.commandDispatcher.focusedElement == focusedElement) {
+ gBrowser.selectedBrowser.focus();
+ }
+ });
+ } else {
+ // If the initial browser is not remote, we can focus the browser
+ // immediately with no paint performance impact.
+ gBrowser.selectedBrowser.focus();
+ }
+ }
+
+ // Enable/Disable auto-hide tabbar
+ gBrowser.tabContainer.updateVisibility();
+
+ BookmarkingUI.init();
+ AutoShowBookmarksToolbar.init();
+
+ gPrefService.addObserver(gHomeButton.prefDomain, gHomeButton, false);
+
+ var homeButton = document.getElementById("home-button");
+ gHomeButton.updateTooltip(homeButton);
+
+ let safeMode = document.getElementById("helpSafeMode");
+ if (Services.appinfo.inSafeMode) {
+ safeMode.label = safeMode.getAttribute("stoplabel");
+ safeMode.accesskey = safeMode.getAttribute("stopaccesskey");
+ }
+
+ // BiDi UI
+ gBidiUI = isBidiEnabled();
+ if (gBidiUI) {
+ document.getElementById("documentDirection-separator").hidden = false;
+ document.getElementById("documentDirection-swap").hidden = false;
+ document.getElementById("textfieldDirection-separator").hidden = false;
+ document.getElementById("textfieldDirection-swap").hidden = false;
+ }
+
+ // Setup click-and-hold gestures access to the session history
+ // menus if global click-and-hold isn't turned on
+ if (!getBoolPref("ui.click_hold_context_menus", false))
+ SetClickAndHoldHandlers();
+
+ let NP = {};
+ Cu.import("resource:///modules/NetworkPrioritizer.jsm", NP);
+ NP.trackBrowserWindow(window);
+
+ PlacesToolbarHelper.init();
+
+ ctrlTab.readPref();
+ gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false);
+
+ // Initialize the download manager some time after the app starts so that
+ // auto-resume downloads begin (such as after crashing or quitting with
+ // active downloads) and speeds up the first-load of the download manager UI.
+ // If the user manually opens the download manager before the timeout, the
+ // downloads will start right away, and initializing again won't hurt.
+ setTimeout(function() {
+ try {
+ Cu.import("resource:///modules/DownloadsCommon.jsm", {})
+ .DownloadsCommon.initializeAllDataLinks();
+ Cu.import("resource:///modules/DownloadsTaskbar.jsm", {})
+ .DownloadsTaskbar.registerIndicator(window);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }, 10000);
+
+ // Load the Login Manager data from disk off the main thread, some time
+ // after startup. If the data is required before the timeout, for example
+ // because a restored page contains a password field, it will be loaded on
+ // the main thread, and this initialization request will be ignored.
+ setTimeout(function() {
+ try {
+ Services.logins;
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }, 3000);
+
+ // The object handling the downloads indicator is also initialized here in the
+ // delayed startup function, but the actual indicator element is not loaded
+ // unless there are downloads to be displayed.
+ DownloadsButton.initializeIndicator();
+
+ if (AppConstants.platform != "macosx") {
+ updateEditUIVisibility();
+ let placesContext = document.getElementById("placesContext");
+ placesContext.addEventListener("popupshowing", updateEditUIVisibility, false);
+ placesContext.addEventListener("popuphiding", updateEditUIVisibility, false);
+ }
+
+ LightWeightThemeWebInstaller.init();
+
+ if (Win7Features)
+ Win7Features.onOpenWindow();
+
+ PointerlockFsWarning.init();
+ FullScreen.init();
+ PointerLock.init();
+
+ // initialize the sync UI
+ gSyncUI.init();
+ gFxAccounts.init();
+
+ if (AppConstants.MOZ_DATA_REPORTING)
+ gDataNotificationInfoBar.init();
+
+ gBrowserThumbnails.init();
+
+ gMenuButtonBadgeManager.init();
+
+ gMenuButtonUpdateBadge.init();
+
+ window.addEventListener("mousemove", MousePosTracker, false);
+ window.addEventListener("dragover", MousePosTracker, false);
+
+ gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
+ gNavToolbox.addEventListener("customizationchange", CustomizationHandler);
+ gNavToolbox.addEventListener("customizationending", CustomizationHandler);
+
+ // End startup crash tracking after a delay to catch crashes while restoring
+ // tabs and to postpone saving the pref to disk.
+ try {
+ const startupCrashEndDelay = 30 * 1000;
+ setTimeout(Services.startup.trackStartupCrashEnd, startupCrashEndDelay);
+ } catch (ex) {
+ Cu.reportError("Could not end startup crash tracking: " + ex);
+ }
+
+ // Delay this a minute because there's no rush
+ setTimeout(() => {
+ this.gmpInstallManager = new GMPInstallManager();
+ // We don't really care about the results, if someone is interested they
+ // can check the log.
+ this.gmpInstallManager.simpleCheckAndInstall().then(null, () => {});
+ }, 1000 * 60);
+
+ // Report via telemetry whether we're able to play MP4/H.264/AAC video.
+ // We suspect that some Windows users have a broken or have not installed
+ // Windows Media Foundation, and we'd like to know how many. We'd also like
+ // to know how good our coverage is on other platforms.
+ // Note: we delay by 90 seconds reporting this, as calling canPlayType()
+ // on Windows will cause DLLs to load, i.e. cause disk I/O.
+ setTimeout(() => {
+ let v = document.createElementNS("http://www.w3.org/1999/xhtml", "video");
+ let aacWorks = v.canPlayType("audio/mp4") != "";
+ Services.telemetry.getHistogramById("VIDEO_CAN_CREATE_AAC_DECODER").add(aacWorks);
+ let h264Works = v.canPlayType("video/mp4") != "";
+ Services.telemetry.getHistogramById("VIDEO_CAN_CREATE_H264_DECODER").add(h264Works);
+ }, 90 * 1000);
+
+ SessionStore.promiseInitialized.then(() => {
+ // Bail out if the window has been closed in the meantime.
+ if (window.closed) {
+ return;
+ }
+
+ // Enable the Restore Last Session command if needed
+ RestoreLastSessionObserver.init();
+
+ SocialUI.init();
+
+ // Start monitoring slow add-ons
+ AddonWatcher.init();
+
+ // Telemetry for master-password - we do this after 5 seconds as it
+ // can cause IO if NSS/PSM has not already initialized.
+ setTimeout(() => {
+ if (window.closed) {
+ return;
+ }
+ let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"]
+ .getService(Ci.nsIPKCS11ModuleDB);
+ let slot = secmodDB.findSlotByName("");
+ let mpEnabled = slot &&
+ slot.status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED &&
+ slot.status != Ci.nsIPKCS11Slot.SLOT_READY;
+ if (mpEnabled) {
+ Services.telemetry.getHistogramById("MASTER_PASSWORD_ENABLED").add(mpEnabled);
+ }
+ }, 5000);
+
+ PanicButtonNotifier.init();
+ });
+
+ gBrowser.tabContainer.addEventListener("TabSelect", function() {
+ for (let panel of document.querySelectorAll("panel[tabspecific='true']")) {
+ if (panel.state == "open") {
+ panel.hidePopup();
+ }
+ }
+ });
+
+ this.delayedStartupFinished = true;
+
+ Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
+ TelemetryTimestamps.add("delayedStartupFinished");
+ },
+
+ // Returns the URI(s) to load at startup.
+ _getUriToLoad: function () {
+ // window.arguments[0]: URI to load (string), or an nsIArray of
+ // nsISupportsStrings to load, or a xul:tab of
+ // a tabbrowser, which will be replaced by this
+ // window (for this case, all other arguments are
+ // ignored).
+ if (!window.arguments || !window.arguments[0])
+ return null;
+
+ let uri = window.arguments[0];
+ let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"]
+ .getService(Ci.nsISessionStartup);
+ let defaultArgs = Cc["@mozilla.org/browser/clh;1"]
+ .getService(Ci.nsIBrowserHandler)
+ .defaultArgs;
+
+ // If the given URI matches defaultArgs (the default homepage) we want
+ // to block its load if we're going to restore a session anyway.
+ if (uri == defaultArgs && sessionStartup.willOverrideHomepage)
+ return null;
+
+ return uri;
+ },
+
+ onUnload: function() {
+ // In certain scenarios it's possible for unload to be fired before onload,
+ // (e.g. if the window is being closed after browser.js loads but before the
+ // load completes). In that case, there's nothing to do here.
+ if (!this._loadHandled)
+ return;
+
+ // First clean up services initialized in gBrowserInit.onLoad (or those whose
+ // uninit methods don't depend on the services having been initialized).
+
+ CombinedStopReload.uninit();
+
+ gGestureSupport.init(false);
+
+ gHistorySwipeAnimation.uninit();
+
+ FullScreen.uninit();
+
+ gFxAccounts.uninit();
+
+ Services.obs.removeObserver(gPluginHandler.NPAPIPluginCrashed, "plugin-crashed");
+
+ try {
+ gBrowser.removeProgressListener(window.XULBrowserWindow);
+ gBrowser.removeTabsProgressListener(window.TabsProgressListener);
+ } catch (ex) {
+ }
+
+ PlacesToolbarHelper.uninit();
+
+ BookmarkingUI.uninit();
+
+ TabsInTitlebar.uninit();
+
+ ToolbarIconColor.uninit();
+
+ TabletModeUpdater.uninit();
+
+ gTabletModePageCounter.finish();
+
+ BrowserOnClick.uninit();
+
+ FeedHandler.uninit();
+
+ DevEdition.uninit();
+
+ TrackingProtection.uninit();
+
+ RefreshBlocker.uninit();
+
+ CaptivePortalWatcher.uninit();
+
+ gMenuButtonUpdateBadge.uninit();
+
+ gMenuButtonBadgeManager.uninit();
+
+ SidebarUI.uninit();
+
+ // Now either cancel delayedStartup, or clean up the services initialized from
+ // it.
+ if (this._boundDelayedStartup) {
+ this._cancelDelayedStartup();
+ } else {
+ if (Win7Features)
+ Win7Features.onCloseWindow();
+
+ gPrefService.removeObserver(ctrlTab.prefName, ctrlTab);
+ ctrlTab.uninit();
+ SocialUI.uninit();
+ gBrowserThumbnails.uninit();
+ FullZoom.destroy();
+
+ Services.obs.removeObserver(gIdentityHandler, "perm-changed");
+ Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-origin-blocked");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-confirmation");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
+ window.messageManager.removeMessageListener("Browser:URIFixup", gKeywordURIFixup);
+ window.messageManager.removeMessageListener("Browser:LoadURI", RedirectLoad);
+
+ try {
+ gPrefService.removeObserver(gHomeButton.prefDomain, gHomeButton);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ if (this.gmpInstallManager) {
+ this.gmpInstallManager.uninit();
+ }
+
+ BrowserOffline.uninit();
+ IndexedDBPromptHelper.uninit();
+ LightweightThemeListener.uninit();
+ PanelUI.uninit();
+ AutoShowBookmarksToolbar.uninit();
+ }
+
+ // Final window teardown, do this last.
+ window.XULBrowserWindow = null;
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIXULWindow)
+ .XULBrowserWindow = null;
+ window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = null;
+ },
+};
+
+if (AppConstants.platform == "macosx") {
+ // nonBrowserWindowStartup(), nonBrowserWindowDelayedStartup(), and
+ // nonBrowserWindowShutdown() are used for non-browser windows in
+ // macBrowserOverlay
+ gBrowserInit.nonBrowserWindowStartup = function() {
+ // Disable inappropriate commands / submenus
+ var disabledItems = ['Browser:SavePage',
+ 'Browser:SendLink', 'cmd_pageSetup', 'cmd_print', 'cmd_find', 'cmd_findAgain',
+ 'viewToolbarsMenu', 'viewSidebarMenuMenu', 'Browser:Reload',
+ 'viewFullZoomMenu', 'pageStyleMenu', 'charsetMenu', 'View:PageSource', 'View:FullScreen',
+ 'viewHistorySidebar', 'Browser:AddBookmarkAs', 'Browser:BookmarkAllTabs',
+ 'View:PageInfo'];
+ var element;
+
+ for (let disabledItem of disabledItems) {
+ element = document.getElementById(disabledItem);
+ if (element)
+ element.setAttribute("disabled", "true");
+ }
+
+ // If no windows are active (i.e. we're the hidden window), disable the close, minimize
+ // and zoom menu commands as well
+ if (window.location.href == "chrome://browser/content/hiddenWindow.xul") {
+ var hiddenWindowDisabledItems = ['cmd_close', 'minimizeWindow', 'zoomWindow'];
+ for (let hiddenWindowDisabledItem of hiddenWindowDisabledItems) {
+ element = document.getElementById(hiddenWindowDisabledItem);
+ if (element)
+ element.setAttribute("disabled", "true");
+ }
+
+ // also hide the window-list separator
+ element = document.getElementById("sep-window-list");
+ element.setAttribute("hidden", "true");
+
+ // Setup the dock menu.
+ let dockMenuElement = document.getElementById("menu_mac_dockmenu");
+ if (dockMenuElement != null) {
+ let nativeMenu = Cc["@mozilla.org/widget/standalonenativemenu;1"]
+ .createInstance(Ci.nsIStandaloneNativeMenu);
+
+ try {
+ nativeMenu.init(dockMenuElement);
+
+ let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"]
+ .getService(Ci.nsIMacDockSupport);
+ dockSupport.dockMenu = nativeMenu;
+ }
+ catch (e) {
+ }
+ }
+ }
+
+ if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ document.getElementById("macDockMenuNewWindow").hidden = true;
+ }
+
+ this._delayedStartupTimeoutId = setTimeout(this.nonBrowserWindowDelayedStartup.bind(this), 0);
+ };
+
+ gBrowserInit.nonBrowserWindowDelayedStartup = function() {
+ this._delayedStartupTimeoutId = null;
+
+ // initialise the offline listener
+ BrowserOffline.init();
+
+ // initialize the private browsing UI
+ gPrivateBrowsingUI.init();
+
+ // initialize the sync UI
+ gSyncUI.init();
+
+ if (AppConstants.E10S_TESTING_ONLY) {
+ gRemoteTabsUI.init();
+ }
+ };
+
+ gBrowserInit.nonBrowserWindowShutdown = function() {
+ let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"]
+ .getService(Ci.nsIMacDockSupport);
+ dockSupport.dockMenu = null;
+
+ // If nonBrowserWindowDelayedStartup hasn't run yet, we have no work to do -
+ // just cancel the pending timeout and return;
+ if (this._delayedStartupTimeoutId) {
+ clearTimeout(this._delayedStartupTimeoutId);
+ return;
+ }
+
+ BrowserOffline.uninit();
+ };
+}
+
+
+/* Legacy global init functions */
+var BrowserStartup = gBrowserInit.onLoad.bind(gBrowserInit);
+var BrowserShutdown = gBrowserInit.onUnload.bind(gBrowserInit);
+
+if (AppConstants.platform == "macosx") {
+ var nonBrowserWindowStartup = gBrowserInit.nonBrowserWindowStartup.bind(gBrowserInit);
+ var nonBrowserWindowDelayedStartup = gBrowserInit.nonBrowserWindowDelayedStartup.bind(gBrowserInit);
+ var nonBrowserWindowShutdown = gBrowserInit.nonBrowserWindowShutdown.bind(gBrowserInit);
+}
+
+function HandleAppCommandEvent(evt) {
+ switch (evt.command) {
+ case "Back":
+ BrowserBack();
+ break;
+ case "Forward":
+ BrowserForward();
+ break;
+ case "Reload":
+ BrowserReloadSkipCache();
+ break;
+ case "Stop":
+ if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true")
+ BrowserStop();
+ break;
+ case "Search":
+ BrowserSearch.webSearch();
+ break;
+ case "Bookmarks":
+ SidebarUI.toggle("viewBookmarksSidebar");
+ break;
+ case "Home":
+ BrowserHome();
+ break;
+ case "New":
+ BrowserOpenTab();
+ break;
+ case "Close":
+ BrowserCloseTabOrWindow();
+ break;
+ case "Find":
+ gFindBar.onFindCommand();
+ break;
+ case "Help":
+ openHelpLink('firefox-help');
+ break;
+ case "Open":
+ BrowserOpenFileWindow();
+ break;
+ case "Print":
+ PrintUtils.printWindow(gBrowser.selectedBrowser.outerWindowID,
+ gBrowser.selectedBrowser);
+ break;
+ case "Save":
+ saveBrowser(gBrowser.selectedBrowser);
+ break;
+ case "SendMail":
+ MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
+ break;
+ default:
+ return;
+ }
+ evt.stopPropagation();
+ evt.preventDefault();
+}
+
+function gotoHistoryIndex(aEvent) {
+ let index = aEvent.target.getAttribute("index");
+ if (!index)
+ return false;
+
+ let where = whereToOpenLink(aEvent);
+
+ if (where == "current") {
+ // Normal click. Go there in the current tab and update session history.
+
+ try {
+ gBrowser.gotoIndex(index);
+ }
+ catch (ex) {
+ return false;
+ }
+ return true;
+ }
+ // Modified click. Go there in a new tab/window.
+
+ let historyindex = aEvent.target.getAttribute("historyindex");
+ duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex));
+ return true;
+}
+
+function BrowserForward(aEvent) {
+ let where = whereToOpenLink(aEvent, false, true);
+
+ if (where == "current") {
+ try {
+ gBrowser.goForward();
+ }
+ catch (ex) {
+ }
+ }
+ else {
+ duplicateTabIn(gBrowser.selectedTab, where, 1);
+ }
+}
+
+function BrowserBack(aEvent) {
+ let where = whereToOpenLink(aEvent, false, true);
+
+ if (where == "current") {
+ try {
+ gBrowser.goBack();
+ }
+ catch (ex) {
+ }
+ }
+ else {
+ duplicateTabIn(gBrowser.selectedTab, where, -1);
+ }
+}
+
+function BrowserHandleBackspace()
+{
+ switch (gPrefService.getIntPref("browser.backspace_action")) {
+ case 0:
+ BrowserBack();
+ break;
+ case 1:
+ goDoCommand("cmd_scrollPageUp");
+ break;
+ }
+}
+
+function BrowserHandleShiftBackspace()
+{
+ switch (gPrefService.getIntPref("browser.backspace_action")) {
+ case 0:
+ BrowserForward();
+ break;
+ case 1:
+ goDoCommand("cmd_scrollPageDown");
+ break;
+ }
+}
+
+function BrowserStop() {
+ const stopFlags = nsIWebNavigation.STOP_ALL;
+ gBrowser.webNavigation.stop(stopFlags);
+}
+
+function BrowserReloadOrDuplicate(aEvent) {
+ let metaKeyPressed = AppConstants.platform == "macosx"
+ ? aEvent.metaKey
+ : aEvent.ctrlKey;
+ var backgroundTabModifier = aEvent.button == 1 || metaKeyPressed;
+
+ if (aEvent.shiftKey && !backgroundTabModifier) {
+ BrowserReloadSkipCache();
+ return;
+ }
+
+ let where = whereToOpenLink(aEvent, false, true);
+ if (where == "current")
+ BrowserReload();
+ else
+ duplicateTabIn(gBrowser.selectedTab, where);
+}
+
+function BrowserReload() {
+ if (gBrowser.currentURI.schemeIs("view-source")) {
+ // Bug 1167797: For view source, we always skip the cache
+ return BrowserReloadSkipCache();
+ }
+ const reloadFlags = nsIWebNavigation.LOAD_FLAGS_NONE;
+ BrowserReloadWithFlags(reloadFlags);
+}
+
+function BrowserReloadSkipCache() {
+ // Bypass proxy and cache.
+ const reloadFlags = nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+ BrowserReloadWithFlags(reloadFlags);
+}
+
+var BrowserHome = BrowserGoHome;
+function BrowserGoHome(aEvent) {
+ if (aEvent && "button" in aEvent &&
+ aEvent.button == 2) // right-click: do nothing
+ return;
+
+ var homePage = gHomeButton.getHomePage();
+ var where = whereToOpenLink(aEvent, false, true);
+ var urls;
+
+ // Home page should open in a new tab when current tab is an app tab
+ if (where == "current" &&
+ gBrowser &&
+ gBrowser.selectedTab.pinned)
+ where = "tab";
+
+ // openUILinkIn in utilityOverlay.js doesn't handle loading multiple pages
+ switch (where) {
+ case "current":
+ loadOneOrMoreURIs(homePage);
+ break;
+ case "tabshifted":
+ case "tab":
+ urls = homePage.split("|");
+ var loadInBackground = getBoolPref("browser.tabs.loadBookmarksInBackground", false);
+ gBrowser.loadTabs(urls, loadInBackground);
+ break;
+ case "window":
+ OpenBrowserWindow();
+ break;
+ }
+}
+
+function loadOneOrMoreURIs(aURIString)
+{
+ // we're not a browser window, pass the URI string to a new browser window
+ if (window.location.href != getBrowserURL())
+ {
+ window.openDialog(getBrowserURL(), "_blank", "all,dialog=no", aURIString);
+ return;
+ }
+
+ // This function throws for certain malformed URIs, so use exception handling
+ // so that we don't disrupt startup
+ try {
+ gBrowser.loadTabs(aURIString.split("|"), false, true);
+ }
+ catch (e) {
+ }
+}
+
+function focusAndSelectUrlBar() {
+ // In customize mode, the url bar is disabled. If a new tab is opened or the
+ // user switches to a different tab, this function gets called before we've
+ // finished leaving customize mode, and the url bar will still be disabled.
+ // We can't focus it when it's disabled, so we need to re-run ourselves when
+ // we've finished leaving customize mode.
+ if (CustomizationHandler.isExitingCustomizeMode) {
+ gNavToolbox.addEventListener("aftercustomization", function afterCustomize() {
+ gNavToolbox.removeEventListener("aftercustomization", afterCustomize);
+ focusAndSelectUrlBar();
+ });
+
+ return true;
+ }
+
+ if (gURLBar) {
+ if (window.fullScreen)
+ FullScreen.showNavToolbox();
+
+ gURLBar.select();
+ if (document.activeElement == gURLBar.inputField)
+ return true;
+ }
+ return false;
+}
+
+function openLocation() {
+ if (focusAndSelectUrlBar())
+ return;
+
+ if (window.location.href != getBrowserURL()) {
+ var win = getTopWin();
+ if (win) {
+ // If there's an open browser window, it should handle this command
+ win.focus()
+ win.openLocation();
+ }
+ else {
+ // If there are no open browser windows, open a new one
+ window.openDialog("chrome://browser/content/", "_blank",
+ "chrome,all,dialog=no", BROWSER_NEW_TAB_URL);
+ }
+ }
+}
+
+function BrowserOpenTab(event) {
+ let where = "tab";
+ let relatedToCurrent = false;
+
+ if (event) {
+ where = whereToOpenLink(event, false, true);
+
+ switch (where) {
+ case "tab":
+ case "tabshifted":
+ // When accel-click or middle-click are used, open the new tab as
+ // related to the current tab.
+ relatedToCurrent = true;
+ break;
+ case "current":
+ where = "tab";
+ break;
+ }
+ }
+
+ openUILinkIn(BROWSER_NEW_TAB_URL, where, { relatedToCurrent });
+}
+
+/* Called from the openLocation dialog. This allows that dialog to instruct
+ its opener to open a new window and then step completely out of the way.
+ Anything less byzantine is causing horrible crashes, rather believably,
+ though oddly only on Linux. */
+function delayedOpenWindow(chrome, flags, href, postData)
+{
+ // The other way to use setTimeout,
+ // setTimeout(openDialog, 10, chrome, "_blank", flags, url),
+ // doesn't work here. The extra "magic" extra argument setTimeout adds to
+ // the callback function would confuse gBrowserInit.onLoad() by making
+ // window.arguments[1] be an integer instead of null.
+ setTimeout(function() { openDialog(chrome, "_blank", flags, href, null, null, postData); }, 10);
+}
+
+/* Required because the tab needs time to set up its content viewers and get the load of
+ the URI kicked off before becoming the active content area. */
+function delayedOpenTab(aUrl, aReferrer, aCharset, aPostData, aAllowThirdPartyFixup)
+{
+ gBrowser.loadOneTab(aUrl, {
+ referrerURI: aReferrer,
+ charset: aCharset,
+ postData: aPostData,
+ inBackground: false,
+ allowThirdPartyFixup: aAllowThirdPartyFixup});
+}
+
+var gLastOpenDirectory = {
+ _lastDir: null,
+ get path() {
+ if (!this._lastDir || !this._lastDir.exists()) {
+ try {
+ this._lastDir = gPrefService.getComplexValue("browser.open.lastDir",
+ Ci.nsILocalFile);
+ if (!this._lastDir.exists())
+ this._lastDir = null;
+ }
+ catch (e) {}
+ }
+ return this._lastDir;
+ },
+ set path(val) {
+ try {
+ if (!val || !val.isDirectory())
+ return;
+ } catch (e) {
+ return;
+ }
+ this._lastDir = val.clone();
+
+ // Don't save the last open directory pref inside the Private Browsing mode
+ if (!PrivateBrowsingUtils.isWindowPrivate(window))
+ gPrefService.setComplexValue("browser.open.lastDir", Ci.nsILocalFile,
+ this._lastDir);
+ },
+ reset: function() {
+ this._lastDir = null;
+ }
+};
+
+function BrowserOpenFileWindow()
+{
+ // Get filepicker component.
+ try {
+ const nsIFilePicker = Ci.nsIFilePicker;
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult == nsIFilePicker.returnOK) {
+ try {
+ if (fp.file) {
+ gLastOpenDirectory.path =
+ fp.file.parent.QueryInterface(Ci.nsILocalFile);
+ }
+ } catch (ex) {
+ }
+ openUILinkIn(fp.fileURL.spec, "current");
+ }
+ };
+
+ fp.init(window, gNavigatorBundle.getString("openFile"),
+ nsIFilePicker.modeOpen);
+ fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText |
+ nsIFilePicker.filterImages | nsIFilePicker.filterXML |
+ nsIFilePicker.filterHTML);
+ fp.displayDirectory = gLastOpenDirectory.path;
+ fp.open(fpCallback);
+ } catch (ex) {
+ }
+}
+
+function BrowserCloseTabOrWindow() {
+ // If we're not a browser window, just close the window
+ if (window.location.href != getBrowserURL()) {
+ closeWindow(true);
+ return;
+ }
+
+ // If the current tab is the last one, this will close the window.
+ gBrowser.removeCurrentTab({animate: true});
+}
+
+function BrowserTryToCloseWindow()
+{
+ if (WindowIsClosing())
+ window.close(); // WindowIsClosing does all the necessary checks
+}
+
+function loadURI(uri, referrer, postData, allowThirdPartyFixup, referrerPolicy,
+ userContextId, originPrincipal, forceAboutBlankViewerInCurrent) {
+ try {
+ openLinkIn(uri, "current",
+ { referrerURI: referrer,
+ referrerPolicy: referrerPolicy,
+ postData: postData,
+ allowThirdPartyFixup: allowThirdPartyFixup,
+ userContextId: userContextId,
+ originPrincipal,
+ forceAboutBlankViewerInCurrent,
+ });
+ } catch (e) {}
+}
+
+/**
+ * Given a string, will generate a more appropriate urlbar value if a Places
+ * keyword or a search alias is found at the beginning of it.
+ *
+ * @param url
+ * A string that may begin with a keyword or an alias.
+ *
+ * @return {Promise}
+ * @resolves { url, postData, mayInheritPrincipal }. If it's not possible
+ * to discern a keyword or an alias, url will be the input string.
+ */
+function getShortcutOrURIAndPostData(url, callback = null) {
+ if (callback) {
+ Deprecated.warning("Please use the Promise returned by " +
+ "getShortcutOrURIAndPostData() instead of passing a " +
+ "callback",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1100294");
+ }
+ return Task.spawn(function* () {
+ let mayInheritPrincipal = false;
+ let postData = null;
+ // Split on the first whitespace.
+ let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2);
+
+ if (!keyword) {
+ return { url, postData, mayInheritPrincipal };
+ }
+
+ let engine = Services.search.getEngineByAlias(keyword);
+ if (engine) {
+ let submission = engine.getSubmission(param, null, "keyword");
+ return { url: submission.uri.spec,
+ postData: submission.postData,
+ mayInheritPrincipal };
+ }
+
+ // A corrupt Places database could make this throw, breaking navigation
+ // from the location bar.
+ let entry = null;
+ try {
+ entry = yield PlacesUtils.keywords.fetch(keyword);
+ } catch (ex) {
+ Cu.reportError(`Unable to fetch Places keyword "${keyword}": ${ex}`);
+ }
+ if (!entry || !entry.url) {
+ // This is not a Places keyword.
+ return { url, postData, mayInheritPrincipal };
+ }
+
+ try {
+ [url, postData] =
+ yield BrowserUtils.parseUrlAndPostData(entry.url.href,
+ entry.postData,
+ param);
+ if (postData) {
+ postData = getPostDataStream(postData);
+ }
+
+ // Since this URL came from a bookmark, it's safe to let it inherit the
+ // current document's principal.
+ mayInheritPrincipal = true;
+ } catch (ex) {
+ // It was not possible to bind the param, just use the original url value.
+ }
+
+ return { url, postData, mayInheritPrincipal };
+ }).then(data => {
+ if (callback) {
+ callback(data);
+ }
+ return data;
+ });
+}
+
+function getPostDataStream(aPostDataString,
+ aType = "application/x-www-form-urlencoded") {
+ let dataStream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ dataStream.data = aPostDataString;
+
+ let mimeStream = Cc["@mozilla.org/network/mime-input-stream;1"]
+ .createInstance(Ci.nsIMIMEInputStream);
+ mimeStream.addHeader("Content-Type", aType);
+ mimeStream.addContentLength = true;
+ mimeStream.setData(dataStream);
+ return mimeStream.QueryInterface(Ci.nsIInputStream);
+}
+
+function getLoadContext() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+}
+
+function readFromClipboard()
+{
+ var url;
+
+ try {
+ // Create transferable that will transfer the text.
+ var trans = Components.classes["@mozilla.org/widget/transferable;1"]
+ .createInstance(Components.interfaces.nsITransferable);
+ trans.init(getLoadContext());
+
+ trans.addDataFlavor("text/unicode");
+
+ // If available, use selection clipboard, otherwise global one
+ if (Services.clipboard.supportsSelectionClipboard())
+ Services.clipboard.getData(trans, Services.clipboard.kSelectionClipboard);
+ else
+ Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
+
+ var data = {};
+ var dataLen = {};
+ trans.getTransferData("text/unicode", data, dataLen);
+
+ if (data) {
+ data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
+ url = data.data.substring(0, dataLen.value / 2);
+ }
+ } catch (ex) {
+ }
+
+ return url;
+}
+
+/**
+ * Open the View Source dialog.
+ *
+ * @param aArgsOrDocument
+ * Either an object or a Document. Passing a Document is deprecated,
+ * and is not supported with e10s. This function will throw if
+ * aArgsOrDocument is a CPOW.
+ *
+ * If aArgsOrDocument is an object, that object can take the
+ * following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. You only need to provide this if you
+ * want to attempt to retrieve the document source from the network
+ * cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ */
+function BrowserViewSourceOfDocument(aArgsOrDocument) {
+ let args;
+
+ if (aArgsOrDocument instanceof Document) {
+ let doc = aArgsOrDocument;
+ // Deprecated API - callers should pass args object instead.
+ if (Cu.isCrossProcessWrapper(doc)) {
+ throw new Error("BrowserViewSourceOfDocument cannot accept a CPOW " +
+ "as a document.");
+ }
+
+ let requestor = doc.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor);
+ let browser = requestor.getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ let outerWindowID = requestor.getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+ let URL = browser.currentURI.spec;
+ args = { browser, outerWindowID, URL };
+ } else {
+ args = aArgsOrDocument;
+ }
+
+ let viewInternal = () => {
+ let inTab = Services.prefs.getBoolPref("view_source.tab");
+ if (inTab) {
+ let tabBrowser = gBrowser;
+ let forceNotRemote = false;
+ if (!tabBrowser) {
+ if (!args.browser) {
+ throw new Error("BrowserViewSourceOfDocument should be passed the " +
+ "subject browser if called from a window without " +
+ "gBrowser defined.");
+ }
+ forceNotRemote = !args.browser.isRemoteBrowser;
+ } else {
+ // Some internal URLs (such as specific chrome: and about: URLs that are
+ // not yet remote ready) cannot be loaded in a remote browser. View
+ // source in tab expects the new view source browser's remoteness to match
+ // that of the original URL, so disable remoteness if necessary for this
+ // URL.
+ let contentProcess = Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
+ forceNotRemote =
+ gMultiProcessBrowser &&
+ !E10SUtils.canLoadURIInProcess(args.URL, contentProcess)
+ }
+
+ // In the case of popups, we need to find a non-popup browser window.
+ if (!tabBrowser || !window.toolbar.visible) {
+ // This returns only non-popup browser windows by default.
+ let browserWindow = RecentWindow.getMostRecentBrowserWindow();
+ tabBrowser = browserWindow.gBrowser;
+ }
+
+ // `viewSourceInBrowser` will load the source content from the page
+ // descriptor for the tab (when possible) or fallback to the network if
+ // that fails. Either way, the view source module will manage the tab's
+ // location, so use "about:blank" here to avoid unnecessary redundant
+ // requests.
+ let tab = tabBrowser.loadOneTab("about:blank", {
+ relatedToCurrent: true,
+ inBackground: false,
+ forceNotRemote,
+ relatedBrowser: args.browser
+ });
+ args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
+ top.gViewSourceUtils.viewSourceInBrowser(args);
+ } else {
+ top.gViewSourceUtils.viewSource(args);
+ }
+ }
+
+ // Check if external view source is enabled. If so, try it. If it fails,
+ // fallback to internal view source.
+ if (Services.prefs.getBoolPref("view_source.editor.external")) {
+ top.gViewSourceUtils
+ .openInExternalEditor(args, null, null, null, result => {
+ if (!result) {
+ viewInternal();
+ }
+ });
+ } else {
+ // Display using internal view source
+ viewInternal();
+ }
+}
+
+/**
+ * Opens the View Source dialog for the source loaded in the root
+ * top-level document of the browser. This is really just a
+ * convenience wrapper around BrowserViewSourceOfDocument.
+ *
+ * @param browser
+ * The browser that we want to load the source of.
+ */
+function BrowserViewSource(browser) {
+ BrowserViewSourceOfDocument({
+ browser: browser,
+ outerWindowID: browser.outerWindowID,
+ URL: browser.currentURI.spec,
+ });
+}
+
+// documentURL - URL of the document to view, or null for this window's document
+// initialTab - name of the initial tab to display, or null for the first tab
+// imageElement - image to load in the Media Tab of the Page Info window; can be null/omitted
+// frameOuterWindowID - the id of the frame that the context menu opened in; can be null/omitted
+// browser - the browser containing the document we're interested in inspecting; can be null/omitted
+function BrowserPageInfo(documentURL, initialTab, imageElement, frameOuterWindowID, browser) {
+ if (documentURL instanceof HTMLDocument) {
+ Deprecated.warning("Please pass the location URL instead of the document " +
+ "to BrowserPageInfo() as the first argument.",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1238180");
+ documentURL = documentURL.location;
+ }
+
+ let args = { initialTab, imageElement, frameOuterWindowID, browser };
+ var windows = Services.wm.getEnumerator("Browser:page-info");
+
+ documentURL = documentURL || window.gBrowser.selectedBrowser.currentURI.spec;
+
+ // Check for windows matching the url
+ while (windows.hasMoreElements()) {
+ var currentWindow = windows.getNext();
+ if (currentWindow.closed) {
+ continue;
+ }
+ if (currentWindow.document.documentElement.getAttribute("relatedUrl") == documentURL) {
+ currentWindow.focus();
+ currentWindow.resetPageInfo(args);
+ return currentWindow;
+ }
+ }
+
+ // We didn't find a matching window, so open a new one.
+ return openDialog("chrome://browser/content/pageinfo/pageInfo.xul", "",
+ "chrome,toolbar,dialog=no,resizable", args);
+}
+
+function URLBarSetURI(aURI) {
+ var value = gBrowser.userTypedValue;
+ var valid = false;
+
+ if (value == null) {
+ let uri = aURI || gBrowser.currentURI;
+ // Strip off "wyciwyg://" and passwords for the location bar
+ try {
+ uri = Services.uriFixup.createExposableURI(uri);
+ } catch (e) {}
+
+ // Replace initial page URIs with an empty string
+ // 1. only if there's no opener (bug 370555).
+ // 2. if remote newtab is enabled and it's the default remote newtab page
+ let defaultRemoteURL = gAboutNewTabService.remoteEnabled &&
+ uri.spec === gAboutNewTabService.newTabURL;
+ if ((gInitialPages.includes(uri.spec) || defaultRemoteURL) &&
+ checkEmptyPageOrigin(gBrowser.selectedBrowser, uri)) {
+ value = "";
+ } else {
+ // We should deal with losslessDecodeURI throwing for exotic URIs
+ try {
+ value = losslessDecodeURI(uri);
+ } catch (ex) {
+ value = "about:blank";
+ }
+ }
+
+ valid = !isBlankPageURL(uri.spec);
+ }
+
+ let isDifferentValidValue = valid && value != gURLBar.value;
+ gURLBar.value = value;
+ gURLBar.valueIsTyped = !valid;
+ if (isDifferentValidValue) {
+ gURLBar.selectionStart = gURLBar.selectionEnd = 0;
+ }
+
+ SetPageProxyState(valid ? "valid" : "invalid");
+}
+
+function losslessDecodeURI(aURI) {
+ let scheme = aURI.scheme;
+ if (scheme == "moz-action")
+ throw new Error("losslessDecodeURI should never get a moz-action URI");
+
+ var value = aURI.spec;
+
+ let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
+ // Try to decode as UTF-8 if there's no encoding sequence that we would break.
+ if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
+ if (decodeASCIIOnly) {
+ // This only decodes ascii characters (hex) 20-7e, except 25 (%).
+ // This avoids both cases stipulated below (%-related issues, and \r, \n
+ // and \t, which would be %0d, %0a and %09, respectively) as well as any
+ // non-US-ascii characters.
+ value = value.replace(/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, decodeURI);
+ } else {
+ try {
+ value = decodeURI(value)
+ // 1. decodeURI decodes %25 to %, which creates unintended
+ // encoding sequences. Re-encode it, unless it's part of
+ // a sequence that survived decodeURI, i.e. one for:
+ // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
+ // (RFC 3987 section 3.2)
+ // 2. Re-encode select whitespace so that it doesn't get eaten
+ // away by the location bar (bug 410726). Re-encode all
+ // adjacent whitespace, to prevent spoofing attempts where
+ // invisible characters would push part of the URL to
+ // overflow the location bar (bug 1395508).
+ .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]|\s(?=\s)|\s$/ig,
+ encodeURIComponent);
+ } catch (e) {}
+ }
+ }
+
+ // Encode invisible characters (C0/C1 control characters, U+007F [DEL],
+ // U+00A0 [no-break space], line and paragraph separator,
+ // object replacement character) (bug 452979, bug 909264)
+ value = value.replace(/[\u0000-\u001f\u007f-\u00a0\u2028\u2029\ufffc]/g,
+ encodeURIComponent);
+
+ // Encode default ignorable characters (bug 546013)
+ // except ZWNJ (U+200C) and ZWJ (U+200D) (bug 582186).
+ // This includes all bidirectional formatting characters.
+ // (RFC 3987 sections 3.2 and 4.1 paragraph 6)
+ value = value.replace(/[\u00ad\u034f\u061c\u115f-\u1160\u17b4-\u17b5\u180b-\u180d\u200b\u200e-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]/g,
+ encodeURIComponent);
+ return value;
+}
+
+function UpdateUrlbarSearchSplitterState()
+{
+ var splitter = document.getElementById("urlbar-search-splitter");
+ var urlbar = document.getElementById("urlbar-container");
+ var searchbar = document.getElementById("search-container");
+
+ if (document.documentElement.getAttribute("customizing") == "true") {
+ if (splitter) {
+ splitter.remove();
+ }
+ return;
+ }
+
+ // If the splitter is already in the right place, we don't need to do anything:
+ if (splitter &&
+ ((splitter.nextSibling == searchbar && splitter.previousSibling == urlbar) ||
+ (splitter.nextSibling == urlbar && splitter.previousSibling == searchbar))) {
+ return;
+ }
+
+ var ibefore = null;
+ if (urlbar && searchbar) {
+ if (urlbar.nextSibling == searchbar)
+ ibefore = searchbar;
+ else if (searchbar.nextSibling == urlbar)
+ ibefore = urlbar;
+ }
+
+ if (ibefore) {
+ if (!splitter) {
+ splitter = document.createElement("splitter");
+ splitter.id = "urlbar-search-splitter";
+ splitter.setAttribute("resizebefore", "flex");
+ splitter.setAttribute("resizeafter", "flex");
+ splitter.setAttribute("skipintoolbarset", "true");
+ splitter.setAttribute("overflows", "false");
+ splitter.className = "chromeclass-toolbar-additional";
+ }
+ urlbar.parentNode.insertBefore(splitter, ibefore);
+ } else if (splitter)
+ splitter.parentNode.removeChild(splitter);
+}
+
+function UpdatePageProxyState()
+{
+ if (gURLBar && gURLBar.value != gLastValidURLStr)
+ SetPageProxyState("invalid");
+}
+
+function SetPageProxyState(aState)
+{
+ if (!gURLBar)
+ return;
+
+ gURLBar.setAttribute("pageproxystate", aState);
+
+ // the page proxy state is set to valid via OnLocationChange, which
+ // gets called when we switch tabs.
+ if (aState == "valid") {
+ gLastValidURLStr = gURLBar.value;
+ gURLBar.addEventListener("input", UpdatePageProxyState, false);
+ } else if (aState == "invalid") {
+ gURLBar.removeEventListener("input", UpdatePageProxyState, false);
+ }
+}
+
+function PageProxyClickHandler(aEvent)
+{
+ if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste"))
+ middleMousePaste(aEvent);
+}
+
+var gMenuButtonBadgeManager = {
+ BADGEID_APPUPDATE: "update",
+ BADGEID_DOWNLOAD: "download",
+ BADGEID_FXA: "fxa",
+
+ fxaBadge: null,
+ downloadBadge: null,
+ appUpdateBadge: null,
+
+ init: function () {
+ PanelUI.panel.addEventListener("popupshowing", this, true);
+ },
+
+ uninit: function () {
+ PanelUI.panel.removeEventListener("popupshowing", this, true);
+ },
+
+ handleEvent: function (e) {
+ if (e.type === "popupshowing") {
+ this.clearBadges();
+ }
+ },
+
+ _showBadge: function () {
+ let badgeToShow = this.downloadBadge || this.appUpdateBadge || this.fxaBadge;
+
+ if (badgeToShow) {
+ PanelUI.menuButton.setAttribute("badge-status", badgeToShow);
+ } else {
+ PanelUI.menuButton.removeAttribute("badge-status");
+ }
+ },
+
+ _changeBadge: function (badgeId, badgeStatus = null) {
+ if (badgeId == this.BADGEID_APPUPDATE) {
+ this.appUpdateBadge = badgeStatus;
+ } else if (badgeId == this.BADGEID_DOWNLOAD) {
+ this.downloadBadge = badgeStatus;
+ } else if (badgeId == this.BADGEID_FXA) {
+ this.fxaBadge = badgeStatus;
+ } else {
+ Cu.reportError("The badge ID '" + badgeId + "' is unknown!");
+ }
+ this._showBadge();
+ },
+
+ addBadge: function (badgeId, badgeStatus) {
+ if (!badgeStatus) {
+ Cu.reportError("badgeStatus must be defined");
+ return;
+ }
+ this._changeBadge(badgeId, badgeStatus);
+ },
+
+ removeBadge: function (badgeId) {
+ this._changeBadge(badgeId);
+ },
+
+ clearBadges: function () {
+ this.appUpdateBadge = null;
+ this.downloadBadge = null;
+ this.fxaBadge = null;
+ this._showBadge();
+ }
+};
+
+// Setup the hamburger button badges for updates, if enabled.
+var gMenuButtonUpdateBadge = {
+ enabled: false,
+ badgeWaitTime: 0,
+ timer: null,
+ cancelObserverRegistered: false,
+
+ init: function () {
+ try {
+ this.enabled = Services.prefs.getBoolPref("app.update.badge");
+ } catch (e) {}
+ if (this.enabled) {
+ try {
+ this.badgeWaitTime = Services.prefs.getIntPref("app.update.badgeWaitTime");
+ } catch (e) {
+ this.badgeWaitTime = 345600; // 4 days
+ }
+ Services.obs.addObserver(this, "update-staged", false);
+ Services.obs.addObserver(this, "update-downloaded", false);
+ }
+ },
+
+ uninit: function () {
+ if (this.timer)
+ this.timer.cancel();
+ if (this.enabled) {
+ Services.obs.removeObserver(this, "update-staged");
+ Services.obs.removeObserver(this, "update-downloaded");
+ this.enabled = false;
+ }
+ if (this.cancelObserverRegistered) {
+ Services.obs.removeObserver(this, "update-canceled");
+ this.cancelObserverRegistered = false;
+ }
+ },
+
+ onMenuPanelCommand: function(event) {
+ if (event.originalTarget.getAttribute("update-status") === "succeeded") {
+ // restart the app
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+ if (!cancelQuit.data) {
+ Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
+ }
+ } else {
+ // open the page for manual update
+ let url = Services.urlFormatter.formatURLPref("app.update.url.manual");
+ openUILinkIn(url, "tab");
+ }
+ },
+
+ observe: function (subject, topic, status) {
+ if (topic == "update-canceled") {
+ this.reset();
+ return;
+ }
+ if (status == "failed") {
+ // Background update has failed, let's show the UI responsible for
+ // prompting the user to update manually.
+ this.uninit();
+ this.displayBadge(false);
+ return;
+ }
+
+ // Give the user badgeWaitTime seconds to react before prompting.
+ this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.timer.initWithCallback(this, this.badgeWaitTime * 1000,
+ this.timer.TYPE_ONE_SHOT);
+ // The timer callback will call uninit() when it completes.
+ },
+
+ notify: function () {
+ // If the update is successfully applied, or if the updater has fallen back
+ // to non-staged updates, add a badge to the hamburger menu to indicate an
+ // update will be applied once the browser restarts.
+ this.uninit();
+ this.displayBadge(true);
+ },
+
+ displayBadge: function (succeeded) {
+ let status = succeeded ? "succeeded" : "failed";
+ let badgeStatus = "update-" + status;
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, badgeStatus);
+
+ let stringId;
+ let updateButtonText;
+ if (succeeded) {
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+ stringId = "appmenu.restartNeeded.description";
+ updateButtonText = gNavigatorBundle.getFormattedString(stringId,
+ [brandShortName]);
+ Services.obs.addObserver(this, "update-canceled", false);
+ this.cancelObserverRegistered = true;
+ } else {
+ stringId = "appmenu.updateFailed.description";
+ updateButtonText = gNavigatorBundle.getString(stringId);
+ }
+
+ let updateButton = document.getElementById("PanelUI-update-status");
+ updateButton.setAttribute("label", updateButtonText);
+ updateButton.setAttribute("update-status", status);
+ updateButton.hidden = false;
+ },
+
+ reset: function () {
+ gMenuButtonBadgeManager.removeBadge(
+ gMenuButtonBadgeManager.BADGEID_APPUPDATE);
+ let updateButton = document.getElementById("PanelUI-update-status");
+ updateButton.hidden = true;
+ this.uninit();
+ this.init();
+ }
+};
+
+// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
+const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED = 2;
+const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3;
+const TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND = 4;
+const TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND = 5;
+
+const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
+
+const PREF_SSL_IMPACT = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => {
+ return prefs.concat(Services.prefs.getChildList(root));
+}, []);
+
+/**
+ * Handle command events bubbling up from error page content
+ * or from about:newtab or from remote error pages that invoke
+ * us via async messaging.
+ */
+var BrowserOnClick = {
+ init: function () {
+ let mm = window.messageManager;
+ mm.addMessageListener("Browser:CertExceptionError", this);
+ mm.addMessageListener("Browser:OpenCaptivePortalPage", this);
+ mm.addMessageListener("Browser:SiteBlockedError", this);
+ mm.addMessageListener("Browser:EnableOnlineMode", this);
+ mm.addMessageListener("Browser:SendSSLErrorReport", this);
+ mm.addMessageListener("Browser:SetSSLErrorReportAuto", this);
+ mm.addMessageListener("Browser:ResetSSLPreferences", this);
+ mm.addMessageListener("Browser:SSLErrorReportTelemetry", this);
+ mm.addMessageListener("Browser:OverrideWeakCrypto", this);
+ mm.addMessageListener("Browser:SSLErrorGoBack", this);
+
+ Services.obs.addObserver(this, "captive-portal-login-abort", false);
+ Services.obs.addObserver(this, "captive-portal-login-success", false);
+ },
+
+ uninit: function () {
+ let mm = window.messageManager;
+ mm.removeMessageListener("Browser:CertExceptionError", this);
+ mm.removeMessageListener("Browser:SiteBlockedError", this);
+ mm.removeMessageListener("Browser:EnableOnlineMode", this);
+ mm.removeMessageListener("Browser:SendSSLErrorReport", this);
+ mm.removeMessageListener("Browser:SetSSLErrorReportAuto", this);
+ mm.removeMessageListener("Browser:ResetSSLPreferences", this);
+ mm.removeMessageListener("Browser:SSLErrorReportTelemetry", this);
+ mm.removeMessageListener("Browser:OverrideWeakCrypto", this);
+ mm.removeMessageListener("Browser:SSLErrorGoBack", this);
+
+ Services.obs.removeObserver(this, "captive-portal-login-abort");
+ Services.obs.removeObserver(this, "captive-portal-login-success");
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "captive-portal-login-abort":
+ case "captive-portal-login-success":
+ // Broadcast when a captive portal is freed so that error pages
+ // can refresh themselves.
+ window.messageManager.broadcastAsyncMessage("Browser:CaptivePortalFreed");
+ break;
+ }
+ },
+
+ handleEvent: function (event) {
+ if (!event.isTrusted || // Don't trust synthetic events
+ event.button == 2) {
+ return;
+ }
+
+ let originalTarget = event.originalTarget;
+ let ownerDoc = originalTarget.ownerDocument;
+ if (!ownerDoc) {
+ return;
+ }
+
+ if (gMultiProcessBrowser &&
+ ownerDoc.documentURI.toLowerCase() == "about:newtab") {
+ this.onE10sAboutNewTab(event, ownerDoc);
+ }
+ },
+
+ receiveMessage: function (msg) {
+ switch (msg.name) {
+ case "Browser:CertExceptionError":
+ this.onCertError(msg.target, msg.data.elementId,
+ msg.data.isTopFrame, msg.data.location,
+ msg.data.securityInfoAsString);
+ break;
+ case "Browser:OpenCaptivePortalPage":
+ CaptivePortalWatcher.ensureCaptivePortalTab();
+ break;
+ case "Browser:SiteBlockedError":
+ this.onAboutBlocked(msg.data.elementId, msg.data.reason,
+ msg.data.isTopFrame, msg.data.location);
+ break;
+ case "Browser:EnableOnlineMode":
+ if (Services.io.offline) {
+ // Reset network state and refresh the page.
+ Services.io.offline = false;
+ msg.target.reload();
+ }
+ break;
+ case "Browser:SendSSLErrorReport":
+ this.onSSLErrorReport(msg.target,
+ msg.data.uri,
+ msg.data.securityInfo);
+ break;
+ case "Browser:ResetSSLPreferences":
+ for (let prefName of PREF_SSL_IMPACT) {
+ Services.prefs.clearUserPref(prefName);
+ }
+ msg.target.reload();
+ break;
+ case "Browser:SetSSLErrorReportAuto":
+ Services.prefs.setBoolPref("security.ssl.errorReporting.automatic", msg.json.automatic);
+ let bin = TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED;
+ if (msg.json.automatic) {
+ bin = TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED;
+ }
+ Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI").add(bin);
+ break;
+ case "Browser:SSLErrorReportTelemetry":
+ let reportStatus = msg.data.reportStatus;
+ Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI")
+ .add(reportStatus);
+ break;
+ case "Browser:OverrideWeakCrypto":
+ let weakCryptoOverride = Cc["@mozilla.org/security/weakcryptooverride;1"]
+ .getService(Ci.nsIWeakCryptoOverride);
+ weakCryptoOverride.addWeakCryptoOverride(
+ msg.data.uri.host,
+ PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser));
+ break;
+ case "Browser:SSLErrorGoBack":
+ goBackFromErrorPage();
+ break;
+ }
+ },
+
+ onSSLErrorReport: function(browser, uri, securityInfo) {
+ if (!Services.prefs.getBoolPref("security.ssl.errorReporting.enabled")) {
+ Cu.reportError("User requested certificate error report sending, but certificate error reporting is disabled");
+ return;
+ }
+
+ let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ let transportSecurityInfo = serhelper.deserializeObject(securityInfo);
+ transportSecurityInfo.QueryInterface(Ci.nsITransportSecurityInfo)
+
+ let errorReporter = Cc["@mozilla.org/securityreporter;1"]
+ .getService(Ci.nsISecurityReporter);
+ errorReporter.reportTLSError(transportSecurityInfo,
+ uri.host, uri.port);
+ },
+
+ onCertError: function (browser, elementId, isTopFrame, location, securityInfoAsString) {
+ let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
+ let securityInfo;
+
+ switch (elementId) {
+ case "exceptionDialogButton":
+ if (isTopFrame) {
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_CLICK_ADD_EXCEPTION);
+ }
+
+ securityInfo = getSecurityInfo(securityInfoAsString);
+ let sslStatus = securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
+ .SSLStatus;
+ let params = { exceptionAdded : false,
+ sslStatus : sslStatus };
+
+ try {
+ switch (Services.prefs.getIntPref("browser.ssl_override_behavior")) {
+ case 2 : // Pre-fetch & pre-populate
+ params.prefetchCert = true;
+ case 1 : // Pre-populate
+ params.location = location;
+ }
+ } catch (e) {
+ Components.utils.reportError("Couldn't get ssl_override pref: " + e);
+ }
+
+ window.openDialog('chrome://pippki/content/exceptionDialog.xul',
+ '', 'chrome,centerscreen,modal', params);
+
+ // If the user added the exception cert, attempt to reload the page
+ if (params.exceptionAdded) {
+ browser.reload();
+ }
+ break;
+
+ case "returnButton":
+ if (isTopFrame) {
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_GET_ME_OUT_OF_HERE);
+ }
+ goBackFromErrorPage();
+ break;
+
+ case "advancedButton":
+ if (isTopFrame) {
+ secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_UNDERSTAND_RISKS);
+ }
+
+ securityInfo = getSecurityInfo(securityInfoAsString);
+ let errorInfo = getDetailedCertErrorInfo(location,
+ securityInfo);
+ browser.messageManager.sendAsyncMessage( "CertErrorDetails", {
+ code: securityInfo.errorCode,
+ info: errorInfo
+ });
+ break;
+
+ case "copyToClipboard":
+ const gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ securityInfo = getSecurityInfo(securityInfoAsString);
+ let detailedInfo = getDetailedCertErrorInfo(location,
+ securityInfo);
+ gClipboardHelper.copyString(detailedInfo);
+ break;
+
+ }
+ },
+
+ onAboutBlocked: function (elementId, reason, isTopFrame, location) {
+ // Depending on what page we are displaying here (malware/phishing/unwanted)
+ // use the right strings and links for each.
+ let bucketName = "";
+ let sendTelemetry = false;
+ if (reason === 'malware') {
+ sendTelemetry = true;
+ bucketName = "WARNING_MALWARE_PAGE_";
+ } else if (reason === 'phishing') {
+ sendTelemetry = true;
+ bucketName = "WARNING_PHISHING_PAGE_";
+ } else if (reason === 'unwanted') {
+ sendTelemetry = true;
+ bucketName = "WARNING_UNWANTED_PAGE_";
+ }
+ let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
+ let nsISecTel = Ci.nsISecurityUITelemetry;
+ bucketName += isTopFrame ? "TOP_" : "FRAME_";
+ switch (elementId) {
+ case "getMeOutButton":
+ if (sendTelemetry) {
+ secHistogram.add(nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
+ }
+ getMeOutOfHere();
+ break;
+
+ case "reportButton":
+ // This is the "Why is this site blocked" button. We redirect
+ // to the generic page describing phishing/malware protection.
+
+ // We log even if malware/phishing/unwanted info URL couldn't be found:
+ // the measurement is for how many users clicked the WHY BLOCKED button
+ if (sendTelemetry) {
+ secHistogram.add(nsISecTel[bucketName + "WHY_BLOCKED"]);
+ }
+ openHelpLink("phishing-malware", false, "current");
+ break;
+
+ case "ignoreWarningButton":
+ if (gPrefService.getBoolPref("browser.safebrowsing.allowOverride")) {
+ if (sendTelemetry) {
+ secHistogram.add(nsISecTel[bucketName + "IGNORE_WARNING"]);
+ }
+ this.ignoreWarningButton(reason);
+ }
+ break;
+ }
+ },
+
+ /**
+ * This functions prevents navigation from happening directly through the <a>
+ * link in about:newtab (which is loaded in the parent and therefore would load
+ * the next page also in the parent) and instructs the browser to open the url
+ * in the current tab which will make it update the remoteness of the tab.
+ */
+ onE10sAboutNewTab: function(event, ownerDoc) {
+ let isTopFrame = (ownerDoc.defaultView.parent === ownerDoc.defaultView);
+ if (!isTopFrame) {
+ return;
+ }
+
+ let anchorTarget = event.originalTarget.parentNode;
+
+ if (anchorTarget instanceof HTMLAnchorElement &&
+ anchorTarget.classList.contains("newtab-link")) {
+ event.preventDefault();
+ let where = whereToOpenLink(event, false, false);
+ openLinkIn(anchorTarget.href, where, { charset: ownerDoc.characterSet, referrerURI: ownerDoc.documentURIObject });
+ }
+ },
+
+ ignoreWarningButton: function (reason) {
+ // Allow users to override and continue through to the site,
+ // but add a notify bar as a reminder, so that they don't lose
+ // track after, e.g., tab switching.
+ gBrowser.loadURIWithFlags(gBrowser.currentURI.spec,
+ nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
+ null, null, null);
+
+ Services.perms.add(gBrowser.currentURI, "safe-browsing",
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+ let buttons = [{
+ label: gNavigatorBundle.getString("safebrowsing.getMeOutOfHereButton.label"),
+ accessKey: gNavigatorBundle.getString("safebrowsing.getMeOutOfHereButton.accessKey"),
+ callback: function() { getMeOutOfHere(); }
+ }];
+
+ let title;
+ if (reason === 'malware') {
+ title = gNavigatorBundle.getString("safebrowsing.reportedAttackSite");
+ buttons[1] = {
+ label: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.label"),
+ accessKey: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.accessKey"),
+ callback: function() {
+ openUILinkIn(gSafeBrowsing.getReportURL('MalwareMistake'), 'tab');
+ }
+ };
+ } else if (reason === 'phishing') {
+ title = gNavigatorBundle.getString("safebrowsing.deceptiveSite");
+ buttons[1] = {
+ label: gNavigatorBundle.getString("safebrowsing.notADeceptiveSiteButton.label"),
+ accessKey: gNavigatorBundle.getString("safebrowsing.notADeceptiveSiteButton.accessKey"),
+ callback: function() {
+ openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');
+ }
+ };
+ } else if (reason === 'unwanted') {
+ title = gNavigatorBundle.getString("safebrowsing.reportedUnwantedSite");
+ // There is no button for reporting errors since Google doesn't currently
+ // provide a URL endpoint for these reports.
+ }
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let value = "blocked-badware-page";
+
+ let previousNotification = notificationBox.getNotificationWithValue(value);
+ if (previousNotification) {
+ notificationBox.removeNotification(previousNotification);
+ }
+
+ let notification = notificationBox.appendNotification(
+ title,
+ value,
+ "chrome://global/skin/icons/blacklist_favicon.png",
+ notificationBox.PRIORITY_CRITICAL_HIGH,
+ buttons
+ );
+ // Persist the notification until the user removes so it
+ // doesn't get removed on redirects.
+ notification.persistence = -1;
+ },
+};
+
+/**
+ * Re-direct the browser to a known-safe page. This function is
+ * used when, for example, the user browses to a known malware page
+ * and is presented with about:blocked. The "Get me out of here!"
+ * button should take the user to the default start page so that even
+ * when their own homepage is infected, we can get them somewhere safe.
+ */
+function getMeOutOfHere() {
+ gBrowser.loadURI(getDefaultHomePage());
+}
+
+/**
+ * Re-direct the browser to the previous page or a known-safe page if no
+ * previous page is found in history. This function is used when the user
+ * browses to a secure page with certificate issues and is presented with
+ * about:certerror. The "Go Back" button should take the user to the previous
+ * or a default start page so that even when their own homepage is on a server
+ * that has certificate errors, we can get them somewhere safe.
+ */
+function goBackFromErrorPage() {
+ const ss = Cc["@mozilla.org/browser/sessionstore;1"].
+ getService(Ci.nsISessionStore);
+ let state = JSON.parse(ss.getTabState(gBrowser.selectedTab));
+ if (state.index == 1) {
+ // If the unsafe page is the first or the only one in history, go to the
+ // start page.
+ gBrowser.loadURI(getDefaultHomePage());
+ } else {
+ BrowserBack();
+ }
+}
+
+/**
+ * Return the default start page for the cases when the user's own homepage is
+ * infected, so we can get them somewhere safe.
+ */
+function getDefaultHomePage() {
+ // Get the start page from the *default* pref branch, not the user's
+ var prefs = Services.prefs.getDefaultBranch(null);
+ var url = BROWSER_NEW_TAB_URL;
+ try {
+ url = prefs.getComplexValue("browser.startup.homepage",
+ Ci.nsIPrefLocalizedString).data;
+ // If url is a pipe-delimited set of pages, just take the first one.
+ if (url.includes("|"))
+ url = url.split("|")[0];
+ } catch (e) {
+ Components.utils.reportError("Couldn't get homepage pref: " + e);
+ }
+ return url;
+}
+
+function BrowserFullScreen()
+{
+ window.fullScreen = !window.fullScreen;
+}
+
+function mirrorShow(popup) {
+ let services = [];
+ if (Services.prefs.getBoolPref("browser.casting.enabled")) {
+ services = CastingApps.getServicesForMirroring();
+ }
+ popup.ownerDocument.getElementById("menu_mirrorTabCmd").hidden = !services.length;
+}
+
+function mirrorMenuItemClicked(event) {
+ gBrowser.selectedBrowser.messageManager.sendAsyncMessage("SecondScreen:tab-mirror",
+ {service: event.originalTarget._service});
+}
+
+function populateMirrorTabMenu(popup) {
+ popup.innerHTML = null;
+ if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
+ return;
+ }
+ let doc = popup.ownerDocument;
+ let services = CastingApps.getServicesForMirroring();
+ services.forEach(service => {
+ let item = doc.createElement("menuitem");
+ item.setAttribute("label", service.friendlyName);
+ item._service = service;
+ item.addEventListener("command", mirrorMenuItemClicked);
+ popup.appendChild(item);
+ });
+}
+
+function getWebNavigation()
+{
+ return gBrowser.webNavigation;
+}
+
+function BrowserReloadWithFlags(reloadFlags) {
+ let url = gBrowser.currentURI.spec;
+ if (gBrowser.updateBrowserRemotenessByURL(gBrowser.selectedBrowser, url)) {
+ // If the remoteness has changed, the new browser doesn't have any
+ // information of what was loaded before, so we need to load the previous
+ // URL again.
+ gBrowser.loadURIWithFlags(url, reloadFlags);
+ return;
+ }
+
+ let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ gBrowser.selectedBrowser
+ .messageManager
+ .sendAsyncMessage("Browser:Reload",
+ { flags: reloadFlags,
+ handlingUserInput: windowUtils.isHandlingUserInput });
+}
+
+function getSecurityInfo(securityInfoAsString) {
+ if (!securityInfoAsString)
+ return null;
+
+ const serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ let securityInfo = serhelper.deserializeObject(securityInfoAsString);
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+
+ return securityInfo;
+}
+
+/**
+ * Returns a string with detailed information about the certificate validation
+ * failure from the specified URI that can be used to send a report.
+ */
+function getDetailedCertErrorInfo(location, securityInfo) {
+ if (!securityInfo)
+ return "";
+
+ let certErrorDetails = location;
+ let code = securityInfo.errorCode;
+ let errors = Cc["@mozilla.org/nss_errors_service;1"]
+ .getService(Ci.nsINSSErrorsService);
+
+ certErrorDetails += "\r\n\r\n" + errors.getErrorMessage(errors.getXPCOMFromNSSError(code));
+
+ const sss = Cc["@mozilla.org/ssservice;1"]
+ .getService(Ci.nsISiteSecurityService);
+ // SiteSecurityService uses different storage if the channel is
+ // private. Thus we must give isSecureHost correct flags or we
+ // might get incorrect results.
+ let flags = PrivateBrowsingUtils.isWindowPrivate(window) ?
+ Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0;
+
+ let uri = Services.io.newURI(location, null, null);
+
+ let hasHSTS = sss.isSecureHost(sss.HEADER_HSTS, uri.host, flags);
+ let hasHPKP = sss.isSecureHost(sss.HEADER_HPKP, uri.host, flags);
+ certErrorDetails += "\r\n\r\n" +
+ gNavigatorBundle.getFormattedString("certErrorDetailsHSTS.label",
+ [hasHSTS]);
+ certErrorDetails += "\r\n" +
+ gNavigatorBundle.getFormattedString("certErrorDetailsKeyPinning.label",
+ [hasHPKP]);
+
+ let certChain = "";
+ if (securityInfo.failedCertChain) {
+ let certs = securityInfo.failedCertChain.getEnumerator();
+ while (certs.hasMoreElements()) {
+ let cert = certs.getNext();
+ cert.QueryInterface(Ci.nsIX509Cert);
+ certChain += getPEMString(cert);
+ }
+ }
+
+ certErrorDetails += "\r\n\r\n" +
+ gNavigatorBundle.getString("certErrorDetailsCertChain.label") +
+ "\r\n\r\n" + certChain;
+
+ return certErrorDetails;
+}
+
+// TODO: can we pull getDERString and getPEMString in from pippki.js instead of
+// duplicating them here?
+function getDERString(cert)
+{
+ var length = {};
+ var derArray = cert.getRawDER(length);
+ var derString = '';
+ for (var i = 0; i < derArray.length; i++) {
+ derString += String.fromCharCode(derArray[i]);
+ }
+ return derString;
+}
+
+function getPEMString(cert)
+{
+ var derb64 = btoa(getDERString(cert));
+ // Wrap the Base64 string into lines of 64 characters,
+ // with CRLF line breaks (as specified in RFC 1421).
+ var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n");
+ return "-----BEGIN CERTIFICATE-----\r\n"
+ + wrapped
+ + "\r\n-----END CERTIFICATE-----\r\n";
+}
+
+var PrintPreviewListener = {
+ _printPreviewTab: null,
+ _tabBeforePrintPreview: null,
+ _simplifyPageTab: null,
+
+ getPrintPreviewBrowser: function () {
+ if (!this._printPreviewTab) {
+ let browser = gBrowser.selectedTab.linkedBrowser;
+ let forceNotRemote = gMultiProcessBrowser && !browser.isRemoteBrowser;
+ this._tabBeforePrintPreview = gBrowser.selectedTab;
+ this._printPreviewTab = gBrowser.loadOneTab("about:blank",
+ { inBackground: false,
+ forceNotRemote,
+ relatedBrowser: browser });
+ gBrowser.selectedTab = this._printPreviewTab;
+ }
+ return gBrowser.getBrowserForTab(this._printPreviewTab);
+ },
+ createSimplifiedBrowser: function () {
+ this._simplifyPageTab = gBrowser.loadOneTab("about:blank",
+ { inBackground: true });
+ return this.getSimplifiedSourceBrowser();
+ },
+ getSourceBrowser: function () {
+ return this._tabBeforePrintPreview ?
+ this._tabBeforePrintPreview.linkedBrowser : gBrowser.selectedBrowser;
+ },
+ getSimplifiedSourceBrowser: function () {
+ return this._simplifyPageTab ?
+ gBrowser.getBrowserForTab(this._simplifyPageTab) : null;
+ },
+ getNavToolbox: function () {
+ return gNavToolbox;
+ },
+ onEnter: function () {
+ // We might have accidentally switched tabs since the user invoked print
+ // preview
+ if (gBrowser.selectedTab != this._printPreviewTab) {
+ gBrowser.selectedTab = this._printPreviewTab;
+ }
+ gInPrintPreviewMode = true;
+ this._toggleAffectedChrome();
+ },
+ onExit: function () {
+ gBrowser.selectedTab = this._tabBeforePrintPreview;
+ this._tabBeforePrintPreview = null;
+ gInPrintPreviewMode = false;
+ this._toggleAffectedChrome();
+ if (this._simplifyPageTab) {
+ gBrowser.removeTab(this._simplifyPageTab);
+ this._simplifyPageTab = null;
+ }
+ gBrowser.removeTab(this._printPreviewTab);
+ gBrowser.deactivatePrintPreviewBrowsers();
+ this._printPreviewTab = null;
+ },
+ _toggleAffectedChrome: function () {
+ gNavToolbox.collapsed = gInPrintPreviewMode;
+
+ if (gInPrintPreviewMode)
+ this._hideChrome();
+ else
+ this._showChrome();
+
+ TabsInTitlebar.allowedBy("print-preview", !gInPrintPreviewMode);
+ },
+ _hideChrome: function () {
+ this._chromeState = {};
+
+ this._chromeState.sidebarOpen = SidebarUI.isOpen;
+ this._sidebarCommand = SidebarUI.currentID;
+ SidebarUI.hide();
+
+ var notificationBox = gBrowser.getNotificationBox();
+ this._chromeState.notificationsOpen = !notificationBox.notificationsHidden;
+ notificationBox.notificationsHidden = true;
+
+ gBrowser.updateWindowResizers();
+
+ this._chromeState.findOpen = gFindBarInitialized && !gFindBar.hidden;
+ if (gFindBarInitialized)
+ gFindBar.close();
+
+ var globalNotificationBox = document.getElementById("global-notificationbox");
+ this._chromeState.globalNotificationsOpen = !globalNotificationBox.notificationsHidden;
+ globalNotificationBox.notificationsHidden = true;
+
+ this._chromeState.syncNotificationsOpen = false;
+ var syncNotifications = document.getElementById("sync-notifications");
+ if (syncNotifications) {
+ this._chromeState.syncNotificationsOpen = !syncNotifications.notificationsHidden;
+ syncNotifications.notificationsHidden = true;
+ }
+ },
+ _showChrome: function () {
+ if (this._chromeState.notificationsOpen)
+ gBrowser.getNotificationBox().notificationsHidden = false;
+
+ if (this._chromeState.findOpen)
+ gFindBar.open();
+
+ if (this._chromeState.globalNotificationsOpen)
+ document.getElementById("global-notificationbox").notificationsHidden = false;
+
+ if (this._chromeState.syncNotificationsOpen)
+ document.getElementById("sync-notifications").notificationsHidden = false;
+
+ if (this._chromeState.sidebarOpen)
+ SidebarUI.show(this._sidebarCommand);
+ },
+
+ activateBrowser(browser) {
+ gBrowser.activateBrowserForPrintPreview(browser);
+ },
+}
+
+function getMarkupDocumentViewer()
+{
+ return gBrowser.markupDocumentViewer;
+}
+
+// This function is obsolete. Newer code should use <tooltip page="true"/> instead.
+function FillInHTMLTooltip(tipElement)
+{
+ document.getElementById("aHTMLTooltip").fillInPageTooltip(tipElement);
+}
+
+var browserDragAndDrop = {
+ canDropLink: aEvent => Services.droppedLinkHandler.canDropLink(aEvent, true),
+
+ dragOver: function (aEvent)
+ {
+ if (this.canDropLink(aEvent)) {
+ aEvent.preventDefault();
+ }
+ },
+
+ dropLinks: function (aEvent, aDisallowInherit) {
+ return Services.droppedLinkHandler.dropLinks(aEvent, aDisallowInherit);
+ }
+};
+
+var homeButtonObserver = {
+ onDrop: function (aEvent)
+ {
+ // disallow setting home pages that inherit the principal
+ let links = browserDragAndDrop.dropLinks(aEvent, true);
+ if (links.length) {
+ setTimeout(openHomeDialog, 0, links.map(link => link.url).join("|"));
+ }
+ },
+
+ onDragOver: function (aEvent)
+ {
+ if (gPrefService.prefIsLocked("browser.startup.homepage")) {
+ return;
+ }
+ browserDragAndDrop.dragOver(aEvent);
+ aEvent.dropEffect = "link";
+ },
+ onDragExit: function (aEvent)
+ {
+ }
+}
+
+function openHomeDialog(aURL)
+{
+ var promptTitle = gNavigatorBundle.getString("droponhometitle");
+ var promptMsg;
+ if (aURL.includes("|")) {
+ promptMsg = gNavigatorBundle.getString("droponhomemsgMultiple");
+ } else {
+ promptMsg = gNavigatorBundle.getString("droponhomemsg");
+ }
+
+ var pressedVal = Services.prompt.confirmEx(window, promptTitle, promptMsg,
+ Services.prompt.STD_YES_NO_BUTTONS,
+ null, null, null, null, {value:0});
+
+ if (pressedVal == 0) {
+ try {
+ var homepageStr = Components.classes["@mozilla.org/supports-string;1"]
+ .createInstance(Components.interfaces.nsISupportsString);
+ homepageStr.data = aURL;
+ gPrefService.setComplexValue("browser.startup.homepage",
+ Components.interfaces.nsISupportsString, homepageStr);
+ } catch (ex) {
+ dump("Failed to set the home page.\n"+ex+"\n");
+ }
+ }
+}
+
+var newTabButtonObserver = {
+ onDragOver(aEvent) {
+ browserDragAndDrop.dragOver(aEvent);
+ },
+ onDragExit(aEvent) {},
+ onDrop: Task.async(function* (aEvent) {
+ let links = browserDragAndDrop.dropLinks(aEvent);
+ for (let link of links) {
+ if (link.url) {
+ let data = yield getShortcutOrURIAndPostData(link.url);
+ // Allow third-party services to fixup this URL.
+ openNewTabWith(data.url, null, data.postData, aEvent, true);
+ }
+ }
+ })
+}
+
+var newWindowButtonObserver = {
+ onDragOver(aEvent) {
+ browserDragAndDrop.dragOver(aEvent);
+ },
+ onDragExit(aEvent) {},
+ onDrop: Task.async(function* (aEvent) {
+ let links = browserDragAndDrop.dropLinks(aEvent);
+ for (let link of links) {
+ if (link.url) {
+ let data = yield getShortcutOrURIAndPostData(link.url);
+ // Allow third-party services to fixup this URL.
+ openNewWindowWith(data.url, null, data.postData, true);
+ }
+ }
+ })
+}
+
+const DOMLinkHandler = {
+ init: function() {
+ let mm = window.messageManager;
+ mm.addMessageListener("Link:AddFeed", this);
+ mm.addMessageListener("Link:SetIcon", this);
+ mm.addMessageListener("Link:AddSearch", this);
+ },
+
+ receiveMessage: function (aMsg) {
+ switch (aMsg.name) {
+ case "Link:AddFeed":
+ let link = {type: aMsg.data.type, href: aMsg.data.href, title: aMsg.data.title};
+ FeedHandler.addFeed(link, aMsg.target);
+ break;
+
+ case "Link:SetIcon":
+ this.setIcon(aMsg.target, aMsg.data.url, aMsg.data.loadingPrincipal);
+ break;
+
+ case "Link:AddSearch":
+ this.addSearch(aMsg.target, aMsg.data.engine, aMsg.data.url);
+ break;
+ }
+ },
+
+ setIcon: function(aBrowser, aURL, aLoadingPrincipal) {
+ if (gBrowser.isFailedIcon(aURL))
+ return false;
+
+ let tab = gBrowser.getTabForBrowser(aBrowser);
+ if (!tab)
+ return false;
+
+ gBrowser.setIcon(tab, aURL, aLoadingPrincipal);
+ return true;
+ },
+
+ addSearch: function(aBrowser, aEngine, aURL) {
+ let tab = gBrowser.getTabForBrowser(aBrowser);
+ if (!tab)
+ return;
+
+ BrowserSearch.addEngine(aBrowser, aEngine, makeURI(aURL));
+ },
+}
+
+const BrowserSearch = {
+ addEngine: function(browser, engine, uri) {
+ // Check to see whether we've already added an engine with this title
+ if (browser.engines) {
+ if (browser.engines.some(e => e.title == engine.title))
+ return;
+ }
+
+ var hidden = false;
+ // If this engine (identified by title) is already in the list, add it
+ // to the list of hidden engines rather than to the main list.
+ // XXX This will need to be changed when engines are identified by URL;
+ // see bug 335102.
+ if (Services.search.getEngineByName(engine.title))
+ hidden = true;
+
+ var engines = (hidden ? browser.hiddenEngines : browser.engines) || [];
+
+ engines.push({ uri: engine.href,
+ title: engine.title,
+ get icon() { return browser.mIconURL; }
+ });
+
+ if (hidden)
+ browser.hiddenEngines = engines;
+ else {
+ browser.engines = engines;
+ if (browser == gBrowser.selectedBrowser)
+ this.updateOpenSearchBadge();
+ }
+ },
+
+ /**
+ * Update the browser UI to show whether or not additional engines are
+ * available when a page is loaded or the user switches tabs to a page that
+ * has search engines.
+ */
+ updateOpenSearchBadge: function() {
+ var searchBar = this.searchBar;
+ if (!searchBar)
+ return;
+
+ var engines = gBrowser.selectedBrowser.engines;
+ if (engines && engines.length > 0)
+ searchBar.setAttribute("addengines", "true");
+ else
+ searchBar.removeAttribute("addengines");
+ },
+
+ /**
+ * Gives focus to the search bar, if it is present on the toolbar, or loads
+ * the default engine's search form otherwise. For Mac, opens a new window
+ * or focuses an existing window, if necessary.
+ */
+ webSearch: function BrowserSearch_webSearch() {
+ if (window.location.href != getBrowserURL()) {
+ var win = getTopWin();
+ if (win) {
+ // If there's an open browser window, it should handle this command
+ win.focus();
+ win.BrowserSearch.webSearch();
+ } else {
+ // If there are no open browser windows, open a new one
+ var observer = function observer(subject, topic, data) {
+ if (subject == win) {
+ BrowserSearch.webSearch();
+ Services.obs.removeObserver(observer, "browser-delayed-startup-finished");
+ }
+ }
+ win = window.openDialog(getBrowserURL(), "_blank",
+ "chrome,all,dialog=no", "about:blank");
+ Services.obs.addObserver(observer, "browser-delayed-startup-finished", false);
+ }
+ return;
+ }
+
+ let focusUrlBarIfSearchFieldIsNotActive = function(aSearchBar) {
+ if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) {
+ focusAndSelectUrlBar();
+ }
+ };
+
+ let searchBar = this.searchBar;
+ let placement = CustomizableUI.getPlacementOfWidget("search-container");
+ let focusSearchBar = () => {
+ searchBar = this.searchBar;
+ searchBar.select();
+ focusUrlBarIfSearchFieldIsNotActive(searchBar);
+ };
+ if (placement && placement.area == CustomizableUI.AREA_PANEL) {
+ // The panel is not constructed until the first time it is shown.
+ PanelUI.show().then(focusSearchBar);
+ return;
+ }
+ if (placement && placement.area == CustomizableUI.AREA_NAVBAR && searchBar &&
+ searchBar.parentNode.getAttribute("overflowedItem") == "true") {
+ let navBar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+ navBar.overflowable.show().then(() => {
+ focusSearchBar();
+ });
+ return;
+ }
+ if (searchBar) {
+ if (window.fullScreen)
+ FullScreen.showNavToolbox();
+ searchBar.select();
+ }
+ focusUrlBarIfSearchFieldIsNotActive(searchBar);
+ },
+
+ /**
+ * Loads a search results page, given a set of search terms. Uses the current
+ * engine if the search bar is visible, or the default engine otherwise.
+ *
+ * @param searchText
+ * The search terms to use for the search.
+ *
+ * @param useNewTab
+ * Boolean indicating whether or not the search should load in a new
+ * tab.
+ *
+ * @param purpose [optional]
+ * A string meant to indicate the context of the search request. This
+ * allows the search service to provide a different nsISearchSubmission
+ * depending on e.g. where the search is triggered in the UI.
+ *
+ * @return engine The search engine used to perform a search, or null if no
+ * search was performed.
+ */
+ _loadSearch: function (searchText, useNewTab, purpose) {
+ let engine;
+
+ // If the search bar is visible, use the current engine, otherwise, fall
+ // back to the default engine.
+ if (isElementVisible(this.searchBar))
+ engine = Services.search.currentEngine;
+ else
+ engine = Services.search.defaultEngine;
+
+ let submission = engine.getSubmission(searchText, null, purpose); // HTML response
+
+ // getSubmission can return null if the engine doesn't have a URL
+ // with a text/html response type. This is unlikely (since
+ // SearchService._addEngineToStore() should fail for such an engine),
+ // but let's be on the safe side.
+ if (!submission) {
+ return null;
+ }
+
+ let inBackground = Services.prefs.getBoolPref("browser.search.context.loadInBackground");
+ openLinkIn(submission.uri.spec,
+ useNewTab ? "tab" : "current",
+ { postData: submission.postData,
+ inBackground: inBackground,
+ relatedToCurrent: true });
+
+ return engine;
+ },
+
+ /**
+ * Just like _loadSearch, but preserving an old API.
+ *
+ * @return string Name of the search engine used to perform a search or null
+ * if a search was not performed.
+ */
+ loadSearch: function BrowserSearch_search(searchText, useNewTab, purpose) {
+ let engine = BrowserSearch._loadSearch(searchText, useNewTab, purpose);
+ if (!engine) {
+ return null;
+ }
+ return engine.name;
+ },
+
+ /**
+ * Perform a search initiated from the context menu.
+ *
+ * This should only be called from the context menu. See
+ * BrowserSearch.loadSearch for the preferred API.
+ */
+ loadSearchFromContext: function (terms) {
+ let engine = BrowserSearch._loadSearch(terms, true, "contextmenu");
+ if (engine) {
+ BrowserSearch.recordSearchInTelemetry(engine, "contextmenu");
+ }
+ },
+
+ pasteAndSearch: function (event) {
+ BrowserSearch.searchBar.select();
+ goDoCommand("cmd_paste");
+ BrowserSearch.searchBar.handleSearchCommand(event);
+ },
+
+ /**
+ * Returns the search bar element if it is present in the toolbar, null otherwise.
+ */
+ get searchBar() {
+ return document.getElementById("searchbar");
+ },
+
+ get searchEnginesURL() {
+ return formatURL("browser.search.searchEnginesURL", true);
+ },
+
+ loadAddEngines: function BrowserSearch_loadAddEngines() {
+ var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow");
+ var where = newWindowPref == 3 ? "tab" : "window";
+ openUILinkIn(this.searchEnginesURL, where);
+ },
+
+ _getSearchEngineId: function (engine) {
+ if (engine && engine.identifier) {
+ return engine.identifier;
+ }
+
+ if (!engine || (engine.name === undefined) ||
+ !Services.prefs.getBoolPref("toolkit.telemetry.enabled"))
+ return "other";
+
+ return "other-" + engine.name;
+ },
+
+ /**
+ * Helper to record a search with Telemetry.
+ *
+ * Telemetry records only search counts and nothing pertaining to the search itself.
+ *
+ * @param engine
+ * (nsISearchEngine) The engine handling the search.
+ * @param source
+ * (string) Where the search originated from. See BrowserUsageTelemetry for
+ * allowed values.
+ * @param details [optional]
+ * An optional parameter passed to |BrowserUsageTelemetry.recordSearch|.
+ * See its documentation for allowed options.
+ * Additionally, if the search was a suggested search, |details.selection|
+ * indicates where the item was in the suggestion list and how the user
+ * selected it: {selection: {index: The selected index, kind: "key" or "mouse"}}
+ */
+ recordSearchInTelemetry: function (engine, source, details={}) {
+ BrowserUITelemetry.countSearchEvent(source, null, details.selection);
+ try {
+ BrowserUsageTelemetry.recordSearch(engine, source, details);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+
+ /**
+ * Helper to record a one-off search with Telemetry.
+ *
+ * Telemetry records only search counts and nothing pertaining to the search itself.
+ *
+ * @param engine
+ * (nsISearchEngine) The engine handling the search.
+ * @param source
+ * (string) Where the search originated from. See BrowserUsageTelemetry for
+ * allowed values.
+ * @param type
+ * (string) Indicates how the user selected the search item.
+ * @param where
+ * (string) Where was the search link opened (e.g. new tab, current tab, ..).
+ */
+ recordOneoffSearchInTelemetry: function (engine, source, type, where) {
+ let id = this._getSearchEngineId(engine) + "." + source;
+ BrowserUITelemetry.countOneoffSearchEvent(id, type, where);
+ try {
+ const details = {type, isOneOff: true};
+ BrowserUsageTelemetry.recordSearch(engine, source, details);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+};
+
+XPCOMUtils.defineConstant(this, "BrowserSearch", BrowserSearch);
+
+function FillHistoryMenu(aParent) {
+ // Lazily add the hover listeners on first showing and never remove them
+ if (!aParent.hasStatusListener) {
+ // Show history item's uri in the status bar when hovering, and clear on exit
+ aParent.addEventListener("DOMMenuItemActive", function(aEvent) {
+ // Only the current page should have the checked attribute, so skip it
+ if (!aEvent.target.hasAttribute("checked"))
+ XULBrowserWindow.setOverLink(aEvent.target.getAttribute("uri"));
+ }, false);
+ aParent.addEventListener("DOMMenuItemInactive", function() {
+ XULBrowserWindow.setOverLink("");
+ }, false);
+
+ aParent.hasStatusListener = true;
+ }
+
+ // Remove old entries if any
+ let children = aParent.childNodes;
+ for (var i = children.length - 1; i >= 0; --i) {
+ if (children[i].hasAttribute("index"))
+ aParent.removeChild(children[i]);
+ }
+
+ const MAX_HISTORY_MENU_ITEMS = 15;
+
+ const tooltipBack = gNavigatorBundle.getString("tabHistory.goBack");
+ const tooltipCurrent = gNavigatorBundle.getString("tabHistory.current");
+ const tooltipForward = gNavigatorBundle.getString("tabHistory.goForward");
+
+ function updateSessionHistory(sessionHistory, initial)
+ {
+ let count = sessionHistory.entries.length;
+
+ if (!initial) {
+ if (count <= 1) {
+ // if there is only one entry now, close the popup.
+ aParent.hidePopup();
+ return;
+ } else if (aParent.id != "backForwardMenu" && !aParent.parentNode.open) {
+ // if the popup wasn't open before, but now needs to be, reopen the menu.
+ // It should trigger FillHistoryMenu again. This might happen with the
+ // delay from click-and-hold menus but skip this for the context menu
+ // (backForwardMenu) rather than figuring out how the menu should be
+ // positioned and opened as it is an extreme edgecase.
+ aParent.parentNode.open = true;
+ return;
+ }
+ }
+
+ let index = sessionHistory.index;
+ let half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2);
+ let start = Math.max(index - half_length, 0);
+ let end = Math.min(start == 0 ? MAX_HISTORY_MENU_ITEMS : index + half_length + 1, count);
+ if (end == count) {
+ start = Math.max(count - MAX_HISTORY_MENU_ITEMS, 0);
+ }
+
+ let existingIndex = 0;
+
+ for (let j = end - 1; j >= start; j--) {
+ let entry = sessionHistory.entries[j];
+ let uri = entry.url;
+
+ let item = existingIndex < children.length ?
+ children[existingIndex] : document.createElement("menuitem");
+
+ let entryURI = BrowserUtils.makeURI(entry.url, entry.charset, null);
+ item.setAttribute("uri", uri);
+ item.setAttribute("label", entry.title || uri);
+ item.setAttribute("index", j);
+
+ // Cache this so that gotoHistoryIndex doesn't need the original index
+ item.setAttribute("historyindex", j - index);
+
+ if (j != index) {
+ PlacesUtils.favicons.getFaviconURLForPage(entryURI, function (aURI) {
+ if (aURI) {
+ let iconURL = PlacesUtils.favicons.getFaviconLinkForIcon(aURI).spec;
+ item.style.listStyleImage = "url(" + iconURL + ")";
+ }
+ });
+ }
+
+ if (j < index) {
+ item.className = "unified-nav-back menuitem-iconic menuitem-with-favicon";
+ item.setAttribute("tooltiptext", tooltipBack);
+ } else if (j == index) {
+ item.setAttribute("type", "radio");
+ item.setAttribute("checked", "true");
+ item.className = "unified-nav-current";
+ item.setAttribute("tooltiptext", tooltipCurrent);
+ } else {
+ item.className = "unified-nav-forward menuitem-iconic menuitem-with-favicon";
+ item.setAttribute("tooltiptext", tooltipForward);
+ }
+
+ if (!item.parentNode) {
+ aParent.appendChild(item);
+ }
+
+ existingIndex++;
+ }
+
+ if (!initial) {
+ let existingLength = children.length;
+ while (existingIndex < existingLength) {
+ aParent.removeChild(aParent.lastChild);
+ existingIndex++;
+ }
+ }
+ }
+
+ let sessionHistory = SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory);
+ if (!sessionHistory)
+ return false;
+
+ // don't display the popup for a single item
+ if (sessionHistory.entries.length <= 1)
+ return false;
+
+ updateSessionHistory(sessionHistory, true);
+ return true;
+}
+
+function addToUrlbarHistory(aUrlToAdd) {
+ if (!PrivateBrowsingUtils.isWindowPrivate(window) &&
+ aUrlToAdd &&
+ !aUrlToAdd.includes(" ") &&
+ !/[\x00-\x1F]/.test(aUrlToAdd))
+ PlacesUIUtils.markPageAsTyped(aUrlToAdd);
+}
+
+function BrowserDownloadsUI()
+{
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ openUILinkIn("about:downloads", "tab");
+ } else {
+ PlacesCommandHook.showPlacesOrganizer("Downloads");
+ }
+}
+
+function toOpenWindowByType(inType, uri, features)
+{
+ var topWindow = Services.wm.getMostRecentWindow(inType);
+
+ if (topWindow)
+ topWindow.focus();
+ else if (features)
+ window.open(uri, "_blank", features);
+ else
+ window.open(uri, "_blank", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar");
+}
+
+function OpenBrowserWindow(options)
+{
+ var telemetryObj = {};
+ TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj);
+
+ function newDocumentShown(doc, topic, data) {
+ if (topic == "document-shown" &&
+ doc != document &&
+ doc.defaultView == win) {
+ Services.obs.removeObserver(newDocumentShown, "document-shown");
+ Services.obs.removeObserver(windowClosed, "domwindowclosed");
+ TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj);
+ }
+ }
+
+ function windowClosed(subject) {
+ if (subject == win) {
+ Services.obs.removeObserver(newDocumentShown, "document-shown");
+ Services.obs.removeObserver(windowClosed, "domwindowclosed");
+ }
+ }
+
+ // Make sure to remove the 'document-shown' observer in case the window
+ // is being closed right after it was opened to avoid leaking.
+ Services.obs.addObserver(newDocumentShown, "document-shown", false);
+ Services.obs.addObserver(windowClosed, "domwindowclosed", false);
+
+ var charsetArg = new String();
+ var handler = Components.classes["@mozilla.org/browser/clh;1"]
+ .getService(Components.interfaces.nsIBrowserHandler);
+ var defaultArgs = handler.defaultArgs;
+ var wintype = document.documentElement.getAttribute('windowtype');
+
+ var extraFeatures = "";
+ if (options && options.private) {
+ extraFeatures = ",private";
+ if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ // Force the new window to load about:privatebrowsing instead of the default home page
+ defaultArgs = "about:privatebrowsing";
+ }
+ } else {
+ extraFeatures = ",non-private";
+ }
+
+ if (options && options.remote) {
+ extraFeatures += ",remote";
+ } else if (options && options.remote === false) {
+ extraFeatures += ",non-remote";
+ }
+
+ // if and only if the current window is a browser window and it has a document with a character
+ // set, then extract the current charset menu setting from the current document and use it to
+ // initialize the new browser window...
+ var win;
+ if (window && (wintype == "navigator:browser") && window.content && window.content.document)
+ {
+ var DocCharset = window.content.document.characterSet;
+ charsetArg = "charset="+DocCharset;
+
+ // we should "inherit" the charset menu setting in a new window
+ win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs, charsetArg);
+ }
+ else // forget about the charset information.
+ {
+ win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs);
+ }
+
+ return win;
+}
+
+// Only here for backwards compat, we should remove this soon
+function BrowserCustomizeToolbar() {
+ gCustomizeMode.enter();
+}
+
+/**
+ * Update the global flag that tracks whether or not any edit UI (the Edit menu,
+ * edit-related items in the context menu, and edit-related toolbar buttons
+ * is visible, then update the edit commands' enabled state accordingly. We use
+ * this flag to skip updating the edit commands on focus or selection changes
+ * when no UI is visible to improve performance (including pageload performance,
+ * since focus changes when you load a new page).
+ *
+ * If UI is visible, we use goUpdateGlobalEditMenuItems to set the commands'
+ * enabled state so the UI will reflect it appropriately.
+ *
+ * If the UI isn't visible, we enable all edit commands so keyboard shortcuts
+ * still work and just lazily disable them as needed when the user presses a
+ * shortcut.
+ *
+ * This doesn't work on Mac, since Mac menus flash when users press their
+ * keyboard shortcuts, so edit UI is essentially always visible on the Mac,
+ * and we need to always update the edit commands. Thus on Mac this function
+ * is a no op.
+ */
+function updateEditUIVisibility()
+{
+ if (AppConstants.platform == "macosx")
+ return;
+
+ let editMenuPopupState = document.getElementById("menu_EditPopup").state;
+ let contextMenuPopupState = document.getElementById("contentAreaContextMenu").state;
+ let placesContextMenuPopupState = document.getElementById("placesContext").state;
+
+ // The UI is visible if the Edit menu is opening or open, if the context menu
+ // is open, or if the toolbar has been customized to include the Cut, Copy,
+ // or Paste toolbar buttons.
+ gEditUIVisible = editMenuPopupState == "showing" ||
+ editMenuPopupState == "open" ||
+ contextMenuPopupState == "showing" ||
+ contextMenuPopupState == "open" ||
+ placesContextMenuPopupState == "showing" ||
+ placesContextMenuPopupState == "open" ||
+ document.getElementById("edit-controls") ? true : false;
+
+ // If UI is visible, update the edit commands' enabled state to reflect
+ // whether or not they are actually enabled for the current focus/selection.
+ if (gEditUIVisible)
+ goUpdateGlobalEditMenuItems();
+
+ // Otherwise, enable all commands, so that keyboard shortcuts still work,
+ // then lazily determine their actual enabled state when the user presses
+ // a keyboard shortcut.
+ else {
+ goSetCommandEnabled("cmd_undo", true);
+ goSetCommandEnabled("cmd_redo", true);
+ goSetCommandEnabled("cmd_cut", true);
+ goSetCommandEnabled("cmd_copy", true);
+ goSetCommandEnabled("cmd_paste", true);
+ goSetCommandEnabled("cmd_selectAll", true);
+ goSetCommandEnabled("cmd_delete", true);
+ goSetCommandEnabled("cmd_switchTextDirection", true);
+ }
+}
+
+/**
+ * Opens a new tab with the userContextId specified as an attribute of
+ * sourceEvent. This attribute is propagated to the top level originAttributes
+ * living on the tab's docShell.
+ *
+ * @param event
+ * A click event on a userContext File Menu option
+ */
+function openNewUserContextTab(event)
+{
+ openUILinkIn(BROWSER_NEW_TAB_URL, "tab", {
+ userContextId: parseInt(event.target.getAttribute('data-usercontextid')),
+ });
+}
+
+/**
+ * Updates File Menu User Context UI visibility depending on
+ * privacy.userContext.enabled pref state.
+ */
+function updateUserContextUIVisibility()
+{
+ let menu = document.getElementById("menu_newUserContext");
+ menu.hidden = !Services.prefs.getBoolPref("privacy.userContext.enabled");
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ menu.setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Updates the User Context UI indicators if the browser is in a non-default context
+ */
+function updateUserContextUIIndicator()
+{
+ let hbox = document.getElementById("userContext-icons");
+
+ let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid");
+ if (!userContextId) {
+ hbox.setAttribute("data-identity-color", "");
+ hbox.hidden = true;
+ return;
+ }
+
+ let identity = ContextualIdentityService.getIdentityFromId(userContextId);
+ if (!identity) {
+ hbox.setAttribute("data-identity-color", "");
+ hbox.hidden = true;
+ return;
+ }
+
+ hbox.setAttribute("data-identity-color", identity.color);
+
+ let label = document.getElementById("userContext-label");
+ label.setAttribute("value", ContextualIdentityService.getUserContextLabel(userContextId));
+
+ let indicator = document.getElementById("userContext-indicator");
+ indicator.setAttribute("data-identity-icon", identity.icon);
+
+ hbox.hidden = false;
+}
+
+/**
+ * Makes the Character Encoding menu enabled or disabled as appropriate.
+ * To be called when the View menu or the app menu is opened.
+ */
+function updateCharacterEncodingMenuState()
+{
+ let charsetMenu = document.getElementById("charsetMenu");
+ // gBrowser is null on Mac when the menubar shows in the context of
+ // non-browser windows. The above elements may be null depending on
+ // what parts of the menubar are present. E.g. no app menu on Mac.
+ if (gBrowser && gBrowser.selectedBrowser.mayEnableCharacterEncodingMenu) {
+ if (charsetMenu) {
+ charsetMenu.removeAttribute("disabled");
+ }
+ } else if (charsetMenu) {
+ charsetMenu.setAttribute("disabled", "true");
+ }
+}
+
+var XULBrowserWindow = {
+ // Stored Status, Link and Loading values
+ status: "",
+ defaultStatus: "",
+ overLink: "",
+ startTime: 0,
+ statusText: "",
+ isBusy: false,
+ // Left here for add-on compatibility, see bug 752434
+ inContentWhitelist: [],
+
+ QueryInterface: function (aIID) {
+ if (aIID.equals(Ci.nsIWebProgressListener) ||
+ aIID.equals(Ci.nsIWebProgressListener2) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsIXULBrowserWindow) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ get stopCommand () {
+ delete this.stopCommand;
+ return this.stopCommand = document.getElementById("Browser:Stop");
+ },
+ get reloadCommand () {
+ delete this.reloadCommand;
+ return this.reloadCommand = document.getElementById("Browser:Reload");
+ },
+ get statusTextField () {
+ return gBrowser.getStatusPanel();
+ },
+ get isImage () {
+ delete this.isImage;
+ return this.isImage = document.getElementById("isImage");
+ },
+ get canViewSource () {
+ delete this.canViewSource;
+ return this.canViewSource = document.getElementById("canViewSource");
+ },
+
+ init: function () {
+ // Initialize the security button's state and tooltip text.
+ var securityUI = gBrowser.securityUI;
+ this.onSecurityChange(null, null, securityUI.state, true);
+ },
+
+ setJSStatus: function () {
+ // unsupported
+ },
+
+ forceInitialBrowserRemote: function() {
+ let initBrowser =
+ document.getAnonymousElementByAttribute(gBrowser, "anonid", "initialBrowser");
+ return initBrowser.frameLoader.tabParent;
+ },
+
+ forceInitialBrowserNonRemote: function(aOpener) {
+ let initBrowser =
+ document.getAnonymousElementByAttribute(gBrowser, "anonid", "initialBrowser");
+ gBrowser.updateBrowserRemoteness(initBrowser, false, aOpener);
+ },
+
+ setDefaultStatus: function (status) {
+ this.defaultStatus = status;
+ this.updateStatusField();
+ },
+
+ setOverLink: function (url, anchorElt) {
+ // Encode bidirectional formatting characters.
+ // (RFC 3987 sections 3.2 and 4.1 paragraph 6)
+ url = url.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g,
+ encodeURIComponent);
+
+ if (gURLBar && gURLBar._mayTrimURLs /* corresponds to browser.urlbar.trimURLs */)
+ url = trimURL(url);
+
+ this.overLink = url;
+ LinkTargetDisplay.update();
+ },
+
+ showTooltip: function (x, y, tooltip, direction) {
+ if (Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService).
+ getCurrentSession()) {
+ return;
+ }
+
+ // The x,y coordinates are relative to the <browser> element using
+ // the chrome zoom level.
+ let elt = document.getElementById("remoteBrowserTooltip");
+ elt.label = tooltip;
+ elt.style.direction = direction;
+
+ let anchor = gBrowser.selectedBrowser;
+ elt.openPopupAtScreen(anchor.boxObject.screenX + x, anchor.boxObject.screenY + y, false, null);
+ },
+
+ hideTooltip: function () {
+ let elt = document.getElementById("remoteBrowserTooltip");
+ elt.hidePopup();
+ },
+
+ getTabCount: function () {
+ return gBrowser.tabs.length;
+ },
+
+ updateStatusField: function () {
+ var text, type, types = ["overLink"];
+ if (this._busyUI)
+ types.push("status");
+ types.push("defaultStatus");
+ for (type of types) {
+ text = this[type];
+ if (text)
+ break;
+ }
+
+ // check the current value so we don't trigger an attribute change
+ // and cause needless (slow!) UI updates
+ if (this.statusText != text) {
+ let field = this.statusTextField;
+ field.setAttribute("previoustype", field.getAttribute("type"));
+ field.setAttribute("type", type);
+ field.label = text;
+ field.setAttribute("crop", type == "overLink" ? "center" : "end");
+ this.statusText = text;
+ }
+ },
+
+ // Called before links are navigated to to allow us to retarget them if needed.
+ onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
+ let target = BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab);
+ SocialUI.closeSocialPanelForLinkTraversal(target, linkNode);
+ return target;
+ },
+
+ // Check whether this URI should load in the current process
+ shouldLoadURI: function(aDocShell, aURI, aReferrer) {
+ if (!gMultiProcessBrowser)
+ return true;
+
+ let browser = aDocShell.QueryInterface(Ci.nsIDocShellTreeItem)
+ .sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+
+ // Ignore loads that aren't in the main tabbrowser
+ if (browser.localName != "browser" || !browser.getTabBrowser || browser.getTabBrowser() != gBrowser)
+ return true;
+
+ if (!E10SUtils.shouldLoadURI(aDocShell, aURI, aReferrer)) {
+ E10SUtils.redirectLoad(aDocShell, aURI, aReferrer);
+ return false;
+ }
+
+ return true;
+ },
+
+ onProgressChange: function (aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ // Do nothing.
+ },
+
+ onProgressChange64: function (aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ return this.onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress);
+ },
+
+ // This function fires only for the currently selected tab.
+ onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
+ const nsIWebProgressListener = Ci.nsIWebProgressListener;
+ const nsIChannel = Ci.nsIChannel;
+
+ let browser = gBrowser.selectedBrowser;
+
+ if (aStateFlags & nsIWebProgressListener.STATE_START &&
+ aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
+
+ if (aRequest && aWebProgress.isTopLevel) {
+ // clear out feed data
+ browser.feeds = null;
+
+ // clear out search-engine data
+ browser.engines = null;
+ }
+
+ this.isBusy = true;
+
+ if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) {
+ this._busyUI = true;
+
+ // XXX: This needs to be based on window activity...
+ this.stopCommand.removeAttribute("disabled");
+ CombinedStopReload.switchToStop();
+ }
+ }
+ else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
+ // This (thanks to the filter) is a network stop or the last
+ // request stop outside of loading the document, stop throbbers
+ // and progress bars and such
+ if (aRequest) {
+ let msg = "";
+ let location;
+ let canViewSource = true;
+ // Get the URI either from a channel or a pseudo-object
+ if (aRequest instanceof nsIChannel || "URI" in aRequest) {
+ location = aRequest.URI;
+
+ // For keyword URIs clear the user typed value since they will be changed into real URIs
+ if (location.scheme == "keyword" && aWebProgress.isTopLevel)
+ gBrowser.userTypedValue = null;
+
+ canViewSource = !Services.prefs.getBoolPref("view_source.tab") ||
+ location.scheme != "view-source";
+
+ if (location.spec != "about:blank") {
+ switch (aStatus) {
+ case Components.results.NS_ERROR_NET_TIMEOUT:
+ msg = gNavigatorBundle.getString("nv_timeout");
+ break;
+ }
+ }
+ }
+
+ this.status = "";
+ this.setDefaultStatus(msg);
+
+ // Disable menu entries for images, enable otherwise
+ if (browser.documentContentType && BrowserUtils.mimeTypeIsTextBased(browser.documentContentType)) {
+ this.isImage.removeAttribute('disabled');
+ } else {
+ canViewSource = false;
+ this.isImage.setAttribute('disabled', 'true');
+ }
+
+ if (canViewSource) {
+ this.canViewSource.removeAttribute('disabled');
+ } else {
+ this.canViewSource.setAttribute('disabled', 'true');
+ }
+ }
+
+ this.isBusy = false;
+
+ if (this._busyUI) {
+ this._busyUI = false;
+
+ this.stopCommand.setAttribute("disabled", "true");
+ CombinedStopReload.switchToReload(aRequest instanceof Ci.nsIRequest);
+ }
+ }
+ },
+
+ onLocationChange: function (aWebProgress, aRequest, aLocationURI, aFlags) {
+ var location = aLocationURI ? aLocationURI.spec : "";
+
+ // If displayed, hide the form validation popup.
+ FormValidationHandler.hidePopup();
+
+ let pageTooltip = document.getElementById("aHTMLTooltip");
+ let tooltipNode = pageTooltip.triggerNode;
+ if (tooltipNode) {
+ // Optimise for the common case
+ if (aWebProgress.isTopLevel) {
+ pageTooltip.hidePopup();
+ }
+ else {
+ for (let tooltipWindow = tooltipNode.ownerGlobal;
+ tooltipWindow != tooltipWindow.parent;
+ tooltipWindow = tooltipWindow.parent) {
+ if (tooltipWindow == aWebProgress.DOMWindow) {
+ pageTooltip.hidePopup();
+ break;
+ }
+ }
+ }
+ }
+
+ let browser = gBrowser.selectedBrowser;
+
+ // Disable menu entries for images, enable otherwise
+ if (browser.documentContentType && BrowserUtils.mimeTypeIsTextBased(browser.documentContentType))
+ this.isImage.removeAttribute('disabled');
+ else
+ this.isImage.setAttribute('disabled', 'true');
+
+ this.hideOverLinkImmediately = true;
+ this.setOverLink("", null);
+ this.hideOverLinkImmediately = false;
+
+ // We should probably not do this if the value has changed since the user
+ // searched
+ // Update urlbar only if a new page was loaded on the primary content area
+ // Do not update urlbar if there was a subframe navigation
+
+ if (aWebProgress.isTopLevel) {
+ if ((location == "about:blank" && checkEmptyPageOrigin()) ||
+ location == "") { // Second condition is for new tabs, otherwise
+ // reload function is enabled until tab is refreshed.
+ this.reloadCommand.setAttribute("disabled", "true");
+ } else {
+ this.reloadCommand.removeAttribute("disabled");
+ }
+
+ URLBarSetURI(aLocationURI);
+
+ BookmarkingUI.onLocationChange();
+
+ gIdentityHandler.onLocationChange();
+
+ SocialUI.updateState();
+
+ UITour.onLocationChange(location);
+
+ gTabletModePageCounter.inc();
+
+ // Utility functions for disabling find
+ var shouldDisableFind = function shouldDisableFind(aDocument) {
+ let docElt = aDocument.documentElement;
+ return docElt && docElt.getAttribute("disablefastfind") == "true";
+ }
+
+ var disableFindCommands = function disableFindCommands(aDisable) {
+ let findCommands = [document.getElementById("cmd_find"),
+ document.getElementById("cmd_findAgain"),
+ document.getElementById("cmd_findPrevious")];
+ for (let elt of findCommands) {
+ if (aDisable)
+ elt.setAttribute("disabled", "true");
+ else
+ elt.removeAttribute("disabled");
+ }
+ }
+
+ var onContentRSChange = function onContentRSChange(e) {
+ if (e.target.readyState != "interactive" && e.target.readyState != "complete")
+ return;
+
+ e.target.removeEventListener("readystatechange", onContentRSChange);
+ disableFindCommands(shouldDisableFind(e.target));
+ }
+
+ // Disable find commands in documents that ask for them to be disabled.
+ if (!gMultiProcessBrowser && aLocationURI &&
+ (aLocationURI.schemeIs("about") || aLocationURI.schemeIs("chrome"))) {
+ // Don't need to re-enable/disable find commands for same-document location changes
+ // (e.g. the replaceStates in about:addons)
+ if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
+ if (content.document.readyState == "interactive" || content.document.readyState == "complete")
+ disableFindCommands(shouldDisableFind(content.document));
+ else {
+ content.document.addEventListener("readystatechange", onContentRSChange);
+ }
+ }
+ } else
+ disableFindCommands(false);
+
+ // Try not to instantiate gCustomizeMode as much as possible,
+ // so don't use CustomizeMode.jsm to check for URI or customizing.
+ if (location == "about:blank" &&
+ gBrowser.selectedTab.hasAttribute("customizemode")) {
+ gCustomizeMode.enter();
+ } else if (CustomizationHandler.isEnteringCustomizeMode ||
+ CustomizationHandler.isCustomizing()) {
+ gCustomizeMode.exit();
+ }
+ }
+ UpdateBackForwardCommands(gBrowser.webNavigation);
+ ReaderParent.updateReaderButton(gBrowser.selectedBrowser);
+
+ gGestureSupport.restoreRotationState();
+
+ // See bug 358202, when tabs are switched during a drag operation,
+ // timers don't fire on windows (bug 203573)
+ if (aRequest)
+ setTimeout(function () { XULBrowserWindow.asyncUpdateUI(); }, 0);
+ else
+ this.asyncUpdateUI();
+
+ if (AppConstants.MOZ_CRASHREPORTER && aLocationURI) {
+ let uri = aLocationURI.clone();
+ try {
+ // If the current URI contains a username/password, remove it.
+ uri.userPass = "";
+ } catch (ex) { /* Ignore failures on about: URIs. */ }
+
+ try {
+ gCrashReporter.annotateCrashReport("URL", uri.spec);
+ } catch (ex) {
+ // Don't make noise when the crash reporter is built but not enabled.
+ if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) {
+ throw ex;
+ }
+ }
+ }
+ },
+
+ asyncUpdateUI: function () {
+ FeedHandler.updateFeeds();
+ BrowserSearch.updateOpenSearchBadge();
+ },
+
+ // Left here for add-on compatibility, see bug 752434
+ hideChromeForLocation: function() {},
+
+ onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) {
+ this.status = aMessage;
+ this.updateStatusField();
+ },
+
+ // Properties used to cache security state used to update the UI
+ _state: null,
+ _lastLocation: null,
+
+ // This is called in multiple ways:
+ // 1. Due to the nsIWebProgressListener.onSecurityChange notification.
+ // 2. Called by tabbrowser.xml when updating the current browser.
+ // 3. Called directly during this object's initializations.
+ // aRequest will be null always in case 2 and 3, and sometimes in case 1 (for
+ // instance, there won't be a request when STATE_BLOCKED_TRACKING_CONTENT is observed).
+ onSecurityChange: function (aWebProgress, aRequest, aState, aIsSimulated) {
+ // Don't need to do anything if the data we use to update the UI hasn't
+ // changed
+ let uri = gBrowser.currentURI;
+ let spec = uri.spec;
+ if (this._state == aState &&
+ this._lastLocation == spec)
+ return;
+ this._state = aState;
+ this._lastLocation = spec;
+
+ if (typeof(aIsSimulated) != "boolean" && typeof(aIsSimulated) != "undefined") {
+ throw "onSecurityChange: aIsSimulated receieved an unexpected type";
+ }
+
+ // Make sure the "https" part of the URL is striked out or not,
+ // depending on the current mixed active content blocking state.
+ gURLBar.formatValue();
+
+ try {
+ uri = Services.uriFixup.createExposableURI(uri);
+ } catch (e) {}
+ gIdentityHandler.updateIdentity(this._state, uri);
+ TrackingProtection.onSecurityChange(this._state, aIsSimulated);
+ },
+
+ // simulate all change notifications after switching tabs
+ onUpdateCurrentBrowser: function XWB_onUpdateCurrentBrowser(aStateFlags, aStatus, aMessage, aTotalProgress) {
+ if (FullZoom.updateBackgroundTabs)
+ FullZoom.onLocationChange(gBrowser.currentURI, true);
+ var nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ var loadingDone = aStateFlags & nsIWebProgressListener.STATE_STOP;
+ // use a pseudo-object instead of a (potentially nonexistent) channel for getting
+ // a correct error message - and make sure that the UI is always either in
+ // loading (STATE_START) or done (STATE_STOP) mode
+ this.onStateChange(
+ gBrowser.webProgress,
+ { URI: gBrowser.currentURI },
+ loadingDone ? nsIWebProgressListener.STATE_STOP : nsIWebProgressListener.STATE_START,
+ aStatus
+ );
+ // status message and progress value are undefined if we're done with loading
+ if (loadingDone)
+ return;
+ this.onStatusChange(gBrowser.webProgress, null, 0, aMessage);
+ }
+};
+
+var LinkTargetDisplay = {
+ get DELAY_SHOW() {
+ delete this.DELAY_SHOW;
+ return this.DELAY_SHOW = Services.prefs.getIntPref("browser.overlink-delay");
+ },
+
+ DELAY_HIDE: 250,
+ _timer: 0,
+
+ get _isVisible () {
+ return XULBrowserWindow.statusTextField.label != "";
+ },
+
+ update: function () {
+ clearTimeout(this._timer);
+ window.removeEventListener("mousemove", this, true);
+
+ if (!XULBrowserWindow.overLink) {
+ if (XULBrowserWindow.hideOverLinkImmediately)
+ this._hide();
+ else
+ this._timer = setTimeout(this._hide.bind(this), this.DELAY_HIDE);
+ return;
+ }
+
+ if (this._isVisible) {
+ XULBrowserWindow.updateStatusField();
+ } else {
+ // Let the display appear when the mouse doesn't move within the delay
+ this._showDelayed();
+ window.addEventListener("mousemove", this, true);
+ }
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "mousemove":
+ // Restart the delay since the mouse was moved
+ clearTimeout(this._timer);
+ this._showDelayed();
+ break;
+ }
+ },
+
+ _showDelayed: function () {
+ this._timer = setTimeout(function (self) {
+ XULBrowserWindow.updateStatusField();
+ window.removeEventListener("mousemove", self, true);
+ }, this.DELAY_SHOW, this);
+ },
+
+ _hide: function () {
+ clearTimeout(this._timer);
+
+ XULBrowserWindow.updateStatusField();
+ }
+};
+
+var CombinedStopReload = {
+ init: function () {
+ if (this._initialized)
+ return;
+
+ let reload = document.getElementById("urlbar-reload-button");
+ let stop = document.getElementById("urlbar-stop-button");
+ if (!stop || !reload || reload.nextSibling != stop)
+ return;
+
+ this._initialized = true;
+ if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true")
+ reload.setAttribute("displaystop", "true");
+ stop.addEventListener("click", this, false);
+ this.reload = reload;
+ this.stop = stop;
+ },
+
+ uninit: function () {
+ if (!this._initialized)
+ return;
+
+ this._cancelTransition();
+ this._initialized = false;
+ this.stop.removeEventListener("click", this, false);
+ this.reload = null;
+ this.stop = null;
+ },
+
+ handleEvent: function (event) {
+ // the only event we listen to is "click" on the stop button
+ if (event.button == 0 &&
+ !this.stop.disabled)
+ this._stopClicked = true;
+ },
+
+ switchToStop: function () {
+ if (!this._initialized)
+ return;
+
+ this._cancelTransition();
+ this.reload.setAttribute("displaystop", "true");
+ },
+
+ switchToReload: function (aDelay) {
+ if (!this._initialized)
+ return;
+
+ this.reload.removeAttribute("displaystop");
+
+ if (!aDelay || this._stopClicked) {
+ this._stopClicked = false;
+ this._cancelTransition();
+ this.reload.disabled = XULBrowserWindow.reloadCommand
+ .getAttribute("disabled") == "true";
+ return;
+ }
+
+ if (this._timer)
+ return;
+
+ // Temporarily disable the reload button to prevent the user from
+ // accidentally reloading the page when intending to click the stop button
+ this.reload.disabled = true;
+ this._timer = setTimeout(function (self) {
+ self._timer = 0;
+ self.reload.disabled = XULBrowserWindow.reloadCommand
+ .getAttribute("disabled") == "true";
+ }, 650, this);
+ },
+
+ _cancelTransition: function () {
+ if (this._timer) {
+ clearTimeout(this._timer);
+ this._timer = 0;
+ }
+ }
+};
+
+var TabsProgressListener = {
+ // Keep track of which browsers we've started load timers for, since
+ // we won't see STATE_START events for pre-rendered tabs.
+ _startedLoadTimer: new WeakSet(),
+
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Collect telemetry data about tab load times.
+ if (aWebProgress.isTopLevel && (!aRequest.originalURI || aRequest.originalURI.spec.scheme != "about")) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ this._startedLoadTimer.add(aBrowser);
+ TelemetryStopwatch.start("FX_PAGE_LOAD_MS", aBrowser);
+ Services.telemetry.getHistogramById("FX_TOTAL_TOP_VISITS").add(true);
+ } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ this._startedLoadTimer.has(aBrowser)) {
+ this._startedLoadTimer.delete(aBrowser);
+ TelemetryStopwatch.finish("FX_PAGE_LOAD_MS", aBrowser);
+ }
+ } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStatus == Cr.NS_BINDING_ABORTED &&
+ this._startedLoadTimer.has(aBrowser)) {
+ this._startedLoadTimer.delete(aBrowser);
+ TelemetryStopwatch.cancel("FX_PAGE_LOAD_MS", aBrowser);
+ }
+ }
+
+ // Attach a listener to watch for "click" events bubbling up from error
+ // pages and other similar pages (like about:newtab). This lets us fix bugs
+ // like 401575 which require error page UI to do privileged things, without
+ // letting error pages have any privilege themselves.
+ // We can't look for this during onLocationChange since at that point the
+ // document URI is not yet the about:-uri of the error page.
+
+ let isRemoteBrowser = aBrowser.isRemoteBrowser;
+ // We check isRemoteBrowser here to avoid requesting the doc CPOW
+ let doc = isRemoteBrowser ? null : aWebProgress.DOMWindow.document;
+
+ if (!isRemoteBrowser &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ Components.isSuccessCode(aStatus) &&
+ doc.documentURI.startsWith("about:") &&
+ !doc.documentURI.toLowerCase().startsWith("about:blank") &&
+ !doc.documentURI.toLowerCase().startsWith("about:home") &&
+ !doc.documentElement.hasAttribute("hasBrowserHandlers")) {
+ // STATE_STOP may be received twice for documents, thus store an
+ // attribute to ensure handling it just once.
+ doc.documentElement.setAttribute("hasBrowserHandlers", "true");
+ aBrowser.addEventListener("click", BrowserOnClick, true);
+ aBrowser.addEventListener("pagehide", function onPageHide(event) {
+ if (event.target.defaultView.frameElement)
+ return;
+ aBrowser.removeEventListener("click", BrowserOnClick, true);
+ aBrowser.removeEventListener("pagehide", onPageHide, true);
+ if (event.target.documentElement)
+ event.target.documentElement.removeAttribute("hasBrowserHandlers");
+ }, true);
+ }
+ },
+
+ onLocationChange: function (aBrowser, aWebProgress, aRequest, aLocationURI,
+ aFlags) {
+ // Filter out location changes caused by anchor navigation
+ // or history.push/pop/replaceState.
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ // Reader mode actually cares about these:
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.sendAsyncMessage("Reader:PushState", {isArticle: gBrowser.selectedBrowser.isArticle});
+ return;
+ }
+
+ // Filter out location changes in sub documents.
+ if (!aWebProgress.isTopLevel)
+ return;
+
+ // Only need to call locationChange if the PopupNotifications object
+ // for this window has already been initialized (i.e. its getter no
+ // longer exists)
+ if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get)
+ PopupNotifications.locationChange(aBrowser);
+
+ let tab = gBrowser.getTabForBrowser(aBrowser);
+ if (tab && tab._sharingState) {
+ gBrowser.setBrowserSharing(aBrowser, {});
+ webrtcUI.forgetStreamsFromBrowser(aBrowser);
+ }
+
+ gBrowser.getNotificationBox(aBrowser).removeTransientNotifications();
+
+ FullZoom.onLocationChange(aLocationURI, false, aBrowser);
+ },
+}
+
+function nsBrowserAccess() { }
+
+nsBrowserAccess.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]),
+
+ _openURIInNewTab: function(aURI, aReferrer, aReferrerPolicy, aIsPrivate,
+ aIsExternal, aForceNotRemote=false,
+ aUserContextId=Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID,
+ aOpener=null) {
+ let win, needToFocusWin;
+
+ // try the current window. if we're in a popup, fall back on the most recent browser window
+ if (window.toolbar.visible)
+ win = window;
+ else {
+ win = RecentWindow.getMostRecentBrowserWindow({private: aIsPrivate});
+ needToFocusWin = true;
+ }
+
+ if (!win) {
+ // we couldn't find a suitable window, a new one needs to be opened.
+ return null;
+ }
+
+ if (aIsExternal && (!aURI || aURI.spec == "about:blank")) {
+ win.BrowserOpenTab(); // this also focuses the location bar
+ win.focus();
+ return win.gBrowser.selectedBrowser;
+ }
+
+ let loadInBackground = gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground");
+
+ let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", {
+ referrerURI: aReferrer,
+ referrerPolicy: aReferrerPolicy,
+ userContextId: aUserContextId,
+ fromExternal: aIsExternal,
+ inBackground: loadInBackground,
+ forceNotRemote: aForceNotRemote,
+ opener: aOpener,
+ });
+ let browser = win.gBrowser.getBrowserForTab(tab);
+
+ if (needToFocusWin || (!loadInBackground && aIsExternal))
+ win.focus();
+
+ return browser;
+ },
+
+ openURI: function (aURI, aOpener, aWhere, aFlags) {
+ // This function should only ever be called if we're opening a URI
+ // from a non-remote browser window (via nsContentTreeOwner).
+ if (aOpener && Cu.isCrossProcessWrapper(aOpener)) {
+ Cu.reportError("nsBrowserAccess.openURI was passed a CPOW for aOpener. " +
+ "openURI should only ever be called from non-remote browsers.");
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ var newWindow = null;
+ var isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+
+ if (aOpener && isExternal) {
+ Cu.reportError("nsBrowserAccess.openURI did not expect an opener to be " +
+ "passed if the context is OPEN_EXTERNAL.");
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ if (isExternal && aURI && aURI.schemeIs("chrome")) {
+ dump("use --chrome command-line option to load external chrome urls\n");
+ return null;
+ }
+
+ if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
+ if (isExternal &&
+ gPrefService.prefHasUserValue("browser.link.open_newwindow.override.external"))
+ aWhere = gPrefService.getIntPref("browser.link.open_newwindow.override.external");
+ else
+ aWhere = gPrefService.getIntPref("browser.link.open_newwindow");
+ }
+
+ let referrer = aOpener ? makeURI(aOpener.location.href) : null;
+ let referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT;
+ if (aOpener && aOpener.document) {
+ referrerPolicy = aOpener.document.referrerPolicy;
+ }
+ let isPrivate = aOpener
+ ? PrivateBrowsingUtils.isContentWindowPrivate(aOpener)
+ : PrivateBrowsingUtils.isWindowPrivate(window);
+
+ switch (aWhere) {
+ case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW :
+ // FIXME: Bug 408379. So how come this doesn't send the
+ // referrer like the other loads do?
+ var url = aURI ? aURI.spec : "about:blank";
+ let features = "all,dialog=no";
+ if (isPrivate) {
+ features += ",private";
+ }
+ // Pass all params to openDialog to ensure that "url" isn't passed through
+ // loadOneOrMoreURIs, which splits based on "|"
+ newWindow = openDialog(getBrowserURL(), "_blank", features, url, null, null, null);
+ break;
+ case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB :
+ // If we have an opener, that means that the caller is expecting access
+ // to the nsIDOMWindow of the opened tab right away. For e10s windows,
+ // this means forcing the newly opened browser to be non-remote so that
+ // we can hand back the nsIDOMWindow. The XULBrowserWindow.shouldLoadURI
+ // will do the job of shuttling off the newly opened browser to run in
+ // the right process once it starts loading a URI.
+ let forceNotRemote = !!aOpener;
+ let userContextId = aOpener && aOpener.document
+ ? aOpener.document.nodePrincipal.originAttributes.userContextId
+ : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+ let openerWindow = (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener;
+ let browser = this._openURIInNewTab(aURI, referrer, referrerPolicy,
+ isPrivate, isExternal,
+ forceNotRemote, userContextId,
+ openerWindow);
+ if (browser)
+ newWindow = browser.contentWindow;
+ break;
+ default : // OPEN_CURRENTWINDOW or an illegal value
+ newWindow = content;
+ if (aURI) {
+ let loadflags = isExternal ?
+ Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ gBrowser.loadURIWithFlags(aURI.spec, {
+ flags: loadflags,
+ referrerURI: referrer,
+ referrerPolicy: referrerPolicy,
+ });
+ }
+ if (!gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground"))
+ window.focus();
+ }
+ return newWindow;
+ },
+
+ openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aFlags) {
+ if (aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) {
+ dump("Error: openURIInFrame can only open in new tabs");
+ return null;
+ }
+
+ var isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+
+ var userContextId = aParams.openerOriginAttributes &&
+ ("userContextId" in aParams.openerOriginAttributes)
+ ? aParams.openerOriginAttributes.userContextId
+ : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID
+
+ let browser = this._openURIInNewTab(aURI, aParams.referrer,
+ aParams.referrerPolicy,
+ aParams.isPrivate,
+ isExternal, false,
+ userContextId);
+ if (browser)
+ return browser.QueryInterface(Ci.nsIFrameLoaderOwner);
+
+ return null;
+ },
+
+ isTabContentWindow: function (aWindow) {
+ return gBrowser.browsers.some(browser => browser.contentWindow == aWindow);
+ },
+
+ canClose() {
+ return CanCloseWindow();
+ },
+}
+
+function getTogglableToolbars() {
+ let toolbarNodes = Array.slice(gNavToolbox.childNodes);
+ toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars);
+ toolbarNodes = toolbarNodes.filter(node => node.getAttribute("toolbarname"));
+ return toolbarNodes;
+}
+
+function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
+ var popup = aEvent.target;
+ if (popup != aEvent.currentTarget)
+ return;
+
+ // Empty the menu
+ for (var i = popup.childNodes.length-1; i >= 0; --i) {
+ var deadItem = popup.childNodes[i];
+ if (deadItem.hasAttribute("toolbarId"))
+ popup.removeChild(deadItem);
+ }
+
+ var firstMenuItem = aInsertPoint || popup.firstChild;
+
+ let toolbarNodes = getTogglableToolbars();
+
+ for (let toolbar of toolbarNodes) {
+ let menuItem = document.createElement("menuitem");
+ let hidingAttribute = toolbar.getAttribute("type") == "menubar" ?
+ "autohide" : "collapsed";
+ menuItem.setAttribute("id", "toggle_" + toolbar.id);
+ menuItem.setAttribute("toolbarId", toolbar.id);
+ menuItem.setAttribute("type", "checkbox");
+ menuItem.setAttribute("label", toolbar.getAttribute("toolbarname"));
+ menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true");
+ menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
+ if (popup.id != "toolbar-context-menu")
+ menuItem.setAttribute("key", toolbar.getAttribute("key"));
+
+ popup.insertBefore(menuItem, firstMenuItem);
+
+ menuItem.addEventListener("command", onViewToolbarCommand, false);
+ }
+
+
+ let moveToPanel = popup.querySelector(".customize-context-moveToPanel");
+ let removeFromToolbar = popup.querySelector(".customize-context-removeFromToolbar");
+ // View -> Toolbars menu doesn't have the moveToPanel or removeFromToolbar items.
+ if (!moveToPanel || !removeFromToolbar) {
+ return;
+ }
+
+ // triggerNode can be a nested child element of a toolbaritem.
+ let toolbarItem = popup.triggerNode;
+
+ if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") {
+ toolbarItem = toolbarItem.firstChild;
+ } else if (toolbarItem && toolbarItem.localName != "toolbar") {
+ while (toolbarItem && toolbarItem.parentNode) {
+ let parent = toolbarItem.parentNode;
+ if ((parent.classList && parent.classList.contains("customization-target")) ||
+ parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well.
+ parent.localName == "toolbarpaletteitem" ||
+ parent.localName == "toolbar")
+ break;
+ toolbarItem = parent;
+ }
+ } else {
+ toolbarItem = null;
+ }
+
+ let showTabStripItems = toolbarItem && toolbarItem.id == "tabbrowser-tabs";
+ for (let node of popup.querySelectorAll('menuitem[contexttype="toolbaritem"]')) {
+ node.hidden = showTabStripItems;
+ }
+
+ for (let node of popup.querySelectorAll('menuitem[contexttype="tabbar"]')) {
+ node.hidden = !showTabStripItems;
+ }
+
+ if (showTabStripItems) {
+ PlacesCommandHook.updateBookmarkAllTabsCommand();
+
+ let haveMultipleTabs = gBrowser.visibleTabs.length > 1;
+ document.getElementById("toolbar-context-reloadAllTabs").disabled = !haveMultipleTabs;
+
+ document.getElementById("toolbar-context-undoCloseTab").disabled =
+ SessionStore.getClosedTabCount(window) == 0;
+ return;
+ }
+
+ // In some cases, we will exit the above loop with toolbarItem being the
+ // xul:document. That has no parentNode, and we should disable the items in
+ // this case.
+ let movable = toolbarItem && toolbarItem.parentNode &&
+ CustomizableUI.isWidgetRemovable(toolbarItem);
+ if (movable) {
+ moveToPanel.removeAttribute("disabled");
+ removeFromToolbar.removeAttribute("disabled");
+ } else {
+ moveToPanel.setAttribute("disabled", true);
+ removeFromToolbar.setAttribute("disabled", true);
+ }
+}
+
+function onViewToolbarCommand(aEvent) {
+ var toolbarId = aEvent.originalTarget.getAttribute("toolbarId");
+ var isVisible = aEvent.originalTarget.getAttribute("checked") == "true";
+ CustomizableUI.setToolbarVisibility(toolbarId, isVisible);
+}
+
+function setToolbarVisibility(toolbar, isVisible, persist=true) {
+ let hidingAttribute;
+ if (toolbar.getAttribute("type") == "menubar") {
+ hidingAttribute = "autohide";
+ if (AppConstants.platform == "linux") {
+ Services.prefs.setBoolPref("ui.key.menuAccessKeyFocuses", !isVisible);
+ }
+ } else {
+ hidingAttribute = "collapsed";
+ }
+
+ toolbar.setAttribute(hidingAttribute, !isVisible);
+ if (persist) {
+ document.persist(toolbar.id, hidingAttribute);
+ }
+
+ let eventParams = {
+ detail: {
+ visible: isVisible
+ },
+ bubbles: true
+ };
+ let event = new CustomEvent("toolbarvisibilitychange", eventParams);
+ toolbar.dispatchEvent(event);
+
+ PlacesToolbarHelper.init();
+ BookmarkingUI.onToolbarVisibilityChange();
+ gBrowser.updateWindowResizers();
+ if (isVisible)
+ ToolbarIconColor.inferFromText();
+}
+
+var TabletModeUpdater = {
+ init() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ this.update(WindowsUIUtils.inTabletMode);
+ Services.obs.addObserver(this, "tablet-mode-change", false);
+ }
+ },
+
+ uninit() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ Services.obs.removeObserver(this, "tablet-mode-change");
+ }
+ },
+
+ observe(subject, topic, data) {
+ this.update(data == "tablet-mode");
+ },
+
+ update(isInTabletMode) {
+ let wasInTabletMode = document.documentElement.hasAttribute("tabletmode");
+ if (isInTabletMode) {
+ document.documentElement.setAttribute("tabletmode", "true");
+ } else {
+ document.documentElement.removeAttribute("tabletmode");
+ }
+ if (wasInTabletMode != isInTabletMode) {
+ TabsInTitlebar.updateAppearance(true);
+ }
+ },
+};
+
+var gTabletModePageCounter = {
+ enabled: false,
+ inc() {
+ this.enabled = AppConstants.isPlatformAndVersionAtLeast("win", "10.0");
+ if (!this.enabled) {
+ this.inc = () => {};
+ return;
+ }
+ this.inc = this._realInc;
+ this.inc();
+ },
+
+ _desktopCount: 0,
+ _tabletCount: 0,
+ _realInc() {
+ let inTabletMode = document.documentElement.hasAttribute("tabletmode");
+ this[inTabletMode ? "_tabletCount" : "_desktopCount"]++;
+ },
+
+ finish() {
+ if (this.enabled) {
+ let histogram = Services.telemetry.getKeyedHistogramById("FX_TABLETMODE_PAGE_LOAD");
+ histogram.add("tablet", this._tabletCount);
+ histogram.add("desktop", this._desktopCount);
+ }
+ },
+};
+
+function displaySecurityInfo()
+{
+ BrowserPageInfo(null, "securityTab");
+}
+
+
+var gHomeButton = {
+ prefDomain: "browser.startup.homepage",
+ observe: function (aSubject, aTopic, aPrefName)
+ {
+ if (aTopic != "nsPref:changed" || aPrefName != this.prefDomain)
+ return;
+
+ this.updateTooltip();
+ },
+
+ updateTooltip: function (homeButton)
+ {
+ if (!homeButton)
+ homeButton = document.getElementById("home-button");
+ if (homeButton) {
+ var homePage = this.getHomePage();
+ homePage = homePage.replace(/\|/g, ', ');
+ if (["about:home", "about:newtab"].includes(homePage.toLowerCase()))
+ homeButton.setAttribute("tooltiptext", homeButton.getAttribute("aboutHomeOverrideTooltip"));
+ else
+ homeButton.setAttribute("tooltiptext", homePage);
+ }
+ },
+
+ getHomePage: function ()
+ {
+ var url;
+ try {
+ url = gPrefService.getComplexValue(this.prefDomain,
+ Components.interfaces.nsIPrefLocalizedString).data;
+ } catch (e) {
+ }
+
+ // use this if we can't find the pref
+ if (!url) {
+ var configBundle = Services.strings
+ .createBundle("chrome://branding/locale/browserconfig.properties");
+ url = configBundle.GetStringFromName(this.prefDomain);
+ }
+
+ return url;
+ },
+};
+
+const nodeToTooltipMap = {
+ "bookmarks-menu-button": "bookmarksMenuButton.tooltip",
+ "new-window-button": "newWindowButton.tooltip",
+ "new-tab-button": "newTabButton.tooltip",
+ "tabs-newtab-button": "newTabButton.tooltip",
+ "fullscreen-button": "fullscreenButton.tooltip",
+ "downloads-button": "downloads.tooltip",
+};
+const nodeToShortcutMap = {
+ "bookmarks-menu-button": "manBookmarkKb",
+ "new-window-button": "key_newNavigator",
+ "new-tab-button": "key_newNavigatorTab",
+ "tabs-newtab-button": "key_newNavigatorTab",
+ "fullscreen-button": "key_fullScreen",
+ "downloads-button": "key_openDownloads"
+};
+
+if (AppConstants.platform == "macosx") {
+ nodeToTooltipMap["print-button"] = "printButton.tooltip";
+ nodeToShortcutMap["print-button"] = "printKb";
+}
+
+const gDynamicTooltipCache = new Map();
+function UpdateDynamicShortcutTooltipText(aTooltip) {
+ let nodeId = aTooltip.triggerNode.id || aTooltip.triggerNode.getAttribute("anonid");
+ if (!gDynamicTooltipCache.has(nodeId) && nodeId in nodeToTooltipMap) {
+ let strId = nodeToTooltipMap[nodeId];
+ let args = [];
+ if (nodeId in nodeToShortcutMap) {
+ let shortcutId = nodeToShortcutMap[nodeId];
+ let shortcut = document.getElementById(shortcutId);
+ if (shortcut) {
+ args.push(ShortcutUtils.prettifyShortcut(shortcut));
+ }
+ }
+ gDynamicTooltipCache.set(nodeId, gNavigatorBundle.getFormattedString(strId, args));
+ }
+ aTooltip.setAttribute("label", gDynamicTooltipCache.get(nodeId));
+}
+
+function getBrowserSelection(aCharLen) {
+ Deprecated.warning("getBrowserSelection",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1134769");
+
+ let focusedElement = document.activeElement;
+ if (focusedElement && focusedElement.localName == "browser" &&
+ focusedElement.isRemoteBrowser) {
+ throw "getBrowserSelection doesn't support child process windows";
+ }
+
+ return BrowserUtils.getSelectionDetails(window, aCharLen).text;
+}
+
+var gWebPanelURI;
+function openWebPanel(title, uri) {
+ // Ensure that the web panels sidebar is open.
+ SidebarUI.show("viewWebPanelsSidebar");
+
+ // Set the title of the panel.
+ SidebarUI.title = title;
+
+ // Tell the Web Panels sidebar to load the bookmark.
+ if (SidebarUI.browser.docShell && SidebarUI.browser.contentDocument &&
+ SidebarUI.browser.contentDocument.getElementById("web-panels-browser")) {
+ SidebarUI.browser.contentWindow.loadWebPanel(uri);
+ if (gWebPanelURI) {
+ gWebPanelURI = "";
+ SidebarUI.browser.removeEventListener("load", asyncOpenWebPanel, true);
+ }
+ } else {
+ // The panel is still being constructed. Attach an onload handler.
+ if (!gWebPanelURI) {
+ SidebarUI.browser.addEventListener("load", asyncOpenWebPanel, true);
+ }
+ gWebPanelURI = uri;
+ }
+}
+
+function asyncOpenWebPanel(event) {
+ if (gWebPanelURI && SidebarUI.browser.contentDocument &&
+ SidebarUI.browser.contentDocument.getElementById("web-panels-browser")) {
+ SidebarUI.browser.contentWindow.loadWebPanel(gWebPanelURI);
+ }
+ gWebPanelURI = "";
+ SidebarUI.browser.removeEventListener("load", asyncOpenWebPanel, true);
+}
+
+/*
+ * - [ Dependencies ] ---------------------------------------------------------
+ * utilityOverlay.js:
+ * - gatherTextUnder
+ */
+
+/**
+ * Extracts linkNode and href for the current click target.
+ *
+ * @param event
+ * The click event.
+ * @return [href, linkNode].
+ *
+ * @note linkNode will be null if the click wasn't on an anchor
+ * element (or XLink).
+ */
+function hrefAndLinkNodeForClickEvent(event)
+{
+ function isHTMLLink(aNode)
+ {
+ // Be consistent with what nsContextMenu.js does.
+ return ((aNode instanceof HTMLAnchorElement && aNode.href) ||
+ (aNode instanceof HTMLAreaElement && aNode.href) ||
+ aNode instanceof HTMLLinkElement);
+ }
+
+ let node = event.target;
+ while (node && !isHTMLLink(node)) {
+ node = node.parentNode;
+ }
+
+ if (node)
+ return [node.href, node];
+
+ // If there is no linkNode, try simple XLink.
+ let href, baseURI;
+ node = event.target;
+ while (node && !href) {
+ if (node.nodeType == Node.ELEMENT_NODE &&
+ (node.localName == "a" ||
+ node.namespaceURI == "http://www.w3.org/1998/Math/MathML")) {
+ href = node.getAttribute("href") ||
+ node.getAttributeNS("http://www.w3.org/1999/xlink", "href");
+
+ if (href) {
+ baseURI = node.baseURI;
+ break;
+ }
+ }
+ node = node.parentNode;
+ }
+
+ // In case of XLink, we don't return the node we got href from since
+ // callers expect <a>-like elements.
+ return [href ? makeURLAbsolute(baseURI, href) : null, null];
+}
+
+/**
+ * Called whenever the user clicks in the content area.
+ *
+ * @param event
+ * The click event.
+ * @param isPanelClick
+ * Whether the event comes from a web panel.
+ * @note default event is prevented if the click is handled.
+ */
+function contentAreaClick(event, isPanelClick)
+{
+ if (!event.isTrusted || event.defaultPrevented || event.button == 2)
+ return;
+
+ let [href, linkNode] = hrefAndLinkNodeForClickEvent(event);
+ if (!href) {
+ // Not a link, handle middle mouse navigation.
+ if (event.button == 1 &&
+ gPrefService.getBoolPref("middlemouse.contentLoadURL") &&
+ !gPrefService.getBoolPref("general.autoScroll")) {
+ middleMousePaste(event);
+ event.preventDefault();
+ }
+ return;
+ }
+
+ // This code only applies if we have a linkNode (i.e. clicks on real anchor
+ // elements, as opposed to XLink).
+ if (linkNode && event.button == 0 &&
+ !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
+ // A Web panel's links should target the main content area. Do this
+ // if no modifier keys are down and if there's no target or the target
+ // equals _main (the IE convention) or _content (the Mozilla convention).
+ let target = linkNode.target;
+ let mainTarget = !target || target == "_content" || target == "_main";
+ if (isPanelClick && mainTarget) {
+ // javascript and data links should be executed in the current browser.
+ if (linkNode.getAttribute("onclick") ||
+ href.startsWith("javascript:") ||
+ href.startsWith("data:"))
+ return;
+
+ try {
+ urlSecurityCheck(href, linkNode.ownerDocument.nodePrincipal);
+ }
+ catch (ex) {
+ // Prevent loading unsecure destinations.
+ event.preventDefault();
+ return;
+ }
+
+ loadURI(href, null, null, false);
+ event.preventDefault();
+ return;
+ }
+
+ if (linkNode.getAttribute("rel") == "sidebar") {
+ // This is the Opera convention for a special link that, when clicked,
+ // allows to add a sidebar panel. The link's title attribute contains
+ // the title that should be used for the sidebar panel.
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "bookmark"
+ , uri: makeURI(href)
+ , title: linkNode.getAttribute("title")
+ , loadBookmarkInSidebar: true
+ , hiddenRows: [ "description"
+ , "location"
+ , "keyword" ]
+ }, window);
+ event.preventDefault();
+ return;
+ }
+ }
+
+ handleLinkClick(event, href, linkNode);
+
+ // Mark the page as a user followed link. This is done so that history can
+ // distinguish automatic embed visits from user activated ones. For example
+ // pages loaded in frames are embed visits and lost with the session, while
+ // visits across frames should be preserved.
+ try {
+ if (!PrivateBrowsingUtils.isWindowPrivate(window))
+ PlacesUIUtils.markPageAsFollowedLink(href);
+ } catch (ex) { /* Skip invalid URIs. */ }
+}
+
+/**
+ * Handles clicks on links.
+ *
+ * @return true if the click event was handled, false otherwise.
+ */
+function handleLinkClick(event, href, linkNode) {
+ if (event.button == 2) // right click
+ return false;
+
+ var where = whereToOpenLink(event);
+ if (where == "current")
+ return false;
+
+ var doc = event.target.ownerDocument;
+
+ if (where == "save") {
+ saveURL(href, linkNode ? gatherTextUnder(linkNode) : "", null, true,
+ true, doc.documentURIObject, doc);
+ event.preventDefault();
+ return true;
+ }
+
+ var referrerURI = doc.documentURIObject;
+ // if the mixedContentChannel is present and the referring URI passes
+ // a same origin check with the target URI, we can preserve the users
+ // decision of disabling MCB on a page for it's child tabs.
+ var persistAllowMixedContentInChildTab = false;
+
+ if (where == "tab" && gBrowser.docShell.mixedContentChannel) {
+ const sm = Services.scriptSecurityManager;
+ try {
+ var targetURI = makeURI(href);
+ sm.checkSameOriginURI(referrerURI, targetURI, false);
+ persistAllowMixedContentInChildTab = true;
+ }
+ catch (e) { }
+ }
+
+ // first get document wide referrer policy, then
+ // get referrer attribute from clicked link and parse it and
+ // allow per element referrer to overrule the document wide referrer if enabled
+ let referrerPolicy = doc.referrerPolicy;
+ if (Services.prefs.getBoolPref("network.http.enablePerElementReferrer") &&
+ linkNode) {
+ let referrerAttrValue = Services.netUtils.parseAttributePolicyString(linkNode.
+ getAttribute("referrerpolicy"));
+ if (referrerAttrValue != Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
+ referrerPolicy = referrerAttrValue;
+ }
+ }
+
+ urlSecurityCheck(href, doc.nodePrincipal);
+ let params = {
+ charset: doc.characterSet,
+ allowMixedContent: persistAllowMixedContentInChildTab,
+ referrerURI: referrerURI,
+ referrerPolicy: referrerPolicy,
+ noReferrer: BrowserUtils.linkHasNoReferrer(linkNode),
+ originPrincipal: doc.nodePrincipal,
+ };
+
+ // The new tab/window must use the same userContextId
+ if (doc.nodePrincipal.originAttributes.userContextId) {
+ params.userContextId = doc.nodePrincipal.originAttributes.userContextId;
+ }
+
+ openLinkIn(href, where, params);
+ event.preventDefault();
+ return true;
+}
+
+function middleMousePaste(event) {
+ let clipboard = readFromClipboard();
+ if (!clipboard)
+ return;
+
+ // Strip embedded newlines and surrounding whitespace, to match the URL
+ // bar's behavior (stripsurroundingwhitespace)
+ clipboard = clipboard.replace(/\s*\n\s*/g, "");
+
+ clipboard = stripUnsafeProtocolOnPaste(clipboard);
+
+ // if it's not the current tab, we don't need to do anything because the
+ // browser doesn't exist.
+ let where = whereToOpenLink(event, true, false);
+ let lastLocationChange;
+ if (where == "current") {
+ lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+ }
+
+ getShortcutOrURIAndPostData(clipboard).then(data => {
+ try {
+ makeURI(data.url);
+ } catch (ex) {
+ // Not a valid URI.
+ return;
+ }
+
+ try {
+ addToUrlbarHistory(data.url);
+ } catch (ex) {
+ // Things may go wrong when adding url to session history,
+ // but don't let that interfere with the loading of the url.
+ Cu.reportError(ex);
+ }
+
+ if (where != "current" ||
+ lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) {
+ openUILink(data.url, event,
+ { ignoreButton: true,
+ disallowInheritPrincipal: !data.mayInheritPrincipal });
+ }
+ });
+
+ event.stopPropagation();
+}
+
+function stripUnsafeProtocolOnPaste(pasteData) {
+ // Don't allow pasting javascript URIs since we don't support
+ // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL for those.
+ return pasteData.replace(/^(?:\s*javascript:)+/i, "");
+}
+
+// handleDroppedLink has the following 2 overloads:
+// handleDroppedLink(event, url, name)
+// handleDroppedLink(event, links)
+function handleDroppedLink(event, urlOrLinks, name)
+{
+ let links;
+ if (Array.isArray(urlOrLinks)) {
+ links = urlOrLinks;
+ } else {
+ links = [{ url: urlOrLinks, name, type: "" }];
+ }
+
+ let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+
+ let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid");
+
+ // event is null if links are dropped in content process.
+ // inBackground should be false, as it's loading into current browser.
+ let inBackground = false;
+ if (event) {
+ inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+ if (event.shiftKey)
+ inBackground = !inBackground;
+ }
+
+ Task.spawn(function*() {
+ let urls = [];
+ let postDatas = [];
+ for (let link of links) {
+ let data = yield getShortcutOrURIAndPostData(link.url);
+ urls.push(data.url);
+ postDatas.push(data.postData);
+ }
+ if (lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) {
+ gBrowser.loadTabs(urls, {
+ inBackground,
+ replace: true,
+ allowThirdPartyFixup: false,
+ postDatas,
+ userContextId,
+ });
+ }
+ });
+
+ // If links are dropped in content process, event.preventDefault() should be
+ // called in content process.
+ if (event) {
+ // Keep the event from being handled by the dragDrop listeners
+ // built-in to gecko if they happen to be above us.
+ event.preventDefault();
+ }
+}
+
+function BrowserSetForcedCharacterSet(aCharset)
+{
+ if (aCharset) {
+ gBrowser.selectedBrowser.characterSet = aCharset;
+ // Save the forced character-set
+ if (!PrivateBrowsingUtils.isWindowPrivate(window))
+ PlacesUtils.setCharsetForURI(getWebNavigation().currentURI, aCharset);
+ }
+ BrowserCharsetReload();
+}
+
+function BrowserCharsetReload()
+{
+ BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+}
+
+function UpdateCurrentCharset(target) {
+ let selectedCharset = CharsetMenu.foldCharset(gBrowser.selectedBrowser.characterSet);
+ for (let menuItem of target.getElementsByTagName("menuitem")) {
+ let isSelected = menuItem.getAttribute("charset") === selectedCharset;
+ menuItem.setAttribute("checked", isSelected);
+ }
+}
+
+var gPageStyleMenu = {
+ // This maps from a <browser> element (or, more specifically, a
+ // browser's permanentKey) to an Object that contains the most recent
+ // information about the browser content's stylesheets. That Object
+ // is populated via the PageStyle:StyleSheets message from the content
+ // process. The Object should have the following structure:
+ //
+ // filteredStyleSheets (Array):
+ // An Array of objects with a filtered list representing all stylesheets
+ // that the current page offers. Each object has the following members:
+ //
+ // title (String):
+ // The title of the stylesheet
+ //
+ // disabled (bool):
+ // Whether or not the stylesheet is currently applied
+ //
+ // href (String):
+ // The URL of the stylesheet. Stylesheets loaded via a data URL will
+ // have this property set to null.
+ //
+ // authorStyleDisabled (bool):
+ // Whether or not the user currently has "No Style" selected for
+ // the current page.
+ //
+ // preferredStyleSheetSet (bool):
+ // Whether or not the user currently has the "Default" style selected
+ // for the current page.
+ //
+ _pageStyleSheets: new WeakMap(),
+
+ init: function() {
+ let mm = window.messageManager;
+ mm.addMessageListener("PageStyle:StyleSheets", (msg) => {
+ this._pageStyleSheets.set(msg.target.permanentKey, msg.data);
+ });
+ },
+
+ /**
+ * Returns an array of Objects representing stylesheets in a
+ * browser. Note that the pageshow event needs to fire in content
+ * before this information will be available.
+ *
+ * @param browser (optional)
+ * The <xul:browser> to search for stylesheets. If omitted, this
+ * defaults to the currently selected tab's browser.
+ * @returns Array
+ * An Array of Objects representing stylesheets in the browser.
+ * See the documentation for gPageStyleMenu for a description
+ * of the Object structure.
+ */
+ getBrowserStyleSheets: function (browser) {
+ if (!browser) {
+ browser = gBrowser.selectedBrowser;
+ }
+
+ let data = this._pageStyleSheets.get(browser.permanentKey);
+ if (!data) {
+ return [];
+ }
+ return data.filteredStyleSheets;
+ },
+
+ _getStyleSheetInfo: function (browser) {
+ let data = this._pageStyleSheets.get(browser.permanentKey);
+ if (!data) {
+ return {
+ filteredStyleSheets: [],
+ authorStyleDisabled: false,
+ preferredStyleSheetSet: true
+ };
+ }
+
+ return data;
+ },
+
+ fillPopup: function (menuPopup) {
+ let styleSheetInfo = this._getStyleSheetInfo(gBrowser.selectedBrowser);
+ var noStyle = menuPopup.firstChild;
+ var persistentOnly = noStyle.nextSibling;
+ var sep = persistentOnly.nextSibling;
+ while (sep.nextSibling)
+ menuPopup.removeChild(sep.nextSibling);
+
+ let styleSheets = styleSheetInfo.filteredStyleSheets;
+ var currentStyleSheets = {};
+ var styleDisabled = styleSheetInfo.authorStyleDisabled;
+ var haveAltSheets = false;
+ var altStyleSelected = false;
+
+ for (let currentStyleSheet of styleSheets) {
+ if (!currentStyleSheet.disabled)
+ altStyleSelected = true;
+
+ haveAltSheets = true;
+
+ let lastWithSameTitle = null;
+ if (currentStyleSheet.title in currentStyleSheets)
+ lastWithSameTitle = currentStyleSheets[currentStyleSheet.title];
+
+ if (!lastWithSameTitle) {
+ let menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("type", "radio");
+ menuItem.setAttribute("label", currentStyleSheet.title);
+ menuItem.setAttribute("data", currentStyleSheet.title);
+ menuItem.setAttribute("checked", !currentStyleSheet.disabled && !styleDisabled);
+ menuItem.setAttribute("oncommand", "gPageStyleMenu.switchStyleSheet(this.getAttribute('data'));");
+ menuPopup.appendChild(menuItem);
+ currentStyleSheets[currentStyleSheet.title] = menuItem;
+ } else if (currentStyleSheet.disabled) {
+ lastWithSameTitle.removeAttribute("checked");
+ }
+ }
+
+ noStyle.setAttribute("checked", styleDisabled);
+ persistentOnly.setAttribute("checked", !altStyleSelected && !styleDisabled);
+ persistentOnly.hidden = styleSheetInfo.preferredStyleSheetSet ? haveAltSheets : false;
+ sep.hidden = (noStyle.hidden && persistentOnly.hidden) || !haveAltSheets;
+ },
+
+ switchStyleSheet: function (title) {
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.sendAsyncMessage("PageStyle:Switch", {title: title});
+ },
+
+ disableStyle: function () {
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.sendAsyncMessage("PageStyle:Disable");
+ },
+};
+
+/* Legacy global page-style functions */
+var stylesheetFillPopup = gPageStyleMenu.fillPopup.bind(gPageStyleMenu);
+function stylesheetSwitchAll(contentWindow, title) {
+ // We ignore the contentWindow param. Add-ons don't appear to use
+ // it, and it's difficult to support in e10s (where it will be a
+ // CPOW).
+ gPageStyleMenu.switchStyleSheet(title);
+}
+function setStyleDisabled(disabled) {
+ if (disabled)
+ gPageStyleMenu.disableStyle();
+}
+
+
+var LanguageDetectionListener = {
+ init: function() {
+ window.messageManager.addMessageListener("Translation:DocumentState", msg => {
+ Translation.documentStateReceived(msg.target, msg.data);
+ });
+ }
+};
+
+
+var BrowserOffline = {
+ _inited: false,
+
+ // BrowserOffline Public Methods
+ init: function ()
+ {
+ if (!this._uiElement)
+ this._uiElement = document.getElementById("workOfflineMenuitemState");
+
+ Services.obs.addObserver(this, "network:offline-status-changed", false);
+
+ this._updateOfflineUI(Services.io.offline);
+
+ this._inited = true;
+ },
+
+ uninit: function ()
+ {
+ if (this._inited) {
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+ }
+ },
+
+ toggleOfflineStatus: function ()
+ {
+ var ioService = Services.io;
+
+ if (!ioService.offline && !this._canGoOffline()) {
+ this._updateOfflineUI(false);
+ return;
+ }
+
+ ioService.offline = !ioService.offline;
+ },
+
+ // nsIObserver
+ observe: function (aSubject, aTopic, aState)
+ {
+ if (aTopic != "network:offline-status-changed")
+ return;
+
+ // This notification is also received because of a loss in connectivity,
+ // which we ignore by updating the UI to the current value of io.offline
+ this._updateOfflineUI(Services.io.offline);
+ },
+
+ // BrowserOffline Implementation Methods
+ _canGoOffline: function ()
+ {
+ try {
+ var cancelGoOffline = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelGoOffline, "offline-requested", null);
+
+ // Something aborted the quit process.
+ if (cancelGoOffline.data)
+ return false;
+ }
+ catch (ex) {
+ }
+
+ return true;
+ },
+
+ _uiElement: null,
+ _updateOfflineUI: function (aOffline)
+ {
+ var offlineLocked = gPrefService.prefIsLocked("network.online");
+ if (offlineLocked)
+ this._uiElement.setAttribute("disabled", "true");
+
+ this._uiElement.setAttribute("checked", aOffline);
+ }
+};
+
+var OfflineApps = {
+ warnUsage(browser, uri) {
+ if (!browser)
+ return;
+
+ let mainAction = {
+ label: gNavigatorBundle.getString("offlineApps.manageUsage"),
+ accessKey: gNavigatorBundle.getString("offlineApps.manageUsageAccessKey"),
+ callback: this.manage
+ };
+
+ let warnQuotaKB = Services.prefs.getIntPref("offline-apps.quota.warn");
+ // This message shows the quota in MB, and so we divide the quota (in kb) by 1024.
+ let message = gNavigatorBundle.getFormattedString("offlineApps.usage",
+ [ uri.host,
+ warnQuotaKB / 1024 ]);
+
+ let anchorID = "indexedDB-notification-icon";
+ PopupNotifications.show(browser, "offline-app-usage", message,
+ anchorID, mainAction);
+
+ // Now that we've warned once, prevent the warning from showing up
+ // again.
+ Services.perms.add(uri, "offline-app",
+ Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN);
+ },
+
+ // XXX: duplicated in preferences/advanced.js
+ _getOfflineAppUsage(host, groups) {
+ let cacheService = Cc["@mozilla.org/network/application-cache-service;1"].
+ getService(Ci.nsIApplicationCacheService);
+ if (!groups) {
+ try {
+ groups = cacheService.getGroups();
+ } catch (ex) {
+ return 0;
+ }
+ }
+
+ let usage = 0;
+ for (let group of groups) {
+ let uri = Services.io.newURI(group, null, null);
+ if (uri.asciiHost == host) {
+ let cache = cacheService.getActiveCache(group);
+ usage += cache.usage;
+ }
+ }
+
+ return usage;
+ },
+
+ _usedMoreThanWarnQuota(uri) {
+ // if the user has already allowed excessive usage, don't bother checking
+ if (Services.perms.testExactPermission(uri, "offline-app") !=
+ Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN) {
+ let usageBytes = this._getOfflineAppUsage(uri.asciiHost);
+ let warnQuotaKB = Services.prefs.getIntPref("offline-apps.quota.warn");
+ // The pref is in kb, the usage we get is in bytes, so multiply the quota
+ // to compare correctly:
+ if (usageBytes >= warnQuotaKB * 1024) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ requestPermission(browser, docId, uri) {
+ let host = uri.asciiHost;
+ let notificationID = "offline-app-requested-" + host;
+ let notification = PopupNotifications.getNotification(notificationID, browser);
+
+ if (notification) {
+ notification.options.controlledItems.push([
+ Cu.getWeakReference(browser), docId, uri
+ ]);
+ } else {
+ let mainAction = {
+ label: gNavigatorBundle.getString("offlineApps.allow"),
+ accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"),
+ callback: function() {
+ for (let [browser, docId, uri] of notification.options.controlledItems) {
+ OfflineApps.allowSite(browser, docId, uri);
+ }
+ }
+ };
+ let secondaryActions = [{
+ label: gNavigatorBundle.getString("offlineApps.never"),
+ accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"),
+ callback: function() {
+ for (let [, , uri] of notification.options.controlledItems) {
+ OfflineApps.disallowSite(uri);
+ }
+ }
+ }];
+ let message = gNavigatorBundle.getFormattedString("offlineApps.available",
+ [host]);
+ let anchorID = "indexedDB-notification-icon";
+ let options = {
+ controlledItems : [[Cu.getWeakReference(browser), docId, uri]]
+ };
+ notification = PopupNotifications.show(browser, notificationID, message,
+ anchorID, mainAction,
+ secondaryActions, options);
+ }
+ },
+
+ disallowSite(uri) {
+ Services.perms.add(uri, "offline-app", Services.perms.DENY_ACTION);
+ },
+
+ allowSite(browserRef, docId, uri) {
+ Services.perms.add(uri, "offline-app", Services.perms.ALLOW_ACTION);
+
+ // When a site is enabled while loading, manifest resources will
+ // start fetching immediately. This one time we need to do it
+ // ourselves.
+ let browser = browserRef.get();
+ if (browser && browser.messageManager) {
+ browser.messageManager.sendAsyncMessage("OfflineApps:StartFetching", {
+ docId,
+ });
+ }
+ },
+
+ manage() {
+ openAdvancedPreferences("networkTab");
+ },
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "OfflineApps:CheckUsage":
+ let uri = makeURI(msg.data.uri);
+ if (this._usedMoreThanWarnQuota(uri)) {
+ this.warnUsage(msg.target, uri);
+ }
+ break;
+ case "OfflineApps:RequestPermission":
+ this.requestPermission(msg.target, msg.data.docId, makeURI(msg.data.uri));
+ break;
+ }
+ },
+
+ init() {
+ let mm = window.messageManager;
+ mm.addMessageListener("OfflineApps:CheckUsage", this);
+ mm.addMessageListener("OfflineApps:RequestPermission", this);
+ },
+};
+
+var IndexedDBPromptHelper = {
+ _permissionsPrompt: "indexedDB-permissions-prompt",
+ _permissionsResponse: "indexedDB-permissions-response",
+
+ _notificationIcon: "indexedDB-notification-icon",
+
+ init:
+ function IndexedDBPromptHelper_init() {
+ Services.obs.addObserver(this, this._permissionsPrompt, false);
+ },
+
+ uninit:
+ function IndexedDBPromptHelper_uninit() {
+ Services.obs.removeObserver(this, this._permissionsPrompt);
+ },
+
+ observe:
+ function IndexedDBPromptHelper_observe(subject, topic, data) {
+ if (topic != this._permissionsPrompt) {
+ throw new Error("Unexpected topic!");
+ }
+
+ var requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
+
+ var browser = requestor.getInterface(Ci.nsIDOMNode);
+ if (browser.ownerGlobal != window) {
+ // Only listen for notifications for browsers in our chrome window.
+ return;
+ }
+
+ var host = browser.currentURI.asciiHost;
+
+ var message;
+ var responseTopic;
+ if (topic == this._permissionsPrompt) {
+ message = gNavigatorBundle.getFormattedString("offlineApps.available",
+ [ host ]);
+ responseTopic = this._permissionsResponse;
+ }
+
+ const hiddenTimeoutDuration = 30000; // 30 seconds
+ const firstTimeoutDuration = 300000; // 5 minutes
+
+ var timeoutId;
+
+ var observer = requestor.getInterface(Ci.nsIObserver);
+
+ var mainAction = {
+ label: gNavigatorBundle.getString("offlineApps.allow"),
+ accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"),
+ callback: function() {
+ clearTimeout(timeoutId);
+ observer.observe(null, responseTopic,
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+ }
+ };
+
+ var secondaryActions = [
+ {
+ label: gNavigatorBundle.getString("offlineApps.never"),
+ accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"),
+ callback: function() {
+ clearTimeout(timeoutId);
+ observer.observe(null, responseTopic,
+ Ci.nsIPermissionManager.DENY_ACTION);
+ }
+ }
+ ];
+
+ // This will be set to the result of PopupNotifications.show().
+ var notification;
+
+ function timeoutNotification() {
+ // Remove the notification.
+ if (notification) {
+ notification.remove();
+ }
+
+ // Clear all of our timeout stuff. We may be called directly, not just
+ // when the timeout actually elapses.
+ clearTimeout(timeoutId);
+
+ // And tell the page that the popup timed out.
+ observer.observe(null, responseTopic,
+ Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ }
+
+ var options = {
+ eventCallback: function(state) {
+ // Don't do anything if the timeout has not been set yet.
+ if (!timeoutId) {
+ return;
+ }
+
+ // If the popup is being dismissed start the short timeout.
+ if (state == "dismissed") {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(timeoutNotification, hiddenTimeoutDuration);
+ return;
+ }
+
+ // If the popup is being re-shown then clear the timeout allowing
+ // unlimited waiting.
+ if (state == "shown") {
+ clearTimeout(timeoutId);
+ }
+ }
+ };
+
+ notification = PopupNotifications.show(browser, topic, message,
+ this._notificationIcon, mainAction,
+ secondaryActions, options);
+
+ // Set the timeoutId after the popup has been created, and use the long
+ // timeout value. If the user doesn't notice the popup after this amount of
+ // time then it is most likely not visible and we want to alert the page.
+ timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
+ }
+};
+
+function CanCloseWindow()
+{
+ // Avoid redundant calls to canClose from showing multiple
+ // PermitUnload dialogs.
+ if (Services.startup.shuttingDown || window.skipNextCanClose) {
+ return true;
+ }
+
+ for (let browser of gBrowser.browsers) {
+ let {permitUnload, timedOut} = browser.permitUnload();
+ if (timedOut) {
+ return true;
+ }
+ if (!permitUnload) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function WindowIsClosing()
+{
+ if (!closeWindow(false, warnAboutClosingWindow))
+ return false;
+
+ // In theory we should exit here and the Window's internal Close
+ // method should trigger canClose on nsBrowserAccess. However, by
+ // that point it's too late to be able to show a prompt for
+ // PermitUnload. So we do it here, when we still can.
+ if (CanCloseWindow()) {
+ // This flag ensures that the later canClose call does nothing.
+ // It's only needed to make tests pass, since they detect the
+ // prompt even when it's not actually shown.
+ window.skipNextCanClose = true;
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Checks if this is the last full *browser* window around. If it is, this will
+ * be communicated like quitting. Otherwise, we warn about closing multiple tabs.
+ * @returns true if closing can proceed, false if it got cancelled.
+ */
+function warnAboutClosingWindow() {
+ // Popups aren't considered full browser windows; we also ignore private windows.
+ let isPBWindow = PrivateBrowsingUtils.isWindowPrivate(window) &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing;
+ if (!isPBWindow && !toolbar.visible)
+ return gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL);
+
+ // Figure out if there's at least one other browser window around.
+ let otherPBWindowExists = false;
+ let nonPopupPresent = false;
+ for (let win of browserWindows()) {
+ if (!win.closed && win != window) {
+ if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win))
+ otherPBWindowExists = true;
+ if (win.toolbar.visible)
+ nonPopupPresent = true;
+ // If the current window is not in private browsing mode we don't need to
+ // look for other pb windows, we can leave the loop when finding the
+ // first non-popup window. If however the current window is in private
+ // browsing mode then we need at least one other pb and one non-popup
+ // window to break out early.
+ if ((!isPBWindow || otherPBWindowExists) && nonPopupPresent)
+ break;
+ }
+ }
+
+ if (isPBWindow && !otherPBWindowExists) {
+ let exitingCanceled = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ exitingCanceled.data = false;
+ Services.obs.notifyObservers(exitingCanceled,
+ "last-pb-context-exiting",
+ null);
+ if (exitingCanceled.data)
+ return false;
+ }
+
+ if (nonPopupPresent) {
+ return isPBWindow || gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL);
+ }
+
+ let os = Services.obs;
+
+ let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ os.notifyObservers(closingCanceled,
+ "browser-lastwindow-close-requested", null);
+ if (closingCanceled.data)
+ return false;
+
+ os.notifyObservers(null, "browser-lastwindow-close-granted", null);
+
+ // OS X doesn't quit the application when the last window is closed, but keeps
+ // the session alive. Hence don't prompt users to save tabs, but warn about
+ // closing multiple tabs.
+ return AppConstants.platform != "macosx"
+ || (isPBWindow || gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL));
+}
+
+var MailIntegration = {
+ sendLinkForBrowser: function (aBrowser) {
+ this.sendMessage(aBrowser.currentURI.spec, aBrowser.contentTitle);
+ },
+
+ sendMessage: function (aBody, aSubject) {
+ // generate a mailto url based on the url and the url's title
+ var mailtoUrl = "mailto:";
+ if (aBody) {
+ mailtoUrl += "?body=" + encodeURIComponent(aBody);
+ mailtoUrl += "&subject=" + encodeURIComponent(aSubject);
+ }
+
+ var uri = makeURI(mailtoUrl);
+
+ // now pass this uri to the operating system
+ this._launchExternalUrl(uri);
+ },
+
+ // a generic method which can be used to pass arbitrary urls to the operating
+ // system.
+ // aURL --> a nsIURI which represents the url to launch
+ _launchExternalUrl: function (aURL) {
+ var extProtocolSvc =
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService);
+ if (extProtocolSvc)
+ extProtocolSvc.loadUrl(aURL);
+ }
+};
+
+function BrowserOpenAddonsMgr(aView) {
+ return new Promise(resolve => {
+ if (aView) {
+ let emWindow;
+ let browserWindow;
+
+ var receivePong = function receivePong(aSubject, aTopic, aData) {
+ let browserWin = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ if (!emWindow || browserWin == window /* favor the current window */) {
+ emWindow = aSubject;
+ browserWindow = browserWin;
+ }
+ }
+ Services.obs.addObserver(receivePong, "EM-pong", false);
+ Services.obs.notifyObservers(null, "EM-ping", "");
+ Services.obs.removeObserver(receivePong, "EM-pong");
+
+ if (emWindow) {
+ emWindow.loadView(aView);
+ browserWindow.gBrowser.selectedTab =
+ browserWindow.gBrowser._getTabForContentWindow(emWindow);
+ emWindow.focus();
+ resolve(emWindow);
+ return;
+ }
+ }
+
+ switchToTabHavingURI("about:addons", true);
+
+ if (aView) {
+ // This must be a new load, else the ping/pong would have
+ // found the window above.
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+ aSubject.loadView(aView);
+ resolve(aSubject);
+ }, "EM-loaded", false);
+ } else {
+ resolve();
+ }
+ });
+}
+
+function AddKeywordForSearchField() {
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage);
+
+ let bookmarkData = message.data;
+ let title = gNavigatorBundle.getFormattedString("addKeywordTitleAutoFill",
+ [bookmarkData.title]);
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "bookmark"
+ , uri: makeURI(bookmarkData.spec)
+ , title: title
+ , description: bookmarkData.description
+ , keyword: ""
+ , postData: bookmarkData.postData
+ , charSet: bookmarkData.charset
+ , hiddenRows: [ "location"
+ , "description"
+ , "tags"
+ , "loadInSidebar" ]
+ }, window);
+ }
+ mm.addMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage);
+
+ mm.sendAsyncMessage("ContextMenu:SearchFieldBookmarkData", {}, { target: gContextMenu.target });
+}
+
+/**
+ * Re-open a closed tab.
+ * @param aIndex
+ * The index of the tab (via SessionStore.getClosedTabData)
+ * @returns a reference to the reopened tab.
+ */
+function undoCloseTab(aIndex) {
+ // wallpaper patch to prevent an unnecessary blank tab (bug 343895)
+ var blankTabToRemove = null;
+ if (gBrowser.tabs.length == 1 && isTabEmpty(gBrowser.selectedTab))
+ blankTabToRemove = gBrowser.selectedTab;
+
+ var tab = null;
+ if (SessionStore.getClosedTabCount(window) > (aIndex || 0)) {
+ tab = SessionStore.undoCloseTab(window, aIndex || 0);
+
+ if (blankTabToRemove)
+ gBrowser.removeTab(blankTabToRemove);
+ }
+
+ return tab;
+}
+
+/**
+ * Re-open a closed window.
+ * @param aIndex
+ * The index of the window (via SessionStore.getClosedWindowData)
+ * @returns a reference to the reopened window.
+ */
+function undoCloseWindow(aIndex) {
+ let window = null;
+ if (SessionStore.getClosedWindowCount() > (aIndex || 0))
+ window = SessionStore.undoCloseWindow(aIndex || 0);
+
+ return window;
+}
+
+/*
+ * Determines if a tab is "empty", usually used in the context of determining
+ * if it's ok to close the tab.
+ */
+function isTabEmpty(aTab) {
+ if (aTab.hasAttribute("busy"))
+ return false;
+
+ if (aTab.hasAttribute("customizemode"))
+ return false;
+
+ let browser = aTab.linkedBrowser;
+ if (!isBlankPageURL(browser.currentURI.spec))
+ return false;
+
+ if (!checkEmptyPageOrigin(browser))
+ return false;
+
+ if (browser.canGoForward || browser.canGoBack)
+ return false;
+
+ return true;
+}
+
+/**
+ * Check whether a page can be considered as 'empty', that its URI
+ * reflects its origin, and that if it's loaded in a tab, that tab
+ * could be considered 'empty' (e.g. like the result of opening
+ * a 'blank' new tab).
+ *
+ * We have to do more than just check the URI, because especially
+ * for things like about:blank, it is possible that the opener or
+ * some other page has control over the contents of the page.
+ *
+ * @param browser {Browser}
+ * The browser whose page we're checking (the selected browser
+ * in this window if omitted).
+ * @param uri {nsIURI}
+ * The URI against which we're checking (the browser's currentURI
+ * if omitted).
+ *
+ * @return false if the page was opened by or is controlled by arbitrary web
+ * content, unless that content corresponds with the URI.
+ * true if the page is blank and controlled by a principal matching
+ * that URI (or the system principal if the principal has no URI)
+ */
+function checkEmptyPageOrigin(browser = gBrowser.selectedBrowser,
+ uri = browser.currentURI) {
+ // If another page opened this page with e.g. window.open, this page might
+ // be controlled by its opener - return false.
+ if (browser.hasContentOpener) {
+ return false;
+ }
+ let contentPrincipal = browser.contentPrincipal;
+ // Not all principals have URIs...
+ if (contentPrincipal.URI) {
+ // There are two specialcases involving about:blank. One is where
+ // the user has manually loaded it and it got created with a null
+ // principal. The other involves the case where we load
+ // some other empty page in a browser and the current page is the
+ // initial about:blank page (which has that as its principal, not
+ // just URI in which case it could be web-based). Especially in
+ // e10s, we need to tackle that case specifically to avoid race
+ // conditions when updating the URL bar.
+ if ((uri.spec == "about:blank" && contentPrincipal.isNullPrincipal) ||
+ contentPrincipal.URI.spec == "about:blank") {
+ return true;
+ }
+ return contentPrincipal.URI.equals(uri);
+ }
+ // ... so for those that don't have them, enforce that the page has the
+ // system principal (this matches e.g. on about:newtab).
+ let ssm = Services.scriptSecurityManager;
+ return ssm.isSystemPrincipal(contentPrincipal);
+}
+
+function BrowserOpenSyncTabs() {
+ gSyncUI.openSyncedTabsPanel();
+}
+
+/**
+ * Format a URL
+ * eg:
+ * echo formatURL("https://addons.mozilla.org/%LOCALE%/%APP%/%VERSION%/");
+ * > https://addons.mozilla.org/en-US/firefox/3.0a1/
+ *
+ * Currently supported built-ins are LOCALE, APP, and any value from nsIXULAppInfo, uppercased.
+ */
+function formatURL(aFormat, aIsPref) {
+ var formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+ return aIsPref ? formatter.formatURLPref(aFormat) : formatter.formatURL(aFormat);
+}
+
+/**
+ * Utility object to handle manipulations of the identity indicators in the UI
+ */
+var gIdentityHandler = {
+ /**
+ * nsIURI for which the identity UI is displayed. This has been already
+ * processed by nsIURIFixup.createExposableURI.
+ */
+ _uri: null,
+
+ /**
+ * We only know the connection type if this._uri has a defined "host" part.
+ *
+ * These URIs, like "about:" and "data:" URIs, will usually be treated as a
+ * non-secure connection, unless they refer to an internally implemented
+ * browser page or resolve to "file:" URIs.
+ */
+ _uriHasHost: false,
+
+ /**
+ * Whether this._uri refers to an internally implemented browser page.
+ *
+ * Note that this is set for some "about:" pages, but general "chrome:" URIs
+ * are not included in this category by default.
+ */
+ _isSecureInternalUI: false,
+
+ /**
+ * nsISSLStatus metadata provided by gBrowser.securityUI the last time the
+ * identity UI was updated, or null if the connection is not secure.
+ */
+ _sslStatus: null,
+
+ /**
+ * Bitmask provided by nsIWebProgressListener.onSecurityChange.
+ */
+ _state: 0,
+
+ /**
+ * This flag gets set if the identity popup was opened by a keypress,
+ * to be able to focus it on the popupshown event.
+ */
+ _popupTriggeredByKeyboard: false,
+
+ /**
+ * Whether a permission is just removed from permission list.
+ */
+ _permissionJustRemoved: false,
+
+ get _isBroken() {
+ return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
+ },
+
+ get _isSecure() {
+ // If a <browser> is included within a chrome document, then this._state
+ // will refer to the security state for the <browser> and not the top level
+ // document. In this case, don't upgrade the security state in the UI
+ // with the secure state of the embedded <browser>.
+ return !this._isURILoadedFromFile && this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
+ },
+
+ get _isEV() {
+ // If a <browser> is included within a chrome document, then this._state
+ // will refer to the security state for the <browser> and not the top level
+ // document. In this case, don't upgrade the security state in the UI
+ // with the EV state of the embedded <browser>.
+ return !this._isURILoadedFromFile && this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
+ },
+
+ get _isMixedActiveContentLoaded() {
+ return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT;
+ },
+
+ get _isMixedActiveContentBlocked() {
+ return this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
+ },
+
+ get _isMixedPassiveContentLoaded() {
+ return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT;
+ },
+
+ get _isCertUserOverridden() {
+ return this._state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN;
+ },
+
+ get _hasInsecureLoginForms() {
+ // checks if the page has been flagged for an insecure login. Also checks
+ // if the pref to degrade the UI is set to true
+ return LoginManagerParent.hasInsecureLoginForms(gBrowser.selectedBrowser) &&
+ Services.prefs.getBoolPref("security.insecure_password.ui.enabled");
+ },
+
+ // smart getters
+ get _identityPopup () {
+ delete this._identityPopup;
+ return this._identityPopup = document.getElementById("identity-popup");
+ },
+ get _identityBox () {
+ delete this._identityBox;
+ return this._identityBox = document.getElementById("identity-box");
+ },
+ get _identityPopupMultiView () {
+ delete _identityPopupMultiView;
+ return document.getElementById("identity-popup-multiView");
+ },
+ get _identityPopupContentHosts () {
+ delete this._identityPopupContentHosts;
+ let selector = ".identity-popup-headline.host";
+ return this._identityPopupContentHosts = [
+ ...this._identityPopupMultiView._mainView.querySelectorAll(selector),
+ ...document.querySelectorAll(selector)
+ ];
+ },
+ get _identityPopupContentHostless () {
+ delete this._identityPopupContentHostless;
+ let selector = ".identity-popup-headline.hostless";
+ return this._identityPopupContentHostless = [
+ ...this._identityPopupMultiView._mainView.querySelectorAll(selector),
+ ...document.querySelectorAll(selector)
+ ];
+ },
+ get _identityPopupContentOwner () {
+ delete this._identityPopupContentOwner;
+ return this._identityPopupContentOwner =
+ document.getElementById("identity-popup-content-owner");
+ },
+ get _identityPopupContentSupp () {
+ delete this._identityPopupContentSupp;
+ return this._identityPopupContentSupp =
+ document.getElementById("identity-popup-content-supplemental");
+ },
+ get _identityPopupContentVerif () {
+ delete this._identityPopupContentVerif;
+ return this._identityPopupContentVerif =
+ document.getElementById("identity-popup-content-verifier");
+ },
+ get _identityPopupMixedContentLearnMore () {
+ delete this._identityPopupMixedContentLearnMore;
+ return this._identityPopupMixedContentLearnMore =
+ document.getElementById("identity-popup-mcb-learn-more");
+ },
+ get _identityPopupInsecureLoginFormsLearnMore () {
+ delete this._identityPopupInsecureLoginFormsLearnMore;
+ return this._identityPopupInsecureLoginFormsLearnMore =
+ document.getElementById("identity-popup-insecure-login-forms-learn-more");
+ },
+ get _identityIconLabels () {
+ delete this._identityIconLabels;
+ return this._identityIconLabels = document.getElementById("identity-icon-labels");
+ },
+ get _identityIconLabel () {
+ delete this._identityIconLabel;
+ return this._identityIconLabel = document.getElementById("identity-icon-label");
+ },
+ get _connectionIcon () {
+ delete this._connectionIcon;
+ return this._connectionIcon = document.getElementById("connection-icon");
+ },
+ get _overrideService () {
+ delete this._overrideService;
+ return this._overrideService = Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService);
+ },
+ get _identityIconCountryLabel () {
+ delete this._identityIconCountryLabel;
+ return this._identityIconCountryLabel = document.getElementById("identity-icon-country-label");
+ },
+ get _identityIcon () {
+ delete this._identityIcon;
+ return this._identityIcon = document.getElementById("identity-icon");
+ },
+ get _permissionList () {
+ delete this._permissionList;
+ return this._permissionList = document.getElementById("identity-popup-permission-list");
+ },
+ get _permissionEmptyHint() {
+ delete this._permissionEmptyHint;
+ return this._permissionEmptyHint = document.getElementById("identity-popup-permission-empty-hint");
+ },
+ get _permissionReloadHint () {
+ delete this._permissionReloadHint;
+ return this._permissionReloadHint = document.getElementById("identity-popup-permission-reload-hint");
+ },
+ get _permissionAnchors () {
+ delete this._permissionAnchors;
+ let permissionAnchors = {};
+ for (let anchor of document.getElementById("blocked-permissions-container").children) {
+ permissionAnchors[anchor.getAttribute("data-permission-id")] = anchor;
+ }
+ return this._permissionAnchors = permissionAnchors;
+ },
+
+ /**
+ * Handler for mouseclicks on the "More Information" button in the
+ * "identity-popup" panel.
+ */
+ handleMoreInfoClick : function(event) {
+ displaySecurityInfo();
+ event.stopPropagation();
+ this._identityPopup.hidePopup();
+ },
+
+ toggleSubView(name, anchor) {
+ let view = this._identityPopupMultiView;
+ if (view.showingSubView) {
+ view.showMainView();
+ } else {
+ view.showSubView(`identity-popup-${name}View`, anchor);
+ }
+
+ // If an element is focused that's not the anchor, clear the focus.
+ // Elements of hidden views have -moz-user-focus:ignore but setting that
+ // per CSS selector doesn't blur a focused element in those hidden views.
+ if (Services.focus.focusedElement != anchor) {
+ Services.focus.clearFocus(window);
+ }
+ },
+
+ disableMixedContentProtection() {
+ // Use telemetry to measure how often unblocking happens
+ const kMIXED_CONTENT_UNBLOCK_EVENT = 2;
+ let histogram =
+ Services.telemetry.getHistogramById(
+ "MIXED_CONTENT_UNBLOCK_COUNTER");
+ histogram.add(kMIXED_CONTENT_UNBLOCK_EVENT);
+ // Reload the page with the content unblocked
+ BrowserReloadWithFlags(
+ Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT);
+ this._identityPopup.hidePopup();
+ },
+
+ enableMixedContentProtection() {
+ gBrowser.selectedBrowser.messageManager.sendAsyncMessage(
+ "MixedContent:ReenableProtection", {});
+ BrowserReload();
+ this._identityPopup.hidePopup();
+ },
+
+ removeCertException() {
+ if (!this._uriHasHost) {
+ Cu.reportError("Trying to revoke a cert exception on a URI without a host?");
+ return;
+ }
+ let host = this._uri.host;
+ let port = this._uri.port > 0 ? this._uri.port : 443;
+ this._overrideService.clearValidityOverride(host, port);
+ BrowserReloadSkipCache();
+ this._identityPopup.hidePopup();
+ },
+
+ /**
+ * Helper to parse out the important parts of _sslStatus (of the SSL cert in
+ * particular) for use in constructing identity UI strings
+ */
+ getIdentityData : function() {
+ var result = {};
+ var cert = this._sslStatus.serverCert;
+
+ // Human readable name of Subject
+ result.subjectOrg = cert.organization;
+
+ // SubjectName fields, broken up for individual access
+ if (cert.subjectName) {
+ result.subjectNameFields = {};
+ cert.subjectName.split(",").forEach(function(v) {
+ var field = v.split("=");
+ this[field[0]] = field[1];
+ }, result.subjectNameFields);
+
+ // Call out city, state, and country specifically
+ result.city = result.subjectNameFields.L;
+ result.state = result.subjectNameFields.ST;
+ result.country = result.subjectNameFields.C;
+ }
+
+ // Human readable name of Certificate Authority
+ result.caOrg = cert.issuerOrganization || cert.issuerCommonName;
+ result.cert = cert;
+
+ return result;
+ },
+
+ /**
+ * Update the identity user interface for the page currently being displayed.
+ *
+ * This examines the SSL certificate metadata, if available, as well as the
+ * connection type and other security-related state information for the page.
+ *
+ * @param state
+ * Bitmask provided by nsIWebProgressListener.onSecurityChange.
+ * @param uri
+ * nsIURI for which the identity UI should be displayed, already
+ * processed by nsIURIFixup.createExposableURI.
+ */
+ updateIdentity(state, uri) {
+ let shouldHidePopup = this._uri && (this._uri.spec != uri.spec);
+ this._state = state;
+
+ // Firstly, populate the state properties required to display the UI. See
+ // the documentation of the individual properties for details.
+ this.setURI(uri);
+ this._sslStatus = gBrowser.securityUI
+ .QueryInterface(Ci.nsISSLStatusProvider)
+ .SSLStatus;
+ if (this._sslStatus) {
+ this._sslStatus.QueryInterface(Ci.nsISSLStatus);
+ }
+
+ // Then, update the user interface with the available data.
+ this.refreshIdentityBlock();
+ // Handle a location change while the Control Center is focused
+ // by closing the popup (bug 1207542)
+ if (shouldHidePopup) {
+ this._identityPopup.hidePopup();
+ }
+ this.showWeakCryptoInfoBar();
+
+ // NOTE: We do NOT update the identity popup (the control center) when
+ // we receive a new security state on the existing page (i.e. from a
+ // subframe). If the user opened the popup and looks at the provided
+ // information we don't want to suddenly change the panel contents.
+ },
+
+ /**
+ * This is called asynchronously when requested by the Logins module, after
+ * the insecure login forms state for the page has been updated.
+ */
+ refreshForInsecureLoginForms() {
+ // Check this._uri because we don't want to refresh the user interface if
+ // this is called before the first page load in the window for any reason.
+ if (!this._uri) {
+ Cu.reportError("Unexpected early call to refreshForInsecureLoginForms.");
+ return;
+ }
+ this.refreshIdentityBlock();
+ },
+
+ updateSharingIndicator() {
+ let tab = gBrowser.selectedTab;
+ let sharing = tab.getAttribute("sharing");
+ if (sharing)
+ this._identityBox.setAttribute("sharing", sharing);
+ else
+ this._identityBox.removeAttribute("sharing");
+
+ this._sharingState = tab._sharingState;
+
+ if (this._identityPopup.state == "open") {
+ this._handleHeightChange(() => this.updateSitePermissions());
+ }
+ },
+
+ /**
+ * Attempt to provide proper IDN treatment for host names
+ */
+ getEffectiveHost: function() {
+ if (!this._IDNService)
+ this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
+ .getService(Ci.nsIIDNService);
+ try {
+ return this._IDNService.convertToDisplayIDN(this._uri.host, {});
+ } catch (e) {
+ // If something goes wrong (e.g. host is an IP address) just fail back
+ // to the full domain.
+ return this._uri.host;
+ }
+ },
+
+ /**
+ * Return the CSS class name to set on the "fullscreen-warning" element to
+ * display information about connection security in the notification shown
+ * when a site enters the fullscreen mode.
+ */
+ get pointerlockFsWarningClassName() {
+ // Note that the fullscreen warning does not handle _isSecureInternalUI.
+ if (this._uriHasHost && this._isEV) {
+ return "verifiedIdentity";
+ }
+ if (this._uriHasHost && this._isSecure) {
+ return "verifiedDomain";
+ }
+ return "unknownIdentity";
+ },
+
+ /**
+ * Updates the identity block user interface with the data from this object.
+ */
+ refreshIdentityBlock() {
+ if (!this._identityBox) {
+ return;
+ }
+
+ let icon_label = "";
+ let tooltip = "";
+ let icon_country_label = "";
+ let icon_labels_dir = "ltr";
+
+ if (this._isSecureInternalUI) {
+ this._identityBox.className = "chromeUI";
+ let brandBundle = document.getElementById("bundle_brand");
+ icon_label = brandBundle.getString("brandShorterName");
+ } else if (this._uriHasHost && this._isEV) {
+ this._identityBox.className = "verifiedIdentity";
+ if (this._isMixedActiveContentBlocked) {
+ this._identityBox.classList.add("mixedActiveBlocked");
+ }
+
+ if (!this._isCertUserOverridden) {
+ // If it's identified, then we can populate the dialog with credentials
+ let iData = this.getIdentityData();
+ tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier",
+ [iData.caOrg]);
+ icon_label = iData.subjectOrg;
+ if (iData.country)
+ icon_country_label = "(" + iData.country + ")";
+
+ // If the organization name starts with an RTL character, then
+ // swap the positions of the organization and country code labels.
+ // The Unicode ranges reflect the definition of the UCS2_CHAR_IS_BIDI
+ // macro in intl/unicharutil/util/nsBidiUtils.h. When bug 218823 gets
+ // fixed, this test should be replaced by one adhering to the
+ // Unicode Bidirectional Algorithm proper (at the paragraph level).
+ icon_labels_dir = /^[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/.test(icon_label) ?
+ "rtl" : "ltr";
+ }
+
+ } else if (this._uriHasHost && this._isSecure) {
+ this._identityBox.className = "verifiedDomain";
+ if (this._isMixedActiveContentBlocked) {
+ this._identityBox.classList.add("mixedActiveBlocked");
+ }
+ if (!this._isCertUserOverridden) {
+ // It's a normal cert, verifier is the CA Org.
+ tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier",
+ [this.getIdentityData().caOrg]);
+ }
+ } else {
+ this._identityBox.className = "unknownIdentity";
+ if (this._isBroken) {
+ if (this._isMixedActiveContentLoaded) {
+ this._identityBox.classList.add("mixedActiveContent");
+ } else if (this._isMixedActiveContentBlocked) {
+ this._identityBox.classList.add("mixedDisplayContentLoadedActiveBlocked");
+ } else if (this._isMixedPassiveContentLoaded) {
+ this._identityBox.classList.add("mixedDisplayContent");
+ } else {
+ this._identityBox.classList.add("weakCipher");
+ }
+ }
+ if (this._hasInsecureLoginForms) {
+ // Insecure login forms can only be present on "unknown identity"
+ // pages, either already insecure or with mixed active content loaded.
+ this._identityBox.classList.add("insecureLoginForms");
+ }
+ }
+
+ if (this._isCertUserOverridden) {
+ this._identityBox.classList.add("certUserOverridden");
+ // Cert is trusted because of a security exception, verifier is a special string.
+ tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you");
+ }
+
+ let permissionAnchors = this._permissionAnchors;
+
+ // hide all permission icons
+ for (let icon of Object.values(permissionAnchors)) {
+ icon.removeAttribute("showing");
+ }
+
+ // keeps track if we should show an indicator that there are active permissions
+ let hasGrantedPermissions = false;
+
+ // show permission icons
+ for (let permission of SitePermissions.getAllByURI(this._uri)) {
+ if (permission.state === SitePermissions.BLOCK) {
+
+ let icon = permissionAnchors[permission.id];
+ if (icon) {
+ icon.setAttribute("showing", "true");
+ }
+
+ } else if (permission.state === SitePermissions.ALLOW ||
+ permission.state === SitePermissions.SESSION) {
+ hasGrantedPermissions = true;
+ }
+ }
+
+ if (hasGrantedPermissions) {
+ this._identityBox.classList.add("grantedPermissions");
+ }
+
+ // Push the appropriate strings out to the UI
+ this._connectionIcon.tooltipText = tooltip;
+ this._identityIconLabels.tooltipText = tooltip;
+ this._identityIcon.tooltipText = gNavigatorBundle.getString("identity.icon.tooltip");
+ this._identityIconLabel.value = icon_label;
+ this._identityIconCountryLabel.value = icon_country_label;
+ // Set cropping and direction
+ this._identityIconLabel.crop = icon_country_label ? "end" : "center";
+ this._identityIconLabel.parentNode.style.direction = icon_labels_dir;
+ // Hide completely if the organization label is empty
+ this._identityIconLabel.parentNode.collapsed = icon_label ? false : true;
+ },
+
+ /**
+ * Show the weak crypto notification bar.
+ */
+ showWeakCryptoInfoBar() {
+ if (!this._uriHasHost || !this._isBroken || !this._sslStatus.cipherName ||
+ this._sslStatus.cipherName.indexOf("_RC4_") < 0) {
+ return;
+ }
+
+ let notificationBox = gBrowser.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue("weak-crypto");
+ if (notification) {
+ return;
+ }
+
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+ let message = gNavigatorBundle.getFormattedString("weakCryptoOverriding.message",
+ [brandShortName]);
+
+ let host = this._uri.host;
+ let port = 443;
+ try {
+ if (this._uri.port > 0) {
+ port = this._uri.port;
+ }
+ } catch (e) {}
+
+ let buttons = [{
+ label: gNavigatorBundle.getString("revokeOverride.label"),
+ accessKey: gNavigatorBundle.getString("revokeOverride.accesskey"),
+ callback: function (aNotification, aButton) {
+ try {
+ let weakCryptoOverride = Cc["@mozilla.org/security/weakcryptooverride;1"]
+ .getService(Ci.nsIWeakCryptoOverride);
+ weakCryptoOverride.removeWeakCryptoOverride(host, port,
+ PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser));
+ BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }];
+
+ const priority = notificationBox.PRIORITY_WARNING_MEDIUM;
+ notificationBox.appendNotification(message, "weak-crypto", null,
+ priority, buttons);
+ },
+
+ /**
+ * Set up the title and content messages for the identity message popup,
+ * based on the specified mode, and the details of the SSL cert, where
+ * applicable
+ */
+ refreshIdentityPopup() {
+ // Update "Learn More" for Mixed Content Blocking and Insecure Login Forms.
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ this._identityPopupMixedContentLearnMore
+ .setAttribute("href", baseURL + "mixed-content");
+ this._identityPopupInsecureLoginFormsLearnMore
+ .setAttribute("href", baseURL + "insecure-password");
+
+ // Determine connection security information.
+ let connection = "not-secure";
+ if (this._isSecureInternalUI) {
+ connection = "chrome";
+ } else if (this._isURILoadedFromFile) {
+ connection = "file";
+ } else if (this._isEV) {
+ connection = "secure-ev";
+ } else if (this._isCertUserOverridden) {
+ connection = "secure-cert-user-overridden";
+ } else if (this._isSecure) {
+ connection = "secure";
+ }
+
+ // Determine if there are insecure login forms.
+ let loginforms = "secure";
+ if (this._hasInsecureLoginForms) {
+ loginforms = "insecure";
+ }
+
+ // Determine the mixed content state.
+ let mixedcontent = [];
+ if (this._isMixedPassiveContentLoaded) {
+ mixedcontent.push("passive-loaded");
+ }
+ if (this._isMixedActiveContentLoaded) {
+ mixedcontent.push("active-loaded");
+ } else if (this._isMixedActiveContentBlocked) {
+ mixedcontent.push("active-blocked");
+ }
+ mixedcontent = mixedcontent.join(" ");
+
+ // We have no specific flags for weak ciphers (yet). If a connection is
+ // broken and we can't detect any mixed content loaded then it's a weak
+ // cipher.
+ let ciphers = "";
+ if (this._isBroken && !this._isMixedActiveContentLoaded && !this._isMixedPassiveContentLoaded) {
+ ciphers = "weak";
+ }
+
+ // Update all elements.
+ let elementIDs = [
+ "identity-popup",
+ "identity-popup-securityView-body",
+ ];
+
+ function updateAttribute(elem, attr, value) {
+ if (value) {
+ elem.setAttribute(attr, value);
+ } else {
+ elem.removeAttribute(attr);
+ }
+ }
+
+ for (let id of elementIDs) {
+ let element = document.getElementById(id);
+ updateAttribute(element, "connection", connection);
+ updateAttribute(element, "loginforms", loginforms);
+ updateAttribute(element, "ciphers", ciphers);
+ updateAttribute(element, "mixedcontent", mixedcontent);
+ updateAttribute(element, "isbroken", this._isBroken);
+ }
+
+ // Initialize the optional strings to empty values
+ let supplemental = "";
+ let verifier = "";
+ let host = "";
+ let owner = "";
+ let hostless = false;
+
+ try {
+ host = this.getEffectiveHost();
+ } catch (e) {
+ // Some URIs might have no hosts.
+ }
+
+ // Fallback for special protocols.
+ if (!host) {
+ host = this._uri.specIgnoringRef;
+ // Special URIs without a host (eg, about:) should crop the end so
+ // the protocol can be seen.
+ hostless = true;
+ }
+
+ // Fill in the CA name if we have a valid TLS certificate.
+ if (this._isSecure || this._isCertUserOverridden) {
+ verifier = this._identityIconLabels.tooltipText;
+ }
+
+ // Fill in organization information if we have a valid EV certificate.
+ if (this._isEV) {
+ let iData = this.getIdentityData();
+ host = owner = iData.subjectOrg;
+ verifier = this._identityIconLabels.tooltipText;
+
+ // Build an appropriate supplemental block out of whatever location data we have
+ if (iData.city)
+ supplemental += iData.city + "\n";
+ if (iData.state && iData.country)
+ supplemental += gNavigatorBundle.getFormattedString("identity.identified.state_and_country",
+ [iData.state, iData.country]);
+ else if (iData.state) // State only
+ supplemental += iData.state;
+ else if (iData.country) // Country only
+ supplemental += iData.country;
+ }
+
+ // Push the appropriate strings out to the UI.
+ this._identityPopupContentHosts.forEach((el) => {
+ el.textContent = host;
+ el.hidden = hostless;
+ });
+ this._identityPopupContentHostless.forEach((el) => {
+ el.setAttribute("value", host);
+ el.hidden = !hostless;
+ });
+ this._identityPopupContentOwner.textContent = owner;
+ this._identityPopupContentSupp.textContent = supplemental;
+ this._identityPopupContentVerif.textContent = verifier;
+
+ // Update per-site permissions section.
+ this.updateSitePermissions();
+ },
+
+ setURI(uri) {
+ this._uri = uri;
+
+ try {
+ this._uri.host;
+ this._uriHasHost = true;
+ } catch (ex) {
+ this._uriHasHost = false;
+ }
+
+ let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|searchreset|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
+ this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.path);
+
+ // Create a channel for the sole purpose of getting the resolved URI
+ // of the request to determine if it's loaded from the file system.
+ this._isURILoadedFromFile = false;
+ let chanOptions = {uri: this._uri, loadUsingSystemPrincipal: true};
+ let resolvedURI;
+ try {
+ resolvedURI = NetUtil.newChannel(chanOptions).URI;
+ if (resolvedURI.schemeIs("jar")) {
+ // Given a URI "jar:<jar-file-uri>!/<jar-entry>"
+ // create a new URI using <jar-file-uri>!/<jar-entry>
+ resolvedURI = NetUtil.newURI(resolvedURI.path);
+ }
+ // Check the URI again after resolving.
+ this._isURILoadedFromFile = resolvedURI.schemeIs("file");
+ } catch (ex) {
+ // NetUtil's methods will throw for malformed URIs and the like
+ }
+ },
+
+ /**
+ * Click handler for the identity-box element in primary chrome.
+ */
+ handleIdentityButtonEvent : function(event) {
+ event.stopPropagation();
+
+ if ((event.type == "click" && event.button != 0) ||
+ (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
+ event.keyCode != KeyEvent.DOM_VK_RETURN)) {
+ return; // Left click, space or enter only
+ }
+
+ // Don't allow left click, space or enter if the location has been modified.
+ if (gURLBar.getAttribute("pageproxystate") != "valid") {
+ return;
+ }
+
+ this._popupTriggeredByKeyboard = event.type == "keypress";
+
+ // Make sure that the display:none style we set in xul is removed now that
+ // the popup is actually needed
+ this._identityPopup.hidden = false;
+
+ // Update the popup strings
+ this.refreshIdentityPopup();
+
+ // Add the "open" attribute to the identity box for styling
+ this._identityBox.setAttribute("open", "true");
+
+ // Now open the popup, anchored off the primary chrome element
+ this._identityPopup.openPopup(this._identityIcon, "bottomcenter topleft");
+ },
+
+ onPopupShown(event) {
+ if (event.target == this._identityPopup) {
+ if (this._popupTriggeredByKeyboard) {
+ // Move focus to the next available element in the identity popup.
+ // This is required by role=alertdialog and fixes an issue where
+ // an already open panel would steal focus from the identity popup.
+ document.commandDispatcher.advanceFocusIntoSubtree(this._identityPopup);
+ }
+
+ window.addEventListener("focus", this, true);
+ }
+ },
+
+ onPopupHidden(event) {
+ if (event.target == this._identityPopup) {
+ window.removeEventListener("focus", this, true);
+ this._identityBox.removeAttribute("open");
+ }
+ },
+
+ handleEvent(event) {
+ let elem = document.activeElement;
+ let position = elem.compareDocumentPosition(this._identityPopup);
+
+ if (!(position & (Node.DOCUMENT_POSITION_CONTAINS |
+ Node.DOCUMENT_POSITION_CONTAINED_BY)) &&
+ !this._identityPopup.hasAttribute("noautohide")) {
+ // Hide the panel when focusing an element that is
+ // neither an ancestor nor descendant unless the panel has
+ // @noautohide (e.g. for a tour).
+ this._identityPopup.hidePopup();
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "perm-changed") {
+ this.refreshIdentityBlock();
+ }
+ },
+
+ onDragStart: function (event) {
+ if (gURLBar.getAttribute("pageproxystate") != "valid")
+ return;
+
+ let value = gBrowser.currentURI.spec;
+ let urlString = value + "\n" + gBrowser.contentTitle;
+ let htmlString = "<a href=\"" + value + "\">" + value + "</a>";
+
+ let dt = event.dataTransfer;
+ dt.setData("text/x-moz-url", urlString);
+ dt.setData("text/uri-list", value);
+ dt.setData("text/plain", value);
+ dt.setData("text/html", htmlString);
+ dt.setDragImage(this._identityIcon, 16, 16);
+ },
+
+ onLocationChange: function () {
+ this._permissionJustRemoved = false;
+ this.updatePermissionHint();
+ },
+
+ updatePermissionHint: function () {
+ if (!this._permissionList.hasChildNodes() && !this._permissionJustRemoved) {
+ this._permissionEmptyHint.removeAttribute("hidden");
+ } else {
+ this._permissionEmptyHint.setAttribute("hidden", "true");
+ }
+
+ if (this._permissionJustRemoved) {
+ this._permissionReloadHint.removeAttribute("hidden");
+ } else {
+ this._permissionReloadHint.setAttribute("hidden", "true");
+ }
+ },
+
+ updateSitePermissions: function () {
+ while (this._permissionList.hasChildNodes())
+ this._permissionList.removeChild(this._permissionList.lastChild);
+
+ let uri = gBrowser.currentURI;
+
+ let permissions = SitePermissions.getPermissionDetailsByURI(uri);
+ if (this._sharingState) {
+ // If WebRTC device or screen permissions are in use, we need to find
+ // the associated permission item to set the inUse field to true.
+ for (let id of ["camera", "microphone", "screen"]) {
+ if (this._sharingState[id]) {
+ let found = false;
+ for (let permission of permissions) {
+ if (permission.id != id)
+ continue;
+ found = true;
+ permission.inUse = true;
+ break;
+ }
+ if (!found) {
+ // If the permission item we were looking for doesn't exist,
+ // the user has temporarily allowed sharing and we need to add
+ // an item in the permissions array to reflect this.
+ let permission = SitePermissions.getPermissionItem(id);
+ permission.inUse = true;
+ permissions.push(permission);
+ }
+ }
+ }
+ }
+ for (let permission of permissions) {
+ let item = this._createPermissionItem(permission);
+ this._permissionList.appendChild(item);
+ }
+
+ this.updatePermissionHint();
+ },
+
+ _handleHeightChange: function(aFunction, aWillShowReloadHint) {
+ let heightBefore = getComputedStyle(this._permissionList).height;
+ aFunction();
+ let heightAfter = getComputedStyle(this._permissionList).height;
+ // Showing the reload hint increases the height, we need to account for it.
+ if (aWillShowReloadHint) {
+ heightAfter = parseInt(heightAfter) +
+ parseInt(getComputedStyle(this._permissionList.nextSibling).height);
+ }
+ let heightChange = parseInt(heightAfter) - parseInt(heightBefore);
+ if (heightChange)
+ this._identityPopupMultiView.setHeightToFit(heightChange);
+ },
+
+ _createPermissionItem: function (aPermission) {
+ let container = document.createElement("hbox");
+ container.setAttribute("class", "identity-popup-permission-item");
+ container.setAttribute("align", "center");
+
+ let img = document.createElement("image");
+ let classes = "identity-popup-permission-icon " + aPermission.id + "-icon";
+ if (aPermission.state == SitePermissions.BLOCK)
+ classes += " blocked-permission-icon";
+ if (aPermission.inUse)
+ classes += " in-use";
+ img.setAttribute("class", classes);
+
+ let nameLabel = document.createElement("label");
+ nameLabel.setAttribute("flex", "1");
+ nameLabel.setAttribute("class", "identity-popup-permission-label");
+ nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id);
+
+ let stateLabel = document.createElement("label");
+ stateLabel.setAttribute("flex", "1");
+ stateLabel.setAttribute("class", "identity-popup-permission-state-label");
+ stateLabel.textContent = SitePermissions.getStateLabel(
+ aPermission.id, aPermission.state, aPermission.inUse || false);
+
+ let button = document.createElement("button");
+ button.setAttribute("class", "identity-popup-permission-remove-button");
+ let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip");
+ button.setAttribute("tooltiptext", tooltiptext);
+ button.addEventListener("command", () => {
+ this._handleHeightChange(() =>
+ this._permissionList.removeChild(container), !this._permissionJustRemoved);
+ if (aPermission.inUse &&
+ ["camera", "microphone", "screen"].includes(aPermission.id)) {
+ let windowId = this._sharingState.windowId;
+ if (aPermission.id == "screen") {
+ windowId = "screen:" + windowId;
+ } else {
+ // If we set persistent permissions or the sharing has
+ // started due to existing persistent permissions, we need
+ // to handle removing these even for frames with different hostnames.
+ let uris = gBrowser.selectedBrowser._devicePermissionURIs || [];
+ for (let uri of uris) {
+ // It's not possible to stop sharing one of camera/microphone
+ // without the other.
+ for (let id of ["camera", "microphone"]) {
+ if (this._sharingState[id] &&
+ SitePermissions.get(uri, id) == SitePermissions.ALLOW)
+ SitePermissions.remove(uri, id);
+ }
+ }
+ }
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.sendAsyncMessage("webrtc:StopSharing", windowId);
+ }
+ SitePermissions.remove(gBrowser.currentURI, aPermission.id);
+ this._permissionJustRemoved = true;
+ this.updatePermissionHint();
+
+ // Set telemetry values for clearing a permission
+ let histogram = Services.telemetry.getKeyedHistogramById("WEB_PERMISSION_CLEARED");
+
+ let permissionType = 0;
+ if (aPermission.state == SitePermissions.ALLOW) {
+ // 1 : clear permanently allowed permission
+ permissionType = 1;
+ } else if (aPermission.state == SitePermissions.BLOCK) {
+ // 2 : clear permanently blocked permission
+ permissionType = 2;
+ }
+ // 3 : TODO clear temporary allowed permission
+ // 4 : TODO clear temporary blocked permission
+
+ histogram.add("(all)", permissionType);
+ histogram.add(aPermission.id, permissionType);
+ });
+
+ container.appendChild(img);
+ container.appendChild(nameLabel);
+ container.appendChild(stateLabel);
+ container.appendChild(button);
+
+ return container;
+ }
+};
+
+function getNotificationBox(aWindow) {
+ var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document);
+ if (foundBrowser)
+ return gBrowser.getNotificationBox(foundBrowser)
+ return null;
+}
+
+function getTabModalPromptBox(aWindow) {
+ var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document);
+ if (foundBrowser)
+ return gBrowser.getTabModalPromptBox(foundBrowser);
+ return null;
+}
+
+/* DEPRECATED */
+function getBrowser() {
+ return gBrowser;
+}
+function getNavToolbox() {
+ return gNavToolbox;
+}
+
+var gPrivateBrowsingUI = {
+ init: function PBUI_init() {
+ // Do nothing for normal windows
+ if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+
+ // Disable the Clear Recent History... menu item when in PB mode
+ // temporary fix until bug 463607 is fixed
+ document.getElementById("Tools:Sanitize").setAttribute("disabled", "true");
+
+ if (window.location.href == getBrowserURL()) {
+ // Adjust the window's title
+ let docElement = document.documentElement;
+ if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ docElement.setAttribute("title",
+ docElement.getAttribute("title_privatebrowsing"));
+ docElement.setAttribute("titlemodifier",
+ docElement.getAttribute("titlemodifier_privatebrowsing"));
+ }
+ docElement.setAttribute("privatebrowsingmode",
+ PrivateBrowsingUtils.permanentPrivateBrowsing ? "permanent" : "temporary");
+ gBrowser.updateTitlebar();
+
+ if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ // Adjust the New Window menu entries
+ [
+ { normal: "menu_newNavigator", private: "menu_newPrivateWindow" },
+ ].forEach(function(menu) {
+ let newWindow = document.getElementById(menu.normal);
+ let newPrivateWindow = document.getElementById(menu.private);
+ if (newWindow && newPrivateWindow) {
+ newPrivateWindow.hidden = true;
+ newWindow.label = newPrivateWindow.label;
+ newWindow.accessKey = newPrivateWindow.accessKey;
+ newWindow.command = newPrivateWindow.command;
+ }
+ });
+ }
+ }
+
+ let urlBarSearchParam = gURLBar.getAttribute("autocompletesearchparam") || "";
+ if (!PrivateBrowsingUtils.permanentPrivateBrowsing &&
+ !urlBarSearchParam.includes("disable-private-actions")) {
+ // Disable switch to tab autocompletion for private windows.
+ // We leave it enabled for permanent private browsing mode though.
+ urlBarSearchParam += " disable-private-actions";
+ }
+ if (!urlBarSearchParam.includes("private-window")) {
+ urlBarSearchParam += " private-window";
+ }
+ gURLBar.setAttribute("autocompletesearchparam", urlBarSearchParam);
+ }
+};
+
+var gRemoteTabsUI = {
+ init: function() {
+ if (window.location.href != getBrowserURL() &&
+ // Also check hidden window for the Mac no-window case
+ window.location.href != "chrome://browser/content/hiddenWindow.xul") {
+ return;
+ }
+
+ if (AppConstants.platform == "macosx" &&
+ Services.prefs.getBoolPref("layers.acceleration.disabled")) {
+ // On OS X, "Disable Hardware Acceleration" also disables OMTC and forces
+ // a fallback to Basic Layers. This is incompatible with e10s.
+ return;
+ }
+
+ let newNonRemoteWindow = document.getElementById("menu_newNonRemoteWindow");
+ let autostart = Services.appinfo.browserTabsRemoteAutostart;
+ newNonRemoteWindow.hidden = !autostart;
+ }
+};
+
+/**
+ * Switch to a tab that has a given URI, and focuses its browser window.
+ * If a matching tab is in this window, it will be switched to. Otherwise, other
+ * windows will be searched.
+ *
+ * @param aURI
+ * URI to search for
+ * @param aOpenNew
+ * True to open a new tab and switch to it, if no existing tab is found.
+ * If no suitable window is found, a new one will be opened.
+ * @param aOpenParams
+ * If switching to this URI results in us opening a tab, aOpenParams
+ * will be the parameter object that gets passed to openUILinkIn. Please
+ * see the documentation for openUILinkIn to see what parameters can be
+ * passed via this object.
+ * This object also allows:
+ * - 'ignoreFragment' property to be set to true to exclude fragment-portion
+ * matching when comparing URIs.
+ * If set to "whenComparing", the fragment will be unmodified.
+ * If set to "whenComparingAndReplace", the fragment will be replaced.
+ * - 'ignoreQueryString' boolean property to be set to true to exclude query string
+ * matching when comparing URIs.
+ * - 'replaceQueryString' boolean property to be set to true to exclude query string
+ * matching when comparing URIs and overwrite the initial query string with
+ * the one from the new URI.
+ * @return True if an existing tab was found, false otherwise
+ */
+function switchToTabHavingURI(aURI, aOpenNew, aOpenParams={}) {
+ // Certain URLs can be switched to irrespective of the source or destination
+ // window being in private browsing mode:
+ const kPrivateBrowsingWhitelist = new Set([
+ "about:addons",
+ ]);
+
+ let ignoreFragment = aOpenParams.ignoreFragment;
+ let ignoreQueryString = aOpenParams.ignoreQueryString;
+ let replaceQueryString = aOpenParams.replaceQueryString;
+
+ // These properties are only used by switchToTabHavingURI and should
+ // not be used as a parameter for the new load.
+ delete aOpenParams.ignoreFragment;
+ delete aOpenParams.ignoreQueryString;
+ delete aOpenParams.replaceQueryString;
+
+ // This will switch to the tab in aWindow having aURI, if present.
+ function switchIfURIInWindow(aWindow) {
+ // Only switch to the tab if neither the source nor the destination window
+ // are private and they are not in permanent private browsing mode
+ if (!kPrivateBrowsingWhitelist.has(aURI.spec) &&
+ (PrivateBrowsingUtils.isWindowPrivate(window) ||
+ PrivateBrowsingUtils.isWindowPrivate(aWindow)) &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ return false;
+ }
+
+ // Remove the query string, fragment, both, or neither from a given url.
+ function cleanURL(url, removeQuery, removeFragment) {
+ let ret = url;
+ if (removeFragment) {
+ ret = ret.split("#")[0];
+ if (removeQuery) {
+ // This removes a query, if present before the fragment.
+ ret = ret.split("?")[0];
+ }
+ } else if (removeQuery) {
+ // This is needed in case there is a fragment after the query.
+ let fragment = ret.split("#")[1];
+ ret = ret.split("?")[0].concat(
+ (fragment != undefined) ? "#".concat(fragment) : "");
+ }
+ return ret;
+ }
+
+ // Need to handle nsSimpleURIs here too (e.g. about:...), which don't
+ // work correctly with URL objects - so treat them as strings
+ let ignoreFragmentWhenComparing = typeof ignoreFragment == "string" &&
+ ignoreFragment.startsWith("whenComparing");
+ let requestedCompare = cleanURL(
+ aURI.spec, ignoreQueryString || replaceQueryString, ignoreFragmentWhenComparing);
+ let browsers = aWindow.gBrowser.browsers;
+ for (let i = 0; i < browsers.length; i++) {
+ let browser = browsers[i];
+ let browserCompare = cleanURL(
+ browser.currentURI.spec, ignoreQueryString || replaceQueryString, ignoreFragmentWhenComparing);
+ if (requestedCompare == browserCompare) {
+ aWindow.focus();
+ if (ignoreFragment == "whenComparingAndReplace" || replaceQueryString) {
+ browser.loadURI(aURI.spec);
+ }
+ aWindow.gBrowser.tabContainer.selectedIndex = i;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // This can be passed either nsIURI or a string.
+ if (!(aURI instanceof Ci.nsIURI))
+ aURI = Services.io.newURI(aURI, null, null);
+
+ let isBrowserWindow = !!window.gBrowser;
+
+ // Prioritise this window.
+ if (isBrowserWindow && switchIfURIInWindow(window))
+ return true;
+
+ for (let browserWin of browserWindows()) {
+ // Skip closed (but not yet destroyed) windows,
+ // and the current window (which was checked earlier).
+ if (browserWin.closed || browserWin == window)
+ continue;
+ if (switchIfURIInWindow(browserWin))
+ return true;
+ }
+
+ // No opened tab has that url.
+ if (aOpenNew) {
+ if (isBrowserWindow && isTabEmpty(gBrowser.selectedTab))
+ openUILinkIn(aURI.spec, "current", aOpenParams);
+ else
+ openUILinkIn(aURI.spec, "tab", aOpenParams);
+ }
+
+ return false;
+}
+
+var RestoreLastSessionObserver = {
+ init: function () {
+ if (SessionStore.canRestoreLastSession &&
+ !PrivateBrowsingUtils.isWindowPrivate(window)) {
+ Services.obs.addObserver(this, "sessionstore-last-session-cleared", true);
+ goSetCommandEnabled("Browser:RestoreLastSession", true);
+ }
+ },
+
+ observe: function () {
+ // The last session can only be restored once so there's
+ // no way we need to re-enable our menu item.
+ Services.obs.removeObserver(this, "sessionstore-last-session-cleared");
+ goSetCommandEnabled("Browser:RestoreLastSession", false);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference])
+};
+
+function restoreLastSession() {
+ SessionStore.restoreLastSession();
+}
+
+var TabContextMenu = {
+ contextTab: null,
+ _updateToggleMuteMenuItem(aTab, aConditionFn) {
+ ["muted", "soundplaying"].forEach(attr => {
+ if (!aConditionFn || aConditionFn(attr)) {
+ if (aTab.hasAttribute(attr)) {
+ aTab.toggleMuteMenuItem.setAttribute(attr, "true");
+ } else {
+ aTab.toggleMuteMenuItem.removeAttribute(attr);
+ }
+ }
+ });
+ },
+ updateContextMenu: function updateContextMenu(aPopupMenu) {
+ this.contextTab = aPopupMenu.triggerNode.localName == "tab" ?
+ aPopupMenu.triggerNode : gBrowser.selectedTab;
+ let disabled = gBrowser.tabs.length == 1;
+
+ var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple");
+ for (let menuItem of menuItems)
+ menuItem.disabled = disabled;
+
+ if (AppConstants.E10S_TESTING_ONLY) {
+ menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-remote");
+ for (let menuItem of menuItems) {
+ menuItem.hidden = !gMultiProcessBrowser;
+ if (menuItem.id == "context_openNonRemoteWindow") {
+ menuItem.disabled = !!parseInt(this.contextTab.getAttribute("usercontextid"));
+ }
+ }
+ }
+
+ disabled = gBrowser.visibleTabs.length == 1;
+ menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple-visible");
+ for (let menuItem of menuItems)
+ menuItem.disabled = disabled;
+
+ // Session store
+ document.getElementById("context_undoCloseTab").disabled =
+ SessionStore.getClosedTabCount(window) == 0;
+
+ // Only one of pin/unpin should be visible
+ document.getElementById("context_pinTab").hidden = this.contextTab.pinned;
+ document.getElementById("context_unpinTab").hidden = !this.contextTab.pinned;
+
+ // Disable "Close Tabs to the Right" if there are no tabs
+ // following it and hide it when the user rightclicked on a pinned
+ // tab.
+ document.getElementById("context_closeTabsToTheEnd").disabled =
+ gBrowser.getTabsToTheEndFrom(this.contextTab).length == 0;
+ document.getElementById("context_closeTabsToTheEnd").hidden = this.contextTab.pinned;
+
+ // Disable "Close other Tabs" if there is only one unpinned tab and
+ // hide it when the user rightclicked on a pinned tab.
+ let unpinnedTabs = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs;
+ document.getElementById("context_closeOtherTabs").disabled = unpinnedTabs <= 1;
+ document.getElementById("context_closeOtherTabs").hidden = this.contextTab.pinned;
+
+ // Hide "Bookmark All Tabs" for a pinned tab. Update its state if visible.
+ let bookmarkAllTabs = document.getElementById("context_bookmarkAllTabs");
+ bookmarkAllTabs.hidden = this.contextTab.pinned;
+ if (!bookmarkAllTabs.hidden)
+ PlacesCommandHook.updateBookmarkAllTabsCommand();
+
+ // Adjust the state of the toggle mute menu item.
+ let toggleMute = document.getElementById("context_toggleMuteTab");
+ if (this.contextTab.hasAttribute("muted")) {
+ toggleMute.label = gNavigatorBundle.getString("unmuteTab.label");
+ toggleMute.accessKey = gNavigatorBundle.getString("unmuteTab.accesskey");
+ } else {
+ toggleMute.label = gNavigatorBundle.getString("muteTab.label");
+ toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey");
+ }
+
+ this.contextTab.toggleMuteMenuItem = toggleMute;
+ this._updateToggleMuteMenuItem(this.contextTab);
+
+ this.contextTab.addEventListener("TabAttrModified", this, false);
+ aPopupMenu.addEventListener("popuphiding", this, false);
+
+ gFxAccounts.updateTabContextMenu(aPopupMenu);
+ },
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "popuphiding":
+ gBrowser.removeEventListener("TabAttrModified", this);
+ aEvent.target.removeEventListener("popuphiding", this);
+ break;
+ case "TabAttrModified":
+ let tab = aEvent.target;
+ this._updateToggleMuteMenuItem(tab,
+ attr => aEvent.detail.changed.indexOf(attr) >= 0);
+ break;
+ }
+ }
+};
+
+Object.defineProperty(this, "HUDService", {
+ get: function HUDService_getter() {
+ let devtools = Cu.import("resource://devtools/shared/Loader.jsm", {}).devtools;
+ return devtools.require("devtools/client/webconsole/hudservice");
+ },
+ configurable: true,
+ enumerable: true
+});
+
+// Prompt user to restart the browser in safe mode
+function safeModeRestart() {
+ if (Services.appinfo.inSafeMode) {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+ if (cancelQuit.data)
+ return;
+
+ Services.startup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+ return;
+ }
+
+ Services.obs.notifyObservers(null, "restart-in-safe-mode", "");
+}
+
+/* duplicateTabIn duplicates tab in a place specified by the parameter |where|.
+ *
+ * |where| can be:
+ * "tab" new tab
+ * "tabshifted" same as "tab" but in background if default is to select new
+ * tabs, and vice versa
+ * "window" new window
+ *
+ * delta is the offset to the history entry that you want to load.
+ */
+function duplicateTabIn(aTab, where, delta) {
+ switch (where) {
+ case "window":
+ let otherWin = OpenBrowserWindow();
+ let delayedStartupFinished = (subject, topic) => {
+ if (topic == "browser-delayed-startup-finished" &&
+ subject == otherWin) {
+ Services.obs.removeObserver(delayedStartupFinished, topic);
+ let otherGBrowser = otherWin.gBrowser;
+ let otherTab = otherGBrowser.selectedTab;
+ SessionStore.duplicateTab(otherWin, aTab, delta);
+ otherGBrowser.removeTab(otherTab, { animate: false });
+ }
+ };
+
+ Services.obs.addObserver(delayedStartupFinished,
+ "browser-delayed-startup-finished",
+ false);
+ break;
+ case "tabshifted":
+ SessionStore.duplicateTab(window, aTab, delta);
+ // A background tab has been opened, nothing else to do here.
+ break;
+ case "tab":
+ let newTab = SessionStore.duplicateTab(window, aTab, delta);
+ gBrowser.selectedTab = newTab;
+ break;
+ }
+}
+
+var Scratchpad = {
+ openScratchpad: function SP_openScratchpad() {
+ return this.ScratchpadManager.openScratchpad();
+ }
+};
+
+XPCOMUtils.defineLazyGetter(Scratchpad, "ScratchpadManager", function() {
+ let tmp = {};
+ Cu.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm", tmp);
+ return tmp.ScratchpadManager;
+});
+
+var ResponsiveUI = {
+ toggle: function RUI_toggle() {
+ this.ResponsiveUIManager.toggle(window, gBrowser.selectedTab);
+ }
+};
+
+XPCOMUtils.defineLazyGetter(ResponsiveUI, "ResponsiveUIManager", function() {
+ let tmp = {};
+ Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", tmp);
+ return tmp.ResponsiveUIManager;
+});
+
+var MousePosTracker = {
+ _listeners: new Set(),
+ _x: 0,
+ _y: 0,
+ get _windowUtils() {
+ delete this._windowUtils;
+ return this._windowUtils = window.getInterface(Ci.nsIDOMWindowUtils);
+ },
+
+ addListener: function (listener) {
+ if (this._listeners.has(listener))
+ return;
+
+ listener._hover = false;
+ this._listeners.add(listener);
+
+ this._callListener(listener);
+ },
+
+ removeListener: function (listener) {
+ this._listeners.delete(listener);
+ },
+
+ handleEvent: function (event) {
+ var fullZoom = this._windowUtils.fullZoom;
+ this._x = event.screenX / fullZoom - window.mozInnerScreenX;
+ this._y = event.screenY / fullZoom - window.mozInnerScreenY;
+
+ this._listeners.forEach(function (listener) {
+ try {
+ this._callListener(listener);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }, this);
+ },
+
+ _callListener: function (listener) {
+ let rect = listener.getMouseTargetRect();
+ let hover = this._x >= rect.left &&
+ this._x <= rect.right &&
+ this._y >= rect.top &&
+ this._y <= rect.bottom;
+
+ if (hover == listener._hover)
+ return;
+
+ listener._hover = hover;
+
+ if (hover) {
+ if (listener.onMouseEnter)
+ listener.onMouseEnter();
+ } else if (listener.onMouseLeave) {
+ listener.onMouseLeave();
+ }
+ }
+};
+
+var ToolbarIconColor = {
+ init: function () {
+ this._initialized = true;
+
+ window.addEventListener("activate", this);
+ window.addEventListener("deactivate", this);
+ Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
+
+ // If the window isn't active now, we assume that it has never been active
+ // before and will soon become active such that inferFromText will be
+ // called from the initial activate event.
+ if (Services.focus.activeWindow == window)
+ this.inferFromText();
+ },
+
+ uninit: function () {
+ this._initialized = false;
+
+ window.removeEventListener("activate", this);
+ window.removeEventListener("deactivate", this);
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update");
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "activate":
+ case "deactivate":
+ this.inferFromText();
+ break;
+ }
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "lightweight-theme-styling-update":
+ // inferFromText needs to run after LightweightThemeConsumer.jsm's
+ // lightweight-theme-styling-update observer.
+ setTimeout(() => { this.inferFromText(); }, 0);
+ break;
+ }
+ },
+
+ inferFromText: function () {
+ if (!this._initialized)
+ return;
+
+ function parseRGB(aColorString) {
+ let rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/);
+ rgb.shift();
+ return rgb.map(x => parseInt(x));
+ }
+
+ let toolbarSelector = "#navigator-toolbox > toolbar:not([collapsed=true]):not(#addon-bar)";
+ if (AppConstants.platform == "macosx")
+ toolbarSelector += ":not([type=menubar])";
+
+ // The getComputedStyle calls and setting the brighttext are separated in
+ // two loops to avoid flushing layout and making it dirty repeatedly.
+
+ let luminances = new Map;
+ for (let toolbar of document.querySelectorAll(toolbarSelector)) {
+ let [r, g, b] = parseRGB(getComputedStyle(toolbar).color);
+ let luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
+ luminances.set(toolbar, luminance);
+ }
+
+ for (let [toolbar, luminance] of luminances) {
+ if (luminance <= 110)
+ toolbar.removeAttribute("brighttext");
+ else
+ toolbar.setAttribute("brighttext", "true");
+ }
+ }
+}
+
+var PanicButtonNotifier = {
+ init: function() {
+ this._initialized = true;
+ if (window.PanicButtonNotifierShouldNotify) {
+ delete window.PanicButtonNotifierShouldNotify;
+ this.notify();
+ }
+ },
+ notify: function() {
+ if (!this._initialized) {
+ window.PanicButtonNotifierShouldNotify = true;
+ return;
+ }
+ // Display notification panel here...
+ try {
+ let popup = document.getElementById("panic-button-success-notification");
+ popup.hidden = false;
+ let widget = CustomizableUI.getWidget("panic-button").forWindow(window);
+ let anchor = widget.anchor;
+ anchor = document.getAnonymousElementByAttribute(anchor, "class", "toolbarbutton-icon");
+ popup.openPopup(anchor, popup.getAttribute("position"));
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+ close: function() {
+ let popup = document.getElementById("panic-button-success-notification");
+ popup.hidePopup();
+ },
+};
+
+var AboutPrivateBrowsingListener = {
+ init: function () {
+ window.messageManager.addMessageListener(
+ "AboutPrivateBrowsing:OpenPrivateWindow",
+ msg => {
+ OpenBrowserWindow({private: true});
+ });
+ window.messageManager.addMessageListener(
+ "AboutPrivateBrowsing:ToggleTrackingProtection",
+ msg => {
+ const PREF = "privacy.trackingprotection.pbmode.enabled";
+ Services.prefs.setBoolPref(PREF, !Services.prefs.getBoolPref(PREF));
+ });
+ window.messageManager.addMessageListener(
+ "AboutPrivateBrowsing:DontShowIntroPanelAgain",
+ msg => {
+ TrackingProtection.dontShowIntroPanelAgain();
+ });
+ }
+};
+
+function TabModalPromptBox(browser) {
+ this._weakBrowserRef = Cu.getWeakReference(browser);
+}
+
+TabModalPromptBox.prototype = {
+ _promptCloseCallback(onCloseCallback, principalToAllowFocusFor, allowFocusCheckbox, ...args) {
+ if (principalToAllowFocusFor && allowFocusCheckbox &&
+ allowFocusCheckbox.checked) {
+ Services.perms.addFromPrincipal(principalToAllowFocusFor, "focus-tab-by-prompt",
+ Services.perms.ALLOW_ACTION);
+ }
+ onCloseCallback.apply(this, args);
+ },
+
+ appendPrompt(args, onCloseCallback) {
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let newPrompt = document.createElementNS(XUL_NS, "tabmodalprompt");
+ let browser = this.browser;
+ browser.parentNode.insertBefore(newPrompt, browser.nextSibling);
+ browser.setAttribute("tabmodalPromptShowing", true);
+
+ newPrompt.clientTop; // style flush to assure binding is attached
+
+ let prompts = this.listPrompts();
+ if (prompts.length > 1) {
+ // Let's hide ourself behind the current prompt.
+ newPrompt.hidden = true;
+ }
+
+ let principalToAllowFocusFor = this._allowTabFocusByPromptPrincipal;
+ delete this._allowTabFocusByPromptPrincipal;
+
+ let allowFocusCheckbox; // Define outside the if block so we can bind it into the callback.
+ let hostForAllowFocusCheckbox = "";
+ try {
+ hostForAllowFocusCheckbox = principalToAllowFocusFor.URI.host;
+ } catch (ex) { /* Ignore exceptions for host-less URIs */ }
+ if (hostForAllowFocusCheckbox) {
+ let allowFocusRow = document.createElementNS(XUL_NS, "row");
+ allowFocusCheckbox = document.createElementNS(XUL_NS, "checkbox");
+ let spacer = document.createElementNS(XUL_NS, "spacer");
+ allowFocusRow.appendChild(spacer);
+ let label = gBrowser.mStringBundle.getFormattedString("tabs.allowTabFocusByPromptForSite",
+ [hostForAllowFocusCheckbox]);
+ allowFocusCheckbox.setAttribute("label", label);
+ allowFocusRow.appendChild(allowFocusCheckbox);
+ newPrompt.appendChild(allowFocusRow);
+ }
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ let closeCB = this._promptCloseCallback.bind(null, onCloseCallback, principalToAllowFocusFor,
+ allowFocusCheckbox);
+ newPrompt.init(args, tab, closeCB);
+ return newPrompt;
+ },
+
+ removePrompt(aPrompt) {
+ let browser = this.browser;
+ browser.parentNode.removeChild(aPrompt);
+
+ let prompts = this.listPrompts();
+ if (prompts.length) {
+ let prompt = prompts[prompts.length - 1];
+ prompt.hidden = false;
+ prompt.Dialog.setDefaultFocus();
+ } else {
+ browser.removeAttribute("tabmodalPromptShowing");
+ browser.focus();
+ }
+ },
+
+ listPrompts(aPrompt) {
+ // Get the nodelist, then return as an array
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let els = this.browser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
+ return Array.from(els);
+ },
+
+ onNextPromptShowAllowFocusCheckboxFor(principal) {
+ this._allowTabFocusByPromptPrincipal = principal;
+ },
+
+ get browser() {
+ let browser = this._weakBrowserRef.get();
+ if (!browser) {
+ throw "Stale promptbox! The associated browser is gone.";
+ }
+ return browser;
+ },
+};
diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul
new file mode 100644
index 000000000..2c74aecdf
--- /dev/null
+++ b/browser/base/content/browser.xul
@@ -0,0 +1,1134 @@
+#filter substitution
+<?xml version="1.0"?>
+# -*- Mode: HTML -*-
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/usercontext/usercontext.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/devtools-browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/controlcenter/panel.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/customizableui/panelUI.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/browser-lightweightTheme.css" type="text/css"?>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+# All DTD information is stored in a separate file so that it can be shared by
+# hiddenWindow.xul.
+#include browser-doctype.inc
+
+<window id="main-window"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="gBrowserInit.onLoad()" onunload="gBrowserInit.onUnload()" onclose="return WindowIsClosing();"
+ title="&mainWindow.title;"
+ title_normal="&mainWindow.title;"
+#ifdef XP_MACOSX
+ title_privatebrowsing="&mainWindow.title;&mainWindow.titlemodifiermenuseparator;&mainWindow.titlePrivateBrowsingSuffix;"
+ titledefault="&mainWindow.title;"
+ titlemodifier=""
+ titlemodifier_normal=""
+ titlemodifier_privatebrowsing="&mainWindow.titlePrivateBrowsingSuffix;"
+#else
+ title_privatebrowsing="&mainWindow.titlemodifier; &mainWindow.titlePrivateBrowsingSuffix;"
+ titlemodifier="&mainWindow.titlemodifier;"
+ titlemodifier_normal="&mainWindow.titlemodifier;"
+ titlemodifier_privatebrowsing="&mainWindow.titlemodifier; &mainWindow.titlePrivateBrowsingSuffix;"
+#endif
+#ifdef CAN_DRAW_IN_TITLEBAR
+#ifdef XP_WIN
+ chromemargin="0,2,2,2"
+#else
+ chromemargin="0,-1,-1,-1"
+#endif
+ tabsintitlebar="true"
+#endif
+ titlemenuseparator="&mainWindow.titlemodifiermenuseparator;"
+ lightweightthemes="true"
+ lightweightthemesfooter="browser-bottombox"
+ windowtype="navigator:browser"
+ macanimationtype="document"
+ screenX="4" screenY="4"
+ fullscreenbutton="true"
+ sizemode="normal"
+ retargetdocumentfocus="urlbar"
+ persist="screenX screenY width height sizemode">
+
+# All JS files which are not content (only) dependent that browser.xul
+# wishes to include *must* go into the global-scripts.inc file
+# so that they can be shared by macBrowserOverlay.xul.
+#include global-scripts.inc
+<script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
+
+<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+
+<script type="application/javascript" src="chrome://browser/content/downloads/downloads.js"/>
+<script type="application/javascript" src="chrome://browser/content/downloads/indicator.js"/>
+<script type="application/javascript" src="chrome://browser/content/places/editBookmarkOverlay.js"/>
+
+# All sets except for popupsets (commands, keys, stringbundles and broadcasters) *must* go into the
+# browser-sets.inc file for sharing with hiddenWindow.xul.
+#define FULL_BROWSER_WINDOW
+#include browser-sets.inc
+#undef FULL_BROWSER_WINDOW
+
+ <popupset id="mainPopupSet">
+ <menupopup id="tabContextMenu"
+ onpopupshowing="if (event.target == this) TabContextMenu.updateContextMenu(this);"
+ onpopuphidden="if (event.target == this) TabContextMenu.contextTab = null;">
+ <menuitem id="context_reloadTab" label="&reloadTab.label;" accesskey="&reloadTab.accesskey;"
+ oncommand="gBrowser.reloadTab(TabContextMenu.contextTab);"/>
+ <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
+ <menuseparator/>
+ <menuitem id="context_pinTab" label="&pinTab.label;"
+ accesskey="&pinTab.accesskey;"
+ oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>
+ <menuitem id="context_unpinTab" label="&unpinTab.label;" hidden="true"
+ accesskey="&unpinTab.accesskey;"
+ oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/>
+ <menuitem id="context_openTabInWindow" label="&moveToNewWindow.label;"
+ accesskey="&moveToNewWindow.accesskey;"
+ tbattr="tabbrowser-multiple"
+ oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
+#ifdef E10S_TESTING_ONLY
+ <menuitem id="context_openNonRemoteWindow" label="Open in new non-e10s window"
+ tbattr="tabbrowser-remote"
+ hidden="true"
+ oncommand="gBrowser.openNonRemoteWindow(TabContextMenu.contextTab);"/>
+#endif
+ <menuseparator id="context_sendTabToDevice_separator" hidden="true"/>
+ <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
+ accesskey="&sendTabToDevice.accesskey;" hidden="true">
+ <menupopup id="context_sendTabToDevicePopupMenu"
+ onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
+ </menu>
+ <menuseparator/>
+ <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
+ tbattr="tabbrowser-multiple-visible"
+ oncommand="gBrowser.reloadAllTabs();"/>
+ <menuitem id="context_bookmarkAllTabs"
+ label="&bookmarkAllTabs.label;"
+ accesskey="&bookmarkAllTabs.accesskey;"
+ command="Browser:BookmarkAllTabs"/>
+ <menuitem id="context_closeTabsToTheEnd" label="&closeTabsToTheEnd.label;" accesskey="&closeTabsToTheEnd.accesskey;"
+ oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab, {animate: true});"/>
+ <menuitem id="context_closeOtherTabs" label="&closeOtherTabs.label;" accesskey="&closeOtherTabs.accesskey;"
+ oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/>
+ <menuseparator/>
+ <menuitem id="context_undoCloseTab"
+ label="&undoCloseTab.label;"
+ accesskey="&undoCloseTab.accesskey;"
+ observes="History:UndoCloseTab"/>
+ <menuitem id="context_closeTab" label="&closeTab.label;" accesskey="&closeTab.accesskey;"
+ oncommand="gBrowser.removeTab(TabContextMenu.contextTab, { animate: true });"/>
+ </menupopup>
+
+ <!-- bug 415444/582485: event.stopPropagation is here for the cloned version
+ of this menupopup -->
+ <menupopup id="backForwardMenu"
+ onpopupshowing="return FillHistoryMenu(event.target);"
+ oncommand="gotoHistoryIndex(event); event.stopPropagation();"
+ onclick="checkForMiddleClick(this, event);"/>
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <tooltip id="remoteBrowserTooltip"/>
+
+ <!-- for search and content formfill/pw manager -->
+
+ <panel type="autocomplete-richlistbox"
+ id="PopupAutoComplete"
+ noautofocus="true"
+ hidden="true"
+ overflowpadding="4"
+ norolluponanchor="true" />
+
+ <!-- for search with one-off buttons -->
+ <panel type="autocomplete" id="PopupSearchAutoComplete" noautofocus="true" hidden="true"/>
+
+ <!-- for url bar autocomplete -->
+ <panel type="autocomplete-richlistbox"
+ id="PopupAutoCompleteRichResult"
+ noautofocus="true"
+ hidden="true"
+ flip="none"
+ level="parent"
+ overflowpadding="30" />
+
+ <panel id="DateTimePickerPanel"
+ type="arrow"
+ hidden="true"
+ orient="vertical"
+ noautofocus="true"
+ consumeoutsideclicks="false"
+ level="parent">
+ <iframe id="dateTimePopupFrame"/>
+ </panel>
+
+ <!-- for select dropdowns. The menupopup is what shows the list of options,
+ and the popuponly menulist makes things like the menuactive attributes
+ work correctly on the menupopup. ContentSelectDropdown expects the
+ popuponly menulist to be its immediate parent. -->
+ <menulist popuponly="true" id="ContentSelectDropdown" hidden="true">
+ <menupopup rolluponmousewheel="true"
+ activateontab="true" position="after_start"
+#ifdef XP_WIN
+ consumeoutsideclicks="false" ignorekeys="shortcuts"
+#endif
+ />
+ </menulist>
+
+ <!-- for invalid form error message -->
+ <panel id="invalid-form-popup" type="arrow" orient="vertical" noautofocus="true" hidden="true" level="parent">
+ <description/>
+ </panel>
+
+ <panel id="editBookmarkPanel"
+ type="arrow"
+ orient="vertical"
+ ignorekeys="true"
+ hidden="true"
+ tabspecific="true"
+ onpopupshown="StarUI.panelShown(event);"
+ aria-labelledby="editBookmarkPanelTitle">
+ <row id="editBookmarkPanelHeader" align="center" hidden="true">
+ <vbox align="center">
+ <image id="editBookmarkPanelStarIcon"/>
+ </vbox>
+ <vbox>
+ <label id="editBookmarkPanelTitle"/>
+ <description id="editBookmarkPanelDescription"/>
+ </vbox>
+ </row>
+ <vbox id="editBookmarkPanelContent" flex="1" hidden="true"/>
+ <hbox id="editBookmarkPanelBottomButtons" pack="end">
+#ifndef XP_UNIX
+ <button id="editBookmarkPanelDoneButton"
+ class="editBookmarkPanelBottomButton"
+ label="&editBookmark.done.label;"
+ default="true"
+ oncommand="StarUI.panel.hidePopup();"/>
+ <button id="editBookmarkPanelRemoveButton"
+ class="editBookmarkPanelBottomButton"
+ oncommand="StarUI.removeBookmarkButtonCommand();"
+ accesskey="&editBookmark.removeBookmark.accessKey;"/>
+#else
+ <button id="editBookmarkPanelRemoveButton"
+ class="editBookmarkPanelBottomButton"
+ oncommand="StarUI.removeBookmarkButtonCommand();"
+ accesskey="&editBookmark.removeBookmark.accessKey;"/>
+ <button id="editBookmarkPanelDoneButton"
+ class="editBookmarkPanelBottomButton"
+ label="&editBookmark.done.label;"
+ default="true"
+ oncommand="StarUI.panel.hidePopup();"/>
+#endif
+ </hbox>
+ </panel>
+
+ <!-- UI tour experience -->
+ <panel id="UITourTooltip"
+ type="arrow"
+ hidden="true"
+ noautofocus="true"
+ noautohide="true"
+ align="start"
+ orient="vertical"
+ role="alert">
+ <vbox>
+ <hbox id="UITourTooltipBody">
+ <image id="UITourTooltipIcon"/>
+ <vbox flex="1">
+ <hbox id="UITourTooltipTitleContainer">
+ <label id="UITourTooltipTitle" flex="1"/>
+ <toolbarbutton id="UITourTooltipClose" class="close-icon"
+ tooltiptext="&uiTour.infoPanel.close;"/>
+ </hbox>
+ <description id="UITourTooltipDescription" flex="1"/>
+ </vbox>
+ </hbox>
+ <hbox id="UITourTooltipButtons" flex="1" align="center"/>
+ </vbox>
+ </panel>
+ <!-- type="default" forces frames to be created so that the panel's size can be determined -->
+ <panel id="UITourHighlightContainer"
+ type="default"
+ hidden="true"
+ noautofocus="true"
+ noautohide="true"
+ flip="none"
+ consumeoutsideclicks="false"
+ mousethrough="always">
+ <box id="UITourHighlight"></box>
+ </panel>
+
+ <panel id="social-share-panel"
+ class="social-panel"
+ type="arrow"
+ orient="vertical"
+ onpopupshowing="SocialShare.onShowing()"
+ onpopuphidden="SocialShare.onHidden()"
+ hidden="true">
+ <hbox class="social-share-toolbar">
+ <toolbarbutton id="manage-share-providers" class="share-provider-button"
+ tooltiptext="&social.addons.label;"
+ oncommand="BrowserOpenAddonsMgr('addons://list/service');
+ this.parentNode.parentNode.hidePopup();"/>
+ <arrowscrollbox id="social-share-provider-buttons" orient="horizontal" flex="1" pack="end">
+ <toolbarbutton id="add-share-provider" class="share-provider-button" type="radio"
+ group="share-providers" tooltiptext="&findShareServices.label;"
+ oncommand="SocialShare.showDirectory()"/>
+ </arrowscrollbox>
+ </hbox>
+ <hbox id="share-container" flex="1"/>
+ </panel>
+
+ <menupopup id="toolbar-context-menu"
+ onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator'));">
+ <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
+ accesskey="&customizeMenu.moveToPanel.accesskey;"
+ label="&customizeMenu.moveToPanel.label;"
+ contexttype="toolbaritem"
+ class="customize-context-moveToPanel"/>
+ <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)"
+ accesskey="&customizeMenu.removeFromToolbar.accesskey;"
+ label="&customizeMenu.removeFromToolbar.label;"
+ contexttype="toolbaritem"
+ class="customize-context-removeFromToolbar"/>
+ <menuitem id="toolbar-context-reloadAllTabs"
+ class="toolbaritem-tabsmenu"
+ contexttype="tabbar"
+ oncommand="gBrowser.reloadAllTabs();"
+ label="&toolbarContextMenu.reloadAllTabs.label;"
+ accesskey="&toolbarContextMenu.reloadAllTabs.accesskey;"/>
+ <menuitem id="toolbar-context-bookmarkAllTabs"
+ class="toolbaritem-tabsmenu"
+ contexttype="tabbar"
+ command="Browser:BookmarkAllTabs"
+ label="&toolbarContextMenu.bookmarkAllTabs.label;"
+ accesskey="&toolbarContextMenu.bookmarkAllTabs.accesskey;"/>
+ <menuitem id="toolbar-context-undoCloseTab"
+ class="toolbaritem-tabsmenu"
+ contexttype="tabbar"
+ label="&toolbarContextMenu.undoCloseTab.label;"
+ accesskey="&toolbarContextMenu.undoCloseTab.accesskey;"
+ observes="History:UndoCloseTab"/>
+ <menuseparator/>
+ <menuseparator id="viewToolbarsMenuSeparator"/>
+ <!-- XXXgijs: we're using oncommand handler here to avoid the event being
+ redirected to the command element, thus preventing
+ listeners on the menupopup or further up the tree from
+ seeing the command event pass by. The observes attribute is
+ here so that the menuitem is still disabled and re-enabled
+ correctly. -->
+ <menuitem oncommand="BrowserCustomizeToolbar()"
+ observes="cmd_CustomizeToolbars"
+ class="viewCustomizeToolbar"
+ label="&viewCustomizeToolbar.label;"
+ accesskey="&viewCustomizeToolbar.accesskey;"/>
+ </menupopup>
+
+ <menupopup id="blockedPopupOptions"
+ onpopupshowing="gPopupBlockerObserver.fillPopupList(event);"
+ onpopuphiding="gPopupBlockerObserver.onPopupHiding(event);">
+ <menuitem observes="blockedPopupAllowSite"/>
+ <menuitem observes="blockedPopupEditSettings"/>
+ <menuitem observes="blockedPopupDontShowMessage"/>
+ <menuseparator observes="blockedPopupsSeparator"/>
+ </menupopup>
+
+ <menupopup id="autohide-context"
+ onpopupshowing="FullScreen.getAutohide(this.firstChild);">
+ <menuitem type="checkbox" label="&fullScreenAutohide.label;"
+ accesskey="&fullScreenAutohide.accesskey;"
+ oncommand="FullScreen.setAutohide();"/>
+ <menuseparator/>
+ <menuitem label="&fullScreenExit.label;"
+ accesskey="&fullScreenExit.accesskey;"
+ oncommand="BrowserFullScreen();"/>
+ </menupopup>
+
+ <menupopup id="contentAreaContextMenu" pagemenu="#page-menu-separator"
+ onpopupshowing="if (event.target != this)
+ return true;
+ gContextMenu = new nsContextMenu(this, event.shiftKey);
+ if (gContextMenu.shouldDisplay)
+ updateEditUIVisibility();
+ return gContextMenu.shouldDisplay;"
+ onpopuphiding="if (event.target != this)
+ return;
+ gContextMenu.hiding();
+ gContextMenu = null;
+ updateEditUIVisibility();">
+#include browser-context.inc
+ </menupopup>
+
+ <menupopup id="placesContext">
+ <menuseparator id="placesContext_recentlyBookmarkedSeparator"
+ ignoreitem="true"
+ hidden="true"/>
+ <menuitem id="placesContext_hideRecentlyBookmarked"
+ label="&hideRecentlyBookmarked.label;"
+ accesskey="&hideRecentlyBookmarked.accesskey;"
+ oncommand="BookmarkingUI.hideRecentlyBookmarked();"
+ closemenu="single"
+ ignoreitem="true"
+ hidden="true"/>
+ <menuitem id="placesContext_showRecentlyBookmarked"
+ label="&showRecentlyBookmarked.label;"
+ accesskey="&showRecentlyBookmarked.accesskey;"
+ oncommand="BookmarkingUI.showRecentlyBookmarked();"
+ closemenu="single"
+ ignoreitem="true"
+ hidden="true"/>
+ </menupopup>
+
+ <panel id="ctrlTab-panel" hidden="true" norestorefocus="true" level="top">
+ <hbox>
+ <button class="ctrlTab-preview" flex="1"/>
+ <button class="ctrlTab-preview" flex="1"/>
+ <button class="ctrlTab-preview" flex="1"/>
+ <button class="ctrlTab-preview" flex="1"/>
+ <button class="ctrlTab-preview" flex="1"/>
+ <button class="ctrlTab-preview" flex="1"/>
+ </hbox>
+ <hbox pack="center">
+ <button id="ctrlTab-showAll" class="ctrlTab-preview" noicon="true"/>
+ </hbox>
+ </panel>
+
+ <!-- Bookmarks and history tooltip -->
+ <tooltip id="bhTooltip"/>
+
+ <tooltip id="tabbrowser-tab-tooltip" onpopupshowing="gBrowser.createTooltip(event);"/>
+
+ <tooltip id="back-button-tooltip">
+ <label class="tooltip-label" value="&backButton.tooltip;"/>
+#ifdef XP_MACOSX
+ <label class="tooltip-label" value="&backForwardButtonMenuMac.tooltip;"/>
+#else
+ <label class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/>
+#endif
+ </tooltip>
+
+ <tooltip id="forward-button-tooltip">
+ <label class="tooltip-label" value="&forwardButton.tooltip;"/>
+#ifdef XP_MACOSX
+ <label class="tooltip-label" value="&backForwardButtonMenuMac.tooltip;"/>
+#else
+ <label class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/>
+#endif
+ </tooltip>
+
+ <tooltip id="share-button-tooltip" onpopupshowing="SocialShare.createTooltip(event);">
+ <label class="tooltip-label"/>
+ <label class="tooltip-label"/>
+ </tooltip>
+
+#include popup-notifications.inc
+
+#include ../../components/customizableui/content/panelUI.inc.xul
+#include ../../components/controlcenter/content/panel.inc.xul
+
+ <hbox id="downloads-animation-container" mousethrough="always">
+ <vbox id="downloads-notification-anchor">
+ <vbox id="downloads-indicator-notification"/>
+ </vbox>
+ </hbox>
+
+ <hbox id="bookmarked-notification-container" mousethrough="always">
+ <vbox id="bookmarked-notification-anchor">
+ <vbox id="bookmarked-notification"/>
+ </vbox>
+ <vbox id="bookmarked-notification-dropmarker-anchor">
+ <image id="bookmarked-notification-dropmarker-icon"/>
+ </vbox>
+ </hbox>
+
+ <tooltip id="dynamic-shortcut-tooltip"
+ onpopupshowing="UpdateDynamicShortcutTooltipText(this);"/>
+
+ <menupopup id="SyncedTabsSidebarContext">
+ <menuitem label="&syncedTabs.context.open.label;"
+ accesskey="&syncedTabs.context.open.accesskey;"
+ id="syncedTabsOpenSelected" where="current"/>
+ <menuitem label="&syncedTabs.context.openInNewTab.label;"
+ accesskey="&syncedTabs.context.openInNewTab.accesskey;"
+ id="syncedTabsOpenSelectedInTab" where="tab"/>
+ <menuitem label="&syncedTabs.context.openInNewWindow.label;"
+ accesskey="&syncedTabs.context.openInNewWindow.accesskey;"
+ id="syncedTabsOpenSelectedInWindow" where="window"/>
+ <menuitem label="&syncedTabs.context.openInNewPrivateWindow.label;"
+ accesskey="&syncedTabs.context.openInNewPrivateWindow.accesskey;"
+ id="syncedTabsOpenSelectedInPrivateWindow" where="window" private="true"/>
+ <menuseparator/>
+ <menuitem label="&syncedTabs.context.bookmarkSingleTab.label;"
+ accesskey="&syncedTabs.context.bookmarkSingleTab.accesskey;"
+ id="syncedTabsBookmarkSelected"/>
+ <menuitem label="&syncedTabs.context.copy.label;"
+ accesskey="&syncedTabs.context.copy.accesskey;"
+ id="syncedTabsCopySelected"/>
+ <menuseparator/>
+ <menuitem label="&syncSyncNowItem.label;"
+ accesskey="&syncSyncNowItem.accesskey;"
+ id="syncedTabsRefresh"/>
+ </menupopup>
+ <menupopup id="SyncedTabsSidebarTabsFilterContext"
+ class="textbox-contextmenu">
+ <menuitem label="&undoCmd.label;"
+ accesskey="&undoCmd.accesskey;"
+ cmd="cmd_undo"/>
+ <menuseparator/>
+ <menuitem label="&cutCmd.label;"
+ accesskey="&cutCmd.accesskey;"
+ cmd="cmd_cut"/>
+ <menuitem label="&copyCmd.label;"
+ accesskey="&copyCmd.accesskey;"
+ cmd="cmd_copy"/>
+ <menuitem label="&pasteCmd.label;"
+ accesskey="&pasteCmd.accesskey;"
+ cmd="cmd_paste"/>
+ <menuitem label="&deleteCmd.label;"
+ accesskey="&deleteCmd.accesskey;"
+ cmd="cmd_delete"/>
+ <menuseparator/>
+ <menuitem label="&selectAllCmd.label;"
+ accesskey="&selectAllCmd.accesskey;"
+ cmd="cmd_selectAll"/>
+ <menuseparator/>
+ <menuitem label="&syncSyncNowItem.label;"
+ accesskey="&syncSyncNowItem.accesskey;"
+ id="syncedTabsRefreshFilter"/>
+ </menupopup>
+ </popupset>
+
+#ifdef CAN_DRAW_IN_TITLEBAR
+<vbox id="titlebar">
+ <hbox id="titlebar-content">
+ <spacer id="titlebar-spacer" flex="1"/>
+ <hbox id="titlebar-buttonbox-container">
+#ifdef XP_WIN
+ <hbox id="private-browsing-indicator-titlebar">
+ <hbox class="private-browsing-indicator"/>
+ </hbox>
+#endif
+ <hbox id="titlebar-buttonbox">
+ <toolbarbutton class="titlebar-button" id="titlebar-min" oncommand="window.minimize();"/>
+ <toolbarbutton class="titlebar-button" id="titlebar-max" oncommand="onTitlebarMaxClick();"/>
+ <toolbarbutton class="titlebar-button" id="titlebar-close" command="cmd_closeWindow"/>
+ </hbox>
+ </hbox>
+#ifdef XP_MACOSX
+ <!-- OS X does not natively support RTL for its titlebar items, so we prevent this secondary
+ buttonbox from reversing order in RTL by forcing an LTR direction. -->
+ <hbox id="titlebar-secondary-buttonbox" dir="ltr">
+ <hbox class="private-browsing-indicator"/>
+ <hbox id="titlebar-fullscreen-button"/>
+ </hbox>
+#endif
+ </hbox>
+</vbox>
+#endif
+
+<deck flex="1" id="tab-view-deck">
+<vbox flex="1" id="browser-panel">
+
+ <toolbox id="navigator-toolbox" mode="icons">
+ <!-- Menu -->
+ <toolbar type="menubar" id="toolbar-menubar" class="chromeclass-menubar" customizable="true"
+ mode="icons" iconsize="small"
+#ifdef MENUBAR_CAN_AUTOHIDE
+ toolbarname="&menubarCmd.label;"
+ accesskey="&menubarCmd.accesskey;"
+#if defined(MOZ_WIDGET_GTK)
+ autohide="true"
+#endif
+#endif
+ context="toolbar-context-menu">
+ <toolbaritem id="menubar-items" align="center">
+# The entire main menubar is placed into browser-menubar.inc, so that it can be shared by
+# hiddenWindow.xul.
+#include browser-menubar.inc
+ </toolbaritem>
+
+#ifdef CAN_DRAW_IN_TITLEBAR
+#ifndef XP_MACOSX
+ <hbox class="titlebar-placeholder" type="caption-buttons" ordinal="1000"
+ id="titlebar-placeholder-on-menubar-for-caption-buttons" persist="width"
+ skipintoolbarset="true"/>
+#endif
+#endif
+ </toolbar>
+
+ <toolbar id="TabsToolbar"
+ fullscreentoolbar="true"
+ customizable="true"
+ mode="icons"
+ iconsize="small"
+ aria-label="&tabsToolbar.label;"
+ context="toolbar-context-menu"
+ collapsed="true">
+
+#if defined(MOZ_WIDGET_GTK)
+ <hbox id="private-browsing-indicator"
+ skipintoolbarset="true"/>
+#endif
+
+ <tabs id="tabbrowser-tabs"
+ class="tabbrowser-tabs"
+ tabbrowser="content"
+ flex="1"
+ setfocus="false"
+ tooltip="tabbrowser-tab-tooltip"
+ stopwatchid="FX_TAB_CLICK_MS">
+ <tab class="tabbrowser-tab" selected="true" visuallyselected="true" fadein="true"/>
+ </tabs>
+
+ <toolbarbutton id="new-tab-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional"
+ label="&tabCmd.label;"
+ command="cmd_newNavigatorTab"
+ onclick="checkForMiddleClick(this, event);"
+ tooltip="dynamic-shortcut-tooltip"
+ ondrop="newTabButtonObserver.onDrop(event)"
+ ondragover="newTabButtonObserver.onDragOver(event)"
+ ondragenter="newTabButtonObserver.onDragOver(event)"
+ ondragexit="newTabButtonObserver.onDragExit(event)"
+ cui-areatype="toolbar"
+ removable="true"/>
+
+ <toolbarbutton id="alltabs-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button"
+ type="menu"
+ label="&listAllTabs.label;"
+ tooltiptext="&listAllTabs.label;"
+ removable="false">
+ <menupopup id="alltabs-popup"
+ position="after_end">
+ <menuitem id="alltabs_undoCloseTab"
+ class="menuitem-iconic"
+ key="key_undoCloseTab"
+ label="&undoCloseTab.label;"
+ observes="History:UndoCloseTab"/>
+ <menuseparator id="alltabs-popup-separator-1"/>
+ <menu id="alltabs_containersTab"
+ label="&newUserContext.label;">
+ <menupopup id="alltabs_containersMenuTab" />
+ </menu>
+ <menuseparator id="alltabs-popup-separator-2"/>
+ </menupopup>
+ </toolbarbutton>
+
+#if !defined(MOZ_WIDGET_GTK)
+ <hbox class="private-browsing-indicator" skipintoolbarset="true"/>
+#endif
+#ifdef CAN_DRAW_IN_TITLEBAR
+ <hbox class="titlebar-placeholder" type="caption-buttons"
+ id="titlebar-placeholder-on-TabsToolbar-for-captions-buttons" persist="width"
+#ifndef XP_MACOSX
+ ordinal="1000"
+#endif
+ skipintoolbarset="true"/>
+
+#ifdef XP_MACOSX
+ <hbox class="titlebar-placeholder" type="fullscreen-button"
+ id="titlebar-placeholder-on-TabsToolbar-for-fullscreen-button" persist="width"
+ skipintoolbarset="true"/>
+#endif
+#endif
+ </toolbar>
+
+ <toolbar id="nav-bar"
+ aria-label="&navbarCmd.label;"
+ fullscreentoolbar="true" mode="icons" customizable="true"
+ iconsize="small"
+ customizationtarget="nav-bar-customization-target"
+ overflowable="true"
+ overflowbutton="nav-bar-overflow-button"
+ overflowtarget="widget-overflow-list"
+ overflowpanel="widget-overflow"
+ context="toolbar-context-menu">
+
+ <hbox id="nav-bar-customization-target" flex="1">
+ <toolbaritem id="urlbar-container" flex="400" persist="width"
+ removable="false"
+ class="chromeclass-location" overflows="false">
+ <toolbarbutton id="back-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ label="&backCmd.label;"
+ command="Browser:BackOrBackDuplicate"
+ onclick="checkForMiddleClick(this, event);"
+ tooltip="back-button-tooltip"
+ context="backForwardMenu"/>
+ <hbox id="urlbar-wrapper" flex="1">
+ <toolbarbutton id="forward-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ label="&forwardCmd.label;"
+ command="Browser:ForwardOrForwardDuplicate"
+ onclick="checkForMiddleClick(this, event);"
+ tooltip="forward-button-tooltip"
+ context="backForwardMenu"/>
+ <textbox id="urlbar" flex="1"
+ placeholder="&urlbar.placeholder2;"
+ type="autocomplete"
+ autocompletesearch="unifiedcomplete"
+ autocompletesearchparam="enable-actions"
+ autocompletepopup="PopupAutoCompleteRichResult"
+ completeselectedindex="true"
+ shrinkdelay="250"
+ tabscrolling="true"
+ showcommentcolumn="true"
+ showimagecolumn="true"
+ enablehistory="true"
+ maxrows="10"
+ newlines="stripsurroundingwhitespace"
+ ontextentered="this.handleCommand(param);"
+ ontextreverted="return this.handleRevert();"
+ pageproxystate="invalid">
+ <!-- Use onclick instead of normal popup= syntax since the popup
+ code fires onmousedown, and hence eats our favicon drag events. -->
+ <box id="identity-box" role="button"
+ align="center"
+ aria-label="&urlbar.viewSiteInfo.label;"
+ onclick="gIdentityHandler.handleIdentityButtonEvent(event);"
+ onkeypress="gIdentityHandler.handleIdentityButtonEvent(event);"
+ ondragstart="gIdentityHandler.onDragStart(event);">
+ <image id="identity-icon"
+ consumeanchor="identity-box"
+ onclick="PageProxyClickHandler(event);"/>
+ <image id="sharing-icon" mousethrough="always"/>
+ <box id="blocked-permissions-container" align="center">
+ <image data-permission-id="geo" class="blocked-permission-icon geo-icon" role="button"
+ tooltiptext="&urlbar.geolocationBlocked.tooltip;"/>
+ <image data-permission-id="desktop-notification" class="blocked-permission-icon desktop-notification-icon" role="button"
+ tooltiptext="&urlbar.webNotificationsBlocked.tooltip;"/>
+ <image data-permission-id="camera" class="blocked-permission-icon camera-icon" role="button"
+ tooltiptext="&urlbar.cameraBlocked.tooltip;"/>
+ <image data-permission-id="indexedDB" class="blocked-permission-icon indexedDB-icon" role="button"
+ tooltiptext="&urlbar.indexedDBBlocked.tooltip;"/>
+ <image data-permission-id="microphone" class="blocked-permission-icon microphone-icon" role="button"
+ tooltiptext="&urlbar.microphoneBlocked.tooltip;"/>
+ <image data-permission-id="screen" class="blocked-permission-icon screen-icon" role="button"
+ tooltiptext="&urlbar.screenBlocked.tooltip;"/>
+ </box>
+ <box id="notification-popup-box"
+ hidden="true"
+ onmouseover="document.getElementById('identity-icon').classList.add('no-hover');"
+ onmouseout="document.getElementById('identity-icon').classList.remove('no-hover');"
+ align="center">
+ <image id="default-notification-icon" class="notification-anchor-icon" role="button"
+ tooltiptext="&urlbar.defaultNotificationAnchor.tooltip;"/>
+ <image id="geo-notification-icon" class="notification-anchor-icon geo-icon" role="button"
+ tooltiptext="&urlbar.geolocationNotificationAnchor.tooltip;"/>
+ <image id="addons-notification-icon" class="notification-anchor-icon install-icon" role="button"
+ tooltiptext="&urlbar.addonsNotificationAnchor.tooltip;"/>
+ <image id="indexedDB-notification-icon" class="notification-anchor-icon indexedDB-icon" role="button"
+ tooltiptext="&urlbar.indexedDBNotificationAnchor.tooltip;"/>
+ <image id="password-notification-icon" class="notification-anchor-icon login-icon" role="button"
+ tooltiptext="&urlbar.passwordNotificationAnchor.tooltip;"/>
+ <image id="plugins-notification-icon" class="notification-anchor-icon plugin-icon" role="button"
+ tooltiptext="&urlbar.pluginsNotificationAnchor.tooltip;"/>
+ <image id="web-notifications-notification-icon" class="notification-anchor-icon desktop-notification-icon" role="button"
+ tooltiptext="&urlbar.webNotificationAnchor.tooltip;"/>
+ <image id="webRTC-shareDevices-notification-icon" class="notification-anchor-icon camera-icon" role="button"
+ tooltiptext="&urlbar.webRTCShareDevicesNotificationAnchor.tooltip;"/>
+ <image id="webRTC-shareMicrophone-notification-icon" class="notification-anchor-icon microphone-icon" role="button"
+ tooltiptext="&urlbar.webRTCShareMicrophoneNotificationAnchor.tooltip;"/>
+ <image id="webRTC-shareScreen-notification-icon" class="notification-anchor-icon screen-icon" role="button"
+ tooltiptext="&urlbar.webRTCShareScreenNotificationAnchor.tooltip;"/>
+ <image id="servicesInstall-notification-icon" class="notification-anchor-icon service-icon" role="button"
+ tooltiptext="&urlbar.servicesNotificationAnchor.tooltip;"/>
+ <image id="translate-notification-icon" class="notification-anchor-icon translation-icon" role="button"
+ tooltiptext="&urlbar.translateNotificationAnchor.tooltip;"/>
+ <image id="translated-notification-icon" class="notification-anchor-icon translation-icon in-use" role="button"
+ tooltiptext="&urlbar.translatedNotificationAnchor.tooltip;"/>
+ <image id="eme-notification-icon" class="notification-anchor-icon drm-icon" role="button"
+ tooltiptext="&urlbar.emeNotificationAnchor.tooltip;"/>
+ </box>
+ <image id="tracking-protection-icon"/>
+ <image id="connection-icon"/>
+ <hbox id="identity-icon-labels">
+ <label id="identity-icon-label" class="plain" flex="1"/>
+ <label id="identity-icon-country-label" class="plain"/>
+ </hbox>
+ </box>
+ <box id="urlbar-display-box" align="center">
+ <label id="switchtab" class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
+ <label id="extension" class="urlbar-display urlbar-display-extension" value="&urlbar.extension.label;"/>
+ </box>
+ <hbox id="urlbar-icons">
+ <image id="page-report-button"
+ class="urlbar-icon"
+ hidden="true"
+ tooltiptext="&pageReportIcon.tooltip;"
+ onmousedown="gPopupBlockerObserver.onReportButtonMousedown(event);"/>
+ <image id="reader-mode-button"
+ class="urlbar-icon"
+ hidden="true"
+ onclick="ReaderParent.buttonClick(event);"/>
+ <toolbarbutton id="urlbar-zoom-button"
+ onclick="FullZoom.reset();"
+ tooltiptext="&urlbar.zoomReset.tooltip;"
+ hidden="true"/>
+ </hbox>
+ <hbox id="userContext-icons" hidden="true">
+ <label id="userContext-label"/>
+ <image id="userContext-indicator"/>
+ </hbox>
+ <toolbarbutton id="urlbar-go-button"
+ class="chromeclass-toolbar-additional"
+ onclick="gURLBar.handleCommand(event);"
+ tooltiptext="&goEndCap.tooltip;"/>
+ <toolbarbutton id="urlbar-reload-button"
+ class="chromeclass-toolbar-additional"
+ command="Browser:ReloadOrDuplicate"
+ onclick="checkForMiddleClick(this, event);"
+ tooltiptext="&reloadButton.tooltip;"/>
+ <toolbarbutton id="urlbar-stop-button"
+ class="chromeclass-toolbar-additional"
+ command="Browser:Stop"
+ tooltiptext="&stopButton.tooltip;"/>
+ </textbox>
+ </hbox>
+ </toolbaritem>
+
+ <toolbaritem id="search-container" title="&searchItem.title;"
+ align="center" class="chromeclass-toolbar-additional panel-wide-item"
+ cui-areatype="toolbar"
+ flex="100" persist="width" removable="true">
+ <searchbar id="searchbar" flex="1"/>
+ </toolbaritem>
+
+ <toolbarbutton id="bookmarks-menu-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional"
+ removable="true"
+ type="menu-button"
+ label="&bookmarksMenuButton.label;"
+ tooltip="dynamic-shortcut-tooltip"
+ anchor="dropmarker"
+ ondragenter="PlacesMenuDNDHandler.onDragEnter(event);"
+ ondragover="PlacesMenuDNDHandler.onDragOver(event);"
+ ondragleave="PlacesMenuDNDHandler.onDragLeave(event);"
+ ondrop="PlacesMenuDNDHandler.onDrop(event);"
+ cui-areatype="toolbar"
+ oncommand="BookmarkingUI.onCommand(event);">
+ <observes element="bookmarkThisPageBroadcaster" attribute="starred"/>
+ <observes element="bookmarkThisPageBroadcaster" attribute="buttontooltiptext"/>
+ <menupopup id="BMB_bookmarksPopup"
+ class="cui-widget-panel cui-widget-panelview cui-widget-panelWithFooter PanelUI-subView"
+ placespopup="true"
+ context="placesContext"
+ openInTabs="children"
+ oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);"
+ onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);"
+ onpopupshowing="BookmarkingUI.onPopupShowing(event);
+ BookmarkingUI.attachPlacesView(event, this);"
+ tooltip="bhTooltip" popupsinherittooltip="true">
+ <menuitem id="BMB_viewBookmarksSidebar"
+ class="subviewbutton"
+ label="&viewBookmarksSidebar2.label;"
+ type="checkbox"
+ oncommand="SidebarUI.toggle('viewBookmarksSidebar');">
+ <observes element="viewBookmarksSidebar" attribute="checked"/>
+ </menuitem>
+ <!-- NB: temporary solution for bug 985024, this should go away soon. -->
+ <menuitem id="BMB_bookmarksShowAllTop"
+ class="menuitem-iconic subviewbutton"
+ label="&showAllBookmarks2.label;"
+ command="Browser:ShowAllBookmarks"
+ key="manBookmarkKb"/>
+ <menuseparator/>
+ <menuitem label="&recentBookmarks.label;"
+ id="BMB_recentBookmarks"
+ disabled="true"
+ class="menuitem-iconic subviewbutton"/>
+ <menuseparator/>
+ <menu id="BMB_bookmarksToolbar"
+ class="menu-iconic bookmark-item subviewbutton"
+ label="&personalbarCmd.label;"
+ container="true">
+ <menupopup id="BMB_bookmarksToolbarPopup"
+ placespopup="true"
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, 'place:folder=TOOLBAR',
+ PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);">
+ <menuitem id="BMB_viewBookmarksToolbar"
+ placesanonid="view-toolbar"
+ toolbarId="PersonalToolbar"
+ type="checkbox"
+ oncommand="onViewToolbarCommand(event)"
+ label="&viewBookmarksToolbar.label;"/>
+ <menuseparator/>
+ <!-- Bookmarks toolbar items -->
+ </menupopup>
+ </menu>
+ <menu id="BMB_unsortedBookmarks"
+ class="menu-iconic bookmark-item subviewbutton"
+ label="&bookmarksMenuButton.other.label;"
+ container="true">
+ <menupopup id="BMB_unsortedBookmarksPopup"
+ placespopup="true"
+ context="placesContext"
+ onpopupshowing="if (!this.parentNode._placesView)
+ new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS',
+ PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);"/>
+ </menu>
+ <menuseparator/>
+ <!-- Bookmarks menu items will go here -->
+ <menuitem id="BMB_bookmarksShowAll"
+ class="subviewbutton panel-subview-footer"
+ label="&showAllBookmarks2.label;"
+ command="Browser:ShowAllBookmarks"
+ key="manBookmarkKb"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <!-- This is a placeholder for the Downloads Indicator. It is visible
+ during the customization of the toolbar, in the palette, and before
+ the Downloads Indicator overlay is loaded. -->
+ <toolbarbutton id="downloads-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional badged-button"
+ key="key_openDownloads"
+ oncommand="DownloadsIndicatorView.onCommand(event);"
+ ondrop="DownloadsIndicatorView.onDrop(event);"
+ ondragover="DownloadsIndicatorView.onDragOver(event);"
+ ondragenter="DownloadsIndicatorView.onDragOver(event);"
+ label="&downloads.label;"
+ removable="true"
+ cui-areatype="toolbar"
+ tooltip="dynamic-shortcut-tooltip"/>
+
+ <toolbarbutton id="home-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ removable="true"
+ label="&homeButton.label;"
+ ondragover="homeButtonObserver.onDragOver(event)"
+ ondragenter="homeButtonObserver.onDragOver(event)"
+ ondrop="homeButtonObserver.onDrop(event)"
+ ondragexit="homeButtonObserver.onDragExit(event)"
+ key="goHome"
+ onclick="BrowserGoHome(event);"
+ cui-areatype="toolbar"
+ aboutHomeOverrideTooltip="&abouthome.pageTitle;"/>
+ </hbox>
+
+ <toolbarbutton id="nav-bar-overflow-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional overflow-button"
+ skipintoolbarset="true"
+ tooltiptext="&navbarOverflow.label;"/>
+
+ <toolbaritem id="PanelUI-button"
+ class="chromeclass-toolbar-additional"
+ removable="false">
+ <toolbarbutton id="PanelUI-menu-button"
+ class="toolbarbutton-1 badged-button"
+ consumeanchor="PanelUI-button"
+ label="&brandShortName;"
+ tooltiptext="&appmenu.tooltip;"/>
+ </toolbaritem>
+
+ <hbox id="window-controls" hidden="true" pack="end" skipintoolbarset="true"
+ ordinal="1000">
+ <toolbarbutton id="minimize-button"
+ tooltiptext="&fullScreenMinimize.tooltip;"
+ oncommand="window.minimize();"/>
+
+ <toolbarbutton id="restore-button"
+#ifdef XP_MACOSX
+# Prior to 10.7 there wasn't a native fullscreen button so we use #restore-button
+# to exit fullscreen and want it to behave like other toolbar buttons.
+ class="toolbarbutton-1"
+#endif
+ tooltiptext="&fullScreenRestore.tooltip;"
+ oncommand="BrowserFullScreen();"/>
+
+ <toolbarbutton id="close-button"
+ tooltiptext="&fullScreenClose.tooltip;"
+ oncommand="BrowserTryToCloseWindow();"/>
+ </hbox>
+ </toolbar>
+
+ <toolbarset id="customToolbars" context="toolbar-context-menu"/>
+
+ <toolbar id="PersonalToolbar"
+ mode="icons" iconsize="small"
+ class="chromeclass-directories"
+ context="toolbar-context-menu"
+ toolbarname="&personalbarCmd.label;" accesskey="&personalbarCmd.accesskey;"
+ collapsed="true"
+ customizable="true">
+ <toolbaritem id="personal-bookmarks"
+ title="&bookmarksToolbarItem.label;"
+ cui-areatype="toolbar"
+ removable="true">
+ <toolbarbutton id="bookmarks-toolbar-placeholder"
+ class="toolbarbutton-1"
+ mousethrough="never"
+ label="&bookmarksToolbarItem.label;"
+ oncommand="PlacesToolbarHelper.onPlaceholderCommand();"/>
+ <hbox flex="1"
+ id="PlacesToolbar"
+ context="placesContext"
+ onclick="BookmarksEventHandler.onClick(event, this._placesView);"
+ oncommand="BookmarksEventHandler.onCommand(event, this._placesView);"
+ tooltip="bhTooltip"
+ popupsinherittooltip="true">
+ <hbox flex="1">
+ <hbox id="PlacesToolbarDropIndicatorHolder" align="center" collapsed="true">
+ <image id="PlacesToolbarDropIndicator"
+ mousethrough="always"
+ collapsed="true"/>
+ </hbox>
+ <scrollbox orient="horizontal"
+ id="PlacesToolbarItems"
+ flex="1"/>
+ <toolbarbutton type="menu"
+ id="PlacesChevron"
+ class="chevron"
+ mousethrough="never"
+ collapsed="true"
+ tooltiptext="&bookmarksToolbarChevron.tooltip;"
+ onpopupshowing="document.getElementById('PlacesToolbar')
+ ._placesView._onChevronPopupShowing(event);">
+ <menupopup id="PlacesChevronPopup"
+ placespopup="true"
+ tooltip="bhTooltip" popupsinherittooltip="true"
+ context="placesContext"/>
+ </toolbarbutton>
+ </hbox>
+ </hbox>
+ </toolbaritem>
+ </toolbar>
+
+ <!-- This is a shim which will go away ASAP. See bug 749804 for details -->
+ <toolbar id="addon-bar" toolbar-delegate="nav-bar" mode="icons" iconsize="small"
+ customizable="true">
+ <hbox id="addonbar-closebutton"/>
+ <statusbar id="status-bar"/>
+ </toolbar>
+
+ <toolbarpalette id="BrowserToolbarPalette">
+
+# Update primaryToolbarButtons in browser/themes/shared/browser.inc when adding
+# or removing default items with the toolbarbutton-1 class.
+
+ <toolbarbutton id="print-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+#ifdef XP_MACOSX
+ command="cmd_print"
+ tooltip="dynamic-shortcut-tooltip"
+#else
+ command="cmd_printPreview"
+ tooltiptext="&printButton.tooltip;"
+#endif
+ label="&printButton.label;"/>
+
+
+ <toolbarbutton id="new-window-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ label="&newNavigatorCmd.label;"
+ command="key_newNavigator"
+ tooltip="dynamic-shortcut-tooltip"
+ ondrop="newWindowButtonObserver.onDrop(event)"
+ ondragover="newWindowButtonObserver.onDragOver(event)"
+ ondragenter="newWindowButtonObserver.onDragOver(event)"
+ ondragexit="newWindowButtonObserver.onDragExit(event)"/>
+
+ <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+ observes="View:FullScreen"
+ type="checkbox"
+ label="&fullScreenCmd.label;"
+ tooltip="dynamic-shortcut-tooltip"/>
+ </toolbarpalette>
+ </toolbox>
+
+ <hbox id="fullscr-toggler" hidden="true"/>
+
+ <deck id="content-deck" flex="1">
+ <hbox flex="1" id="browser">
+ <vbox id="browser-border-start" hidden="true" layer="true"/>
+ <vbox id="sidebar-box" hidden="true" class="chromeclass-extrachrome">
+ <sidebarheader id="sidebar-header" align="center">
+ <label id="sidebar-title" persist="value" flex="1" crop="end" control="sidebar"/>
+ <image id="sidebar-throbber"/>
+ <toolbarbutton class="close-icon tabbable" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="SidebarUI.hide();"/>
+ </sidebarheader>
+ <browser id="sidebar" flex="1" autoscroll="false" disablehistory="true" disablefullscreen="true"
+ style="min-width: 14em; width: 18em; max-width: 36em;" tooltip="aHTMLTooltip"/>
+ </vbox>
+
+ <splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/>
+ <vbox id="appcontent" flex="1">
+ <notificationbox id="high-priority-global-notificationbox" notificationside="top"/>
+ <tabbrowser id="content"
+ flex="1" contenttooltip="aHTMLTooltip"
+ tabcontainer="tabbrowser-tabs"
+ contentcontextmenu="contentAreaContextMenu"
+ autocompletepopup="PopupAutoComplete"
+ selectmenulist="ContentSelectDropdown"
+ datetimepicker="DateTimePickerPanel"/>
+ </vbox>
+ <vbox id="browser-border-end" hidden="true" layer="true"/>
+ </hbox>
+#include ../../components/customizableui/content/customizeMode.inc.xul
+ </deck>
+
+ <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true">
+ <html:div class="pointerlockfswarning-domain-text">
+ &fullscreenWarning.beforeDomain.label;
+ <html:span class="pointerlockfswarning-domain"/>
+ &fullscreenWarning.afterDomain.label;
+ </html:div>
+ <html:div class="pointerlockfswarning-generic-text">
+ &fullscreenWarning.generic.label;
+ </html:div>
+ <html:button id="fullscreen-exit-button"
+ onclick="FullScreen.exitDomFullScreen();">
+#ifdef XP_MACOSX
+ &exitDOMFullscreenMac.button;
+#else
+ &exitDOMFullscreen.button;
+#endif
+ </html:button>
+ </html:div>
+
+ <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true">
+ <html:div class="pointerlockfswarning-domain-text">
+ &pointerlockWarning.beforeDomain.label;
+ <html:span class="pointerlockfswarning-domain"/>
+ &pointerlockWarning.afterDomain.label;
+ </html:div>
+ <html:div class="pointerlockfswarning-generic-text">
+ &pointerlockWarning.generic.label;
+ </html:div>
+ </html:div>
+
+ <vbox id="browser-bottombox" layer="true">
+ <notificationbox id="global-notificationbox" notificationside="bottom"/>
+ </vbox>
+
+ <svg:svg height="0">
+#include tab-shape.inc.svg
+ <svg:clipPath id="urlbar-back-button-clip-path">
+#ifndef XP_MACOSX
+ <svg:path d="M -9,-4 l 0,1 a 15 15 0 0,1 0,30 l 0,1 l 10000,0 l 0,-32 l -10000,0 z" />
+#else
+ <svg:path d="M -11,-5 a 16 16 0 0 1 0,34 l 10000,0 l 0,-34 l -10000,0 z"/>
+#endif
+ </svg:clipPath>
+#ifdef XP_WIN
+ <svg:clipPath id="urlbar-back-button-clip-path-win10">
+ <svg:path d="M -6,-2 l 0,1 a 15 15 0 0,1 0,30 l 0,1 l 10000,0 l 0,-32 l -10000,0 z" />
+ </svg:clipPath>
+#endif
+ </svg:svg>
+
+</vbox>
+# <iframe id="tab-view"> is dynamically appended as the 2nd child of #tab-view-deck.
+# Introducing the iframe dynamically, as needed, was found to be better than
+# starting with an empty iframe here in browser.xul from a Ts standpoint.
+</deck>
+
+</window>
diff --git a/browser/base/content/browserMountPoints.inc b/browser/base/content/browserMountPoints.inc
new file mode 100644
index 000000000..e4315b04a
--- /dev/null
+++ b/browser/base/content/browserMountPoints.inc
@@ -0,0 +1,12 @@
+<stringbundleset id="stringbundleset"/>
+
+<commandset id="mainCommandSet"/>
+<commandset id="baseMenuCommandSet"/>
+<commandset id="placesCommands"/>
+
+<broadcasterset id="mainBroadcasterSet"/>
+
+<keyset id="mainKeyset"/>
+<keyset id="baseMenuKeyset"/>
+
+<menubar id="main-menubar"/> \ No newline at end of file
diff --git a/browser/base/content/content.js b/browser/base/content/content.js
new file mode 100644
index 000000000..658d2014d
--- /dev/null
+++ b/browser/base/content/content.js
@@ -0,0 +1,1503 @@
+/* -*- 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 content script should work in any browser or iframe and should not
+ * depend on the frame being contained in tabbrowser. */
+
+var {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:///modules/ContentWebRTC.jsm");
+Cu.import("resource:///modules/ContentObservers.jsm");
+Cu.import("resource://gre/modules/InlineSpellChecker.jsm");
+Cu.import("resource://gre/modules/InlineSpellCheckerContent.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
+ "resource:///modules/E10SUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
+ "resource:///modules/ContentLinkHandler.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
+ "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory",
+ "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+ "resource://gre/modules/InsecurePasswordUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
+ "resource:///modules/PluginContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
+ "resource:///modules/FormSubmitObserver.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
+ "resource://gre/modules/PageMetadata.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUIUtils",
+ "resource:///modules/PlacesUIUtils.jsm");
+XPCOMUtils.defineLazyGetter(this, "PageMenuChild", function() {
+ let tmp = {};
+ Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
+ return new tmp.PageMenuChild();
+});
+XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
+ "resource:///modules/Feeds.jsm");
+
+Cu.importGlobalProperties(["URL"]);
+
+// TabChildGlobal
+var global = this;
+
+// Load the form validation popup handler
+var formSubmitObserver = new FormSubmitObserver(content, this);
+
+addMessageListener("ContextMenu:DoCustomCommand", function(message) {
+ E10SUtils.wrapHandlingUserInput(
+ content, message.data.handlingUserInput,
+ () => PageMenuChild.executeMenu(message.data.generatedItemId));
+});
+
+addMessageListener("RemoteLogins:fillForm", function(message) {
+ LoginManagerContent.receiveMessage(message, content);
+});
+addEventListener("DOMFormHasPassword", function(event) {
+ LoginManagerContent.onDOMFormHasPassword(event, content);
+ let formLike = LoginFormFactory.createFromForm(event.target);
+ InsecurePasswordUtils.reportInsecurePasswords(formLike);
+});
+addEventListener("DOMInputPasswordAdded", function(event) {
+ LoginManagerContent.onDOMInputPasswordAdded(event, content);
+ let formLike = LoginFormFactory.createFromField(event.target);
+ InsecurePasswordUtils.reportInsecurePasswords(formLike);
+});
+addEventListener("pageshow", function(event) {
+ LoginManagerContent.onPageShow(event, content);
+});
+addEventListener("DOMAutoComplete", function(event) {
+ LoginManagerContent.onUsernameInput(event);
+});
+addEventListener("blur", function(event) {
+ LoginManagerContent.onUsernameInput(event);
+});
+
+var handleContentContextMenu = function (event) {
+ let defaultPrevented = event.defaultPrevented;
+ if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
+ let plugin = null;
+ try {
+ plugin = event.target.QueryInterface(Ci.nsIObjectLoadingContent);
+ } catch (e) {}
+ if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
+ // Don't open a context menu for plugins.
+ return;
+ }
+
+ defaultPrevented = false;
+ }
+
+ if (defaultPrevented)
+ return;
+
+ let addonInfo = {};
+ let subject = {
+ event: event,
+ addonInfo: addonInfo,
+ };
+ subject.wrappedJSObject = subject;
+ Services.obs.notifyObservers(subject, "content-contextmenu", null);
+
+ let doc = event.target.ownerDocument;
+ let docLocation = doc.mozDocumentURIIfNotForErrorPages;
+ docLocation = docLocation && docLocation.spec;
+ let charSet = doc.characterSet;
+ let baseURI = doc.baseURI;
+ let referrer = doc.referrer;
+ let referrerPolicy = doc.referrerPolicy;
+ let frameOuterWindowID = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+ let loginFillInfo = LoginManagerContent.getFieldContext(event.target);
+
+ // The same-origin check will be done in nsContextMenu.openLinkInTab.
+ let parentAllowsMixedContent = !!docShell.mixedContentChannel;
+
+ // get referrer attribute from clicked link and parse it
+ // if per element referrer is enabled, the element referrer overrules
+ // the document wide referrer
+ if (Services.prefs.getBoolPref("network.http.enablePerElementReferrer")) {
+ let referrerAttrValue = Services.netUtils.parseAttributePolicyString(event.target.
+ getAttribute("referrerpolicy"));
+ if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
+ referrerPolicy = referrerAttrValue;
+ }
+ }
+
+ let disableSetDesktopBg = null;
+ // Media related cache info parent needs for saving
+ let contentType = null;
+ let contentDisposition = null;
+ if (event.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
+ event.target instanceof Ci.nsIImageLoadingContent &&
+ event.target.currentURI) {
+ disableSetDesktopBg = disableSetDesktopBackground(event.target);
+
+ try {
+ let imageCache =
+ Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
+ .getImgCacheForDocument(doc);
+ let props =
+ imageCache.findEntryProperties(event.target.currentURI, doc);
+ try {
+ contentType = props.get("type", Ci.nsISupportsCString).data;
+ } catch (e) {}
+ try {
+ contentDisposition =
+ props.get("content-disposition", Ci.nsISupportsCString).data;
+ } catch (e) {}
+ } catch (e) {}
+ }
+
+ let selectionInfo = BrowserUtils.getSelectionDetails(content);
+
+ let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
+ let userContextId = loadContext.originAttributes.userContextId;
+
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ let editFlags = SpellCheckHelper.isEditable(event.target, content);
+ let spellInfo;
+ if (editFlags &
+ (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
+ spellInfo =
+ InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
+ }
+
+ // Set the event target first as the copy image command needs it to
+ // determine what was context-clicked on. Then, update the state of the
+ // commands on the context menu.
+ docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
+ .setCommandNode(event.target);
+ event.target.ownerGlobal.updateCommands("contentcontextmenu");
+
+ let customMenuItems = PageMenuChild.build(event.target);
+ let principal = doc.nodePrincipal;
+ sendRpcMessage("contextmenu",
+ { editFlags, spellInfo, customMenuItems, addonInfo,
+ principal, docLocation, charSet, baseURI, referrer,
+ referrerPolicy, contentType, contentDisposition,
+ frameOuterWindowID, selectionInfo, disableSetDesktopBg,
+ loginFillInfo, parentAllowsMixedContent, userContextId },
+ { event, popupNode: event.target });
+ }
+ else {
+ // Break out to the parent window and pass the add-on info along
+ let browser = docShell.chromeEventHandler;
+ let mainWin = browser.ownerGlobal;
+ mainWin.gContextMenuContentData = {
+ isRemote: false,
+ event: event,
+ popupNode: event.target,
+ browser: browser,
+ addonInfo: addonInfo,
+ documentURIObject: doc.documentURIObject,
+ docLocation: docLocation,
+ charSet: charSet,
+ referrer: referrer,
+ referrerPolicy: referrerPolicy,
+ contentType: contentType,
+ contentDisposition: contentDisposition,
+ selectionInfo: selectionInfo,
+ disableSetDesktopBackground: disableSetDesktopBg,
+ loginFillInfo,
+ parentAllowsMixedContent,
+ userContextId,
+ };
+ }
+}
+
+Cc["@mozilla.org/eventlistenerservice;1"]
+ .getService(Ci.nsIEventListenerService)
+ .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
+
+// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
+const TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN = 0;
+const TLS_ERROR_REPORT_TELEMETRY_EXPANDED = 1;
+const TLS_ERROR_REPORT_TELEMETRY_SUCCESS = 6;
+const TLS_ERROR_REPORT_TELEMETRY_FAILURE = 7;
+
+const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
+const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;
+
+const SEC_ERROR_EXPIRED_CERTIFICATE = SEC_ERROR_BASE + 11;
+const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13;
+const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30;
+const SEC_ERROR_OCSP_FUTURE_RESPONSE = SEC_ERROR_BASE + 131;
+const SEC_ERROR_OCSP_OLD_RESPONSE = SEC_ERROR_BASE + 132;
+const MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 5;
+const MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 6;
+
+const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
+
+const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
+
+const PREF_SSL_IMPACT = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => {
+ return prefs.concat(Services.prefs.getChildList(root));
+}, []);
+
+
+function getSerializedSecurityInfo(docShell) {
+ let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+
+ let securityInfo = docShell.failedChannel && docShell.failedChannel.securityInfo;
+ if (!securityInfo) {
+ return "";
+ }
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo)
+ .QueryInterface(Ci.nsISerializable);
+
+ return serhelper.serializeToString(securityInfo);
+}
+
+var AboutNetAndCertErrorListener = {
+ init: function(chromeGlobal) {
+ addMessageListener("CertErrorDetails", this);
+ addMessageListener("Browser:CaptivePortalFreed", this);
+ chromeGlobal.addEventListener('AboutNetErrorLoad', this, false, true);
+ chromeGlobal.addEventListener('AboutNetErrorOpenCaptivePortal', this, false, true);
+ chromeGlobal.addEventListener('AboutNetErrorSetAutomatic', this, false, true);
+ chromeGlobal.addEventListener('AboutNetErrorOverride', this, false, true);
+ chromeGlobal.addEventListener('AboutNetErrorResetPreferences', this, false, true);
+ },
+
+ get isAboutNetError() {
+ return content.document.documentURI.startsWith("about:neterror");
+ },
+
+ get isAboutCertError() {
+ return content.document.documentURI.startsWith("about:certerror");
+ },
+
+ receiveMessage: function(msg) {
+ if (!this.isAboutCertError) {
+ return;
+ }
+
+ switch (msg.name) {
+ case "CertErrorDetails":
+ this.onCertErrorDetails(msg);
+ break;
+ case "Browser:CaptivePortalFreed":
+ this.onCaptivePortalFreed(msg);
+ break;
+ }
+ },
+
+ onCertErrorDetails(msg) {
+ let div = content.document.getElementById("certificateErrorText");
+ div.textContent = msg.data.info;
+ let learnMoreLink = content.document.getElementById("learnMoreLink");
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+
+ switch (msg.data.code) {
+ case SEC_ERROR_UNKNOWN_ISSUER:
+ learnMoreLink.href = baseURL + "security-error";
+ break;
+
+ // in case the certificate expired we make sure the system clock
+ // matches settings server (kinto) time
+ case SEC_ERROR_EXPIRED_CERTIFICATE:
+ case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE:
+ case SEC_ERROR_OCSP_FUTURE_RESPONSE:
+ case SEC_ERROR_OCSP_OLD_RESPONSE:
+ case MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE:
+ case MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE:
+
+ // use blocklist stats if available
+ if (Services.prefs.getPrefType(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS)) {
+ let difference = Services.prefs.getIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS);
+
+ // if the difference is more than a day
+ if (Math.abs(difference) > 60 * 60 * 24) {
+ let formatter = new Intl.DateTimeFormat();
+ let systemDate = formatter.format(new Date());
+ // negative difference means local time is behind server time
+ let actualDate = formatter.format(new Date(Date.now() - difference * 1000));
+
+ content.document.getElementById("wrongSystemTime_URL")
+ .textContent = content.document.location.hostname;
+ content.document.getElementById("wrongSystemTime_systemDate")
+ .textContent = systemDate;
+ content.document.getElementById("wrongSystemTime_actualDate")
+ .textContent = actualDate;
+
+ content.document.getElementById("errorShortDesc")
+ .style.display = "none";
+ content.document.getElementById("wrongSystemTimePanel")
+ .style.display = "block";
+ }
+ }
+ learnMoreLink.href = baseURL + "time-errors";
+ break;
+ }
+ },
+
+ onCaptivePortalFreed(msg) {
+ content.dispatchEvent(new content.CustomEvent("AboutNetErrorCaptivePortalFreed"));
+ },
+
+ handleEvent: function(aEvent) {
+ if (!this.isAboutNetError && !this.isAboutCertError) {
+ return;
+ }
+
+ switch (aEvent.type) {
+ case "AboutNetErrorLoad":
+ this.onPageLoad(aEvent);
+ break;
+ case "AboutNetErrorOpenCaptivePortal":
+ this.openCaptivePortalPage(aEvent);
+ break;
+ case "AboutNetErrorSetAutomatic":
+ this.onSetAutomatic(aEvent);
+ break;
+ case "AboutNetErrorOverride":
+ this.onOverride(aEvent);
+ break;
+ case "AboutNetErrorResetPreferences":
+ this.onResetPreferences(aEvent);
+ break;
+ }
+ },
+
+ changedCertPrefs: function () {
+ for (let prefName of PREF_SSL_IMPACT) {
+ if (Services.prefs.prefHasUserValue(prefName)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ onPageLoad: function(evt) {
+ if (this.isAboutCertError) {
+ let originalTarget = evt.originalTarget;
+ let ownerDoc = originalTarget.ownerDocument;
+ ClickEventHandler.onCertError(originalTarget, ownerDoc);
+ }
+
+ let automatic = Services.prefs.getBoolPref("security.ssl.errorReporting.automatic");
+ content.dispatchEvent(new content.CustomEvent("AboutNetErrorOptions", {
+ detail: JSON.stringify({
+ enabled: Services.prefs.getBoolPref("security.ssl.errorReporting.enabled"),
+ changedCertPrefs: this.changedCertPrefs(),
+ automatic: automatic
+ })
+ }));
+
+ sendAsyncMessage("Browser:SSLErrorReportTelemetry",
+ {reportStatus: TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN});
+ },
+
+ openCaptivePortalPage: function(evt) {
+ sendAsyncMessage("Browser:OpenCaptivePortalPage");
+ },
+
+
+ onResetPreferences: function(evt) {
+ sendAsyncMessage("Browser:ResetSSLPreferences");
+ },
+
+ onSetAutomatic: function(evt) {
+ sendAsyncMessage("Browser:SetSSLErrorReportAuto", {
+ automatic: evt.detail
+ });
+
+ // if we're enabling reports, send a report for this failure
+ if (evt.detail) {
+ let {host, port} = content.document.mozDocumentURIIfNotForErrorPages;
+ sendAsyncMessage("Browser:SendSSLErrorReport", {
+ uri: { host, port },
+ securityInfo: getSerializedSecurityInfo(docShell),
+ });
+
+ }
+ },
+
+ onOverride: function(evt) {
+ let {host, port} = content.document.mozDocumentURIIfNotForErrorPages;
+ sendAsyncMessage("Browser:OverrideWeakCrypto", { uri: {host, port} });
+ }
+}
+
+AboutNetAndCertErrorListener.init(this);
+
+
+var ClickEventHandler = {
+ init: function init() {
+ Cc["@mozilla.org/eventlistenerservice;1"]
+ .getService(Ci.nsIEventListenerService)
+ .addSystemEventListener(global, "click", this, true);
+ },
+
+ handleEvent: function(event) {
+ if (!event.isTrusted || event.defaultPrevented || event.button == 2) {
+ return;
+ }
+
+ let originalTarget = event.originalTarget;
+ let ownerDoc = originalTarget.ownerDocument;
+ if (!ownerDoc) {
+ return;
+ }
+
+ // Handle click events from about pages
+ if (ownerDoc.documentURI.startsWith("about:certerror")) {
+ this.onCertError(originalTarget, ownerDoc);
+ return;
+ } else if (ownerDoc.documentURI.startsWith("about:blocked")) {
+ this.onAboutBlocked(originalTarget, ownerDoc);
+ return;
+ } else if (ownerDoc.documentURI.startsWith("about:neterror")) {
+ this.onAboutNetError(event, ownerDoc.documentURI);
+ return;
+ }
+
+ let [href, node, principal] = this._hrefAndLinkNodeForClickEvent(event);
+
+ // get referrer attribute from clicked link and parse it
+ // if per element referrer is enabled, the element referrer overrules
+ // the document wide referrer
+ let referrerPolicy = ownerDoc.referrerPolicy;
+ if (Services.prefs.getBoolPref("network.http.enablePerElementReferrer") &&
+ node) {
+ let referrerAttrValue = Services.netUtils.parseAttributePolicyString(node.
+ getAttribute("referrerpolicy"));
+ if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
+ referrerPolicy = referrerAttrValue;
+ }
+ }
+
+ let json = { button: event.button, shiftKey: event.shiftKey,
+ ctrlKey: event.ctrlKey, metaKey: event.metaKey,
+ altKey: event.altKey, href: null, title: null,
+ bookmark: false, referrerPolicy: referrerPolicy,
+ originAttributes: principal ? principal.originAttributes : {},
+ isContentWindowPrivate: PrivateBrowsingUtils.isContentWindowPrivate(ownerDoc.defaultView)};
+
+ if (href) {
+ try {
+ BrowserUtils.urlSecurityCheck(href, principal);
+ } catch (e) {
+ return;
+ }
+
+ json.href = href;
+ if (node) {
+ json.title = node.getAttribute("title");
+ if (event.button == 0 && !event.ctrlKey && !event.shiftKey &&
+ !event.altKey && !event.metaKey) {
+ json.bookmark = node.getAttribute("rel") == "sidebar";
+ if (json.bookmark) {
+ event.preventDefault(); // Need to prevent the pageload.
+ }
+ }
+ }
+ json.noReferrer = BrowserUtils.linkHasNoReferrer(node)
+
+ // Check if the link needs to be opened with mixed content allowed.
+ // Only when the owner doc has |mixedContentChannel| and the same origin
+ // should we allow mixed content.
+ json.allowMixedContent = false;
+ let docshell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ if (docShell.mixedContentChannel) {
+ const sm = Services.scriptSecurityManager;
+ try {
+ let targetURI = BrowserUtils.makeURI(href);
+ sm.checkSameOriginURI(docshell.mixedContentChannel.URI, targetURI, false);
+ json.allowMixedContent = true;
+ } catch (e) {}
+ }
+ json.originPrincipal = ownerDoc.nodePrincipal;
+
+ sendAsyncMessage("Content:Click", json);
+ return;
+ }
+
+ // This might be middle mouse navigation.
+ if (event.button == 1) {
+ sendAsyncMessage("Content:Click", json);
+ }
+ },
+
+ onCertError: function (targetElement, ownerDoc) {
+ let docShell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ sendAsyncMessage("Browser:CertExceptionError", {
+ location: ownerDoc.location.href,
+ elementId: targetElement.getAttribute("id"),
+ isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView),
+ securityInfoAsString: getSerializedSecurityInfo(docShell),
+ });
+ },
+
+ onAboutBlocked: function (targetElement, ownerDoc) {
+ var reason = 'phishing';
+ if (/e=malwareBlocked/.test(ownerDoc.documentURI)) {
+ reason = 'malware';
+ } else if (/e=unwantedBlocked/.test(ownerDoc.documentURI)) {
+ reason = 'unwanted';
+ }
+ sendAsyncMessage("Browser:SiteBlockedError", {
+ location: ownerDoc.location.href,
+ reason: reason,
+ elementId: targetElement.getAttribute("id"),
+ isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView)
+ });
+ },
+
+ onAboutNetError: function (event, documentURI) {
+ let elmId = event.originalTarget.getAttribute("id");
+ if (elmId == "returnButton") {
+ sendAsyncMessage("Browser:SSLErrorGoBack", {});
+ return;
+ }
+ if (elmId != "errorTryAgain" || !/e=netOffline/.test(documentURI)) {
+ return;
+ }
+ // browser front end will handle clearing offline mode and refreshing
+ // the page *if* we're in offline mode now. Otherwise let the error page
+ // handle the click.
+ if (Services.io.offline) {
+ event.preventDefault();
+ sendAsyncMessage("Browser:EnableOnlineMode", {});
+ }
+ },
+
+ /**
+ * Extracts linkNode and href for the current click target.
+ *
+ * @param event
+ * The click event.
+ * @return [href, linkNode, linkPrincipal].
+ *
+ * @note linkNode will be null if the click wasn't on an anchor
+ * element. This includes SVG links, because callers expect |node|
+ * to behave like an <a> element, which SVG links (XLink) don't.
+ */
+ _hrefAndLinkNodeForClickEvent: function(event) {
+ function isHTMLLink(aNode) {
+ // Be consistent with what nsContextMenu.js does.
+ return ((aNode instanceof content.HTMLAnchorElement && aNode.href) ||
+ (aNode instanceof content.HTMLAreaElement && aNode.href) ||
+ aNode instanceof content.HTMLLinkElement);
+ }
+
+ let node = event.target;
+ while (node && !isHTMLLink(node)) {
+ node = node.parentNode;
+ }
+
+ if (node)
+ return [node.href, node, node.ownerDocument.nodePrincipal];
+
+ // If there is no linkNode, try simple XLink.
+ let href, baseURI;
+ node = event.target;
+ while (node && !href) {
+ if (node.nodeType == content.Node.ELEMENT_NODE &&
+ (node.localName == "a" ||
+ node.namespaceURI == "http://www.w3.org/1998/Math/MathML")) {
+ href = node.getAttribute("href") ||
+ node.getAttributeNS("http://www.w3.org/1999/xlink", "href");
+ if (href) {
+ baseURI = node.ownerDocument.baseURIObject;
+ break;
+ }
+ }
+ node = node.parentNode;
+ }
+
+ // In case of XLink, we don't return the node we got href from since
+ // callers expect <a>-like elements.
+ // Note: makeURI() will throw if aUri is not a valid URI.
+ return [href ? BrowserUtils.makeURI(href, null, baseURI).spec : null, null,
+ node && node.ownerDocument.nodePrincipal];
+ }
+};
+ClickEventHandler.init();
+
+ContentLinkHandler.init(this);
+
+// TODO: Load this lazily so the JSM is run only if a relevant event/message fires.
+var pluginContent = new PluginContent(global);
+
+addEventListener("DOMWebNotificationClicked", function(event) {
+ sendAsyncMessage("DOMWebNotificationClicked", {});
+}, false);
+
+addEventListener("DOMServiceWorkerFocusClient", function(event) {
+ sendAsyncMessage("DOMServiceWorkerFocusClient", {});
+}, false);
+
+ContentWebRTC.init();
+addMessageListener("rtcpeer:Allow", ContentWebRTC);
+addMessageListener("rtcpeer:Deny", ContentWebRTC);
+addMessageListener("webrtc:Allow", ContentWebRTC);
+addMessageListener("webrtc:Deny", ContentWebRTC);
+addMessageListener("webrtc:StopSharing", ContentWebRTC);
+addMessageListener("webrtc:StartBrowserSharing", () => {
+ let windowID = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ sendAsyncMessage("webrtc:response:StartBrowserSharing", {
+ windowID: windowID
+ });
+});
+
+addEventListener("pageshow", function(event) {
+ if (event.target == content.document) {
+ sendAsyncMessage("PageVisibility:Show", {
+ persisted: event.persisted,
+ });
+ }
+});
+addEventListener("pagehide", function(event) {
+ if (event.target == content.document) {
+ sendAsyncMessage("PageVisibility:Hide", {
+ persisted: event.persisted,
+ });
+ }
+});
+
+var PageMetadataMessenger = {
+ init() {
+ addMessageListener("PageMetadata:GetPageData", this);
+ addMessageListener("PageMetadata:GetMicroformats", this);
+ },
+ receiveMessage(message) {
+ switch (message.name) {
+ case "PageMetadata:GetPageData": {
+ let target = message.objects.target;
+ let result = PageMetadata.getData(content.document, target);
+ sendAsyncMessage("PageMetadata:PageDataResult", result);
+ break;
+ }
+ case "PageMetadata:GetMicroformats": {
+ let target = message.objects.target;
+ let result = PageMetadata.getMicroformats(content.document, target);
+ sendAsyncMessage("PageMetadata:MicroformatsResult", result);
+ break;
+ }
+ }
+ }
+}
+PageMetadataMessenger.init();
+
+addEventListener("ActivateSocialFeature", function (aEvent) {
+ let document = content.document;
+ let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ if (!dwu.isHandlingUserInput) {
+ Cu.reportError("attempt to activate provider without user input from " + document.nodePrincipal.origin);
+ return;
+ }
+
+ let node = aEvent.target;
+ let ownerDocument = node.ownerDocument;
+ let data = node.getAttribute("data-service");
+ if (data) {
+ try {
+ data = JSON.parse(data);
+ } catch (e) {
+ Cu.reportError("Social Service manifest parse error: " + e);
+ return;
+ }
+ } else {
+ Cu.reportError("Social Service manifest not available");
+ return;
+ }
+
+ sendAsyncMessage("Social:Activation", {
+ url: ownerDocument.location.href,
+ origin: ownerDocument.nodePrincipal.origin,
+ manifest: data
+ });
+}, true, true);
+
+addMessageListener("ContextMenu:SaveVideoFrameAsImage", (message) => {
+ let video = message.objects.target;
+ let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+
+ let ctxDraw = canvas.getContext("2d");
+ ctxDraw.drawImage(video, 0, 0);
+ sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", {
+ dataURL: canvas.toDataURL("image/jpeg", ""),
+ });
+});
+
+addMessageListener("ContextMenu:MediaCommand", (message) => {
+ E10SUtils.wrapHandlingUserInput(
+ content, message.data.handlingUserInput,
+ () => {
+ let media = message.objects.element;
+ switch (message.data.command) {
+ case "play":
+ media.play();
+ break;
+ case "pause":
+ media.pause();
+ break;
+ case "loop":
+ media.loop = !media.loop;
+ break;
+ case "mute":
+ media.muted = true;
+ break;
+ case "unmute":
+ media.muted = false;
+ break;
+ case "playbackRate":
+ media.playbackRate = message.data.data;
+ break;
+ case "hidecontrols":
+ media.removeAttribute("controls");
+ break;
+ case "showcontrols":
+ media.setAttribute("controls", "true");
+ break;
+ case "fullscreen":
+ if (content.document.fullscreenEnabled)
+ media.requestFullscreen();
+ break;
+ }
+ });
+});
+
+addMessageListener("ContextMenu:Canvas:ToBlobURL", (message) => {
+ message.objects.target.toBlob((blob) => {
+ let blobURL = URL.createObjectURL(blob);
+ sendAsyncMessage("ContextMenu:Canvas:ToBlobURL:Result", { blobURL });
+ });
+});
+
+addMessageListener("ContextMenu:ReloadFrame", (message) => {
+ message.objects.target.ownerDocument.location.reload();
+});
+
+addMessageListener("ContextMenu:ReloadImage", (message) => {
+ let image = message.objects.target;
+ if (image instanceof Ci.nsIImageLoadingContent)
+ image.forceReload();
+});
+
+addMessageListener("ContextMenu:BookmarkFrame", (message) => {
+ let frame = message.objects.target.ownerDocument;
+ sendAsyncMessage("ContextMenu:BookmarkFrame:Result",
+ { title: frame.title,
+ description: PlacesUIUtils.getDescriptionFromDocument(frame) });
+});
+
+addMessageListener("ContextMenu:SearchFieldBookmarkData", (message) => {
+ let node = message.objects.target;
+
+ let charset = node.ownerDocument.characterSet;
+
+ let formBaseURI = BrowserUtils.makeURI(node.form.baseURI,
+ charset);
+
+ let formURI = BrowserUtils.makeURI(node.form.getAttribute("action"),
+ charset,
+ formBaseURI);
+
+ let spec = formURI.spec;
+
+ let isURLEncoded =
+ (node.form.method.toUpperCase() == "POST"
+ && (node.form.enctype == "application/x-www-form-urlencoded" ||
+ node.form.enctype == ""));
+
+ let title = node.ownerDocument.title;
+ let description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument);
+
+ let formData = [];
+
+ function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) {
+ if (aIsFormUrlEncoded) {
+ return escape(aName + "=" + aValue);
+ }
+ return escape(aName) + "=" + escape(aValue);
+ }
+
+ for (let el of node.form.elements) {
+ if (!el.type) // happens with fieldsets
+ continue;
+
+ if (el == node) {
+ formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) :
+ // Don't escape "%s", just append
+ escapeNameValuePair(el.name, "", false) + "%s");
+ continue;
+ }
+
+ let type = el.type.toLowerCase();
+
+ if (((el instanceof content.HTMLInputElement && el.mozIsTextField(true)) ||
+ type == "hidden" || type == "textarea") ||
+ ((type == "checkbox" || type == "radio") && el.checked)) {
+ formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded));
+ } else if (el instanceof content.HTMLSelectElement && el.selectedIndex >= 0) {
+ for (let j=0; j < el.options.length; j++) {
+ if (el.options[j].selected)
+ formData.push(escapeNameValuePair(el.name, el.options[j].value,
+ isURLEncoded));
+ }
+ }
+ }
+
+ let postData;
+
+ if (isURLEncoded)
+ postData = formData.join("&");
+ else {
+ let separator = spec.includes("?") ? "&" : "?";
+ spec += separator + formData.join("&");
+ }
+
+ sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result",
+ { spec, title, description, postData, charset });
+});
+
+addMessageListener("Bookmarks:GetPageDetails", (message) => {
+ let doc = content.document;
+ let isErrorPage = /^about:(neterror|certerror|blocked)/.test(doc.documentURI);
+ sendAsyncMessage("Bookmarks:GetPageDetails:Result",
+ { isErrorPage: isErrorPage,
+ description: PlacesUIUtils.getDescriptionFromDocument(doc) });
+});
+
+var LightWeightThemeWebInstallListener = {
+ _previewWindow: null,
+
+ init: function() {
+ addEventListener("InstallBrowserTheme", this, false, true);
+ addEventListener("PreviewBrowserTheme", this, false, true);
+ addEventListener("ResetBrowserThemePreview", this, false, true);
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "InstallBrowserTheme": {
+ sendAsyncMessage("LightWeightThemeWebInstaller:Install", {
+ baseURI: event.target.baseURI,
+ themeData: event.target.getAttribute("data-browsertheme"),
+ });
+ break;
+ }
+ case "PreviewBrowserTheme": {
+ sendAsyncMessage("LightWeightThemeWebInstaller:Preview", {
+ baseURI: event.target.baseURI,
+ themeData: event.target.getAttribute("data-browsertheme"),
+ });
+ this._previewWindow = event.target.ownerGlobal;
+ this._previewWindow.addEventListener("pagehide", this, true);
+ break;
+ }
+ case "pagehide": {
+ sendAsyncMessage("LightWeightThemeWebInstaller:ResetPreview");
+ this._resetPreviewWindow();
+ break;
+ }
+ case "ResetBrowserThemePreview": {
+ if (this._previewWindow) {
+ sendAsyncMessage("LightWeightThemeWebInstaller:ResetPreview",
+ {baseURI: event.target.baseURI});
+ this._resetPreviewWindow();
+ }
+ break;
+ }
+ }
+ },
+
+ _resetPreviewWindow: function () {
+ this._previewWindow.removeEventListener("pagehide", this, true);
+ this._previewWindow = null;
+ }
+};
+
+LightWeightThemeWebInstallListener.init();
+
+function disableSetDesktopBackground(aTarget) {
+ // Disable the Set as Desktop Background menu item if we're still trying
+ // to load the image or the load failed.
+ if (!(aTarget instanceof Ci.nsIImageLoadingContent))
+ return true;
+
+ if (("complete" in aTarget) && !aTarget.complete)
+ return true;
+
+ if (aTarget.currentURI.schemeIs("javascript"))
+ return true;
+
+ let request = aTarget.QueryInterface(Ci.nsIImageLoadingContent)
+ .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ if (!request)
+ return true;
+
+ return false;
+}
+
+addMessageListener("ContextMenu:SetAsDesktopBackground", (message) => {
+ let target = message.objects.target;
+
+ // Paranoia: check disableSetDesktopBackground again, in case the
+ // image changed since the context menu was initiated.
+ let disable = disableSetDesktopBackground(target);
+
+ if (!disable) {
+ try {
+ BrowserUtils.urlSecurityCheck(target.currentURI.spec, target.ownerDocument.nodePrincipal);
+ let canvas = content.document.createElement("canvas");
+ canvas.width = target.naturalWidth;
+ canvas.height = target.naturalHeight;
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(target, 0, 0);
+ let dataUrl = canvas.toDataURL();
+ sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
+ { dataUrl });
+ }
+ catch (e) {
+ Cu.reportError(e);
+ disable = true;
+ }
+ }
+
+ if (disable)
+ sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result", { disable });
+});
+
+var PageInfoListener = {
+
+ init: function() {
+ addMessageListener("PageInfo:getData", this);
+ },
+
+ receiveMessage: function(message) {
+ let strings = message.data.strings;
+ let window;
+ let document;
+
+ let frameOuterWindowID = message.data.frameOuterWindowID;
+
+ // If inside frame then get the frame's window and document.
+ if (frameOuterWindowID) {
+ window = Services.wm.getOuterWindowWithId(frameOuterWindowID);
+ document = window.document;
+ }
+ else {
+ window = content.window;
+ document = content.document;
+ }
+
+ let imageElement = message.objects.imageElement;
+
+ let pageInfoData = {metaViewRows: this.getMetaInfo(document),
+ docInfo: this.getDocumentInfo(document),
+ feeds: this.getFeedsInfo(document, strings),
+ windowInfo: this.getWindowInfo(window),
+ imageInfo: this.getImageInfo(imageElement)};
+
+ sendAsyncMessage("PageInfo:data", pageInfoData);
+
+ // Separate step so page info dialog isn't blank while waiting for this to finish.
+ this.getMediaInfo(document, window, strings);
+ },
+
+ getImageInfo: function(imageElement) {
+ let imageInfo = null;
+ if (imageElement) {
+ imageInfo = {
+ currentSrc: imageElement.currentSrc,
+ width: imageElement.width,
+ height: imageElement.height,
+ imageText: imageElement.title || imageElement.alt
+ };
+ }
+ return imageInfo;
+ },
+
+ getMetaInfo: function(document) {
+ let metaViewRows = [];
+
+ // Get the meta tags from the page.
+ let metaNodes = document.getElementsByTagName("meta");
+
+ for (let metaNode of metaNodes) {
+ metaViewRows.push([metaNode.name || metaNode.httpEquiv || metaNode.getAttribute("property"),
+ metaNode.content]);
+ }
+
+ return metaViewRows;
+ },
+
+ getWindowInfo: function(window) {
+ let windowInfo = {};
+ windowInfo.isTopWindow = window == window.top;
+
+ let hostName = null;
+ try {
+ hostName = window.location.host;
+ }
+ catch (exception) { }
+
+ windowInfo.hostName = hostName;
+ return windowInfo;
+ },
+
+ getDocumentInfo: function(document) {
+ let docInfo = {};
+ docInfo.title = document.title;
+ docInfo.location = document.location.toString();
+ docInfo.referrer = document.referrer;
+ docInfo.compatMode = document.compatMode;
+ docInfo.contentType = document.contentType;
+ docInfo.characterSet = document.characterSet;
+ docInfo.lastModified = document.lastModified;
+ docInfo.principal = document.nodePrincipal;
+
+ let documentURIObject = {};
+ documentURIObject.spec = document.documentURIObject.spec;
+ documentURIObject.originCharset = document.documentURIObject.originCharset;
+ docInfo.documentURIObject = documentURIObject;
+
+ docInfo.isContentWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(content);
+
+ return docInfo;
+ },
+
+ getFeedsInfo: function(document, strings) {
+ let feeds = [];
+ // Get the feeds from the page.
+ let linkNodes = document.getElementsByTagName("link");
+ let length = linkNodes.length;
+ for (let i = 0; i < length; i++) {
+ let link = linkNodes[i];
+ if (!link.href) {
+ continue;
+ }
+ let rel = link.rel && link.rel.toLowerCase();
+ let rels = {};
+
+ if (rel) {
+ for (let relVal of rel.split(/\s+/)) {
+ rels[relVal] = true;
+ }
+ }
+
+ if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) {
+ let type = Feeds.isValidFeed(link, document.nodePrincipal, "feed" in rels);
+ if (type) {
+ type = strings[type] || strings["application/rss+xml"];
+ feeds.push([link.title, type, link.href]);
+ }
+ }
+ }
+ return feeds;
+ },
+
+ // Only called once to get the media tab's media elements from the content page.
+ getMediaInfo: function(document, window, strings)
+ {
+ let frameList = this.goThroughFrames(document, window);
+ Task.spawn(() => this.processFrames(document, frameList, strings));
+ },
+
+ goThroughFrames: function(document, window)
+ {
+ let frameList = [document];
+ if (window && window.frames.length > 0) {
+ let num = window.frames.length;
+ for (let i = 0; i < num; i++) {
+ // Recurse through the frames.
+ frameList.concat(this.goThroughFrames(window.frames[i].document,
+ window.frames[i]));
+ }
+ }
+ return frameList;
+ },
+
+ processFrames: function*(document, frameList, strings)
+ {
+ let nodeCount = 0;
+ for (let doc of frameList) {
+ let iterator = doc.createTreeWalker(doc, content.NodeFilter.SHOW_ELEMENT);
+
+ // Goes through all the elements on the doc. imageViewRows takes only the media elements.
+ while (iterator.nextNode()) {
+ let mediaItems = this.getMediaItems(document, strings, iterator.currentNode);
+
+ if (mediaItems.length) {
+ sendAsyncMessage("PageInfo:mediaData",
+ {mediaItems, isComplete: false});
+ }
+
+ if (++nodeCount % 500 == 0) {
+ // setTimeout every 500 elements so we don't keep blocking the content process.
+ yield new Promise(resolve => setTimeout(resolve, 10));
+ }
+ }
+ }
+ // Send that page info media fetching has finished.
+ sendAsyncMessage("PageInfo:mediaData", {isComplete: true});
+ },
+
+ getMediaItems: function(document, strings, elem)
+ {
+ // Check for images defined in CSS (e.g. background, borders)
+ let computedStyle = elem.ownerGlobal.getComputedStyle(elem);
+ // A node can have multiple media items associated with it - for example,
+ // multiple background images.
+ let mediaItems = [];
+
+ let addImage = (url, type, alt, elem, isBg) => {
+ let element = this.serializeElementInfo(document, url, type, alt, elem, isBg);
+ mediaItems.push([url, type, alt, element, isBg]);
+ };
+
+ if (computedStyle) {
+ let addImgFunc = (label, val) => {
+ if (val.primitiveType == content.CSSPrimitiveValue.CSS_URI) {
+ addImage(val.getStringValue(), label, strings.notSet, elem, true);
+ }
+ else if (val.primitiveType == content.CSSPrimitiveValue.CSS_STRING) {
+ // This is for -moz-image-rect.
+ // TODO: Reimplement once bug 714757 is fixed.
+ let strVal = val.getStringValue();
+ if (strVal.search(/^.*url\(\"?/) > -1) {
+ let url = strVal.replace(/^.*url\(\"?/, "").replace(/\"?\).*$/, "");
+ addImage(url, label, strings.notSet, elem, true);
+ }
+ }
+ else if (val.cssValueType == content.CSSValue.CSS_VALUE_LIST) {
+ // Recursively resolve multiple nested CSS value lists.
+ for (let i = 0; i < val.length; i++) {
+ addImgFunc(label, val.item(i));
+ }
+ }
+ };
+
+ addImgFunc(strings.mediaBGImg, computedStyle.getPropertyCSSValue("background-image"));
+ addImgFunc(strings.mediaBorderImg, computedStyle.getPropertyCSSValue("border-image-source"));
+ addImgFunc(strings.mediaListImg, computedStyle.getPropertyCSSValue("list-style-image"));
+ addImgFunc(strings.mediaCursor, computedStyle.getPropertyCSSValue("cursor"));
+ }
+
+ // One swi^H^H^Hif-else to rule them all.
+ if (elem instanceof content.HTMLImageElement) {
+ addImage(elem.src, strings.mediaImg,
+ (elem.hasAttribute("alt")) ? elem.alt : strings.notSet, elem, false);
+ }
+ else if (elem instanceof content.SVGImageElement) {
+ try {
+ // Note: makeURLAbsolute will throw if either the baseURI is not a valid URI
+ // or the URI formed from the baseURI and the URL is not a valid URI.
+ if (elem.href.baseVal) {
+ let href = Services.io.newURI(elem.href.baseVal, null, Services.io.newURI(elem.baseURI)).spec;
+ addImage(href, strings.mediaImg, "", elem, false);
+ }
+ } catch (e) { }
+ }
+ else if (elem instanceof content.HTMLVideoElement) {
+ addImage(elem.currentSrc, strings.mediaVideo, "", elem, false);
+ }
+ else if (elem instanceof content.HTMLAudioElement) {
+ addImage(elem.currentSrc, strings.mediaAudio, "", elem, false);
+ }
+ else if (elem instanceof content.HTMLLinkElement) {
+ if (elem.rel && /\bicon\b/i.test(elem.rel)) {
+ addImage(elem.href, strings.mediaLink, "", elem, false);
+ }
+ }
+ else if (elem instanceof content.HTMLInputElement || elem instanceof content.HTMLButtonElement) {
+ if (elem.type.toLowerCase() == "image") {
+ addImage(elem.src, strings.mediaInput,
+ (elem.hasAttribute("alt")) ? elem.alt : strings.notSet, elem, false);
+ }
+ }
+ else if (elem instanceof content.HTMLObjectElement) {
+ addImage(elem.data, strings.mediaObject, this.getValueText(elem), elem, false);
+ }
+ else if (elem instanceof content.HTMLEmbedElement) {
+ addImage(elem.src, strings.mediaEmbed, "", elem, false);
+ }
+
+ return mediaItems;
+ },
+
+ /**
+ * Set up a JSON element object with all the instanceOf and other infomation that
+ * makePreview in pageInfo.js uses to figure out how to display the preview.
+ */
+
+ serializeElementInfo: function(document, url, type, alt, item, isBG)
+ {
+ let result = {};
+
+ let imageText;
+ if (!isBG &&
+ !(item instanceof content.SVGImageElement) &&
+ !(document instanceof content.ImageDocument)) {
+ imageText = item.title || item.alt;
+
+ if (!imageText && !(item instanceof content.HTMLImageElement)) {
+ imageText = this.getValueText(item);
+ }
+ }
+
+ result.imageText = imageText;
+ result.longDesc = item.longDesc;
+ result.numFrames = 1;
+
+ if (item instanceof content.HTMLObjectElement ||
+ item instanceof content.HTMLEmbedElement ||
+ item instanceof content.HTMLLinkElement) {
+ result.mimeType = item.type;
+ }
+
+ if (!result.mimeType && !isBG && item instanceof Ci.nsIImageLoadingContent) {
+ // Interface for image loading content.
+ let imageRequest = item.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ if (imageRequest) {
+ result.mimeType = imageRequest.mimeType;
+ let image = !(imageRequest.imageStatus & imageRequest.STATUS_ERROR) && imageRequest.image;
+ if (image) {
+ result.numFrames = image.numFrames;
+ }
+ }
+ }
+
+ // If we have a data url, get the MIME type from the url.
+ if (!result.mimeType && url.startsWith("data:")) {
+ let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url);
+ if (dataMimeType)
+ result.mimeType = dataMimeType[1].toLowerCase();
+ }
+
+ result.HTMLLinkElement = item instanceof content.HTMLLinkElement;
+ result.HTMLInputElement = item instanceof content.HTMLInputElement;
+ result.HTMLImageElement = item instanceof content.HTMLImageElement;
+ result.HTMLObjectElement = item instanceof content.HTMLObjectElement;
+ result.SVGImageElement = item instanceof content.SVGImageElement;
+ result.HTMLVideoElement = item instanceof content.HTMLVideoElement;
+ result.HTMLAudioElement = item instanceof content.HTMLAudioElement;
+
+ if (isBG) {
+ // Items that are showing this image as a background
+ // image might not necessarily have a width or height,
+ // so we'll dynamically generate an image and send up the
+ // natural dimensions.
+ let img = content.document.createElement("img");
+ img.src = url;
+ result.naturalWidth = img.naturalWidth;
+ result.naturalHeight = img.naturalHeight;
+ } else {
+ // Otherwise, we can use the current width and height
+ // of the image.
+ result.width = item.width;
+ result.height = item.height;
+ }
+
+ if (item instanceof content.SVGImageElement) {
+ result.SVGImageElementWidth = item.width.baseVal.value;
+ result.SVGImageElementHeight = item.height.baseVal.value;
+ }
+
+ result.baseURI = item.baseURI;
+
+ return result;
+ },
+
+ // Other Misc Stuff
+ // Modified from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html
+ // parse a node to extract the contents of the node
+ getValueText: function(node)
+ {
+
+ let valueText = "";
+
+ // Form input elements don't generally contain information that is useful to our callers, so return nothing.
+ if (node instanceof content.HTMLInputElement ||
+ node instanceof content.HTMLSelectElement ||
+ node instanceof content.HTMLTextAreaElement) {
+ return valueText;
+ }
+
+ // Otherwise recurse for each child.
+ let length = node.childNodes.length;
+
+ for (let i = 0; i < length; i++) {
+ let childNode = node.childNodes[i];
+ let nodeType = childNode.nodeType;
+
+ // Text nodes are where the goods are.
+ if (nodeType == content.Node.TEXT_NODE) {
+ valueText += " " + childNode.nodeValue;
+ }
+ // And elements can have more text inside them.
+ else if (nodeType == content.Node.ELEMENT_NODE) {
+ // Images are special, we want to capture the alt text as if the image weren't there.
+ if (childNode instanceof content.HTMLImageElement) {
+ valueText += " " + this.getAltText(childNode);
+ }
+ else {
+ valueText += " " + this.getValueText(childNode);
+ }
+ }
+ }
+
+ return this.stripWS(valueText);
+ },
+
+ // Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html.
+ // Traverse the tree in search of an img or area element and grab its alt tag.
+ getAltText: function(node)
+ {
+ let altText = "";
+
+ if (node.alt) {
+ return node.alt;
+ }
+ let length = node.childNodes.length;
+ for (let i = 0; i < length; i++) {
+ if ((altText = this.getAltText(node.childNodes[i]) != undefined)) { // stupid js warning...
+ return altText;
+ }
+ }
+ return "";
+ },
+
+ // Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html.
+ // Strip leading and trailing whitespace, and replace multiple consecutive whitespace characters with a single space.
+ stripWS: function(text)
+ {
+ let middleRE = /\s+/g;
+ let endRE = /(^\s+)|(\s+$)/g;
+
+ text = text.replace(middleRE, " ");
+ return text.replace(endRE, "");
+ }
+};
+PageInfoListener.init();
+
+let OfflineApps = {
+ _docId: 0,
+ _docIdMap: new Map(),
+
+ _docManifestSet: new Set(),
+
+ _observerAdded: false,
+ registerWindow(aWindow) {
+ if (!this._observerAdded) {
+ this._observerAdded = true;
+ Services.obs.addObserver(this, "offline-cache-update-completed", true);
+ }
+ let manifestURI = this._getManifestURI(aWindow);
+ this._docManifestSet.add(manifestURI.spec);
+ },
+
+ handleEvent(event) {
+ if (event.type == "MozApplicationManifest") {
+ this.offlineAppRequested(event.originalTarget.defaultView);
+ }
+ },
+
+ _getManifestURI(aWindow) {
+ if (!aWindow.document.documentElement)
+ return null;
+
+ var attr = aWindow.document.documentElement.getAttribute("manifest");
+ if (!attr)
+ return null;
+
+ try {
+ var contentURI = BrowserUtils.makeURI(aWindow.location.href, null, null);
+ return BrowserUtils.makeURI(attr, aWindow.document.characterSet, contentURI);
+ } catch (e) {
+ return null;
+ }
+ },
+
+ offlineAppRequested(aContentWindow) {
+ this.registerWindow(aContentWindow);
+ if (!Services.prefs.getBoolPref("browser.offline-apps.notify")) {
+ return;
+ }
+
+ let currentURI = aContentWindow.document.documentURIObject;
+ // don't bother showing UI if the user has already made a decision
+ if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION)
+ return;
+
+ try {
+ if (Services.prefs.getBoolPref("offline-apps.allow_by_default")) {
+ // all pages can use offline capabilities, no need to ask the user
+ return;
+ }
+ } catch (e) {
+ // this pref isn't set by default, ignore failures
+ }
+ let docId = ++this._docId;
+ this._docIdMap.set(docId, Cu.getWeakReference(aContentWindow.document));
+ sendAsyncMessage("OfflineApps:RequestPermission", {
+ uri: currentURI.spec,
+ docId,
+ });
+ },
+
+ _startFetching(aDocument) {
+ if (!aDocument.documentElement)
+ return;
+
+ let manifestURI = this._getManifestURI(aDocument.defaultView);
+ if (!manifestURI)
+ return;
+
+ var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"].
+ getService(Ci.nsIOfflineCacheUpdateService);
+ updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject,
+ aDocument.nodePrincipal, aDocument.defaultView);
+ },
+
+ receiveMessage(aMessage) {
+ if (aMessage.name == "OfflineApps:StartFetching") {
+ let doc = this._docIdMap.get(aMessage.data.docId);
+ doc = doc && doc.get();
+ if (doc) {
+ this._startFetching(doc);
+ }
+ this._docIdMap.delete(aMessage.data.docId);
+ }
+ },
+
+ observe(aSubject, aTopic, aState) {
+ if (aTopic == "offline-cache-update-completed") {
+ let cacheUpdate = aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate);
+ let uri = cacheUpdate.manifestURI;
+ if (uri && this._docManifestSet.has(uri.spec)) {
+ sendAsyncMessage("OfflineApps:CheckUsage", {uri: uri.spec});
+ }
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+};
+
+addEventListener("MozApplicationManifest", OfflineApps, false);
+addMessageListener("OfflineApps:StartFetching", OfflineApps);
diff --git a/browser/base/content/contentSearchUI.css b/browser/base/content/contentSearchUI.css
new file mode 100644
index 000000000..cd5cf5008
--- /dev/null
+++ b/browser/base/content/contentSearchUI.css
@@ -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/. */
+
+.contentSearchSuggestionTable {
+ background-color: hsla(0,0%,100%,.99);
+ border: 1px solid hsla(0, 0%, 0%, .2);
+ border-top: none;
+ box-shadow: 0 5px 10px hsla(0, 0%, 0%, .1);
+ position: absolute;
+ left: 0;
+ z-index: 1001;
+ -moz-user-select: none;
+ cursor: default;
+}
+
+.contentSearchSuggestionTable:-moz-dir(rtl) {
+ left: auto;
+ right: 0;
+}
+
+.contentSearchSuggestionsList {
+ border-bottom: 1px solid hsl(0, 0%, 92%);
+ width: 100%;
+ height: 100%;
+}
+
+.contentSearchSuggestionTable,
+.contentSearchSuggestionsList {
+ border-spacing: 0;
+ overflow: hidden;
+ padding: 0;
+ margin: 0;
+ text-align: start;
+}
+
+.contentSearchHeaderRow,
+.contentSearchSuggestionRow {
+ margin: 0;
+ max-width: inherit;
+ padding: 0;
+}
+
+.contentSearchHeaderRow > td > img,
+.contentSearchSuggestionRow > td > .historyIcon {
+ margin-inline-end: 8px;
+ margin-bottom: -3px;
+}
+
+.contentSearchSuggestionTable .historyIcon {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ background-image: url("chrome://browser/skin/search-history-icon.svg#search-history-icon");
+}
+
+.contentSearchSuggestionRow.selected > td > .historyIcon {
+ background-image: url("chrome://browser/skin/search-history-icon.svg#search-history-icon-active");
+}
+
+.contentSearchHeader > img {
+ height: 16px;
+ width: 16px;
+ margin: 0;
+ padding: 0;
+}
+
+.contentSearchSuggestionRow.remote > td > .historyIcon {
+ visibility: hidden;
+}
+
+.contentSearchSuggestionRow.selected {
+ background-color: Highlight;
+ color: HighlightText;
+}
+
+.contentSearchHeader,
+.contentSearchSuggestionEntry {
+ margin: 0;
+ max-width: inherit;
+ overflow: hidden;
+ padding: 4px 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 75%;
+}
+
+.contentSearchHeader {
+ background-color: hsl(0, 0%, 97%);
+ color: #666;
+ border-bottom: 1px solid hsl(0, 0%, 92%);
+}
+
+.contentSearchSuggestionsContainer {
+ margin: 0;
+ padding: 0;
+ border-spacing: 0;
+ width: 100%;
+}
+
+.contentSearchSearchWithHeaderSearchText {
+ white-space: pre;
+ font-weight: bold;
+}
+
+.contentSearchOneOffItem {
+ -moz-appearance: none;
+ height: 32px;
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: none;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAWCAYAAAABxvaqAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gofECQNNVW2/AAAABBJREFUGFdjOHPmzH8GehEA/KpKg9YTf4AAAAAASUVORK5CYII=');
+ background-repeat: no-repeat;
+ background-position: right center;
+}
+
+.contentSearchOneOffItem:-moz-dir(rtl) {
+ background-position: left center;
+}
+
+.contentSearchOneOffItem > img {
+ width: 16px;
+ height: 16px;
+ margin-bottom: -2px;
+}
+
+.contentSearchOneOffItem:not(.last-row) {
+ border-bottom: 1px solid hsl(0, 0%, 92%);
+}
+
+.contentSearchOneOffItem.end-of-row {
+ background-image: none;
+}
+
+.contentSearchOneOffItem.selected {
+ background-color: Highlight;
+ background-image: none;
+}
+
+.contentSearchOneOffsTable {
+ width: 100%;
+}
+
+.contentSearchSettingsButton {
+ margin: 0;
+ padding: 0;
+ height: 32px;
+ border: none;
+ border-top: 1px solid hsla(0, 0%, 0%, .08);
+ text-align: center;
+ width: 100%;
+}
+
+.contentSearchSettingsButton.selected {
+ background-color: hsl(0, 0%, 90%);
+}
+
+.contentSearchSettingsButton:active {
+ background-color: hsl(0, 0%, 85%);
+}
diff --git a/browser/base/content/contentSearchUI.js b/browser/base/content/contentSearchUI.js
new file mode 100644
index 000000000..9136ea8f2
--- /dev/null
+++ b/browser/base/content/contentSearchUI.js
@@ -0,0 +1,915 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.ContentSearchUIController = (function () {
+
+const MAX_DISPLAYED_SUGGESTIONS = 6;
+const SUGGESTION_ID_PREFIX = "searchSuggestion";
+const ONE_OFF_ID_PREFIX = "oneOff";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * Creates a new object that manages search suggestions and their UI for a text
+ * box.
+ *
+ * The UI consists of an html:table that's inserted into the DOM after the given
+ * text box and styled so that it appears as a dropdown below the text box.
+ *
+ * @param inputElement
+ * Search suggestions will be based on the text in this text box.
+ * Assumed to be an html:input. xul:textbox is untested but might work.
+ * @param tableParent
+ * The suggestion table is appended as a child to this element. Since
+ * the table is absolutely positioned and its top and left values are set
+ * to be relative to the top and left of the page, either the parent and
+ * all its ancestors should not be positioned elements (i.e., their
+ * positions should be "static"), or the parent's position should be the
+ * top left of the page.
+ * @param healthReportKey
+ * This will be sent with the search data for FHR to record the search.
+ * @param searchPurpose
+ * Sent with search data, see nsISearchEngine.getSubmission.
+ * @param idPrefix
+ * The IDs of elements created by the object will be prefixed with this
+ * string.
+ */
+function ContentSearchUIController(inputElement, tableParent, healthReportKey,
+ searchPurpose, idPrefix="") {
+ this.input = inputElement;
+ this._idPrefix = idPrefix;
+ this._healthReportKey = healthReportKey;
+ this._searchPurpose = searchPurpose;
+
+ let tableID = idPrefix + "searchSuggestionTable";
+ this.input.autocomplete = "off";
+ this.input.setAttribute("aria-autocomplete", "true");
+ this.input.setAttribute("aria-controls", tableID);
+ tableParent.appendChild(this._makeTable(tableID));
+
+ this.input.addEventListener("keypress", this);
+ this.input.addEventListener("input", this);
+ this.input.addEventListener("focus", this);
+ this.input.addEventListener("blur", this);
+ window.addEventListener("ContentSearchService", this);
+
+ this._stickyInputValue = "";
+ this._hideSuggestions();
+
+ this._getSearchEngines();
+ this._getStrings();
+}
+
+ContentSearchUIController.prototype = {
+
+ // The timeout (ms) of the remote suggestions. Corresponds to
+ // SearchSuggestionController.remoteTimeout. Uses
+ // SearchSuggestionController's default timeout if falsey.
+ remoteTimeout: undefined,
+ _oneOffButtons: [],
+ // Setting up the one off buttons causes an uninterruptible reflow. If we
+ // receive the list of engines while the newtab page is loading, this reflow
+ // may regress performance - so we set this flag and only set up the buttons
+ // if it's set when the suggestions table is actually opened.
+ _pendingOneOffRefresh: undefined,
+
+ get defaultEngine() {
+ return this._defaultEngine;
+ },
+
+ set defaultEngine(engine) {
+ if (this._defaultEngine && this._defaultEngine.icon) {
+ URL.revokeObjectURL(this._defaultEngine.icon);
+ }
+ let icon;
+ if (engine.iconBuffer) {
+ icon = this._getFaviconURIFromBuffer(engine.iconBuffer);
+ }
+ else {
+ icon = this._getImageURIForCurrentResolution(
+ "chrome://mozapps/skin/places/defaultFavicon.png");
+ }
+ this._defaultEngine = {
+ name: engine.name,
+ icon: icon,
+ };
+ this._updateDefaultEngineHeader();
+
+ if (engine && document.activeElement == this.input) {
+ this._speculativeConnect();
+ }
+ },
+
+ get engines() {
+ return this._engines;
+ },
+
+ set engines(val) {
+ this._engines = val;
+ this._pendingOneOffRefresh = true;
+ },
+
+ // The selectedIndex is the index of the element with the "selected" class in
+ // the list obtained by concatenating the suggestion rows, one-off buttons, and
+ // search settings button.
+ get selectedIndex() {
+ let allElts = [...this._suggestionsList.children,
+ ...this._oneOffButtons,
+ document.getElementById("contentSearchSettingsButton")];
+ for (let i = 0; i < allElts.length; ++i) {
+ let elt = allElts[i];
+ if (elt.classList.contains("selected")) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ set selectedIndex(idx) {
+ // Update the table's rows, and the input when there is a selection.
+ this._table.removeAttribute("aria-activedescendant");
+ this.input.removeAttribute("aria-activedescendant");
+
+ let allElts = [...this._suggestionsList.children,
+ ...this._oneOffButtons,
+ document.getElementById("contentSearchSettingsButton")];
+ // If we are selecting a suggestion and a one-off is selected, don't deselect it.
+ let excludeIndex = idx < this.numSuggestions && this.selectedButtonIndex > -1 ?
+ this.numSuggestions + this.selectedButtonIndex : -1;
+ for (let i = 0; i < allElts.length; ++i) {
+ let elt = allElts[i];
+ let ariaSelectedElt = i < this.numSuggestions ? elt.firstChild : elt;
+ if (i == idx) {
+ elt.classList.add("selected");
+ ariaSelectedElt.setAttribute("aria-selected", "true");
+ this.input.setAttribute("aria-activedescendant", ariaSelectedElt.id);
+ }
+ else if (i != excludeIndex) {
+ elt.classList.remove("selected");
+ ariaSelectedElt.setAttribute("aria-selected", "false");
+ }
+ }
+ },
+
+ get selectedButtonIndex() {
+ let elts = [...this._oneOffButtons,
+ document.getElementById("contentSearchSettingsButton")];
+ for (let i = 0; i < elts.length; ++i) {
+ if (elts[i].classList.contains("selected")) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ set selectedButtonIndex(idx) {
+ let elts = [...this._oneOffButtons,
+ document.getElementById("contentSearchSettingsButton")];
+ for (let i = 0; i < elts.length; ++i) {
+ let elt = elts[i];
+ if (i == idx) {
+ elt.classList.add("selected");
+ elt.setAttribute("aria-selected", "true");
+ }
+ else {
+ elt.classList.remove("selected");
+ elt.setAttribute("aria-selected", "false");
+ }
+ }
+ },
+
+ get selectedEngineName() {
+ let selectedElt = this._oneOffsTable.querySelector(".selected");
+ if (selectedElt) {
+ return selectedElt.engineName;
+ }
+ return this.defaultEngine.name;
+ },
+
+ get numSuggestions() {
+ return this._suggestionsList.children.length;
+ },
+
+ selectAndUpdateInput: function (idx) {
+ this.selectedIndex = idx;
+ let newValue = this.suggestionAtIndex(idx) || this._stickyInputValue;
+ // Setting the input value when the value has not changed commits the current
+ // IME composition, which we don't want to do.
+ if (this.input.value != newValue) {
+ this.input.value = newValue;
+ }
+ this._updateSearchWithHeader();
+ },
+
+ suggestionAtIndex: function (idx) {
+ let row = this._suggestionsList.children[idx];
+ return row ? row.textContent : null;
+ },
+
+ deleteSuggestionAtIndex: function (idx) {
+ // Only form history suggestions can be deleted.
+ if (this.isFormHistorySuggestionAtIndex(idx)) {
+ let suggestionStr = this.suggestionAtIndex(idx);
+ this._sendMsg("RemoveFormHistoryEntry", suggestionStr);
+ this._suggestionsList.children[idx].remove();
+ this.selectAndUpdateInput(-1);
+ }
+ },
+
+ isFormHistorySuggestionAtIndex: function (idx) {
+ let row = this._suggestionsList.children[idx];
+ return row && row.classList.contains("formHistory");
+ },
+
+ addInputValueToFormHistory: function () {
+ this._sendMsg("AddFormHistoryEntry", this.input.value);
+ },
+
+ handleEvent: function (event) {
+ this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event);
+ },
+
+ _onCommand: function(aEvent) {
+ if (this.selectedButtonIndex == this._oneOffButtons.length) {
+ // Settings button was selected.
+ this._sendMsg("ManageEngines");
+ return;
+ }
+
+ this.search(aEvent);
+
+ if (aEvent) {
+ aEvent.preventDefault();
+ }
+ },
+
+ search: function (aEvent) {
+ if (!this.defaultEngine) {
+ return; // Not initialized yet.
+ }
+
+ let searchText = this.input;
+ let searchTerms;
+ if (this._table.hidden ||
+ aEvent.originalTarget.id == "contentSearchDefaultEngineHeader" ||
+ aEvent instanceof KeyboardEvent) {
+ searchTerms = searchText.value;
+ }
+ else {
+ searchTerms = this.suggestionAtIndex(this.selectedIndex) || searchText.value;
+ }
+ // Send an event that will perform a search and Firefox Health Report will
+ // record that a search from the healthReportKey passed to the constructor.
+ let eventData = {
+ engineName: this.selectedEngineName,
+ searchString: searchTerms,
+ healthReportKey: this._healthReportKey,
+ searchPurpose: this._searchPurpose,
+ originalEvent: {
+ shiftKey: aEvent.shiftKey,
+ ctrlKey: aEvent.ctrlKey,
+ metaKey: aEvent.metaKey,
+ altKey: aEvent.altKey,
+ },
+ };
+ if ("button" in aEvent) {
+ eventData.originalEvent.button = aEvent.button;
+ }
+
+ if (this.suggestionAtIndex(this.selectedIndex)) {
+ eventData.selection = {
+ index: this.selectedIndex,
+ kind: undefined,
+ };
+ if (aEvent instanceof MouseEvent) {
+ eventData.selection.kind = "mouse";
+ } else if (aEvent instanceof KeyboardEvent) {
+ eventData.selection.kind = "key";
+ }
+ }
+
+ this._sendMsg("Search", eventData);
+ this.addInputValueToFormHistory();
+ },
+
+ _onInput: function () {
+ if (!this.input.value) {
+ this._stickyInputValue = "";
+ this._hideSuggestions();
+ }
+ else if (this.input.value != this._stickyInputValue) {
+ // Only fetch new suggestions if the input value has changed.
+ this._getSuggestions();
+ this.selectAndUpdateInput(-1);
+ }
+ this._updateSearchWithHeader();
+ },
+
+ _onKeypress: function (event) {
+ let selectedIndexDelta = 0;
+ let selectedSuggestionDelta = 0;
+ let selectedOneOffDelta = 0;
+
+ switch (event.keyCode) {
+ case event.DOM_VK_UP:
+ if (this._table.hidden) {
+ return;
+ }
+ if (event.getModifierState("Accel")) {
+ if (event.shiftKey) {
+ selectedSuggestionDelta = -1;
+ break;
+ }
+ this._cycleCurrentEngine(true);
+ break;
+ }
+ if (event.altKey) {
+ selectedOneOffDelta = -1;
+ break;
+ }
+ selectedIndexDelta = -1;
+ break;
+ case event.DOM_VK_DOWN:
+ if (this._table.hidden) {
+ this._getSuggestions();
+ return;
+ }
+ if (event.getModifierState("Accel")) {
+ if (event.shiftKey) {
+ selectedSuggestionDelta = 1;
+ break;
+ }
+ this._cycleCurrentEngine(false);
+ break;
+ }
+ if (event.altKey) {
+ selectedOneOffDelta = 1;
+ break;
+ }
+ selectedIndexDelta = 1;
+ break;
+ case event.DOM_VK_TAB:
+ if (this._table.hidden) {
+ return;
+ }
+ // Shift+tab when either the first or no one-off is selected, as well as
+ // tab when the settings button is selected, should change focus as normal.
+ if ((this.selectedButtonIndex <= 0 && event.shiftKey) ||
+ this.selectedButtonIndex == this._oneOffButtons.length && !event.shiftKey) {
+ return;
+ }
+ selectedOneOffDelta = event.shiftKey ? -1 : 1;
+ break;
+ case event.DOM_VK_RIGHT:
+ // Allow normal caret movement until the caret is at the end of the input.
+ if (this.input.selectionStart != this.input.selectionEnd ||
+ this.input.selectionEnd != this.input.value.length) {
+ return;
+ }
+ if (this.numSuggestions && this.selectedIndex >= 0 &&
+ this.selectedIndex < this.numSuggestions) {
+ this.input.value = this.suggestionAtIndex(this.selectedIndex);
+ this.input.setAttribute("selection-index", this.selectedIndex);
+ this.input.setAttribute("selection-kind", "key");
+ } else {
+ // If we didn't select anything, make sure to remove the attributes
+ // in case they were populated last time.
+ this.input.removeAttribute("selection-index");
+ this.input.removeAttribute("selection-kind");
+ }
+ this._stickyInputValue = this.input.value;
+ this._hideSuggestions();
+ return;
+ case event.DOM_VK_RETURN:
+ this._onCommand(event);
+ return;
+ case event.DOM_VK_DELETE:
+ if (this.selectedIndex >= 0) {
+ this.deleteSuggestionAtIndex(this.selectedIndex);
+ }
+ return;
+ case event.DOM_VK_ESCAPE:
+ if (!this._table.hidden) {
+ this._hideSuggestions();
+ }
+ return;
+ default:
+ return;
+ }
+
+ let currentIndex = this.selectedIndex;
+ if (selectedIndexDelta) {
+ let newSelectedIndex = currentIndex + selectedIndexDelta;
+ if (newSelectedIndex < -1) {
+ newSelectedIndex = this.numSuggestions + this._oneOffButtons.length;
+ }
+ // If are moving up from the first one off, we have to deselect the one off
+ // manually because the selectedIndex setter tries to exclude the selected
+ // one-off (which is desirable for accel+shift+up/down).
+ if (currentIndex == this.numSuggestions && selectedIndexDelta == -1) {
+ this.selectedButtonIndex = -1;
+ }
+ this.selectAndUpdateInput(newSelectedIndex);
+ }
+
+ else if (selectedSuggestionDelta) {
+ let newSelectedIndex;
+ if (currentIndex >= this.numSuggestions || currentIndex == -1) {
+ // No suggestion already selected, select the first/last one appropriately.
+ newSelectedIndex = selectedSuggestionDelta == 1 ?
+ 0 : this.numSuggestions - 1;
+ }
+ else {
+ newSelectedIndex = currentIndex + selectedSuggestionDelta;
+ }
+ if (newSelectedIndex >= this.numSuggestions) {
+ newSelectedIndex = -1;
+ }
+ this.selectAndUpdateInput(newSelectedIndex);
+ }
+
+ else if (selectedOneOffDelta) {
+ let newSelectedIndex;
+ let currentButton = this.selectedButtonIndex;
+ if (currentButton == -1 || currentButton == this._oneOffButtons.length) {
+ // No one-off already selected, select the first/last one appropriately.
+ newSelectedIndex = selectedOneOffDelta == 1 ?
+ 0 : this._oneOffButtons.length - 1;
+ }
+ else {
+ newSelectedIndex = currentButton + selectedOneOffDelta;
+ }
+ // Allow selection of the settings button via the tab key.
+ if (newSelectedIndex == this._oneOffButtons.length &&
+ event.keyCode != event.DOM_VK_TAB) {
+ newSelectedIndex = -1;
+ }
+ this.selectedButtonIndex = newSelectedIndex;
+ }
+
+ // Prevent the input's caret from moving.
+ event.preventDefault();
+ },
+
+ _currentEngineIndex: -1,
+ _cycleCurrentEngine: function (aReverse) {
+ if ((this._currentEngineIndex == this._engines.length - 1 && !aReverse) ||
+ (this._currentEngineIndex == 0 && aReverse)) {
+ return;
+ }
+ this._currentEngineIndex += aReverse ? -1 : 1;
+ let engineName = this._engines[this._currentEngineIndex].name;
+ this._sendMsg("SetCurrentEngine", engineName);
+ },
+
+ _onFocus: function () {
+ if (this._mousedown) {
+ return;
+ }
+ // When the input box loses focus to something in our table, we refocus it
+ // immediately. This causes the focus highlight to flicker, so we set a
+ // custom attribute which consumers should use for focus highlighting. This
+ // attribute is removed only when we do not immediately refocus the input
+ // box, thus eliminating flicker.
+ this.input.setAttribute("keepfocus", "true");
+ this._speculativeConnect();
+ },
+
+ _onBlur: function () {
+ if (this._mousedown) {
+ // At this point, this.input has lost focus, but a new element has not yet
+ // received it. If we re-focus this.input directly, the new element will
+ // steal focus immediately, so we queue it instead.
+ setTimeout(() => this.input.focus(), 0);
+ return;
+ }
+ this.input.removeAttribute("keepfocus");
+ this._hideSuggestions();
+ },
+
+ _onMousemove: function (event) {
+ let idx = this._indexOfTableItem(event.target);
+ if (idx >= this.numSuggestions) {
+ this.selectedButtonIndex = idx - this.numSuggestions;
+ return;
+ }
+ this.selectedIndex = idx;
+ },
+
+ _onMouseup: function (event) {
+ if (event.button == 2) {
+ return;
+ }
+ this._onCommand(event);
+ },
+
+ _onMouseout: function (event) {
+ // We only deselect one-off buttons and the settings button when they are
+ // moused out.
+ let idx = this._indexOfTableItem(event.originalTarget);
+ if (idx >= this.numSuggestions) {
+ this.selectedButtonIndex = -1;
+ }
+ },
+
+ _onClick: function (event) {
+ this._onMouseup(event);
+ },
+
+ _onContentSearchService: function (event) {
+ let methodName = "_onMsg" + event.detail.type;
+ if (methodName in this) {
+ this[methodName](event.detail.data);
+ }
+ },
+
+ _onMsgFocusInput: function (event) {
+ this.input.focus();
+ },
+
+ _onMsgSuggestions: function (suggestions) {
+ // Ignore the suggestions if their search string or engine doesn't match
+ // ours. Due to the async nature of message passing, this can easily happen
+ // when the user types quickly.
+ if (this._stickyInputValue != suggestions.searchString ||
+ this.defaultEngine.name != suggestions.engineName) {
+ return;
+ }
+
+ this._clearSuggestionRows();
+
+ // Position and size the table.
+ let { left } = this.input.getBoundingClientRect();
+ this._table.style.top = this.input.offsetHeight + "px";
+ this._table.style.minWidth = this.input.offsetWidth + "px";
+ this._table.style.maxWidth = (window.innerWidth - left - 40) + "px";
+
+ // Add the suggestions to the table.
+ let searchWords =
+ new Set(suggestions.searchString.trim().toLowerCase().split(/\s+/));
+ for (let i = 0; i < MAX_DISPLAYED_SUGGESTIONS; i++) {
+ let type, idx;
+ if (i < suggestions.formHistory.length) {
+ [type, idx] = ["formHistory", i];
+ }
+ else {
+ let j = i - suggestions.formHistory.length;
+ if (j < suggestions.remote.length) {
+ [type, idx] = ["remote", j];
+ }
+ else {
+ break;
+ }
+ }
+ this._suggestionsList.appendChild(
+ this._makeTableRow(type, suggestions[type][idx], i, searchWords));
+ }
+
+ if (this._table.hidden) {
+ this.selectedIndex = -1;
+ if (this._pendingOneOffRefresh) {
+ this._setUpOneOffButtons();
+ delete this._pendingOneOffRefresh;
+ }
+ this._currentEngineIndex =
+ this._engines.findIndex(aEngine => aEngine.name == this.defaultEngine.name);
+ this._table.hidden = false;
+ this.input.setAttribute("aria-expanded", "true");
+ this._originalDefaultEngine = {
+ name: this.defaultEngine.name,
+ icon: this.defaultEngine.icon,
+ };
+ }
+ },
+
+ _onMsgSuggestionsCancelled: function () {
+ if (!this._table.hidden) {
+ this._hideSuggestions();
+ }
+ },
+
+ _onMsgState: function (state) {
+ this.engines = state.engines;
+ // No point updating the default engine (and the header) if there's no change.
+ if (this.defaultEngine &&
+ this.defaultEngine.name == state.currentEngine.name &&
+ this.defaultEngine.icon == state.currentEngine.icon) {
+ return;
+ }
+ this.defaultEngine = state.currentEngine;
+ },
+
+ _onMsgCurrentState: function (state) {
+ this._onMsgState(state);
+ },
+
+ _onMsgCurrentEngine: function (engine) {
+ this.defaultEngine = engine;
+ this._pendingOneOffRefresh = true;
+ },
+
+ _onMsgStrings: function (strings) {
+ this._strings = strings;
+ this._updateDefaultEngineHeader();
+ this._updateSearchWithHeader();
+ document.getElementById("contentSearchSettingsButton").textContent =
+ this._strings.searchSettings;
+ this.input.setAttribute("placeholder", this._strings.searchPlaceholder);
+ },
+
+ _updateDefaultEngineHeader: function () {
+ let header = document.getElementById("contentSearchDefaultEngineHeader");
+ header.firstChild.setAttribute("src", this.defaultEngine.icon);
+ if (!this._strings) {
+ return;
+ }
+ while (header.firstChild.nextSibling) {
+ header.firstChild.nextSibling.remove();
+ }
+ header.appendChild(document.createTextNode(
+ this._strings.searchHeader.replace("%S", this.defaultEngine.name)));
+ },
+
+ _updateSearchWithHeader: function () {
+ if (!this._strings) {
+ return;
+ }
+ let searchWithHeader = document.getElementById("contentSearchSearchWithHeader");
+ if (this.input.value) {
+ searchWithHeader.innerHTML = this._strings.searchForSomethingWith;
+ searchWithHeader.querySelector('.contentSearchSearchWithHeaderSearchText').textContent = this.input.value;
+ } else {
+ searchWithHeader.textContent = this._strings.searchWithHeader;
+ }
+ },
+
+ _speculativeConnect: function () {
+ if (this.defaultEngine) {
+ this._sendMsg("SpeculativeConnect", this.defaultEngine.name);
+ }
+ },
+
+ _makeTableRow: function (type, suggestionStr, currentRow, searchWords) {
+ let row = document.createElementNS(HTML_NS, "tr");
+ row.dir = "auto";
+ row.classList.add("contentSearchSuggestionRow");
+ row.classList.add(type);
+ row.setAttribute("role", "presentation");
+ row.addEventListener("mousemove", this);
+ row.addEventListener("mouseup", this);
+
+ let entry = document.createElementNS(HTML_NS, "td");
+ let img = document.createElementNS(HTML_NS, "div");
+ img.setAttribute("class", "historyIcon");
+ entry.appendChild(img);
+ entry.classList.add("contentSearchSuggestionEntry");
+ entry.setAttribute("role", "option");
+ entry.id = this._idPrefix + SUGGESTION_ID_PREFIX + currentRow;
+ entry.setAttribute("aria-selected", "false");
+
+ let suggestionWords = suggestionStr.trim().toLowerCase().split(/\s+/);
+ for (let i = 0; i < suggestionWords.length; i++) {
+ let word = suggestionWords[i];
+ let wordSpan = document.createElementNS(HTML_NS, "span");
+ if (searchWords.has(word)) {
+ wordSpan.classList.add("typed");
+ }
+ wordSpan.textContent = word;
+ entry.appendChild(wordSpan);
+ if (i < suggestionWords.length - 1) {
+ entry.appendChild(document.createTextNode(" "));
+ }
+ }
+
+ row.appendChild(entry);
+ return row;
+ },
+
+ // Converts favicon array buffer into a data URI.
+ _getFaviconURIFromBuffer: function (buffer) {
+ let blob = new Blob([buffer]);
+ return URL.createObjectURL(blob);
+ },
+
+ // Adds "@2x" to the name of the given PNG url for "retina" screens.
+ _getImageURIForCurrentResolution: function (uri) {
+ if (window.devicePixelRatio > 1) {
+ return uri.replace(/\.png$/, "@2x.png");
+ }
+ return uri;
+ },
+
+ _getSearchEngines: function () {
+ this._sendMsg("GetState");
+ },
+
+ _getStrings: function () {
+ this._sendMsg("GetStrings");
+ },
+
+ _getSuggestions: function () {
+ this._stickyInputValue = this.input.value;
+ if (this.defaultEngine) {
+ this._sendMsg("GetSuggestions", {
+ engineName: this.defaultEngine.name,
+ searchString: this.input.value,
+ remoteTimeout: this.remoteTimeout,
+ });
+ }
+ },
+
+ _clearSuggestionRows: function() {
+ while (this._suggestionsList.firstElementChild) {
+ this._suggestionsList.firstElementChild.remove();
+ }
+ },
+
+ _hideSuggestions: function () {
+ this.input.setAttribute("aria-expanded", "false");
+ this.selectedIndex = -1;
+ this.selectedButtonIndex = -1;
+ this._currentEngineIndex = -1;
+ this._table.hidden = true;
+ },
+
+ _indexOfTableItem: function (elt) {
+ if (elt.classList.contains("contentSearchOneOffItem")) {
+ return this.numSuggestions + this._oneOffButtons.indexOf(elt);
+ }
+ if (elt.classList.contains("contentSearchSettingsButton")) {
+ return this.numSuggestions + this._oneOffButtons.length;
+ }
+ while (elt && elt.localName != "tr") {
+ elt = elt.parentNode;
+ }
+ if (!elt) {
+ throw new Error("Element is not a row");
+ }
+ return elt.rowIndex;
+ },
+
+ _makeTable: function (id) {
+ this._table = document.createElementNS(HTML_NS, "table");
+ this._table.id = id;
+ this._table.hidden = true;
+ this._table.classList.add("contentSearchSuggestionTable");
+ this._table.setAttribute("role", "presentation");
+
+ // When the search input box loses focus, we want to immediately give focus
+ // back to it if the blur was because the user clicked somewhere in the table.
+ // onBlur uses the _mousedown flag to detect this.
+ this._table.addEventListener("mousedown", () => { this._mousedown = true; });
+ document.addEventListener("mouseup", () => { delete this._mousedown; });
+
+ // Deselect the selected element on mouseout if it wasn't a suggestion.
+ this._table.addEventListener("mouseout", this);
+
+ // If a search is loaded in the same tab, ensure the suggestions dropdown
+ // is hidden immediately when the page starts loading and not when it first
+ // appears, in order to provide timely feedback to the user.
+ window.addEventListener("beforeunload", () => { this._hideSuggestions(); });
+
+ let headerRow = document.createElementNS(HTML_NS, "tr");
+ let header = document.createElementNS(HTML_NS, "td");
+ headerRow.setAttribute("class", "contentSearchHeaderRow");
+ header.setAttribute("class", "contentSearchHeader");
+ let iconImg = document.createElementNS(HTML_NS, "img");
+ header.appendChild(iconImg);
+ header.id = "contentSearchDefaultEngineHeader";
+ headerRow.appendChild(header);
+ headerRow.addEventListener("click", this);
+ this._table.appendChild(headerRow);
+
+ let row = document.createElementNS(HTML_NS, "tr");
+ row.setAttribute("class", "contentSearchSuggestionsContainer");
+ let cell = document.createElementNS(HTML_NS, "td");
+ cell.setAttribute("class", "contentSearchSuggestionsContainer");
+ this._suggestionsList = document.createElementNS(HTML_NS, "table");
+ this._suggestionsList.setAttribute("class", "contentSearchSuggestionsList");
+ cell.appendChild(this._suggestionsList);
+ row.appendChild(cell);
+ this._table.appendChild(row);
+ this._suggestionsList.setAttribute("role", "listbox");
+
+ this._oneOffsTable = document.createElementNS(HTML_NS, "table");
+ this._oneOffsTable.setAttribute("class", "contentSearchOneOffsTable");
+ this._oneOffsTable.classList.add("contentSearchSuggestionsContainer");
+ this._oneOffsTable.setAttribute("role", "group");
+ this._table.appendChild(this._oneOffsTable);
+
+ headerRow = document.createElementNS(HTML_NS, "tr");
+ header = document.createElementNS(HTML_NS, "td");
+ headerRow.setAttribute("class", "contentSearchHeaderRow");
+ header.setAttribute("class", "contentSearchHeader");
+ headerRow.appendChild(header);
+ header.id = "contentSearchSearchWithHeader";
+ this._oneOffsTable.appendChild(headerRow);
+
+ let button = document.createElementNS(HTML_NS, "button");
+ button.setAttribute("class", "contentSearchSettingsButton");
+ button.classList.add("contentSearchHeaderRow");
+ button.classList.add("contentSearchHeader");
+ button.id = "contentSearchSettingsButton";
+ button.addEventListener("click", this);
+ button.addEventListener("mousemove", this);
+ this._table.appendChild(button);
+
+ return this._table;
+ },
+
+ _setUpOneOffButtons: function () {
+ // Sometimes we receive a CurrentEngine message from the ContentSearch service
+ // before we've received a State message - i.e. before we have our engines.
+ if (!this._engines) {
+ return;
+ }
+
+ while (this._oneOffsTable.firstChild.nextSibling) {
+ this._oneOffsTable.firstChild.nextSibling.remove();
+ }
+
+ this._oneOffButtons = [];
+
+ let engines = this._engines.filter(aEngine => aEngine.name != this.defaultEngine.name)
+ .filter(aEngine => !aEngine.hidden);
+ if (!engines.length) {
+ this._oneOffsTable.hidden = true;
+ return;
+ }
+
+ const kDefaultButtonWidth = 49; // 48px + 1px border.
+ let rowWidth = this.input.offsetWidth - 2; // 2px border.
+ let enginesPerRow = Math.floor(rowWidth / kDefaultButtonWidth);
+ let buttonWidth = Math.floor(rowWidth / enginesPerRow);
+
+ let row = document.createElementNS(HTML_NS, "tr");
+ let cell = document.createElementNS(HTML_NS, "td");
+ row.setAttribute("class", "contentSearchSuggestionsContainer");
+ cell.setAttribute("class", "contentSearchSuggestionsContainer");
+
+ for (let i = 0; i < engines.length; ++i) {
+ let engine = engines[i];
+ if (i > 0 && i % enginesPerRow == 0) {
+ row.appendChild(cell);
+ this._oneOffsTable.appendChild(row);
+ row = document.createElementNS(HTML_NS, "tr");
+ cell = document.createElementNS(HTML_NS, "td");
+ row.setAttribute("class", "contentSearchSuggestionsContainer");
+ cell.setAttribute("class", "contentSearchSuggestionsContainer");
+ }
+ let button = document.createElementNS(HTML_NS, "button");
+ button.setAttribute("class", "contentSearchOneOffItem");
+ let img = document.createElementNS(HTML_NS, "img");
+ let uri;
+ if (engine.iconBuffer) {
+ uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
+ }
+ else {
+ uri = this._getImageURIForCurrentResolution(
+ "chrome://browser/skin/search-engine-placeholder.png");
+ }
+ img.setAttribute("src", uri);
+ img.addEventListener("load", function imgLoad() {
+ img.removeEventListener("load", imgLoad);
+ URL.revokeObjectURL(uri);
+ });
+ button.appendChild(img);
+ button.style.width = buttonWidth + "px";
+ button.setAttribute("title", engine.name);
+
+ button.engineName = engine.name;
+ button.addEventListener("click", this);
+ button.addEventListener("mousemove", this);
+
+ if (engines.length - i <= enginesPerRow - (i % enginesPerRow)) {
+ button.classList.add("last-row");
+ }
+
+ if ((i + 1) % enginesPerRow == 0) {
+ button.classList.add("end-of-row");
+ }
+
+ button.id = ONE_OFF_ID_PREFIX + i;
+ cell.appendChild(button);
+ this._oneOffButtons.push(button);
+ }
+ row.appendChild(cell);
+ this._oneOffsTable.appendChild(row);
+ this._oneOffsTable.hidden = false;
+ },
+
+ _sendMsg: function (type, data=null) {
+ dispatchEvent(new CustomEvent("ContentSearchClient", {
+ detail: {
+ type: type,
+ data: data,
+ },
+ }));
+ },
+};
+
+return ContentSearchUIController;
+})();
diff --git a/browser/base/content/defaultthemes/1.footer.jpg b/browser/base/content/defaultthemes/1.footer.jpg
new file mode 100644
index 000000000..cb5ff2705
--- /dev/null
+++ b/browser/base/content/defaultthemes/1.footer.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/1.header.jpg b/browser/base/content/defaultthemes/1.header.jpg
new file mode 100644
index 000000000..58c52f86a
--- /dev/null
+++ b/browser/base/content/defaultthemes/1.header.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/1.icon.jpg b/browser/base/content/defaultthemes/1.icon.jpg
new file mode 100644
index 000000000..67b316d9f
--- /dev/null
+++ b/browser/base/content/defaultthemes/1.icon.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/1.preview.jpg b/browser/base/content/defaultthemes/1.preview.jpg
new file mode 100644
index 000000000..1394c5936
--- /dev/null
+++ b/browser/base/content/defaultthemes/1.preview.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/2.footer.jpg b/browser/base/content/defaultthemes/2.footer.jpg
new file mode 100644
index 000000000..a8cce0ef8
--- /dev/null
+++ b/browser/base/content/defaultthemes/2.footer.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/2.header.jpg b/browser/base/content/defaultthemes/2.header.jpg
new file mode 100644
index 000000000..8a4aec353
--- /dev/null
+++ b/browser/base/content/defaultthemes/2.header.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/2.icon.jpg b/browser/base/content/defaultthemes/2.icon.jpg
new file mode 100644
index 000000000..4eeed30ca
--- /dev/null
+++ b/browser/base/content/defaultthemes/2.icon.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/2.preview.jpg b/browser/base/content/defaultthemes/2.preview.jpg
new file mode 100644
index 000000000..cc45cfc94
--- /dev/null
+++ b/browser/base/content/defaultthemes/2.preview.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/3.footer.png b/browser/base/content/defaultthemes/3.footer.png
new file mode 100644
index 000000000..235a5ad54
--- /dev/null
+++ b/browser/base/content/defaultthemes/3.footer.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/3.header.png b/browser/base/content/defaultthemes/3.header.png
new file mode 100644
index 000000000..b25d673c4
--- /dev/null
+++ b/browser/base/content/defaultthemes/3.header.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/3.icon.png b/browser/base/content/defaultthemes/3.icon.png
new file mode 100644
index 000000000..186519d3e
--- /dev/null
+++ b/browser/base/content/defaultthemes/3.icon.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/3.preview.png b/browser/base/content/defaultthemes/3.preview.png
new file mode 100644
index 000000000..46850f139
--- /dev/null
+++ b/browser/base/content/defaultthemes/3.preview.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/4.footer.png b/browser/base/content/defaultthemes/4.footer.png
new file mode 100644
index 000000000..bd944d58b
--- /dev/null
+++ b/browser/base/content/defaultthemes/4.footer.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/4.header.png b/browser/base/content/defaultthemes/4.header.png
new file mode 100644
index 000000000..1487ff10e
--- /dev/null
+++ b/browser/base/content/defaultthemes/4.header.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/4.icon.png b/browser/base/content/defaultthemes/4.icon.png
new file mode 100644
index 000000000..8dd688ef1
--- /dev/null
+++ b/browser/base/content/defaultthemes/4.icon.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/4.preview.png b/browser/base/content/defaultthemes/4.preview.png
new file mode 100644
index 000000000..36ac2a0bf
--- /dev/null
+++ b/browser/base/content/defaultthemes/4.preview.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/5.footer.png b/browser/base/content/defaultthemes/5.footer.png
new file mode 100644
index 000000000..8e87c69a0
--- /dev/null
+++ b/browser/base/content/defaultthemes/5.footer.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/5.header.png b/browser/base/content/defaultthemes/5.header.png
new file mode 100644
index 000000000..8e87c69a0
--- /dev/null
+++ b/browser/base/content/defaultthemes/5.header.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/5.icon.jpg b/browser/base/content/defaultthemes/5.icon.jpg
new file mode 100644
index 000000000..b3e103ee5
--- /dev/null
+++ b/browser/base/content/defaultthemes/5.icon.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/5.preview.jpg b/browser/base/content/defaultthemes/5.preview.jpg
new file mode 100644
index 000000000..78c2f1248
--- /dev/null
+++ b/browser/base/content/defaultthemes/5.preview.jpg
Binary files differ
diff --git a/browser/base/content/defaultthemes/devedition.header.png b/browser/base/content/defaultthemes/devedition.header.png
new file mode 100644
index 000000000..e4e8dcaa3
--- /dev/null
+++ b/browser/base/content/defaultthemes/devedition.header.png
Binary files differ
diff --git a/browser/base/content/defaultthemes/devedition.icon.png b/browser/base/content/defaultthemes/devedition.icon.png
new file mode 100644
index 000000000..04cfba796
--- /dev/null
+++ b/browser/base/content/defaultthemes/devedition.icon.png
Binary files differ
diff --git a/browser/base/content/docs/sslerrorreport/dataformat.rst b/browser/base/content/docs/sslerrorreport/dataformat.rst
new file mode 100644
index 000000000..f69dc7417
--- /dev/null
+++ b/browser/base/content/docs/sslerrorreport/dataformat.rst
@@ -0,0 +1,54 @@
+.. _sslerrorreport_dataformat:
+
+==============
+Payload Format
+==============
+
+An example report::
+
+ {
+ "hostname":"example.com",
+ "port":443,
+ "timestamp":1413490449,
+ "errorCode":-16384,
+ "failedCertChain":[
+ ],
+ "userAgent":"Mozilla/5.0 (X11; Linux x86_64; rv:36.0) Gecko/20100101 Firefox/36.0",
+ "version":1,
+ "build":"20141022164419",
+ "product":"Firefox",
+ "channel":"default"
+ }
+
+Where the data represents the following:
+
+"hostname"
+ The name of the host the connection was being made to.
+
+"port"
+ The TCP port the connection was being made to.
+
+"timestamp"
+ The (local) time at which the report was generated. Seconds since 1 Jan 1970,
+ UTC.
+
+"errorCode"
+ The error code. This is the error code from certificate verification. Here's a small list of the most commonly-encountered errors:
+ https://wiki.mozilla.org/SecurityEngineering/x509Certs#Error_Codes_in_Firefox
+ In theory many of the errors from sslerr.h, secerr.h, and pkixnss.h could be encountered. We're starting with just MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE, which means that key pinning failed (i.e. there wasn't an intersection between the keys in any computed trusted certificate chain and the expected list of keys for the domain the user is attempting to connect to).
+
+"failedCertChain"
+ The certificate chain which caused the pinning violation (array of base64
+ encoded PEM)
+
+"user agent"
+ The user agent string of the browser sending the report
+
+"build"
+ The build ID
+
+"product"
+ The product name
+
+"channel"
+ The user's release channel
diff --git a/browser/base/content/docs/sslerrorreport/index.rst b/browser/base/content/docs/sslerrorreport/index.rst
new file mode 100644
index 000000000..2c4210113
--- /dev/null
+++ b/browser/base/content/docs/sslerrorreport/index.rst
@@ -0,0 +1,15 @@
+.. _sslerrorreport:
+
+===================
+SSL Error Reporting
+===================
+
+With the introduction of HPKP, it becomes useful to be able to capture data
+on pin violations. SSL Error Reporting is an opt-in mechanism to allow users
+to send data on such violations to mozilla.
+
+.. toctree::
+ :maxdepth: 1
+
+ dataformat
+ preferences
diff --git a/browser/base/content/docs/sslerrorreport/preferences.rst b/browser/base/content/docs/sslerrorreport/preferences.rst
new file mode 100644
index 000000000..ed6f384c2
--- /dev/null
+++ b/browser/base/content/docs/sslerrorreport/preferences.rst
@@ -0,0 +1,23 @@
+.. _healthreport_preferences:
+
+===========
+Preferences
+===========
+
+The following preferences are used by SSL Error reporting:
+
+"security.ssl.errorReporting.enabled"
+ Should the SSL Error Reporting UI be shown on pin violations? Default
+ value: ``true``
+
+"security.ssl.errorReporting.url"
+ Where should SSL error reports be sent? Default value:
+ ``https://incoming.telemetry.mozilla.org/submit/sslreports/``
+
+"security.ssl.errorReporting.automatic"
+ Should error reports be sent without user interaction. Default value:
+ ``false``. Note: this pref is overridden by the value of
+ ``security.ssl.errorReporting.enabled``
+ This is only set when specifically requested by the user. The user can set
+ this value (or unset it) by checking the "Automatically report errors in the
+ future" checkbox when about:neterror is displayed for SSL Errors.
diff --git a/browser/base/content/downloadManagerOverlay.xul b/browser/base/content/downloadManagerOverlay.xul
new file mode 100644
index 000000000..9987820cb
--- /dev/null
+++ b/browser/base/content/downloadManagerOverlay.xul
@@ -0,0 +1,32 @@
+<?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/.
+
+<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?>
+
+<overlay id="downloadManagerOverlay"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<window id="downloadManager">
+
+#include browserMountPoints.inc
+
+<script type="application/javascript"><![CDATA[
+ window.addEventListener("load", function(event) {
+ // Bug 405696: Map Edit -> Find command to the download manager's command
+ var findMenuItem = document.getElementById("menu_find");
+ findMenuItem.setAttribute("command", "cmd_findDownload");
+ findMenuItem.setAttribute("key", "key_findDownload");
+
+ // Bug 429614: Map Edit -> Select All command to download manager's command
+ let selectAllMenuItem = document.getElementById("menu_selectAll");
+ selectAllMenuItem.setAttribute("command", "cmd_selectAllDownloads");
+ selectAllMenuItem.setAttribute("key", "key_selectAllDownloads");
+ }, false);
+]]></script>
+
+</window>
+
+</overlay>
diff --git a/browser/base/content/gcli_sec_bad.svg b/browser/base/content/gcli_sec_bad.svg
new file mode 100644
index 000000000..4f440eb6b
--- /dev/null
+++ b/browser/base/content/gcli_sec_bad.svg
@@ -0,0 +1,7 @@
+<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg">
+ <circle cx="15" cy="15" r="15" fill="#e74c3c"/>
+ <g stroke="white" stroke-width="3">
+ <line x1="9" y1="9" x2="21" y2="21"/>
+ <line x1="21" y1="9" x2="9" y2="21"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/browser/base/content/gcli_sec_good.svg b/browser/base/content/gcli_sec_good.svg
new file mode 100644
index 000000000..f1b33d073
--- /dev/null
+++ b/browser/base/content/gcli_sec_good.svg
@@ -0,0 +1,4 @@
+<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
+ <circle cx="30" cy="30" r="30" fill="#2CBB0F"/>
+ <polygon points="17,32 25,39 26,39 44,18 45,18 48,21 27,46 25,46 14,36 13,36 16,33 16,32" fill="white"/>
+</svg> \ No newline at end of file
diff --git a/browser/base/content/gcli_sec_moderate.svg b/browser/base/content/gcli_sec_moderate.svg
new file mode 100644
index 000000000..3a88aa468
--- /dev/null
+++ b/browser/base/content/gcli_sec_moderate.svg
@@ -0,0 +1,4 @@
+<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg">
+ <circle cx="15" cy="15" r="15" fill="#F5B400"/>
+ <rect x="7.5" y="13" width="15" height="4" fill="white"/>
+</svg> \ No newline at end of file
diff --git a/browser/base/content/global-scripts.inc b/browser/base/content/global-scripts.inc
new file mode 100755
index 000000000..dac75878d
--- /dev/null
+++ b/browser/base/content/global-scripts.inc
@@ -0,0 +1,38 @@
+# -*- 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/.
+
+<script type="application/javascript" src="chrome://global/content/printUtils.js"/>
+<script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
+<script type="application/javascript" src="chrome://browser/content/places/browserPlacesViews.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser.js"/>
+<script type="application/javascript" src="chrome://browser/content/customizableui/panelUI.js"/>
+<script type="application/javascript" src="chrome://global/content/viewSourceUtils.js"/>
+
+<script type="application/javascript" src="chrome://browser/content/browser-addons.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-captivePortal.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-ctrlTab.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-customization.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-devedition.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-feeds.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-fullScreenAndPointerLock.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-fullZoom.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-gestureSupport.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-media.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-plugins.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-refreshblocker.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-safebrowsing.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-sidebar.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-syncui.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-tabsintitlebar.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-thumbnails.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-trackingprotection.js"/>
+
+#ifdef MOZ_DATA_REPORTING
+<script type="application/javascript" src="chrome://browser/content/browser-data-submission-info-bar.js"/>
+#endif
+
+<script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
diff --git a/browser/base/content/hiddenWindow.xul b/browser/base/content/hiddenWindow.xul
new file mode 100644
index 000000000..c708071cd
--- /dev/null
+++ b/browser/base/content/hiddenWindow.xul
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+# -*- Mode: HTML -*-
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifdef XP_MACOSX
+<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?>
+<?xml-stylesheet href="chrome://browser/skin/webRTC-indicator.css" type="text/css"?>
+
+<window id="main-window"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+#include browserMountPoints.inc
+
+</window>
+
+#endif
diff --git a/browser/base/content/macBrowserOverlay.xul b/browser/base/content/macBrowserOverlay.xul
new file mode 100644
index 000000000..4b2cb0d89
--- /dev/null
+++ b/browser/base/content/macBrowserOverlay.xul
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+# -*- Mode: HTML -*-
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#define MAC_NON_BROWSER_WINDOW
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+# All DTD information is stored in a separate file so that it can be shared by
+# hiddenWindow.xul.
+#include browser-doctype.inc
+
+<overlay id="hidden-overlay"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+# All JS files which are not content (only) dependent that browser.xul
+# wishes to include *must* go into the global-scripts.inc file
+# so that they can be shared by this overlay.
+#include global-scripts.inc
+
+<script type="application/javascript">
+ function OpenBrowserWindowFromDockMenu(options) {
+ let win = OpenBrowserWindow(options);
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener);
+ let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"]
+ .getService(Ci.nsIMacDockSupport);
+ dockSupport.activateApplication(true);
+ });
+
+ return win;
+ }
+
+ addEventListener("load", function() { gBrowserInit.nonBrowserWindowStartup() }, false);
+ addEventListener("unload", function() { gBrowserInit.nonBrowserWindowShutdown() }, false);
+</script>
+
+# All sets except for popupsets (commands, keys, stringbundles and broadcasters) *must* go into the
+# browser-sets.inc file for sharing with hiddenWindow.xul.
+#include browser-sets.inc
+
+# The entire main menubar is placed into browser-menubar.inc, so that it can be shared by
+# hiddenWindow.xul.
+#include browser-menubar.inc
+
+<!-- Dock menu -->
+<popupset>
+ <menupopup id="menu_mac_dockmenu">
+ <!-- The command cannot be cmd_newNavigator because we need to activate
+ the application. -->
+ <menuitem label="&newNavigatorCmd.label;" oncommand="OpenBrowserWindowFromDockMenu();"
+ id="macDockMenuNewWindow" />
+ <menuitem label="&newPrivateWindow.label;" oncommand="OpenBrowserWindowFromDockMenu({private: true});" />
+ </menupopup>
+</popupset>
+
+</overlay>
diff --git a/browser/base/content/newtab/alternativeDefaultSites.json b/browser/base/content/newtab/alternativeDefaultSites.json
new file mode 100644
index 000000000..018d3edcc
--- /dev/null
+++ b/browser/base/content/newtab/alternativeDefaultSites.json
@@ -0,0 +1,50 @@
+{
+ "directory": [
+ {
+ "bgColor": "#ffffff",
+ "directoryId": 10000000,
+ "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAIX0lEQVR4nO1afXDbZh1+sxbWsbGvsrLbuBt36766sR3sOEgiuU4kx2vTpFGKm0SKsyyOlI8laUZTulLojQEHx4CyAaNnt72yrivklo1YSpxcW7YVbKXHgCvduCvroDt6o6OtLSWN1MRWX/5I4imyJMu2bPNHnrvfSZbf6P09z+/jfV+3ACxhCUsoAEouNWGlMRLri5GuZwUSe1Ug8bcFEjsjUPgHAoX/UyCxkyKJjQoU/iOhCd8WJSvW/8vpXFFsx7PGeaJsVaytul+g8KMiic+IFA7NTNB/rggk/jeRcu26RGFris3JEqIUtkFsdo2KFK6kI52pCSR2UmirZgY9YFmxeaYg1uyqFSj8lMXI5mofRb3rOwEAJcXmDS72ex8QvO43skjv3MeT2DtRX2150cgLzVUDIonF8xBh64KQ2FWRwncPejyFK4tYq/Nmoa1mPE/pnZ1ILe4TFxpdd+SdvOhxrhabXaeLTVjfsHfzSj5K4V8QKfyjnGs3P2UiCt/v/1LeyAsNlXebkbeRSOZ/Q+JCrBH/Yt7IX+qlbhS96dM+Y+dJLC6S+BWRwmazFizf5AEAQPRtHLEhukqMwo8KTfg2oamy8t+e0uvUczwDwDWxhopHBBLvElrc+wUSm/r/IN9a3ZtrbYqUe9eHTc7PZDLvoAcsi/lq2wQSO1M88k93r7ayjzdI76sxEv/5hdryT+foRknM696q9qMg5AEAQHx83WhWtU5i56PtG1E7fYkO+B4SKNd7AoWLBSEv9G3Asol8zOs+ddFTeWc+fDrXgq2MNmEP5+PdKZB/XR4RvRnWe8tjJ8+1ECsL4mA+EQ+vcCg8gHHuPjjZvc5S6guU6/3/POG5rdi+24IEv+KwEgFQ4QFU/rAKTu90pal5fCY60PlQsf22BefHwfUKD2YVfl6ACIAKfx2UX0QNBYj6arqL7bdtSIQBnYy+2iIAzg49Cid9bm3qv11sn21FggeHUwSILGQCgIljd8Kpreq+4Koqts+2QuGBmBL9FLsFSj+sgAKFnyq2v7ZCDoO7dKOvvc7fS3vcW4rts61I8OBxXdI6/UCJADgVvmGVHfMSAelgfUA6VBeQDqmv2nuzZ3rf6Y2r3SM7DB1RwmCnOsLaiGuenbWD/JwAcqI+IEPCL8H6gAwX7omADIn5z/Wqe0Iz1sgI3edSv6EjcR78OEk4jQiJCNhnlwD1ATlBmDqdCUHzcYRf3mXoSCIMXjdK95SeEAbfslUAk4iaEc1EsLnx0vNmAhw3q3mNAFvtFCBd2i8i4bdWAgYCDBkLwIMJw8antTDos12ARSTNM8JK5Amd8URAPmosQAS8Zdr41N/lIQOS0Z2/pkZe0iWoS35+rIY8rPfLvzcWgAdDljMgAr5tlwCEWgArjUxnnJXSmX8eMnQkPrH8OasCJMJgfyEE0KZxMrJ+i2WgyZq6gHTQ0BGFv+abujs//ab4gV0CqEvAar1n0gQXizj9PUNHEmHgTSFt1At4O3eCH+8D0tW2YUmoMkKvfyQzwC/vMHREegN8Tpe4zjIY55fD14+X9tgiwC8mV2ZrHS/LGOGXRd3y8aeKVee/3GrqjBIGMcMsmL/GJu6AW4+1QZRl/mKHADmipD4gz1jJHCIgw9oXJ+8zfVsi/ImXU7bDquvfI2ugZ7wHoiwDUY6BTrbTWRie+ti0d7rGrHTU5UD45StpX5iILPMZrQTD4Q0QG+2GKMckzcF28AXgaYiv7ZNesdokCb90PO0LP2TBpxQezKpFkPmb4Q/ebFlEHOWYZBY8FtriKwDXFDAHhLvr/bJi9TzQtPeycQNUI3HihkML5M9NfB4+caRTl/jHRsvtb333gTzzTUHDPonNZFkkfjVl7Zfr+MRtiMID+McICqtDvfrE2cWfHRxz2j321K155pxE84HLLWb7BK0odX75TEYTBI43hB1ch0HE554h7GJhKrkn/4wPMjfliXMSO4alcsIvx/UanZE17pcGMppkU2iL2zDt9QSZtwqu66/Okdbb88Qd7PydVFYfkGVthLWHHk1JyOteuHRjxpNh3JNjRtE3EgDlGIiwzLlqrq/MbvKtLwkewi/PalPc8PyQPDdIP8lqQvdw92qEZWbNyBqJgnD0VTTI7H74Je/1uRL/8mDr7VWjfYeJkecgdfC/6c8JavPL0+6fitn3JtdIzxbdqLPqiNMmwtAxR7BzZ+mgL2MnHGzHPQhL/wzlmCsLc64PbYPU4Xd1zwN6WdCwT/p61uQXsH6sL5TS/dOUgY4QCZRlxsuD9EDZsM/x4KDnk5ppSsqGfHehw3QjytHfQVj6HaP+U8F1wZbhMf1Dkerzpr3Sn4Ad/5/Yc2T7TQ6OOW2YBWZimAsVRzlaRoL0DMLRV/WW19SVh05+v5nbAxsPCPpZ4Jclwj95b87kF+BgO+5Bg8yFFHKZZkO68XqimoyvGd0FqUNnU/pA1yvTdbaRXwDCtj2CsszFjFLfSjZoRbEiqEqoqtE+6H2VT5LfvPfydtvJJ0V4jb537QhzxjLRdMSsimIhq5q538DNgSnbfqs0FoHrusUd6j2ScdTNyGZTNovfHa8Z6/fmnbwaTq5r+1wj0ziZrjlmvIKYP3ew9Hvk+NOPFpT8AjxHd6ypGOl607Z+YDUL5s4iChpkdq8b7b22KOTVQIPtGxet3bmQtVA6eKhnyBP6hvnPW8UAwvpq1nIdoeS6bpWY0fK3aMfJTCMc/csqtuf+YvNMi68Mt38WDTJPoSx9DGHpmaxLI8hcqBzpCiDB9ibnM87lxeaVLUrKg+1fRVimby3LPItw9G9Rlj6Bssw/EI4+i3DM+0iQPomy9JiD69hdztI7SoPtTWWvddry7w5LWIIx/gdCuvcjoZqlLQAAAABJRU5ErkJggg==",
+ "type": "affiliate",
+ "title": "Google",
+ "url": "https://www.google.com/"
+ },
+ {
+ "bgColor": "#E62117",
+ "directoryId": 10000001,
+ "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAACd0lEQVR4nO3av0tVYRzH8WOIBEVDUUIGCa32FwQ5OrS42NKeRUOTdGmoqUGiLVBqa5UG+7WUREGiEqhTDkVJEA2iYZBkxKvhHumo96o3uM9z0ucNZ7zn834+XO59znm+WZZIJBKJRCKRSGwCx3AWfbiIaxjEPYzgOd7gLWbwIb8WsFjjWrU9q3U+u1C4/0yeOZ47jOROg6jkrn25e3uji27NbzC9A9n/hWnVNbVut/hOTMZ1bSqT6Ky3+A58jOsXhE/o2Lj4FryILBaSF2gpFtAT2ygCPcUCHse2icDTtcW3YSW2TQRW0JahK7ZJRLoy9Ma2iEhvhoHYFhEZyDAU2yIiwxkeBgj6FSDjXxjN8CpA0A1cxucAWY0wkeFdgKBK/pe7H1fxNUDmTpjLMB8gqLJh631A9dF6IUD2Vsxn+BYgaF0BhSIO4WYgh1osZVgOEFSzgEIRh3EL3wO4FFnOAgVtWUChiKO4gx+BvJSqgEIRx3EXP5stVsoCCkWcxH1N3EeUuoBCEafwshlipS8AJzTxW1DaAnAEtzX5B7F0BeAgrmMphFhpClB9M3UFXwI5oQQFYB8u4H0gl3VE3QninOoxVyyWozwL4AxeB8jdjqWgT4M4jUcB8nbKfKj3AUN4gN8BshphLtQbobIyEeqdYFkZzTAc2yIiw+lcQDoZSmeDe/t0ON+gPIltE4Fnxa3pnp8QacFYbKOAjCnOCOUldKhOUO12Nk+JFUroxFRkwWYypd6cYKGEVvTbfZOil2w3KVqjjHZ047xqKRWbZ4XHVed2Z/2d5a0167vYgHC9z6/df9bWs8L9uXO3RmeFE4lEIpFIJPYKfwAcall+TY3q/wAAAABJRU5ErkJggg==",
+ "type": "affiliate",
+ "title": "YouTube",
+ "url": "https://www.youtube.com/"
+ },
+ {
+ "directoryId": 10000002,
+ "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAACT0lEQVR4nO3bzWsTQRgG8PlPHMVLqRVMoQiFehJyUBAFtRCFHspExaaSoFg8CMGPSg+R3kQhh3osIbOlMVrbGKtrsbYESTbB0gqNVGu1WCkWutnXg+ihQjqxi7Oz8w481533+V32iyGEEEIZ76Rho0SZ4ewOG+D3UMatXSx9nPwqb5ySPZC0dPMzhDJelD6IrDCjQijjtvRBJIWGeY3IHkJ2EED2ALKDALIHkB2lAJoujkIwnoNQwoQL96aha3AKQgkTTtyZhKM383D4+gQEoll/AQRiWbg1XILZ+VWwaw6IrE3bgZbejNoAe8+PwEC6DOsbm0Klt679lxQGCESzMD335Z+KKw+wrzcDpcVvOyqvNMCwubjj8soCnBx44Up5ZQGeFZf1BWi7/BhqjthtzpcAseRsQwVrjgNzS9/hZWUFJq3Pf6U5MqoWQHJ8Qbh8aqoKB688cWVfzwBMvP0kVN6srMCec+7t6xmAmfmvQgCRBzOu7usZgHJ1TQjgyI283gCHrj3VG6C9bwwBEAABEEAtgAPRR9A3VKibpdUfQgD9KWvba529+8pbAMF4TqicW+th/r3eAP0pS2+Anvtv9AY4dvu53gCtMfGfI74DWN+wG5rNdwDl6preAGOFj3oDJMcXvAfQHMlAKGHWTeWD2KPw1aFC3et0NPi9wDPvAq/fif0LDMZzru6LALKLIwACIAACIAACIAACIAACIAACIAACIAACIMD/BvDKwUkZAJRx2zNHZyUBFAntTp+WXV4iQCf5c3yecUsXAMq49bv8Txsl6ZCTabOAAAAAAElFTkSuQmCC",
+ "title": "Facebook",
+ "type": "affiliate",
+ "url": "https://www.facebook.com/"
+ },
+ {
+ "bgColor": "#ffffff",
+ "directoryId": 10000003,
+ "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA/CAYAAABQHc7KAAAEyUlEQVRoge2abYhVRRjHf7veVqSFhIQwa7E3ojXNpSiNgl0S+tAHKUjohbQohUgqKKwMSXpbsATdqOgFU4zKpAzMioploxI/WFBZmhRlL/S2lbpsudvd24dzjve5c58558ycey9B84MB95x5/vM/d+bMzDNHCAQCgUAgEAgE/rf0AOd6lJmKVrdDfI8SfxawIKNcEscfa8TOzBFrlj6Aimd5XnmAbx01TD5xiL3IiH3Y8zm4DXgaGALGMip/DDwHDACXKQ9wA/AiMJyi8T3wAnCvEr841t9tiR2JY/uB443YC4BHgA3ApyntTxD90FuAlaaBU4EfLIHvKIZtnAT8ZtE5O6fGCiNuFJiTM7YNWKe0PQScmRV8ncX4+zkbT1hq0bkyZ/wyI+5ux/bPM+I/ACbnCSyhv8tloMvBQAnYr+h8mDP+PRHzGXCMQ9sA14j4ceAMl+A70HtvjaMJ22jqy4i70Kh/sWO7AO+K+Jdcg6cCh6k3/gfQ6aBTAr5UdN7OiHtL1H3NxXhMN9Fkl2jM89BgPXrv3eKoc71Fx2ZqvqhTJv+kKRkQGjs94gE4PTZgGt9DNMvmpQR8pehst9R/Q9TZ5OG7EzgkNK720DjKNvTeu9RRZ4miMUH9bnCeuH8EOMXD881C40egw0PjKL3oP8Cbjjq2FWGrUW+HuDfg6XmP0NA2W060GYKy97odtbQVoQzMiu+fL66PACd4+O0VGqPANA+NOm5CHwVPOeqUgH2Kzub4/uvi2gOeXrcU8GdlCtG7ZBr/C5juqHWtojMOXCX+HgaO8/B5YqyV6GjZpjf3oI+C+x11JgF7FZ1/xL/v9PS4WmgMempYsW2MfsdtYwTRsqT9mBXgANGIc6WD2lG60EMjk0fRTS931GkHPrdo3ejpbZHQ2B+30XC6iNZm0/Q3RBOcC7Yc4WRPb0NC43ZPjVxsRDe+yFHHHLJJWevhabaIP0z0ujaNWdQmGUn5yEPrPkVnBPe1+0kRv97DhzPb0UdBr6OObZv9kIOGnJzLRPlL0+lFN+6yPe5BH0kV4E/yD+PlIm6bQ/uF2Um98Qlgbs74V5V4WVbl0GgDvsB/BBbiCnTj2lG5yVyqvX+QaOSYOsNk7y8WiPo+c1Ah2tFPesbJTmFfEfX7qU2BZVmRoSPnkMU+D1EUW5L0WErMHKq9P0o149NeqZ+w7wq7qO77f02p11Qmo6/laWnoVlFvnbh+uaJTAW616Dwo6rjmIw3lLnTjminZ+38DM8Q92yv1HfUnOh3AL/H9I7hnpA1lKrXnb0nRkqSXxf0nFC3bK7XMqCdT6o2NeIiirEE3Lvfks6kesI6hf1m2nTt8TW2uIeeLhub8vsxAT5IOUB2+8qTm2RStlYqOnOV7xLXBRj5EUWxJ0hKic/2k98dJ367azh32Es0Tz4hrTcn5fZE9I8s+ajc6m20CgrUWrdVEK0yFJub8RRhEN56UMjk+TVO7xtuK6yFMS1hIuuk82+SETSk6h3A/hmsJ7egfPypEh555ej/hHOzZYktyfl9keiqLzzc+LUlqWc7vSyfRJ3Rpegw4zUOrj/ofoKU5vy/91Jp+vIDWLkPL/J9h/0mmAT9TXa58vvIkzKe6ydpQ3FogEAgEAoFAIBCI+Rf5q90lTPZHIwAAAABJRU5ErkJggg==",
+ "title": "Wikipedia",
+ "type": "affiliate",
+ "url": "https://www.wikipedia.org/"
+ },
+ {
+ "bgColor": "#400090",
+ "directoryId": 10000004,
+ "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAD3ElEQVR4nO3bS4xecxjH8WmLBVVEx63JBIkEISkhJqURpAsZQioWqpeNSxraYlKpSwiDIRY0aSakqgS9qHRBIhKxYdFFFxWXIGkilYZEUCKj4zLzsfi/LN55zpiZc/lr+/6Ss3nfc57ze77n9r88/y7swxaswgLM7jpMhTlYiNWtnPd1Ga9RfIVtuB+L0J3b/FSFeejDQ9iBvRhrTzYCUKTv8RFewlrcgAtxQsYk5+Ii3IQHsBm78ONkk5oKgIn0Mz7BO3gBA7gHS3EtLsXZre1UnITj2pI5vvV7D85BL67DCvRjEJvwHj7HcBXGqwJwyKoDILeB3OoAyG0gt8oAGMYj0qdnJ17DY3i2vK1CPYmn8Cq2YwgP4rfpBix7BwwE3+aZ2F0ybqQPMSM439NlgpYFMILzAlO9glZXCf2F+cF5LsafZQJX8Q4oujKbK4j9j4aC+EdhT9nAVb0Ebw8MduNABbF/wMlB/LUVxK4MwE84JTC5uoLYK4O48/BrBbEr/Qy+XnCbfloi5h7MCuJurcSxagGMYWFg9qr/SbxQVTeEiq7Ym9OI9UYQ52ipJ1iZ6mgJ3h0Y7zG17usvOC2I01+12ToAHBCMIGHdFGKsCo4/QwJTqerqC2wMEjgGX0zi2KLH6JU6jNYFYBSXBElc/R/HjaE3OO4y1bYs/1WdvcFd4hbilgmOie6cGa1Ytaju7vDSIKHTxc9yUYtvWZ0G6wawH8cGSa0J9r0t2G92K0ZtamJA5PEgsfaOTNHjMlC3uSYADKMnSG6B9GIr6uqehYN1m2tqSGxre4KtJDdhfcF/O5ow1hSAMVweJNmNOcHvlbb3J1KTg6K7MTO62m3Jz1LBQMdk1fSo8IpJALizSUNNA/jWBNPvOFGahG1MOeYFnpgAwHNNm8kB4CDODJI/F380bSbXzNAzAYD1OYzkAjAYABjKYaQDIMdJxQDqnFMsVC4A6wIAgzmMdADkOKkYwKM5jOQCcFcAYCqjxpUpF4Bovu+IB3BfDiO5AIzrFWJlDiO5ACzpABgP4I4cRnIBWBwAWJLDSC4AfR0A4wHcksNILgCLAgB9OYzkAnDFkQ5gDZbj3ta2XCp5bVydYuncBnKrAyC3gdzqAMhtILfqBDCCb6SZ3velatGXsUFa5NC+vdjatrf2/QAfSyUyv9dlsgyAEXyGt/G8VBl+PS7A3PaGTllJCy7n40Zp8GQD3sWXSgCaDIBRqcBxGx7GYmll57hixlySaojPx83SuqW3pLXC0wKwX7oF+3GloILjUJE03X6NtAh8J76LAOzFRtwqKGY63CStX14mLen5+m9Vgqdbrd+9cgAAAABJRU5ErkJggg==",
+ "title": "Yahoo!",
+ "type": "affiliate",
+ "url": "https://www.yahoo.com/"
+ },
+ {
+ "directoryId": 10000005,
+ "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAJdklEQVR4nO1ba1QTZwL9iCY8hCDxDUpYOIUDBOVZAlJ5NQXRFlEQ8YGiRaqESlhxeRQ3FAWURRC6FYglqBHTEaI8Eg+CRMRatfKwLNL6oqer1h57XEWYbwJK9sdKixiSmRCCbrnn3D8w351770wy3zd8ADCJSUxiPMHhcPS3bdv2/tq1a9MWL16MODs7t5iamt4zMjLq0dfX76VSqU8sLCzueHt7S+3t7QtjYmLWJyYmzp1o32NGSUmJd0BAQLmRkVEPAEBOhCQS6bm3t7c0MjIyHACgMyEB1AGCILTY2NgUOzu7fwGCoUfj/Pnz/x0XF/fXgoICXW3nIYTs7Owlpqam94CGgo+ktbX1j4mJiY7azIQbMTExu3V0dF6AcQo/RF1dXYggSIj2kuFAfHx8Lhjn4ODVElCxWOygpXjKkZWVxQZaDD9EOp3eXV9fb6yVkKOho6Njgb6+fp8ig9pgfHx8gjZyjgYdX1/fhtHMqSKJRHpOJpNl6o4HAMidnJxatJJUEWpraz3xmBxOExOT30JDQ/+elpZmD14+26uqqubExcVFzJs3T53H5uCJEydMtZN4BKKiovKJmA0KChLu3bt3zmh6Bw4c0F+3bl0BEU0AgLygoCByXAKqgq2tbSdek0FBQTV4dZlM5kW8ugAAOZPJ/IeGIuHH9evXp+no6DzHa7K+vt4Lr3ZiYuIOvLoAALmnp6dQQ7HwIzc31xqvQRsbmy5AYC6fnJzsjlcbACD38/M7p5lUBBEeHp5lbm7+E4lEUnonREdHpxPRLSwsNFWmN5Kurq5XNZNITfj4+EwtLCw0dXFxcebz+UFhYWFbmExm6qZNm/IjIiKyampqDIjonTp1ajp4mwrQNP70BfD5/D9PAcXFxQZcLteOwWCw9u/f/3FwcHC6paVlOfg/K4CUlpbmEB0dvSEkJCTLxsamwtnZuXXmzJmPAACDgEBYRXxjC8jJyfFYtmzZYWNj48dgjCGV8Y0qICsryyQiIiKVTqf/BMYxNHgTC8jPz/+ISqU+AVoKDt6kAnbs2MGeMmXKANByePAmFBAbG5utyJi2OKEFJCcnx+MxqYizZs361d/fvyw9PX0bg8FgZWRk2FZVVc1pampaQERnwgoQi8VLVK0DRpJMJstWrlx5hMPhvMflckmKdN+amSDRdbuenl5fRkaGnyrdt6KAsrKyD4iYpFKpTy9cuPAuHu23ogAmk9lIxGRCQsLneLVLS0tnEdHWegFisXgu0b8ANTc3W+LVT0pKsiSirfUCDh06tJyIQSsrq5tE9PPy8jyI6Gu9gMDAQA5Bg9VE9Hfu3BlFRJ/BYHSMPRUBsNnsfUQMksnkciL6q1ev5hHRNzQ07AHa3D9A9N19QEAA7lfiAAAdOp3eTUQfACBHEMR87MlwgmgBZmZm9wHOK1RUVLSFiPYQd+/eHaOheKrB4XCyiBrk8XguqnTb29vNDAwMeolqAwDkCxcuvKbBiMoREhLyKVGDy5cvr1Cm2d3dPd3Ly+sCUd3h3LNnT7xGg46GoqKiQHUMbt68uZjL5VJG6qWkpPjS6fS76miO4Ivm5maVd9qYUV5ePlPdtb+FhUV3cHBwkbm5efrGjRvz7O3tr2og+O90dHRslUqlUxX5lkq5U7HmJEtUsnTxwG0kEJ7b4Cer32ArlXIVHq8ULBarVpPGNcng4GDP4V6xloxArNq3oq+M9hTlUeSvsfwvPxMuoKGhwWO8N0RRKBSM6Bg6nf4jgiC0zk6Egl3czsYQhxsKQ48g4QIAAMDf3//0eIWn0WiPjh075jx79uyHeMfY2dl1IAgyFwAAZA1rjqE8ihw9rPscrXRp7xO5lckaN6X0t+dt7m/LiYanPI+iPPLgmApoa2t75+W7fY2GX7RoUcvx48fpAAAgEom88NwJLBbr65ycnGlD3gZuli15VhfC6JZy9Ubzj1W6to6pAAAAEAgEDsbGxv/RVHh3d/ezEomEOvwcqamp6crGsNns/NHeLg0HPPOhV98Pf2ynQRHG+ZcFDKpdAAAAFBcX+6qzF3gkly5dqjCIVCrVs7e3v6RozKpVqw6q8idrybCFonfrUB5FDqsWfzv0c1jh2IHyKHIotL0J+lsyY7CqJXmyO4hamw/Ly8vnhIeH51Op1KdEQtNotN9WrFjxpVAodFOmv337dkMmk1k9NI5CoWCZmZk7EQSZouj4bilfT/ZtUjhW4yNCDxv0ozyKHJaZ9Mq+S1sDAACdnQgF8o1RlEeRy5picsDA3dPv9QnMHqI88iAUudYNdJV4q1NEXFyc7q5duzZ4eXmdtLKyukUikV6ZL1Cp1CeOjo6tHh4eRVwudwWfzx/1MzoSXC53alRUVCaLxapjs9kBio55fC3bGPvm0wRUQL/3yjc9n9Yz0HX090zolTSP//2OPIjdEVkDAAB42nmYJjsbWjo0CEMcfoCNkXvR5lh3BAlT2LQqhIWFTZFIJFSJRELdunUrWR0NVXhc/zdj9MxHa6B4qRAemfHasx6rYUnQjn8uGD5G9k3CHpRHkcvqQk+8Joh1lX2AHl9wf+RkAWtYvx9eSvaWSn2Iz5w0jId1O6fJLn8Wila6iVCe/oCiZ3sff8YT7CLnE6BgJQoRhxtoqcmz3l8vK9629+xW6Sx4dnUmLJv55DXxo3MeQ8ThDKxlfS77Pm8F1pxkqegkmsItSZxu75nAhf1NMZFY47ov4MlFzejLz69ClhrB/vObc3uuZM5QpIf9fM4K5VHk/W25sSpP3tdaaArPrBSgJbovlM6m+LQeWO13HqtwO4i1Zn0CRZ5+2Hf7bB5cK8a9P6hbytfDOgqsMJErC7vyWRw87VUERR5X0VJDiGc2h/IocljL+rr3Klfpv9pg9xttsLOrVIcfDtltEQOtcP0CFZg/wGvmjysyvQ8VmP8ChTa3McThBiZya8NEbm3YSUYXFNrcRgXmv8Ay2jOUp0tMd9gdidWHFsiuH2QQCqUOuFwuSXY5ZT1a6dyullkNEiKMLky6NaYTeX2JrRXILqcslIkDk2GFYyP6lSE27oGPzHiKVTiJZdV+qf2dh50mJPRo6Jby9bBLycvRSpcCeNLpe7SEovw7Aw+/MpDBUx5XsNqAbHgp2buzc4KutDp4dHGXEVq3xh02fbwFq2Htg5WuQljlcwFWLOqAJ97p7hOYPewTmD2EQuu76Emn67DauxmtdEawap8ceH5TNFofznxQs5XQBstJTGISk1AX/wUPVWOeSpK50AAAAABJRU5ErkJggg==",
+ "title": "Amazon",
+ "type": "affiliate",
+ "url": "https://www.amazon.com/"
+ }
+ ]
+}
diff --git a/browser/base/content/newtab/cells.js b/browser/base/content/newtab/cells.js
new file mode 100644
index 000000000..47d4ef52d
--- /dev/null
+++ b/browser/base/content/newtab/cells.js
@@ -0,0 +1,126 @@
+#ifdef 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/. */
+#endif
+
+/**
+ * This class manages a cell's DOM node (not the actually cell content, a site).
+ * It's mostly read-only, i.e. all manipulation of both position and content
+ * aren't handled here.
+ */
+function Cell(aGrid, aNode) {
+ this._grid = aGrid;
+ this._node = aNode;
+ this._node._newtabCell = this;
+
+ // Register drag-and-drop event handlers.
+ ["dragenter", "dragover", "dragexit", "drop"].forEach(function (aType) {
+ this._node.addEventListener(aType, this, false);
+ }, this);
+}
+
+Cell.prototype = {
+ /**
+ * The grid.
+ */
+ _grid: null,
+
+ /**
+ * The cell's DOM node.
+ */
+ get node() { return this._node; },
+
+ /**
+ * The cell's offset in the grid.
+ */
+ get index() {
+ let index = this._grid.cells.indexOf(this);
+
+ // Cache this value, overwrite the getter.
+ Object.defineProperty(this, "index", {value: index, enumerable: true});
+
+ return index;
+ },
+
+ /**
+ * The previous cell in the grid.
+ */
+ get previousSibling() {
+ let prev = this.node.previousElementSibling;
+ prev = prev && prev._newtabCell;
+
+ // Cache this value, overwrite the getter.
+ Object.defineProperty(this, "previousSibling", {value: prev, enumerable: true});
+
+ return prev;
+ },
+
+ /**
+ * The next cell in the grid.
+ */
+ get nextSibling() {
+ let next = this.node.nextElementSibling;
+ next = next && next._newtabCell;
+
+ // Cache this value, overwrite the getter.
+ Object.defineProperty(this, "nextSibling", {value: next, enumerable: true});
+
+ return next;
+ },
+
+ /**
+ * The site contained in the cell, if any.
+ */
+ get site() {
+ let firstChild = this.node.firstElementChild;
+ return firstChild && firstChild._newtabSite;
+ },
+
+ /**
+ * Checks whether the cell contains a pinned site.
+ * @return Whether the cell contains a pinned site.
+ */
+ containsPinnedSite: function Cell_containsPinnedSite() {
+ let site = this.site;
+ return site && site.isPinned();
+ },
+
+ /**
+ * Checks whether the cell contains a site (is empty).
+ * @return Whether the cell is empty.
+ */
+ isEmpty: function Cell_isEmpty() {
+ return !this.site;
+ },
+
+ /**
+ * Handles all cell events.
+ */
+ handleEvent: function Cell_handleEvent(aEvent) {
+ // We're not responding to external drag/drop events
+ // when our parent window is in private browsing mode.
+ if (inPrivateBrowsingMode() && !gDrag.draggedSite)
+ return;
+
+ if (aEvent.type != "dragexit" && !gDrag.isValid(aEvent))
+ return;
+
+ switch (aEvent.type) {
+ case "dragenter":
+ aEvent.preventDefault();
+ gDrop.enter(this, aEvent);
+ break;
+ case "dragover":
+ aEvent.preventDefault();
+ break;
+ case "dragexit":
+ gDrop.exit(this, aEvent);
+ break;
+ case "drop":
+ aEvent.preventDefault();
+ gDrop.drop(this, aEvent);
+ break;
+ }
+ }
+};
diff --git a/browser/base/content/newtab/customize.js b/browser/base/content/newtab/customize.js
new file mode 100644
index 000000000..28a52373c
--- /dev/null
+++ b/browser/base/content/newtab/customize.js
@@ -0,0 +1,133 @@
+#ifdef 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/. */
+#endif
+
+var gCustomize = {
+ _nodeIDSuffixes: [
+ "blank",
+ "button",
+ "classic",
+ "enhanced",
+ "panel",
+ "overlay",
+ "learn"
+ ],
+
+ _nodes: {},
+
+ init: function() {
+ for (let idSuffix of this._nodeIDSuffixes) {
+ this._nodes[idSuffix] = document.getElementById("newtab-customize-" + idSuffix);
+ }
+
+ this._nodes.button.addEventListener("click", e => this.showPanel(e));
+ this._nodes.blank.addEventListener("click", this);
+ this._nodes.classic.addEventListener("click", this);
+ this._nodes.enhanced.addEventListener("click", this);
+ this._nodes.learn.addEventListener("click", this);
+
+ this.updateSelected();
+ },
+
+ hidePanel: function() {
+ this._nodes.overlay.addEventListener("transitionend", function onTransitionEnd() {
+ gCustomize._nodes.overlay.removeEventListener("transitionend", onTransitionEnd);
+ gCustomize._nodes.overlay.style.display = "none";
+ });
+ this._nodes.overlay.style.opacity = 0;
+ this._nodes.button.removeAttribute("active");
+ this._nodes.panel.removeAttribute("open");
+ document.removeEventListener("click", this);
+ document.removeEventListener("keydown", this);
+ },
+
+ showPanel: function(event) {
+ if (this._nodes.panel.getAttribute("open") == "true") {
+ return;
+ }
+
+ let {panel, button, overlay} = this._nodes;
+ overlay.style.display = "block";
+ panel.setAttribute("open", "true");
+ button.setAttribute("active", "true");
+ setTimeout(() => {
+ // Wait for display update to take place, then animate.
+ overlay.style.opacity = 0.8;
+ }, 0);
+
+ document.addEventListener("click", this);
+ document.addEventListener("keydown", this);
+
+ // Stop the event propogation to prevent panel from immediately closing
+ // via the document click event that we just added.
+ event.stopPropagation();
+ },
+
+ handleEvent: function(event) {
+ switch (event.type) {
+ case "click":
+ this.onClick(event);
+ break;
+ case "keydown":
+ this.onKeyDown(event);
+ break;
+ }
+ },
+
+ onClick: function(event) {
+ if (event.currentTarget == document) {
+ if (!this._nodes.panel.contains(event.target)) {
+ this.hidePanel();
+ }
+ }
+ switch (event.currentTarget.id) {
+ case "newtab-customize-blank":
+ sendAsyncMessage("NewTab:Customize", {enabled: false, enhanced: false});
+ break;
+ case "newtab-customize-classic":
+ if (this._nodes.enhanced.getAttribute("selected")){
+ sendAsyncMessage("NewTab:Customize", {enabled: true, enhanced: true});
+ } else {
+ sendAsyncMessage("NewTab:Customize", {enabled: true, enhanced: false});
+ }
+ break;
+ case "newtab-customize-enhanced":
+ sendAsyncMessage("NewTab:Customize", {enabled: true, enhanced: !gAllPages.enhanced});
+ break;
+ case "newtab-customize-learn":
+ this.showLearn();
+ break;
+ }
+ },
+
+ onKeyDown: function(event) {
+ if (event.keyCode == event.DOM_VK_ESCAPE) {
+ this.hidePanel();
+ }
+ },
+
+ showLearn: function() {
+ window.open(TILES_INTRO_LINK, 'new_window');
+ this.hidePanel();
+ },
+
+ updateSelected: function() {
+ let {enabled, enhanced} = gAllPages;
+ let selected = enabled ? enhanced ? "enhanced" : "classic" : "blank";
+ ["enhanced", "classic", "blank"].forEach(id => {
+ let node = this._nodes[id];
+ if (id == selected) {
+ node.setAttribute("selected", true);
+ }
+ else {
+ node.removeAttribute("selected");
+ }
+ });
+ if (selected == "enhanced") {
+ // If enhanced is selected, so is classic (since enhanced is a subitem of classic)
+ this._nodes.classic.setAttribute("selected", true);
+ }
+ },
+};
diff --git a/browser/base/content/newtab/drag.js b/browser/base/content/newtab/drag.js
new file mode 100644
index 000000000..e3928ebd0
--- /dev/null
+++ b/browser/base/content/newtab/drag.js
@@ -0,0 +1,151 @@
+#ifdef 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/. */
+#endif
+
+/**
+ * This singleton implements site dragging functionality.
+ */
+var gDrag = {
+ /**
+ * The site offset to the drag start point.
+ */
+ _offsetX: null,
+ _offsetY: null,
+
+ /**
+ * The site that is dragged.
+ */
+ _draggedSite: null,
+ get draggedSite() { return this._draggedSite; },
+
+ /**
+ * The cell width/height at the point the drag started.
+ */
+ _cellWidth: null,
+ _cellHeight: null,
+ get cellWidth() { return this._cellWidth; },
+ get cellHeight() { return this._cellHeight; },
+
+ /**
+ * Start a new drag operation.
+ * @param aSite The site that's being dragged.
+ * @param aEvent The 'dragstart' event.
+ */
+ start: function Drag_start(aSite, aEvent) {
+ this._draggedSite = aSite;
+
+ // Mark nodes as being dragged.
+ let selector = ".newtab-site, .newtab-control, .newtab-thumbnail";
+ let parentCell = aSite.node.parentNode;
+ let nodes = parentCell.querySelectorAll(selector);
+ for (let i = 0; i < nodes.length; i++)
+ nodes[i].setAttribute("dragged", "true");
+
+ parentCell.setAttribute("dragged", "true");
+
+ this._setDragData(aSite, aEvent);
+
+ // Store the cursor offset.
+ let node = aSite.node;
+ let rect = node.getBoundingClientRect();
+ this._offsetX = aEvent.clientX - rect.left;
+ this._offsetY = aEvent.clientY - rect.top;
+
+ // Store the cell dimensions.
+ let cellNode = aSite.cell.node;
+ this._cellWidth = cellNode.offsetWidth;
+ this._cellHeight = cellNode.offsetHeight;
+
+ gTransformation.freezeSitePosition(aSite);
+ },
+
+ /**
+ * Handles the 'drag' event.
+ * @param aSite The site that's being dragged.
+ * @param aEvent The 'drag' event.
+ */
+ drag: function Drag_drag(aSite, aEvent) {
+ // Get the viewport size.
+ let {clientWidth, clientHeight} = document.documentElement;
+
+ // We'll want a padding of 5px.
+ let border = 5;
+
+ // Enforce minimum constraints to keep the drag image inside the window.
+ let left = Math.max(scrollX + aEvent.clientX - this._offsetX, border);
+ let top = Math.max(scrollY + aEvent.clientY - this._offsetY, border);
+
+ // Enforce maximum constraints to keep the drag image inside the window.
+ left = Math.min(left, scrollX + clientWidth - this.cellWidth - border);
+ top = Math.min(top, scrollY + clientHeight - this.cellHeight - border);
+
+ // Update the drag image's position.
+ gTransformation.setSitePosition(aSite, {left: left, top: top});
+ },
+
+ /**
+ * Ends the current drag operation.
+ * @param aSite The site that's being dragged.
+ * @param aEvent The 'dragend' event.
+ */
+ end: function Drag_end(aSite, aEvent) {
+ let nodes = gGrid.node.querySelectorAll("[dragged]")
+ for (let i = 0; i < nodes.length; i++)
+ nodes[i].removeAttribute("dragged");
+
+ // Slide the dragged site back into its cell (may be the old or the new cell).
+ gTransformation.slideSiteTo(aSite, aSite.cell, {unfreeze: true});
+
+ this._draggedSite = null;
+ },
+
+ /**
+ * Checks whether we're responsible for a given drag event.
+ * @param aEvent The drag event to check.
+ * @return Whether we should handle this drag and drop operation.
+ */
+ isValid: function Drag_isValid(aEvent) {
+ let link = gDragDataHelper.getLinkFromDragEvent(aEvent);
+
+ // Check that the drag data is non-empty.
+ // Can happen when dragging places folders.
+ if (!link || !link.url) {
+ return false;
+ }
+
+ // Check that we're not accepting URLs which would inherit the caller's
+ // principal (such as javascript: or data:).
+ return gLinkChecker.checkLoadURI(link.url);
+ },
+
+ /**
+ * Initializes the drag data for the current drag operation.
+ * @param aSite The site that's being dragged.
+ * @param aEvent The 'dragstart' event.
+ */
+ _setDragData: function Drag_setDragData(aSite, aEvent) {
+ let {url, title} = aSite;
+
+ let dt = aEvent.dataTransfer;
+ dt.mozCursor = "default";
+ dt.effectAllowed = "move";
+ dt.setData("text/plain", url);
+ dt.setData("text/uri-list", url);
+ dt.setData("text/x-moz-url", url + "\n" + title);
+ dt.setData("text/html", "<a href=\"" + url + "\">" + url + "</a>");
+
+ // Create and use an empty drag element. We don't want to use the default
+ // drag image with its default opacity.
+ let dragElement = document.createElementNS(HTML_NAMESPACE, "div");
+ dragElement.classList.add("newtab-drag");
+ let scrollbox = document.getElementById("newtab-vertical-margin");
+ scrollbox.appendChild(dragElement);
+ dt.setDragImage(dragElement, 0, 0);
+
+ // After the 'dragstart' event has been processed we can remove the
+ // temporary drag element from the DOM.
+ setTimeout(() => scrollbox.removeChild(dragElement), 0);
+ }
+};
diff --git a/browser/base/content/newtab/dragDataHelper.js b/browser/base/content/newtab/dragDataHelper.js
new file mode 100644
index 000000000..675ff2671
--- /dev/null
+++ b/browser/base/content/newtab/dragDataHelper.js
@@ -0,0 +1,22 @@
+#ifdef 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/. */
+#endif
+
+var gDragDataHelper = {
+ get mimeType() {
+ return "text/x-moz-url";
+ },
+
+ getLinkFromDragEvent: function DragDataHelper_getLinkFromDragEvent(aEvent) {
+ let dt = aEvent.dataTransfer;
+ if (!dt || !dt.types.includes(this.mimeType)) {
+ return null;
+ }
+
+ let data = dt.getData(this.mimeType) || "";
+ let [url, title] = data.split(/[\r\n]+/);
+ return {url: url, title: title};
+ }
+};
diff --git a/browser/base/content/newtab/drop.js b/browser/base/content/newtab/drop.js
new file mode 100644
index 000000000..748652455
--- /dev/null
+++ b/browser/base/content/newtab/drop.js
@@ -0,0 +1,150 @@
+#ifdef 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/. */
+#endif
+
+// A little delay that prevents the grid from being too sensitive when dragging
+// sites around.
+const DELAY_REARRANGE_MS = 100;
+
+/**
+ * This singleton implements site dropping functionality.
+ */
+var gDrop = {
+ /**
+ * The last drop target.
+ */
+ _lastDropTarget: null,
+
+ /**
+ * Handles the 'dragenter' event.
+ * @param aCell The drop target cell.
+ */
+ enter: function Drop_enter(aCell) {
+ this._delayedRearrange(aCell);
+ },
+
+ /**
+ * Handles the 'dragexit' event.
+ * @param aCell The drop target cell.
+ * @param aEvent The 'dragexit' event.
+ */
+ exit: function Drop_exit(aCell, aEvent) {
+ if (aEvent.dataTransfer && !aEvent.dataTransfer.mozUserCancelled) {
+ this._delayedRearrange();
+ } else {
+ // The drag operation has been cancelled.
+ this._cancelDelayedArrange();
+ this._rearrange();
+ }
+ },
+
+ /**
+ * Handles the 'drop' event.
+ * @param aCell The drop target cell.
+ * @param aEvent The 'dragexit' event.
+ */
+ drop: function Drop_drop(aCell, aEvent) {
+ // The cell that is the drop target could contain a pinned site. We need
+ // to find out where that site has gone and re-pin it there.
+ if (aCell.containsPinnedSite())
+ this._repinSitesAfterDrop(aCell);
+
+ // Pin the dragged or insert the new site.
+ this._pinDraggedSite(aCell, aEvent);
+
+ this._cancelDelayedArrange();
+
+ // Update the grid and move all sites to their new places.
+ gUpdater.updateGrid();
+ },
+
+ /**
+ * Re-pins all pinned sites in their (new) positions.
+ * @param aCell The drop target cell.
+ */
+ _repinSitesAfterDrop: function Drop_repinSitesAfterDrop(aCell) {
+ let sites = gDropPreview.rearrange(aCell);
+
+ // Filter out pinned sites.
+ let pinnedSites = sites.filter(function (aSite) {
+ return aSite && aSite.isPinned();
+ });
+
+ // Re-pin all shifted pinned cells.
+ pinnedSites.forEach(aSite => aSite.pin(sites.indexOf(aSite)));
+ },
+
+ /**
+ * Pins the dragged site in its new place.
+ * @param aCell The drop target cell.
+ * @param aEvent The 'dragexit' event.
+ */
+ _pinDraggedSite: function Drop_pinDraggedSite(aCell, aEvent) {
+ let index = aCell.index;
+ let draggedSite = gDrag.draggedSite;
+
+ if (draggedSite) {
+ // Pin the dragged site at its new place.
+ if (aCell != draggedSite.cell)
+ draggedSite.pin(index);
+ } else {
+ let link = gDragDataHelper.getLinkFromDragEvent(aEvent);
+ if (link) {
+ // A new link was dragged onto the grid. Create it by pinning its URL.
+ gPinnedLinks.pin(link, index);
+
+ // Make sure the newly added link is not blocked.
+ gBlockedLinks.unblock(link);
+ }
+ }
+ },
+
+ /**
+ * Time a rearrange with a little delay.
+ * @param aCell The drop target cell.
+ */
+ _delayedRearrange: function Drop_delayedRearrange(aCell) {
+ // The last drop target didn't change so there's no need to re-arrange.
+ if (this._lastDropTarget == aCell)
+ return;
+
+ let self = this;
+
+ function callback() {
+ self._rearrangeTimeout = null;
+ self._rearrange(aCell);
+ }
+
+ this._cancelDelayedArrange();
+ this._rearrangeTimeout = setTimeout(callback, DELAY_REARRANGE_MS);
+
+ // Store the last drop target.
+ this._lastDropTarget = aCell;
+ },
+
+ /**
+ * Cancels a timed rearrange, if any.
+ */
+ _cancelDelayedArrange: function Drop_cancelDelayedArrange() {
+ if (this._rearrangeTimeout) {
+ clearTimeout(this._rearrangeTimeout);
+ this._rearrangeTimeout = null;
+ }
+ },
+
+ /**
+ * Rearrange all sites in the grid depending on the current drop target.
+ * @param aCell The drop target cell.
+ */
+ _rearrange: function Drop_rearrange(aCell) {
+ let sites = gGrid.sites;
+
+ // We need to rearrange the grid only if there's a current drop target.
+ if (aCell)
+ sites = gDropPreview.rearrange(aCell);
+
+ gTransformation.rearrangeSites(sites, {unfreeze: !aCell});
+ }
+};
diff --git a/browser/base/content/newtab/dropPreview.js b/browser/base/content/newtab/dropPreview.js
new file mode 100644
index 000000000..fd7587a35
--- /dev/null
+++ b/browser/base/content/newtab/dropPreview.js
@@ -0,0 +1,222 @@
+#ifdef 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/. */
+#endif
+
+/**
+ * This singleton provides the ability to re-arrange the current grid to
+ * indicate the transformation that results from dropping a cell at a certain
+ * position.
+ */
+var gDropPreview = {
+ /**
+ * Rearranges the sites currently contained in the grid when a site would be
+ * dropped onto the given cell.
+ * @param aCell The drop target cell.
+ * @return The re-arranged array of sites.
+ */
+ rearrange: function DropPreview_rearrange(aCell) {
+ let sites = gGrid.sites;
+
+ // Insert the dragged site into the current grid.
+ this._insertDraggedSite(sites, aCell);
+
+ // After the new site has been inserted we need to correct the positions
+ // of all pinned tabs that have been moved around.
+ this._repositionPinnedSites(sites, aCell);
+
+ return sites;
+ },
+
+ /**
+ * Inserts the currently dragged site into the given array of sites.
+ * @param aSites The array of sites to insert into.
+ * @param aCell The drop target cell.
+ */
+ _insertDraggedSite: function DropPreview_insertDraggedSite(aSites, aCell) {
+ let dropIndex = aCell.index;
+ let draggedSite = gDrag.draggedSite;
+
+ // We're currently dragging a site.
+ if (draggedSite) {
+ let dragCell = draggedSite.cell;
+ let dragIndex = dragCell.index;
+
+ // Move the dragged site into its new position.
+ if (dragIndex != dropIndex) {
+ aSites.splice(dragIndex, 1);
+ aSites.splice(dropIndex, 0, draggedSite);
+ }
+ // We're handling an external drag item.
+ } else {
+ aSites.splice(dropIndex, 0, null);
+ }
+ },
+
+ /**
+ * Correct the position of all pinned sites that might have been moved to
+ * different positions after the dragged site has been inserted.
+ * @param aSites The array of sites containing the dragged site.
+ * @param aCell The drop target cell.
+ */
+ _repositionPinnedSites:
+ function DropPreview_repositionPinnedSites(aSites, aCell) {
+
+ // Collect all pinned sites.
+ let pinnedSites = this._filterPinnedSites(aSites, aCell);
+
+ // Correct pinned site positions.
+ pinnedSites.forEach(function (aSite) {
+ aSites[aSites.indexOf(aSite)] = aSites[aSite.cell.index];
+ aSites[aSite.cell.index] = aSite;
+ }, this);
+
+ // There might be a pinned cell that got pushed out of the grid, try to
+ // sneak it in by removing a lower-priority cell.
+ if (this._hasOverflowedPinnedSite(aSites, aCell))
+ this._repositionOverflowedPinnedSite(aSites, aCell);
+ },
+
+ /**
+ * Filter pinned sites out of the grid that are still on their old positions
+ * and have not moved.
+ * @param aSites The array of sites to filter.
+ * @param aCell The drop target cell.
+ * @return The filtered array of sites.
+ */
+ _filterPinnedSites: function DropPreview_filterPinnedSites(aSites, aCell) {
+ let draggedSite = gDrag.draggedSite;
+
+ // When dropping on a cell that contains a pinned site make sure that all
+ // pinned cells surrounding the drop target are moved as well.
+ let range = this._getPinnedRange(aCell);
+
+ return aSites.filter(function (aSite, aIndex) {
+ // The site must be valid, pinned and not the dragged site.
+ if (!aSite || aSite == draggedSite || !aSite.isPinned())
+ return false;
+
+ let index = aSite.cell.index;
+
+ // If it's not in the 'pinned range' it's a valid pinned site.
+ return (index > range.end || index < range.start);
+ });
+ },
+
+ /**
+ * Determines the range of pinned sites surrounding the drop target cell.
+ * @param aCell The drop target cell.
+ * @return The range of pinned cells.
+ */
+ _getPinnedRange: function DropPreview_getPinnedRange(aCell) {
+ let dropIndex = aCell.index;
+ let range = {start: dropIndex, end: dropIndex};
+
+ // We need a pinned range only when dropping on a pinned site.
+ if (aCell.containsPinnedSite()) {
+ let links = gPinnedLinks.links;
+
+ // Find all previous siblings of the drop target that are pinned as well.
+ while (range.start && links[range.start - 1])
+ range.start--;
+
+ let maxEnd = links.length - 1;
+
+ // Find all next siblings of the drop target that are pinned as well.
+ while (range.end < maxEnd && links[range.end + 1])
+ range.end++;
+ }
+
+ return range;
+ },
+
+ /**
+ * Checks if the given array of sites contains a pinned site that has
+ * been pushed out of the grid.
+ * @param aSites The array of sites to check.
+ * @param aCell The drop target cell.
+ * @return Whether there is an overflowed pinned cell.
+ */
+ _hasOverflowedPinnedSite:
+ function DropPreview_hasOverflowedPinnedSite(aSites, aCell) {
+
+ // If the drop target isn't pinned there's no way a pinned site has been
+ // pushed out of the grid so we can just exit here.
+ if (!aCell.containsPinnedSite())
+ return false;
+
+ let cells = gGrid.cells;
+
+ // No cells have been pushed out of the grid, nothing to do here.
+ if (aSites.length <= cells.length)
+ return false;
+
+ let overflowedSite = aSites[cells.length];
+
+ // Nothing to do if the site that got pushed out of the grid is not pinned.
+ return (overflowedSite && overflowedSite.isPinned());
+ },
+
+ /**
+ * We have a overflowed pinned site that we need to re-position so that it's
+ * visible again. We try to find a lower-priority cell (empty or containing
+ * an unpinned site) that we can move it to.
+ * @param aSites The array of sites.
+ * @param aCell The drop target cell.
+ */
+ _repositionOverflowedPinnedSite:
+ function DropPreview_repositionOverflowedPinnedSite(aSites, aCell) {
+
+ // Try to find a lower-priority cell (empty or containing an unpinned site).
+ let index = this._indexOfLowerPrioritySite(aSites, aCell);
+
+ if (index > -1) {
+ let cells = gGrid.cells;
+ let dropIndex = aCell.index;
+
+ // Move all pinned cells to their new positions to let the overflowed
+ // site fit into the grid.
+ for (let i = index + 1, lastPosition = index; i < aSites.length; i++) {
+ if (i != dropIndex) {
+ aSites[lastPosition] = aSites[i];
+ lastPosition = i;
+ }
+ }
+
+ // Finally, remove the overflowed site from its previous position.
+ aSites.splice(cells.length, 1);
+ }
+ },
+
+ /**
+ * Finds the index of the last cell that is empty or contains an unpinned
+ * site. These are considered to be of a lower priority.
+ * @param aSites The array of sites.
+ * @param aCell The drop target cell.
+ * @return The cell's index.
+ */
+ _indexOfLowerPrioritySite:
+ function DropPreview_indexOfLowerPrioritySite(aSites, aCell) {
+
+ let cells = gGrid.cells;
+ let dropIndex = aCell.index;
+
+ // Search (beginning with the last site in the grid) for a site that is
+ // empty or unpinned (an thus lower-priority) and can be pushed out of the
+ // grid instead of the pinned site.
+ for (let i = cells.length - 1; i >= 0; i--) {
+ // The cell that is our drop target is not a good choice.
+ if (i == dropIndex)
+ continue;
+
+ let site = aSites[i];
+
+ // We can use the cell only if it's empty or the site is un-pinned.
+ if (!site || !site.isPinned())
+ return i;
+ }
+
+ return -1;
+ }
+};
diff --git a/browser/base/content/newtab/dropTargetShim.js b/browser/base/content/newtab/dropTargetShim.js
new file mode 100644
index 000000000..57a97fa00
--- /dev/null
+++ b/browser/base/content/newtab/dropTargetShim.js
@@ -0,0 +1,232 @@
+#ifdef 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/. */
+#endif
+
+/**
+ * This singleton provides a custom drop target detection. We need this because
+ * the default DnD target detection relies on the cursor's position. We want
+ * to pick a drop target based on the dragged site's position.
+ */
+var gDropTargetShim = {
+ /**
+ * Cache for the position of all cells, cleaned after drag finished.
+ */
+ _cellPositions: null,
+
+ /**
+ * The last drop target that was hovered.
+ */
+ _lastDropTarget: null,
+
+ /**
+ * Initializes the drop target shim.
+ */
+ init: function () {
+ gGrid.node.addEventListener("dragstart", this, true);
+ },
+
+ /**
+ * Add all event listeners needed during a drag operation.
+ */
+ _addEventListeners: function () {
+ gGrid.node.addEventListener("dragend", this);
+
+ let docElement = document.documentElement;
+ docElement.addEventListener("dragover", this);
+ docElement.addEventListener("dragenter", this);
+ docElement.addEventListener("drop", this);
+ },
+
+ /**
+ * Remove all event listeners that were needed during a drag operation.
+ */
+ _removeEventListeners: function () {
+ gGrid.node.removeEventListener("dragend", this);
+
+ let docElement = document.documentElement;
+ docElement.removeEventListener("dragover", this);
+ docElement.removeEventListener("dragenter", this);
+ docElement.removeEventListener("drop", this);
+ },
+
+ /**
+ * Handles all shim events.
+ */
+ handleEvent: function (aEvent) {
+ switch (aEvent.type) {
+ case "dragstart":
+ this._dragstart(aEvent);
+ break;
+ case "dragenter":
+ aEvent.preventDefault();
+ break;
+ case "dragover":
+ this._dragover(aEvent);
+ break;
+ case "drop":
+ this._drop(aEvent);
+ break;
+ case "dragend":
+ this._dragend(aEvent);
+ break;
+ }
+ },
+
+ /**
+ * Handles the 'dragstart' event.
+ * @param aEvent The 'dragstart' event.
+ */
+ _dragstart: function (aEvent) {
+ if (aEvent.target.classList.contains("newtab-link")) {
+ gGrid.lock();
+ this._addEventListeners();
+ }
+ },
+
+ /**
+ * Handles the 'dragover' event.
+ * @param aEvent The 'dragover' event.
+ */
+ _dragover: function (aEvent) {
+ // XXX bug 505521 - Use the dragover event to retrieve the
+ // current mouse coordinates while dragging.
+ let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode;
+ gDrag.drag(sourceNode._newtabSite, aEvent);
+
+ // Find the current drop target, if there's one.
+ this._updateDropTarget(aEvent);
+
+ // If we have a valid drop target,
+ // let the drag-and-drop service know.
+ if (this._lastDropTarget) {
+ aEvent.preventDefault();
+ }
+ },
+
+ /**
+ * Handles the 'drop' event.
+ * @param aEvent The 'drop' event.
+ */
+ _drop: function (aEvent) {
+ // We're accepting all drops.
+ aEvent.preventDefault();
+
+ // remember that drop event was seen, this explicitly
+ // assumes that drop event preceeds dragend event
+ this._dropSeen = true;
+
+ // Make sure to determine the current drop target
+ // in case the dragover event hasn't been fired.
+ this._updateDropTarget(aEvent);
+
+ // A site was successfully dropped.
+ this._dispatchEvent(aEvent, "drop", this._lastDropTarget);
+ },
+
+ /**
+ * Handles the 'dragend' event.
+ * @param aEvent The 'dragend' event.
+ */
+ _dragend: function (aEvent) {
+ if (this._lastDropTarget) {
+ if (aEvent.dataTransfer.mozUserCancelled || !this._dropSeen) {
+ // The drag operation was cancelled or no drop event was generated
+ this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
+ this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
+ }
+
+ // Clean up.
+ this._lastDropTarget = null;
+ this._cellPositions = null;
+ }
+
+ this._dropSeen = false;
+ gGrid.unlock();
+ this._removeEventListeners();
+ },
+
+ /**
+ * Tries to find the current drop target and will fire
+ * appropriate dragenter, dragexit, and dragleave events.
+ * @param aEvent The current drag event.
+ */
+ _updateDropTarget: function (aEvent) {
+ // Let's see if we find a drop target.
+ let target = this._findDropTarget(aEvent);
+
+ if (target != this._lastDropTarget) {
+ if (this._lastDropTarget)
+ // We left the last drop target.
+ this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
+
+ if (target)
+ // We're now hovering a (new) drop target.
+ this._dispatchEvent(aEvent, "dragenter", target);
+
+ if (this._lastDropTarget)
+ // We left the last drop target.
+ this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
+
+ this._lastDropTarget = target;
+ }
+ },
+
+ /**
+ * Determines the current drop target by matching the dragged site's position
+ * against all cells in the grid.
+ * @return The currently hovered drop target or null.
+ */
+ _findDropTarget: function () {
+ // These are the minimum intersection values - we want to use the cell if
+ // the site is >= 50% hovering its position.
+ let minWidth = gDrag.cellWidth / 2;
+ let minHeight = gDrag.cellHeight / 2;
+
+ let cellPositions = this._getCellPositions();
+ let rect = gTransformation.getNodePosition(gDrag.draggedSite.node);
+
+ // Compare each cell's position to the dragged site's position.
+ for (let i = 0; i < cellPositions.length; i++) {
+ let inter = rect.intersect(cellPositions[i].rect);
+
+ // If the intersection is big enough we found a drop target.
+ if (inter.width >= minWidth && inter.height >= minHeight)
+ return cellPositions[i].cell;
+ }
+
+ // No drop target found.
+ return null;
+ },
+
+ /**
+ * Gets the positions of all cell nodes.
+ * @return The (cached) cell positions.
+ */
+ _getCellPositions: function DropTargetShim_getCellPositions() {
+ if (this._cellPositions)
+ return this._cellPositions;
+
+ return this._cellPositions = gGrid.cells.map(function (cell) {
+ return {cell: cell, rect: gTransformation.getNodePosition(cell.node)};
+ });
+ },
+
+ /**
+ * Dispatches a custom DragEvent on the given target node.
+ * @param aEvent The source event.
+ * @param aType The event type.
+ * @param aTarget The target node that receives the event.
+ */
+ _dispatchEvent: function (aEvent, aType, aTarget) {
+ let node = aTarget.node;
+ let event = document.createEvent("DragEvent");
+
+ // The event should not bubble to prevent recursion.
+ event.initDragEvent(aType, false, true, window, 0, 0, 0, 0, 0, false, false,
+ false, false, 0, node, aEvent.dataTransfer);
+
+ node.dispatchEvent(event);
+ }
+};
diff --git a/browser/base/content/newtab/grid.js b/browser/base/content/newtab/grid.js
new file mode 100644
index 000000000..b6f98fa17
--- /dev/null
+++ b/browser/base/content/newtab/grid.js
@@ -0,0 +1,279 @@
+#ifdef 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/. */
+#endif
+
+/**
+ * Define various fixed dimensions
+ */
+const GRID_BOTTOM_EXTRA = 7; // title's line-height extends 7px past the margin
+const GRID_WIDTH_EXTRA = 1; // provide 1px buffer to allow for rounding error
+const SPONSORED_TAG_BUFFER = 2; // 2px buffer to clip off top of sponsored tag
+
+/**
+ * This singleton represents the grid that contains all sites.
+ */
+var gGrid = {
+ /**
+ * The DOM node of the grid.
+ */
+ _node: null,
+ _gridDefaultContent: null,
+ get node() { return this._node; },
+
+ /**
+ * The cached DOM fragment for sites.
+ */
+ _siteFragment: null,
+
+ /**
+ * All cells contained in the grid.
+ */
+ _cells: [],
+ get cells() { return this._cells; },
+
+ /**
+ * All sites contained in the grid's cells. Sites may be empty.
+ */
+ get sites() { return [for (cell of this.cells) cell.site]; },
+
+ // Tells whether the grid has already been initialized.
+ get ready() { return !!this._ready; },
+
+ // Returns whether the page has finished loading yet.
+ get isDocumentLoaded() { return document.readyState == "complete"; },
+
+ /**
+ * Initializes the grid.
+ * @param aSelector The query selector of the grid.
+ */
+ init: function Grid_init() {
+ this._node = document.getElementById("newtab-grid");
+ this._gridDefaultContent = this._node.lastChild;
+ this._createSiteFragment();
+
+ gLinks.populateCache(() => {
+ this._refreshGrid();
+ this._ready = true;
+
+ // If fetching links took longer than loading the page itself then
+ // we need to resize the grid as that was blocked until now.
+ // We also want to resize now if the page was already loaded when
+ // initializing the grid (the user toggled the page).
+ this._resizeGrid();
+
+ addEventListener("resize", this);
+ });
+
+ // Resize the grid as soon as the page loads.
+ if (!this.isDocumentLoaded) {
+ addEventListener("load", this);
+ }
+ },
+
+ /**
+ * Creates a new site in the grid.
+ * @param aLink The new site's link.
+ * @param aCell The cell that will contain the new site.
+ * @return The newly created site.
+ */
+ createSite: function Grid_createSite(aLink, aCell) {
+ let node = aCell.node;
+ node.appendChild(this._siteFragment.cloneNode(true));
+ return new Site(node.firstElementChild, aLink);
+ },
+
+ /**
+ * Handles all grid events.
+ */
+ handleEvent: function Grid_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "load":
+ case "resize":
+ this._resizeGrid();
+ break;
+ }
+ },
+
+ /**
+ * Locks the grid to block all pointer events.
+ */
+ lock: function Grid_lock() {
+ this.node.setAttribute("locked", "true");
+ },
+
+ /**
+ * Unlocks the grid to allow all pointer events.
+ */
+ unlock: function Grid_unlock() {
+ this.node.removeAttribute("locked");
+ },
+
+ /**
+ * Renders and resizes the gird. _resizeGrid() call is needed to ensure
+ * that scrollbar disappears when the bottom row becomes empty following
+ * the block action, or tile display is turmed off via cog menu
+ */
+
+ refresh() {
+ this._refreshGrid();
+ this._resizeGrid();
+ },
+
+ /**
+ * Renders the grid, including cells and sites.
+ */
+ _refreshGrid() {
+ let cell = document.createElementNS(HTML_NAMESPACE, "div");
+ cell.classList.add("newtab-cell");
+
+ // Creates all the cells up to the maximum
+ let fragment = document.createDocumentFragment();
+ for (let i = 0; i < gGridPrefs.gridColumns * gGridPrefs.gridRows; i++) {
+ fragment.appendChild(cell.cloneNode(true));
+ }
+
+ // Create cells.
+ let cells = Array.from(fragment.childNodes, (cell) => new Cell(this, cell));
+
+ // Fetch links.
+ let links = gLinks.getLinks();
+
+ // Create sites.
+ let numLinks = Math.min(links.length, cells.length);
+ let hasHistoryTiles = false;
+ for (let i = 0; i < numLinks; i++) {
+ if (links[i]) {
+ this.createSite(links[i], cells[i]);
+ if (links[i].type == "history") {
+ hasHistoryTiles = true;
+ }
+ }
+ }
+
+ this._cells = cells;
+ while (this._gridDefaultContent.nextSibling) {
+ this._gridDefaultContent.nextSibling.remove();
+ }
+ this._node.appendChild(fragment);
+
+ document.getElementById("topsites-heading").textContent =
+ hasHistoryTiles ? "Your Top Sites" : "Top Sites";
+ },
+
+ /**
+ * Calculate the height for a number of rows up to the maximum rows
+ * @param rows Number of rows defaulting to the max
+ */
+ _computeHeight: function Grid_computeHeight(aRows) {
+ let {gridRows} = gGridPrefs;
+ aRows = aRows === undefined ? gridRows : Math.min(gridRows, aRows);
+ return aRows * this._cellHeight + GRID_BOTTOM_EXTRA;
+ },
+
+ /**
+ * Creates the DOM fragment that is re-used when creating sites.
+ */
+ _createSiteFragment: function Grid_createSiteFragment() {
+ let site = document.createElementNS(HTML_NAMESPACE, "div");
+ site.classList.add("newtab-site");
+ site.setAttribute("draggable", "true");
+
+ // Create the site's inner HTML code.
+ site.innerHTML =
+ '<span class="newtab-sponsored">' + newTabString("sponsored.button") + '</span>' +
+ '<a class="newtab-link">' +
+ ' <span class="newtab-thumbnail placeholder"/>' +
+ ' <span class="newtab-thumbnail thumbnail"/>' +
+ ' <span class="newtab-thumbnail enhanced-content"/>' +
+ ' <span class="newtab-title"/>' +
+ '</a>' +
+ '<input type="button" title="' + newTabString("pin") + '"' +
+ ' class="newtab-control newtab-control-pin"/>' +
+ '<input type="button" title="' + newTabString("block") + '"' +
+ ' class="newtab-control newtab-control-block"/>' +
+ '<span class="newtab-suggested"/>';
+
+ this._siteFragment = document.createDocumentFragment();
+ this._siteFragment.appendChild(site);
+ },
+
+ /**
+ * Test a tile at a given position for being pinned or history
+ * @param position Position in sites array
+ */
+ _isHistoricalTile: function Grid_isHistoricalTile(aPos) {
+ let site = this.sites[aPos];
+ return site && (site.isPinned() || site.link && site.link.type == "history");
+ },
+
+ /**
+ * Make sure the correct number of rows and columns are visible
+ */
+ _resizeGrid: function Grid_resizeGrid() {
+ // If we're somehow called before the page has finished loading,
+ // let's bail out to avoid caching zero heights and widths.
+ // We'll be called again when DOMContentLoaded fires.
+ // Same goes for the grid if that's not ready yet.
+ if (!this.isDocumentLoaded || !this._ready) {
+ return;
+ }
+
+ // Save the cell's computed height/width including margin and border
+ if (this._cellHeight === undefined) {
+ let refCell = document.querySelector(".newtab-cell");
+ let style = getComputedStyle(refCell);
+ this._cellHeight = refCell.offsetHeight +
+ parseFloat(style.marginTop) + parseFloat(style.marginBottom);
+ this._cellWidth = refCell.offsetWidth +
+ parseFloat(style.marginLeft) + parseFloat(style.marginRight);
+ }
+
+ let searchContainer = document.querySelector("#newtab-search-container");
+ // Save search-container margin height
+ if (this._searchContainerMargin === undefined) {
+ let style = getComputedStyle(searchContainer);
+ this._searchContainerMargin = parseFloat(style.marginBottom) +
+ parseFloat(style.marginTop);
+ }
+
+ // Find the number of rows we can place into view port
+ let availHeight = document.documentElement.clientHeight -
+ searchContainer.offsetHeight - this._searchContainerMargin;
+ let visibleRows = Math.floor(availHeight / this._cellHeight);
+
+ // Find the number of columns that fit into view port
+ let maxGridWidth = gGridPrefs.gridColumns * this._cellWidth + GRID_WIDTH_EXTRA;
+ // available width is current grid width, but no greater than maxGridWidth
+ let availWidth = Math.min(document.querySelector("#newtab-grid").clientWidth,
+ maxGridWidth);
+ // finally get the number of columns we can fit into view port
+ let gridColumns = Math.floor(availWidth / this._cellWidth);
+ // walk sites backwords until a pinned or history tile is found or visibleRows reached
+ let tileIndex = Math.min(gGridPrefs.gridRows * gridColumns, this.sites.length) - 1;
+ while (tileIndex >= visibleRows * gridColumns) {
+ if (this._isHistoricalTile(tileIndex)) {
+ break;
+ }
+ tileIndex--;
+ }
+
+ // Compute the actual number of grid rows we will display (potentially
+ // with a scroll bar). tileIndex now points to a historical tile with
+ // heighest index or to the last index of the visible row, if none found
+ // Dividing tileIndex by number of tiles in a column gives the rows
+ let gridRows = Math.floor(tileIndex / gridColumns) + 1;
+
+ // we need to set grid width, for otherwise the scrollbar may shrink
+ // the grid when shown and cause grid layout to be different from
+ // what being computed above. This, in turn, may cause scrollbar shown
+ // for directory tiles, and introduce jitter when grid width is aligned
+ // exactly on the column boundary
+ this._node.style.width = gridColumns * this._cellWidth + "px";
+ this._node.style.maxWidth = gGridPrefs.gridColumns * this._cellWidth +
+ GRID_WIDTH_EXTRA + "px";
+ this._node.style.height = this._computeHeight() + "px";
+ this._node.style.maxHeight = this._computeHeight(gridRows) - SPONSORED_TAG_BUFFER + "px";
+ }
+};
diff --git a/browser/base/content/newtab/newTab.css b/browser/base/content/newtab/newTab.css
new file mode 100644
index 000000000..658ad2ed3
--- /dev/null
+++ b/browser/base/content/newtab/newTab.css
@@ -0,0 +1,654 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html {
+ width: 100%;
+ height: 100%;
+}
+
+body {
+ font: message-box;
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ margin: 0;
+ background-color: #F9F9F9;
+ display: -moz-box;
+ position: relative;
+ -moz-box-flex: 1;
+ -moz-user-focus: normal;
+ -moz-box-orient: vertical;
+}
+
+input {
+ font: message-box;
+ font-size: 16px;
+}
+
+input[type=button] {
+ cursor: pointer;
+}
+
+/* UNDO */
+#newtab-undo-container {
+ transition: opacity 100ms ease-out;
+ -moz-box-align: center;
+ -moz-box-pack: center;
+}
+
+#newtab-undo-container[undo-disabled] {
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* CUSTOMIZE */
+#newtab-customize-button {
+ position: absolute;
+ top: 10px;
+ right: 20px;
+ z-index: 101;
+}
+
+#newtab-customize-button:dir(rtl) {
+ left: 20px;
+ right: auto;
+}
+
+/* MARGINS */
+#newtab-vertical-margin {
+ display: -moz-box;
+ position: relative;
+ -moz-box-flex: 1;
+ -moz-box-orient: vertical;
+}
+
+#newtab-margin-undo-container {
+ display: -moz-box;
+ left: 6px;
+ position: absolute;
+ top: 6px;
+ z-index: 1;
+}
+
+#newtab-margin-undo-container:dir(rtl) {
+ left: auto;
+ right: 6px;
+}
+
+#newtab-undo-close-button:dir(rtl) {
+ float:left;
+}
+
+#newtab-horizontal-margin {
+ display: -moz-box;
+ -moz-box-flex: 1;
+}
+
+#newtab-margin-top,
+#newtab-margin-bottom {
+ display: -moz-box;
+ position: relative;
+}
+
+#newtab-margin-top {
+ -moz-box-flex: 1;
+}
+
+#newtab-margin-bottom {
+ -moz-box-flex: 2;
+}
+
+.newtab-side-margin {
+ min-width: 10px;
+ -moz-box-flex: 1;
+}
+
+/* GRID */
+#newtab-grid {
+ -moz-box-flex: 5;
+ overflow: hidden;
+ text-align: center;
+ transition: 100ms ease-out;
+ transition-property: opacity;
+}
+
+#newtab-grid[page-disabled] {
+ opacity: 0;
+}
+
+#newtab-grid[locked],
+#newtab-grid[page-disabled] {
+ pointer-events: none;
+}
+
+body:not(.compact) #topsites-heading {
+ display: none;
+}
+
+/*
+ * If you change the sizes here, make sure you
+ * change the preferences:
+ * toolkit.pageThumbs.minWidth
+ * toolkit.pageThumbs.minHeight
+ */
+/* CELLS */
+.newtab-cell {
+ display: -moz-box;
+ height: 210px;
+ margin: 20px 10px 35px;
+ width: 290px;
+}
+
+body.compact .newtab-cell {
+ width: 110px;
+ height: 110px;
+ margin: 12px;
+}
+
+/* SITES */
+.newtab-site {
+ position: relative;
+ -moz-box-flex: 1;
+ transition: 100ms ease-out;
+ transition-property: top, left, opacity;
+}
+
+.newtab-site[frozen] {
+ position: absolute;
+ pointer-events: none;
+}
+
+.newtab-site[dragged] {
+ transition-property: none;
+ z-index: 10;
+}
+
+/* LINK + THUMBNAILS */
+.newtab-link,
+.newtab-thumbnail {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+}
+
+/* TITLES */
+.newtab-sponsored,
+.newtab-title,
+.newtab-suggested {
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ text-align: center;
+}
+
+.newtab-sponsored,
+.newtab-title {
+ bottom: 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+}
+
+.newtab-suggested {
+ border: 1px solid transparent;
+ border-radius: 2px;
+ font-size: 12px;
+ height: 17px;
+ line-height: 17px;
+ margin-bottom: -1px;
+ padding: 2px 8px;
+ display: none;
+ margin-left: auto;
+ margin-right: auto;
+ left: 0;
+ top: 215px;
+ -moz-user-select: none;
+}
+
+.newtab-suggested-bounds {
+ max-height: 34px; /* 34 / 17 = 2 lines maximum */
+}
+
+.newtab-title {
+ left: 0;
+ padding: 0 4px;
+}
+
+.newtab-sponsored {
+ background-color: #FFFFFF;
+ border: 1px solid #E2E2E2;
+ border-radius: 3px;
+ color: #4A4A4A;
+ cursor: pointer;
+ display: none;
+ font-family: Arial;
+ font-size: 9px;
+ height: 17px;
+ left: 0;
+ line-height: 6px;
+ padding: 4px;
+ right: auto;
+ top: -15px;
+}
+
+.newtab-site[suggested=true] > .newtab-sponsored {
+ background-color: #E2E2E2;
+ border: none;
+}
+
+.newtab-site > .newtab-sponsored:-moz-any(:hover, [active]) {
+ background-color: #4A90E2;
+ border: 0;
+ color: white;
+}
+
+.newtab-site > .newtab-sponsored[active] {
+ background-color: #000000;
+}
+
+.newtab-sponsored:dir(rtl) {
+ right: 0;
+ left: auto;
+}
+
+.newtab-site:-moz-any([type=enhanced], [type=sponsored], [suggested]) .newtab-sponsored {
+ display: block;
+}
+
+.newtab-site[suggested] .newtab-suggested {
+ display: table;
+}
+
+.sponsored-explain,
+.sponsored-explain a,
+.suggested-explain,
+.suggested-explain a {
+ color: white;
+}
+
+.sponsored-explain,
+.suggested-explain {
+ background-color: rgba(51, 51, 51, 0.95);
+ bottom: 30px;
+ line-height: 20px;
+ padding: 15px 10px;
+ position: absolute;
+ text-align: start;
+}
+
+.sponsored-explain input,
+.suggested-explain input {
+ background-size: 18px;
+ height: 18px;
+ opacity: 1;
+ pointer-events: none;
+ position: static;
+ width: 18px;
+}
+
+/* CONTROLS */
+.newtab-control {
+ position: absolute;
+ opacity: 0;
+ transition: opacity 100ms ease-out;
+}
+
+.newtab-control:-moz-focusring,
+.newtab-cell:not([ignorehover]) > .newtab-site:hover > .newtab-control {
+ opacity: 1;
+}
+
+.newtab-control[dragged] {
+ opacity: 0 !important;
+}
+
+@media (-moz-touch-enabled) {
+ .newtab-control {
+ opacity: 1;
+ }
+}
+
+/* DRAG & DROP */
+
+/*
+ * This is just a temporary drag element used for dataTransfer.setDragImage()
+ * so that we can use custom drag images and elements. It needs an opacity of
+ * 0.01 so that the core code detects that it's in fact a visible element.
+ */
+.newtab-drag {
+ width: 1px;
+ height: 1px;
+ background-color: #fff;
+ opacity: 0.01;
+}
+
+/* SEARCH */
+#newtab-search-container {
+ display: -moz-box;
+ position: relative;
+ -moz-box-pack: center;
+ margin: 40px 0 15px;
+}
+
+body.compact #newtab-search-container {
+ margin-top: 0;
+ margin-bottom: 80px;
+}
+
+#newtab-search-container[page-disabled] {
+ opacity: 0;
+ pointer-events: none;
+}
+
+#newtab-search-form {
+ display: -moz-box;
+ position: relative;
+ height: 36px;
+ -moz-box-flex: 1;
+ max-width: 600px; /* 2 * (290 cell width + 10 cell margin) */
+}
+
+#newtab-search-icon {
+ border: 1px transparent;
+ padding: 0;
+ margin: 0;
+ width: 36px;
+ height: 36px;
+ background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat;
+ position: absolute;
+}
+
+#newtab-search-text {
+ -moz-box-flex: 1;
+ padding-top: 6px;
+ padding-bottom: 6px;
+ padding-inline-start: 34px;
+ padding-inline-end: 8px;
+ background: hsla(0,0%,100%,.9) padding-box;
+ border: 1px solid;
+ border-spacing: 0;
+ border-radius: 2px 0 0 2px;
+ border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+ box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
+ 0 0 2px hsla(210,65%,9%,.1) inset,
+ 0 1px 0 hsla(0,0%,100%,.2);
+ color: inherit;
+ unicode-bidi: plaintext;
+}
+
+#newtab-search-text:dir(rtl) {
+ border-radius: 0 2px 2px 0;
+}
+
+#newtab-search-text[aria-expanded="true"] {
+ border-radius: 2px 0 0 0;
+}
+
+#newtab-search-text[aria-expanded="true"]:dir(rtl) {
+ border-radius: 0 2px 0 0;
+}
+
+#newtab-search-text[keepfocus],
+#newtab-search-text:focus,
+#newtab-search-text[autofocus] {
+ border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6);
+}
+
+#newtab-search-submit {
+ margin-inline-start: -1px;
+ color: transparent;
+ background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
+ padding: 0;
+ border: 1px solid;
+ border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+ border-radius: 0 2px 2px 0;
+ border-inline-start: 1px solid transparent;
+ box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
+ 0 1px 0 hsla(0,0%,100%,.2);
+ cursor: pointer;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 150ms;
+ width: 50px;
+}
+
+#newtab-search-submit:dir(rtl) {
+ border-radius: 2px 0 0 2px;
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl"), linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1));
+}
+
+#newtab-search-text:focus + #newtab-search-submit,
+#newtab-search-text + #newtab-search-submit:hover,
+#newtab-search-text[autofocus] + #newtab-search-submit {
+ border-color: #59b5fc #45a3e7 #3294d5;
+}
+
+#newtab-search-text:focus + #newtab-search-submit,
+#newtab-search-text[keepfocus] + #newtab-search-submit,
+#newtab-search-text[autofocus] + #newtab-search-submit {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#4cb1ff, #1793e5);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
+ 0 0 0 1px hsla(0,0%,100%,.1) inset,
+ 0 1px 0 hsla(210,54%,20%,.03);
+}
+
+#newtab-search-text + #newtab-search-submit:hover {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#4cb1ff, #1793e5);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
+ 0 0 0 1px hsla(0,0%,100%,.1) inset,
+ 0 1px 0 hsla(210,54%,20%,.03),
+ 0 0 4px hsla(206,100%,20%,.2);
+}
+
+#newtab-search-text + #newtab-search-submit:hover:active {
+ box-shadow: 0 1px 1px hsla(211,79%,6%,.1) inset,
+ 0 0 1px hsla(211,79%,6%,.2) inset;
+ transition-duration: 0ms;
+}
+
+#newtab-search-text:focus + #newtab-search-submit:dir(rtl),
+#newtab-search-text[keepfocus] + #newtab-search-submit:dir(rtl),
+#newtab-search-text[autofocus] + #newtab-search-submit:dir(rtl),
+#newtab-search-text + #newtab-search-submit:dir(rtl):hover {
+ background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl-inverted"), linear-gradient(#4cb1ff, #1793e5);
+}
+
+/* CUSTOMIZE */
+#newtab-customize-overlay {
+ opacity: 0;
+ display: none;
+ width: 100%;
+ height: 100%;
+ background: #F9F9F9;
+ z-index: 100;
+ position: fixed;
+ transition: opacity .07s linear;
+}
+
+.newtab-customize-panel-container {
+ position: absolute;
+ margin-right: 40px;
+ right: 0;
+}
+
+.newtab-customize-panel-container:dir(rtl) {
+ right: auto;
+ left: 0;
+}
+
+#newtab-customize-panel {
+ z-index: 999;
+ margin-top: 55px;
+ min-width: 270px;
+ position: absolute;
+ top: 100%;
+ right: -25px;
+ filter: drop-shadow(0 0 1px rgba(0,0,0,0.4)) drop-shadow(0 3px 4px rgba(0,0,0,0.4));
+ transition: all 200ms ease-in-out;
+ transform-origin: top right;
+ transform: translate(-30px, -20px) scale(0) translate(30px, 20px);
+}
+
+#newtab-customize-panel:dir(rtl) {
+ transform-origin: 40px top 20px;
+}
+
+#newtab-customize-panel:dir(rtl),
+#newtab-customize-panel-anchor:dir(rtl) {
+ left: 15px;
+ right: auto;
+}
+
+#newtab-customize-panel[open="true"] {
+ transform: translate(-30px, -20px) scale(1) translate(30px, 20px);
+}
+
+#newtab-customize-panel-anchor {
+ width: 18px;
+ height: 18px;
+ background-color: white;
+ transform: rotate(45deg);
+ position: absolute;
+ top: -6px;
+ right: 15px;
+}
+
+#newtab-customize-title {
+ color: #7A7A7A;
+ font-size: 14px;
+ background-color: #FFFFFF;
+ line-height: 25px;
+ padding: 15px;
+ font-weight: 600;
+ cursor: default;
+ border-radius: 5px 5px 0px 0px;
+ max-width: 300px;
+ overflow: hidden;
+ display: table-cell;
+ border-top: none;
+}
+
+#newtab-customize-panel-inner-wrapper {
+ background-color: #FFFFFF;
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+#newtab-customize-title > label {
+ cursor: default;
+}
+
+#newtab-customize-panel > .panel-arrowcontainer > .panel-arrowcontent {
+ padding: 0;
+}
+
+.newtab-customize-panel-item {
+ line-height: 25px;
+ padding: 15px;
+ padding-inline-start: 40px;
+ font-size: 14px;
+ cursor: pointer;
+ max-width: 300px;
+}
+
+.newtab-customize-panel-item:not(:first-child) {
+ border-top: 1px solid threedshadow;
+}
+
+.newtab-customize-panel-subitem > label,
+.newtab-customize-panel-item > label,
+.newtab-customize-complex-option {
+ padding: 0;
+ margin: 0;
+ cursor: pointer;
+}
+
+.newtab-customize-panel-item,
+.newtab-customize-complex-option {
+ display: block;
+ text-align: start;
+ background-color: #F9F9F9;
+}
+
+.newtab-customize-panel-item[selected]:-moz-locale-dir(rtl) {
+ background-position: right 15px center;
+}
+
+.newtab-customize-complex-option:hover > .selectable:not([selected]):-moz-locale-dir(rtl),
+.selectable:not([selected]):hover:-moz-locale-dir(rtl) {
+ background-position: right 15px center;
+}
+
+.newtab-customize-panel-item:not([selected]),
+.newtab-customize-panel-subitem:not([selected]){
+ color: #7A7A7A;
+}
+
+.newtab-customize-panel-item:not([selected]):hover {
+ color: #FFFFFF;
+ background-color: #4A90E2
+}
+
+.newtab-customize-complex-option:hover > .selectable:not([selected]),
+.selectable:not([selected]):hover {
+ background: url("chrome://global/skin/menu/shared-menu-check-hover.svg") no-repeat #FFFFFF;
+ background-size: 16px 16px;
+ background-position: 15px 15px;
+ color: #171F26;
+}
+
+.newtab-customize-complex-option:hover > .selectable:not([selected]) + .newtab-customize-panel-subitem {
+ background-color: #FFFFFF;
+}
+
+.newtab-customize-panel-item[selected] {
+ background: url("chrome://global/skin/menu/shared-menu-check-active.svg") no-repeat transparent;
+ background-size: 16px 16px;
+ background-position: 15px 15px;
+ color: black;
+ font-weight: 600;
+}
+
+.newtab-customize-panel-subitem > .checkbox {
+ width: 18px;
+ height: 18px;
+ background-color: #FFFFFF;
+ border: solid 1px threedshadow;
+}
+
+.newtab-customize-panel-subitem[selected] > .checkbox {
+ background: url("chrome://global/skin/menu/shared-menu-check-black.svg") no-repeat #FFFFFF;
+ background-size: 9px 9px;
+ background-position: center;
+ color: #333333;
+}
+
+.newtab-customize-panel-subitem {
+ font-size: 12px;
+ padding: 0px 15px 15px 15px;
+ padding-inline-start: 40px;
+ display: block;
+ max-width: 300px;
+}
+
+.newtab-customize-panel-subitem > label {
+ padding: 0px 10px;
+ line-height: 20px;
+ vertical-align: middle;
+ max-width: 225px;
+}
+
+.newtab-customize-panel-superitem {
+ line-height: 20px;
+ border-bottom: medium none !important;
+ padding: 15px 15px 10px 15px;
+ padding-inline-start: 40px;
+ border-top: 1px solid threedshadow;
+}
+
+.contentSearchSuggestionTable {
+ font: message-box;
+ font-size: 16px;
+}
diff --git a/browser/base/content/newtab/newTab.inadjacent.json b/browser/base/content/newtab/newTab.inadjacent.json
new file mode 100644
index 000000000..53fb542af
--- /dev/null
+++ b/browser/base/content/newtab/newTab.inadjacent.json
@@ -0,0 +1,3209 @@
+{
+ "domains": [
+ "rp5slFCxq/e7hYhXJCd0vQ==",
+ "2rEimAJDNX5g8HPZehOrGg==",
+ "nvLEpj6ZZF3LWH3wUB6lKg==",
+ "9Cqd4Lm3VvXuJxz79Bbqyg==",
+ "vNRy4LR+7TOKTixqsr5ybw==",
+ "N4zSgsZCo6Z4XRwZ4fu8WQ==",
+ "jsDtRfVbMsFg3KkEl2UiZQ==",
+ "TckkKpiq0a6J6NTw7uOZqw==",
+ "9Or7IAYuuIgZA370w9rNIg==",
+ "ul8WvOjCkxTz9LjT4RqTHg==",
+ "ZGJrbwb5878Nsqm0z+A7nQ==",
+ "5iT64HTeeG5SIFXG7A9o3w==",
+ "YSeSEghPe1kV6g8ghFcNAA==",
+ "0jIUl1NDmJZQkDY12VDeIQ==",
+ "aos6UyDyIw0R1nTK5wTawA==",
+ "G1xxubsq65ugK06UT2DO5A==",
+ "lbhavoDrDPP/8m0onwo63w==",
+ "ObcLsjW0SkdvY0nkZmiTGQ==",
+ "FHZ5084LC0nTAzZlnSKN3Q==",
+ "cdEr+0Fv5iaVZzalZToseg==",
+ "Co8WbNYbCPTFPcHpeK3hRQ==",
+ "qXSzhCEhByLQq9N84tqV+Q==",
+ "h3ufhRk5IEFaNH11rIACtQ==",
+ "fQ1PJ/JwazIaYoy/zy49QQ==",
+ "zAJqfbn54Nsm2ddGtkb59A==",
+ "ixPM9T8ik/gWGZ7BRIcaig==",
+ "/E9pwA3E3hVAZoYq3FmCyw==",
+ "U6ygonI8CxpruhpGB2+Q6A==",
+ "Igi4voB8oVMVw6WUeDSjZg==",
+ "jtuHIJhwoTGzavFpM7ilNw==",
+ "eBvTV27n6Gs+ZsBkpVynvw==",
+ "sFbzw0AUOGG0NEzkaSxVDg==",
+ "yAkIS+Ezj6woEff9YvdO7Q==",
+ "IP1+BwG6q60QzDADi8j7oA==",
+ "Q/teQEBFepHtwZ7UHa2TEA==",
+ "B1vDep5a1Gok5Gnth39+LA==",
+ "cyEIyQ2MZaPGf+K1x9Bbkg==",
+ "aaM+oEJnF4/nwMWyXJU8rA==",
+ "qpDNIpxah8FUiqXm5IRaUg==",
+ "ZTeJ35gMPqIv2WWbeNyIEg==",
+ "nzoAGQAnC/Xgg5PmOgXqkA==",
+ "J5pJDuNi3cqQiyaRJAJk4g==",
+ "2vqN53BXhXzPrKYsh6QH1A==",
+ "QlrzHNYxCwCBMVENvbXjQA==",
+ "Ou2HGn43nmsL3RWSNvMdXw==",
+ "3qk9lsvGTMqVMAZW+xihfw==",
+ "RncMe42RB2bhmUbYtGVnKQ==",
+ "hzNXR6dqPq1+vf4Qh5ByWA==",
+ "sRq3S2ZRs3H39cEQHv4Vig==",
+ "B4ThUBTVJOUPyOsHxikHXA==",
+ "A2lU9GkAdSibLO1JJfFnIA==",
+ "ef3HNkSvuWQrAzkuty2iqg==",
+ "yKDiRM6bf2xc0QXIwHYuaA==",
+ "AdCk4ccJuhA0bIT/61J+RQ==",
+ "UXvAZ7ULCVz2f505K0Wkvg==",
+ "ueKWblrOwVJNgiOvkXKLBQ==",
+ "s8u/jPuBAxu1d18HfV5Z0g==",
+ "hUT0Uc5YMUdNZQEGLz4hJw==",
+ "6jo/phmMTrEXKrNRsionGQ==",
+ "s/Ea/3fkyJ9honzPJkgEQQ==",
+ "hgu2/Jf+WrQAHfO+asW2zw==",
+ "kiVuTNwZ1r2lqYEZxIHyiQ==",
+ "24T5KVrVE2mYwJ5Goj3xJw==",
+ "fiWBVlfj97GGjEvf/Q9Spg==",
+ "5VWdlvJe7eoXMGkTtHzCUg==",
+ "+cFQxKa5RWVtc1z00Jujew==",
+ "nVa+rLH5p+yXBksLwQsjRQ==",
+ "5tyI6bMdb3tMIi4ewvr/SQ==",
+ "S6Roj31yS5bZbSFcd3f4Hg==",
+ "uW1Zl8iuEF8ZT/gwCBEqwA==",
+ "YwL+FJgxlZ8JVig+9iP5Cw==",
+ "ThIYK/mQsp9cMf8+rws/4Q==",
+ "w0oSxOhRG6kE9B868aoYVQ==",
+ "DJUDGQ0J32dF1kfItyxALg==",
+ "34/ab69lPkuAKt6WBxJPpA==",
+ "25jH4C9apgqWZGZP15lM6Q==",
+ "GxvwleSaSwILD1pG9k9buA==",
+ "YRAMt2ArEINo83ms6AqJ0A==",
+ "15HyTJNoMYzi3XCkeU5Z7A==",
+ "/SqjXGD+TKC90uz1vsjqUw==",
+ "karhKOknkhtg/LSFo9BGRA==",
+ "+tD1d0t3vfJvc1hUAvTT4Q==",
+ "rkaKbtlnyVr53D0rexLqdQ==",
+ "fAugw4rtnXzzRXfC1wRgOQ==",
+ "RgxoepF/XOwIsGat5r5HpQ==",
+ "Y49/EnzVz3ugXCYxFjFN7g==",
+ "tHMyzBm/2wNDw7TeNeujbg==",
+ "LlqzYV4uZpiJy4ORWPSekA==",
+ "M3Huar7/ded9OGgDwJhZgw==",
+ "QkNNATSx/PJ1XjgZyTtkUQ==",
+ "skVw0v6Wx00sfHAScPK1Bw==",
+ "v1gsIvg+C68T9wixMzL2sQ==",
+ "hDL75EXhl7BaYnAxkoGwbw==",
+ "tReG97snx2ESpXbfllCL7Q==",
+ "EiZBXR15dT1TMrkgkzmkvw==",
+ "5hRHirfD90/sdp0ILJQU8A==",
+ "rabElvtYtG0jW6dxAOHofg==",
+ "JpxoTRKWN+SEeBQ453R1YQ==",
+ "Faz4Lm0cpjvF0IjVkHiZMg==",
+ "jGErFAIoXx+50KFpVIGZiA==",
+ "5GzBkduKpUX7u1uwtYIFug==",
+ "cRBe0J9/KWRX19N2vPkCiw==",
+ "t/7g8t4Kr3/+SCnOn3XFWQ==",
+ "sd08c6jUXs5/hxND0fBkPA==",
+ "nTxKpqIdnHNdpDk7Dx3TEg==",
+ "5l+RALcce+lTDnmXI+Wqqg==",
+ "pzJ4QmEBGRNiiX6z0xHh8g==",
+ "Vfl3YbqR6JRR7SIdsUA/vA==",
+ "cgfhOdB376a4GAcuACADvA==",
+ "inAefsQM6tiIhQCMtcPcyA==",
+ "FTSULGL8CMhmcc7Cyf/X8A==",
+ "9XSWpaZyHEy7V/tuw5uZEw==",
+ "+VtM1opKlgb/jrCwc4YjFA==",
+ "oF46xheuI/NUxUOnOttzvA==",
+ "Qy+lvhDbJCumr6kiPLd1oA==",
+ "swps7UEKpIbVBJ9SnPK3zQ==",
+ "b7wyIiJvJs+29QePxsdWtQ==",
+ "x3iDZxYyuHtG/9rNW5HMYg==",
+ "r1dx1g5UOksywvOaQTamfA==",
+ "KvVF3Si/fr4JQtr7jCJiog==",
+ "spvZ7hhtG5QY7JXs96lBUg==",
+ "ECL8mA5B6CswyDH6yJ4hVw==",
+ "7Uu+YsdS69dMSDYUr6vTag==",
+ "Rnm9pSvQRRbkHpOijraLZw==",
+ "aQJqpnXdzqNSFhMn3EJA2Q==",
+ "TctnXpd7Wd5ZXKMnOFHAQA==",
+ "+lPqG8l6mf2FWVGWflyF/g==",
+ "mPmnmL2oRRJmKYjQ6TfN3g==",
+ "fyXFcT5ZCawDBg74n1WSpg==",
+ "uq5Zrxq10pO1HoPxReT5og==",
+ "3eoCsOKXY8RDrHSdlXqmrA==",
+ "9nQv2BFG56xsHViN5UpHYw==",
+ "RtP/nJgy/ItyuDrpBbAotg==",
+ "5E/drRptfHmBhJ7qplujGg==",
+ "cUxyZvoqXbQ0a/0I9s6Zbg==",
+ "womzqSigwEF30V422YmxKw==",
+ "FPvZqDfN8dTFHLVOuYEbUA==",
+ "YZMXx+scKXp/v9GaJjb1bA==",
+ "bjURu5MRsNIZavG5HV0eZw==",
+ "iY0C9uSMEOn8ikT+J7+/Eg==",
+ "aXkD6BzsdkMEv7A+eYqQQQ==",
+ "dOcOfEDGHYG2kgmrglDkPw==",
+ "c7GjtY05Mh+cp6SNuWY3Ig==",
+ "lM1uY1oVncHXNzKs/cCEtQ==",
+ "7jXnQJkutLsi+r9aYmrMxw==",
+ "NgrugWWduj2qdWnEQf9dLA==",
+ "faYjmy/yn5iXdS28QCIdWw==",
+ "68XbaOvIZpCGb4G1gaKErA==",
+ "Yi67HkOLtGYXeL7WD4GPrA==",
+ "Puo8gXuUkwcoQViaXwkdSQ==",
+ "L202Et5aZh60Vl20LTKNFg==",
+ "4agAzQ5+dnTmLZEjsZs26g==",
+ "LegGM1ft8Y7Ka3CUxpObvg==",
+ "KRdILc1QDOpow5im/qY+Kw==",
+ "peMW+rpwmXrSwplVuB/gTA==",
+ "Pic1ncr+Zn6wv75zjAdzQA==",
+ "ilSPlWYbiPzIC13vQUBlOw==",
+ "GUlDufLoTalBqrG/h3mZ6w==",
+ "5twANNlT57T9BG4r2D9+Hw==",
+ "ENrnM8HlMi+5y8Hsu4Pn4A==",
+ "K/DzpLEbz1MpRjA6qyYn4Q==",
+ "yN1cHJRHDXoFxFZacL6wsw==",
+ "Rc6r+KqIePH+dnj1aNYCsQ==",
+ "8u/z5htgqXVU5Dqwd9whJQ==",
+ "jV575O42EYoqDNxCm9643Q==",
+ "xCxGo0h3lS8N6X+ivKfpjA==",
+ "us+2nfpj2gjI7s14Hw0gmA==",
+ "bp90A/rbESwVU7eh9xRTfQ==",
+ "5QtMXzbTafvKDQOWZP7M8w==",
+ "1gFCxPLjQlQGKmSGmHwmJQ==",
+ "m+/dnOIe6SaIFhfvg+ybDg==",
+ "9Dcg87+RPq9U+swRg4dH3Q==",
+ "mnjbL7WFmrWp0RUqS8AMGA==",
+ "/0e0E+NFmq8GeE5+y2Gekw==",
+ "y11mbpHHtka9Ep8cr2nEvQ==",
+ "GdmjRyliw+W21Q+dHO4CWA==",
+ "X65wWQTpkg756V/Nfn92kQ==",
+ "xj06KvacQOxRSofbhzBNgA==",
+ "nVDxVhaa2o38gd1XJgE3aw==",
+ "4IV+JOGXrltpkQamBRXMgA==",
+ "jIfp8LqaYXT88r/K3a8gNw==",
+ "vhT4dDtbMFVyevS6yCGy0g==",
+ "zMs7/x8hDt8xj2FFc5+6vA==",
+ "1J7u2N62JGb2VrnCRlJIrw==",
+ "3hJs9P/RRxB0CO4q0Icb+g==",
+ "ZpuVY1ZyoKD3hqosdsfT6Q==",
+ "6KIM7C7eWgxZtqZboiJvZQ==",
+ "vtb6fdqirkuUkqITAmXTlw==",
+ "C/rEVr22mw2u/1dwUx9VTg==",
+ "NrY4Q5C67haCWLK8HXHq9g==",
+ "X9qvEftCEFWX3gBU5hXy+Q==",
+ "0zgw7xNB3xVGaH48TyxaNQ==",
+ "g7J9Jy/PJrAGRgVdvA+bEg==",
+ "9zb+anAyZVBzuU9rW4cJtg==",
+ "6Zc5FzT/m0YIjxEPYA6zDQ==",
+ "R2YPNlvCbVK0EodTR7czIw==",
+ "gsI6EGgXMtDu+1u364A8mw==",
+ "Bg2wBFb1/xaxeEiHfBHX+A==",
+ "64aapfVI6dV3LpTK56KZlg==",
+ "HdgcJU0W3yVnH69VYStmug==",
+ "qTDCcv+LK3JPFB/++t66IQ==",
+ "P0HEIXMnAmbvq+QYREwFzw==",
+ "aaU7CAmtyE35jNKTkyXOkg==",
+ "r9G97WKDiQ48qJHP9LBRNg==",
+ "8mPgQhYVDn8KshDDvvf5SA==",
+ "GCQiiOLDguXLiYwuLcFPsA==",
+ "R2Use39If2C0FVBP7KDerA==",
+ "23C4eh3yBb5n/RNZeTyJkA==",
+ "2QQtKtBAm2AjJ5c0WQ6BQA==",
+ "Qc+XYy2qyWJ5VVwd2PExbw==",
+ "zJ7ScHNxr2leCDNNcuDApA==",
+ "vFtC0B2oe1gck28JOM1dyg==",
+ "bLEntCrCHFy9pg3T3gbBzg==",
+ "G3PmmPGHaWHpPW30xQgm3Q==",
+ "me61ST+JrXM5k3/a11gRAA==",
+ "+LJYVZl1iPrdMU3L5+nxZw==",
+ "CLPzjXKGGpJ0VrkSJp7wPQ==",
+ "Pc+u0MAzp4lndTz4m6oQ5w==",
+ "cwBNvZc0u4bGABo88YUsVQ==",
+ "q7m/EtZySBjZNBjQ5m1hKw==",
+ "8ZBiwr842ZMKphlqmNngHw==",
+ "LMCZqd3UoF/kHHwzTdj7Tw==",
+ "0ODJyWKJSfObo+FNdRQkkA==",
+ "ViweSJuNWbx5Lc49ETEs/A==",
+ "x+8rwkqKCv0juoT5m1A4eg==",
+ "pxuSWn1u+bHtRjyh2Z8veA==",
+ "GKzs8mlnQQc58CyOBTlfIg==",
+ "Owg8qCpjZa+PmbhZew6/sw==",
+ "YLz+HA6qIneP+4naavq44Q==",
+ "9ajIS45NTicqRANzRhDWFA==",
+ "DjeSrUoWW2QAZOAybeLGJg==",
+ "qxALQrqHoDq9d91nU0DckA==",
+ "yPIeWcW8+3HjDagegrN8bw==",
+ "ocpLRASvTgqfkY20YlVFHQ==",
+ "RuLeQHP1wHsxhdmYMcgtrQ==",
+ "3WwITQML938W9+MUM56a3A==",
+ "ZbLVNTQSVZQWTNgC4ZGfQg==",
+ "X6Ln4si8G5aKar52ZH/FEQ==",
+ "+gbitI/gpxebN/rK7qj8Fw==",
+ "7cnUHeaPO8txZGGWHL9tKg==",
+ "epY+dsm5EMoXnZCnO4WSHw==",
+ "nf8x+F03kOpMhsCSUWEhVg==",
+ "VE4sLM5bKlLdk85sslxiLQ==",
+ "Hs3vUOOs2TWQdQZHs+FaQQ==",
+ "hkOBNoHbno2iNR7t3/d4vg==",
+ "Ar9N1VYgE7riwmcrM3bA2Q==",
+ "SbMjjI8/P8B9a9H2G0wHEQ==",
+ "tU31r8zla146sqczdKXufg==",
+ "tFmWYH82I3zb+ymk5dhepA==",
+ "XHjrTLXkm/bBY/BewmJcCQ==",
+ "FV/D5uSco+Iz8L+5t7E8SA==",
+ "yKLLiqzxfrCsr6+Rm6kx1Q==",
+ "B6reUwMkQFaCHb9BYZExpw==",
+ "5jyuDp82Fux+B0+zlx8EXw==",
+ "WGKFTWJac8uehn3N59yHJw==",
+ "JQf9UmutPh3tAnu7FDk3nA==",
+ "hv5GrLEIjPb4bGOi8RSO0w==",
+ "p3V7NfveB6cNxFW7+XQNeQ==",
+ "DinJuuBX9OKsK5fUtcaTcQ==",
+ "UEMwF4kwgIGxGT4jrBhMPQ==",
+ "Y78dviyBS3Jq9zoRD5sZtQ==",
+ "zbjXhZaeyMfdTb2zxvmRMg==",
+ "kydoXVaNcx1peR5g6i588g==",
+ "M2suCoFHJ5fh9oKEpUG3xA==",
+ "/VnKh/NDv7y/bfO6CWsLaQ==",
+ "S+b37XhKRm8cDwRb1gSsKQ==",
+ "jz7QlwxCIzysP39Cgro8jg==",
+ "IjmLaf3stWDAwvjzNbJpQA==",
+ "cHSj5dpQ04h/WyefjABfmQ==",
+ "+gO0bg8LY+py2dLM1sM7Ag==",
+ "fSANOaHD0Koaqg7AoieY9A==",
+ "vqYHQ3MnHrAIAr1QHwfIag==",
+ "Uh1mvZNGehK1AaI4a1auKQ==",
+ "HCbHUfsTDl6+bxPjT57lrA==",
+ "S7Vjy/gOWp0HozPP1RUOZw==",
+ "KPh6TwYpspne4KZA6NyMbw==",
+ "cfh5VZFmIqJH/bKboDvtlA==",
+ "H1zH9I8RwfEy5DGz3z+dHw==",
+ "2ksediOVrh4asSBxKcudTg==",
+ "+jVN/3ASc2O44sX6ab8/cg==",
+ "uvKYnKE01D5r7kR9UQyo5A==",
+ "BB9PTlwKAWkExt3kKC/Wog==",
+ "yqQPU4jT9XvRABZgNQXjgg==",
+ "6v3eTZtPYBfKFSjfOo2UaA==",
+ "49z/15Nx9Og7dN9ebVqIzg==",
+ "VjclDY8HN4fSpB263jsEiQ==",
+ "vSKsa0JhLCe9QFZKkcj58Q==",
+ "PolhKCedOsplEcaX4hQ0YQ==",
+ "D0Qt9sRlMaPnOv1xaq+XUg==",
+ "gBgJF0PiGEfcUnXF0RO7/w==",
+ "sC11Rf/mau3FG5SnON4+vQ==",
+ "rKb3TBM4EPx/RErFOFVCnQ==",
+ "+n0K7OB2ItzhySZ4rhUrMg==",
+ "Epm0d/DvXkOFeM4hoPCBrg==",
+ "K8PVQhEJCEH1ghwOdztjRw==",
+ "xjA21QjNdThLW3VV7SCnrg==",
+ "nE72uQToQFVLOzcu/nMjww==",
+ "2Hc5oyl0AYRy2VzcDKy+VA==",
+ "Y7XpxIwsGK3Lm/7jX/rRmg==",
+ "MK7AqlJIGqK2+K5mCvMXRQ==",
+ "mXycPfF5zOvcj1p4hnikWw==",
+ "V1fvtnJ0L3sluj9nI5KzRw==",
+ "TahqPgS7kEg+y6Df0HBASw==",
+ "EKU3OVlT4b/8j3MTBqpMNg==",
+ "EdvIAKdRAXj7e42mMlFOGQ==",
+ "uPm+cF4Jq08S5pQhYFjU8A==",
+ "CnIwpRVC2URVfoiymnsdYQ==",
+ "wyx5mnUMgP5wjykjAfTO7w==",
+ "OwIGvTh8FPFqa4ijNkguAw==",
+ "4ID0PHTzIMZz2rQqDGBVfA==",
+ "rlXt6zKE7DswUl0oWGOQUQ==",
+ "4NP8EFFJyPcuQKnBSxzKgQ==",
+ "bJgsuw29cO2WozqsGZxl7w==",
+ "b3q8kjHJPj9DWrz3yNgwjQ==",
+ "QGYFMpkv37CS2wmyp42ppg==",
+ "Kzs+/IZJO8v4uIv9mlyJ2Q==",
+ "ZJY+hujfd58mTKTdsmHoQQ==",
+ "R8FxgXWKBpEVbnl41+tWEw==",
+ "+CvLiih/gf2ugXAF+LgWqw==",
+ "BDbfe/xa9Mz1lVD82ZYRGA==",
+ "Dz90OhYEjpaJ/pxwg1Qxhg==",
+ "MLHt6Ak288G0RGhCVaOeqA==",
+ "r0QffVKB9OD9yGsOtqzlhA==",
+ "hK8KhTFcR06onlIJjTji/Q==",
+ "wMum67lfk5E1ohUObJgrOg==",
+ "JKmZqz9cUnj6eTsWnFaB0A==",
+ "rtJdfki8fG6CB36CADp0QA==",
+ "cUyqCa7Oue934riyC17F8g==",
+ "y4Y4mSSTw/WrIdRpktc5Hw==",
+ "r36kVMpF+9J+sfI3GeGqow==",
+ "ydVj2odhergi+2zGUwK4/A==",
+ "J2NFyb8cXEpZyxWDthYQiA==",
+ "qYuo5vY8V3tZx41Kh9/4Dw==",
+ "jrfRznO0nAz6tZM1mHOKIA==",
+ "JSr/lqDej81xqUvd/O2s7w==",
+ "vHGjRRSlZHJIliCwIkCAmQ==",
+ "sQAxqWXeiu/Su0pnnXgI9A==",
+ "xPe76nHyHmald6kmMQsKdg==",
+ "50jASqzGm4VyHJbFv8qVRA==",
+ "uuiJ+yB7JLDh2ulthM0mjg==",
+ "TI90EuS/bHq/CAlX32UFXg==",
+ "JgxNrUlL8wutG04ogKFPvw==",
+ "aMa1yVA71/w6Uf1Szc9rMA==",
+ "k/Aou2Jmyh8Bu3k8/+ndsQ==",
+ "iANKiuMqWzrHSk9nbPe3bQ==",
+ "7GgNLBppgAKcgJCDSsRqOQ==",
+ "bzVeU2qM9zHuzf7cVIsSZw==",
+ "rkeLYwMZ1/pW2EmIibALfA==",
+ "91+Yms6Oy/rP0rVjha5z9w==",
+ "JgXSPXDqaS1G9NqmJXZG0A==",
+ "ZzduJxTnXLD9EPKMn1LI4Q==",
+ "6W79FmpUN1ByNtv5IEXY4w==",
+ "Y1Nm3omeWX2MXaCjDDYnWQ==",
+ "ejfikwrSPMqEHjZAk3DMkA==",
+ "WNfDNaWUOqABQ6c6kR+eyw==",
+ "4BkqgraeXY7yaI1FE07Evw==",
+ "AjHz9GkRTFPjrqBokCDzFw==",
+ "T/6gSz2HwWJDFIVrmcm8Ug==",
+ "VWy9lB5t4fNCp4O/4n8S4w==",
+ "/FdZzSprPnNDPwbhV1C0Cg==",
+ "LUWxfy4lfgB5wUrqCOUisw==",
+ "r1VGXWeqGeGbfKjigaAS+Q==",
+ "ztULoqHvCOE6qV7ocqa4/w==",
+ "QCpzCTReHxGm5lcLsgwPCA==",
+ "Hst3yfyTB7yBUinvVzYROQ==",
+ "gf1Ypna/Tt+TZ08Y+GcvGg==",
+ "3rbml1D0gfXnwOs5jRZ3gA==",
+ "2vm7g3rk1ACJOTCXkLB3zA==",
+ "11FE2kknwYi2Qu0JUKMn3A==",
+ "1b2uf+CdVjufqiVpUShvHw==",
+ "0a4SafpDIe8V4FlFWYkMHw==",
+ "7btpMFgeGkUsiTtsmNxGQA==",
+ "dUx1REyXKiDFAABooqrKEA==",
+ "knYKU74onR6NkGVjQLezZg==",
+ "Scto+9TWxj1eZgvNKo+a9A==",
+ "cvZT1pvNbIL8TWg+SoTZdA==",
+ "1nXByug2eKq0kR3H3VjnWQ==",
+ "tG+rpfJBXlyGXxTmkceiKA==",
+ "7W9aF7dxnL+E8lbS/F7brg==",
+ "8vr+ERVrM99dp+IGnCWDGQ==",
+ "oFNMOKbQXcydxnp8fUNOHw==",
+ "uJZGw3IY2nCcdVeWW1geNQ==",
+ "q6LG0VzO1oxiogAAU63hyg==",
+ "f0H/AFSx2KLZi9kVx5BAZg==",
+ "1RQZ2pWSxT+RKyhBigtSFg==",
+ "scCQPl0em2Zmv/RQYar60g==",
+ "A2ODff+ImIkreJtDPUVrlg==",
+ "vRgkZZGVN7YZrlml0vxrKA==",
+ "68jPYo3znYoU4uWI7FH3/g==",
+ "iJ2nT8w8LuK11IXYqBK+YA==",
+ "54XELlPm8gBvx8D5bN3aUg==",
+ "PTAm/jGkie7OlgVOvPKpaA==",
+ "v7BrkRmK0FfWSHunTRHQFQ==",
+ "dVh/XMTUIx1nYN4q1iH1bA==",
+ "TSGL3iQYUgVg/O9SBKP9EA==",
+ "wTO49YX/ePHMWtcoxUAHpw==",
+ "bMb1ia0rElr2ZpZVhva0Jw==",
+ "sNmW2b2Ud7dZi3qOF8O8EQ==",
+ "3djRJvkZk9O2bZeUTe+7xQ==",
+ "I9KNZC1tijiG1T72C4cVqQ==",
+ "sQzCwNDlRsSH7iB9cTbBcg==",
+ "mk1CKDah7EzDJEdhL22B7w==",
+ "lON3WM0uMJ30F8poBMvAjQ==",
+ "88PNi9+yn3Bp4/upgxtWGA==",
+ "C+Ssp+v1r+00+qiTy2d7kA==",
+ "11U5XEwfMI7avx014LfC8g==",
+ "xsf0m31Am0W9eLhopAkfnA==",
+ "d13Rj3NJdcat0K/kxlHLFw==",
+ "UP7NXAE0uxHRXUAWPhto0w==",
+ "ZKXxq9yr7NGBOHidht34uQ==",
+ "Fd2fYFs8vtjws2kx1gf6Rw==",
+ "ojf6uL85EuEYgLvHoGhUrw==",
+ "KjnL3x+56r3M2pDj1pPihA==",
+ "WdCWezJU4JK43EOZ9YHVdg==",
+ "/jH6imhTPZ/tHI4gYz2+HA==",
+ "+OLntmlsMBBYPREPnS6iVw==",
+ "5lfLJAk1L3QzGMML3fOuSw==",
+ "AZs3v4KJYxdi8T1gjVjI2Q==",
+ "7pkUY2UzSbGnwLvyRrbxfA==",
+ "BjfOelfc1IBgmUxMJFjlbQ==",
+ "TcGhAJHRr7eMwGeFgpFBhg==",
+ "Y7iDCWYrO1coopM3RZWIPg==",
+ "mnalaO6xJucSiZ0+99r3Cg==",
+ "plXHHzA8X9QGwWzlJxhLRw==",
+ "Zqd6+81TwYuiIgLrToFOTQ==",
+ "1Pmnur6TbZ9cmemvu0+dSA==",
+ "OaNpzwshdHUZMphQXa6i8w==",
+ "WKehT4nGF2T7aKuzABDMlA==",
+ "4LvQSicqsgxQFWauqlcEjw==",
+ "BMZB1FwvAuEqyrd0rZrEzw==",
+ "YfbfE3WyYOW7083Y8sGfwQ==",
+ "46FCwqh+eMkf+czjhjworw==",
+ "734u4Y1R3u7UNUnD+wWUoA==",
+ "yf06Slv9l3IZEjVqvxP2aA==",
+ "bIk7Fa6SW7X18hfDjTKowg==",
+ "DnF6TYSJxlc+cwdfevLYng==",
+ "ionqS0piAOY2LeSReAz4zg==",
+ "hlMumZ7RJFpILuKs09ABtw==",
+ "NjeDgQ1nzH1XGRnLNqCmSg==",
+ "o7y4zQXQAryST2cak4gVbw==",
+ "29EybnMEO95Ng4l/qK4NWQ==",
+ "udU65VtsvJspYmamiOsgXw==",
+ "v1AWe5qb5y3vSKFb7ADeEw==",
+ "wK6Srd83eLigZ11Q20XGrg==",
+ "GmC+0rNDMIR+YbUudoNUXw==",
+ "W4utAK3ws0zjiba/3i91YA==",
+ "MlKWxeEh8404vXenBLq4bw==",
+ "Gdf4VEDLBrKJNQ8qzDsIyw==",
+ "Z9bDWIgcq6XwMoU2ECDR5Q==",
+ "VIkS30v268x+M1GCcq/A8A==",
+ "iPwX3SbbG9ez9HoHsrHbKw==",
+ "yKrsKX4/1B1C0TyvciNz5w==",
+ "BophnnMszW5o+ywgb+3Qbw==",
+ "eJLrGwPRa6NgWiOrw1pA7w==",
+ "eV+RwWPiGEB+76bqvw+hbA==",
+ "oad5SwflzN0vfNcyEyF4EA==",
+ "Uw6Iw+TP9ZdZGm2b/DAmkg==",
+ "9qWLbRLXWIBJUXYjYhY2pg==",
+ "dxWv00FN/2Cgmgq9U3NVDQ==",
+ "AX1HxQKXD12Yv5HWi39aPQ==",
+ "J0NauydfKsACUUEpMhQg8A==",
+ "mxug34EekabLz0JynutfBg==",
+ "bNq/hj0Cjt4lkLQeVxDVdQ==",
+ "nW3zZshjZEoM8KVJoVfnuQ==",
+ "ghp8sWGKWw20S/z1tbTxFg==",
+ "S4rFuiKLFKZ+cL7ldiTwpg==",
+ "8ZqmPJDnQSOFXvNMRQYG2Q==",
+ "6XYqR2WvDzx4fWO7BIOTjA==",
+ "Uo+FIhw1mfjF6/M8cE1c/Q==",
+ "bsHIShcLS134C+dTxFQHyA==",
+ "19yQHaBemtlgo2QkU5M6jQ==",
+ "sWLcS+m4aWk31BiBF+vfJQ==",
+ "BlCgDd7EYDIqnoAiKOXX6Q==",
+ "MrxR3cJaDHp0t3jQNThEyg==",
+ "cMo6l1EQESx1rIo+R4Vogg==",
+ "VOvrzqiZ1EHw+ZzzTWtpsw==",
+ "1/ZheMsbojazxt31j/l3iA==",
+ "0QxPAqRF8inBuFEEzNmLjA==",
+ "UXUNYEOffgW3AdBs7zTMFA==",
+ "lOPJhHqCtMRFZfWMX/vFZQ==",
+ "rXSbbRABEf4Ymtda45w8Fw==",
+ "jfegbZSZWkDoPulFomVntA==",
+ "hfcH5Az2M7rp+EjtVpPwsg==",
+ "VsXEBIaMkVftkxt1kIh7TA==",
+ "M20iX2sUfw5SXaZLZYlTaA==",
+ "VUDsc9RMS1fSM43c+Jo9dQ==",
+ "itPtn+JaO4i7wz2wOPOmDQ==",
+ "rCxoo4TP/+fupXMuIM0sDA==",
+ "cSHSg9xJz/3F6kc+hKXkwg==",
+ "b4BoZmzVErvuynxirLxn0w==",
+ "e4B3HmWjW+6hQzcOLru6Xg==",
+ "lTE6u9G/RzvmbuAzq2J2/Q==",
+ "897ptlztTjr7yk+pk8MT0Q==",
+ "jd6IpPJwOJW1otHKtKZ5Gw==",
+ "b4aFwwcWMXsSdgS1AdFOXA==",
+ "FltEN+7NKvzt+XAktHpfHA==",
+ "ZyDh3vCQWzS5DI1zSasXWA==",
+ "kcJ1acgBv6FtUhV8KuWoow==",
+ "zgEyxj/sCs63O98sZS94Yw==",
+ "/kGxvyEokQsVz0xlKzCn2A==",
+ "cxqHS4UbPolcYUwMMzgoOA==",
+ "62RHCbpGU8Hb+Ubn+SCTBg==",
+ "ePlsM/iOMme2jEUYwi15ng==",
+ "0fN+eHlbRS6mVZBbH/B9FQ==",
+ "k0XIjxp2vFG7sTrKcfAihA==",
+ "0rfG4gRugAwVP0i3AGVxxg==",
+ "M98hjSxCwvZ27aBaJTGozQ==",
+ "kzGNkWh3fz27cZer4BspUQ==",
+ "3CJbrUdW68E3Drhe4ahUnQ==",
+ "NGApiVkDSwzO45GT57GDQw==",
+ "lMjip5hbCjkD9JQjuhewDg==",
+ "GrSbnecYAC3j5gtoKntL0A==",
+ "9dbn0Kzwr9adCEfBJh78uQ==",
+ "64QzHOYX0A9++FqRzZRHlQ==",
+ "YZt6HwCvdI5DRQqndA/hBQ==",
+ "6GXHGF62/+jZ7PfIBlMxZw==",
+ "PBULPuFXb6V3Di713n3Gug==",
+ "8Cm19vJW8ivhFPy0oQXVNA==",
+ "zDSQ3NJuUGkVOlvVCATRwA==",
+ "6QAtjOK9enNLRhcVa2iaTg==",
+ "v/PshI6JjkL9nojLlMNfhg==",
+ "yTgN5xFIdz1MzFS6xMl5uQ==",
+ "SCO9nQncEcyVXGCtx30Jdg==",
+ "7b0oo4+qphu6HRvJq6qkHQ==",
+ "ol9xhVTG9e1wNo50JdZbOA==",
+ "hIABph+vhtSF5kkZQtOCTA==",
+ "k+IBS52XdOe5/hLp28ufnA==",
+ "6HnWgYNKohqhoa1tnjjU3A==",
+ "HDxGhvdQwGh0aLRYEGFqnw==",
+ "LDuBcL5r3PUuzKKZ9x6Kfw==",
+ "HPvYV94ufwiNHEImu4OYvQ==",
+ "h2cnQQF2/R3Mq2hWdDdrTg==",
+ "nqpKfidczdgrNaAyPi7BOQ==",
+ "2ywo4t5PPSVUCWDwUlOVwQ==",
+ "jZMDIu95ITTjaUX0pk4V5g==",
+ "bA2kaTpeXflTElTnQRp6GQ==",
+ "lwYQm2ynA3ik2gE1m11IEg==",
+ "5ugVOraop5P5z5XLlYPJyQ==",
+ "l2NppPcweAtmA1V2CNdk2Q==",
+ "DbWQI3H2tcJsVJThszfHGA==",
+ "H6HPFAcdHFbQUNrYnB74dA==",
+ "H1NJEI+fvOQbI51kaNQQjQ==",
+ "53UccFNzMi9mKmdeD82vAw==",
+ "lffapwUUgaQOIqLz2QPbAg==",
+ "rSvhrHyIlnIBlfNJqemEbw==",
+ "BLJk9wA88z6e0IQNrWJIVw==",
+ "5m1ijXEW+4RTNGZsDA/rxQ==",
+ "GG8a3BlwGrYIwZH9j3cnPA==",
+ "HhBHt5lQauNl7EZXpsDHJA==",
+ "/XjB6c5fxFGcKVAQ4o+OMw==",
+ "+tuUmnRDRWVLA+1k0dcUvg==",
+ "SM7E98MyViSSS9G0Pwzwyw==",
+ "c5q/8n7Oeffv3B1snHM/lA==",
+ "kwlAQhR2jPMmfLTAwcmoxw==",
+ "0b/xj6fd0x+aB8EB0LC4SA==",
+ "S8jlvuYuankCnvIvMVMzmg==",
+ "kZkmDatUOdIqs7GzH3nI1A==",
+ "obW3kzv2KBvuckU7F+tfjA==",
+ "pa8nkpAAzDKUldWjIvYMYg==",
+ "m+eh+ZqS74w2q0vejBkjaw==",
+ "LcoJBEPTlSsQwfuoKQUxEw==",
+ "KO2XVYyNZadcQv8aCNn5JA==",
+ "uvzmRcvgepW6mZbMfYgcNw==",
+ "KhUT2buOXavGCpcDOcbOYg==",
+ "fo3JL+2kPgDWfP+CCrFlFw==",
+ "wIfvvLKC61gOpsddUFjVog==",
+ "SPHU6ES1WVm0Mu2LB+YjrA==",
+ "LWWfRqgtph1XrpxF4N64TA==",
+ "LCvz/h9hbouXCmdWDPGWqg==",
+ "PXC6ZpdMH0ATis/jGW12iA==",
+ "z920R8eahJPiTsifrPYdxA==",
+ "GIHKW6plyLra0BmMOurFgA==",
+ "k6OmSlaSZ5CB0i7SD9LczQ==",
+ "YZ39RIXpeLAhyMgmW2vfkQ==",
+ "bs2QG8yYWxPzhtyMqO6u3A==",
+ "pKaTI+TfcV3p/sxbd2e7YQ==",
+ "xWYecfzAtXT9WyQ8NYY/hw==",
+ "Fz8EI+ZpYlbcttSHs5PfpA==",
+ "wfwuxn+Vja1DNwiDwL2pcQ==",
+ "wux5Y8AipBnc5tJapTzgEQ==",
+ "U+oTpcjhc0E+6UjP11OE/Q==",
+ "yTVJKBn72RjakMBXDoBKHg==",
+ "0TxcYwG72dT7Tg+eG8pP1w==",
+ "imZ+mwiT22sW2M9alcUFfg==",
+ "CkDIoAFLlIRXra78bxT/ZA==",
+ "4qMSNAxichi3ori/pR+o0w==",
+ "zNLlWGW/aKBhUwQZ4DZWoQ==",
+ "D31ZticrjGWAO45l5hFh7A==",
+ "HdXg64DBy5WcL5fRRiUVOg==",
+ "yhI5jHlfFJxu4eV5VJO2zQ==",
+ "e9GqAEnk8XI5ix6kJuieNQ==",
+ "EC0+iUdSZvmIEzipXgj7Gg==",
+ "chwv4+xbEAa93PHg8q9zgQ==",
+ "B1VVUbl8pU0Phyl1RYrmBg==",
+ "A+DLpIlYyCb9DaarpLN76g==",
+ "wHA+D5cObfV3kGORCdEknw==",
+ "+Mp+JIyO0XC5urvMyi3wvQ==",
+ "vUE8Iw3NyWXURpXyoNJdaw==",
+ "ParhxI6RtLETBSwB0vwChQ==",
+ "NxSdT2+MUkQN49pyNO2bJw==",
+ "JSyhTcHLTfzHsPrxJyiVrA==",
+ "PAlx9+U+yQCAc5Fi0BOG0w==",
+ "W/0s1x3Qm+wN8DhROk6FrQ==",
+ "L3Jt5dHQpWQk74IAuDOL8g==",
+ "VWb8U4jF/Ic0+wpoXi/y/g==",
+ "1wBuHqS1ciup31WTfm3NPg==",
+ "BDNM1u/9mefjuW1YM2DuBg==",
+ "SDi5+FoP9bMyKYp+vVv1XA==",
+ "23d9B9Gz5kUOi1I//EYsSQ==",
+ "/a9O7kWeXa0le45ab3+nVw==",
+ "PcoVtZrS1x1Q+6nfm4f80w==",
+ "A6TLWhipfymkjPYq8kaoDQ==",
+ "lzUQ1o7JAbdJYpmEqi6KnQ==",
+ "/2jGyMekNu7U136K+2N3Jg==",
+ "ZItMIn1vhGqAlpDHclg0Ig==",
+ "Ee4A3lTMLQ7iDQ7b8QP8Qg==",
+ "bO55S58bqDiRWXSAIUGJKw==",
+ "zeHF6fdeqcOId3fRUGscRw==",
+ "BxsDnI8jXr4lBwDbyHaYXw==",
+ "ylA6sU7Kaf9fMNIx1+sIlw==",
+ "ZWXfE3uGU91WpPMGyknmqw==",
+ "f1+fHgR5rDPsCZOzqrHM7Q==",
+ "8VqeoQELbCs232+Mu+HblA==",
+ "beSrliUu0BOadCWmx+yZyA==",
+ "NQVQfN3nIg9ipHiFh4BvfQ==",
+ "4wnUAbPT3AHRJrPwTTEjyw==",
+ "/cdR1i5TuQvO+u3Ov3b0KQ==",
+ "wtyAZIfhomcHe9dLbYoSvA==",
+ "ulpDxLeQnIRPnq6oaah2AA==",
+ "pdPwUHauXOowaq9hpL2yFw==",
+ "1+A9FCGP3bZhk6gU3LQtNg==",
+ "raYifKqev8pASjjuV+UTKQ==",
+ "+OERSmo7OQUUjudkccSMOA==",
+ "FeRovookFQIsXmHXUJhGOw==",
+ "USCvrMEm/Wqeu9oX6FrgcQ==",
+ "kly/2kE4/7ffbO34WTgoGg==",
+ "IindlAnepkazs5DssBCPhA==",
+ "Bq82MoMcDjIo/exqd/6UoA==",
+ "ocvA1/NbyxM0hanwwY6EiA==",
+ "rtd6mqFgGe98mqO0pFGbSw==",
+ "nvLEpj6ZZF3LWH3wUB6lKg==",
+ "AGd0rcLnQ0n+meYyJur1Pw==",
+ "wI7JrSPQwYHpv2lRsQu9nQ==",
+ "OnmvXbyT2BYsSDJYZhLScA==",
+ "CmBf5qchS1V3C2mS6Rl4bw==",
+ "TafM7nTE5d+tBpRCsb8TjQ==",
+ "wxkb8evGEaGf/rg/1XUWiA==",
+ "y1J+o6DC2sETFsySgpDZyA==",
+ "SVLHWPCCH7GPVCF7QApPbw==",
+ "HMWOlMmzocOIiJ7yG1YaDQ==",
+ "DJmrmNRKARzsTCKSMLmcNA==",
+ "/XC/FmMIOdhMTPqmy4DfUA==",
+ "63OTPaKM0xCfJOy9EDto+Q==",
+ "PxReytUUn/BbxYTFMu1r2Q==",
+ "WjDqf1LyFyhdd8qkwWk+MA==",
+ "/DiUApY7cVp5W9o24rkgRA==",
+ "alJtvTAD7dH/zss/Ek1DMQ==",
+ "xLm/bJBonpTs0PwsF0DvRg==",
+ "eAOEgF5N80A/oDVnlZYRAw==",
+ "LqgzKxbI6WTMz0AMIDJR5w==",
+ "MJ1FuK8PXcmnBAG9meU84A==",
+ "JLq/DrW2f26NaRwfpDXIEA==",
+ "fsrX00onlGvfsuiCc35pGg==",
+ "tXVb5f90k9l3e1oK2NGXog==",
+ "1JRgSHnfAQFQtSkFTttkqQ==",
+ "B0TaUQ6dKhPfSc5V/MjLEQ==",
+ "nkbLVLvh3ClKED97+nH+7Q==",
+ "avFTp3rS6z5zxQUZQuaBHQ==",
+ "lNF8PvUIN02NattcGi5u4g==",
+ "bBEndaOStXBpAK79FrgHaw==",
+ "dM9up4vKQV5LeX82j//1jQ==",
+ "4WO6eT0Rh6sokb29zSJQnQ==",
+ "RHKCMAqrPjvUYt13BVcmvw==",
+ "Ju4YwtPw+MKzpbC0wJsZow==",
+ "tzV7ixFH37ze4zuLILTlfA==",
+ "oPlhC4ebXdkIDazeMSn1fQ==",
+ "5pje7qyz8BRsa8U4a4rmoA==",
+ "7E6V6/zSjbtqraG7Umj+Jw==",
+ "8QK7emHS6rAcAF5QQemW/A==",
+ "LhqRc9oewY4XaaXTcnXIHQ==",
+ "p/7qM5+Lwzw1/lIPY91YxQ==",
+ "fy54Milpa7KZH/zgrDmMXQ==",
+ "LyPXOoOPMieqINtX8C9Zag==",
+ "aD4QvtMlr8Lk/zZgZ6zIMg==",
+ "dsueq9eygFXILDC7ZpamuA==",
+ "+mJLK+6qq8xFv7O/mbILTw==",
+ "nHUpYmfV59fe3RWaXhPs3Q==",
+ "VbCoGr8apEcN7xfdaVwVXw==",
+ "/2Chaw2M9DzsadFFkCu6WQ==",
+ "rKAQxu80Q8g1EEhW5Wh8tg==",
+ "RJJqFMeiCZHdsqs72J17MQ==",
+ "GF2yvI9UWf1WY7V7HXmKPA==",
+ "JyIDGL1m/w+pQDOyyeYupA==",
+ "wR2Gxb07nkaPcZHlEjr8iA==",
+ "PbDVq2Iw1eeM8c2o/XYdTA==",
+ "BL3buzSCV78rCXNEhUhuKQ==",
+ "i42XumprV/aDT5R0HcmfIQ==",
+ "DuEKxykezAvyaFO2/5ZmKQ==",
+ "6ACvJNfryPSjGOK39ov8Qg==",
+ "YaUKOTyByjUvp1XaoLiW5Q==",
+ "jNcMS2zX1iSZN9uYnb2EIg==",
+ "VRnx+kd6VdxChwsfbo1oeQ==",
+ "4Qinl7cWmVeLJgah8bcNkw==",
+ "Fiy3hkcGZQjNKSQP9vRqyA==",
+ "HaSc7MZphCMysTy2JbTJkw==",
+ "VhYGC8KYe5Up+UJ2OTLKUw==",
+ "K2gk9zWGd0lJFRMQ1AjQ/Q==",
+ "NfxVYc3RNWZwzh2RmfXpiA==",
+ "JGeqHRQpf4No74aCs+YTfA==",
+ "7VHlLw20dWck+I8tCEZilA==",
+ "V5HKdaTHjA8IzvHNd9C51g==",
+ "9TalxEyFgy6hFCM73hgb7Q==",
+ "R/y6+JJP8rzz1KITJ4qWBw==",
+ "7bM/pn4G7g7Zl6Xf1r62Lg==",
+ "CHsFJfsvZkPWDXkA6ZMsDQ==",
+ "uXuPA/2KJbb7ZX+NymN3dw==",
+ "o+nYS4TqJc6XOiuUzEpC3A==",
+ "8N3mhHt29FZDHn1P2WH1wQ==",
+ "uZ2gUA74/7Q33tI2TcGQlg==",
+ "8B12CamjOGzJDnQ+RkUf4w==",
+ "9FdpxlIFu11qIPdO7WC5nw==",
+ "G+sGF13VXPH4Ih6XgFEXxg==",
+ "y+1I05LDAYJ09tKMs3zW6g==",
+ "gnkadeCgjdmLdlu/AjBZJg==",
+ "1I+UVx3krrD4NhzO7dgfHQ==",
+ "8LNNoHe6rEQyJ0ebl151Mw==",
+ "yOE90OHQdyOfrAgwDvn2gA==",
+ "ayBGGPEy++biljvGcwIjXA==",
+ "o/Y4U6rWfsUCXJ72p5CUGw==",
+ "5kvyy902llnYGQdn2Py04w==",
+ "6k2cuk0McTThSMW/QRHfjA==",
+ "2XrR2hjDEvx8MQpHk9dnjw==",
+ "fv/PW8oexJYWf5De30fdLQ==",
+ "861mBNvjIkVgkBiocCUj/Q==",
+ "NKGY0ANVZ0gnUtzVx1pKSw==",
+ "4DIPP/yWRgRuFqVeqIyxMQ==",
+ "cgSEbLqqvDsNUyeA3ryJ6Q==",
+ "xbBxUP9JyY0wDgHDipBHeg==",
+ "c3WVxyC5ZFtzGeQlH5Gw+w==",
+ "ZKeTDCboOgCptrjSfgu0xw==",
+ "DjHszpS8Dgocv3oQkW/VZQ==",
+ "Iqszlv4R49UevjGxIPMhIA==",
+ "uChFnF0oCwARhAOz/d47eA==",
+ "0egBaMnAf0CQEXf1pCIKnA==",
+ "FnVNxl5AFH1AieYru2ZG+A==",
+ "2Ct+pLXrK6Ku1f4qehjurQ==",
+ "x2nSgcTjA3oGgI8mMgiqjw==",
+ "AUGmvZkpkKBry5bHZn4DJA==",
+ "x8kRVzohTdhkryvYeMvkMw==",
+ "rXfWkabSPN+23Ei1bdxfmQ==",
+ "ElTNyMR4Rg8ApKrPw88WPg==",
+ "9jxA/t3TQx8dQ+FBsn/YCg==",
+ "I07W2eDQwe6DVsm1zHKM8A==",
+ "0p1jMr06OyBoXQuSLYN4aQ==",
+ "odGhKtO4bDW5R8SYiI5yCg==",
+ "5Q/Y2V0iSVTK8HE8JerEig==",
+ "Ily2MKoFI1zr5LxBy93EmQ==",
+ "8dUcSkd2qnX5lD9B+fUe+Q==",
+ "80UE+Ivby3nwplO/HA7cPw==",
+ "sS6QcitMPdvUBLiMXkWQkw==",
+ "5VY++KiWgo7jXSdFJsPN3A==",
+ "aY6B28XdPnuYnbOy9uSP8A==",
+ "ZfRlID+pC1Rr4IY14jolMw==",
+ "/YuQw7oAF08KDptxJEBS9g==",
+ "16d+fhFlgayu3ttKVV/pbg==",
+ "8dBIsHMEAk7aoArLZKDZtg==",
+ "wRqaDZVHHurp5whOQ1kDbQ==",
+ "lFUq6PGk9dBRtUuiEW7Cug==",
+ "FoJZ61VrU8i084pAuoWhDQ==",
+ "4mig4AMLUw+T/ect9p4CfA==",
+ "Po0lhBfiMaXhl+vYh1D8gA==",
+ "z9cd+Qj+ueX34Zf3997MNQ==",
+ "1dsKN1nG6upj7kKTKuJWsQ==",
+ "UtLYUlQJ02oKcjNR3l+ktg==",
+ "O538ibsrI4gkE5tfwjxjmg==",
+ "G736AX070whraDxChqUrqw==",
+ "THs1r8ZEPChSGrrhrNTlsA==",
+ "pVG1hL96/+hQ+58rJJy6/A==",
+ "1BjsijOzgHt/0i36ZGffoQ==",
+ "6rIWazDEWU5WPZHLkqznuQ==",
+ "cdWUm6uLNzR/knuj2x75eA==",
+ "nsnX3tKkN1elr18E31tXDw==",
+ "0fnruVOCxEczscBuv4yL9A==",
+ "SVuEYfQ9FGyVMo1672n0Yg==",
+ "ZRWyfXyXqAaOEjkzWl949Q==",
+ "S2MAIYeDQeJ1pl9vhtYtUg==",
+ "vsRNZx4thFFFPneubKq1Fw==",
+ "kuWGANwzNRpG4XmY7KjjNg==",
+ "i6r+mZfyhZyqlYv56o0H+w==",
+ "wqWqe0KRjZlUIrGgEOG9Mg==",
+ "t5wh9JGSkQO78QoQoEqvXA==",
+ "AGoVLd0QPcXnTedT5T95JQ==",
+ "aRrcmH+Ud3mF1vEXcpEm4w==",
+ "C65PZm8rZxJ6tTEb6d08Eg==",
+ "oAHVGBSJ2cf4dVnb/KEYmw==",
+ "BuDVDLl0OGdomEcr+73XhQ==",
+ "bLsStF0DDebpO+xulqGNtg==",
+ "xukOAM0QVsA72qEy0yku9A==",
+ "LpoayYsTO8WLFLCSh2kf2w==",
+ "LEVYAE54618FrlXkDN01Kw==",
+ "Jm862vBTCYbv/V4T1t46+Q==",
+ "X4kdXUuhcUqMSduqhfLpxA==",
+ "cLR0Ry4/N5swqga1R6QDMw==",
+ "0klouNfZRHFFpdHi4ZR2hA==",
+ "JGx8sTyvr4bLREIhSqpFkw==",
+ "ZiJ/kJ9GneF3TIEm08lfvQ==",
+ "hP7dSa8lLn9KTE/Z0s4GVQ==",
+ "600bwlyhcy754W1E6tuyYg==",
+ "U49SfOBeqQV9wzsNkboi8Q==",
+ "5DDb7fFJQEb3XTc3YyOTjg==",
+ "6uT7LZiWjLnnqnnSEW4e/Q==",
+ "tq5xUJt8GtjDIh1b48SthQ==",
+ "eJFIQh/TR7JriMzYiTw4Sg==",
+ "jdRzkUJrWxrqoyNH9paHfQ==",
+ "RKVDdE1AkILTFndYWi9wFg==",
+ "AEpTVUQhIEJGlXJB6rS26A==",
+ "PD+yHtJxZJ2XEvjIPIJHsQ==",
+ "dOS+mVCy3rFX9FvpkTxGXA==",
+ "lz+SeifYXxamOLs1FsFmSQ==",
+ "QTz21WkhpPjfK8YoBrpo+w==",
+ "9wUIeSgNN36SFxy8v2unVg==",
+ "ash1r2J6B0PUxJe8P0otVQ==",
+ "y7yS9x3yshVhMpDbQtfYOQ==",
+ "f07bdNVAe9x+cAMdF1bByQ==",
+ "N2KovXW14hN/6+iWa1Yv3g==",
+ "2DNbXVgesUa7PgYQ4zX5Lw==",
+ "WQznrwqvMhUlM3CzmbhAOQ==",
+ "FpWDTLTDmkUhH/Sgo+g1Gg==",
+ "OVHqwV8oQMC5KSMzd5VemA==",
+ "Bv4mNIC72KppYw/nHQxfpQ==",
+ "MI+HSMRh8KTW+Afiaxd/Fw==",
+ "10OltdxPXOvfatJuwPVKbQ==",
+ "y4/HohCJxtt+cT7nLJB08w==",
+ "RhcqXY4OsZlVVF7ZlkTeRw==",
+ "/mrqas0eDX+sFUNJvCQY8g==",
+ "ZIZx4MehWTVXPN9cVQBmyA==",
+ "z20AAnvj7WsfJeOu3vemlA==",
+ "dL6n/JsK+Iq6UTbQuo/GOw==",
+ "rMm9bHK69h0fcMkMdGgeeA==",
+ "ftsf2qztw3NC78ep/CZXWQ==",
+ "/n1RLTTVpygre1dl36PDwQ==",
+ "/FsJYFNe+7UvsSkiotNJEQ==",
+ "Yy2pPhITTmkEwoudXizHqQ==",
+ "lizovLQxu6L9sbafNQuShQ==",
+ "XV5MYe0Q7YMtoBD6/iMdSw==",
+ "5jHgQF4SfO/zy9xy9t+9dw==",
+ "16iT/jCcPDrJEfi2bE5F+Q==",
+ "syeBfQBUmkXNWCZ1GV8xSA==",
+ "sr3UXbMg5zzkRduFx/as7g==",
+ "xUXEE7OBBCudsQnuj5ycOA==",
+ "ojZY7Gi2QJXE/fp6Wy31iA==",
+ "RlNPyhgYOIn28R4vKCVtYA==",
+ "KOm8PTa+ICgDrgK9QxCJZw==",
+ "DJoy1NSZZw87oxWGlNHhfg==",
+ "jEdanvXKyZdZJG6mj/3FWw==",
+ "Omr+zPWVucPCSfkgOzLmSQ==",
+ "71w3aSvuh2mBLtdqJCN3wA==",
+ "xjTMO2mvtpvwQrounD4e8g==",
+ "Zz/5VMbw1TqwazReplvsEg==",
+ "hIjgi20+km+Ks23NJ4VQ6Q==",
+ "00TVKawojyqrJkC7YqT41Q==",
+ "YgVpC5d5V6K/BpOD663yQA==",
+ "wX70jKLKJApHnhyK0r6t3A==",
+ "lacCCRiWdquNm4YRO7FoKA==",
+ "cWdlhVZD7NWHUGte24tMjg==",
+ "t5U+VMsTtlWAAWSW+00SfQ==",
+ "AMfL0rH+g8c0VqOUSgNzQw==",
+ "0G93AxGPVwmr66ZOleM90A==",
+ "9tiibT8V9VwnPOErWGNT3w==",
+ "+dBv88reDrjEz6a2xX3Hzw==",
+ "xX6atcCApI08oVLjjLteLg==",
+ "+YrqTEJlJCv0A2RHQ8tr1A==",
+ "aqcOby9QyEbizPsgO3g0yw==",
+ "s/BZAhh1cTV3JCDUQsV8mA==",
+ "x9VwDdFPp/rJ+SF16ooWYg==",
+ "k/OVIllJvW6BefaLEPq7DA==",
+ "rIMXaCaozDvrdpvpWvyZOQ==",
+ "qQQwJ/aF87BbnLu3okXxaw==",
+ "TIWSM78m0RprwgPGK/e0JA==",
+ "r/b5px/UImGNjT/X5sYjuA==",
+ "7K8l6KoP0BH82/WMLntfrg==",
+ "gEHGeR2F82OgBeAlnYhRSw==",
+ "1/SGIab+NnizimUmNDC4wA==",
+ "WADmxH7R6B4LR+W6HqQQ6A==",
+ "pcoBh5ic7baSD4TZWb3BSw==",
+ "es/L9iW8wsyLeC5S4Q8t+g==",
+ "D175i+2bZ7aWa4quSSkQpA==",
+ "WQMffxULFKJ+bun6NrCURA==",
+ "82hTTe1Nr4N2g7zwgGjxkw==",
+ "oyYtf08AkWLR52bXm5+sKw==",
+ "8uP4HUnSodw88yoiWXOIcw==",
+ "x2NpqNnqRihktNzpxmepkQ==",
+ "x5zMDuW66467ofgL3spLUQ==",
+ "OMO4pqzfcbQ11YO4nkTXfg==",
+ "N4/mQFyhDpPzmihjFJJn6w==",
+ "NN/ymVQNa17JOTGr6ki3eQ==",
+ "htDbVu1xGhCRd8qoMlBoMg==",
+ "S47hklz3Ow+n5aY6+qsCoA==",
+ "ji+1YHlRvzevs3q5Uw1gfA==",
+ "3Y4w0nETru3SiSVUMcWXqw==",
+ "XfBOCJwi2dezYzLe316ivw==",
+ "kMUdiwM7WR8KGOucLK4Brw==",
+ "V/xG5QFyx1pihimKmAo8ZA==",
+ "sQskMBELEq86o1SJGQqfzg==",
+ "6+jhreeBLfw64tJ+Nhyipw==",
+ "8iYdEleTXGM+Wc85/7vU9w==",
+ "D7piVoB2NJlBxK5owyo4+g==",
+ "hDGa2yLwNvgBd/v6mxmQaQ==",
+ "WLsh3UF4WXdHwgnbKEwRlQ==",
+ "D5jaV+HtXkSpSxJPmaBDXg==",
+ "jCgdKXsBCgf7giUKnr6paQ==",
+ "XqW7UBTobbV4lt1yfh0LZw==",
+ "EbGG4X18upaiVQmPfwKytg==",
+ "dXDPnL1ggEoBqR13aaW9HA==",
+ "Vik8tGNxO0xfdV0pFmmFDw==",
+ "Swjn3YkWgj0uxbZ1Idtk+A==",
+ "JPxEncA4IkfBDvpjHsQzig==",
+ "F5FcNti7lUa9DyF2iEpBug==",
+ "HJYgUxFZ66fRT8Ka73RaUg==",
+ "Jbxl8Nw1vlHO9rtu0q/Fpg==",
+ "fmC+85h5WBuk8fDEUWPjtQ==",
+ "dZgMquvZmfLqP4EcFaWCiA==",
+ "XF/yncdoT4ruPeXCxEhl9Q==",
+ "QJEbr3+42P9yiAfrekKdRQ==",
+ "Sr9c0ReRpkDYGAiqSy683g==",
+ "Nr4zGo5VUrjXbI8Lr4YVWQ==",
+ "NDZWIhhixq7NT8baJUR4VQ==",
+ "GFRJoPcXlkKSvJRuBOAYHQ==",
+ "WHutPin+uUEqtrA7L8878A==",
+ "2rhjiY0O0Lo36wTHjmlNyw==",
+ "XsF7R12agx/KkRWl0TyXRA==",
+ "R6cO8GzYfOGTIi773jtkXw==",
+ "zrZWcqQsUE3ocWE0fG+SOA==",
+ "uNzpptKjihEfKRo5A1nWmw==",
+ "gICaI06E9scnisonpvqCsA==",
+ "TA9WjiLAFgJubLN4StPwLw==",
+ "sBpytpE38xz0zYeT+0qc2A==",
+ "Ej7W3+67kCIng3yulXGpRQ==",
+ "nR3ACzeVF5YcLX6Gj6AGyQ==",
+ "b0vZfEyuTja2JYMa20Rtbg==",
+ "f1h+Vp+xmdZsZIziHrB2+g==",
+ "WzjvUJ4jZAEK7sBqw+m07A==",
+ "OzMR5D2LriC5yrVd5hchnA==",
+ "cw1gBLtxH/m4H7dSM7yvFg==",
+ "CZbd+UoTz0Qu1kkCS3k8Xg==",
+ "WtT0QAERZSiIt2SFDiAizg==",
+ "QsquNcCZL9wv7oZFqm64vQ==",
+ "FXzaxi3nAXBc8WZfFElQeA==",
+ "Ml3mi1lGS1IspHp3dYYClg==",
+ "XGAXhUFjORwKmAq9gGEcRg==",
+ "wOhbpTzmFla8R0kI9OiHaA==",
+ "qoK2keBg3hdbn7Q24kkVXg==",
+ "ZAQHWU6RMg4IadOxuaukyw==",
+ "RiahBXX2JbPzt8baPiP/8g==",
+ "Qx6rVv9Xj8CBjqikWI9KFA==",
+ "ZRnR6i+5WKMRfs3BDRBCJg==",
+ "91LQuW6bMSxl10J/UDX23A==",
+ "0dIeIM5Zvm5nSVWLy94LWg==",
+ "Ja3ECL7ClwDrWMTdcSQ6Ug==",
+ "f6iLrMpxKhFxIlfRsFAuew==",
+ "iSeH0JFSGK73F470Rhtesw==",
+ "DwOTyyCoUfaSShHZx9u6xg==",
+ "rdeftHE7gwAT67wwhCmkYQ==",
+ "kUhyc3G8Zvx8+q5q5nVEhw==",
+ "W8bATujVUT80v2XGJTKXDg==",
+ "dMRx4Mf6LrN64tiJuyWmDw==",
+ "9cvHJmim9e0pOaoUEtiM6A==",
+ "RHToSGASrwEmvzjX6VPvNQ==",
+ "V7eji28JSg3vTi30BCS7gw==",
+ "4+htiqjEz9oq0YcI/ErBVg==",
+ "jKJn4czwUl/6wtZklcMsSg==",
+ "bvyB6OEwhwCIfJ6KRhjnRw==",
+ "59ipbMH7cKBsF9bNf4PLeQ==",
+ "M/cQja3uIk1im9++brbBOA==",
+ "AChOz8avRYsvxlbWcorQ3w==",
+ "FcKjlHKfQAGoovtpf+DxWQ==",
+ "y+cl1/Knb9MZPz8nBB0M+w==",
+ "b8BZV1NfBdLi70ir4vYvZg==",
+ "aFJuE/s+Kbge4ppn+wulkA==",
+ "CWBGcRFYwZ0va6115vV/oQ==",
+ "glnqaRfwm6NxivtB2nySzw==",
+ "mPk1IsU5DmDFA/Ym5+1ojw==",
+ "LGwcvetzQ3QqKjNh5vA8vw==",
+ "yctId8ltkl3+xqi9bj+RqA==",
+ "spJI3xFUlpCDqzg0XCxopA==",
+ "V8m51xgUgywRoV6BGKUrgg==",
+ "rgcXxjx3pDLotH7TTfAoZw==",
+ "/TSsi/AwKHtP6kQaeReI3w==",
+ "8dbyfox/isKLsnVjQNsEXg==",
+ "MOrAbuJTyGKPC6MgYJlx5Q==",
+ "uNWFZlP7DA96sf+LWiAhtQ==",
+ "hNHqznsrIVRSQdII6crkww==",
+ "GT6WUDXiheKAM7tPg3he9A==",
+ "JC8Q+8yOJ52NvtVeyHo68w==",
+ "HMQarkPWOUDIg5+5ja2dBQ==",
+ "nknBKPgb7US42v8A0fTl/w==",
+ "fDOUzPTU2ndpbH0vgkgrJQ==",
+ "GTNttXfMniNhrbhn92Aykg==",
+ "D2JcY4zWwqaCKebLM8lPiQ==",
+ "/c34NtdUZAHWIwGl3JM8Tw==",
+ "/G26n5Xoviqldr5sg/Jl3w==",
+ "GF0lY77rx1NQzAsZpFtXIQ==",
+ "BMOi5JmFUg5sCkbTTffXHw==",
+ "R+beucURp/H5jLs4kW6wmg==",
+ "xfYZ6qhWNBqqJ0PdWRjOwA==",
+ "Ahpi9+nl13kPTdzL+jgqMw==",
+ "oIU19xAvLJwQSZzIH577aA==",
+ "50xwiYvGQytEDyVgeeOnMg==",
+ "M0ESOGwJ4WZ4Ons1ljP0bQ==",
+ "fS471/rN4K2m10mUwGFuLg==",
+ "RrE3B3X/SJi3CqCUlTYwaw==",
+ "oDca3JEdRb4vONT9GUUsaQ==",
+ "pHo1O5zrCHCiLvopP2xaWw==",
+ "7sCJ4RxbxRqVnF4MBoKfuQ==",
+ "7R5rFaXCxM3moIUtoCfM2g==",
+ "4rrSL6N0wyucuxeRELfAmw==",
+ "9Gkw+hvsR/tFY1cO89topg==",
+ "aw4CzX8pYbPVMuNrGCEcWg==",
+ "KyLQxi5UP+qOiyZl0PoHNQ==",
+ "T1pMWdoNDpIsHF8nKuOn2A==",
+ "Qv6wWP4PpycDGxe7EZNSCw==",
+ "ZJc7GV0Yb6MrXkpDVIuc8g==",
+ "aXrbsro7KLV8s4I4NMi4Eg==",
+ "7k5rBuh8FbTTI4TP87wBPQ==",
+ "NRyFx6jqO/oo9ojvbYzsAg==",
+ "P7eMlOz9YUcJO+pJy0Kpkw==",
+ "jpjpNjL1IKzJdGqWujhxCw==",
+ "9k1u/5TgPmXrsx3/NsYUhg==",
+ "c1wbFbN7AdUERO/xVPJlgw==",
+ "Yw4ztKv6yqxK9U1L0noFXg==",
+ "GnJKlRzmgKN9vWyGfMq3aA==",
+ "91VcAVv7YDzkC1XtluPigw==",
+ "h1NNwMy0RjQmLloSw1hvdg==",
+ "pzC8Y0Vj9MPBy3YXR32z6w==",
+ "UTmTgvl+vGiCDQpLXyVgOg==",
+ "CzWhuxwYbNB/Ffj/uSCtbw==",
+ "VOB+9Bcfu8aHKGdNO0iMRw==",
+ "X2Tawm2Cra6H7WtXi1Z4Qw==",
+ "6cTETZ9iebhWl+4W5CB+YQ==",
+ "X4hrgqMIcApsjA9qOWBoCw==",
+ "1buQEv2YlH/ljTgH0uJEtw==",
+ "FH5Z60RXXUiDk+dSZBxD3g==",
+ "FI2WhaSMb3guFLe3e9il8Q==",
+ "O/EizzJSuFY8MpusBRn7Tg==",
+ "b6rrRA0W247O+FfvDHbVCQ==",
+ "ng1Q0A7ljho3TUWWYl46sw==",
+ "1Ym0lyBJ9aFjhJb/GdUPvQ==",
+ "+OXdvbTxHtSoLg7bZMho4w==",
+ "cuQslgfqD2VOMhAdnApHrA==",
+ "pCQmlnn3BxhsV2GwqjRhXg==",
+ "6PzjncEw2wHZg7SP7SQk9w==",
+ "nqtQI1bSM7DCO9P1jGV97Q==",
+ "O1ckWUwuhD44MswpaD6/rw==",
+ "RUmhye56tQu9xXs4SRJpOQ==",
+ "llujnWE17U8MIHmx4SbrSA==",
+ "UwqBVd4Wfias4ElOjk2BzQ==",
+ "kBAB2PSjXwqoQOXNrv80AA==",
+ "w1zN28mSrI/gqHsgs4ME3A==",
+ "301utVPZ93AnPLYbsiJggw==",
+ "qIFpKKwUmztsBpJgMaVvSg==",
+ "QmcURiMzmVeUNaYPSOtTTg==",
+ "x/MpsQvziUpW40nNUHDS5Q==",
+ "t1O9jSNjg4DTIv/Za4NbtA==",
+ "1B5gxGQSGzVKoNd5Ol4N7g==",
+ "81iQLU+YwxNwq4of6e9z7A==",
+ "x0eIHCvQLd2jdDaXwSWTYQ==",
+ "96ORaz1JRHY1Gk8H74+C2g==",
+ "bNDKcFu8T5Y6OoLSV+o/Sw==",
+ "WrJMOuXSLKKzgmIDALkyNw==",
+ "+gpHnUj2GWocP74t5XWz4w==",
+ "z5DveTu377UW8IHnsiUGZg==",
+ "irnD9K8bsT+up/JUrxPw6A==",
+ "ginkFyNVMwkZLE49AbfqfA==",
+ "2hEzujfG3mR5uQJXbvOPTQ==",
+ "E9yeifEZtpqlD0N3pomnGw==",
+ "OpC/sL320wl5anx6AVEL+A==",
+ "D7wN7b5u5PKkMaLJBP9Ksw==",
+ "83WGpQGWyt6mCV+emaomog==",
+ "X6ulLp4noBgefQTsbuIbYQ==",
+ "BH+rkZWQjTp7au6vtll/CQ==",
+ "Ex3x5HeDPhgO2S9jjCFy4g==",
+ "YNqIHCmBp/EbCgaPKJ7phw==",
+ "312g8iTB9oJgk/OqcgR7Cw==",
+ "LcF0OqPWrcpHby8RwXz1Yg==",
+ "gaEtlJtD6ZjF5Ftx0IFt0A==",
+ "bvbMJZMHScwjJALxEyGIyg==",
+ "StoXC7TBzyRViPzytAlzyQ==",
+ "XqFSbgvgZn0CpaZoZiRauQ==",
+ "AqHVaj3JcR44hnMzUPvVYg==",
+ "jTg9Y6EfpON4CRFOq0QovA==",
+ "q/siBRjx6wNu+OTvpFKDwA==",
+ "goSgZ8N5UbT5NMnW3PjIlQ==",
+ "9onh6QKp70glZk9cX3s34A==",
+ "o5XVEpdP4OXH0NEO4Yfc/A==",
+ "a5gZ5uuRrXEAjgaoh7PXAg==",
+ "PaROi5U16Tk35p0EKX5JpA==",
+ "dtnE401dC0zRWU0S/QOTAg==",
+ "7J3FoFGuTIW36q0PZkgBiw==",
+ "hiYg+aVzdBUDCG0CXz9kCw==",
+ "vhdFtKVH4bVatb4n8KzeXw==",
+ "DWKsPfKDAtfuwgmc2dKUNg==",
+ "M2JMnViESVHTZaru6LDM6w==",
+ "G/PA+kt0N+jXDVKjR/054A==",
+ "6rqK8sjLPJUIp7ohkEwfZg==",
+ "wajwXfWz2J+O+NVaj6j2UQ==",
+ "C4QEzQKGxyRi2rjwioHttA==",
+ "N/HgDydvaXuJvTCBhG/KtA==",
+ "6erpZS36qZRXeZ9RN9L+kw==",
+ "bbBsi6tXMVWyq3SDVTIXUg==",
+ "aySnrShOW4/xRSzl/dtSKQ==",
+ "rxfACPLtKXbYua18l3WlUw==",
+ "L4+C6I7ausPl6JbIbmozAg==",
+ "R3ijnutzvK6IKV3AKHQZSA==",
+ "leDlMcM+B1mDE8k5SWtUeg==",
+ "KGI/cXVz6v6CfL8H6akcUQ==",
+ "NtwqUO3SKZE/9MXLbTJo/g==",
+ "dJHKDkfMFJeoULg7U4wwDQ==",
+ "IEz72W2/W8xBx5aCobUFOQ==",
+ "wUYhs4j3W9nIywu1HIv2JA==",
+ "GzbeM7snhe+M+J7X+gAsQw==",
+ "3/1puZTGSrD9qNKPGaUZww==",
+ "eKQCVzLuzoCLcB4im8147A==",
+ "CCK+6Dr72G3WlNCzV7nmqw==",
+ "CJoZn5wdTXbhrWO5LkiW0g==",
+ "bJ1cZW7KsXmoLw0BcoppJg==",
+ "OlpA9HsF8MBh7b45WZSSlg==",
+ "JZRjdJLgZ+S0ieWVDj8IJg==",
+ "uhT12XY79CtbwhcSfAmAXQ==",
+ "isep9d+Q7DEUf0W7CJJYzw==",
+ "K9A87aMlJC8XB9LuFM913g==",
+ "uqe3rFveJ2JIkcZQ3ZMXHQ==",
+ "0e8hM3E5tnABRyy29A8yFw==",
+ "4iiCq+HhC+hPMldNQMt0NA==",
+ "X4o0OkTz0ec70mzgwRfltA==",
+ "1E3pMgAHOnHx3ALdNoHr8Q==",
+ "xNilc7UOu1kyP0+nK5MrLw==",
+ "DQlZWBgdTCoYB1tJrNS5YQ==",
+ "iruDC5MeywV4yA8o1tw/KQ==",
+ "z+1oDVy8GJ5u/UDF+bIQdA==",
+ "uExgqZkkJnZj252l5dKAGg==",
+ "ZgdpqFrVGiaHkh9o3rDszg==",
+ "5N2oi2pB69NxeNt08yPLhw==",
+ "G37U8XTFyshfCs7qzFxATg==",
+ "0ZEC3hy411LkOhKblvTcqg==",
+ "ITZ3P47ALS0JguFms6/cDA==",
+ "WWN44lbUnEdHmxSfMCZc6w==",
+ "r2f2MyT+ww1g9uEBzdYI1w==",
+ "ZvvxwDd0I6MsYd7aobjLUA==",
+ "uQs79rbD/wEakMUxqMI48A==",
+ "022B0oiRMx8Xb4Af98mTvQ==",
+ "afMd/Hr3rYz/l7a3CfdDjg==",
+ "xmsYnsJq78/f9xuKuQ2pBQ==",
+ "dFetwmFw+D6bPMAZodUMZQ==",
+ "TBQpcKq2huNC5OmI2wzRQw==",
+ "skrQRB9xbOsiSA19YgAdIQ==",
+ "anyANMnNkUqr3JuPJz5Qzw==",
+ "6QUGE2S8oFYx4T4nW56cCw==",
+ "rwtF86ZAbWyKI6kLn4+KBw==",
+ "6txm8z4/LGCH0cpaet/Hsg==",
+ "wdRyYjaM11VmqkkxV/5bsA==",
+ "+k5lDb+QdNc9iZ01hL5yBg==",
+ "k/pBSWE2BvUsvJhA9Zl5uw==",
+ "jQjyjWCEo9nWFjP4O8lehw==",
+ "R6Me6sSGP5xpNI8R0xGOWw==",
+ "9+hjTVMQUsvVKs7Tmp52tg==",
+ "VQIpquUqmeyt/q6OgxzduQ==",
+ "KXvdjZ3rRKn60djPTCENGA==",
+ "5HovoyHtul8lXh+z8ywq9A==",
+ "1+XWdu4qCqLLVjqkKz3nmA==",
+ "LCj4hI520tA685Sscq6uLw==",
+ "b53qqLnrTBthRXmmnuXWvw==",
+ "WTr3q/gDkmB4Zyj7Ly20+w==",
+ "FbxScyuRacAQkdQ034ShTA==",
+ "qaTdVEeZ6S8NMOxfm+wOMA==",
+ "ZNrjP1fLdQpGykFXoLBNPw==",
+ "/Bwpt5fllzDHq2Ul6v86fA==",
+ "/mFp3GFkGNLhx2CiDvJv4A==",
+ "RppDe/WGt1Ed6Vqg1+cCkQ==",
+ "6M6QapJ5xtMXfiD3bMaiLA==",
+ "Ghuj9hAyfehmYgebBktfgA==",
+ "GncGQgmWpI/fZyb/6zaFCg==",
+ "R1TCCfgltnXBvt5AiUnCtQ==",
+ "5NEP7Xt7ynj6xCzWzt21hQ==",
+ "4yEkKp2FYZ09mAhw2IcrrA==",
+ "y2Tn2gmhKs5WKc01ce74rg==",
+ "wnfYUctNK+UPwefX5y4/Rw==",
+ "BV1moliPL15M14xkL+H1zw==",
+ "80C9TB9/XT1gGFfQDJxRoA==",
+ "yL1DwlIIREPuyuCFULi0uw==",
+ "D09afzGpwCEH0EgZUSmIZA==",
+ "eCy/T+a8kXggn1L8SQwgvA==",
+ "+dIEf5FBrHpkjmwUmGS6eg==",
+ "kzXsrxWRnWhkA82LsLRYog==",
+ "Nf9fbRHm844KZ2sqUjNgkA==",
+ "XAq/C+XyR6m3uzzLlMWO5Q==",
+ "jiV+b/1EFMnHG6J0hHpzBg==",
+ "HK0yf7F97bkf1VYCrEFoWA==",
+ "Cz1G77hsDtAjpe0WzEgQog==",
+ "xdCCdP8SNBOK3IsX6PiPQA==",
+ "8snljTGo/uICl9q0Hxy7/A==",
+ "sLdxIKap0ZfC3GpUk3gjog==",
+ "IA1jmtfpYkz/E2wD0+27WA==",
+ "PPa7BDMpRdxJdBxkuWCxKA==",
+ "CuGIxWhRLN7AalafBZLCKQ==",
+ "MWcV03ULc0vSt/pFPYPvFA==",
+ "QVwuN66yPajcjiRnVk/V8g==",
+ "aLY2pCT0WfFO5EJyinLpPg==",
+ "dGrf9SWJ13+eWS6BtmKCNw==",
+ "YtZ8CYfnIpMd2FFA5fJ+1Q==",
+ "Umd+5fTcxa3mzRFDL9Z8Ww==",
+ "Al8+d/dlOA5BXsUc5GL8Tg==",
+ "/KYZdUWrkfxSsIrp46xxow==",
+ "kr8tw1+3NxoPExnAtTmfxg==",
+ "PwvPBc+4L73xK22S9kTrdA==",
+ "VWNDBOtjiiI4uVNntOlu/A==",
+ "lJFPmPWcDzDp5B2S8Ad8AA==",
+ "Mofqu40zMRrlcGRLS42eBw==",
+ "BuENxPg7JNrWXcCxBltOPg==",
+ "nmD7fEU4u7/4+W/pkC4/0Q==",
+ "axEl7xXt/bwlvxKhI7hx4g==",
+ "W04GeDh+Tk/I1S85KlozRA==",
+ "tVw8U1AsslIFmQs4H1xshg==",
+ "TSPFvkgw6uLsJh66Ou0H9w==",
+ "IYIbEaErHoFBn8sTT9ICIQ==",
+ "WBu0gJmmjVdVbjDmQOkU6w==",
+ "ZgjifTVKmxOieco81gnccQ==",
+ "ZrCnZB/U/vcqEtI1cSvnww==",
+ "2D6yhuABiaFFoXz0Lh0C+w==",
+ "SfwnYZCKP1iUJyU1yq4eKg==",
+ "tsiqwelcBAMU/HpLGBtMGw==",
+ "S9L29U2P5K8wNW+sWbiH7w==",
+ "sGLPmr568+SalaQr8SE/PA==",
+ "Hm6MG6BXbAGURVJKWRM6ZA==",
+ "euxzbIq4vfGYoY3s1QmLcw==",
+ "/FchS2nPezycB8Bcqc2dbg==",
+ "ZKvox7BaQg4/p5jIX69Umw==",
+ "HkbdaMuDTPBDnt3wAn5RpQ==",
+ "eddhS+FkXxiUnbPoCd5JJw==",
+ "Muf2Eafcf9G3U2ZvQ9OgtQ==",
+ "a7Pv1SOWYnkhIUC22dhdDA==",
+ "O839JUrR+JS30/nOp428QA==",
+ "2qK2ZEY9LgdKSTaLf6VnLA==",
+ "BTiGLT6XdZIpFBc91IJY6g==",
+ "EqYq2aVOrdX5r7hBqUJP7g==",
+ "SIuKH/Qediq0TyvqUF93HQ==",
+ "c5ymZKqx/td1MiS2ERiz9A==",
+ "rqucO37p86LpzehR/asCSQ==",
+ "1tpM0qgdo7JDFwvT0TD78g==",
+ "Ar1Eb/f/LtuIjXnnVPYQlA==",
+ "V8q+xz4ljszLZMrOMOngug==",
+ "P5WPQc5NOaK7WQiRtFabkw==",
+ "Xo8ZjXOIoXlBjFCGdlPuZw==",
+ "jTmPbq+wh30+yJ/dRXk1cA==",
+ "KSumhnbKxMXQDkZIpDSWmQ==",
+ "Kh/J1NpDBGoyDU+Mrnnxkg==",
+ "3BjLFon1Il0SsjxHE2A1LQ==",
+ "dml2gqLPsKpbIZ93zTXwCQ==",
+ "ZyoaR1cMiKAsElmYZqKjLA==",
+ "vnOJ3e9Zd4wPx8PX7QgZzQ==",
+ "2melaInV0wnhBpiI3da6/A==",
+ "mUek9NkXm8HiVhQ6YXiyzA==",
+ "RZTpYKxOAH9JgF1QFGN+hw==",
+ "a/Y6IAVFv0ykRs9WD+ming==",
+ "yhRi5M9Etuu9HSu4d24i3w==",
+ "+1gcqAqaRZwCj5BGiZp3CA==",
+ "o1zeXHJEKevURAAbUE/Vog==",
+ "cvOg7N4DmTM+ok1NBLyBiQ==",
+ "uPdjKJIGzN7pbGZDZdCGaA==",
+ "REnDNe9mGfqVGZt+GdsmjQ==",
+ "XqTK/2QuGWj50tGmiDxysA==",
+ "bL2FuwsPT7a7oserJQnPcw==",
+ "uO+uK1DntCxVRr1KttfUIw==",
+ "Xconi1dtldH90Wou9swggw==",
+ "HRF3WL/ue3/QlYyu7NUTrA==",
+ "5LuFDNKzMd2BzpWEIYO2Ww==",
+ "dNTU+/2DdZyGGTdc+3KMhQ==",
+ "H+NHjk/GJDh/GaNzMQSzjg==",
+ "/Ph/6l/lFNVqxAje1+PgFA==",
+ "4WRdAjiUmOQg2MahsunjAg==",
+ "j+lDhAnWAyso+1N8cm85hQ==",
+ "nFBXCPeiwxK9mLXPScXzTA==",
+ "vGKknndb4j6VTV8DxeT4fQ==",
+ "fdqt93OrpG13KAJ5cASvkg==",
+ "1MIn73MLroxXirrb+vyg2Q==",
+ "Q7teXmTHAC5qBy+t7ugf0w==",
+ "bWwtTFlhO3xEh/pdw0uWaQ==",
+ "Omi2ZB9kdR1HrVP2nueQkA==",
+ "+ZozWaPWw8ws1cE5DJACeg==",
+ "3FH4D31nKV13sC9RpRZFIg==",
+ "4kXlJNuT79XXf1HuuFOlHw==",
+ "36XDmX6j542q+Oei1/x0gw==",
+ "MqqDg9Iyt4k3vYVW5F+LDw==",
+ "cvrGmub2LoJ+FaM5HTPt9A==",
+ "uC2lzm7HaMAoczJO6Z/IhQ==",
+ "MnStiFQAr3QlaRZ02SYGaQ==",
+ "ZuayB6IpbeITokKGVi9R5w==",
+ "FtxpWdhEmC6MT61qQv4DGA==",
+ "KujFdhhgB9q4oJfjYMSsLg==",
+ "ZV8mEgJweIYk0/l0BFKetA==",
+ "gDLjxT7vm07arF4SRX5/Vg==",
+ "/MEOgAhwb7F0nBnV4tIRZA==",
+ "k2KP9oPMnHmFlZO6u6tgyw==",
+ "fbTm027Ms0/tEzbGnKZMDA==",
+ "HOi+vsGAae4vhr+lJ5ATnQ==",
+ "9Bet5waJF5/ZvsYaHUVEjQ==",
+ "Wd0dOs7eIMqW5wnILTQBtg==",
+ "z/e5M2lE9qh3bzB97jZCKA==",
+ "b16O4LF7sVqB7aLU2f3F1A==",
+ "lsBTMnse2BgPS6wvPbe7JA==",
+ "0nOg18ZJ/NicqVUz5Jr0Hg==",
+ "MFeXfNZy6Q9wBfZmPQy3xg==",
+ "ksOFI9C7IrDNk4OP6SpPgw==",
+ "NquRbPn8fFQhBrUCQeRRoQ==",
+ "ccmy4GVuX967KaQyycmO0w==",
+ "DY0IolKTYlW+jbKLPAlYjQ==",
+ "aJFbBhYtMbTyMFBFIz/dTA==",
+ "9pdeedz1UZUlv8jPfPeZ1g==",
+ "qZ2q5j2gH3O56xqxkNhlIA==",
+ "N7fHwb397tuQHtBz1P80ZQ==",
+ "uOkMpYy/7DYYoethJdixfQ==",
+ "E9ajQQMe02gyUiW3YLjO/A==",
+ "dFSavcNwGd8OaLUdWq3sng==",
+ "TAD0Lk95CD86vbwrcRogaQ==",
+ "jLI3XpVfjJ6IzrwOc4g9Pw==",
+ "CzP13PM/mNpJcJg8JD3s6w==",
+ "GSWncBq4nwomZCBoxCULww==",
+ "9k17UqdR1HzlF7OBAjpREA==",
+ "TrWS+reCJ0vbrDNT5HDR9w==",
+ "CXMKIdGvm60bgfsNc+Imvg==",
+ "6NP81geiL14BeQW6TpLnUA==",
+ "hW9DJA1YCxHmVUAF7rhSmQ==",
+ "8M0kSvjn5KN8bjsMdUqKZQ==",
+ "eS/vTdSlMUnpmnl1PbHjyw==",
+ "h2B0ty0GobQhDnFqmKOpKQ==",
+ "n7KL1Kv027TSxBVwzt9qeA==",
+ "yYmnM/WOgi+48Rw7foGyXA==",
+ "FhthAO5IkMyW4dFwpFS7RA==",
+ "81ZH3SO0NrOO+xoR/Ngw1g==",
+ "t7HaNlXL16fVwjgSXmeOAQ==",
+ "N+K1ibXAOyMWdfYctNDSZQ==",
+ "yQCLV9IoPyXEOaj3IdFMWw==",
+ "3+zsjCi7TnJhti//YXK35w==",
+ "600mjiWke4u0CDaSQKLOOg==",
+ "K4VS+DDkTdBblG93l2eNkA==",
+ "5KOgetfZR+O2wHQSKt41BQ==",
+ "kj5WqpRCjWAfjM7ULMcuPQ==",
+ "AxEjImKz4tMFieSo7m60Sg==",
+ "jp5Em/0Ml4Txr1ptTUQjpg==",
+ "jQVlDU+HjZ2OHSDBidxX5A==",
+ "4NHQwbb3zWq2klqbT/pG6g==",
+ "PeJS+mXnAA6jQ0WxybRQ8w==",
+ "l6Ssc04/CnsqUua9ELu2iQ==",
+ "nFPDZGZowr3XXLmDVpo7hg==",
+ "yYBIS9PZbKo7Gram7IXWPA==",
+ "/HU2+fBqfWTEuqINc0UZSA==",
+ "adT+OjEB2kqpeYi4kQ6FPg==",
+ "GW1Uaq622QamiiF24QUA0g==",
+ "rTwJggSxTbwIYdp07ly0LA==",
+ "4yrFNgqWq17zVCyffULocA==",
+ "vvh9vAIrXjIwLVkuJb5oDQ==",
+ "C7UaoIEXsVRxjeA0u99Qmw==",
+ "x1A74vg/hwwjAx6GrkU8zw==",
+ "7XRiYvytcwscemlxd9iXIQ==",
+ "64AA4jLHXc1Dp15aMaGVcA==",
+ "u/QxrP1NOM/bOJlJlsi/jQ==",
+ "5M3dFrAOemzQ0MAbA8bI5w==",
+ "wyqmQGB6vgRVrYtmB2vB7w==",
+ "8vLA9MOdmLTo3Qg+/2GzLA==",
+ "/u5W2Gab4GgCMIc4KTp2mg==",
+ "lhAOM81Ej6YZYBu45pQYgg==",
+ "MArbGuIAGnw4+fw6mZIxaw==",
+ "ZZImGypBWwYOAW43xDRWCQ==",
+ "L2IeUnATZHqOPcrnW2APbA==",
+ "bQKkL+/KUCsAXlwwIH0N3w==",
+ "f09F7+1LRolRL5nZTcfKGA==",
+ "hPnPQOhz4QKhZi02KD6C+A==",
+ "78b8sDBp28zUlYPV5UTnYw==",
+ "iVDd2Zk7vwmEh97LkOONpQ==",
+ "LHQETSI5zsejvDaPpsO29g==",
+ "Yjm5tSq1ejZn3aWqqysNvA==",
+ "gkrg0NR0iCaL7edq0vtewA==",
+ "Lo1xTCEWSxVuIGEbBEkVxA==",
+ "8GyPup4QAiolFJ9v80/Nkw==",
+ "3L3KEBHhgDwH615w4OvgZA==",
+ "hJSP7CostefBkJrwVEjKHA==",
+ "9oQ/SVNJ4Ye9lq8AaguGAQ==",
+ "n7Bns42aTungqxKkRfQ5OQ==",
+ "K5lhaAIZkGeP5rH2ebSJFw==",
+ "ZaPsR9X77SNt7dLjMJUh8A==",
+ "18ndtDM9UaNfBR1cr3SHdA==",
+ "0QbH4oI8IjZ9BRcqRyvvDQ==",
+ "J/eAtAPswMELIj8K2ai+Xg==",
+ "qenHZKKlTUiEFv6goKM/Mw==",
+ "vjrSYGUpeKOtJ2cNgLFg2g==",
+ "DA+3fjr7mgpwf6BZcExj0w==",
+ "rh7bzsTQ1UZjG7amysr0Gg==",
+ "tFMJRXfWE9g78O1uBUxeqQ==",
+ "e/nWuo5YalCAFKsoJmFyFA==",
+ "gqehq46BhFX2YLknuMv02w==",
+ "Uudn69Kcv2CGz2FbfJSSEA==",
+ "Otz/PgYOEZ1CQDW54FWJIQ==",
+ "IwfeA6d0cT4nDTCCRhK+pA==",
+ "jgNijyoj2JrQNSlUv4gk4A==",
+ "KzWdWPP2gH0DoMYV4ndJRg==",
+ "pv/m2mA/RJiEQu2Qyfv9RA==",
+ "ATmMzriwGLl+M3ppkfcZNA==",
+ "tVvWdA+JqH0HR2OlNVRoag==",
+ "n6QVaozMGniCO0PCwGQZ6w==",
+ "gU3gu8Y5CYVPqHrZmLYHbQ==",
+ "cBBOQn7ZjxDku0CUrxq2ng==",
+ "w+jzM0I5DRzoUiLS/9QIMQ==",
+ "MLlVniZ08FHAS5xe+ZKRaA==",
+ "wMyJLQJdmrC2TSeFkIuSvQ==",
+ "dG98w8MynOoX7aWmkvt+jg==",
+ "zm+z+OOyHhljV2TjA3U9zw==",
+ "Tk5MAqd1gyHpkYi8ErlbWg==",
+ "g6zSo8BvLuKqdmBFM1ejLA==",
+ "d0VAZLbLcDUgLgIfT1GmVQ==",
+ "SNPYH4r/J9vpciGN2ybP5Q==",
+ "XA2hUgq3GVPpxtRYiqnclg==",
+ "fVCRaPsTCKEVLkoF4y3zEw==",
+ "FpgdsQ2OG+bVEy3AeuLXFQ==",
+ "JquDByOmaQEpFb47ZJ4+JA==",
+ "e369ZIQjxMZJtopA//G55Q==",
+ "Nsd+DfRX6L54xs+iWeMjCQ==",
+ "+/UCpAhZhz368iGioEO8aQ==",
+ "e5l9ZiNWXglpw6nVCtO8JQ==",
+ "Cl1u5nGyXaoGyDmNdt38Bw==",
+ "6sNP0rzCCm3w976I2q2s/w==",
+ "qcpeZWUlPllQYZU6mHVwUw==",
+ "kzYddqiMsY3EYrpxve2/CQ==",
+ "3iC21ByW/YVL+pSyppanWw==",
+ "3HPOzIZxoaQAmWRy9OkoSg==",
+ "xsCZVhCk2qJmOqvUjK3Y8Q==",
+ "i2sSvrTh/RdLJX0uKhbrew==",
+ "7Y87wVJok20UfuwkGbXxLg==",
+ "ibsb1ncaLZXAYgGkMO7tjQ==",
+ "+VfRcTBQ80KSeJRdg0cDfw==",
+ "kgKWQJJQKLUuD2VYKIKvxA==",
+ "ARKIvf4+zRF8eCvUITWPng==",
+ "1fztTtQWNMIMSAc5Hr6jMQ==",
+ "md6zNd7ZBn3qArYqQz7/fw==",
+ "kvAaIJb+aRAfKK104dxFAA==",
+ "UIXytIHyVODxlrg+eQoARA==",
+ "Dk0L/lQizPEb3Qud6VHb1Q==",
+ "64YsV2qeDxk2Q6WK/h7OqA==",
+ "90dtIMq0ozJXezT2r79vMQ==",
+ "wy/Z8505o4sVovk4UuBp1A==",
+ "ytDXLDBqWiU1w3sTurYmaw==",
+ "9pk75mBzhmcdT+koHvgDlw==",
+ "DQeib845UqBMEl96sqsaSg==",
+ "UPYR575ASaBSZIR3aX1IgQ==",
+ "swsVVsPi/5aPFBGP+jmPIw==",
+ "1cj1Fpd3+UiBAOahEhsluA==",
+ "ifuJCv9ZA84Vz1FYAPsyEA==",
+ "uu+ncs63SdQIvG6z4r7Q3Q==",
+ "UvC1WADanMrhT+gPp/yVqA==",
+ "llOvGOUDVfX68jKnAlvVRA==",
+ "SusSOsWNoAerAIMBVWHtfA==",
+ "VznvTPAAwAev+yhl9oZT0w==",
+ "luR/kvHLwA6tSdLeTM4TzA==",
+ "PcdBtV8pfKU0YbDpsjPgwg==",
+ "5l6kDfjtZjkTZPJvNNOVFw==",
+ "4FBBtWPvqJ3dv4w25tRHiQ==",
+ "JJbzQ/trOeqQomsKXKwUpQ==",
+ "0bj069wXgEJbw7dpiPr8Tg==",
+ "tejpAZp7y32SO2+o4OGvwQ==",
+ "kq26VyDyJTH/eM6QvS2cMw==",
+ "+zBkeHF4P8vLzk1iO1Zn3Q==",
+ "BzkNYH03gF/mQY71RwO3VA==",
+ "RnxOYPSQdHS6fw4KkDJtrA==",
+ "65KhGKUBFQubRRIEdh9SwQ==",
+ "k1DPiH6NkOFXP/r3N12GyA==",
+ "DqzWt1gfyu/e7RQl5zWnuQ==",
+ "gnez1VrH+UHT8C/SB9qGdA==",
+ "vZtL0yWpSIA+9v8i23bZSg==",
+ "FNvQqYoe0s/SogpAB7Hr1Q==",
+ "6nwR+e9Qw0qp8qIwH9S/Mg==",
+ "BPT4PQxeQcsZsUQl33VGmg==",
+ "rOYeIcB+Rg5V6JG2k4zS2w==",
+ "Je1UESovkBa9T6wS0hevLw==",
+ "HFHMGgfOeO0UPrray1G+Zw==",
+ "NBmB/cQfS+ipERd7j9+oVg==",
+ "iIm8c9uDotr87Aij+4vnMw==",
+ "S3VQa6DH+BdlSrxT/g6B5g==",
+ "BwRA+tMtwEvth28IwpZx+w==",
+ "vg3jozLXEmAnmJwdfcEN0g==",
+ "gW0oKhtQQ7BxozxUWw5XvQ==",
+ "Q6vGRQiNwoyz7bDETGvi5g==",
+ "Ak3rlzEOds6ykivfg39xmw==",
+ "G4qzBI1sFP2faN+tlRL/Bw==",
+ "ND9l4JWcncRaSLATsq0LVw==",
+ "yQmNZnp/JZywbBiZs3gecA==",
+ "ZoNSxARrRiKZF5Wvpg7bew==",
+ "GhpJfRSWZigLg/azTssyVA==",
+ "QyyiJ5I/OZC50o89fa5EmQ==",
+ "4kj0S8XlmhHXoUP7dQItUw==",
+ "Dt8Q5ORzTmpPR2Wdk0k+Aw==",
+ "/hFhjFGJx2wRfz6hyrIpvA==",
+ "eFimq+LuHi42byKnBeqnZQ==",
+ "JrKGKAKdjfAaYeQH8Y2ZRQ==",
+ "JFFeXsFsMA59iNtZey7LAA==",
+ "91SdBFJEZ65M+ixGaprY/A==",
+ "+S+WXgVDSU1oGmCzGwuT3g==",
+ "1X14kHeKwGmLeYqpe60XEA==",
+ "4xojeUxTFmMLGm6jiMYh/Q==",
+ "+1e7jvUo8f2/2l0TFrQqfA==",
+ "8WU1vLKV1GhrL7oS9PpABg==",
+ "DYWCPUq/hpjr6puBE7KBHg==",
+ "birqO8GOwGEI97zYaHyAuw==",
+ "6e8boFcyc8iF0/tHVje4eQ==",
+ "FLvED9nB9FEl9LqPn7OOrA==",
+ "ji306HRiq965zb8EZD2uig==",
+ "AklOdt9/2//3ylUhWebHRw==",
+ "VGRCSrgGTkBNb8sve0fYnQ==",
+ "oqlkgrYe9aCOwHXddxuyag==",
+ "KXuFON8tMBizNkCC48ICLA==",
+ "9aKH1u5+4lgYhhLztQ4KWA==",
+ "3hVslsq98QCDIiO40JNOuA==",
+ "OOS6wQCJsXH8CsWEidB35A==",
+ "YXHQ3JI9+oca8pc/jMH6mA==",
+ "V9vkAanK+Pkc4FGAokJsTA==",
+ "OFLn4wun6lq484I7f6yEwg==",
+ "3WVBP9fyAiBPZAq3DpMwOQ==",
+ "5gGoDPTc/sOIDLngmlEq4A==",
+ "E2lvMXqHdTw0x+KCKVnblg==",
+ "f1Gs++Iilgq9GHukcnBG3w==",
+ "uIkVijg7RPi/1j7c18G1qA==",
+ "9T7gB0ZkdWB0VpbKIXiujQ==",
+ "KCJJfgLe00+tjSfP6EBcUg==",
+ "WbAdlac/PhYUq7J2+n5f+w==",
+ "GLnS9wDCje7TOMvBX9jJVA==",
+ "VAg/aU5nl72O+cdNuPRO4g==",
+ "kzTl7WH/JXsX1fqgnuTOgw==",
+ "1HDgfU7xU7LWO/BXsODZAQ==",
+ "D0W5F7gKMljoG5rlue1jrg==",
+ "9reBKZ1Rp6xcdH1pFQacjw==",
+ "SSKhl2L3Mvy93DcZulADtA==",
+ "hlu7os0KtAkpBTBV6D2jyQ==",
+ "sfte/o9vVNyida/yLvqADA==",
+ "gYGQBLo5TdMyXks0LsZhsQ==",
+ "dNq2InSVDGnYXjkxPNPRxA==",
+ "fiv0DJivQeqUkrzDNlluRw==",
+ "msstzxq++XO0AqNTmA7Bmg==",
+ "DCjgaGV5hgSVtFY5tcwkuA==",
+ "aMmrAzoRWLOMPHhBuxczKg==",
+ "qNOSm15bdkIDSc/iUr+UTQ==",
+ "2nSTEYzLK77h5Rgyti+ULQ==",
+ "BhKO1s1O693Fjy1LItR/Jw==",
+ "kRnBEH6ILR5GNSmjHYOclw==",
+ "R97chlspND/sE9/HMScXjQ==",
+ "1Oykse0jQVbuR3MvW5ot4A==",
+ "Dmyb+a7/QFsU4d2cVQsxDw==",
+ "W5now3RWSzzMDAxsHSl++Q==",
+ "IrDuBrVu1HWm0BthAHyOLQ==",
+ "V6zyoX6MERIybGhhULnZiw==",
+ "ZQSDYgpsimK+lYGdXBWE/w==",
+ "lV70RNlE++04G1KFB3BMXA==",
+ "QmSBVvdk0tqH9RAicXq2zA==",
+ "qNyy6Fc0b8oOMWqqaliZ/w==",
+ "xvipmmwKdYt4eoKvvRnjEg==",
+ "Q7Df6zGwvb4rC+EtIKfaSw==",
+ "n1M2dgFPpmaICP+JwxHUug==",
+ "1k8tL2xmGFVYMgKUcmDcEw==",
+ "fFvXa1dbMoOOoWZdHxPGjw==",
+ "UP9mmAKzeQqGhod7NCqzhg==",
+ "PMCWKgog/G+GFZcIruSONw==",
+ "dnvatwSEcl73ROwcZ4bbIQ==",
+ "hY82j+sUQQRpCi6CCGea5A==",
+ "QoUC9nyK1BAzoUVnBLV2zw==",
+ "+aF4ilbjQbLpAuFXQEYMWQ==",
+ "XTCcsVfEvqxnjc0K5PLcyw==",
+ "ML7ipnY/g8mA1PUIju1j8Q==",
+ "tOkYq1BZY152/7IJ6ZYKUg==",
+ "2bsIpvnGcFhTCSrK9EW1FQ==",
+ "Af9j1naGtnZf0u1LyYmK1w==",
+ "ZmblZauRqO5tGysY3/0kDw==",
+ "PF0lpolQQXlpc3qTLMBk8w==",
+ "emVLJVzha7ui5OFHPJzeRQ==",
+ "gR0sgItXIH8hE4FVs9Q07w==",
+ "PTW+fhZq/ErxHqpM0DZwHQ==",
+ "g0kHTNRI7x/lAsr92EEppw==",
+ "24H9q+E8pgCEdFS7JO5kzQ==",
+ "HtDXgMuF8PJ1haWk88S0Ew==",
+ "pulldyBt2sw6QDvTrCh6zw==",
+ "ehwc2vvwNUAI7MxU4MWQZw==",
+ "enj9VEzLbmeOyYugTmdGfQ==",
+ "auvG6kWMnhCMi7c7e9eHrw==",
+ "R36O31Pj8jn0AWSuqI7X2Q==",
+ "3AVYtcIv7A5mVbVnQMaCeA==",
+ "T9WoUJNwp8h4Yydixbx6nA==",
+ "t0WN8TwMLgi8UVEImoFXKg==",
+ "mS99D+CXhwyfVt8xJ+dJZA==",
+ "AFdelaqvxRj6T3YdLgCFyg==",
+ "Lu02ic/E94s42A14m7NGCA==",
+ "7w3b73nN/fIBvuLuGZDCYQ==",
+ "O209ftgvu0vSr0UZywRFXA==",
+ "MQvAr+OOfnYnr/Il/2Ubkg==",
+ "e5txnNRcGs2a9+mBFcF1Qg==",
+ "YA0kMTJ82PYuLA4pkn4rfw==",
+ "QIKjir/ppRyS63BwUcHWmw==",
+ "P3y5MoXrkRTSLhCdLlnc4A==",
+ "WY7mCUGvpXrC8gkBB46euw==",
+ "g0GbRp2hFVIdc7ct7Ky7ag==",
+ "Cv079ZF55RnbsDT27MOQIA==",
+ "cvMJ714elj/HUh89a9lzOQ==",
+ "9inw7xzbqAnZDKOl/MfCqA==",
+ "F58ktE4O0f7C9HdsXYm+lw==",
+ "CsPkyTZADMnKcgSuNu1qxg==",
+ "mAzsVkijuqihhmhNTTz65g==",
+ "FxnbKnuDct4OWcnFMT/a5w==",
+ "P5wS+xB8srW4a5KDp/JVkA==",
+ "ctJYJegZhG42i+vnPFWAWw==",
+ "OrqJKjRndcZ8OjE3cSQv7g==",
+ "aXqiibI6BpW3qilV6izHaQ==",
+ "BA18GEAOOyVXO2yZt2U35w==",
+ "saEpnDGBSZWqeXSJm34eOA==",
+ "CUEueo8QXRxkfVdfNIk/gg==",
+ "H0UMAUfHFQH92A2AXRCBKA==",
+ "CT9g8mKsIN/VeHLSTFJcNQ==",
+ "E4NtzxQruLcetC23zKVIng==",
+ "203EqmJI9Q4tWxTJaBdSzA==",
+ "Do3aqbRKtmlQI2fXtSZfxQ==",
+ "JaYQXntiyznQzrTlEeZMIw==",
+ "VK95g27ws2C6J2h/7rC2qA==",
+ "CQ0PPwgdG3N6Ohfwx1C8xA==",
+ "/MeHciFhvFzQsCIw39xIZA==",
+ "u5cUPxM6/spLIV8VidPrAA==",
+ "OwArFF1hpdBupCkanpwT+Q==",
+ "PdBgXFq5mBqNxgCiqaRnkw==",
+ "lC5EumoIcctvxYqwELqIqw==",
+ "xoPSM86Se+1hHX0y3hhdkw==",
+ "F5bs0GGWBx9eBwcJJpXbqg==",
+ "1mw6LfTiirFyfjejf8QNGA==",
+ "daBhAvmE9shDgmciDAC5eg==",
+ "AvdeYb9XNOUFWiiz+XGfng==",
+ "JJJkp1TpuDx5wrua2Wml7g==",
+ "3y5Xk65ShGvWFbQxcZaQAQ==",
+ "l6QHU5JsJExNoOnqxBPVbw==",
+ "X2YfnPXgF2VHVX95ZcBaxQ==",
+ "g6udffWh7qUnSIo1Ldn3eA==",
+ "V2P75JFB4Se9h7TCUMfeNA==",
+ "IUZ5aGpkJ9rLgSg6oAmMlw==",
+ "pyrUqiZ98gVXxlXQNXv5fA==",
+ "83ERX2XJV3ST4XwvN7YWCg==",
+ "eJDUejE/Ez/7kV+S74PDYg==",
+ "M9oqlPb63e0kZE0zWOm+JQ==",
+ "0rTYcuVYdilO7zEfKrxY3A==",
+ "rfPTskbnoh3hRJH6ZAzQRg==",
+ "QtD35QhE8sAccPrDnhtQmQ==",
+ "jpNUgFnanr9Sxvj2xbBXZw==",
+ "nykEOLL/o7h0cs0yvdeT2g==",
+ "wX2URK6eDDHeEOF3cgPgHA==",
+ "jqPQ0aOuvOJte/ghI1RVng==",
+ "nHTsDl0xeQPC5zNRnoa0Rw==",
+ "mNv2Q67zePjk/jbQuvkAFA==",
+ "HjlPM2FQWdILUXHalIhQ5w==",
+ "cHkOsVd80Rgwepeweq4S1g==",
+ "kTCHqcb3Cos51o8cL+MXcg==",
+ "nvmBgp0YlUrdZ05INsEE8Q==",
+ "kFrRjz7Cf2KvLtz9X6oD+w==",
+ "Tmx0suRHzlUK4FdBivwOwA==",
+ "bG+P+p34t/IJ1ubRiWg6IA==",
+ "uESeJe/nYrHCq4RQbrNpGA==",
+ "ehfPlu6YctzzpQmFiQDxGA==",
+ "ZH5Es/4lJ+D5KEkF1BVSGg==",
+ "HHxn4iIQ7m0tF1rSd+BZBg==",
+ "DQJRsUwO1fOuGlkgJavcwQ==",
+ "HITIVoFoWNg04NExe13dNA==",
+ "MeKXnEfxeuQu9t3r/qWvcw==",
+ "Y7OofF9eUvp7qlpgdrzvkg==",
+ "XSb71ae0v+yDxNF5HJXGbQ==",
+ "p8W1LgFuW6JSOKjHkx3+aA==",
+ "y2JOIoIiT9cV1VxplZPraQ==",
+ "MN94B0r5CNAF9sl3Kccdbw==",
+ "Q1pdQadt12anX1QRmU2Y/A==",
+ "JIC8R48jGVqro6wmG2KXIw==",
+ "eWgLAqJOU+fdn8raHb9HCw==",
+ "5CMadLqS2KWwwMCpzlDmLw==",
+ "H1y2iXVaQYwP0SakN6sa+Q==",
+ "CUCjG2UaEBmiYWQc6+AS1Q==",
+ "yV3IbbTWAbHMhMGVvgb/ZQ==",
+ "80PCwYh4llIKAplcDvMj4g==",
+ "fgdUFvQPb5h+Rqz8pzLsmw==",
+ "2SI4F7Vvde2yjzMLAwxOog==",
+ "kJdY3XEdJS/hyHdR+IN0GA==",
+ "IKgNa2oPaFVGYnOsL+GC5Q==",
+ "eXFOya6x5inTdGwJx/xtUQ==",
+ "uTA0XbiH3fTeVV7u5z0b3w==",
+ "onFcHOO1c3pDdfCb5N4WkQ==",
+ "Slu3z535ijcs5kzDnR7kfA==",
+ "SElc2+YVi3afE1eG1MI7dQ==",
+ "ND2hYtAIQGMxBF7o7+u7nQ==",
+ "Pv9FWQEDLKnG/9K9EIz4Gw==",
+ "6CjtF1S2Y6RCbhl7hMsD+g==",
+ "rs2QrN4qzAHCHhkcrAvIfA==",
+ "eTMPXa60OTGjSPmvR4IgGw==",
+ "pvXHwJ3dwf9GDzfDD9JI3g==",
+ "CRmAj3JcasAb4iZ9ZbNIbw==",
+ "rcY4Ot40678ByCfqvGOGdg==",
+ "l4ddTxbTCW5UmZW+KRmx6A==",
+ "NKRzJndo2uXNiNppVnqy1g==",
+ "0NrvBuyjcJ2q6yaHpz/FOA==",
+ "3YXp1PmMldUjBz3hC6ItbA==",
+ "CmVD6nh8b/04/6JV9SovlA==",
+ "HjyxyL0db2hGDq2ZjwOOhg==",
+ "4PBaoeEwUj79njftnYYqLg==",
+ "vFFzkWgGyw6OPADONtEojQ==",
+ "czBWiYsQtNFrksWwoQxlOw==",
+ "9iB7+VwXRbi6HLkWyh9/kg==",
+ "zwY6tCjjya/bgrYaCncaag==",
+ "mW6TCje9Zg2Ep7nzmDjSYQ==",
+ "5LJqHFRyIwQKA4HbtqAYQQ==",
+ "INNBBin5ePwTyhPIyndHHg==",
+ "dChBe9QR29ObPFu/9PusLg==",
+ "1dhq3ozNCx0o4dV1syLVDA==",
+ "nyaekSYTKzfSeSfPrB114Q==",
+ "TfNHjSTV8w6Pg6+FaGlxvA==",
+ "m/Lp4U75AQyk9c8cX14HJg==",
+ "uU1TX5DoDg6EcFKgFcn0GA==",
+ "B+TsxQZf0IiQrU8X9S4dsQ==",
+ "6b7ue29cBDsvmj1VSa5njw==",
+ "RvXWAFwM+mUAPW1MjPBaHA==",
+ "pdaY6kZ8+QqkMOInvvACNA==",
+ "7nr3zyWL+HHtJhRrCPhYZA==",
+ "BXGlq54wIH6R3OdYfSSDRw==",
+ "b06KGv5zDYsTxyTbQ9/eyA==",
+ "8ylI1AS3QJpAi3I/NLMYdg==",
+ "0fpe9E6m3eLp/5j5rLrz2Q==",
+ "Qrh7OEHjp80IW+YzQwzlJg==",
+ "lqhgbgEqROAdfzEnJ17eXA==",
+ "Dulw855DfgIwiK7hr3X8vg==",
+ "wsp+vmW8sEqXYVURd/gjHA==",
+ "VoPth5hDHhkQcrQTxHXbuw==",
+ "TgWe70YalDPyyUz6n88ujg==",
+ "9lLhHcrPWI4EsA4fHIIXuw==",
+ "UymZUnEEQWVnLDdRemv+Tw==",
+ "qnkFUlJ8QT322JuCI3LQgg==",
+ "/p/aCTIhi1bU0/liuO/a2Q==",
+ "hWoxz5HhE50oYBNRoPp1JQ==",
+ "88tB/HgUIUnqWXEX++b5Aw==",
+ "Z8T1b9RsUWf59D06MUrXCQ==",
+ "BZTzHJGhzhs3mCXHDqMjnQ==",
+ "XfY+QUriCAA1+3QAsswdgg==",
+ "TZ3ATPOFjNqFGSKY3vP2Hw==",
+ "cl4t9FXabQg7tbh1g7a0OA==",
+ "9SgfpAY0UhNC6sYGus9GgQ==",
+ "d/Wd3Ma1xYyoMByPQnA9Cw==",
+ "DDitrRSvovaiXe2nfAtp4g==",
+ "s+eHg5K9zZ2Jozu5Oya9ZQ==",
+ "z3L2BNjQOMOfTVBUxcpnRA==",
+ "v4xIYrfPGILEbD/LwVDDzA==",
+ "HoaBBw2aPCyhh0f5GxF+/Q==",
+ "i9IRqAqKjBTppsxtPB7rdw==",
+ "cWUg7AfqhiiEmBIu+ryImA==",
+ "E+02smwQGBIxv42LIF2Y4Q==",
+ "W4CfeVp9mXgk04flryL7iA==",
+ "9SUOfKtfKmkGICJnvbIDMg==",
+ "xweGAZf+Yb3TtwR/sGmGIA==",
+ "EJgedRYsZPc4cT9rlwaZhg==",
+ "wv4NC9CIpwuGf/nOQYe/oA==",
+ "ZXeMG5eqQpZO/SGKC4WQkA==",
+ "bzXXzQGZs8ustv0K4leklA==",
+ "RkQK9S1ezo+dFYHQP57qrw==",
+ "mrinv7KooPQPrLCNTRWCFg==",
+ "qIUJPanWmGzTD1XxvHp+6w==",
+ "Js7g8Dr6XsnGURA4UNF0Ug==",
+ "dpSTNOCPFHN5yGoMpl1EUA==",
+ "ugY8rTtJkN4CXWMVcRZiZw==",
+ "rqHKB91H3qVuQAm+Ym5cUA==",
+ "UjmDFO7uzjl4RZDPeMeNyg==",
+ "cu4ZluwohhfIYLkWp72pqA==",
+ "ZydKlOpn2ySBW0G3uAqwuw==",
+ "LWd0+N3M94n81qd346LfJQ==",
+ "VbHoWmtiiPdABvkbt+3XKQ==",
+ "J4MC9He6oqjOWsYQh9nl3Q==",
+ "ahAbmGJZvUOXrcK6OydNGQ==",
+ "Byhi4ymFqqH8uIeoMRvPug==",
+ "LSN9GmT6LUHlCAMFqpuPIA==",
+ "IAMInfSYb76GxDlAr1dsTg==",
+ "qYHdgFAXhF/XcW4lxqfvWQ==",
+ "26+yXbqI+fmIZsYl4UhUzw==",
+ "AwPTZpC28NJQhf5fNiJuLA==",
+ "SESKbGF35rjO64gktmLTWA==",
+ "YVlRQHQglkbj3J2nHiP/Hw==",
+ "DdaT4JLC7U0EkF50LzIj9w==",
+ "G0LChrb0OE5YFqsfTpIL1Q==",
+ "5Yrj6uevT8wHRyqqgnSfeg==",
+ "NmWmDxwK5FpKlZbo0Rt8RA==",
+ "iUsUCB0mfRsE9KPEQctIzw==",
+ "Tm4zk2Lmg8w4ITMI31NfTA==",
+ "Vu0E+IJXBnc25x4n41kQig==",
+ "6wkfN8hyKmKU6tG3YetCmw==",
+ "trjM81KANPZrg9iSThWx6Q==",
+ "iGuY4VxcotHvMFXuXum7KA==",
+ "ICPdBCdONUqPwD5BXU5lrw==",
+ "alqHQBz8V446EdzuVfeY5Q==",
+ "74FW/QYTzr/P1k6QwVHMcw==",
+ "avZp5K7zJvRvJvpLSldNAw==",
+ "TIKadc6FAaRWSQUg5OATgg==",
+ "PfkWkSbAxIt1Iso0znW0+Q==",
+ "Z+bsbVP91KrJvxrujBLrrQ==",
+ "mrxlFD3FBqpSZr1kuuwxGg==",
+ "nUgYO7/oVNSX8fJqP2dbdg==",
+ "tVhXk9Ff3wAg56FbdNtcFg==",
+ "DdiNGiOSoIZxrMrGNvqkXw==",
+ "CDsanJz7e3r/eQe+ZYFeVQ==",
+ "wVfSZYjMjbTsD2gaSbwuqQ==",
+ "6c0iuya20Ys8BsvoI4iQaQ==",
+ "qCPfJTR8ecTw6u6b1yHibA==",
+ "fZrj3wGQSt8RXv0ykJROcQ==",
+ "gR3B8usSEb0NLos51BmJQg==",
+ "vTAmgfq3GxL4+ubXpzwk5w==",
+ "jLkmUZ6fV56GfhC0nkh4GA==",
+ "3v09RHCPTLUztqapThYaHg==",
+ "nULSbtw2dXbfVjZh33pDiA==",
+ "IHhyR6+5sZXTH+/NrghIPg==",
+ "tnUtJ/DQX9WaVJyTgemsUA==",
+ "7xTKFcog69nTmMfr5qFUTA==",
+ "IshzWega6zr3979khNVFQQ==",
+ "Ng5v/B9Z10TTfsDFQ/XrXQ==",
+ "hnCUnoxofUiqQvrxl73M8w==",
+ "VPa7DG6v7KnzMvtJPb88LQ==",
+ "4LtQrahKXVtsbXrEzYU1zQ==",
+ "Ev/xjTi7akYBI7IeZJ4Igw==",
+ "41WEjhYUlG6jp2UPGj11eQ==",
+ "JvXTdChcE3AqMbFYTT3/wg==",
+ "2rOkEVl90EPqfHOF5q2FYw==",
+ "mjFBVRJ7TgnJx+Q74xllPg==",
+ "Uy4QI8D2y1bq/HDNItCtAw==",
+ "wMOE/pEKVIklE75xjt6b6w==",
+ "ZcuIvc8fDI+2uF0I0uLiVA==",
+ "CX/N/lHckmAtHKysYtGdZA==",
+ "j8to4gtSIRYpCogv2TESuQ==",
+ "iS9wumBV5ktCTefFzKYfkA==",
+ "ewPT4dM12nDWEDoRfiZZnA==",
+ "vWn9OPnrJgfPavg4D6T/HQ==",
+ "J/PNYu4y6ZMWFFXsAhaoow==",
+ "catI+QUNk3uJ+mUBY3bY8Q==",
+ "F8tEIT5EhcvLNRU5f0zlXQ==",
+ "zyA9f5J7mw5InjhcfeumAQ==",
+ "MlOOZOwcRGIkifaktEq0aQ==",
+ "Pt3i49uweYVgWze3OjkjJA==",
+ "sfIClgTMtZo9CM9MHaoqhQ==",
+ "HeQbUuBM9sqfXFXRBDISSw==",
+ "SFn78uklZfMtKoz2N0xDaQ==",
+ "H6j2nPbBaxHecXruxiWYkA==",
+ "fU32wmMeD44UsFSqFY0wBA==",
+ "hDILjSpTLqJpiSSSGu445A==",
+ "ieEAgvK9LsWh2t6DsQOpWA==",
+ "xfjBQk3CrNjhufdPIhr91A==",
+ "j+8/VARfbQSYhHzj0KPurQ==",
+ "/zFLRvi75UL8qvg+a6zqGg==",
+ "U0KmEI6e5zJkaI4YJyA5Ew==",
+ "uXvr6vi5kazZ9BCg2PWPJA==",
+ "jEqP0dyHKHiUjZ9dNNGTlQ==",
+ "1xWx5V3G9murZP7srljFmA==",
+ "OIwtfdq37eQ0qoXuB2j7Hw==",
+ "fUAy3f9bAglLvZWvkO2Lug==",
+ "duRFqmvqF93uf/vWn8aOmg==",
+ "ysRQ+7Aq7eVLOp88KnFVMA==",
+ "CkZUmKBAGu0FLpgPDrybpw==",
+ "TrLmfgwaNATh24eSrOT+pw==",
+ "83wtvSoSP9FVBsdWaiWfpA==",
+ "pUfWmRXo70yGkUD/x5oIvA==",
+ "PybPZhJErbRTuAafrrkb3g==",
+ "8hsfXqi4uiuL+bV1VrHqCw==",
+ "TVlHoi8J7sOZ2Ti7Dm92cQ==",
+ "za4rzveYVMFe3Gw531DQJQ==",
+ "JKphO0UYjFqcbPr6EeBuqg==",
+ "hqeSvwu8eqA072iidlJBAw==",
+ "bUF0JIfS4uKd3JZj2xotLQ==",
+ "hKOsXOBoFTl/K4xE+RNHDA==",
+ "JHBjKpCgSgrNNACZW1W+1w==",
+ "Rrq0ak9YexLqqbSD4SSXlw==",
+ "+NmjwjsPhGJh9bM10SFkLw==",
+ "xMIHeno2qj3V8q9H1xezeg==",
+ "TcFinyBrUoAEcLzWdFymow==",
+ "Rvchz/xjcY9uKiDAkRBMmA==",
+ "TYlnrwgyeZoRgOpBYneRAg==",
+ "PbnxuVerGwHyshkumqAARg==",
+ "iFtadcw8v6betKka9yaJfg==",
+ "7wgT9WIiMVcrj48PVAMIgw==",
+ "2HHqeGRMfzf3RXwVybx+ZQ==",
+ "tdgI9v7cqJsgCAeW1Fii1A==",
+ "4ZFYKa7ZgvHyZLS6WpM8gA==",
+ "gB8wkuIzvuDAIhDtNT1gyA==",
+ "g1ELwsk6hQ+RAY1BH640Pg==",
+ "UZoibx+y1YJy/uRSa9Oa2w==",
+ "yS/yMnJDHW0iaOsbj4oPTg==",
+ "JzW+yhrjXW1ivKu3mUXPXg==",
+ "/wIZAye9h1TUiZmDW0ZmYA==",
+ "YK+q7uJObkQZvOwQ9hplMg==",
+ "Rs8deApkoosIJSfX7NXtAA==",
+ "MsCloSmTFoBpm7XWYb+ueQ==",
+ "3ltw31yJuAl4VT6MieEXXw==",
+ "1+qmrbC8c7MJ6pxmDMcKuA==",
+ "AYxGETZs477n2sa1Ulu/RQ==",
+ "Q0TJZxpn3jk67L7N+YDaNA==",
+ "OGpsXRHlaN8BvZftxh1e7A==",
+ "UbABE6ECnjB+9YvblE9CYw==",
+ "kZ0D191c/uv4YMG15yVLDw==",
+ "QWURrsEgxbJ8MWcaRmOWqw==",
+ "xiFlcSfa/gnPiO+LwbixcQ==",
+ "Szko0IPE7RX2+mfsWczrMg==",
+ "Ugt8HVC/aUzyWpiHd0gCOQ==",
+ "8j9GVPiFdfIRm/+ho7hpoA==",
+ "KR401XBdgCrtVDSaXqPEiA==",
+ "d0NBFiwGlQNclKObRtGVMQ==",
+ "XEwOJG24eaEtAuBWtMxhwg==",
+ "0Y6iiZjCwPDwD/CwJzfioQ==",
+ "MvMbvZNKbXFe2XdN+HtnpQ==",
+ "fsoXIbq0T0nmSpW8b+bj+g==",
+ "Uje3Ild84sN41JEg3PEHDg==",
+ "i6ZYpFwsyWyMJNgqUMSV1A==",
+ "+P5q4YD1Rr5SX26Xr+tzlw==",
+ "z4oKy2wKH+sbNSgGjbdHGw==",
+ "XwKWd03sAz8MmvJEuN08xA==",
+ "Xv0mNYedaBc57RrcbHr9OA==",
+ "9oUawSwUGOmb0sDn3XS6og==",
+ "9RGIQ2qyevNbSSEF36xk/A==",
+ "q8YF9G2jqydAxSqwyyys5Q==",
+ "m5JIUETVXcRza4VL4xlJbg==",
+ "aRpdnrOyu5mWB1P5YMbvOA==",
+ "rM/BOovNgnvebKMxZQdk7g==",
+ "fQS0jnQMnHBn7+JZWkiE/g==",
+ "gAoV4BZYdW1Wm712YXOhWQ==",
+ "hCzsi1yDv9ja5/o7t94j9Q==",
+ "CoLvjQDQGldGDqRxfQo+WQ==",
+ "pfGcaa49SM3S6yJIPk/EJQ==",
+ "yYp4iuI5f/y/l1AEJxYolQ==",
+ "Jj4IrSVpqQnhFrzNvylSzA==",
+ "4jeOFKuKpCmMXUVJSh9y0g==",
+ "+NMUaQ7XPsAi0rk7tTT9wQ==",
+ "Jt4Eg6MJn8O4Ph/K2LeSUA==",
+ "CiiUeJ0LeWfm7+gmEmYXtg==",
+ "c5Tc7rTFXNJqYyc0ppW+Iw==",
+ "4KJZPCE9NKTfzFxl76GWjg==",
+ "aXs9qTEXLTkN956ch3pnOA==",
+ "f5Xo7F1uaiM760Qbt978iw==",
+ "wpZqFkKafFpLcykN2IISqg==",
+ "vIORTYSHFIXk5E2NyIvWcQ==",
+ "prOsOG0adI4o+oz50moipw==",
+ "blygTgAHZJ3NzyAT33Bfww==",
+ "rBt6L/KLT7eybxKt5wtFdg==",
+ "vMuaLvAntJB5o7lmt/kVXA==",
+ "iujlt9fXcUXEYc+T2s5UjA==",
+ "LyYPOZKm8bBegMr5NTSBfg==",
+ "ZtWvgitOSRDWq7LAKYYd4Q==",
+ "kh51WUI5TRnKhur6ZEpRTQ==",
+ "VzQ1NwNv9btxUzxwVqvHQg==",
+ "8fJLQeIHaTnJ8wGqUiKU6g==",
+ "vvEH5A39TTe1AOC11rRCLA==",
+ "dihDsG7+6aocG6M9BWrCzQ==",
+ "3jqsY8/xTWELmu/az3Daug==",
+ "mpOtwBvle+nyY6lUBwTemw==",
+ "E1CvxFbuu9AYW604mnpGTw==",
+ "1LPC0BzhJbepHTSAiZ3QTw==",
+ "XpGXh76RDgXC4qnTCsnNHA==",
+ "3Gg9N7vjAfQEYOtQKuF/Eg==",
+ "+WpF8+poKmHPUBB4UYh/ig==",
+ "UNt7CNMtltJWq8giDciGyA==",
+ "RIZYDgXqsIdTf9o2Tp/S7g==",
+ "0QCQORCYfLuSbq94Sbt0bQ==",
+ "hvsZ5JmVevK1zclFYmxHaw==",
+ "3+9nURtBK3FKn0J9DQDa3g==",
+ "jdVMQqApseHH3fd91NFhxg==",
+ "VX+cVXV8p9i5EBTMoiQOQQ==",
+ "I5qDndyelK4Njv4YrX7S6w==",
+ "rWliqgfZ3/uCRBOZ9sMmdA==",
+ "vwno3vugCvt6ooT3CD4qIQ==",
+ "cffrYrBX3UQhfX1TbAF+GQ==",
+ "nOiwBFnXxCBfPCHYITgqNg==",
+ "LQttmX92SI94+hDNVd8Gtw==",
+ "iCF+GWw9/YGQXsOOPAnPHQ==",
+ "nwtCsN1xEYaHvEOPzBv+qQ==",
+ "CQpJFrpOvcQhsTXIlJli+Q==",
+ "tYeIZjIm0tVEsYxH1iIiUQ==",
+ "iCnm5fPmSmxsIzuRK6osrA==",
+ "tX8X8KoxUQ8atFSCxgwE1Q==",
+ "hZlX6qOfwxW5SPfqtRqaMw==",
+ "2aIx9UdMxxZWvrfeJ+DcTw==",
+ "TlJizlASbPtShZhkPww4UA==",
+ "p+bx+/WQWALXEBCTnIMr4w==",
+ "4VR5LiXLew6Nyn91zH9L4w==",
+ "bfUD03N2PRDT+MZ+WFVtow==",
+ "cTvDd8okNUx0RCMer6O8sw==",
+ "49jZr/mEW6fvnyzskyN40w==",
+ "vHmQUl4WHXs1E/Shh+TeyA==",
+ "fgXfRuqFfAu8qxbTi4bmhA==",
+ "Wn+Vj4eiWx0WPUHr3nFbyA==",
+ "2SwIiUwT4vRZPrg7+vZqDA==",
+ "nkedTagkmf6YE4tEY+0fKw==",
+ "8nOTDhFyZ8YUA4b6M5p84w==",
+ "qnzWszsyJhYtx8wkMN6b1g==",
+ "ka7pMp8eSiv92WgAsz2vdA==",
+ "pGQEWJ38hb/ZYy2P1+FIuw==",
+ "cVhdRFuZaW/09CYPmtNv5g==",
+ "prCOYlboBnzmLEBG/OeVrQ==",
+ "oIWwTbkVS5DDL47mY9/1KQ==",
+ "PKtXc4x4DEjM45dnmPWzyg==",
+ "f9ywiGXsz+PuEsLTV3zIbQ==",
+ "6G2bD3Y7qbGmfPqH9TqLFA==",
+ "DMHmyn2U2n+UXxkqdvKpnA==",
+ "XOG1PYgqoG8gVLIbVLTQgg==",
+ "1FSrgkUXgZot2CsmbAtkPw==",
+ "BxFP+4o6PSlGN78eSVT1pA==",
+ "EZVQGsXTZvht1qedRLF8bQ==",
+ "eYAQWuWZX2346VMCD6s7/A==",
+ "jkUpkLoIXuu7aSH8ZghIAQ==",
+ "mXPtbPaoNAAlGmUMmJEWBQ==",
+ "HLesnV3DL+FhWF3h6RXe8g==",
+ "nDAsSla+9XfAlQSPsXtzPA==",
+ "RAECgYZmcF4WxcFcZ4A0Ww==",
+ "W+M4BcYNmjj7xAximDGWsA==",
+ "ueODvMv/f9ZD8O0aIHn4sg==",
+ "cszpMdGbsbe6BygqMlnC9Q==",
+ "siHwJx6EgeB1gBT9z/vTyw==",
+ "FN7oLGBQGHXXn5dLnr/ElA==",
+ "Tud+AMyuFkWYYZ73yoJGpQ==",
+ "TuaG3wRdM9BWKAxh2UmAsg==",
+ "8CjmgWQSAAGcXX9kz3kssw==",
+ "ays5/F7JANIgPHN0vp2dqQ==",
+ "PCOGl7GIqbizAKj/sZmlwQ==",
+ "rZKD8oJnIj5fSNGiccfcvA==",
+ "gFEnTI8os2BfRGqx9p5x8w==",
+ "5r1ZsGkrzNQEpgt/gENibw==",
+ "1YO9G8qAhLIu2rShvekedw==",
+ "6ZKmm7IW7IdWuVytLr68CQ==",
+ "mMfn8OaKBxtetweulho+xQ==",
+ "GQJxu1SoMBH14KPV/G/KrQ==",
+ "IYIP2UBRyWetVfYLRsi1SQ==",
+ "Jit0X0srSNFnn8Ymi1EY+g==",
+ "ARCWkHAnVgBOIkCDQ19ZuA==",
+ "qA0sTaeNPNIiQbjIe1bOgQ==",
+ "iGI9uqMoBBAjPszpxjZBWQ==",
+ "+L1FDsr5VQtuYc2Is5QGjw==",
+ "4XNUmgwxsqDYsNmPkgNQYQ==",
+ "Yig+Wh18VIqdsmwtwfoUQw==",
+ "uqp92lAqjec8UQYfyjaEZw==",
+ "QiozlNcQCbqXtwItWExqJQ==",
+ "JFHutgSe1/SlcYKIbNNYwQ==",
+ "Y26jxXvl79RcffH8O8b9Ew==",
+ "bQ7J5mebp38rfP/fuqQOsg==",
+ "HI4ZIE5s8ez8Rb+Mv39FxA==",
+ "OzH7jTcyeM7RPVFtBdakpQ==",
+ "HLxROy6fx/mLXFTDSX4eLA==",
+ "s5RUHVRNAoKMuPR/Jkfc2Q==",
+ "X9QAaNjgiOeAWSphrGtyVw==",
+ "ALJWKUImVE40MbEooqsrng==",
+ "9MDG0WeBPpjGJLEmUJgBWg==",
+ "9RXymE9kCkDvBzWGyMgIWA==",
+ "vFox1d3llOeBeCUZGvTy0A==",
+ "r3lQAYOYhwlLnDWQIunKqg==",
+ "2os5s7j7Tl46ZmoZJH8FjA==",
+ "O5N2yd+QQggPBinQ+zIhtQ==",
+ "ZygAjaN62XhW5smlLkks+Q==",
+ "AgDJsaW0LkpGE65Kxk5+IA==",
+ "omAjyj1l6gyQAlBGfdxJTw==",
+ "fY9VATklOvceDfHZDDk57A==",
+ "StpQm/cQF8cT0LFzKUhC5w==",
+ "CYJB3qy5GalPLAv1KGFEZA==",
+ "coGEgMVs2b314qrXMjNumQ==",
+ "DQQB/l55iPN9XcySieNX3A==",
+ "6dshA8knH5qqD+KmR/kdSQ==",
+ "qyRmvxh8p4j4f+61c10ZFQ==",
+ "apWEPWUvMC24Y+2vTSLXoA==",
+ "RzX2OfSFEd//LhZwRwzBVw==",
+ "NdULoUDGhIolzw1PyYKV0A==",
+ "5w/c9WkI/FA+4lOtdPxoww==",
+ "bV9r7j2kNJpDCEM5E2339Q==",
+ "vbyiKeDCQ4q9dDRI1Q0Ong==",
+ "9xIgKpZGqq0/OU6wM5ZSHw==",
+ "RYkDwwng6eeffPHxt8iD9A==",
+ "w5N/aHbtOIKzcvG3GlMjGA==",
+ "3P2aJxV8Trll2GH9ptElYA==",
+ "yteeQr3ub2lDXgLziZV+DQ==",
+ "yqtj8GfLaUHYv/BsdjxIVw==",
+ "NyF+4VRog7etp90B9FuEjA==",
+ "uwA6N5LptSXqIBkTO0Jd7Q==",
+ "6lVSzYUQ/r0ep4W2eCzFpg==",
+ "1d7RPHdZ9qzAbG3Vi9BdFA==",
+ "7br49X11xc2GxQLSpZWjKQ==",
+ "peMW+rpwmXrSwplVuB/gTA==",
+ "RqYpA5AY7mKPaSxoQfI1CA==",
+ "dqVw2q2nhCvTcW82MT7z0g==",
+ "5S5/asYfWjOwnzYpbK6JDw==",
+ "NvkR0inSzAdetpI4SOXGhw==",
+ "tIqwBotg052wGBL65DZ+yA==",
+ "S4RvORcJ3m6WhnAgV4YfYA==",
+ "UAqf4owQ+EmrE45hBcUMEw==",
+ "4aPU6053cfMLHgLwAZJRNg==",
+ "3Y6/HqS1trYc9Dh778sefg==",
+ "ck86G8HsbXflyrK7MBntLg==",
+ "GLmWLXURlUOJ+PMjpWEXVA==",
+ "jNJQ6otieHBYIXA9LjXprg==",
+ "AsAHrIkMgc3RRWnklY9lJw==",
+ "FCLQocqxxhJeleARZ6kSPg==",
+ "3Leu2Sc+YOntJFlrvhaXeg==",
+ "hSkY45CeB6Ilvh0Io4W6cg==",
+ "DwrNdmU5VFFf3TwCCcptPA==",
+ "u2WQlcMxOACy6VbJXK4FwA==",
+ "E9IlDyULLdeaVUzN6eky8g==",
+ "EXveRXjzsjh8zbbQY2pM9g==",
+ "5VO1inwXMvLDBQSOahT6rg==",
+ "HaHTsLzx7V3G1SFknXpGxA==",
+ "MMaegl2Md9s/wOx5o9564w==",
+ "mpWNaUH9kn4WY26DWNAh3Q==",
+ "w3G+qXXqqKi8F5s+qvkBUg==",
+ "wM8tnXO4PDlLVHspZFcjYw==",
+ "LFcpCtnSnsCPD2gT/RA+Zg==",
+ "bhVbgJ4Do4v56D9mBuR/EA==",
+ "yU3N0HMSP5etuHPNrVkZtg==",
+ "FzqIpOcTsckSNHExrl+9jg==",
+ "BYz52gYI/Z6AbYbjWefcEA==",
+ "h3vYYI9yhpSZV2MQMJtwFQ==",
+ "adJAjAFyR2ne1puEgRiH+g==",
+ "eDcyiPaB954q5cPXcuxAQw==",
+ "40gCrW4YWi+2lkqMSPKBPg==",
+ "ulLuTZqhEDkX0EJ3xwRP9A==",
+ "y4iBxAMn/KzMmaWShdYiIw==",
+ "ilBBNK/IV69xKTShvI94fQ==",
+ "0HN6MIGtkdzNPsrGs611xA==",
+ "twPn6wTGqI0aR//0wP3xtA==",
+ "3UNJ37f+gnNyYk9yLFeoYA==",
+ "4SdHWowXgCpCDL28jEFpAw==",
+ "Mr5mCtC53+wwmwujOU/fWw==",
+ "81pAhreEPxcKse+++h1qBg==",
+ "KmcGEE0pacQ/HDUgjlt7Pg==",
+ "Gt4/MMrLBErhbFjGbiNqQQ==",
+ "lf1fwA0YoWUZaEybE+LyMQ==",
+ "RIVYGO2smx9rmRoDVYMPXw==",
+ "rJ9qVn8/2nOxexWzqIHlcQ==",
+ "lfOLLyZNbsWQgHRhicr4ag==",
+ "wgH1GlUxWi6/yLLFzE76uQ==",
+ "Qg1ubGl+orphvT990e5ZPA==",
+ "Z5B+uOmPZbpbFWHpI9WhPw==",
+ "snGTzo540cCqgBjxrfNpKw==",
+ "ZqkmoGB0p5uT5J6XBGh7Tw==",
+ "uPi8TsGY3vQsMVo/nsbgVQ==",
+ "Y5XR8Igvau/h+c1pRgKayg==",
+ "ZmVpw1TUVuT13Zw/MNI5hQ==",
+ "60suecbWRfexSh7C67RENA==",
+ "kZ/mZZg9YSDmk2rCGChYAg==",
+ "OpL+vHwPasW30s2E1TYgpA==",
+ "ZVnErH1Si4u51QoT0OT7pA==",
+ "3pi3aNVq1QNJmu1j0iyL0g==",
+ "tb5+2dmYALJibez1W4zXgA==",
+ "jOPdd330tB6+7C29a9wn0Q==",
+ "5oD/aGqoakxaezq43x0Tvw==",
+ "HdB7Se47cWjPgpJN0pZuiA==",
+ "6WhHPWlqEUqXC52rHGRHjA==",
+ "WLwpjgr9KzevuogoHZaVUw==",
+ "E8yMPK7W0SIGTK6gIqhxiQ==",
+ "1/Hxu8M9N/oNwk8bCj4FNQ==",
+ "Uo1ebgsOxc3eDRds1ah3ag==",
+ "5pqqzC/YmRIMA9tMFPi7rg==",
+ "ri4AOITPdB1YHyXV+5S51g==",
+ "HfvsiCQN/3mT0FabCU5ygQ==",
+ "UQTQk5rrs6lEb1a+nkLwfg==",
+ "VH70dN82yPCRctmAHMfCig==",
+ "yD3Dd4ToRrl53k/2NSCJiw==",
+ "fO0+6TsjL+45p9mSsMRiIg==",
+ "fM5uYpkvJFArnYiQ3MrQnA==",
+ "V+QzdKh5gxTPp2yPC9ZNEg==",
+ "XHHEg/8KZioW/4/wgSEkbQ==",
+ "2abfl3N46tznOpr+94VONQ==",
+ "gxwbqZDHLbQVqXjaq42BCg==",
+ "WnHK5ZQDR6Da5cGODXeo0A==",
+ "SChDh/Np1HyTPWfICfE1uA==",
+ "yhexr/OFKfZl0o3lS70e4w==",
+ "N65PqIWiQeS082D6qpfrAg==",
+ "RM5CpIiB94Sqxi462G7caA==",
+ "CBAGa5l95f3hVzNi6MPWeQ==",
+ "OHJBT2SEv5b5NxBpiAf7oQ==",
+ "p48i7AfSSAyTdJSyHvOONw==",
+ "/SP6pOdYFzcAl2OL05z4uQ==",
+ "N8dXCawxSBX40fgRRSDqlQ==",
+ "bMWFvjM8eVezU1ZXKmdgqw==",
+ "Um1ftRBycvb+363a90Osog==",
+ "QAz7FA+jpz9GgLvwdoNTEQ==",
+ "qO4HlyHMK5ygX+6HbwQe8w==",
+ "UgvtdE2eBZBUCAJG/6c0og==",
+ "q5g3c8tnQTW2EjNfb2sukw==",
+ "gsC/mWD8KFblxB0JxNuqJw==",
+ "SVFbcjXbV7HRg+7jUrzpwg==",
+ "bz294kSG4egZnH2dJ8HwEg==",
+ "ybpTgPr3SjJ12Rj5lC/IMA==",
+ "yDrAd1ot38soBk7zKdnT8A==",
+ "BB/R8oQOcoE4j63Hrh8ifg==",
+ "GNrMvNXQkW7PydlyJa+f1w==",
+ "w0PKdssv+Zc5J/BbphoxpA==",
+ "D5ibbo8UJMfFZ48RffuhgQ==",
+ "MdvhC1cuXqni/0mtQlSOCw==",
+ "wQKL8Ga6JQkpZ7yymDkC3w==",
+ "o1uhaQg5/zfne84BFAINUQ==",
+ "Ft2wXUokFdUf6d2Y/lwriw==",
+ "sLJrshdEANp0qk2xOUtTnQ==",
+ "jx7rpxbm1NaUMcE2ktg5sA==",
+ "ZQ0ZnTsZKWxbRj7Tilh24Q==",
+ "KhrIIHfqXl9zGE9aGrkRVg==",
+ "jS0JuioLGAVaHdo/96JFoQ==",
+ "tr+U/vt+MIGXPRQYYWJfRg==",
+ "TXab/hqNGWaSK+fXAoB2bg==",
+ "0K4NBxqEa3RYpnrkrD/XjQ==",
+ "3oMTbWf7Bv83KRlfjNWQZA==",
+ "yLAhLNezvqVHmN1SfMRrPw==",
+ "ZYW30FfgwHmW6nAbUGmwzA==",
+ "CZNoTy26VUQirvYxSPc/5A==",
+ "CF1sAlhjDQY/KWOBnSSveA==",
+ "+CLf5witKkuOvPCulTlkqw==",
+ "1m1yD4L9A7Q1Ot+wCsrxJQ==",
+ "2E41e0MgM3WhFx2oasIQeA==",
+ "mDXHuOmI4ayjy2kLSHku1Q==",
+ "sCLMrLjEUQ6P1L8tz90Kxg==",
+ "zDUZCzQesFjO1JI3PwDjfg==",
+ "x/BIDm6TKMhqu/gtb3kGyw==",
+ "DEaZD/8aWV6+zkiLSVN/gA==",
+ "7dz+W494zwU5sg63v5flCg==",
+ "Y5iDQySR2c3MK7RPMCgSrw==",
+ "GglPoW5fvr4JSM3Zv99oiA==",
+ "myzvc+2MfxGD9uuvZYdnqQ==",
+ "V9G1we3DOIQGKXjjPqIppQ==",
+ "gYvdNJCDDQmNhtJ6NKSuTA==",
+ "rXtGpN17Onx8LnccJnXwJQ==",
+ "/a+bLXOq02sa/s8h7PhUTg==",
+ "htNVAogFakQkTX6GHoCVXg==",
+ "eshD40tvOA6bXb0Fs/cH3A==",
+ "K1CGbMfhlhIuS0YHLG30PQ==",
+ "aOeJZUIZM9YWjIEokFPnzQ==",
+ "r0hAwlS0mPZVfCSB+2G6uQ==",
+ "0q+erphtrB+6HBnnYg7O6w==",
+ "bkRdUHAksJZGzE1gugizYQ==",
+ "J8v2f6hWFu8oLuwhOeoQjA==",
+ "qkvEep4vvXhc2ZJ6R449Mg==",
+ "6HGeEPyTAu9oiKhNVLjQnA==",
+ "JoATsk/aJH0UcDchFMksWA==",
+ "QozQL0DTtr+PXNKifv6l6g==",
+ "HiAgt86AyznvbI2pnLalVQ==",
+ "lY+tivtsfvU0LJzBQ6itYQ==",
+ "EfXDc6h69aBPE6qsB+6+Ig==",
+ "gnAIpoCyl3mQytLFgBEgGA==",
+ "p2JPOX8yDQ0agG+tUyyT/g==",
+ "zeELfk015D5krExLKRUYtg==",
+ "wDiGoFEfIVEDyyc4VpwhWQ==",
+ "7Ephy+mklG2Y3MFdqmXqlA==",
+ "8ZFPMJJYVJHsfRpU4DigSg==",
+ "ocRh5LR1ZIN9Johnht8fhQ==",
+ "l5f3I6osM9oxLRAwnUnc5A==",
+ "yxCyBXqGWA735JEyljDP7Q==",
+ "qE/h/Z+6buZWf+cmPdhxog==",
+ "HCu4ZMrcLMZbPXbTlWuvvQ==",
+ "TDrq23VUdzEU/8L5i8jRJQ==",
+ "L+N/6geuokiLPPSDXM9Qkg==",
+ "v6jZicMNM3ysm3U5xu0HoQ==",
+ "b85nxzs8xiHxaqezuDVWvg==",
+ "ca+kx+kf7JuZ3pfYKDwFlg==",
+ "KlY5TGg0pR/57TVX+ik1KQ==",
+ "3jmCreW5ytSuGfmeLv7NfQ==",
+ "ucLMWnNDSqE4NOCGWvcGWw==",
+ "NSrzwNlB0bde3ph8k6ZQcQ==",
+ "nL4iEd3b5v4Y9fHWDs+Lrw==",
+ "W2x0SBzSIsTRgyWUCOZ/lg==",
+ "ifZM0gBm9g9L09YlL+vXBg==",
+ "4WcFEswYU/HHQPw77DYnyA==",
+ "TLJbasOoVO435E5NE5JDcA==",
+ "WyCFB4+6lVtlzu3ExHAGbQ==",
+ "BW0A06zoQw7S+YMGaegT7g==",
+ "qP1cCE4zsKGTPhjbcpczMw==",
+ "UVEZPoH9cysC+17MKHFraw==",
+ "eQ45Mvf5in9xKrP6/qjYbg==",
+ "fOARCnIg/foF/6tm7m9+3w==",
+ "lK2xe+OuPutp4os0ZAZx5w==",
+ "Tug3eh+28ttyf+U7jfpg5w==",
+ "ENFfP93LA257G6pXQkmIdg==",
+ "FuWspiqu5g8Eeli5Az+BkA==",
+ "kIGxCUxSlNgsKZ45Al1lWw==",
+ "RzeH+G3gvuK1z+nJGYqARQ==",
+ "0ofMbUCA3/v5L8lHnX4S5w==",
+ "VI8pgqBZeGWNaxkuqQVe7g==",
+ "x6lNRGgJcRxgKTlzhc1WPg==",
+ "La0gzdbDyXUq6YAXeKPuJA==",
+ "dAq8/1JSQf1f4QPLUitp0g==",
+ "WN7lFJfw4lSnTCcbmt5nsg==",
+ "2aDK0tGNgMLyxT+BQPDE8Q==",
+ "9W57pTzc572EvSURqwrRhw==",
+ "37Nkh06O979nt7xzspOFyQ==",
+ "4TQkMnRsXBobbtnBmfPKnA==",
+ "f/BjtP5fmFw2dRHgocbFlg==",
+ "9vEgJVJLEfed6wJ7hBUGgQ==",
+ "HRWYX2XOdsOqYzCcqkwIyw==",
+ "StDtLMlCI75g4XC59mESEQ==",
+ "99+SBN45LwKCPfrjUKRPmw==",
+ "HbT6W1Ssd3W7ApKzrmsbcg==",
+ "l8/KMItWaW3n4g1Yot/rcQ==",
+ "s7iW1M6gkAMp+D/3jHY58w==",
+ "GWwJ32SZqD5wldrXUdNTLA==",
+ "YhLEPsi/TNyeUJw69SPYzQ==",
+ "g0aTR8aJ0uVy3YvGYu5xrw==",
+ "m6get5wjq5j1i5abnpXuZQ==",
+ "ymtA8EMPMgmMcimWZZ0A1Q==",
+ "HEcOaEd9zCoOVbEmroSvJg==",
+ "F8l+Qd9TZgzV+r8G584lKA==",
+ "3yDD+xT8iRfUVdxcc7RxKw==",
+ "1eRUCdIJe3YGD5jOMbkkOg==",
+ "DO1/jfP/xBI9N0RJNqB2Rw==",
+ "SiSlasZ+6U2IZYogqr2UPg==",
+ "tBQDfy48FnIOZI04rxfdcA==",
+ "HEghmKg3GN60K7otpeNhaA==",
+ "mTLBkP+yGHsdk5g7zLjVUw==",
+ "RgtwfY5pTolKrUGT+6Pp6g==",
+ "EyIsYQxgFa4huyo/Lomv7g==",
+ "HwLSUie8bzH+pOJT3XQFyg==",
+ "7Tauesu7bgs5lJmQROVFiQ==",
+ "ojugpLIfzflgU2lonfdGxA==",
+ "ZqjnqxZE/BjOUY0CMdVl0g==",
+ "oQjugfjraFziga1BcwRLRA==",
+ "JXCYeWjFqcdSf6QwB54G+A==",
+ "TeBGJCqSqbzvljIh9viAqA==",
+ "1Gpj4TPXhdPEI4zfQFsOCg==",
+ "asouSfUjJa8yfMG7BBe+fA==",
+ "ccy3Ke2k4+evIw0agHlh3w==",
+ "CzSumIcYrZlxOUwUnLR2Zw==",
+ "9QFYrCXsGsInUb4SClS3cQ==",
+ "3RTtSaMp1TZegJo5gFtwwA==",
+ "aTWiWjyeSDVY/q8y9xc2zg==",
+ "UK+R+hAoVeZ4xvsoZjdWpw==",
+ "rHagXw+CkF3uEWPWDKXvog==",
+ "MfkyURTBfkNZwB+wZKjP4g==",
+ "Qf7JFJJuuacSzl6djUT2EQ==",
+ "K1RL+tLjICBvMupe7QppIQ==",
+ "R2OOV18CV/YpWL1xzr/VQg==",
+ "o+areESiXgSO0Lby56cBeg==",
+ "VPqyIomYm7HbK5biVDvlpw==",
+ "pw1jplCdTC+b0ThX0FXOjw==",
+ "gTnsH3IzALFscTZ1JkA9pw==",
+ "JYJvOZ4CHktLrYJyAbdOnA==",
+ "P8lUiLFoL100c9YSQWYqDA==",
+ "LATQEY7f47i77M6p11wjWA==",
+ "U9kE50Wq5/EHO03c5hE4Ug==",
+ "pFKzcRHSUBqSMtkEJvrR1Q==",
+ "vHVXsAMQqc0qp7HA5Q+YkA==",
+ "3XyoREdvhmSbyvAbgw2y/A==",
+ "qOEIUWtGm5vx/+fg4tuazg==",
+ "a6IszND1m+6w+W+CvseC7g==",
+ "KuNY8qAJBce+yUIluW8AYw==",
+ "5Wcq+6hgnWsQZ/bojERpUw==",
+ "l2ZB9TvT68rn8AAN4MdxWw==",
+ "h5HsEsObPuPFqREfynVblw==",
+ "fvm0IQfnbfZFETg9v3z/Fg==",
+ "QV0OG5bpjrjku4AzDvp9yw==",
+ "nMuMtK/Zkb3Xr34oFuX/Lg==",
+ "jMZKSMP2THqwpWqJNJRWdw==",
+ "fX4G68hFL7DmEmjbWlCBJQ==",
+ "ZlBNHAiYsfaEEiPQ1z+rCA==",
+ "ckugAisBNX18eQz+EnEjjw==",
+ "Dt6hvhPJu94CJpiyJ5uUkg==",
+ "eYE9No9sN5kUZ5ePEyS3+Q==",
+ "Tp52d1NndiC9w3crFqFm9g==",
+ "MBjMU/17AXBK0tqyARZP5w==",
+ "1EI9aa955ejNo1dJepcZJw==",
+ "FqWLkhWl0iiD/u2cp+XK9A==",
+ "j8nMH8mK/0Aae7ZkqyPgdg==",
+ "ZtmnX24AwYAXHb2ZDC6MeQ==",
+ "who8uUamlHWHXnBf7dwy4A==",
+ "CmkmWcMK4eqPBcRbdnQvhw==",
+ "61V74uIjaSfZM8au1dxr1A==",
+ "778O1hdVKHLG2q9dycUS0Q==",
+ "IdadoCPmSgHDHzn1zyf8Jw==",
+ "Z2rwGmVEMCY6nCfHO3qOzw==",
+ "Q3TpCE+wnmH/1h/EPWsBtQ==",
+ "HnVfyqgJ+1xSsN4deTXcIA==",
+ "XgPHx2+ULpm14IOZU2lrDg==",
+ "IbN736G1Px5bsYqE5gW1JQ==",
+ "nY/H7vThZ+dDxoPRyql+Cg==",
+ "wlWxtQDJ+siGhN2fJn3qtw==",
+ "MrbEUlTagbesBNg0OemHpw==",
+ "LJtRcR70ug6UHiuqbT6NGw==",
+ "hSNZWNKUtDtMo6otkXA/DA==",
+ "LawT9ZygiVtBk0XJ+KkQgQ==",
+ "DLzHkTjjuH6LpWHo2ITD0Q==",
+ "i8XXN7jcrmhnrOVDV8a2Hw==",
+ "ogcuGHUZJkmv+vCz567a2g==",
+ "rUp5Mfc57+A8Q29SPcvH/Q==",
+ "6706ncrH1OANFnaK6DUMqQ==",
+ "gK7dhke5ChQzlYc/bcIkcg==",
+ "t3Txxjq43e/CtQmfQTKwWg==",
+ "6ZMs9vCzK9lsbS6eyzZlIA==",
+ "uTHBqApdKOAgdwX3cjrCYQ==",
+ "zirOtGUXeRL22ezfotZfQg==",
+ "iK0dWKHjVVexuXvMWJV9pg==",
+ "uzEgwx1iAXAvWPKSVwYSeQ==",
+ "FHvI0IVNvih8tC7JgzvCOw==",
+ "jjNMPXbmpFNsCpWY0cv3eg==",
+ "/cJ0Nn5YbXeUpOHMfWXNHQ==",
+ "WkSJpxBa45XJRWWZFee7hw==",
+ "edlXkskLx287vOBZ9+gVYg==",
+ "+Pl0bSMBAdXpRIA+zE02JA==",
+ "3xw8+0/WU51Yz4TWIMK8mw==",
+ "GdTanUprpE3X/YjJDPpkhQ==",
+ "qnsBdl050y9cUaWxbCczRw==",
+ "pnJnBzAJlO4j3IRqcfmhkQ==",
+ "USq1iF90eUv41QBebs3bhw==",
+ "QH3lAwOYBAJ0Fd5pULAZqw==",
+ "gvvyX5ATi4q9NhnwxRxC8w==",
+ "7xDIG/80SnhgxAYPL9YJtg==",
+ "WVhfn2yJZ43qCTu0TVWJwA==",
+ "twjiDKJM7528oIu/el4Zbg==",
+ "6sBemZt4qY/TBwqk3YcLOQ==",
+ "m3XYojKO+I6PXlVRUQBC3w==",
+ "gUNP5w7ANJm257qjFxSJrA==",
+ "mMLhjdWNnZ8zts9q+a2v3g==",
+ "kjWYVC7Eok2w2YT4rrI+IA==",
+ "ZzT5b0dYQXkQHTXySpWEaA==",
+ "YzTV0esAxBFVls3e0qRsnA==",
+ "9xmtuClkFlpz/X5E9JBWBA==",
+ "nhAnHuCGXcYlqzOxrrEe1g==",
+ "cbBXgB1WQ/i8Xul0bYY2fg==",
+ "AkAes5oErTaJiGD2I4A1Pw==",
+ "Wx9jh/teM0LJHrvTScssyQ==",
+ "fU5ZZ1bIVsV+eXxOpGWo/Q==",
+ "k8eZxqwxiN/ievXdLSEL/w==",
+ "E2LR1aZ3DcdCBuVT7BhReA==",
+ "1eCHcz4swFH+uRhiilOinQ==",
+ "JipruVZx4ban3Zo5nNM37g==",
+ "IPLD9nT5EEYG9ioaSIYuuA==",
+ "pHozgRyMiEmyzThtJnY4MQ==",
+ "p0eNK7zJd7D/HEGaVOrtrQ==",
+ "dGjcKAOGBd4gIjJq7fL+qQ==",
+ "uMq8cDVWFD+tpn8aeP8Pqg==",
+ "gC7gUwGumN7GNlWwfIOjJQ==",
+ "It+K/RCYMOfNrDZxo7lbcA==",
+ "4CfEP8TeMKX33ktwgifGgA==",
+ "nxDGRpePV3H4NChn4eLwag==",
+ "300hoYyMR/mk1mfWJxS8/w==",
+ "DmxgZsQg+Qy1GP0fPkW3VA==",
+ "1vqRt79ukuvdJNyIlIag8Q==",
+ "RWI0HfpP7643OSEZR8kxzw==",
+ "zZtYkKU50PPEj6qSbO5/Sw==",
+ "UNRlg6+CYVOt68NwgufGNA==",
+ "kkbX+a00dfiTgbMI+aJpMg==",
+ "VIC7inSiqzM6v9VqtXDyCw==",
+ "l+x2QhxG8wb5AQbcRxXlmA==",
+ "GUiinC3vgBjbQC2ybMrMNQ==",
+ "6uMF5i0b/xsk55DlPumT7A==",
+ "aK9nybtiIBUvxgs1iQFgsw==",
+ "BLbTFLSb4mkxMaq4/B2khg==",
+ "mTAqtg6oi0iytHQCaSVUsA==",
+ "eBapvE+hdyFTsZ0y5yrahg==",
+ "lHN2dn2cUKJ8ocVL3vEhUQ==",
+ "Mj87ajJ/yR41XwAbFzJbcA==",
+ "FA+nK6mpFWdD0kLFcEdhxA==",
+ "FrTgaF5YZCNkyfR1kVzTLQ==",
+ "5eHStFN7wEmIE+uuRwIlPQ==",
+ "AyWlT+EGzIXc395zTlEU5Q==",
+ "I+wVQA+jpPTJ6xEsAlYucg==",
+ "Y1flEyZZAYxauMo4cmtJ1w==",
+ "1AeReq55UQotRQVKJ66pmg==",
+ "xzGzN5Hhbh0m/KezjNvXbQ==",
+ "meHzY9dIF7llDpFQo1gyMg==",
+ "RnOXOygwJFqrD+DlM3R5Ew==",
+ "JKg64m6mU7C/CkTwVn4ASg==",
+ "gGLz3Ss+amU7y6JF09jq7A==",
+ "Pu9pEf+Tek3J+3jmQNqrKw==",
+ "EATnlYm0p3h04cLAL95JgA==",
+ "o64LDtKq/Fulf1PkVfFcyg==",
+ "hUWqqG1QwYgGC5uXJpCvJw==",
+ "RfSwpO/ywQx4lfgeYlBr2w==",
+ "VaJc9vtYlqJbRPGb5Tf0ow==",
+ "9JKIJrlQjhNSC46H3Cstcw==",
+ "6Z9myGCF5ylWljgIYAmhqw==",
+ "9bAWYElyRN1oJ6eJwPtCtQ==",
+ "ohK6EftXOqBzIMI+5XnESw==",
+ "AVjwqrTBQH1VREuBlOyUOg==",
+ "G2UponGde3/Z+9b2m9abpQ==",
+ "DoiItHSms0B9gYmunVbRkQ==",
+ "vUC0HlTTHj6qNHwfviDtAw==",
+ "hq35Fjgvrcx6I9e6egWS4w==",
+ "sw+bmpzqsM4gEQtnqocQLQ==",
+ "ApiuEPWr8UjuRyJjsYZQBw==",
+ "VXu4ARjq7DS2IR/gT24Pfw==",
+ "3TbRZtFtsh9ez8hqZuTDeA==",
+ "CazLJMJjQMeHhYLwXW7YNg==",
+ "ROSt+NlEoiPFtpRqKtDUrQ==",
+ "IUwVHH6+8/0c+nOrjclOWA==",
+ "lkzFdvtBx5bV6xZO0cxK7g==",
+ "4ekt4m38G9m599xJCmhlug==",
+ "fzkmVWKhJsxyCwiqB/ULnQ==",
+ "LZAKplVoNjeQgfaHqkyEJA==",
+ "91vfsZ7Lx9x5gqWTOdM4sg==",
+ "MVoxyIA+emaulH8Oks8Weg==",
+ "oGH7SMLI2/qjd9Vnhi3s0A==",
+ "vmqfGJE6r4yDahtU/HLrxw==",
+ "Y5KKN7t/v9JSxG/m1GMPSA==",
+ "gXlb7bbRqHXusTE5deolGA==",
+ "/2c4oNniwhL3z5IOngfggg==",
+ "HgIFX42oUdRPu7sKAXhNWg==",
+ "A3dX2ShyL9+WOi6MNJBoYQ==",
+ "hN9bmMHfmnVBVr+7Ibd2Ng==",
+ "DB706G73NpBSRS8TKQOVZw==",
+ "JSyq2MIuObPnEgEUDyALjQ==",
+ "kSUectNPXpXNg+tIveTFRw==",
+ "XVVy3e6dTnO3HpgD6BtwQw==",
+ "td7nDgTDmKPSODRusMcupw==",
+ "Lt/pVD4TFRoiikmgAxEWEw==",
+ "mmRob7iyTkTLDu8ObmTPow==",
+ "Fd0c8f2eykUp9GYhqOcKoA==",
+ "18RKixTv12q3xoBLz6eKiA==",
+ "RClzwwKh51rbB4ekl99EZA==",
+ "oONlXCW4aAqGczQ/bUllBw==",
+ "foPAmiABJ3IXBoed2EgQXA==",
+ "wEJDulZafLuXCvcqBYioFQ==",
+ "K1RgR6HR5uDEQgZ32TAFgA==",
+ "SEIZhyguLoyH7So0p1KY0A==",
+ "ggIfX1J4dX3xQoHnHUI7VA==",
+ "HBRzLacCVYfwUVGzrefZYg==",
+ "aWZRql2IUPVe9hS3dxgVfQ==",
+ "Err1mbWJud80JNsDEmXcYg==",
+ "Z2MkqmpQXdlctCTCUDPyzw==",
+ "JnE6BK0vpWIhNkaeaYNUzw==",
+ "5dUry23poD+0wxZ3hH6WmA==",
+ "DwP0MQf71VsqvAbAMtC3QQ==",
+ "kHcBZXoxnFJ+GMwBZ/xhfQ==",
+ "SUAwMWLMml8uGqagz5oqhQ==",
+ "79uTykH43voFC3XhHHUzKg==",
+ "P5fucOJhtcRIoElFJS4ffg==",
+ "s8NpalwgPdHPla7Zi9FJ3w==",
+ "8cXqZub6rjgJXmh1CYJBOg==",
+ "tY916jrSySzrL+YTcVmYKQ==",
+ "DRiFNojs7wM8sfkWcmLnhQ==",
+ "wqUJ1Gq1Yz2cXFkbcCmzHQ==",
+ "0u+0WHr7WI6IlVBBgiRi6w==",
+ "GCYI9Dn1h3gOuueKc7pdKA==",
+ "nVDxVhaa2o38gd1XJgE3aw==",
+ "5I/heFSQG/UpWGx0uhAqGQ==",
+ "1PvTn90xwZJPoVfyT5/uIQ==",
+ "jHOoSl3ldFYr9YErEBnD3w==",
+ "swJhrPwllq5JORWiP5EkDA==",
+ "tj2rWvF2Fl+XIccctj8Mhw==",
+ "QvYZxsLdu+3nV/WhY1DsYg==",
+ "fKalNdhsyxTt1w08bv9fJA==",
+ "CHLHizLruvCrVi9chj9sXA==",
+ "sa2DECaqYH1z1/AFhpHi+g==",
+ "LbPp1oL0t3K2BAlIN+l8DA==",
+ "5SbwLDNT6sBOy6nONtUcTg==",
+ "AfVPdxD3FyfwwNrQnVNQ7A==",
+ "jt9Ocr9D8EwGRgrXVz//aQ==",
+ "KkwQL0DeUM3nPFfHb2ej+A==",
+ "WwraoO97OTalvavjUsqhxQ==",
+ "fAKFfwlCOyhtdBK6yNnsNg==",
+ "EqMlrz1to7HG4GIFTPaehQ==",
+ "YmjZJyNfHN5FaTL/HAm8ww==",
+ "L2D7G0btrwxl9V4dP3XM5Q==",
+ "oUqO4HrBvkpSL781qAC9+w==",
+ "c6Yhwy/q3j7skXq52l36Ww==",
+ "FWphIPZMumqnXr1glnbK4w==",
+ "AcKwfS8FRVqb72uSkDNY/Q==",
+ "uSIiF1r9F18avZczmlEuMQ==",
+ "XrFDomoH2qFjQ2jJ2yp9lA==",
+ "N2X7KWekNN+fMmwyXgKD5w==",
+ "IdmcpJXyVDajzeiGZixhSA==",
+ "Wf2olJCYZRGTTZxZoBePuQ==",
+ "oVlG+0rjrg2tdFImxIeVBA==",
+ "7w4PDRJxptG8HMe/ijL6cQ==",
+ "rueNryrchijjmWaA3kljYg==",
+ "ZybIEGf1Rn/26vlHmuMxhw==",
+ "yYVW07lOZHdgtX42xJONIA==",
+ "4ifNsmjYf1iOn2YpMfzihg==",
+ "KTjwL+qswa+Bid8xLdjMTg==",
+ "THfzE2G2NVKKfO+A2TjeFw==",
+ "QoqHzpHDHTwQD5UF30NruQ==",
+ "dTMoNd6DDr1Tu8tuZWLudw==",
+ "wOc4TbwQGUwOC1B3BEZ4OQ==",
+ "gfhkPuMvjoC3CGcnOvki3Q==",
+ "vljJciS+uuIvL7XXm5688g==",
+ "EGLOaMe6Nvzs/cmb7pNpbg==",
+ "oLWWIn/2AbKRHnddr2og9g==",
+ "7l0RMKbONGS/goW/M+gnMQ==",
+ "eFkXKRd2dwu/KWI5ZFpEzw==",
+ "jWsC7kdp2YmIZpfXGUimiA==",
+ "Jcxjli2tcIAjCe+5LyvqdQ==",
+ "MUkRa/PjeWMhbCTq43g6Aw==",
+ "g2nh2xENCFOpHZfdEXnoQA==",
+ "x6M66krXSi0EhppwmDmsxA==",
+ "26Wmdp6SkKN74W0/XPcnmA==",
+ "ycjv4XkS5O7zcF3sqq9MwQ==",
+ "gfnbviaVhKvv1UvlRGznww==",
+ "aIPde9CtyZrhbHLK740bfw==",
+ "0p8YbEMxeb73HbAfvPLQRw==",
+ "Is3uxoSNqoIo5I15z6Z2UQ==",
+ "NZtcY8fIpSKPso/KA6ZfzA==",
+ "iQ304I1hmLZktA1d1cuOJA==",
+ "0QB0OUW5x2JLHfrtmpZQ+w==",
+ "kgyUtd8MFe0tuuxDEUZA9w==",
+ "AcbG0e6xN8pZfYAv7QJe1Q==",
+ "bb/U8UynPHwczew/hxLQxw==",
+ "NuBYjwlxadAH+vLWYRZ3bg==",
+ "Ao1Zc0h5AdSHtYt1caWZnQ==",
+ "FL/j3GJBuXdAo54JYiWklQ==",
+ "E2v8Kk60qVpQ232YzjS2ow==",
+ "zVupSPz7cD0v/mD/eUIIjg==",
+ "sEeblUmISi1HK4omrWuPTA==",
+ "xQpYjaAmrQudWgsdu24J0A==",
+ "vCekQ2nOQKiN/q8Be/qwZg==",
+ "8g08gjG/QtvAYer32xgNAg==",
+ "miiOqnhtef1ODjFzMHnxjA==",
+ "sXlFMSTBFnq0STHj6cS/8w==",
+ "+SclwwY8R2RPrnX54Z+A6w==",
+ "g8TcogVxHpw7uhgNFt5VCQ==",
+ "9viAzLFGYYudBYFu7kFamg==",
+ "BAJ+/jbk2HyobezZyB9LiQ==",
+ "/DJgKE9ouibewuZ2QEnk6w==",
+ "fxg/vQq9WPpmQsqQ4RFYaA==",
+ "lM/EhwTsbivA7MDecaVTPw==",
+ "pVgjGg4TeTNhKimyOu3AAw==",
+ "gYnznEt9r97haD/j2Cko7g==",
+ "/ngbFuKIAVpdSwsA3VxvNw==",
+ "VCL3xfPVCL5RjihQM59fgg==",
+ "eDWsx4isnr2xPveBOGc7Hw==",
+ "FIOCTEbzb2+KMCnEdJ7jZw==",
+ "40HzgVKYnqIb6NJhpSIF0A==",
+ "ccK42Lm8Tsv73YMVZRwL6A==",
+ "MpAwWMt7bcs4eL7hCSLudQ==",
+ "zxsSqovedB3HT99jVblCnQ==",
+ "4erEA42TqGA9K4iFKkxMMA==",
+ "BaRwTrc5ulyKbW4+QqD0dw==",
+ "CT3ldhWpS1SEEmPtjejR/Q==",
+ "lkl6XkrTMUpXi46dPxTPxg==",
+ "3EhLkC9NqD3A6ApV6idmgg==",
+ "fsW2DaKYTCC7gswCT+ByQQ==",
+ "pW4gDKtVLj48gNz6V17QdA==",
+ "KjfL7YyVqmCJGBGDFdJ0gw==",
+ "bGGUhiG9SqJMHQWitXTcYQ==",
+ "8RtLlzkGEiisy1v9Xo0sbw==",
+ "R81DX/5a7DYKkS4CU+TL+w==",
+ "Tu6w6DtX2RJJ3Ym3o3QAWw==",
+ "nx/U4Tode5ILux4DSR+QMg==",
+ "mjQS8CpyGnsZIDOIEdYUxg==",
+ "wJpepvmtQQ3sz3tVFDnFqw==",
+ "a4rPqbDWiMivVzaRxvAj7g==",
+ "6o5g9JfKLKQ2vBPqKs6kjg==",
+ "UzPPFSXgeV7KW4CN5GIQXA==",
+ "NdVyHoTbBhX6Umz/9vbi0g==",
+ "Fzuq+Wg7clo6DTujNrxsSA==",
+ "XXFr0WUuGsH5nXPas7hR3Q==",
+ "JVSLiwurnCelNBiG2nflpQ==",
+ "NiawWuMBDo0Q3P2xK/vnLQ==",
+ "nNaGqigseHw30DaAhjBU3g==",
+ "+edqJYGvcy1AH2mEjJtSIg==",
+ "1WIi4I62GqkjDXOYqHWJfQ==",
+ "rwplpbNJz0ADUHTmzAj15Q==",
+ "iWNlSnwrtCmVF89B+DZqOQ==",
+ "tHDbi43e6k6uBgO0hA+Uiw==",
+ "fHNpW230mNib08aB7IM3XQ==",
+ "OChiB4BzcRE8Qxilu6TgJg==",
+ "d+ctfXU0j07rpRRzb5/HDA==",
+ "GDMqfhPQN0PxfJPnK1Bb9A==",
+ "bLd38ZNkVeuhf0joEAxnBQ==",
+ "nvUKoKfC6j8fz3gEDQrc/w==",
+ "fhcbn9xE/6zobqQ2niSBgA==",
+ "HGxe+5/kkh6R9GXzEOOFHA==",
+ "mPwCyD0yrIDonVi+fhXyEQ==",
+ "5PfGtbH9fmVuNnq83xIIgQ==",
+ "XePy/hhnQwHXFeXUQQ55Vg==",
+ "yfAaL0MMtSXPQ37pBdmHxQ==",
+ "NiQ/m4DZXUbpca9aZdzWAw==",
+ "uT6WRh5UpVdeABssoP2VTg==",
+ "oxoZP897lgMg/KLcZAtkAg==",
+ "oKt57TPe4PogmsGssc3Cbg==",
+ "RxmdoO8ak8y/HzMSIm+yBQ==",
+ "6leyDVmC5jglAa98NQ3+Hg==",
+ "+QosBAnSM2h4lsKuBlqEZw==",
+ "hy303iin+Wm7JA6MeelwiQ==",
+ "m9iuy4UtsjmyPzy6FTTZvw==",
+ "f6Ye5F0Lkn34uLVDCzogFQ==",
+ "iGykaF+h4p46HhrWqL8Ffg==",
+ "LPYFDbTEp5nGtG6uO8epSw==",
+ "t2vWMIh2BvfDSQaz5T1TZw==",
+ "OONAvFS/kmH7+vPhAGTNSg==",
+ "g/z9yk94XaeBRFj4hqPzdw==",
+ "2wesXiib76wM9sqRZ7JYwQ==",
+ "n7h9v2N1gOcvMuBEf8uThw==",
+ "ITYL3tDwddEdWSD6J6ULaA==",
+ "inrUwXyKikpOW0y2Kl1wGw==",
+ "iwKBOGDTFzV4aXgDGfyUkw==",
+ "+fcjH2kZKNj8quOytUk4nQ==",
+ "Srl4HivgHMxMOUHyM3jvNw==",
+ "qngzBJbiTB4fivrdnE5gOg==",
+ "G0MlFNCbRjXk4ekcPO/chQ==",
+ "t+bYn9UqrzKiuxAYGF7RLA==",
+ "RVD3Ij6sRwwxTUDAxwELtA==",
+ "RNdyt6ZRGvwYG5Ws3QTuEA==",
+ "9DRHdyX8ECKHUoEsGuqR4Q==",
+ "oMJLQTH1wW7LvOV0KRx/dw==",
+ "bjLZ7ot/X/vWSVx4EYwMCg==",
+ "+p8pofUlwn8vV6Rp6+sz9g==",
+ "cchuqe+CWCJpoakjHLvUfA==",
+ "NvurnIHin4O+wNP7MnrZ1w==",
+ "RBMv0IxXEO3o7MnV47Bzow==",
+ "xTizUioizbMQxD0T6fy/EQ==",
+ "ZCdad3AwhVArttapWFwT/Q==",
+ "Hy1nqC40l5ItxumkIC2LAA==",
+ "W/5ThNLu43uT1O+fg0Fzwg==",
+ "b3BQG9/9qDNC/bNSTBY/sQ==",
+ "neQoa8pvETr07blVMN3pgA==",
+ "oR8rvIZoeoaZ/ufpo0htfQ==",
+ "zEzWZ6l7EKoVUxvk/l78Mw==",
+ "IHyIeMad23fSDisblwyfpA==",
+ "m6srF+pMehggHB1tdoxlPg==",
+ "kggaIvN2tlbZdZRI8S5Apw==",
+ "2RFaMPlSbVuoEqKXgkIa5A==",
+ "//eHwmDOQRSrv+k9C/k3ZQ==",
+ "X/Gha4Ajjm/GStp/tv+Jvw==",
+ "+H0Rglt/HnhZwdty2hsDHg==",
+ "a1aL8zQ+ie3YPogE3hyFFg==",
+ "HxEU37uBMeiR5y8q/pM42g==",
+ "68nqDtXOuxF7DSw6muEZvg==",
+ "s5+78jS4hQYrFtxqTW3g1Q==",
+ "drfODfDI6GyMW7hzkmzQvA==",
+ "pT1raq2fChffFSIBX3fRiA==",
+ "sfowXUMdN2mCoBVrUzulZg==",
+ "AV/YJfdoDUdRcrXVwinhQg==",
+ "3AKEYQqpkfW7CZMFQZoxOw==",
+ "PHwJ5ZAqqftZ4ypr8H1qiQ==",
+ "AoN/pnK4KEUaGw4V9SFjpg==",
+ "soBA65OmZdfBGJkBmY/4Iw==",
+ "mSstwJq7IkJ0JBJ5T8xDKg==",
+ "h13Xuonj+0dD1xH86IhSyQ==",
+ "HK9xG03FjgCy8vSR+hx8+Q==",
+ "oFanDWdePmmZN0xqwpUukA==",
+ "zCRZgVsHbQZcVMHd9pGD3A==",
+ "EvSB+rCggob2RBeXyDQRvQ==",
+ "tXuu7YpZOuMLTv87NjKerA==",
+ "DJ+a37tCaGF5OgUhG+T0NA==",
+ "KkXlgPJPen6HLxbNn5llBw==",
+ "2W6lz1Z7PhkvObEAg2XKJw==",
+ "n+xYzfKmMoB3lWkdZ+D3rg==",
+ "CPDs+We/1wvsGdaiqxzeCQ==",
+ "2Wvk/kouEEOY0evUkQLhOQ==",
+ "ezsm4aFd6+DO9FUxz0A8Pg==",
+ "9sYLg75/hudZaBA3FrzKHw==",
+ "Pp1ZMxJ8yajdbfKM4HAQxA==",
+ "xiyRfVG0EfBA+rCk+tgWRQ==",
+ "/IarsLzJB8bf0AupJJ+/Eg==",
+ "LJeLdqmriyAQp+QjZGFkdQ==",
+ "IhHyHbHGyQS+VawxteLP0w==",
+ "nGzPc0kI/EduVjiK7bzM6Q==",
+ "m06wctjNc3o7iyBHDMZs2w==",
+ "mSJF9dJnxZ15lTC6ilbJ2A==",
+ "xdmY+qyoxxuRZa9kuNpDEg==",
+ "oNOI17POQCAkDwj6lJsYOA==",
+ "p73gSu4d+4T/ZNNkIv9Nlw==",
+ "vOJ55zFdgPPauPyFYBf01w==",
+ "4A+RHIw+aDzw0rSRYfbc7g==",
+ "/gi3UZmunVOIXhZSktZ8zQ==",
+ "a6vem8n6WmRZAalDrHNP0g==",
+ "kGeXrHEN6o7h5qJYcThCPw==",
+ "wrewZ0hoHODf7qmoGcOd7g==",
+ "Z0sjccxzKylgEiPCFBqPSA==",
+ "LKyOFgUKKGUU/PxpFYMILw==",
+ "L2RofFWDO0fVgSz4D2mtdw==",
+ "KI7tQFYW38zYHOzkKp9/lQ==",
+ "ewe/P3pJLYu/kMb5tpvVog==",
+ "IADk81pIu8NIL/+9Fi94pA==",
+ "0L0FVcH5Dlj3oL8+e9Na7g==",
+ "tdiTXKrkqxstDasT0D5BPA==",
+ "R906Kxp2VFVR3VD+o6Vxcw==",
+ "wc+8ohFWgOF4VlSYiZIGwQ==",
+ "wJKFMqh6MGctWfasjHrPEg==",
+ "UHpge5Bldt9oPGo2oxnYvQ==",
+ "vX7RIhatQeXAMr1+OjzhZw==",
+ "s2AKVTwrY65/SWqQxDGJQg==",
+ "Q4bfQslDSqU64MOQbBQEUw==",
+ "mVT74Eht+gAowINoMKV7IQ==",
+ "EuGWtIbyKToOe6DN3NkVpQ==",
+ "ALlGgVDO8So71ccX0D6u2g==",
+ "Rww3qkF3kWSd+AaMT0kfdw==",
+ "hlvtFGW8r0PkbUAYXEM+Hw==",
+ "Oc3BqTF3ZBW3xE0QsnFn/A==",
+ "3j0kFUZ6g+yeeEljx+WXGg==",
+ "8BLkvEkfnOizJq0OTCYGzw==",
+ "Lqel4GdU0ZkfoJVXI5WC/Q==",
+ "rvE64KQGkVkbl07y7JwBqw==",
+ "HbXv8InyZqFT7i3VrllBgg==",
+ "zwQ/3MzTJ9rfBmrANIh14w==",
+ "gglLMohmJDPRGMY1XKndjQ==",
+ "lyfqic/AbEJbCiw+wA01FA==",
+ "XqUO7ULEYhDOuT/I2J8BOA==",
+ "wPhJcp7U7IVX83szbIOOxQ==",
+ "1gA65t5FiBTEgMELTQFUPQ==",
+ "ll2M0QQzBsj5OFi02fv3Yg==",
+ "wt+qDLU38kzNU75ZYi3Hbw==",
+ "a4EYNljinYTx9vb1VvUA6A==",
+ "T6LA+daQqRI38iDKZTdg1A==",
+ "gwyVIrTk5o0YMKQq4lpJ+Q==",
+ "bPRX2zl+K1S0iWAWUn1DZw==",
+ "KQw25X4LnQ9is+qdqfxo0w==",
+ "6tfM6dx3R5TiVKaqYQjnCg==",
+ "OlwHO6Sg2zIwsCOCRu0HiQ==",
+ "mr1qjhliRfl87wPOrJbFQg==",
+ "8c+lvG5sZNimvx9NKNH3ug==",
+ "5Nk2Z94DhlIdfG5HNgvBbQ==",
+ "F50iXjRo1aSTr37GQQXuJA==",
+ "tfgO55QqUyayjDfQh+Zo1Q==",
+ "h7Fc+eT/GuC8iWI+YTD0UQ==",
+ "3TjntNWtpG7VqBt3729L6Q==",
+ "+DWs0vvFGt6d3mzdcsdsyA==",
+ "VJt2kPVBLEBpGpgvuv1oUw==",
+ "XLq/nWX8lQqjxsK9jlCqUg==",
+ "9s3ar9q32Y5A3tla5GW/2Q==",
+ "51yLpfEdvqXmtB6+q27/AQ==",
+ "AiMtfedwGcddA+XYNc+21g==",
+ "p/48hurJ1kh2FFPpyChzJg==",
+ "CRiL6zpjfznhGXhCIbz8pQ==",
+ "/jDVt9dRIn+o4IQ1DPwbsg==",
+ "UNdKik7Vy23LjjPzEdzNsg==",
+ "Koiog/hpN7ew5kgJbty34A==",
+ "4itEKfbRCJvqlgKnyEdIOQ==",
+ "zi04Yc01ZheuFAQc59E45A==",
+ "etRjRvfL/IwceY/IJ1tgzQ==",
+ "3sNJJIx1NnjYcgJhjOLJOg==",
+ "4yVqq66iHYQjiTSxGgX2oA==",
+ "Q8RVI/kRbKuXa8HAQD7zUA==",
+ "OERGn45uzfDfglzFFn6JAg==",
+ "JGEy6VP3sz3LHiyT2UwNHQ==",
+ "1zDfWw5LdG20ClNP1HYxgw==",
+ "TGB+FIzzKnouLh5bAiVOQg==",
+ "n5GA+pA9mO/f4RN9NL9lNg==",
+ "bUxQBaqKyvlSHcuRL9whjg==",
+ "tOdlnsE3L3XCBDJRmb/OqA==",
+ "XdkxmYYooeDKzy7PXVigBQ==",
+ "PMvG4NqJP76kMRAup6TSZA==",
+ "qpFJZqzkklby+u1UT3c1iA==",
+ "fW3QZyq5UixIA1mP6eWgqQ==",
+ "9nMltdrrBmM5ESBY2FRjGA==",
+ "1Vtrv6QUAfiYQjlLTpNovg==",
+ "ur9JDCVNwzSH4q4ngDlHNQ==",
+ "4u3eyKc+y3uRnkASrgBVUw==",
+ "XddlSluOH6VkR7spFIFmdQ==",
+ "NOmu8oZc6CcKLu+Wfz2YOQ==",
+ "3Ejtsqw3Iep/UQd0tXnSlg==",
+ "y/e3HSdg7T19FanRpJ7+7Q==",
+ "YodhkayN5wsgPZEYN7/KNA==",
+ "pZfn6IiG+V28fN8E2hawDQ==",
+ "jGHMJqbj6X1NdTDyWmXYAQ==",
+ "olTSlmirL9MFhKORiOKYkQ==",
+ "CrJDgdfzOea2M2hVedTrIg==",
+ "fpXijBOM3Ai1RkmHven5Ww==",
+ "eLYKLr4labZeLiRrDJ9mnA==",
+ "9vmJUS7WIVOlhMqwipAknQ==",
+ "G7J/za99BFbAZH+Q+/B8WA==",
+ "Hb+pdSavvJ9lUXkSVZW8Og==",
+ "gTB2zM3RPm27mUQRXc/YRg==",
+ "e5KCqQ/1GAyVMRNgQpYf6g==",
+ "1ApqwW7pE+XUB2Cs2M6y7g==",
+ "/wiA2ltAuWyBhIvQAYBTQw==",
+ "HFCQEiZf7/SNc+oNSkkwlA==",
+ "JFi6N1PlrpKaYECOnI7GFg==",
+ "E4ojRDwGsIiyuxBuXHsKBA==",
+ "+25t/2lo0FUEtWYK8LdQZQ==",
+ "up2MVDi9ve+s83/nwNtZ7Q==",
+ "cXpfd6Io6Glj2/QzrDMCvA==",
+ "DCvI9byhw0wOFwF1uP6xIQ==",
+ "PibGJQNw7VHPTgqeCzGUGA==",
+ "0ZRGz+oj2infCAkuKKuHiQ==",
+ "2QS/6OBA1T01NlIbfkTYJg==",
+ "P14k+fyz0TG9yIPdojp52w==",
+ "g5EzTJ0KA4sO3+Opss3LMg==",
+ "R5oOM58zdbVxFSDQnNWqeA==",
+ "Vg2E5qEDfC+QxZTZDCu9yQ==",
+ "YPgMthbpcBN2CMkugV60hQ==",
+ "gZWTFt5CuLqMz6OhWL+hqQ==",
+ "YrEP9z2WPQ8l7TY1qWncDA==",
+ "7p4NpnoNSQR7ISg+w+4yFg==",
+ "9L6yLO93sRN70+3qq3ObfA==",
+ "QH36wzyIhh6I56Vnx79hRA==",
+ "9DtM1vls4rFTdrSnQ7uWXw==",
+ "ZlOAnCLV1PkR0kb3E+Nfuw==",
+ "9UhKmKtr4vMzXTEn74BEhg==",
+ "Ndx5LDiVyyTz/Fh3oBTgvA==",
+ "mXZ4JeBwT2WJQL4a/Tm4jQ==",
+ "N9nD7BGEM7LDwWIMDB+rEQ==",
+ "dmAfbd9F0OJHRAhNMEkRsA==",
+ "jV/D2B11NLXZRH77sG9lBw==",
+ "1C50kisi9nvyVJNfq2hOEQ==",
+ "NMbAjbnuK7EkVeY3CQI5VA==",
+ "J1nYqJ7tIQK1+a/3sMXI/Q==",
+ "m416yrrAlv+YPClGvGh+qQ==",
+ "rLZII1R6EGus+tYCiUtm6g==",
+ "xktOghh1S9nIX6fXWnT+Ug==",
+ "FcFcn4qmPse5mJCX5yNlsA==",
+ "xAAipGfHTGTjp9Qk1MR8RQ==",
+ "RQOlmzHwQKFpafKPJj0D8w==",
+ "WRjYdKdtnd1G9e/vFXCt0g==",
+ "z0BU//aSjYHAkGGk3ZSGNg==",
+ "M55eersiJuN9v61r8DoAjQ==",
+ "l2mAbuFF3QBIUILDODiUHQ==",
+ "IhpXs1TK7itQ3uTzZPRP5Q==",
+ "t2EkpUsLOEOsrnep0nZSmA==",
+ "lMaO8Yf+6YNowGyhDkPhQA==",
+ "UbSFw5jtyLk5MealqJw++A==",
+ "5u2PdDcIY3RQgtchSGDCGg==",
+ "MQYM3BT77i35LG9HcqxY2Q==",
+ "8AfCSZC0uasVON9Y/0P2Pw==",
+ "evaWFoxZNQcRszIRnxqB+A==",
+ "+8PiQt6O7pJI/nIvQpDaAg==",
+ "eRwaYiog2DdlGQyaltCMJg==",
+ "JyUJEnU6hJu8x2NCnGrYFw==",
+ "l0E0U/CJsyCVSTsXW4Fp+w==",
+ "XV13yK0QypJXmgI+dj4KYw==",
+ "jrRH0aTUYCOpPLZwzwPRfQ==",
+ "N3YDSkBUqSmrmNvZZx4a1Q==",
+ "0yJ7TQYzcp3DXVSvwavr+w==",
+ "rhgtLQh0F9bRA6IllM7AGw==",
+ "IWZnTJ3Hb9qw9HAK/M9gTw==",
+ "izeyFvXOumNgVyLrbKW45g==",
+ "xYD8jrCDmuQna+p1ebnKDQ==",
+ "SOdpdrk2ayeyv0xWdNuy9g==",
+ "HYylUirJRqLm+dkp39fSOQ==",
+ "q4z6A4l3nhX3smTmXr+Sig==",
+ "Zyo0fzewcqXiKe2mAwKx5g==",
+ "LMEtzh0+J27+4zORfcjITw==",
+ "LoUv/f2lcWpjftzpdivMww==",
+ "mXBfDUt/sBW5OUZs2sihvw==",
+ "PggVPQL5YKqSU/1asihcrg==",
+ "mI0eT4Rlr7QerMIngcu/ng==",
+ "NmQrsmb8PVP05qnSulPe5Q==",
+ "TcyyXrSsQsnz0gJ36w4Dxw==",
+ "y4mfEDerrhaqApDdhP5vjA==",
+ "ynaj4XjU27b7XbqPyxI8Ig==",
+ "Ua6aO6HwM+rY4sPR19CNFA==",
+ "3go7bJ9WqH/PPUTjNP3q/Q==",
+ "n1ixvP7SfwYT3L2iWpJg6A==",
+ "W8y32OLHihfeV0XFw7LmOg==",
+ "uzkNhmo2d08tv5AmnyqkoQ==",
+ "hJ8leLNuJ6DK5V8scnDaZQ==",
+ "KodYHHN62zESrXUye7M01g==",
+ "H+yPRiooEh5J7lAJB4RZ7Q==",
+ "dZg5w8rFETMp9SgW7m0gfg==",
+ "LsmsPokAwWNCuC74MaqFCQ==",
+ "1QGhj9NONF2rC44UdO+Izw==",
+ "uwGivY3/C9WK+dirRPJZ4A==",
+ "rXGWY/Gq+ZEsmvBHUfFMmQ==",
+ "j4FBMnNfdBwx0VsDeTvhFg==",
+ "81nkjWtpBhqhvOp6K8dcWg==",
+ "dCDaYYrgASXPMGFRV0RCGg==",
+ "Kj1QI+s9261S3lTtPKd9eg==",
+ "LblwOqNiciHmt2NXjd89tg==",
+ "46piyANQVvvLqcoMq5G8tQ==",
+ "XJihma9zSRrXLC+T+VcFDA==",
+ "K3NBEG8jJTJbSrYSOC3FKw==",
+ "cT3PwwS6ALZA/na9NjtdzA==",
+ "wJ4uCrl4DPg70ltw1dZO3w==",
+ "JATLdpQm//SQnkyCfI5x7Q==",
+ "X1PaCfEDScclLtOTiF5JUw==",
+ "444F9T6Y7J67Y9sULG81qg==",
+ "8JVHFRwAd/SCLU0CRJYofg==",
+ "aLh1XEUrfR9W82gzusKcOg==",
+ "U+bB5NjFIuQr/Y5UpXHwxA==",
+ "Egs14xVbRWjfBBX7X5Z60g==",
+ "KSorNz/PLR/YYkxaj1fuqw==",
+ "RDgGGxTtcPvRg/5KRRlz4w==",
+ "5T39s5CtSrK5awMPUcEWJg==",
+ "+PUVXkoTqHxJHO18z4KMfw==",
+ "Bvk8NX4l6WktLcRDRKsK/A==",
+ "kNGIV3+jQmJlZDTXy1pnyA==",
+ "E3jMjAgXwvwR8PA53g4+PQ==",
+ "MbI04HlTGCoc/6WDejwtaQ==",
+ "aEnHUfn7UE/Euh6jsMuZ7g==",
+ "z4Bft++f72QeDh4PWGr/sw==",
+ "1lCcQWGDePPYco4vYrA5vw==",
+ "iu5csar0IQQBOTgw5OvJwQ==",
+ "raKMXnnX6PFFsbloDqyVzQ==",
+ "uPnL9tboMZo0Kl2fe24CmA==",
+ "8OFxXwnPmrogpNoueZlC4Q==",
+ "V6CRKrKezPwsRdbm0DJ2Yg==",
+ "xmGgK3W5y+oCd0K2u8XjZQ==",
+ "Ry3zgZ6KHrpNyb7+Tt2Pkw==",
+ "IwLbkL33z+LdTjaFYh93kg==",
+ "caepyBOAFu0MxbcXrGf6TA==",
+ "iIWxFdolLcnXqIjPMg+5kQ==",
+ "P430CeF2MDkuq11YdjvV8A==",
+ "yCu+DVU/ceMTOZ5h/7wQTg==",
+ "4mQVNv7FHj+/O6XFqWFt/Q==",
+ "OEJ40VmMDYzc2ESEMontRA==",
+ "D66Suu3tWBD+eurBpPXfjA==",
+ "RNK9G1hfuz3ETY/RmA9+aA==",
+ "BYpHADmEnzBsegdYTv8B5Q==",
+ "DBKrdpCE0awppxST4o/zzg==",
+ "KOmdvm+wJuZ/nT/o1+xOuw==",
+ "gDxqUdxxeXDYhJk9zcrNyA==",
+ "UPzS4LR3p/h0u69+7YemrQ==",
+ "hf9HFxWRNX2ucH8FLS7ytA==",
+ "ozVqYsmUueKifb4lDyVyrg==",
+ "TfHvdbl2M4deg65QKBTPng==",
+ "SzCGM8ypE58FLaR1+1ccxQ==",
+ "3nthUmLZ30HxQrzr2d7xFA==",
+ "1jBaRO8Bg5l6TH7qJ8EPiw==",
+ "eJlcN+gJnqAnctbWSIO9uA==",
+ "G8LFBop8u6IIng+gQuVg3w==",
+ "3JhnM6G4L06NHt31lR0zXA==",
+ "342VOUOxoLHUqtHANt83Hw==",
+ "hRxbdeniAVFgKUgB9Q3Y+g==",
+ "cFFE2R4GztNoftYkqalqUQ==",
+ "YmaksRzoU+OwlpiEaBDYaQ==",
+ "jon1y9yMEGfiIBjsDeeJdA==",
+ "oSnrpW4UmmVXtUGWqLq+tQ==",
+ "zaqyy3GaJ7cp8qDoLJWcTw==",
+ "luO1R8dUM9gy1E2lojRQoA==",
+ "YHM6NNHjmodv+G0mRLK7kw==",
+ "ZSmN8mmI9lDEHkJqBBg0Nw==",
+ "520wTzrysiRi2Td92Zq0HQ==",
+ "RAAw14BA1ws5Wu/rU7oegw==",
+ "vb6Agwzk4JG0Nn7qRPPFMQ==",
+ "joDXdLpXvRjOqkRiYaD/Sw==",
+ "dK2DU3t1ns+DWDwfBvH3SQ==",
+ "gZNJ1Qq6OcnwXqc+jXzMLQ==",
+ "R8ULpSNu9FcCwXZM0QedSg==",
+ "mc45FSMtzdw2PTcEBwHWPw==",
+ "d0qvm3bl38rRCpYdWqolCQ==",
+ "o9tdzmIu+3J/EYU4YWyTkA==",
+ "5eXpiczlRdmqMYSaodOUiQ==",
+ "KYuUNrkTvjUWQovw9dNakA==",
+ "02im2RooJQ/9UfUrh5LO+A==",
+ "kWPUUi7x9kKKa6nJ+FDR5Q==",
+ "6z8CRivao3IMyV4p4gMh7g==",
+ "SmRWEzqddY9ucGAP5jXjAg==",
+ "DJscTYNFPyPmTb57g/1w+Q==",
+ "uOHrw37yF9oLLVd16nUpeg==",
+ "HaIRV9SNPRTPDOSX9sK/bg==",
+ "K4yZNVoqHjXNhrZzz2gTew==",
+ "bTNRjJm+FfSQVfd56nNNqQ==",
+ "x5lyMArsv1MuJmEFlWCnNw==",
+ "cxpZ4bloGv734LBf4NpVhA==",
+ "kUudvRfA33uJDzHIShQd3Q==",
+ "3Wfj05vCLFAB9vII5AU9tw==",
+ "FUQySDFodnRhr+NUsWt0KA==",
+ "eC/RcoCVQBlXdE9WtcgXIw==",
+ "NoX8lkY+kd2GPuGjp+s0tQ==",
+ "EzjbinBHx3Wr08eXpH3HXA==",
+ "0VsaJHR0Ms8zegsCpAKoyg==",
+ "e2xLFVavnZIUUtxJx+qa1g==",
+ "Kt6BTG1zdeBZ3nlVk+BZKQ==",
+ "EUXQZwLgnDG+C8qxVoBNdw==",
+ "0SkC/4PtnX1bMYgD6r6CLA==",
+ "rzj6mjHCcMEouL66083BAg==",
+ "V5HEaY3v9agOhsbYOAZgJA==",
+ "tJt6VDdAPEemBUvnoc4viA==",
+ "g0lWrzEYMntVIahC7i0O2g==",
+ "zCpibjrZOA3FQ4lYt0WoVA==",
+ "4Xh/B3C16rrjbES+FM1W8g==",
+ "GHEdXgGWOeOa6RuPMF0xXg==",
+ "3kREs/qaMX0AwFXN0LO5ow==",
+ "GLDNTSwygNBmuFwCIm7HtA==",
+ "JBkbaBiorCtFq9M9lSUdMg==",
+ "rJCuanCy51ydVD4nInf9IQ==",
+ "OzFRv+PzPqTNmOnvZGoo5g==",
+ "7mxU5fJl/c6dXss9H3vGcQ==",
+ "9J53kk+InE3CKa7cPyCXMw==",
+ "x9TIZ9Ua++3BX+MpjgTuWA==",
+ "h0MH5NGFfChgmRJ3E/R3HQ==",
+ "25w3ZRUzCvJwAVHYCIO5uw==",
+ "1Wc8jQlDSB4Dp32wkL2odw==",
+ "ipPPjxpXHS1tcykXmrHPMQ==",
+ "r95wJtP5rsTExKMS7QhHcw==",
+ "TZT86wXfzFffjt0f95UF5w==",
+ "VpmBstwR7qPVqPgKYQTA3g==",
+ "3++dZXzZ6AFEz7hK+i5hww==",
+ "mAiD16zf+rCc7Qzxjd5buA==",
+ "1JI9bT92UzxI8txjhst9LQ==",
+ "TNyvLixb03aP2f8cDozzfA==",
+ "spHVvA/pc7nF9Q4ON020+w==",
+ "GA8k6GQ20DGduVoC+gieRA==",
+ "T7waQc3PvTFr0yWGKmFQdQ==",
+ "P0Pc8owrqt6spdf7FgBFSw==",
+ "DKApp/alXiaPSRNm3MfSuA==",
+ "UreSZCIdDgloih8KLeX7gg==",
+ "xJi0T+psHOXMivSOVpMWeQ==",
+ "cNsC9bH30eM1EZS6IdEdtQ==",
+ "XjjrIpsmATV/lyln4tPb+g==",
+ "qt5CsMts2aD4lw/4Q6bHYQ==",
+ "h+KRDKIvyVUBmRjv1LcCyg==",
+ "2j83jrPwPfYlpJJ2clEBYQ==",
+ "ZrCezGLz38xKmzAom6yCTQ==",
+ "SEGu+cSbeeeZg4xWwsSErQ==",
+ "Duz/8Ebbd0w6oHwOs0Wnwg==",
+ "Ci7sS7Yi1+IwAM3VMAB4ew==",
+ "DG2Qe2DqPs5MkZPOqX363Q==",
+ "v0Bvws1WYVoEgDt8xmVKew==",
+ "CtDj/h2Q/lRey20G8dzSgA==",
+ "WRoJMO0BCJyn5V6qnpUi4Q==",
+ "RQywrOLZEKw9+kG6qTzr3g==",
+ "mU4CqbAwpwqegxJaOz9ofQ==",
+ "aN5x46Gw1VihRalwCt1CGg==",
+ "U6VQghxOXsydh3Naa5Nz4A==",
+ "YA+zdEC+yEgFWRIgS1Eiqw==",
+ "oPcxgoismve6+jXyIKK6AQ==",
+ "PqLCd/pwc+q5GkL6MB0jTg==",
+ "fHL+fHtDxhALZFb9W/uHuw==",
+ "dhTevyxTYAuKbdLWhG47Kw==",
+ "VllbOAjeW3Dpbj5lp2OSmA==",
+ "3itfXtlLPRmPCSYaSvc39Q==",
+ "GNak/LFeoHWlTdLW1iU4eg==",
+ "HuDuxs2KiGqmeyY1s1PjpQ==",
+ "xs8J3cesq7lDhP/dNltqOw==",
+ "foXSDEUwMhfHWJSmSejsQg==",
+ "6fWom3YoKvW6NIg6y9o9CQ==",
+ "NhZbSq0CjDNOAIvBHBM9zA==",
+ "5w4FbRhWACP7k2WnNitiHg==",
+ "0UeRwDID2RBIikInqFI7uw==",
+ "/y/jHHEpUu5TR+R2o96kXA==",
+ "voO3krg4sdy4Iu+MZEr8+g==",
+ "hdzol5dk//Q6tCm4+OndIA==",
+ "Nc5kiwXCAyjpzt43G5RF1A==",
+ "3UBYBMejKInSbCHRoJJ7dg==",
+ "dRFCIbVu0Y8XbjG5i+UFCQ==",
+ "t8pjhdyNJirkvYgWIO/eKg==",
+ "FAXzjjIr8l1nsQFPpgxM/g==",
+ "SPGpjEJrpflv1hF0qsFlPw==",
+ "9Y1ZmfiHJd9vCiZ6KfO1xQ==",
+ "7Eqzyb+Kep+dIahYJWNNxQ==",
+ "9rL8nC/VbSqrvnUtH9WsxQ==",
+ "H4FZ5Wcnb40hQM1DMGGe8A==",
+ "AjoXWGb/l9xH/hscgEc6kQ==",
+ "6nzFl41uutgDdC30oOeCqg==",
+ "3jo1jRy3MybXtoLR+JIbJw==",
+ "mXdE08dv+OlIhlcqMBH2Gg==",
+ "Ifd7DI6o8N5gnyAKqZTlRw==",
+ "JNUvg/kxL3rdcZnD4IqUxw==",
+ "ry8B+sAHNeFIZHCCDynFyw==",
+ "TXaEd5lIKhzjcncfNcBgSg==",
+ "Mr3ehuDMUimOSn+FlkchdA==",
+ "cwiGhjmX9v8I7E/ekQ0h+g==",
+ "I/r5+1jnqumCPprKC/2BqA==",
+ "S4V3MfGYk8I4fd3WH09yYw==",
+ "A+crVyUeynAkEMYKbnFjZw==",
+ "vtyHcNQPcUTRuZcQvRUX4Q==",
+ "UNKx1ZVv3HNp21zrUSm6ew==",
+ "rsAlvGLv2D0swd6ol3WlvA==",
+ "2qwqb8ENAR2fpQnw55sPDw==",
+ "xBJJuYYnsTJOeFggZSKC4Q==",
+ "omvtZZKruPiEt6fV0YXTdg==",
+ "JZEgKUhUN+USJsvtF4HZOg==",
+ "euG/kpJ5elSDOGNbWWDfNQ==",
+ "DiiVmM6/WNcp0MUjSaFq6w==",
+ "QCNS8gAml1M2pJ+MxZsueg==",
+ "M6+pggFsHfM3alFxcMOFNQ==",
+ "YLoWpDTwXnszEQm8FA164Q==",
+ "N08oUZtlXbQvO9t3vXnGog==",
+ "jkjuJowWuOa4CLY+RZiErQ==",
+ "mPf+S+6oAoVIYEVveaiNFA==",
+ "R0iVyo5qreP/68uZlZphDA==",
+ "GYlqhQgp03B0mXpUhQ+ZCA==",
+ "lQNbmWD7PhwNGye+zbc3GQ==",
+ "cNeaOJEOzUSDdRmenPQyuw==",
+ "Gp66/Txv6ebv5bn85TuQtA==",
+ "xAda6DVkcvvqhI8vWZeGyA==",
+ "Ggk1Qa0lEdAgCXG6SmCkBA==",
+ "MYuO7ZURXtyaf56q7hH4Zw==",
+ "RUIdZRTgJBudWUZQFgiFaQ==",
+ "bgFJxLirUom2zT0h7LdOpw==",
+ "A2gaOpIlrS7TKVQgy9XMSw==",
+ "zevXp0lqqnXv9X6Bgmjtqg==",
+ "a5iuFqWAdFFsRgp7SFYwNg==",
+ "TxTy0TaDsWTcRH3wdBEQLQ==",
+ "jephVdKDeJIhXPrdMOJ4qA==",
+ "C4KdamfqUPuJ3RGFdpIEdw==",
+ "zl6l2Ioz1qovRUIWrSyxVA==",
+ "+gGaDxUe0UnNrf3PPg1qQQ==",
+ "1HgbrlaLMHS6Qj/0kkaJxg==",
+ "eGxTly6Pnu7eV/MKYMmuYw==",
+ "RAMKfnlrzNjpyh2BWt6JHg==",
+ "4pZQm9ogCZ/EAR9pjJm1eA==",
+ "l1zv3erwXIegQFd02NlCag==",
+ "uHGyRZchuA4ulmuD5LqquQ==",
+ "/vFu89tsV+lbcoiqM/XWog==",
+ "63SUgqfQimrmjvy/bEDQ0w==",
+ "JLHuf+FlChFDa9LYfTQ4Eg==",
+ "I+ZnPePTFX8ZODe14bxgyA==",
+ "CtoK1k3U82BkvzuPfQ4pjQ==",
+ "6nqQm4C7y+wZ+qX0kVjwmA==",
+ "+C3kBxRXIjqBk0EJxe3Xfg==",
+ "qVu748pIxEZtiywg4/4qhw==",
+ "07o+sKjjRCYkwy/ACyoYhg==",
+ "CiLF4dkbLURekBcQbwPUVA==",
+ "W/N5/nkp4iQIPYfAagVV7A==",
+ "3PJOphhEjw0E4arTfVVwdg==",
+ "YdMbARHwB+bSOd0PlTlXiA==",
+ "41hbx5Yr7UWxsV6+bWUYUA==",
+ "SqJHXD0MorNwHtHL9TbWLg==",
+ "pWKGUzm/muwOiBtzkRMnRg==",
+ "az9zZ7HTa4FJGRQMcamvEw==",
+ "zavAAN8C9Wo8oBLyztp63Q==",
+ "yBAnPmwrMJ8kpPP292S/Lw==",
+ "E6szQhjuUAz2e0h9ffQfEQ==",
+ "Fs3cQxQyS9kM4T8j5R7rWw==",
+ "GB5fRLZxnjRUfEe0SwcePQ==",
+ "+9OY8xkT9dM/rb2T6ACtOQ==",
+ "If2xFBD1p91iDD7ZrsfgjA==",
+ "QCFfoMhy8EleZAOpfRY88w==",
+ "NobWPk1Z6bHt5s9NHXt/pg==",
+ "nK6T4vV4384OIcqO5tQMhA==",
+ "Zov1EzK+VomiuwT1+ulQ8g==",
+ "pF98OKDvLUlnTzo7wmlpOw==",
+ "Wrq9YDsieAMC3Y2DSY5Rcg=="
+ ]
+}
diff --git a/browser/base/content/newtab/newTab.js b/browser/base/content/newtab/newTab.js
new file mode 100644
index 000000000..bbd2ef39d
--- /dev/null
+++ b/browser/base/content/newtab/newTab.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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;
+var Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PageThumbs.jsm");
+Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm");
+Cu.import("resource:///modules/DirectoryLinksProvider.jsm");
+Cu.import("resource://gre/modules/NewTabUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Rect",
+ "resource://gre/modules/Geometry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+var {
+ links: gLinks,
+ allPages: gAllPages,
+ linkChecker: gLinkChecker,
+ pinnedLinks: gPinnedLinks,
+ blockedLinks: gBlockedLinks,
+ gridPrefs: gGridPrefs
+} = NewTabUtils;
+
+XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
+ return Services.strings.
+ createBundle("chrome://browser/locale/newTab.properties");
+});
+
+function newTabString(name, args) {
+ let stringName = "newtab." + name;
+ if (!args) {
+ return gStringBundle.GetStringFromName(stringName);
+ }
+ return gStringBundle.formatStringFromName(stringName, args, args.length);
+}
+
+function inPrivateBrowsingMode() {
+ return PrivateBrowsingUtils.isContentWindowPrivate(window);
+}
+
+const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
+const XUL_NAMESPACE = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const TILES_EXPLAIN_LINK = "https://support.mozilla.org/kb/how-do-tiles-work-firefox";
+const TILES_INTRO_LINK = "https://www.mozilla.org/firefox/tiles/";
+const TILES_PRIVACY_LINK = "https://www.mozilla.org/privacy/";
+
+#include transformations.js
+#include page.js
+#include grid.js
+#include cells.js
+#include sites.js
+#include drag.js
+#include dragDataHelper.js
+#include drop.js
+#include dropTargetShim.js
+#include dropPreview.js
+#include updater.js
+#include undo.js
+#include search.js
+#include customize.js
+
+// Everything is loaded. Initialize the New Tab Page.
+gPage.init();
diff --git a/browser/base/content/newtab/newTab.xhtml b/browser/base/content/newtab/newTab.xhtml
new file mode 100644
index 000000000..07fb0093e
--- /dev/null
+++ b/browser/base/content/newtab/newTab.xhtml
@@ -0,0 +1,96 @@
+<?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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd">
+ %newTabDTD;
+ <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+ %browserDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&newtab.pageTitle;</title>
+
+ <link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/" />
+ <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/content/contentSearchUI.css" />
+ <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/content/newtab/newTab.css" />
+ <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/newtab/newTab.css" />
+</head>
+
+<body dir="&locale.dir;">
+ <div id="newtab-customize-overlay"></div>
+
+ <div class="newtab-customize-panel-container">
+ <div id="newtab-customize-panel" orient="vertical">
+ <div id="newtab-customize-panel-anchor"></div>
+ <div id="newtab-customize-panel-inner-wrapper">
+ <div id="newtab-customize-title" class="newtab-customize-panel-item">
+ <label>&newtab.customize.cog.title2;</label>
+ </div>
+
+ <div class="newtab-customize-complex-option">
+ <div id="newtab-customize-classic" class="newtab-customize-panel-superitem newtab-customize-panel-item selectable">
+ <label>&newtab.customize.classic;</label>
+ </div>
+ <div id="newtab-customize-enhanced" class="newtab-customize-panel-subitem">
+ <label class="checkbox"></label>
+ <label>&newtab.customize.cog.enhanced;</label>
+ </div>
+ </div>
+ <div id="newtab-customize-blank" class="newtab-customize-panel-item selectable">
+ <label>&newtab.customize.blank2;</label>
+ </div>
+ <div id="newtab-customize-learn" class="newtab-customize-panel-item">
+ <label>&newtab.customize.cog.learn;</label>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="newtab-vertical-margin">
+ <div id="newtab-margin-top"/>
+
+ <div id="newtab-margin-undo-container">
+ <div id="newtab-undo-container" undo-disabled="true">
+ <label id="newtab-undo-label">&newtab.undo.removedLabel;</label>
+ <button id="newtab-undo-button" tabindex="-1"
+ class="newtab-undo-button">&newtab.undo.undoButton;</button>
+ <button id="newtab-undo-restore-button" tabindex="-1"
+ class="newtab-undo-button">&newtab.undo.restoreButton;</button>
+ <button id="newtab-undo-close-button" tabindex="-1" title="&newtab.undo.closeTooltip;"/>
+ </div>
+ </div>
+
+ <div id="newtab-search-container">
+ <div id="newtab-search-form">
+ <div id="newtab-search-icon"/>
+ <input type="text" name="q" value="" id="newtab-search-text"
+ aria-label="&contentSearchInput.label;" maxlength="256"/>
+ <input id="newtab-search-submit" type="button"
+ title="&contentSearchSubmit.tooltip;"/>
+ </div>
+ </div>
+
+ <div id="newtab-horizontal-margin">
+ <div class="newtab-side-margin"/>
+ <div id="newtab-grid">
+ <h1 id="topsites-heading"/>
+ </div>
+ <div class="newtab-side-margin"/>
+ </div>
+
+ <div id="newtab-margin-bottom"/>
+ </div>
+ <input id="newtab-customize-button" type="button" dir="&locale.dir;"
+ value="&#x2699;"
+ title="&newtab.customize.title;"/>
+</body>
+<script type="text/javascript;version=1.8" src="chrome://browser/content/contentSearchUI.js"/>
+<script type="text/javascript;version=1.8" src="chrome://browser/content/newtab/newTab.js"/>
+</html>
diff --git a/browser/base/content/newtab/page.js b/browser/base/content/newtab/page.js
new file mode 100644
index 000000000..f7626ced2
--- /dev/null
+++ b/browser/base/content/newtab/page.js
@@ -0,0 +1,297 @@
+#ifdef 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/. */
+#endif
+
+// The amount of time we wait while coalescing updates for hidden pages.
+const SCHEDULE_UPDATE_TIMEOUT_MS = 1000;
+
+/**
+ * This singleton represents the whole 'New Tab Page' and takes care of
+ * initializing all its components.
+ */
+var gPage = {
+ /**
+ * Initializes the page.
+ */
+ init: function Page_init() {
+ // Add ourselves to the list of pages to receive notifications.
+ gAllPages.register(this);
+
+ // Listen for 'unload' to unregister this page.
+ addEventListener("unload", this, false);
+
+ // XXX bug 991111 - Not all click events are correctly triggered when
+ // listening from xhtml nodes -- in particular middle clicks on sites, so
+ // listen from the xul window and filter then delegate
+ addEventListener("click", this, false);
+
+ // Check if the new tab feature is enabled.
+ let enabled = gAllPages.enabled;
+ if (enabled)
+ this._init();
+
+ this._updateAttributes(enabled);
+
+ // Initialize customize controls.
+ gCustomize.init();
+ },
+
+ /**
+ * Listens for notifications specific to this page.
+ */
+ observe: function Page_observe(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed") {
+ gCustomize.updateSelected();
+
+ let enabled = gAllPages.enabled;
+ this._updateAttributes(enabled);
+
+ // Update thumbnails to the new enhanced setting
+ if (aData == "browser.newtabpage.enhanced") {
+ this.update();
+ }
+
+ // Initialize the whole page if we haven't done that, yet.
+ if (enabled) {
+ this._init();
+ } else {
+ gUndoDialog.hide();
+ }
+ } else if (aTopic == "page-thumbnail:create" && gGrid.ready) {
+ for (let site of gGrid.sites) {
+ if (site && site.url === aData) {
+ site.refreshThumbnail();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the page's grid right away for visible pages. If the page is
+ * currently hidden, i.e. in a background tab or in the preloader, then we
+ * batch multiple update requests and refresh the grid once after a short
+ * delay. Accepts a single parameter the specifies the reason for requesting
+ * a page update. The page may decide to delay or prevent a requested updated
+ * based on the given reason.
+ */
+ update(reason = "") {
+ // Update immediately if we're visible.
+ if (!document.hidden) {
+ // Ignore updates where reason=links-changed as those signal that the
+ // provider's set of links changed. We don't want to update visible pages
+ // in that case, it is ok to wait until the user opens the next tab.
+ if (reason != "links-changed" && gGrid.ready) {
+ gGrid.refresh();
+ }
+
+ return;
+ }
+
+ // Bail out if we scheduled before.
+ if (this._scheduleUpdateTimeout) {
+ return;
+ }
+
+ this._scheduleUpdateTimeout = setTimeout(() => {
+ // Refresh if the grid is ready.
+ if (gGrid.ready) {
+ gGrid.refresh();
+ }
+
+ this._scheduleUpdateTimeout = null;
+ }, SCHEDULE_UPDATE_TIMEOUT_MS);
+ },
+
+ /**
+ * Internally initializes the page. This runs only when/if the feature
+ * is/gets enabled.
+ */
+ _init: function Page_init() {
+ if (this._initialized)
+ return;
+
+ this._initialized = true;
+
+ // Set submit button label for when CSS background are disabled (e.g.
+ // high contrast mode).
+ document.getElementById("newtab-search-submit").value =
+ document.body.getAttribute("dir") == "ltr" ? "\u25B6" : "\u25C0";
+
+ if (Services.prefs.getBoolPref("browser.newtabpage.compact")) {
+ document.body.classList.add("compact");
+ }
+
+ // Initialize search.
+ gSearch.init();
+
+ if (document.hidden) {
+ addEventListener("visibilitychange", this);
+ } else {
+ setTimeout(() => this.onPageFirstVisible());
+ }
+
+ // Initialize and render the grid.
+ gGrid.init();
+
+ // Initialize the drop target shim.
+ gDropTargetShim.init();
+
+#ifdef XP_MACOSX
+ // Workaround to prevent a delay on MacOSX due to a slow drop animation.
+ document.addEventListener("dragover", this, false);
+ document.addEventListener("drop", this, false);
+#endif
+ },
+
+ /**
+ * Updates the 'page-disabled' attributes of the respective DOM nodes.
+ * @param aValue Whether the New Tab Page is enabled or not.
+ */
+ _updateAttributes: function Page_updateAttributes(aValue) {
+ // Set the nodes' states.
+ let nodeSelector = "#newtab-grid, #newtab-search-container";
+ for (let node of document.querySelectorAll(nodeSelector)) {
+ if (aValue)
+ node.removeAttribute("page-disabled");
+ else
+ node.setAttribute("page-disabled", "true");
+ }
+
+ // Enables/disables the control and link elements.
+ let inputSelector = ".newtab-control, .newtab-link";
+ for (let input of document.querySelectorAll(inputSelector)) {
+ if (aValue)
+ input.removeAttribute("tabindex");
+ else
+ input.setAttribute("tabindex", "-1");
+ }
+ },
+
+ /**
+ * Handles unload event
+ */
+ _handleUnloadEvent: function Page_handleUnloadEvent() {
+ gAllPages.unregister(this);
+ // compute page life-span and send telemetry probe: using milli-seconds will leave
+ // many low buckets empty. Instead we use half-second precision to make low end
+ // of histogram linear and not lose the change in user attention
+ let delta = Math.round((Date.now() - this._firstVisibleTime) / 500);
+ if (this._suggestedTilePresent) {
+ Services.telemetry.getHistogramById("NEWTAB_PAGE_LIFE_SPAN_SUGGESTED").add(delta);
+ }
+ else {
+ Services.telemetry.getHistogramById("NEWTAB_PAGE_LIFE_SPAN").add(delta);
+ }
+ },
+
+ /**
+ * Handles all page events.
+ */
+ handleEvent: function Page_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "load":
+ this.onPageVisibleAndLoaded();
+ break;
+ case "unload":
+ this._handleUnloadEvent();
+ break;
+ case "click":
+ let {button, target} = aEvent;
+ // Go up ancestors until we find a Site or not
+ while (target) {
+ if (target.hasOwnProperty("_newtabSite")) {
+ target._newtabSite.onClick(aEvent);
+ break;
+ }
+ target = target.parentNode;
+ }
+ break;
+ case "dragover":
+ if (gDrag.isValid(aEvent) && gDrag.draggedSite)
+ aEvent.preventDefault();
+ break;
+ case "drop":
+ if (gDrag.isValid(aEvent) && gDrag.draggedSite) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ break;
+ case "visibilitychange":
+ // Cancel any delayed updates for hidden pages now that we're visible.
+ if (this._scheduleUpdateTimeout) {
+ clearTimeout(this._scheduleUpdateTimeout);
+ this._scheduleUpdateTimeout = null;
+
+ // An update was pending so force an update now.
+ this.update();
+ }
+
+ setTimeout(() => this.onPageFirstVisible());
+ removeEventListener("visibilitychange", this);
+ break;
+ }
+ },
+
+ onPageFirstVisible: function () {
+ // Record another page impression.
+ Services.telemetry.getHistogramById("NEWTAB_PAGE_SHOWN").add(true);
+
+ for (let site of gGrid.sites) {
+ if (site) {
+ // The site may need to modify and/or re-render itself if
+ // something changed after newtab was created by preloader.
+ // For example, the suggested tile endTime may have passed.
+ site.onFirstVisible();
+ }
+ }
+
+ // save timestamp to compute page life-span delta
+ this._firstVisibleTime = Date.now();
+
+ if (document.readyState == "complete") {
+ this.onPageVisibleAndLoaded();
+ } else {
+ addEventListener("load", this);
+ }
+ },
+
+ onPageVisibleAndLoaded() {
+ // Send the index of the last visible tile.
+ this.reportLastVisibleTileIndex();
+ // Maybe tell the user they can undo an initial automigration
+ this.maybeShowAutoMigrationUndoNotification();
+ },
+
+ reportLastVisibleTileIndex() {
+ let cwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ let rect = cwu.getBoundsWithoutFlushing(gGrid.node);
+ let nodes = cwu.nodesFromRect(rect.left, rect.top, 0, rect.width,
+ rect.height, 0, true, false);
+
+ let i = -1;
+ let lastIndex = -1;
+ let sites = gGrid.sites;
+
+ for (let node of nodes) {
+ if (node.classList && node.classList.contains("newtab-cell")) {
+ if (sites[++i]) {
+ lastIndex = i;
+ if (sites[i].link.targetedSite) {
+ // record that suggested tile is shown to use suggested-tiles-histogram
+ this._suggestedTilePresent = true;
+ }
+ }
+ }
+ }
+
+ DirectoryLinksProvider.reportSitesAction(sites, "view", lastIndex);
+ },
+
+ maybeShowAutoMigrationUndoNotification() {
+ sendAsyncMessage("NewTab:MaybeShowAutoMigrationUndoNotification");
+ },
+};
diff --git a/browser/base/content/newtab/search.js b/browser/base/content/newtab/search.js
new file mode 100644
index 000000000..cbbb6e243
--- /dev/null
+++ b/browser/base/content/newtab/search.js
@@ -0,0 +1,15 @@
+#ifdef 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/. */
+#endif
+
+var gSearch = {
+ init: function () {
+ document.getElementById("newtab-search-submit")
+ .addEventListener("click", e => this._contentSearchController.search(e));
+ let textbox = document.getElementById("newtab-search-text");
+ this._contentSearchController =
+ new ContentSearchUIController(textbox, textbox.parentNode, "newtab", "newtab");
+ },
+};
diff --git a/browser/base/content/newtab/sites.js b/browser/base/content/newtab/sites.js
new file mode 100644
index 000000000..9d103ce9b
--- /dev/null
+++ b/browser/base/content/newtab/sites.js
@@ -0,0 +1,440 @@
+#ifdef 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/. */
+#endif
+
+const THUMBNAIL_PLACEHOLDER_ENABLED =
+ Services.prefs.getBoolPref("browser.newtabpage.thumbnailPlaceholder");
+
+/**
+ * This class represents a site that is contained in a cell and can be pinned,
+ * moved around or deleted.
+ */
+function Site(aNode, aLink) {
+ this._node = aNode;
+ this._node._newtabSite = this;
+
+ this._link = aLink;
+
+ this._render();
+ this._addEventHandlers();
+}
+
+Site.prototype = {
+ /**
+ * The site's DOM node.
+ */
+ get node() { return this._node; },
+
+ /**
+ * The site's link.
+ */
+ get link() { return this._link; },
+
+ /**
+ * The url of the site's link.
+ */
+ get url() { return this.link.url; },
+
+ /**
+ * The title of the site's link.
+ */
+ get title() { return this.link.title || this.link.url; },
+
+ /**
+ * The site's parent cell.
+ */
+ get cell() {
+ let parentNode = this.node.parentNode;
+ return parentNode && parentNode._newtabCell;
+ },
+
+ /**
+ * Pins the site on its current or a given index.
+ * @param aIndex The pinned index (optional).
+ * @return true if link changed type after pin
+ */
+ pin: function Site_pin(aIndex) {
+ if (typeof aIndex == "undefined")
+ aIndex = this.cell.index;
+
+ this._updateAttributes(true);
+ let changed = gPinnedLinks.pin(this._link, aIndex);
+ if (changed) {
+ // render site again to remove suggested/sponsored tags
+ this._render();
+ }
+ return changed;
+ },
+
+ /**
+ * Unpins the site and calls the given callback when done.
+ */
+ unpin: function Site_unpin() {
+ if (this.isPinned()) {
+ this._updateAttributes(false);
+ gPinnedLinks.unpin(this._link);
+ gUpdater.updateGrid();
+ }
+ },
+
+ /**
+ * Checks whether this site is pinned.
+ * @return Whether this site is pinned.
+ */
+ isPinned: function Site_isPinned() {
+ return gPinnedLinks.isPinned(this._link);
+ },
+
+ /**
+ * Blocks the site (removes it from the grid) and calls the given callback
+ * when done.
+ */
+ block: function Site_block() {
+ if (!gBlockedLinks.isBlocked(this._link)) {
+ gUndoDialog.show(this);
+ gBlockedLinks.block(this._link);
+ gUpdater.updateGrid();
+ }
+ },
+
+ /**
+ * Gets the DOM node specified by the given query selector.
+ * @param aSelector The query selector.
+ * @return The DOM node we found.
+ */
+ _querySelector: function Site_querySelector(aSelector) {
+ return this.node.querySelector(aSelector);
+ },
+
+ /**
+ * Updates attributes for all nodes which status depends on this site being
+ * pinned or unpinned.
+ * @param aPinned Whether this site is now pinned or unpinned.
+ */
+ _updateAttributes: function (aPinned) {
+ let control = this._querySelector(".newtab-control-pin");
+
+ if (aPinned) {
+ this.node.setAttribute("pinned", true);
+ control.setAttribute("title", newTabString("unpin"));
+ } else {
+ this.node.removeAttribute("pinned");
+ control.setAttribute("title", newTabString("pin"));
+ }
+ },
+
+ _newTabString: function(str, substrArr) {
+ let regExp = /%[0-9]\$S/g;
+ let matches;
+ while ((matches = regExp.exec(str))) {
+ let match = matches[0];
+ let index = match.charAt(1); // Get the digit in the regExp.
+ str = str.replace(match, substrArr[index - 1]);
+ }
+ return str;
+ },
+
+ _getSuggestedTileExplanation: function() {
+ let targetedName = `<strong> ${this.link.targetedName} </strong>`;
+ let targetedSite = `<strong> ${this.link.targetedSite} </strong>`;
+ if (this.link.explanation) {
+ return this._newTabString(this.link.explanation, [targetedName, targetedSite]);
+ }
+ return newTabString("suggested.button", [targetedName]);
+ },
+
+ /**
+ * Checks for and modifies link at campaign end time
+ */
+ _checkLinkEndTime: function Site_checkLinkEndTime() {
+ if (this.link.endTime && this.link.endTime < Date.now()) {
+ let oldUrl = this.url;
+ // chop off the path part from url
+ this.link.url = Services.io.newURI(this.url, null, null).resolve("/");
+ // clear supplied images - this triggers thumbnail download for new url
+ delete this.link.imageURI;
+ delete this.link.enhancedImageURI;
+ // remove endTime to avoid further time checks
+ delete this.link.endTime;
+ // clear enhanced-content image that may still exist in preloaded page
+ this._querySelector(".enhanced-content").style.backgroundImage = "";
+ gPinnedLinks.replace(oldUrl, this.link);
+ }
+ },
+
+ /**
+ * Renders the site's data (fills the HTML fragment).
+ */
+ _render: function Site_render() {
+ // first check for end time, as it may modify the link
+ this._checkLinkEndTime();
+ // setup display variables
+ let enhanced = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link);
+ let url = this.url;
+ let title = enhanced && enhanced.title ? enhanced.title :
+ this.link.type == "history" ? this.link.baseDomain :
+ this.title;
+ let tooltip = (this.title == url ? this.title : this.title + "\n" + url);
+
+ let link = this._querySelector(".newtab-link");
+ link.setAttribute("title", tooltip);
+ link.setAttribute("href", url);
+ this.node.setAttribute("type", this.link.type);
+
+ let titleNode = this._querySelector(".newtab-title");
+ titleNode.textContent = title;
+ if (this.link.titleBgColor) {
+ titleNode.style.backgroundColor = this.link.titleBgColor;
+ }
+
+ // remove "suggested" attribute to avoid showing "suggested" tag
+ // after site was pinned or dropped
+ this.node.removeAttribute("suggested");
+
+ if (this.link.targetedSite) {
+ if (this.node.getAttribute("type") != "sponsored") {
+ this._querySelector(".newtab-sponsored").textContent =
+ newTabString("suggested.tag");
+ }
+
+ this.node.setAttribute("suggested", true);
+ let explanation = this._getSuggestedTileExplanation();
+ this._querySelector(".newtab-suggested").innerHTML =
+ `<div class='newtab-suggested-bounds'> ${explanation} </div>`;
+ }
+
+ if (this.isPinned())
+ this._updateAttributes(true);
+ // Capture the page if the thumbnail is missing, which will cause page.js
+ // to be notified and call our refreshThumbnail() method.
+ this.captureIfMissing();
+ // but still display whatever thumbnail might be available now.
+ this.refreshThumbnail();
+ },
+
+ /**
+ * Called when the site's tab becomes visible for the first time.
+ * Since the newtab may be preloaded long before it's displayed,
+ * check for changed conditions and re-render if needed
+ */
+ onFirstVisible: function Site_onFirstVisible() {
+ if (this.link.endTime && this.link.endTime < Date.now()) {
+ // site needs to change landing url and background image
+ this._render();
+ }
+ else {
+ this.captureIfMissing();
+ }
+ },
+
+ /**
+ * Captures the site's thumbnail in the background, but only if there's no
+ * existing thumbnail and the page allows background captures.
+ */
+ captureIfMissing: function Site_captureIfMissing() {
+ if (!document.hidden && !this.link.imageURI) {
+ BackgroundPageThumbs.captureIfMissing(this.url);
+ }
+ },
+
+ /**
+ * Refreshes the thumbnail for the site.
+ */
+ refreshThumbnail: function Site_refreshThumbnail() {
+ // Only enhance tiles if that feature is turned on
+ let link = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link) ||
+ this.link;
+
+ let thumbnail = this._querySelector(".newtab-thumbnail.thumbnail");
+ if (link.bgColor) {
+ thumbnail.style.backgroundColor = link.bgColor;
+ }
+ let uri = link.imageURI || PageThumbs.getThumbnailURL(this.url);
+ thumbnail.style.backgroundImage = 'url("' + uri + '")';
+
+ if (THUMBNAIL_PLACEHOLDER_ENABLED &&
+ link.type == "history" &&
+ link.baseDomain) {
+ let placeholder = this._querySelector(".newtab-thumbnail.placeholder");
+ let charCodeSum = 0;
+ for (let c of link.baseDomain) {
+ charCodeSum += c.charCodeAt(0);
+ }
+ const COLORS = 16;
+ let hue = Math.round((charCodeSum % COLORS) / COLORS * 360);
+ placeholder.style.backgroundColor = "hsl(" + hue + ",80%,40%)";
+ placeholder.textContent = link.baseDomain.substr(0,1).toUpperCase();
+ }
+
+ if (link.enhancedImageURI) {
+ let enhanced = this._querySelector(".enhanced-content");
+ enhanced.style.backgroundImage = 'url("' + link.enhancedImageURI + '")';
+
+ if (this.link.type != link.type) {
+ this.node.setAttribute("type", "enhanced");
+ this.enhancedId = link.directoryId;
+ }
+ }
+ },
+
+ _ignoreHoverEvents: function(element) {
+ element.addEventListener("mouseover", () => {
+ this.cell.node.setAttribute("ignorehover", "true");
+ });
+ element.addEventListener("mouseout", () => {
+ this.cell.node.removeAttribute("ignorehover");
+ });
+ },
+
+ /**
+ * Adds event handlers for the site and its buttons.
+ */
+ _addEventHandlers: function Site_addEventHandlers() {
+ // Register drag-and-drop event handlers.
+ this._node.addEventListener("dragstart", this, false);
+ this._node.addEventListener("dragend", this, false);
+ this._node.addEventListener("mouseover", this, false);
+
+ // Specially treat the sponsored icon & suggested explanation
+ // text to prevent regular hover effects
+ let sponsored = this._querySelector(".newtab-sponsored");
+ let suggested = this._querySelector(".newtab-suggested");
+ this._ignoreHoverEvents(sponsored);
+ this._ignoreHoverEvents(suggested);
+ },
+
+ /**
+ * Speculatively opens a connection to the current site.
+ */
+ _speculativeConnect: function Site_speculativeConnect() {
+ let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
+ let uri = Services.io.newURI(this.url, null, null);
+ try {
+ // This can throw for certain internal URLs, when they wind up in
+ // about:newtab. Be sure not to propagate the error.
+ sc.speculativeConnect(uri, null);
+ } catch (e) {}
+ },
+
+ /**
+ * Record interaction with site using telemetry.
+ */
+ _recordSiteClicked: function Site_recordSiteClicked(aIndex) {
+ if (Services.prefs.prefHasUserValue("browser.newtabpage.rows") ||
+ Services.prefs.prefHasUserValue("browser.newtabpage.columns") ||
+ aIndex > 8) {
+ // We only want to get indices for the default configuration, everything
+ // else goes in the same bucket.
+ aIndex = 9;
+ }
+ Services.telemetry.getHistogramById("NEWTAB_PAGE_SITE_CLICKED")
+ .add(aIndex);
+ },
+
+ _toggleLegalText: function(buttonClass, explanationTextClass) {
+ let button = this._querySelector(buttonClass);
+ if (button.hasAttribute("active")) {
+ let explain = this._querySelector(explanationTextClass);
+ explain.parentNode.removeChild(explain);
+
+ button.removeAttribute("active");
+ }
+ else {
+ let explain = document.createElementNS(HTML_NAMESPACE, "div");
+ explain.className = explanationTextClass.slice(1); // Slice off the first character, '.'
+ this.node.appendChild(explain);
+
+ let link = '<a href="' + TILES_EXPLAIN_LINK + '">' +
+ newTabString("learn.link") + "</a>";
+ let type = (this.node.getAttribute("suggested") && this.node.getAttribute("type") == "affiliate") ?
+ "suggested" : this.node.getAttribute("type");
+ let icon = '<input type="button" class="newtab-control newtab-' +
+ (type == "enhanced" ? "customize" : "control-block") + '"/>';
+ explain.innerHTML = newTabString(type + (type == "sponsored" ? ".explain2" : ".explain"), [icon, link]);
+
+ button.setAttribute("active", "true");
+ }
+ },
+
+ /**
+ * Handles site click events.
+ */
+ onClick: function Site_onClick(aEvent) {
+ let action;
+ let pinned = this.isPinned();
+ let tileIndex = this.cell.index;
+ let {button, target} = aEvent;
+
+ // Handle tile/thumbnail link click
+ if (target.classList.contains("newtab-link") ||
+ target.parentElement.classList.contains("newtab-link")) {
+ // Record for primary and middle clicks
+ if (button == 0 || button == 1) {
+ this._recordSiteClicked(tileIndex);
+ action = "click";
+ }
+ }
+ // Handle sponsored explanation link click
+ else if (target.parentElement.classList.contains("sponsored-explain")) {
+ action = "sponsored_link";
+ }
+ else if (target.parentElement.classList.contains("suggested-explain")) {
+ action = "suggested_link";
+ }
+ // Only handle primary clicks for the remaining targets
+ else if (button == 0) {
+ aEvent.preventDefault();
+ if (target.classList.contains("newtab-control-block")) {
+ // Notify DirectoryLinksProvider of suggested tile block, this may
+ // affect if and how suggested tiles are recommended and needs to
+ // be reported before pages are updated inside block() call
+ if (this.link.targetedSite) {
+ DirectoryLinksProvider.handleSuggestedTileBlock();
+ }
+ this.block();
+ action = "block";
+ }
+ else if (target.classList.contains("sponsored-explain") ||
+ target.classList.contains("newtab-sponsored")) {
+ this._toggleLegalText(".newtab-sponsored", ".sponsored-explain");
+ action = "sponsored";
+ }
+ else if (pinned && target.classList.contains("newtab-control-pin")) {
+ this.unpin();
+ action = "unpin";
+ }
+ else if (!pinned && target.classList.contains("newtab-control-pin")) {
+ if (this.pin()) {
+ // suggested link has changed - update rest of the pages
+ gAllPages.update(gPage);
+ }
+ action = "pin";
+ }
+ }
+
+ // Report all link click actions
+ if (action) {
+ DirectoryLinksProvider.reportSitesAction(gGrid.sites, action, tileIndex);
+ }
+ },
+
+ /**
+ * Handles all site events.
+ */
+ handleEvent: function Site_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "mouseover":
+ this._node.removeEventListener("mouseover", this, false);
+ this._speculativeConnect();
+ break;
+ case "dragstart":
+ gDrag.start(this, aEvent);
+ break;
+ case "dragend":
+ gDrag.end(this, aEvent);
+ break;
+ }
+ }
+};
diff --git a/browser/base/content/newtab/transformations.js b/browser/base/content/newtab/transformations.js
new file mode 100644
index 000000000..f7db0ad84
--- /dev/null
+++ b/browser/base/content/newtab/transformations.js
@@ -0,0 +1,270 @@
+#ifdef 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/. */
+#endif
+
+/**
+ * This singleton allows to transform the grid by repositioning a site's node
+ * in the DOM and by showing or hiding the node. It additionally provides
+ * convenience methods to work with a site's DOM node.
+ */
+var gTransformation = {
+ /**
+ * Returns the width of the left and top border of a cell. We need to take it
+ * into account when measuring and comparing site and cell positions.
+ */
+ get _cellBorderWidths() {
+ let cstyle = window.getComputedStyle(gGrid.cells[0].node, null);
+ let widths = {
+ left: parseInt(cstyle.getPropertyValue("border-left-width")),
+ top: parseInt(cstyle.getPropertyValue("border-top-width"))
+ };
+
+ // Cache this value, overwrite the getter.
+ Object.defineProperty(this, "_cellBorderWidths",
+ {value: widths, enumerable: true});
+
+ return widths;
+ },
+
+ /**
+ * Gets a DOM node's position.
+ * @param aNode The DOM node.
+ * @return A Rect instance with the position.
+ */
+ getNodePosition: function Transformation_getNodePosition(aNode) {
+ let {left, top, width, height} = aNode.getBoundingClientRect();
+ return new Rect(left + scrollX, top + scrollY, width, height);
+ },
+
+ /**
+ * Fades a given node from zero to full opacity.
+ * @param aNode The node to fade.
+ * @param aCallback The callback to call when finished.
+ */
+ fadeNodeIn: function Transformation_fadeNodeIn(aNode, aCallback) {
+ this._setNodeOpacity(aNode, 1, function () {
+ // Clear the style property.
+ aNode.style.opacity = "";
+
+ if (aCallback)
+ aCallback();
+ });
+ },
+
+ /**
+ * Fades a given node from full to zero opacity.
+ * @param aNode The node to fade.
+ * @param aCallback The callback to call when finished.
+ */
+ fadeNodeOut: function Transformation_fadeNodeOut(aNode, aCallback) {
+ this._setNodeOpacity(aNode, 0, aCallback);
+ },
+
+ /**
+ * Fades a given site from zero to full opacity.
+ * @param aSite The site to fade.
+ * @param aCallback The callback to call when finished.
+ */
+ showSite: function Transformation_showSite(aSite, aCallback) {
+ this.fadeNodeIn(aSite.node, aCallback);
+ },
+
+ /**
+ * Fades a given site from full to zero opacity.
+ * @param aSite The site to fade.
+ * @param aCallback The callback to call when finished.
+ */
+ hideSite: function Transformation_hideSite(aSite, aCallback) {
+ this.fadeNodeOut(aSite.node, aCallback);
+ },
+
+ /**
+ * Allows to set a site's position.
+ * @param aSite The site to re-position.
+ * @param aPosition The desired position for the given site.
+ */
+ setSitePosition: function Transformation_setSitePosition(aSite, aPosition) {
+ let style = aSite.node.style;
+ let {top, left} = aPosition;
+
+ style.top = top + "px";
+ style.left = left + "px";
+ },
+
+ /**
+ * Freezes a site in its current position by positioning it absolute.
+ * @param aSite The site to freeze.
+ */
+ freezeSitePosition: function Transformation_freezeSitePosition(aSite) {
+ if (this._isFrozen(aSite))
+ return;
+
+ let style = aSite.node.style;
+ let comp = getComputedStyle(aSite.node, null);
+ style.width = comp.getPropertyValue("width");
+ style.height = comp.getPropertyValue("height");
+
+ aSite.node.setAttribute("frozen", "true");
+ this.setSitePosition(aSite, this.getNodePosition(aSite.node));
+ },
+
+ /**
+ * Unfreezes a site by removing its absolute positioning.
+ * @param aSite The site to unfreeze.
+ */
+ unfreezeSitePosition: function Transformation_unfreezeSitePosition(aSite) {
+ if (!this._isFrozen(aSite))
+ return;
+
+ let style = aSite.node.style;
+ style.left = style.top = style.width = style.height = "";
+ aSite.node.removeAttribute("frozen");
+ },
+
+ /**
+ * Slides the given site to the target node's position.
+ * @param aSite The site to move.
+ * @param aTarget The slide target.
+ * @param aOptions Set of options (see below).
+ * unfreeze - unfreeze the site after sliding
+ * callback - the callback to call when finished
+ */
+ slideSiteTo: function Transformation_slideSiteTo(aSite, aTarget, aOptions) {
+ let currentPosition = this.getNodePosition(aSite.node);
+ let targetPosition = this.getNodePosition(aTarget.node)
+ let callback = aOptions && aOptions.callback;
+
+ let self = this;
+
+ function finish() {
+ if (aOptions && aOptions.unfreeze)
+ self.unfreezeSitePosition(aSite);
+
+ if (callback)
+ callback();
+ }
+
+ // We need to take the width of a cell's border into account.
+ targetPosition.left += this._cellBorderWidths.left;
+ targetPosition.top += this._cellBorderWidths.top;
+
+ // Nothing to do here if the positions already match.
+ if (currentPosition.left == targetPosition.left &&
+ currentPosition.top == targetPosition.top) {
+ finish();
+ } else {
+ this.setSitePosition(aSite, targetPosition);
+ this._whenTransitionEnded(aSite.node, ["left", "top"], finish);
+ }
+ },
+
+ /**
+ * Rearranges a given array of sites and moves them to their new positions or
+ * fades in/out new/removed sites.
+ * @param aSites An array of sites to rearrange.
+ * @param aOptions Set of options (see below).
+ * unfreeze - unfreeze the site after rearranging
+ * callback - the callback to call when finished
+ */
+ rearrangeSites: function Transformation_rearrangeSites(aSites, aOptions) {
+ let batch = [];
+ let cells = gGrid.cells;
+ let callback = aOptions && aOptions.callback;
+ let unfreeze = aOptions && aOptions.unfreeze;
+
+ aSites.forEach(function (aSite, aIndex) {
+ // Do not re-arrange empty cells or the dragged site.
+ if (!aSite || aSite == gDrag.draggedSite)
+ return;
+
+ batch.push(new Promise(resolve => {
+ if (!cells[aIndex]) {
+ // The site disappeared from the grid, hide it.
+ this.hideSite(aSite, resolve);
+ } else if (this._getNodeOpacity(aSite.node) != 1) {
+ // The site disappeared before but is now back, show it.
+ this.showSite(aSite, resolve);
+ } else {
+ // The site's position has changed, move it around.
+ this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: resolve});
+ }
+ }));
+ }, this);
+
+ if (callback) {
+ Promise.all(batch).then(callback);
+ }
+ },
+
+ /**
+ * Listens for the 'transitionend' event on a given node and calls the given
+ * callback.
+ * @param aNode The node that is transitioned.
+ * @param aProperties The properties we'll wait to be transitioned.
+ * @param aCallback The callback to call when finished.
+ */
+ _whenTransitionEnded:
+ function Transformation_whenTransitionEnded(aNode, aProperties, aCallback) {
+
+ let props = new Set(aProperties);
+ aNode.addEventListener("transitionend", function onEnd(e) {
+ if (props.has(e.propertyName)) {
+ aNode.removeEventListener("transitionend", onEnd);
+ aCallback();
+ }
+ });
+ },
+
+ /**
+ * Gets a given node's opacity value.
+ * @param aNode The node to get the opacity value from.
+ * @return The node's opacity value.
+ */
+ _getNodeOpacity: function Transformation_getNodeOpacity(aNode) {
+ let cstyle = window.getComputedStyle(aNode, null);
+ return cstyle.getPropertyValue("opacity");
+ },
+
+ /**
+ * Sets a given node's opacity.
+ * @param aNode The node to set the opacity value for.
+ * @param aOpacity The opacity value to set.
+ * @param aCallback The callback to call when finished.
+ */
+ _setNodeOpacity:
+ function Transformation_setNodeOpacity(aNode, aOpacity, aCallback) {
+
+ if (this._getNodeOpacity(aNode) == aOpacity) {
+ if (aCallback)
+ aCallback();
+ } else {
+ if (aCallback) {
+ this._whenTransitionEnded(aNode, ["opacity"], aCallback);
+ }
+
+ aNode.style.opacity = aOpacity;
+ }
+ },
+
+ /**
+ * Moves a site to the cell with the given index.
+ * @param aSite The site to move.
+ * @param aIndex The target cell's index.
+ * @param aOptions Options that are directly passed to slideSiteTo().
+ */
+ _moveSite: function Transformation_moveSite(aSite, aIndex, aOptions) {
+ this.freezeSitePosition(aSite);
+ this.slideSiteTo(aSite, gGrid.cells[aIndex], aOptions);
+ },
+
+ /**
+ * Checks whether a site is currently frozen.
+ * @param aSite The site to check.
+ * @return Whether the given site is frozen.
+ */
+ _isFrozen: function Transformation_isFrozen(aSite) {
+ return aSite.node.hasAttribute("frozen");
+ }
+};
diff --git a/browser/base/content/newtab/undo.js b/browser/base/content/newtab/undo.js
new file mode 100644
index 000000000..b856914d2
--- /dev/null
+++ b/browser/base/content/newtab/undo.js
@@ -0,0 +1,116 @@
+#ifdef 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/. */
+#endif
+
+/**
+ * Dialog allowing to undo the removal of single site or to completely restore
+ * the grid's original state.
+ */
+var gUndoDialog = {
+ /**
+ * The undo dialog's timeout in miliseconds.
+ */
+ HIDE_TIMEOUT_MS: 15000,
+
+ /**
+ * Contains undo information.
+ */
+ _undoData: null,
+
+ /**
+ * Initializes the undo dialog.
+ */
+ init: function UndoDialog_init() {
+ this._undoContainer = document.getElementById("newtab-undo-container");
+ this._undoContainer.addEventListener("click", this, false);
+ this._undoButton = document.getElementById("newtab-undo-button");
+ this._undoCloseButton = document.getElementById("newtab-undo-close-button");
+ this._undoRestoreButton = document.getElementById("newtab-undo-restore-button");
+ },
+
+ /**
+ * Shows the undo dialog.
+ * @param aSite The site that just got removed.
+ */
+ show: function UndoDialog_show(aSite) {
+ if (this._undoData)
+ clearTimeout(this._undoData.timeout);
+
+ this._undoData = {
+ index: aSite.cell.index,
+ wasPinned: aSite.isPinned(),
+ blockedLink: aSite.link,
+ timeout: setTimeout(this.hide.bind(this), this.HIDE_TIMEOUT_MS)
+ };
+
+ this._undoContainer.removeAttribute("undo-disabled");
+ this._undoButton.removeAttribute("tabindex");
+ this._undoCloseButton.removeAttribute("tabindex");
+ this._undoRestoreButton.removeAttribute("tabindex");
+ },
+
+ /**
+ * Hides the undo dialog.
+ */
+ hide: function UndoDialog_hide() {
+ if (!this._undoData)
+ return;
+
+ clearTimeout(this._undoData.timeout);
+ this._undoData = null;
+ this._undoContainer.setAttribute("undo-disabled", "true");
+ this._undoButton.setAttribute("tabindex", "-1");
+ this._undoCloseButton.setAttribute("tabindex", "-1");
+ this._undoRestoreButton.setAttribute("tabindex", "-1");
+ },
+
+ /**
+ * The undo dialog event handler.
+ * @param aEvent The event to handle.
+ */
+ handleEvent: function UndoDialog_handleEvent(aEvent) {
+ switch (aEvent.target.id) {
+ case "newtab-undo-button":
+ this._undo();
+ break;
+ case "newtab-undo-restore-button":
+ this._undoAll();
+ break;
+ case "newtab-undo-close-button":
+ this.hide();
+ break;
+ }
+ },
+
+ /**
+ * Undo the last blocked site.
+ */
+ _undo: function UndoDialog_undo() {
+ if (!this._undoData)
+ return;
+
+ let {index, wasPinned, blockedLink} = this._undoData;
+ gBlockedLinks.unblock(blockedLink);
+
+ if (wasPinned) {
+ gPinnedLinks.pin(blockedLink, index);
+ }
+
+ gUpdater.updateGrid();
+ this.hide();
+ },
+
+ /**
+ * Undo all blocked sites.
+ */
+ _undoAll: function UndoDialog_undoAll() {
+ NewTabUtils.undoAll(function() {
+ gUpdater.updateGrid();
+ this.hide();
+ }.bind(this));
+ }
+};
+
+gUndoDialog.init();
diff --git a/browser/base/content/newtab/updater.js b/browser/base/content/newtab/updater.js
new file mode 100644
index 000000000..2bab74d70
--- /dev/null
+++ b/browser/base/content/newtab/updater.js
@@ -0,0 +1,177 @@
+#ifdef 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/. */
+#endif
+
+/**
+ * This singleton provides functionality to update the current grid to a new
+ * set of pinned and blocked sites. It adds, moves and removes sites.
+ */
+var gUpdater = {
+ /**
+ * Updates the current grid according to its pinned and blocked sites.
+ * This removes old, moves existing and creates new sites to fill gaps.
+ * @param aCallback The callback to call when finished.
+ */
+ updateGrid: function Updater_updateGrid(aCallback) {
+ let links = gLinks.getLinks().slice(0, gGrid.cells.length);
+
+ // Find all sites that remain in the grid.
+ let sites = this._findRemainingSites(links);
+
+ // Remove sites that are no longer in the grid.
+ this._removeLegacySites(sites, () => {
+ // Freeze all site positions so that we can move their DOM nodes around
+ // without any visual impact.
+ this._freezeSitePositions(sites);
+
+ // Move the sites' DOM nodes to their new position in the DOM. This will
+ // have no visual effect as all the sites have been frozen and will
+ // remain in their current position.
+ this._moveSiteNodes(sites);
+
+ // Now it's time to animate the sites actually moving to their new
+ // positions.
+ this._rearrangeSites(sites, () => {
+ // Try to fill empty cells and finish.
+ this._fillEmptyCells(links, aCallback);
+
+ // Update other pages that might be open to keep them synced.
+ gAllPages.update(gPage);
+ });
+ });
+ },
+
+ /**
+ * Takes an array of links and tries to correlate them to sites contained in
+ * the current grid. If no corresponding site can be found (i.e. the link is
+ * new and a site will be created) then just set it to null.
+ * @param aLinks The array of links to find sites for.
+ * @return Array of sites mapped to the given links (can contain null values).
+ */
+ _findRemainingSites: function Updater_findRemainingSites(aLinks) {
+ let map = {};
+
+ // Create a map to easily retrieve the site for a given URL.
+ gGrid.sites.forEach(function (aSite) {
+ if (aSite)
+ map[aSite.url] = aSite;
+ });
+
+ // Map each link to its corresponding site, if any.
+ return aLinks.map(function (aLink) {
+ return aLink && (aLink.url in map) && map[aLink.url];
+ });
+ },
+
+ /**
+ * Freezes the given sites' positions.
+ * @param aSites The array of sites to freeze.
+ */
+ _freezeSitePositions: function Updater_freezeSitePositions(aSites) {
+ aSites.forEach(function (aSite) {
+ if (aSite)
+ gTransformation.freezeSitePosition(aSite);
+ });
+ },
+
+ /**
+ * Moves the given sites' DOM nodes to their new positions.
+ * @param aSites The array of sites to move.
+ */
+ _moveSiteNodes: function Updater_moveSiteNodes(aSites) {
+ let cells = gGrid.cells;
+
+ // Truncate the given array of sites to not have more sites than cells.
+ // This can happen when the user drags a bookmark (or any other new kind
+ // of link) onto the grid.
+ let sites = aSites.slice(0, cells.length);
+
+ sites.forEach(function (aSite, aIndex) {
+ let cell = cells[aIndex];
+ let cellSite = cell.site;
+
+ // The site's position didn't change.
+ if (!aSite || cellSite != aSite) {
+ let cellNode = cell.node;
+
+ // Empty the cell if necessary.
+ if (cellSite)
+ cellNode.removeChild(cellSite.node);
+
+ // Put the new site in place, if any.
+ if (aSite)
+ cellNode.appendChild(aSite.node);
+ }
+ }, this);
+ },
+
+ /**
+ * Rearranges the given sites and slides them to their new positions.
+ * @param aSites The array of sites to re-arrange.
+ * @param aCallback The callback to call when finished.
+ */
+ _rearrangeSites: function Updater_rearrangeSites(aSites, aCallback) {
+ let options = {callback: aCallback, unfreeze: true};
+ gTransformation.rearrangeSites(aSites, options);
+ },
+
+ /**
+ * Removes all sites from the grid that are not in the given links array or
+ * exceed the grid.
+ * @param aSites The array of sites remaining in the grid.
+ * @param aCallback The callback to call when finished.
+ */
+ _removeLegacySites: function Updater_removeLegacySites(aSites, aCallback) {
+ let batch = [];
+
+ // Delete sites that were removed from the grid.
+ gGrid.sites.forEach(function (aSite) {
+ // The site must be valid and not in the current grid.
+ if (!aSite || aSites.indexOf(aSite) != -1)
+ return;
+
+ batch.push(new Promise(resolve => {
+ // Fade out the to-be-removed site.
+ gTransformation.hideSite(aSite, function () {
+ let node = aSite.node;
+
+ // Remove the site from the DOM.
+ node.parentNode.removeChild(node);
+ resolve();
+ });
+ }));
+ });
+
+ Promise.all(batch).then(aCallback);
+ },
+
+ /**
+ * Tries to fill empty cells with new links if available.
+ * @param aLinks The array of links.
+ * @param aCallback The callback to call when finished.
+ */
+ _fillEmptyCells: function Updater_fillEmptyCells(aLinks, aCallback) {
+ let {cells, sites} = gGrid;
+
+ // Find empty cells and fill them.
+ Promise.all(sites.map((aSite, aIndex) => {
+ if (aSite || !aLinks[aIndex])
+ return null;
+
+ return new Promise(resolve => {
+ // Create the new site and fade it in.
+ let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]);
+
+ // Set the site's initial opacity to zero.
+ site.node.style.opacity = 0;
+
+ // Flush all style changes for the dynamically inserted site to make
+ // the fade-in transition work.
+ window.getComputedStyle(site.node).opacity;
+ gTransformation.showSite(site, resolve);
+ });
+ })).then(aCallback).catch(console.exception);
+ }
+};
diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js
new file mode 100644
index 000000000..8eb9b034f
--- /dev/null
+++ b/browser/base/content/nsContextMenu.js
@@ -0,0 +1,1878 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 sw=2 sts=2 et 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/.
+
+Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm");
+Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
+Components.utils.import("resource://gre/modules/LoginManagerContextMenu.jsm");
+Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+var gContextMenuContentData = null;
+
+function nsContextMenu(aXulMenu, aIsShift) {
+ this.shouldDisplay = true;
+ this.initMenu(aXulMenu, aIsShift);
+}
+
+// Prototype for nsContextMenu "class."
+nsContextMenu.prototype = {
+ initMenu: function CM_initMenu(aXulMenu, aIsShift) {
+ // Get contextual info.
+ this.setTarget(document.popupNode, document.popupRangeParent,
+ document.popupRangeOffset);
+ if (!this.shouldDisplay)
+ return;
+
+ this.hasPageMenu = false;
+ this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
+ if (!aIsShift) {
+ if (this.isRemote) {
+ this.hasPageMenu =
+ PageMenuParent.addToPopup(gContextMenuContentData.customMenuItems,
+ this.browser, aXulMenu);
+ }
+ else {
+ this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target, aXulMenu);
+ }
+
+ let subject = {
+ menu: aXulMenu,
+ tab: gBrowser ? gBrowser.getTabForBrowser(this.browser) : undefined,
+ isContentSelected: this.isContentSelected,
+ inFrame: this.inFrame,
+ isTextSelected: this.isTextSelected,
+ onTextInput: this.onTextInput,
+ onLink: this.onLink,
+ onImage: this.onImage,
+ onVideo: this.onVideo,
+ onAudio: this.onAudio,
+ onCanvas: this.onCanvas,
+ onEditableArea: this.onEditableArea,
+ srcUrl: this.mediaURL,
+ frameUrl: gContextMenuContentData ? gContextMenuContentData.docLocation : undefined,
+ pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
+ linkUrl: this.linkURL,
+ selectionText: this.isTextSelected ? this.selectionInfo.text : undefined,
+ };
+ subject.wrappedJSObject = subject;
+ Services.obs.notifyObservers(subject, "on-build-contextmenu", null);
+ }
+
+ this.isFrameImage = document.getElementById("isFrameImage");
+ this.ellipsis = "\u2026";
+ try {
+ this.ellipsis = gPrefService.getComplexValue("intl.ellipsis",
+ Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+
+ // Reset after "on-build-contextmenu" notification in case selection was
+ // changed during the notification.
+ this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
+ this.onPlainTextLink = false;
+
+ let bookmarkPage = document.getElementById("context-bookmarkpage");
+ if (bookmarkPage)
+ BookmarkingUI.onCurrentPageContextPopupShowing();
+
+ // Initialize (disable/remove) menu items.
+ this.initItems();
+
+ // Register this opening of the menu with telemetry:
+ this._checkTelemetryForMenu(aXulMenu);
+ },
+
+ hiding: function CM_hiding() {
+ gContextMenuContentData = null;
+ InlineSpellCheckerUI.clearSuggestionsFromMenu();
+ InlineSpellCheckerUI.clearDictionaryListFromMenu();
+ InlineSpellCheckerUI.uninit();
+ LoginManagerContextMenu.clearLoginsFromMenu(document);
+
+ // This handler self-deletes, only run it if it is still there:
+ if (this._onPopupHiding) {
+ this._onPopupHiding();
+ }
+ },
+
+ initItems: function CM_initItems() {
+ this.initPageMenuSeparator();
+ this.initOpenItems();
+ this.initNavigationItems();
+ this.initViewItems();
+ this.initMiscItems();
+ this.initSpellingItems();
+ this.initSaveItems();
+ this.initClipboardItems();
+ this.initMediaPlayerItems();
+ this.initLeaveDOMFullScreenItems();
+ this.initClickToPlayItems();
+ this.initPasswordManagerItems();
+ this.initSyncItems();
+ },
+
+ initPageMenuSeparator: function CM_initPageMenuSeparator() {
+ this.showItem("page-menu-separator", this.hasPageMenu);
+ },
+
+ initOpenItems: function CM_initOpenItems() {
+ var isMailtoInternal = false;
+ if (this.onMailtoLink) {
+ var mailtoHandler = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService).
+ getProtocolHandlerInfo("mailto");
+ isMailtoInternal = (!mailtoHandler.alwaysAskBeforeHandling &&
+ mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
+ (mailtoHandler.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp));
+ }
+
+ if (this.isTextSelected && !this.onLink &&
+ this.selectionInfo && this.selectionInfo.linkURL) {
+ this.linkURL = this.selectionInfo.linkURL;
+ try {
+ this.linkURI = makeURI(this.linkURL);
+ } catch (ex) {}
+
+ this.linkTextStr = this.selectionInfo.linkText;
+ this.onPlainTextLink = true;
+ }
+
+ var inContainer = false;
+ if (gContextMenuContentData.userContextId) {
+ inContainer = true;
+ var item = document.getElementById("context-openlinkincontainertab");
+
+ item.setAttribute("data-usercontextid", gContextMenuContentData.userContextId);
+
+ var label =
+ ContextualIdentityService.getUserContextLabel(gContextMenuContentData.userContextId);
+ item.setAttribute("label",
+ gBrowserBundle.formatStringFromName("userContextOpenLink.label",
+ [label], 1));
+ }
+
+ var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
+ var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
+ var showContainers = Services.prefs.getBoolPref("privacy.userContext.enabled");
+ this.showItem("context-openlink", shouldShow && !isWindowPrivate);
+ this.showItem("context-openlinkprivate", shouldShow);
+ this.showItem("context-openlinkintab", shouldShow && !inContainer);
+ this.showItem("context-openlinkincontainertab", shouldShow && inContainer);
+ this.showItem("context-openlinkinusercontext-menu", shouldShow && !isWindowPrivate && showContainers);
+ this.showItem("context-openlinkincurrent", this.onPlainTextLink);
+ this.showItem("context-sep-open", shouldShow);
+ },
+
+ initNavigationItems: function CM_initNavigationItems() {
+ var shouldShow = !(this.isContentSelected || this.onLink || this.onImage ||
+ this.onCanvas || this.onVideo || this.onAudio ||
+ this.onTextInput || this.onSocial);
+ this.showItem("context-navigation", shouldShow);
+ this.showItem("context-sep-navigation", shouldShow);
+
+ let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true";
+
+ let stopReloadItem = "";
+ if (shouldShow || this.onSocial) {
+ stopReloadItem = (stopped || this.onSocial) ? "reload" : "stop";
+ }
+
+ this.showItem("context-reload", stopReloadItem == "reload");
+ this.showItem("context-stop", stopReloadItem == "stop");
+
+ // XXX: Stop is determined in browser.js; the canStop broadcaster is broken
+ //this.setItemAttrFromNode( "context-stop", "disabled", "canStop" );
+ },
+
+ initLeaveDOMFullScreenItems: function CM_initLeaveFullScreenItem() {
+ // only show the option if the user is in DOM fullscreen
+ var shouldShow = (this.target.ownerDocument.fullscreenElement != null);
+ this.showItem("context-leave-dom-fullscreen", shouldShow);
+
+ // Explicitly show if in DOM fullscreen, but do not hide it has already been shown
+ if (shouldShow)
+ this.showItem("context-media-sep-commands", true);
+ },
+
+ initSaveItems: function CM_initSaveItems() {
+ var shouldShow = !(this.onTextInput || this.onLink ||
+ this.isContentSelected || this.onImage ||
+ this.onCanvas || this.onVideo || this.onAudio);
+ this.showItem("context-savepage", shouldShow);
+
+ // Save link depends on whether we're in a link, or selected text matches valid URL pattern.
+ this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink);
+
+ // Save image depends on having loaded its content, video and audio don't.
+ this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
+ this.showItem("context-savevideo", this.onVideo);
+ this.showItem("context-saveaudio", this.onAudio);
+ this.showItem("context-video-saveimage", this.onVideo);
+ this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
+ this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
+ // Send media URL (but not for canvas, since it's a big data: URL)
+ this.showItem("context-sendimage", this.onImage);
+ this.showItem("context-sendvideo", this.onVideo);
+ this.showItem("context-castvideo", this.onVideo);
+ this.showItem("context-sendaudio", this.onAudio);
+ let mediaIsBlob = this.mediaURL.startsWith("blob:");
+ this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL || mediaIsBlob);
+ this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL || mediaIsBlob);
+ let shouldShowCast = Services.prefs.getBoolPref("browser.casting.enabled");
+ // getServicesForVideo alone would be sufficient here (it depends on
+ // SimpleServiceDiscovery.services), but SimpleServiceDiscovery is guaranteed
+ // to be already loaded, since we load it on startup in nsBrowserGlue,
+ // and CastingApps isn't, so check SimpleServiceDiscovery.services first
+ // to avoid needing to load CastingApps.jsm if we don't need to.
+ shouldShowCast = shouldShowCast && this.mediaURL &&
+ SimpleServiceDiscovery.services.length > 0 &&
+ CastingApps.getServicesForVideo(this.target).length > 0;
+ this.setItemAttr("context-castvideo", "disabled", !shouldShowCast);
+ },
+
+ initViewItems: function CM_initViewItems() {
+ // View source is always OK, unless in directory listing.
+ this.showItem("context-viewpartialsource-selection",
+ this.isContentSelected);
+ this.showItem("context-viewpartialsource-mathml",
+ this.onMathML && !this.isContentSelected);
+
+ var shouldShow = !(this.isContentSelected ||
+ this.onImage || this.onCanvas ||
+ this.onVideo || this.onAudio ||
+ this.onLink || this.onTextInput);
+ var showInspect = !this.onSocial && gPrefService.getBoolPref("devtools.inspector.enabled");
+ this.showItem("context-viewsource", shouldShow);
+ this.showItem("context-viewinfo", shouldShow);
+ this.showItem("inspect-separator", showInspect);
+ this.showItem("context-inspect", showInspect);
+
+ this.showItem("context-sep-viewsource", shouldShow);
+
+ // Set as Desktop background depends on whether an image was clicked on,
+ // and only works if we have a shell service.
+ var haveSetDesktopBackground = false;
+#ifdef HAVE_SHELL_SERVICE
+ // Only enable Set as Desktop Background if we can get the shell service.
+ var shell = getShellService();
+ if (shell)
+ haveSetDesktopBackground = shell.canSetDesktopBackground;
+#endif
+ this.showItem("context-setDesktopBackground",
+ haveSetDesktopBackground && this.onLoadedImage);
+
+ if (haveSetDesktopBackground && this.onLoadedImage) {
+ document.getElementById("context-setDesktopBackground")
+ .disabled = gContextMenuContentData.disableSetDesktopBackground;
+ }
+
+ // Reload image depends on an image that's not fully loaded
+ this.showItem("context-reloadimage", (this.onImage && !this.onCompletedImage));
+
+ // View image depends on having an image that's not standalone
+ // (or is in a frame), or a canvas.
+ this.showItem("context-viewimage", (this.onImage &&
+ (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas);
+
+ // View video depends on not having a standalone video.
+ this.showItem("context-viewvideo", this.onVideo && (!this.inSyntheticDoc || this.inFrame));
+ this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL);
+
+ // View background image depends on whether there is one, but don't make
+ // background images of a stand-alone media document available.
+ this.showItem("context-viewbgimage", shouldShow &&
+ !this._hasMultipleBGImages &&
+ !this.inSyntheticDoc);
+ this.showItem("context-sep-viewbgimage", shouldShow &&
+ !this._hasMultipleBGImages &&
+ !this.inSyntheticDoc);
+ document.getElementById("context-viewbgimage")
+ .disabled = !this.hasBGImage;
+
+ this.showItem("context-viewimageinfo", this.onImage);
+ this.showItem("context-viewimagedesc", this.onImage && this.imageDescURL !== "");
+ },
+
+ initMiscItems: function CM_initMiscItems() {
+ // Use "Bookmark This Link" if on a link.
+ let bookmarkPage = document.getElementById("context-bookmarkpage");
+ this.showItem(bookmarkPage,
+ !(this.isContentSelected || this.onTextInput || this.onLink ||
+ this.onImage || this.onVideo || this.onAudio || this.onSocial ||
+ this.onCanvas));
+ bookmarkPage.setAttribute("tooltiptext", bookmarkPage.getAttribute("buttontooltiptext"));
+
+ this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink &&
+ !this.onSocial) || this.onPlainTextLink);
+ this.showItem("context-keywordfield",
+ this.onTextInput && this.onKeywordField);
+ this.showItem("frame", this.inFrame);
+
+ let showSearchSelect = (this.isTextSelected || this.onLink) && !this.onImage;
+ this.showItem("context-searchselect", showSearchSelect);
+ if (showSearchSelect) {
+ this.formatSearchContextItem();
+ }
+
+ // srcdoc cannot be opened separately due to concerns about web
+ // content with about:srcdoc in location bar masquerading as trusted
+ // chrome/addon content.
+ // No need to also test for this.inFrame as this is checked in the parent
+ // submenu.
+ this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
+ this.showItem("context-openframeintab", !this.inSrcdocFrame);
+ this.showItem("context-openframe", !this.inSrcdocFrame);
+ this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
+ this.showItem("open-frame-sep", !this.inSrcdocFrame);
+
+ this.showItem("frame-sep", this.inFrame && this.isTextSelected);
+
+ // Hide menu entries for images, show otherwise
+ if (this.inFrame) {
+ if (BrowserUtils.mimeTypeIsTextBased(this.target.ownerDocument.contentType))
+ this.isFrameImage.removeAttribute('hidden');
+ else
+ this.isFrameImage.setAttribute('hidden', 'true');
+ }
+
+ // BiDi UI
+ this.showItem("context-sep-bidi", !this.onNumeric && top.gBidiUI);
+ this.showItem("context-bidi-text-direction-toggle",
+ this.onTextInput && !this.onNumeric && top.gBidiUI);
+ this.showItem("context-bidi-page-direction-toggle",
+ !this.onTextInput && top.gBidiUI);
+
+ // SocialShare
+ let shareButton = SocialShare.shareButton;
+ let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
+ let pageShare = shareEnabled && !(this.isContentSelected ||
+ this.onTextInput || this.onLink || this.onImage ||
+ this.onVideo || this.onAudio || this.onCanvas);
+ this.showItem("context-sharepage", pageShare);
+ this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
+ this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink);
+ this.showItem("context-shareimage", shareEnabled && this.onImage);
+ this.showItem("context-sharevideo", shareEnabled && this.onVideo);
+ this.setItemAttr("context-sharevideo", "disabled", !this.mediaURL || this.mediaURL.startsWith("blob:"));
+ },
+
+ initSpellingItems: function() {
+ var canSpell = InlineSpellCheckerUI.canSpellCheck &&
+ !InlineSpellCheckerUI.initialSpellCheckPending &&
+ this.canSpellCheck;
+ let showDictionaries = canSpell && InlineSpellCheckerUI.enabled;
+ var onMisspelling = InlineSpellCheckerUI.overMisspelling;
+ var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
+ this.showItem("spell-check-enabled", canSpell);
+ this.showItem("spell-separator", canSpell);
+ document.getElementById("spell-check-enabled")
+ .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
+
+ this.showItem("spell-add-to-dictionary", onMisspelling);
+ this.showItem("spell-undo-add-to-dictionary", showUndo);
+
+ // suggestion list
+ this.showItem("spell-suggestions-separator", onMisspelling || showUndo);
+ if (onMisspelling) {
+ var suggestionsSeparator =
+ document.getElementById("spell-add-to-dictionary");
+ var numsug =
+ InlineSpellCheckerUI.addSuggestionsToMenu(suggestionsSeparator.parentNode,
+ suggestionsSeparator, 5);
+ this.showItem("spell-no-suggestions", numsug == 0);
+ }
+ else
+ this.showItem("spell-no-suggestions", false);
+
+ // dictionary list
+ this.showItem("spell-dictionaries", showDictionaries);
+ if (canSpell) {
+ var dictMenu = document.getElementById("spell-dictionaries-menu");
+ var dictSep = document.getElementById("spell-language-separator");
+ let count = InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
+ this.showItem(dictSep, count > 0);
+ this.showItem("spell-add-dictionaries-main", false);
+ }
+ else if (this.onEditableArea) {
+ // when there is no spellchecker but we might be able to spellcheck
+ // add the add to dictionaries item. This will ensure that people
+ // with no dictionaries will be able to download them
+ this.showItem("spell-language-separator", showDictionaries);
+ this.showItem("spell-add-dictionaries-main", showDictionaries);
+ }
+ else
+ this.showItem("spell-add-dictionaries-main", false);
+ },
+
+ initClipboardItems: function() {
+ // Copy depends on whether there is selected text.
+ // Enabling this context menu item is now done through the global
+ // command updating system
+ // this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() );
+ goUpdateGlobalEditMenuItems();
+
+ this.showItem("context-undo", this.onTextInput);
+ this.showItem("context-sep-undo", this.onTextInput);
+ this.showItem("context-cut", this.onTextInput);
+ this.showItem("context-copy",
+ this.isContentSelected || this.onTextInput);
+ this.showItem("context-paste", this.onTextInput);
+ this.showItem("context-delete", this.onTextInput);
+ this.showItem("context-sep-paste", this.onTextInput);
+ this.showItem("context-selectall", !(this.onLink || this.onImage ||
+ this.onVideo || this.onAudio ||
+ this.inSyntheticDoc) ||
+ this.isDesignMode);
+ this.showItem("context-sep-selectall", this.isContentSelected );
+
+ // XXX dr
+ // ------
+ // nsDocumentViewer.cpp has code to determine whether we're
+ // on a link or an image. we really ought to be using that...
+
+ // Copy email link depends on whether we're on an email link.
+ this.showItem("context-copyemail", this.onMailtoLink);
+
+ // Copy link location depends on whether we're on a non-mailto link.
+ this.showItem("context-copylink", this.onLink && !this.onMailtoLink);
+ this.showItem("context-sep-copylink", this.onLink &&
+ (this.onImage || this.onVideo || this.onAudio));
+
+#ifdef CONTEXT_COPY_IMAGE_CONTENTS
+ // Copy image contents depends on whether we're on an image.
+ this.showItem("context-copyimage-contents", this.onImage);
+#endif
+ // Copy image location depends on whether we're on an image.
+ this.showItem("context-copyimage", this.onImage);
+ this.showItem("context-copyvideourl", this.onVideo);
+ this.showItem("context-copyaudiourl", this.onAudio);
+ this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL);
+ this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL);
+ this.showItem("context-sep-copyimage", this.onImage ||
+ this.onVideo || this.onAudio);
+ },
+
+ initMediaPlayerItems: function() {
+ var onMedia = (this.onVideo || this.onAudio);
+ // Several mutually exclusive items... play/pause, mute/unmute, show/hide
+ this.showItem("context-media-play", onMedia && (this.target.paused || this.target.ended));
+ this.showItem("context-media-pause", onMedia && !this.target.paused && !this.target.ended);
+ this.showItem("context-media-mute", onMedia && !this.target.muted);
+ this.showItem("context-media-unmute", onMedia && this.target.muted);
+ this.showItem("context-media-playbackrate", onMedia && this.target.duration != Number.POSITIVE_INFINITY);
+ this.showItem("context-media-loop", onMedia);
+ this.showItem("context-media-showcontrols", onMedia && !this.target.controls);
+ this.showItem("context-media-hidecontrols", this.target.controls && (this.onVideo || (this.onAudio && !this.inSyntheticDoc)));
+ this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.fullscreenElement == null);
+ this.showItem("context-media-eme-learnmore", this.onDRMMedia);
+ this.showItem("context-media-eme-separator", this.onDRMMedia);
+
+ // Disable them when there isn't a valid media source loaded.
+ if (onMedia) {
+ this.setItemAttr("context-media-playbackrate-050x", "checked", this.target.playbackRate == 0.5);
+ this.setItemAttr("context-media-playbackrate-100x", "checked", this.target.playbackRate == 1.0);
+ this.setItemAttr("context-media-playbackrate-125x", "checked", this.target.playbackRate == 1.25);
+ this.setItemAttr("context-media-playbackrate-150x", "checked", this.target.playbackRate == 1.5);
+ this.setItemAttr("context-media-playbackrate-200x", "checked", this.target.playbackRate == 2.0);
+ this.setItemAttr("context-media-loop", "checked", this.target.loop);
+ var hasError = this.target.error != null ||
+ this.target.networkState == this.target.NETWORK_NO_SOURCE;
+ this.setItemAttr("context-media-play", "disabled", hasError);
+ this.setItemAttr("context-media-pause", "disabled", hasError);
+ this.setItemAttr("context-media-mute", "disabled", hasError);
+ this.setItemAttr("context-media-unmute", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError);
+ this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError);
+ this.setItemAttr("context-media-showcontrols", "disabled", hasError);
+ this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
+ if (this.onVideo) {
+ let canSaveSnapshot = !this.onDRMMedia && this.target.readyState >= this.target.HAVE_CURRENT_DATA;
+ this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot);
+ this.setItemAttr("context-video-fullscreen", "disabled", hasError);
+ }
+ }
+ this.showItem("context-media-sep-commands", onMedia);
+ },
+
+ initClickToPlayItems: function() {
+ this.showItem("context-ctp-play", this.onCTPPlugin);
+ this.showItem("context-ctp-hide", this.onCTPPlugin);
+ this.showItem("context-sep-ctp", this.onCTPPlugin);
+ },
+
+ initPasswordManagerItems: function() {
+ let loginFillInfo = gContextMenuContentData && gContextMenuContentData.loginFillInfo;
+
+ // If we could not find a password field we
+ // don't want to show the form fill option.
+ let showFill = loginFillInfo && loginFillInfo.passwordField.found;
+
+ // Disable the fill option if the user has set a master password
+ // or if the password field or target field are disabled.
+ let disableFill = !loginFillInfo ||
+ !Services.logins ||
+ !Services.logins.isLoggedIn ||
+ loginFillInfo.passwordField.disabled ||
+ (!this.onPassword && loginFillInfo.usernameField.disabled);
+
+ this.showItem("fill-login-separator", showFill);
+ this.showItem("fill-login", showFill);
+ this.setItemAttr("fill-login", "disabled", disableFill);
+
+ // Set the correct label for the fill menu
+ let fillMenu = document.getElementById("fill-login");
+ if (this.onPassword) {
+ fillMenu.setAttribute("label", fillMenu.getAttribute("label-password"));
+ fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-password"));
+ } else {
+ fillMenu.setAttribute("label", fillMenu.getAttribute("label-login"));
+ fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-login"));
+ }
+
+ if (!showFill || disableFill) {
+ return;
+ }
+ let documentURI = gContextMenuContentData.documentURIObject;
+ let fragment = LoginManagerContextMenu.addLoginsToMenu(this.target, this.browser, documentURI);
+
+ this.showItem("fill-login-no-logins", !fragment);
+
+ if (!fragment) {
+ return;
+ }
+ let popup = document.getElementById("fill-login-popup");
+ let insertBeforeElement = document.getElementById("fill-login-no-logins");
+ popup.insertBefore(fragment, insertBeforeElement);
+ },
+
+ initSyncItems: function() {
+ gFxAccounts.initPageContextMenu(this);
+ },
+
+ openPasswordManager: function() {
+ LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
+ },
+
+ inspectNode: function() {
+ let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ let gBrowser = this.browser.ownerGlobal.gBrowser;
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+
+ return gDevTools.showToolbox(target, "inspector").then(toolbox => {
+ let inspector = toolbox.getCurrentPanel();
+
+ // new-node-front tells us when the node has been selected, whether the
+ // browser is remote or not.
+ let onNewNode = inspector.selection.once("new-node-front");
+
+ this.browser.messageManager.sendAsyncMessage("debug:inspect", {}, {node: this.target});
+ inspector.walker.findInspectingNode().then(nodeFront => {
+ inspector.selection.setNodeFront(nodeFront, "browser-context-menu");
+ });
+
+ return onNewNode.then(() => {
+ // Now that the node has been selected, wait until the inspector is
+ // fully updated.
+ return inspector.once("inspector-updated");
+ });
+ });
+ },
+
+ // Set various context menu attributes based on the state of the world.
+ setTarget: function (aNode, aRangeParent, aRangeOffset) {
+ // gContextMenuContentData.isRemote tells us if the event came from a remote
+ // process. gContextMenuContentData can be null if something (like tests)
+ // opens the context menu directly.
+ let editFlags;
+ this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote;
+ if (this.isRemote) {
+ aNode = gContextMenuContentData.event.target;
+ aRangeParent = gContextMenuContentData.event.rangeParent;
+ aRangeOffset = gContextMenuContentData.event.rangeOffset;
+ editFlags = gContextMenuContentData.editFlags;
+ }
+
+ const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ if (aNode.nodeType == Node.DOCUMENT_NODE ||
+ // Not display on XUL element but relax for <label class="text-link">
+ (aNode.namespaceURI == xulNS && !isXULTextLinkLabel(aNode))) {
+ this.shouldDisplay = false;
+ return;
+ }
+
+ // Initialize contextual info.
+ this.onImage = false;
+ this.onLoadedImage = false;
+ this.onCompletedImage = false;
+ this.imageDescURL = "";
+ this.onCanvas = false;
+ this.onVideo = false;
+ this.onAudio = false;
+ this.onDRMMedia = false;
+ this.onTextInput = false;
+ this.onNumeric = false;
+ this.onKeywordField = false;
+ this.mediaURL = "";
+ this.onLink = false;
+ this.onMailtoLink = false;
+ this.onSaveableLink = false;
+ this.link = null;
+ this.linkURL = "";
+ this.linkURI = null;
+ this.linkTextStr = "";
+ this.linkProtocol = "";
+ this.linkDownload = "";
+ this.linkHasNoReferrer = false;
+ this.onMathML = false;
+ this.inFrame = false;
+ this.inSrcdocFrame = false;
+ this.inSyntheticDoc = false;
+ this.hasBGImage = false;
+ this.bgImageURL = "";
+ this.onEditableArea = false;
+ this.isDesignMode = false;
+ this.onCTPPlugin = false;
+ this.canSpellCheck = false;
+ this.onPassword = false;
+
+ if (this.isRemote) {
+ this.selectionInfo = gContextMenuContentData.selectionInfo;
+ } else {
+ this.selectionInfo = BrowserUtils.getSelectionDetails(window);
+ }
+
+ this.textSelected = this.selectionInfo.text;
+ this.isTextSelected = this.textSelected.length != 0;
+
+ // Remember the node that was clicked.
+ this.target = aNode;
+
+ let ownerDoc = this.target.ownerDocument;
+ this.ownerDoc = ownerDoc;
+
+ // If this is a remote context menu event, use the information from
+ // gContextMenuContentData instead.
+ if (this.isRemote) {
+ this.browser = gContextMenuContentData.browser;
+ this.principal = gContextMenuContentData.principal;
+ this.frameOuterWindowID = gContextMenuContentData.frameOuterWindowID;
+ } else {
+ editFlags = SpellCheckHelper.isEditable(this.target, window);
+ this.browser = ownerDoc.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ this.principal = ownerDoc.nodePrincipal;
+ this.frameOuterWindowID = ownerDoc.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+ }
+ this.onSocial = !!this.browser.getAttribute("origin");
+
+ // Check if we are in a synthetic document (stand alone image, video, etc.).
+ this.inSyntheticDoc = ownerDoc.mozSyntheticDocument;
+ // First, do checks for nodes that never have children.
+ if (this.target.nodeType == Node.ELEMENT_NODE) {
+ // See if the user clicked on an image. This check mirrors
+ // nsDocumentViewer::GetInImage. Make sure to update both if this is
+ // changed.
+ if (this.target instanceof Ci.nsIImageLoadingContent &&
+ this.target.currentURI) {
+ this.onImage = true;
+
+ var request =
+ this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
+ this.onLoadedImage = true;
+ if (request &&
+ (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
+ !(request.imageStatus & request.STATUS_ERROR)) {
+ this.onCompletedImage = true;
+ }
+
+ this.mediaURL = this.target.currentURI.spec;
+
+ var descURL = this.target.getAttribute("longdesc");
+ if (descURL) {
+ this.imageDescURL = makeURLAbsolute(ownerDoc.body.baseURI, descURL);
+ }
+ }
+ else if (this.target instanceof HTMLCanvasElement) {
+ this.onCanvas = true;
+ }
+ else if (this.target instanceof HTMLVideoElement) {
+ let mediaURL = this.target.currentSrc || this.target.src;
+ if (this.isMediaURLReusable(mediaURL)) {
+ this.mediaURL = mediaURL;
+ }
+ if (this._isProprietaryDRM()) {
+ this.onDRMMedia = true;
+ }
+ // Firefox always creates a HTMLVideoElement when loading an ogg file
+ // directly. If the media is actually audio, be smarter and provide a
+ // context menu with audio operations.
+ if (this.target.readyState >= this.target.HAVE_METADATA &&
+ (this.target.videoWidth == 0 || this.target.videoHeight == 0)) {
+ this.onAudio = true;
+ } else {
+ this.onVideo = true;
+ }
+ }
+ else if (this.target instanceof HTMLAudioElement) {
+ this.onAudio = true;
+ let mediaURL = this.target.currentSrc || this.target.src;
+ if (this.isMediaURLReusable(mediaURL)) {
+ this.mediaURL = mediaURL;
+ }
+ if (this._isProprietaryDRM()) {
+ this.onDRMMedia = true;
+ }
+ }
+ else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
+ this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
+ this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
+ this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
+ this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
+ if (this.onEditableArea) {
+ if (this.isRemote) {
+ InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+ }
+ else {
+ InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
+ InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
+ }
+ }
+ this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
+ }
+ else if (this.target instanceof HTMLHtmlElement) {
+ var bodyElt = ownerDoc.body;
+ if (bodyElt) {
+ let computedURL;
+ try {
+ computedURL = this.getComputedURL(bodyElt, "background-image");
+ this._hasMultipleBGImages = false;
+ } catch (e) {
+ this._hasMultipleBGImages = true;
+ }
+ if (computedURL) {
+ this.hasBGImage = true;
+ this.bgImageURL = makeURLAbsolute(bodyElt.baseURI,
+ computedURL);
+ }
+ }
+ }
+ else if ((this.target instanceof HTMLEmbedElement ||
+ this.target instanceof HTMLObjectElement ||
+ this.target instanceof HTMLAppletElement) &&
+ this.target.displayedType == HTMLObjectElement.TYPE_NULL &&
+ this.target.pluginFallbackType == HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
+ this.onCTPPlugin = true;
+ }
+
+ this.canSpellCheck = this._isSpellCheckEnabled(this.target);
+ }
+ else if (this.target.nodeType == Node.TEXT_NODE) {
+ // For text nodes, look at the parent node to determine the spellcheck attribute.
+ this.canSpellCheck = this.target.parentNode &&
+ this._isSpellCheckEnabled(this.target);
+ }
+
+ // Second, bubble out, looking for items of interest that can have childen.
+ // Always pick the innermost link, background image, etc.
+ const XMLNS = "http://www.w3.org/XML/1998/namespace";
+ var elem = this.target;
+ while (elem) {
+ if (elem.nodeType == Node.ELEMENT_NODE) {
+ // Link?
+ if (!this.onLink &&
+ // Be consistent with what hrefAndLinkNodeForClickEvent
+ // does in browser.js
+ (isXULTextLinkLabel(elem) ||
+ (elem instanceof HTMLAnchorElement && elem.href) ||
+ (elem instanceof HTMLAreaElement && elem.href) ||
+ elem instanceof HTMLLinkElement ||
+ elem.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) {
+
+ // Target is a link or a descendant of a link.
+ this.onLink = true;
+
+ // Remember corresponding element.
+ this.link = elem;
+ this.linkURL = this.getLinkURL();
+ this.linkURI = this.getLinkURI();
+ this.linkTextStr = this.getLinkText();
+ this.linkProtocol = this.getLinkProtocol();
+ this.onMailtoLink = (this.linkProtocol == "mailto");
+ this.onSaveableLink = this.isLinkSaveable( this.link );
+ this.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
+ try {
+ if (elem.download) {
+ // Ignore download attribute on cross-origin links
+ this.principal.checkMayLoad(this.linkURI, false, true);
+ this.linkDownload = elem.download;
+ }
+ }
+ catch (ex) {}
+ }
+
+ // Background image? Don't bother if we've already found a
+ // background image further down the hierarchy. Otherwise,
+ // we look for the computed background-image style.
+ if (!this.hasBGImage &&
+ !this._hasMultipleBGImages) {
+ let bgImgUrl;
+ try {
+ bgImgUrl = this.getComputedURL(elem, "background-image");
+ this._hasMultipleBGImages = false;
+ } catch (e) {
+ this._hasMultipleBGImages = true;
+ }
+ if (bgImgUrl) {
+ this.hasBGImage = true;
+ this.bgImageURL = makeURLAbsolute(elem.baseURI,
+ bgImgUrl);
+ }
+ }
+ }
+
+ elem = elem.parentNode;
+ }
+
+ // See if the user clicked on MathML
+ const NS_MathML = "http://www.w3.org/1998/Math/MathML";
+ if ((this.target.nodeType == Node.TEXT_NODE &&
+ this.target.parentNode.namespaceURI == NS_MathML)
+ || (this.target.namespaceURI == NS_MathML))
+ this.onMathML = true;
+
+ // See if the user clicked in a frame.
+ var docDefaultView = ownerDoc.defaultView;
+ if (docDefaultView != docDefaultView.top) {
+ this.inFrame = true;
+
+ if (ownerDoc.isSrcdocDocument) {
+ this.inSrcdocFrame = true;
+ }
+ }
+
+ // if the document is editable, show context menu like in text inputs
+ if (!this.onEditableArea) {
+ if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
+ // If this.onEditableArea is false but editFlags is CONTENTEDITABLE, then
+ // the document itself must be editable.
+ this.onTextInput = true;
+ this.onKeywordField = false;
+ this.onImage = false;
+ this.onLoadedImage = false;
+ this.onCompletedImage = false;
+ this.onMathML = false;
+ this.inFrame = false;
+ this.inSrcdocFrame = false;
+ this.hasBGImage = false;
+ this.isDesignMode = true;
+ this.onEditableArea = true;
+ if (this.isRemote) {
+ InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+ }
+ else {
+ var targetWin = ownerDoc.defaultView;
+ var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
+ InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
+ }
+ var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
+ this.showItem("spell-check-enabled", canSpell);
+ this.showItem("spell-separator", canSpell);
+ }
+ }
+
+ function isXULTextLinkLabel(node) {
+ return node.namespaceURI == xulNS &&
+ node.tagName == "label" &&
+ node.classList.contains('text-link') &&
+ node.href;
+ }
+ },
+
+ // Returns the computed style attribute for the given element.
+ getComputedStyle: function(aElem, aProp) {
+ return aElem.ownerDocument
+ .defaultView
+ .getComputedStyle(aElem, "").getPropertyValue(aProp);
+ },
+
+ // Returns a "url"-type computed style attribute value, with the url() stripped.
+ getComputedURL: function(aElem, aProp) {
+ var url = aElem.ownerDocument
+ .defaultView.getComputedStyle(aElem, "")
+ .getPropertyCSSValue(aProp);
+ if (url instanceof CSSValueList) {
+ if (url.length != 1)
+ throw "found multiple URLs";
+ url = url[0];
+ }
+ return url.primitiveType == CSSPrimitiveValue.CSS_URI ?
+ url.getStringValue() : null;
+ },
+
+ // Returns true if clicked-on link targets a resource that can be saved.
+ isLinkSaveable: function(aLink) {
+ // We don't do the Right Thing for news/snews yet, so turn them off
+ // until we do.
+ return this.linkProtocol && !(
+ this.linkProtocol == "mailto" ||
+ this.linkProtocol == "javascript" ||
+ this.linkProtocol == "news" ||
+ this.linkProtocol == "snews" );
+ },
+
+ _isSpellCheckEnabled: function(aNode) {
+ // We can always force-enable spellchecking on textboxes
+ if (this.isTargetATextBox(aNode)) {
+ return true;
+ }
+ // We can never spell check something which is not content editable
+ var editable = aNode.isContentEditable;
+ if (!editable && aNode.ownerDocument) {
+ editable = aNode.ownerDocument.designMode == "on";
+ }
+ if (!editable) {
+ return false;
+ }
+ // Otherwise make sure that nothing in the parent chain disables spellchecking
+ return aNode.spellcheck;
+ },
+
+ _isProprietaryDRM: function() {
+ return this.target.isEncrypted && this.target.mediaKeys &&
+ this.target.mediaKeys.keySystem != "org.w3.clearkey";
+ },
+
+ _openLinkInParameters : function (extra) {
+ let params = { charset: gContextMenuContentData.charSet,
+ originPrincipal: this.principal,
+ referrerURI: gContextMenuContentData.documentURIObject,
+ referrerPolicy: gContextMenuContentData.referrerPolicy,
+ noReferrer: this.linkHasNoReferrer };
+ for (let p in extra) {
+ params[p] = extra[p];
+ }
+
+ // If we want to change userContextId, we must be sure that we don't
+ // propagate the referrer.
+ if ("userContextId" in params &&
+ params.userContextId != gContextMenuContentData.userContextId) {
+ params.noReferrer = true;
+ }
+
+ return params;
+ },
+
+ // Open linked-to URL in a new window.
+ openLink : function () {
+ urlSecurityCheck(this.linkURL, this.principal);
+ openLinkIn(this.linkURL, "window", this._openLinkInParameters());
+ },
+
+ // Open linked-to URL in a new private window.
+ openLinkInPrivateWindow : function () {
+ urlSecurityCheck(this.linkURL, this.principal);
+ openLinkIn(this.linkURL, "window",
+ this._openLinkInParameters({ private: true }));
+ },
+
+ // Open linked-to URL in a new tab.
+ openLinkInTab: function(event) {
+ urlSecurityCheck(this.linkURL, this.principal);
+ let referrerURI = gContextMenuContentData.documentURIObject;
+
+ // if its parent allows mixed content and the referring URI passes
+ // a same origin check with the target URI, we can preserve the users
+ // decision of disabling MCB on a page for it's child tabs.
+ let persistAllowMixedContentInChildTab = false;
+
+ if (gContextMenuContentData.parentAllowsMixedContent) {
+ const sm = Services.scriptSecurityManager;
+ try {
+ let targetURI = this.linkURI;
+ sm.checkSameOriginURI(referrerURI, targetURI, false);
+ persistAllowMixedContentInChildTab = true;
+ }
+ catch (e) { }
+ }
+
+ let params = {
+ allowMixedContent: persistAllowMixedContentInChildTab,
+ userContextId: parseInt(event.target.getAttribute('data-usercontextid')),
+ };
+
+ openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params));
+ },
+
+ // open URL in current tab
+ openLinkInCurrent: function() {
+ urlSecurityCheck(this.linkURL, this.principal);
+ openLinkIn(this.linkURL, "current", this._openLinkInParameters());
+ },
+
+ // Open frame in a new tab.
+ openFrameInTab: function() {
+ let referrer = gContextMenuContentData.referrer;
+ openLinkIn(gContextMenuContentData.docLocation, "tab",
+ { charset: gContextMenuContentData.charSet,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ // Reload clicked-in frame.
+ reloadFrame: function() {
+ this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadFrame",
+ null, { target: this.target });
+ },
+
+ // Open clicked-in frame in its own window.
+ openFrame: function() {
+ let referrer = gContextMenuContentData.referrer;
+ openLinkIn(gContextMenuContentData.docLocation, "window",
+ { charset: gContextMenuContentData.charSet,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ // Open clicked-in frame in the same window.
+ showOnlyThisFrame: function() {
+ urlSecurityCheck(gContextMenuContentData.docLocation,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ let referrer = gContextMenuContentData.referrer;
+ openUILinkIn(gContextMenuContentData.docLocation, "current",
+ { disallowInheritPrincipal: true,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ reload: function(event) {
+ BrowserReloadOrDuplicate(event);
+ },
+
+ // View Partial Source
+ viewPartialSource: function(aContext) {
+ let inWindow = !Services.prefs.getBoolPref("view_source.tab");
+ let openSelectionFn = inWindow ? null : function() {
+ let tabBrowser = gBrowser;
+ // In the case of popups, we need to find a non-popup browser window.
+ if (!tabBrowser || !window.toolbar.visible) {
+ // This returns only non-popup browser windows by default.
+ let browserWindow = RecentWindow.getMostRecentBrowserWindow();
+ tabBrowser = browserWindow.gBrowser;
+ }
+ let tab = tabBrowser.loadOneTab("about:blank", {
+ relatedToCurrent: true,
+ inBackground: false
+ });
+ return tabBrowser.getBrowserForTab(tab);
+ }
+
+ let target = aContext == "mathml" ? this.target : null;
+ top.gViewSourceUtils.viewPartialSourceInBrowser(gBrowser.selectedBrowser, target, openSelectionFn);
+ },
+
+ // Open new "view source" window with the frame's URL.
+ viewFrameSource: function() {
+ BrowserViewSourceOfDocument({
+ browser: this.browser,
+ URL: gContextMenuContentData.docLocation,
+ outerWindowID: this.frameOuterWindowID,
+ });
+ },
+
+ viewInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, null, null, null, this.browser);
+ },
+
+ viewImageInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab",
+ this.target, null, this.browser);
+ },
+
+ viewImageDesc: function(e) {
+ urlSecurityCheck(this.imageDescURL,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
+ referrerURI: gContextMenuContentData.documentURIObject });
+ },
+
+ viewFrameInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, null, null,
+ this.frameOuterWindowID, this.browser);
+ },
+
+ reloadImage: function() {
+ urlSecurityCheck(this.mediaURL,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+
+ this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadImage",
+ null, { target: this.target });
+ },
+
+ _canvasToBlobURL: function(target) {
+ let mm = this.browser.messageManager;
+ return new Promise(function(resolve) {
+ mm.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL", {}, { target });
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage);
+ resolve(message.data.blobURL);
+ };
+ mm.addMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage);
+ });
+ },
+
+ // Change current window to the URL of the image, video, or audio.
+ viewMedia: function(e) {
+ let referrerURI = gContextMenuContentData.documentURIObject;
+ if (this.onCanvas) {
+ this._canvasToBlobURL(this.target).then(function(blobURL) {
+ openUILink(blobURL, e, { disallowInheritPrincipal: true,
+ referrerURI: referrerURI });
+ }, Cu.reportError);
+ }
+ else {
+ urlSecurityCheck(this.mediaURL,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ openUILink(this.mediaURL, e, { disallowInheritPrincipal: true,
+ referrerURI: referrerURI });
+ }
+ },
+
+ saveVideoFrameAsImage: function () {
+ let mm = this.browser.messageManager;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+
+ let name = "";
+ if (this.mediaURL) {
+ try {
+ let uri = makeURI(this.mediaURL);
+ let url = uri.QueryInterface(Ci.nsIURL);
+ if (url.fileBaseName)
+ name = decodeURI(url.fileBaseName) + ".jpg";
+ } catch (e) { }
+ }
+ if (!name)
+ name = "snapshot.jpg";
+
+ mm.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage", {}, {
+ target: this.target,
+ });
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
+ let dataURL = message.data.dataURL;
+ saveImageURL(dataURL, name, "SaveImageTitle", true, false,
+ document.documentURIObject, null, null, null,
+ isPrivate);
+ };
+ mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
+ },
+
+ leaveDOMFullScreen: function() {
+ document.exitFullscreen();
+ },
+
+ // Change current window to the URL of the background image.
+ viewBGImage: function(e) {
+ urlSecurityCheck(this.bgImageURL,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
+ referrerURI: gContextMenuContentData.documentURIObject });
+ },
+
+ setDesktopBackground: function() {
+ let mm = this.browser.messageManager;
+
+ mm.sendAsyncMessage("ContextMenu:SetAsDesktopBackground", null,
+ { target: this.target });
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:SetAsDesktopBackground:Result",
+ onMessage);
+
+ if (message.data.disable)
+ return;
+
+ let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
+ image.src = message.data.dataUrl;
+
+ // Confirm since it's annoying if you hit this accidentally.
+ const kDesktopBackgroundURL =
+ "chrome://browser/content/setDesktopBackground.xul";
+#ifdef XP_MACOSX
+ // On Mac, the Set Desktop Background window is not modal.
+ // Don't open more than one Set Desktop Background window.
+ const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ let dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground");
+ if (dbWin) {
+ dbWin.gSetBackground.init(image);
+ dbWin.focus();
+ }
+ else {
+ openDialog(kDesktopBackgroundURL, "",
+ "centerscreen,chrome,dialog=no,dependent,resizable=no",
+ image);
+ }
+#else
+ // On non-Mac platforms, the Set Wallpaper dialog is modal.
+ openDialog(kDesktopBackgroundURL, "",
+ "centerscreen,chrome,dialog,modal,dependent",
+ image);
+#endif
+ };
+
+ mm.addMessageListener("ContextMenu:SetAsDesktopBackground:Result", onMessage);
+ },
+
+ // Save URL of clicked-on frame.
+ saveFrame: function () {
+ saveBrowser(this.browser, false, this.frameOuterWindowID);
+ },
+
+ // Helper function to wait for appropriate MIME-type headers and
+ // then prompt the user with a file picker
+ saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc, docURI,
+ windowID, linkDownload) {
+ // canonical def in nsURILoader.h
+ const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
+
+ // an object to proxy the data through to
+ // nsIExternalHelperAppService.doContent, which will wait for the
+ // appropriate MIME-type headers and then prompt the user with a
+ // file picker
+ function saveAsListener() {}
+ saveAsListener.prototype = {
+ extListener: null,
+
+ onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) {
+
+ // if the timer fired, the error status will have been caused by that,
+ // and we'll be restarting in onStopRequest, so no reason to notify
+ // the user
+ if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT)
+ return;
+
+ timer.cancel();
+
+ // some other error occured; notify the user...
+ if (!Components.isSuccessCode(aRequest.status)) {
+ try {
+ const sbs = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ const bundle = sbs.createBundle(
+ "chrome://mozapps/locale/downloads/downloads.properties");
+
+ const title = bundle.GetStringFromName("downloadErrorAlertTitle");
+ const msg = bundle.GetStringFromName("downloadErrorGeneric");
+
+ const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService);
+ const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ let window = wm.getOuterWindowWithId(windowID);
+ promptSvc.alert(window, title, msg);
+ } catch (ex) {}
+ return;
+ }
+
+ let extHelperAppSvc =
+ Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
+ getService(Ci.nsIExternalHelperAppService);
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ this.extListener =
+ extHelperAppSvc.doContent(channel.contentType, aRequest,
+ null, true, window);
+ this.extListener.onStartRequest(aRequest, aContext);
+ },
+
+ onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext,
+ aStatusCode) {
+ if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
+ // do it the old fashioned way, which will pick the best filename
+ // it can without waiting.
+ saveURL(linkURL, linkText, dialogTitle, bypassCache, false, docURI,
+ doc);
+ }
+ if (this.extListener)
+ this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
+ },
+
+ onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
+ aInputStream,
+ aOffset, aCount) {
+ this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
+ aOffset, aCount);
+ }
+ }
+
+ function callbacks() {}
+ callbacks.prototype = {
+ getInterface: function sLA_callbacks_getInterface(aIID) {
+ if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
+ // If the channel demands authentication prompt, we must cancel it
+ // because the save-as-timer would expire and cancel the channel
+ // before we get credentials from user. Both authentication dialog
+ // and save as dialog would appear on the screen as we fall back to
+ // the old fashioned way after the timeout.
+ timer.cancel();
+ channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ }
+
+ // if it we don't have the headers after a short time, the user
+ // won't have received any feedback from their click. that's bad. so
+ // we give up waiting for the filename.
+ function timerCallback() {}
+ timerCallback.prototype = {
+ notify: function sLA_timer_notify(aTimer) {
+ channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
+ return;
+ }
+ }
+
+ // setting up a new channel for 'right click - save link as ...'
+ // ideally we should use:
+ // * doc - as the loadingNode, and/or
+ // * this.principal - as the loadingPrincipal
+ // for now lets use systemPrincipal to bypass mixedContentBlocker
+ // checks after redirects, see bug: 1136055
+ var channel = NetUtil.newChannel({
+ uri: makeURI(linkURL),
+ loadUsingSystemPrincipal: true
+ });
+
+ if (linkDownload)
+ channel.contentDispositionFilename = linkDownload;
+ if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
+ let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser);
+ channel.setPrivate(docIsPrivate);
+ }
+ channel.notificationCallbacks = new callbacks();
+
+ let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
+
+ if (bypassCache)
+ flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+
+ if (channel instanceof Ci.nsICachingChannel)
+ flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
+
+ channel.loadFlags |= flags;
+
+ if (channel instanceof Ci.nsIHttpChannel) {
+ channel.referrer = docURI;
+ if (channel instanceof Ci.nsIHttpChannelInternal)
+ channel.forceAllowThirdPartyCookie = true;
+ }
+
+ // fallback to the old way if we don't see the headers quickly
+ var timeToWait =
+ gPrefService.getIntPref("browser.download.saveLinkAsFilenameTimeout");
+ var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(new timerCallback(), timeToWait,
+ timer.TYPE_ONE_SHOT);
+
+ // kick off the channel with our proxy object as the listener
+ channel.asyncOpen2(new saveAsListener());
+ },
+
+ // Save URL of clicked-on link.
+ saveLink: function() {
+ urlSecurityCheck(this.linkURL, this.principal);
+ this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc,
+ gContextMenuContentData.documentURIObject,
+ this.frameOuterWindowID,
+ this.linkDownload);
+ },
+
+ // Backwards-compatibility wrapper
+ saveImage : function() {
+ if (this.onCanvas || this.onImage)
+ this.saveMedia();
+ },
+
+ // Save URL of the clicked upon image, video, or audio.
+ saveMedia: function() {
+ let doc = this.ownerDoc;
+ let referrerURI = gContextMenuContentData.documentURIObject;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ if (this.onCanvas) {
+ // Bypass cache, since it's a data: URL.
+ this._canvasToBlobURL(this.target).then(function(blobURL) {
+ saveImageURL(blobURL, "canvas.png", "SaveImageTitle",
+ true, false, referrerURI, null, null, null,
+ isPrivate);
+ }, Cu.reportError);
+ }
+ else if (this.onImage) {
+ urlSecurityCheck(this.mediaURL, this.principal);
+ saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
+ false, referrerURI, null, gContextMenuContentData.contentType,
+ gContextMenuContentData.contentDisposition, isPrivate);
+ }
+ else if (this.onVideo || this.onAudio) {
+ urlSecurityCheck(this.mediaURL, this.principal);
+ var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
+ this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
+ this.frameOuterWindowID, "");
+ }
+ },
+
+ // Backwards-compatibility wrapper
+ sendImage : function() {
+ if (this.onCanvas || this.onImage)
+ this.sendMedia();
+ },
+
+ sendMedia: function() {
+ MailIntegration.sendMessage(this.mediaURL, "");
+ },
+
+ castVideo: function() {
+ CastingApps.openExternal(this.target, window);
+ },
+
+ populateCastVideoMenu: function(popup) {
+ let videoEl = this.target;
+ popup.innerHTML = null;
+ let doc = popup.ownerDocument;
+ let services = CastingApps.getServicesForVideo(videoEl);
+ services.forEach(service => {
+ let item = doc.createElement("menuitem");
+ item.setAttribute("label", service.friendlyName);
+ item.addEventListener("command", event => {
+ CastingApps.sendVideoToService(videoEl, service);
+ });
+ popup.appendChild(item);
+ });
+ },
+
+ playPlugin: function() {
+ gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
+ },
+
+ hidePlugin: function() {
+ gPluginHandler.contextMenuCommand(this.browser, this.target, "hide");
+ },
+
+ // Generate email address and put it on clipboard.
+ copyEmail: function() {
+ // Copy the comma-separated list of email addresses only.
+ // There are other ways of embedding email addresses in a mailto:
+ // link, but such complex parsing is beyond us.
+ var url = this.linkURL;
+ var qmark = url.indexOf("?");
+ var addresses;
+
+ // 7 == length of "mailto:"
+ addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7);
+
+ // Let's try to unescape it using a character set
+ // in case the address is not ASCII.
+ try {
+ const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
+ getService(Ci.nsITextToSubURI);
+ addresses = textToSubURI.unEscapeURIForUI(gContextMenuContentData.charSet,
+ addresses);
+ }
+ catch(ex) {
+ // Do nothing.
+ }
+
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(addresses);
+ },
+
+ copyLink: function() {
+ // If we're in a view source tab, remove the view-source: prefix
+ let linkURL = this.linkURL.replace(/^view-source:/, "");
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(linkURL);
+ },
+
+ ///////////////
+ // Utilities //
+ ///////////////
+
+ // Show/hide one item (specified via name or the item element itself).
+ showItem: function(aItemOrId, aShow) {
+ var item = aItemOrId.constructor == String ?
+ document.getElementById(aItemOrId) : aItemOrId;
+ if (item)
+ item.hidden = !aShow;
+ },
+
+ // Set given attribute of specified context-menu item. If the
+ // value is null, then it removes the attribute (which works
+ // nicely for the disabled attribute).
+ setItemAttr: function(aID, aAttr, aVal ) {
+ var elem = document.getElementById(aID);
+ if (elem) {
+ if (aVal == null) {
+ // null indicates attr should be removed.
+ elem.removeAttribute(aAttr);
+ }
+ else {
+ // Set attr=val.
+ elem.setAttribute(aAttr, aVal);
+ }
+ }
+ },
+
+ // Set context menu attribute according to like attribute of another node
+ // (such as a broadcaster).
+ setItemAttrFromNode: function(aItem_id, aAttr, aOther_id) {
+ var elem = document.getElementById(aOther_id);
+ if (elem && elem.getAttribute(aAttr) == "true")
+ this.setItemAttr(aItem_id, aAttr, "true");
+ else
+ this.setItemAttr(aItem_id, aAttr, null);
+ },
+
+ // Temporary workaround for DOM api not yet implemented by XUL nodes.
+ cloneNode: function(aItem) {
+ // Create another element like the one we're cloning.
+ var node = document.createElement(aItem.tagName);
+
+ // Copy attributes from argument item to the new one.
+ var attrs = aItem.attributes;
+ for (var i = 0; i < attrs.length; i++) {
+ var attr = attrs.item(i);
+ node.setAttribute(attr.nodeName, attr.nodeValue);
+ }
+
+ // Voila!
+ return node;
+ },
+
+ // Generate fully qualified URL for clicked-on link.
+ getLinkURL: function() {
+ var href = this.link.href;
+ if (href)
+ return href;
+
+ href = this.link.getAttribute("href") ||
+ this.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
+
+ if (!href || !href.match(/\S/)) {
+ // Without this we try to save as the current doc,
+ // for example, HTML case also throws if empty
+ throw "Empty href";
+ }
+
+ return makeURLAbsolute(this.link.baseURI, href);
+ },
+
+ getLinkURI: function() {
+ try {
+ return makeURI(this.linkURL);
+ }
+ catch (ex) {
+ // e.g. empty URL string
+ }
+
+ return null;
+ },
+
+ getLinkProtocol: function() {
+ if (this.linkURI)
+ return this.linkURI.scheme; // can be |undefined|
+
+ return null;
+ },
+
+ // Get text of link.
+ getLinkText: function() {
+ var text = gatherTextUnder(this.link);
+ if (!text || !text.match(/\S/)) {
+ text = this.link.getAttribute("title");
+ if (!text || !text.match(/\S/)) {
+ text = this.link.getAttribute("alt");
+ if (!text || !text.match(/\S/))
+ text = this.linkURL;
+ }
+ }
+
+ return text;
+ },
+
+ // Kept for addon compat
+ linkText: function() {
+ return this.linkTextStr;
+ },
+
+ isMediaURLReusable: function(aURL) {
+ if (aURL.startsWith("blob:")) {
+ return URL.isValidURL(aURL);
+ }
+ return true;
+ },
+
+ toString: function () {
+ return "contextMenu.target = " + this.target + "\n" +
+ "contextMenu.onImage = " + this.onImage + "\n" +
+ "contextMenu.onLink = " + this.onLink + "\n" +
+ "contextMenu.link = " + this.link + "\n" +
+ "contextMenu.inFrame = " + this.inFrame + "\n" +
+ "contextMenu.hasBGImage = " + this.hasBGImage + "\n";
+ },
+
+ isTargetATextBox: function(node) {
+ if (node instanceof HTMLInputElement)
+ return node.mozIsTextField(false);
+
+ return (node instanceof HTMLTextAreaElement);
+ },
+
+ // Determines whether or not the separator with the specified ID should be
+ // shown or not by determining if there are any non-hidden items between it
+ // and the previous separator.
+ shouldShowSeparator: function (aSeparatorID) {
+ var separator = document.getElementById(aSeparatorID);
+ if (separator) {
+ var sibling = separator.previousSibling;
+ while (sibling && sibling.localName != "menuseparator") {
+ if (!sibling.hidden)
+ return true;
+ sibling = sibling.previousSibling;
+ }
+ }
+ return false;
+ },
+
+ addDictionaries: function() {
+ var uri = formatURL("browser.dictionaries.download.url", true);
+
+ var locale = "-";
+ try {
+ locale = gPrefService.getComplexValue("intl.accept_languages",
+ Ci.nsIPrefLocalizedString).data;
+ }
+ catch (e) { }
+
+ var version = "-";
+ try {
+ version = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo).version;
+ }
+ catch (e) { }
+
+ uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
+
+ var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow");
+ var where = newWindowPref == 3 ? "tab" : "window";
+
+ openUILinkIn(uri, where);
+ },
+
+ bookmarkThisPage: function CM_bookmarkThisPage() {
+ window.top.PlacesCommandHook.bookmarkPage(this.browser, PlacesUtils.bookmarksMenuFolderId, true);
+ },
+
+ bookmarkLink: function CM_bookmarkLink() {
+ window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
+ this.linkURL, this.linkTextStr);
+ },
+
+ addBookmarkForFrame: function CM_addBookmarkForFrame() {
+ let uri = gContextMenuContentData.documentURIObject;
+ let mm = this.browser.messageManager;
+
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);
+
+ window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
+ uri.spec,
+ message.data.title,
+ message.data.description)
+ .catch(Components.utils.reportError);
+ };
+ mm.addMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);
+
+ mm.sendAsyncMessage("ContextMenu:BookmarkFrame", null, { target: this.target });
+ },
+
+ shareLink: function CM_shareLink() {
+ SocialShare.sharePage(null, { url: this.linkURI.spec }, this.target);
+ },
+
+ shareImage: function CM_shareImage() {
+ SocialShare.sharePage(null, { url: this.imageURL, previews: [ this.mediaURL ] }, this.target);
+ },
+
+ shareVideo: function CM_shareVideo() {
+ SocialShare.sharePage(null, { url: this.mediaURL, source: this.mediaURL }, this.target);
+ },
+
+ shareSelect: function CM_shareSelect() {
+ SocialShare.sharePage(null, { url: this.browser.currentURI.spec, text: this.textSelected }, this.target);
+ },
+
+ savePageAs: function CM_savePageAs() {
+ saveBrowser(this.browser);
+ },
+
+ printFrame: function CM_printFrame() {
+ PrintUtils.printWindow(this.frameOuterWindowID, this.browser);
+ },
+
+ switchPageDirection: function CM_switchPageDirection() {
+ this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection");
+ },
+
+ mediaCommand : function CM_mediaCommand(command, data) {
+ let mm = this.browser.messageManager;
+ let win = this.browser.ownerGlobal;
+ let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ mm.sendAsyncMessage("ContextMenu:MediaCommand",
+ {command: command,
+ data: data,
+ handlingUserInput: windowUtils.isHandlingUserInput},
+ {element: this.target});
+ },
+
+ copyMediaLocation : function () {
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(this.mediaURL);
+ },
+
+ drmLearnMore: function(aEvent) {
+ let drmInfoURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content";
+ let dest = whereToOpenLink(aEvent);
+ // Don't ever want this to open in the same tab as it'll unload the
+ // DRM'd video, which is going to be a bad idea in most cases.
+ if (dest == "current") {
+ dest = "tab";
+ }
+ openUILinkIn(drmInfoURL, dest);
+ },
+
+ get imageURL() {
+ if (this.onImage)
+ return this.mediaURL;
+ return "";
+ },
+
+ // Formats the 'Search <engine> for "<selection or link text>"' context menu.
+ formatSearchContextItem: function() {
+ var menuItem = document.getElementById("context-searchselect");
+ let selectedText = this.isTextSelected ? this.textSelected : this.linkTextStr;
+
+ // Store searchTerms in context menu item so we know what to search onclick
+ menuItem.searchTerms = selectedText;
+
+ // Copied to alert.js' prefillAlertInfo().
+ // If the JS character after our truncation point is a trail surrogate,
+ // include it in the truncated string to avoid splitting a surrogate pair.
+ if (selectedText.length > 15) {
+ let truncLength = 15;
+ let truncChar = selectedText[15].charCodeAt(0);
+ if (truncChar >= 0xDC00 && truncChar <= 0xDFFF)
+ truncLength++;
+ selectedText = selectedText.substr(0,truncLength) + this.ellipsis;
+ }
+
+ // format "Search <engine> for <selection>" string to show in menu
+ let engineName = Services.search.currentEngine.name;
+ var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch",
+ [engineName,
+ selectedText]);
+ menuItem.label = menuLabel;
+ menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey");
+ },
+
+ _getTelemetryClickInfo: function(aXulMenu) {
+ this._onPopupHiding = () => {
+ aXulMenu.ownerDocument.removeEventListener("command", activationHandler, true);
+ aXulMenu.removeEventListener("popuphiding", this._onPopupHiding, true);
+ delete this._onPopupHiding;
+
+ let eventKey = [
+ this._telemetryPageContext,
+ this._telemetryHadCustomItems ? "withcustom" : "withoutcustom"
+ ];
+ let target = this._telemetryClickID || "close-without-interaction";
+ BrowserUITelemetry.registerContextMenuInteraction(eventKey, target);
+ };
+ let activationHandler = (e) => {
+ // Deal with command events being routed to command elements; figure out
+ // what triggered the event (which will have the right e.target)
+ if (e.sourceEvent) {
+ e = e.sourceEvent;
+ }
+ // Target should be in the menu (this catches using shortcuts for items
+ // not in the menu while the menu is up)
+ if (!aXulMenu.contains(e.target)) {
+ return;
+ }
+
+ // Check if this is a page menu item:
+ if (e.target.hasAttribute(PageMenuParent.GENERATEDITEMID_ATTR)) {
+ this._telemetryClickID = "custom-page-item";
+ } else {
+ this._telemetryClickID = (e.target.id || "unknown").replace(/^context-/i, "");
+ }
+ };
+ aXulMenu.ownerDocument.addEventListener("command", activationHandler, true);
+ aXulMenu.addEventListener("popuphiding", this._onPopupHiding, true);
+ },
+
+ _getTelemetryPageContextInfo: function() {
+ let rv = [];
+ for (let k of ["isContentSelected", "onLink", "onImage", "onCanvas", "onVideo", "onAudio",
+ "onTextInput", "onSocial"]) {
+ if (this[k]) {
+ rv.push(k.replace(/^(?:is|on)(.)/, (match, firstLetter) => firstLetter.toLowerCase()));
+ }
+ }
+ if (!rv.length) {
+ rv.push('other');
+ }
+
+ return JSON.stringify(rv);
+ },
+
+ _checkTelemetryForMenu: function(aXulMenu) {
+ this._telemetryClickID = null;
+ this._telemetryPageContext = this._getTelemetryPageContextInfo();
+ this._telemetryHadCustomItems = this.hasPageMenu;
+ this._getTelemetryClickInfo(aXulMenu);
+ },
+
+ createContainerMenu: function(aEvent) {
+ return createUserContextMenu(aEvent, true,
+ gContextMenuContentData.userContextId);
+ },
+};
diff --git a/browser/base/content/overrides/app-license.html b/browser/base/content/overrides/app-license.html
new file mode 100644
index 000000000..e7a158c79
--- /dev/null
+++ b/browser/base/content/overrides/app-license.html
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+ <p><b>Binaries</b> of this product have been made available to you by the
+ <a href="http://www.mozilla.org/">Mozilla Project</a> under the Mozilla
+ Public License 2.0 (MPL). <a href="about:rights">Know your rights</a>.</p>
diff --git a/browser/base/content/pageinfo/feeds.js b/browser/base/content/pageinfo/feeds.js
new file mode 100644
index 000000000..c9731b4ef
--- /dev/null
+++ b/browser/base/content/pageinfo/feeds.js
@@ -0,0 +1,32 @@
+/* -*- 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/. */
+
+function initFeedTab(feeds)
+{
+ for (let feed of feeds) {
+ let [name, type, url] = feed;
+ addRow(name, type, url);
+ }
+
+ var feedListbox = document.getElementById("feedListbox");
+ document.getElementById("feedTab").hidden = feedListbox.getRowCount() == 0;
+}
+
+function onSubscribeFeed()
+{
+ var listbox = document.getElementById("feedListbox");
+ openUILinkIn(listbox.selectedItem.getAttribute("feedURL"), "current",
+ { ignoreAlt: true });
+}
+
+function addRow(name, type, url)
+{
+ var item = document.createElement("richlistitem");
+ item.setAttribute("feed", "true");
+ item.setAttribute("name", name);
+ item.setAttribute("type", type);
+ item.setAttribute("feedURL", url);
+ document.getElementById("feedListbox").appendChild(item);
+}
diff --git a/browser/base/content/pageinfo/feeds.xml b/browser/base/content/pageinfo/feeds.xml
new file mode 100644
index 000000000..782c05a73
--- /dev/null
+++ b/browser/base/content/pageinfo/feeds.xml
@@ -0,0 +1,40 @@
+<?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 % pageInfoDTD SYSTEM "chrome://browser/locale/pageInfo.dtd">
+ %pageInfoDTD;
+]>
+
+<bindings id="feedBindings"
+ 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="feed" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:vbox flex="1">
+ <xul:hbox flex="1">
+ <xul:textbox flex="1" readonly="true" xbl:inherits="value=name"
+ class="feedTitle"/>
+ <xul:label xbl:inherits="value=type"/>
+ </xul:hbox>
+ <xul:vbox>
+ <xul:vbox align="start">
+ <xul:hbox>
+ <xul:label xbl:inherits="value=feedURL,tooltiptext=feedURL" class="text-link" flex="1"
+ onclick="openUILink(this.value, event);" crop="end"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:vbox>
+ <xul:hbox flex="1" class="feed-subscribe">
+ <xul:spacer flex="1"/>
+ <xul:button label="&feedSubscribe;" accesskey="&feedSubscribe.accesskey;"
+ oncommand="onSubscribeFeed()"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ </binding>
+</bindings>
diff --git a/browser/base/content/pageinfo/pageInfo.css b/browser/base/content/pageinfo/pageInfo.css
new file mode 100644
index 000000000..622b56bb5
--- /dev/null
+++ b/browser/base/content/pageinfo/pageInfo.css
@@ -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/. */
+
+
+#viewGroup > radio {
+ -moz-binding: url("chrome://browser/content/pageinfo/pageInfo.xml#viewbutton");
+}
+
+richlistitem[feed] {
+ -moz-binding: url("chrome://browser/content/pageinfo/feeds.xml#feed");
+}
+
+richlistitem[feed]:not([selected="true"]) .feed-subscribe {
+ display: none;
+}
+
+groupbox[closed="true"] > .groupbox-body {
+ visibility: collapse;
+}
+
+#thepreviewimage {
+ display: block;
+/* This following entry can be removed when Bug 522850 is fixed. */
+ min-width: 1px;
+}
diff --git a/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js
new file mode 100644
index 000000000..7a6d0a063
--- /dev/null
+++ b/browser/base/content/pageinfo/pageInfo.js
@@ -0,0 +1,1140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/LoadContextInfo.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// define a js object to implement nsITreeView
+function pageInfoTreeView(treeid, copycol)
+{
+ // copycol is the index number for the column that we want to add to
+ // the copy-n-paste buffer when the user hits accel-c
+ this.treeid = treeid;
+ this.copycol = copycol;
+ this.rows = 0;
+ this.tree = null;
+ this.data = [ ];
+ this.selection = null;
+ this.sortcol = -1;
+ this.sortdir = false;
+}
+
+pageInfoTreeView.prototype = {
+ set rowCount(c) { throw "rowCount is a readonly property"; },
+ get rowCount() { return this.rows; },
+
+ setTree: function(tree)
+ {
+ this.tree = tree;
+ },
+
+ getCellText: function(row, column)
+ {
+ // row can be null, but js arrays are 0-indexed.
+ // colidx cannot be null, but can be larger than the number
+ // of columns in the array. In this case it's the fault of
+ // whoever typoed while calling this function.
+ return this.data[row][column.index] || "";
+ },
+
+ setCellValue: function(row, column, value)
+ {
+ },
+
+ setCellText: function(row, column, value)
+ {
+ this.data[row][column.index] = value;
+ },
+
+ addRow: function(row)
+ {
+ this.rows = this.data.push(row);
+ this.rowCountChanged(this.rows - 1, 1);
+ if (this.selection.count == 0 && this.rowCount && !gImageElement) {
+ this.selection.select(0);
+ }
+ },
+
+ addRows: function(rows)
+ {
+ for (let row of rows) {
+ this.addRow(row);
+ }
+ },
+
+ rowCountChanged: function(index, count)
+ {
+ this.tree.rowCountChanged(index, count);
+ },
+
+ invalidate: function()
+ {
+ this.tree.invalidate();
+ },
+
+ clear: function()
+ {
+ if (this.tree)
+ this.tree.rowCountChanged(0, -this.rows);
+ this.rows = 0;
+ this.data = [ ];
+ },
+
+ handleCopy: function(row)
+ {
+ return (row < 0 || this.copycol < 0) ? "" : (this.data[row][this.copycol] || "");
+ },
+
+ performActionOnRow: function(action, row)
+ {
+ if (action == "copy") {
+ var data = this.handleCopy(row)
+ this.tree.treeBody.parentNode.setAttribute("copybuffer", data);
+ }
+ },
+
+ onPageMediaSort : function(columnname)
+ {
+ var tree = document.getElementById(this.treeid);
+ var treecol = tree.columns.getNamedColumn(columnname);
+
+ this.sortdir =
+ gTreeUtils.sort(
+ tree,
+ this,
+ this.data,
+ treecol.index,
+ function textComparator(a, b) { return (a || "").toLowerCase().localeCompare((b || "").toLowerCase()); },
+ this.sortcol,
+ this.sortdir
+ );
+
+ Array.forEach(tree.columns, function(col) {
+ col.element.removeAttribute("sortActive");
+ col.element.removeAttribute("sortDirection");
+ });
+ treecol.element.setAttribute("sortActive", "true");
+ treecol.element.setAttribute("sortDirection", this.sortdir ?
+ "ascending" : "descending");
+
+ this.sortcol = treecol.index;
+ },
+
+ getRowProperties: function(row) { return ""; },
+ getCellProperties: function(row, column) { return ""; },
+ getColumnProperties: function(column) { return ""; },
+ isContainer: function(index) { return false; },
+ isContainerOpen: function(index) { return false; },
+ isSeparator: function(index) { return false; },
+ isSorted: function() { return this.sortcol > -1 },
+ canDrop: function(index, orientation) { return false; },
+ drop: function(row, orientation) { return false; },
+ getParentIndex: function(index) { return 0; },
+ hasNextSibling: function(index, after) { return false; },
+ getLevel: function(index) { return 0; },
+ getImageSrc: function(row, column) { },
+ getProgressMode: function(row, column) { },
+ getCellValue: function(row, column) { },
+ toggleOpenState: function(index) { },
+ cycleHeader: function(col) { },
+ selectionChanged: function() { },
+ cycleCell: function(row, column) { },
+ isEditable: function(row, column) { return false; },
+ isSelectable: function(row, column) { return false; },
+ performAction: function(action) { },
+ performActionOnCell: function(action, row, column) { }
+};
+
+// mmm, yummy. global variables.
+var gDocInfo = null;
+var gImageElement = null;
+
+// column number to help using the data array
+const COL_IMAGE_ADDRESS = 0;
+const COL_IMAGE_TYPE = 1;
+const COL_IMAGE_SIZE = 2;
+const COL_IMAGE_ALT = 3;
+const COL_IMAGE_COUNT = 4;
+const COL_IMAGE_NODE = 5;
+const COL_IMAGE_BG = 6;
+
+// column number to copy from, second argument to pageInfoTreeView's constructor
+const COPYCOL_NONE = -1;
+const COPYCOL_META_CONTENT = 1;
+const COPYCOL_IMAGE = COL_IMAGE_ADDRESS;
+
+// one nsITreeView for each tree in the window
+var gMetaView = new pageInfoTreeView('metatree', COPYCOL_META_CONTENT);
+var gImageView = new pageInfoTreeView('imagetree', COPYCOL_IMAGE);
+
+gImageView.getCellProperties = function(row, col) {
+ var data = gImageView.data[row];
+ var item = gImageView.data[row][COL_IMAGE_NODE];
+ var props = "";
+ if (!checkProtocol(data) ||
+ item instanceof HTMLEmbedElement ||
+ (item instanceof HTMLObjectElement && !item.type.startsWith("image/")))
+ props += "broken";
+
+ if (col.element.id == "image-address")
+ props += " ltr";
+
+ return props;
+};
+
+gImageView.getCellText = function(row, column) {
+ var value = this.data[row][column.index];
+ if (column.index == COL_IMAGE_SIZE) {
+ if (value == -1) {
+ return gStrings.unknown;
+ }
+ var kbSize = Number(Math.round(value / 1024 * 100) / 100);
+ return gBundle.getFormattedString("mediaFileSize", [kbSize]);
+ }
+ return value || "";
+};
+
+gImageView.onPageMediaSort = function(columnname) {
+ var tree = document.getElementById(this.treeid);
+ var treecol = tree.columns.getNamedColumn(columnname);
+
+ var comparator;
+ var index = treecol.index;
+ if (index == COL_IMAGE_SIZE || index == COL_IMAGE_COUNT) {
+ comparator = function numComparator(a, b) { return a - b; };
+ } else {
+ comparator = function textComparator(a, b) { return (a || "").toLowerCase().localeCompare((b || "").toLowerCase()); };
+ }
+
+ this.sortdir =
+ gTreeUtils.sort(
+ tree,
+ this,
+ this.data,
+ index,
+ comparator,
+ this.sortcol,
+ this.sortdir
+ );
+
+ Array.forEach(tree.columns, function(col) {
+ col.element.removeAttribute("sortActive");
+ col.element.removeAttribute("sortDirection");
+ });
+ treecol.element.setAttribute("sortActive", "true");
+ treecol.element.setAttribute("sortDirection", this.sortdir ?
+ "ascending" : "descending");
+
+ this.sortcol = index;
+};
+
+var gImageHash = { };
+
+// localized strings (will be filled in when the document is loaded)
+// this isn't all of them, these are just the ones that would otherwise have been loaded inside a loop
+var gStrings = { };
+var gBundle;
+
+const PERMISSION_CONTRACTID = "@mozilla.org/permissionmanager;1";
+const PREFERENCES_CONTRACTID = "@mozilla.org/preferences-service;1";
+const ATOM_CONTRACTID = "@mozilla.org/atom-service;1";
+
+// a number of services I'll need later
+// the cache services
+const nsICacheStorageService = Components.interfaces.nsICacheStorageService;
+const nsICacheStorage = Components.interfaces.nsICacheStorage;
+const cacheService = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"].getService(nsICacheStorageService);
+
+var loadContextInfo = LoadContextInfo.fromLoadContext(
+ window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsILoadContext), false);
+var diskStorage = cacheService.diskCacheStorage(loadContextInfo, false);
+
+const nsICookiePermission = Components.interfaces.nsICookiePermission;
+const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
+
+const nsICertificateDialogs = Components.interfaces.nsICertificateDialogs;
+const CERTIFICATEDIALOGS_CONTRACTID = "@mozilla.org/nsCertificateDialogs;1"
+
+// clipboard helper
+function getClipboardHelper() {
+ try {
+ return Components.classes["@mozilla.org/widget/clipboardhelper;1"].getService(Components.interfaces.nsIClipboardHelper);
+ } catch (e) {
+ // do nothing, later code will handle the error
+ return null;
+ }
+}
+const gClipboardHelper = getClipboardHelper();
+
+// Interface for image loading content
+const nsIImageLoadingContent = Components.interfaces.nsIImageLoadingContent;
+
+// namespaces, don't need all of these yet...
+const XLinkNS = "http://www.w3.org/1999/xlink";
+const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const XMLNS = "http://www.w3.org/XML/1998/namespace";
+const XHTMLNS = "http://www.w3.org/1999/xhtml";
+const XHTML2NS = "http://www.w3.org/2002/06/xhtml2"
+
+const XHTMLNSre = "^http\:\/\/www\.w3\.org\/1999\/xhtml$";
+const XHTML2NSre = "^http\:\/\/www\.w3\.org\/2002\/06\/xhtml2$";
+const XHTMLre = RegExp(XHTMLNSre + "|" + XHTML2NSre, "");
+
+/* Overlays register functions here.
+ * These arrays are used to hold callbacks that Page Info will call at
+ * various stages. Use them by simply appending a function to them.
+ * For example, add a function to onLoadRegistry by invoking
+ * "onLoadRegistry.push(XXXLoadFunc);"
+ * The XXXLoadFunc should be unique to the overlay module, and will be
+ * invoked as "XXXLoadFunc();"
+ */
+
+// These functions are called to build the data displayed in the Page Info window.
+var onLoadRegistry = [ ];
+
+// These functions are called to remove old data still displayed in
+// the window when the document whose information is displayed
+// changes. For example, at this time, the list of images of the Media
+// tab is cleared.
+var onResetRegistry = [ ];
+
+// These functions are called once when all the elements in all of the target
+// document (and all of its subframes, if any) have been processed
+var onFinished = [ ];
+
+// These functions are called once when the Page Info window is closed.
+var onUnloadRegistry = [ ];
+
+/* Called when PageInfo window is loaded. Arguments are:
+ * window.arguments[0] - (optional) an object consisting of
+ * - doc: (optional) document to use for source. if not provided,
+ * the calling window's document will be used
+ * - initialTab: (optional) id of the inital tab to display
+ */
+function onLoadPageInfo()
+{
+ gBundle = document.getElementById("pageinfobundle");
+ gStrings.unknown = gBundle.getString("unknown");
+ gStrings.notSet = gBundle.getString("notset");
+ gStrings.mediaImg = gBundle.getString("mediaImg");
+ gStrings.mediaBGImg = gBundle.getString("mediaBGImg");
+ gStrings.mediaBorderImg = gBundle.getString("mediaBorderImg");
+ gStrings.mediaListImg = gBundle.getString("mediaListImg");
+ gStrings.mediaCursor = gBundle.getString("mediaCursor");
+ gStrings.mediaObject = gBundle.getString("mediaObject");
+ gStrings.mediaEmbed = gBundle.getString("mediaEmbed");
+ gStrings.mediaLink = gBundle.getString("mediaLink");
+ gStrings.mediaInput = gBundle.getString("mediaInput");
+ gStrings.mediaVideo = gBundle.getString("mediaVideo");
+ gStrings.mediaAudio = gBundle.getString("mediaAudio");
+
+ var args = "arguments" in window &&
+ window.arguments.length >= 1 &&
+ window.arguments[0];
+
+ // init media view
+ var imageTree = document.getElementById("imagetree");
+ imageTree.view = gImageView;
+
+ /* Select the requested tab, if the name is specified */
+ loadTab(args);
+ Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService)
+ .notifyObservers(window, "page-info-dialog-loaded", null);
+}
+
+function loadPageInfo(frameOuterWindowID, imageElement, browser)
+{
+ browser = browser || window.opener.gBrowser.selectedBrowser;
+ let mm = browser.messageManager;
+
+ gStrings["application/rss+xml"] = gBundle.getString("feedRss");
+ gStrings["application/atom+xml"] = gBundle.getString("feedAtom");
+ gStrings["text/xml"] = gBundle.getString("feedXML");
+ gStrings["application/xml"] = gBundle.getString("feedXML");
+ gStrings["application/rdf+xml"] = gBundle.getString("feedXML");
+
+ // Look for pageInfoListener in content.js. Sends message to listener with arguments.
+ mm.sendAsyncMessage("PageInfo:getData", {strings: gStrings,
+ frameOuterWindowID: frameOuterWindowID},
+ { imageElement });
+
+ let pageInfoData;
+
+ // Get initial pageInfoData needed to display the general, feeds, permission and security tabs.
+ mm.addMessageListener("PageInfo:data", function onmessage(message) {
+ mm.removeMessageListener("PageInfo:data", onmessage);
+ pageInfoData = message.data;
+ let docInfo = pageInfoData.docInfo;
+ let windowInfo = pageInfoData.windowInfo;
+ let uri = makeURI(docInfo.documentURIObject.spec,
+ docInfo.documentURIObject.originCharset);
+ let principal = docInfo.principal;
+ gDocInfo = docInfo;
+
+ gImageElement = pageInfoData.imageInfo;
+
+ var titleFormat = windowInfo.isTopWindow ? "pageInfo.page.title"
+ : "pageInfo.frame.title";
+ document.title = gBundle.getFormattedString(titleFormat, [docInfo.location]);
+
+ document.getElementById("main-window").setAttribute("relatedUrl", docInfo.location);
+
+ makeGeneralTab(pageInfoData.metaViewRows, docInfo);
+ initFeedTab(pageInfoData.feeds);
+ onLoadPermission(uri, principal);
+ securityOnLoad(uri, windowInfo);
+ });
+
+ // Get the media elements from content script to setup the media tab.
+ mm.addMessageListener("PageInfo:mediaData", function onmessage(message) {
+ // Page info window was closed.
+ if (window.closed) {
+ mm.removeMessageListener("PageInfo:mediaData", onmessage);
+ return;
+ }
+
+ // The page info media fetching has been completed.
+ if (message.data.isComplete) {
+ mm.removeMessageListener("PageInfo:mediaData", onmessage);
+ onFinished.forEach(function(func) { func(pageInfoData); });
+ return;
+ }
+
+ for (let item of message.data.mediaItems) {
+ addImage(item);
+ }
+
+ selectImage();
+ });
+
+ /* Call registered overlay init functions */
+ onLoadRegistry.forEach(function(func) { func(); });
+}
+
+function resetPageInfo(args)
+{
+ /* Reset Meta tags part */
+ gMetaView.clear();
+
+ /* Reset Media tab */
+ var mediaTab = document.getElementById("mediaTab");
+ if (!mediaTab.hidden) {
+ Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService)
+ .removeObserver(imagePermissionObserver, "perm-changed");
+ mediaTab.hidden = true;
+ }
+ gImageView.clear();
+ gImageHash = {};
+
+ /* Reset Feeds Tab */
+ var feedListbox = document.getElementById("feedListbox");
+ while (feedListbox.firstChild)
+ feedListbox.removeChild(feedListbox.firstChild);
+
+ /* Call registered overlay reset functions */
+ onResetRegistry.forEach(function(func) { func(); });
+
+ /* Rebuild the data */
+ loadTab(args);
+}
+
+function onUnloadPageInfo()
+{
+ // Remove the observer, only if there is at least 1 image.
+ if (!document.getElementById("mediaTab").hidden) {
+ Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService)
+ .removeObserver(imagePermissionObserver, "perm-changed");
+ }
+
+ /* Call registered overlay unload functions */
+ onUnloadRegistry.forEach(function(func) { func(); });
+}
+
+function doHelpButton()
+{
+ const helpTopics = {
+ "generalPanel": "pageinfo_general",
+ "mediaPanel": "pageinfo_media",
+ "feedPanel": "pageinfo_feed",
+ "permPanel": "pageinfo_permissions",
+ "securityPanel": "pageinfo_security"
+ };
+
+ var deck = document.getElementById("mainDeck");
+ var helpdoc = helpTopics[deck.selectedPanel.id] || "pageinfo_general";
+ openHelpLink(helpdoc);
+}
+
+function showTab(id)
+{
+ var deck = document.getElementById("mainDeck");
+ var pagel = document.getElementById(id + "Panel");
+ deck.selectedPanel = pagel;
+}
+
+function loadTab(args)
+{
+ // If the "View Image Info" context menu item was used, the related image
+ // element is provided as an argument. This can't be a background image.
+ let imageElement = args && args.imageElement;
+ let frameOuterWindowID = args && args.frameOuterWindowID;
+ let browser = args && args.browser;
+
+ /* Load the page info */
+ loadPageInfo(frameOuterWindowID, imageElement, browser);
+
+ var initialTab = (args && args.initialTab) || "generalTab";
+ var radioGroup = document.getElementById("viewGroup");
+ initialTab = document.getElementById(initialTab) || document.getElementById("generalTab");
+ radioGroup.selectedItem = initialTab;
+ radioGroup.selectedItem.doCommand();
+ radioGroup.focus();
+}
+
+function toggleGroupbox(id)
+{
+ var elt = document.getElementById(id);
+ if (elt.hasAttribute("closed")) {
+ elt.removeAttribute("closed");
+ if (elt.flexWhenOpened)
+ elt.flex = elt.flexWhenOpened;
+ }
+ else {
+ elt.setAttribute("closed", "true");
+ if (elt.flex) {
+ elt.flexWhenOpened = elt.flex;
+ elt.flex = 0;
+ }
+ }
+}
+
+function openCacheEntry(key, cb)
+{
+ var checkCacheListener = {
+ onCacheEntryCheck: function(entry, appCache) {
+ return Components.interfaces.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+ onCacheEntryAvailable: function(entry, isNew, appCache, status) {
+ cb(entry);
+ }
+ };
+ diskStorage.asyncOpenURI(Services.io.newURI(key, null, null), "", nsICacheStorage.OPEN_READONLY, checkCacheListener);
+}
+
+function makeGeneralTab(metaViewRows, docInfo)
+{
+ var title = (docInfo.title) ? docInfo.title : gBundle.getString("noPageTitle");
+ document.getElementById("titletext").value = title;
+
+ var url = docInfo.location;
+ setItemValue("urltext", url);
+
+ var referrer = ("referrer" in docInfo && docInfo.referrer);
+ setItemValue("refertext", referrer);
+
+ var mode = ("compatMode" in docInfo && docInfo.compatMode == "BackCompat") ? "generalQuirksMode" : "generalStrictMode";
+ document.getElementById("modetext").value = gBundle.getString(mode);
+
+ // find out the mime type
+ var mimeType = docInfo.contentType;
+ setItemValue("typetext", mimeType);
+
+ // get the document characterset
+ var encoding = docInfo.characterSet;
+ document.getElementById("encodingtext").value = encoding;
+
+ let length = metaViewRows.length;
+
+ var metaGroup = document.getElementById("metaTags");
+ if (!length)
+ metaGroup.collapsed = true;
+ else {
+ var metaTagsCaption = document.getElementById("metaTagsCaption");
+ if (length == 1)
+ metaTagsCaption.label = gBundle.getString("generalMetaTag");
+ else
+ metaTagsCaption.label = gBundle.getFormattedString("generalMetaTags", [length]);
+ var metaTree = document.getElementById("metatree");
+ metaTree.view = gMetaView;
+
+ // Add the metaViewRows onto the general tab's meta info tree.
+ gMetaView.addRows(metaViewRows);
+
+ metaGroup.collapsed = false;
+ }
+
+ // get the date of last modification
+ var modifiedText = formatDate(docInfo.lastModified, gStrings.notSet);
+ document.getElementById("modifiedtext").value = modifiedText;
+
+ // get cache info
+ var cacheKey = url.replace(/#.*$/, "");
+ openCacheEntry(cacheKey, function(cacheEntry) {
+ var sizeText;
+ if (cacheEntry) {
+ var pageSize = cacheEntry.dataSize;
+ var kbSize = formatNumber(Math.round(pageSize / 1024 * 100) / 100);
+ sizeText = gBundle.getFormattedString("generalSize", [kbSize, formatNumber(pageSize)]);
+ }
+ setItemValue("sizetext", sizeText);
+ });
+}
+
+function addImage(imageViewRow)
+{
+ let [url, type, alt, elem, isBg] = imageViewRow;
+
+ if (!url)
+ return;
+
+ if (!gImageHash.hasOwnProperty(url))
+ gImageHash[url] = { };
+ if (!gImageHash[url].hasOwnProperty(type))
+ gImageHash[url][type] = { };
+ if (!gImageHash[url][type].hasOwnProperty(alt)) {
+ gImageHash[url][type][alt] = gImageView.data.length;
+ var row = [url, type, -1, alt, 1, elem, isBg];
+ gImageView.addRow(row);
+
+ // Fill in cache data asynchronously
+ openCacheEntry(url, function(cacheEntry) {
+ // The data at row[2] corresponds to the data size.
+ if (cacheEntry) {
+ row[2] = cacheEntry.dataSize;
+ // Invalidate the row to trigger a repaint.
+ gImageView.tree.invalidateRow(gImageView.data.indexOf(row));
+ }
+ });
+
+ // Add the observer, only once.
+ if (gImageView.data.length == 1) {
+ document.getElementById("mediaTab").hidden = false;
+ Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService)
+ .addObserver(imagePermissionObserver, "perm-changed", false);
+ }
+ }
+ else {
+ var i = gImageHash[url][type][alt];
+ gImageView.data[i][COL_IMAGE_COUNT]++;
+ // The same image can occur several times on the page at different sizes.
+ // If the "View Image Info" context menu item was used, ensure we select
+ // the correct element.
+ if (!gImageView.data[i][COL_IMAGE_BG] &&
+ gImageElement && url == gImageElement.currentSrc &&
+ gImageElement.width == elem.width &&
+ gImageElement.height == elem.height &&
+ gImageElement.imageText == elem.imageText) {
+ gImageView.data[i][COL_IMAGE_NODE] = elem;
+ }
+ }
+}
+
+// Link Stuff
+function openURL(target)
+{
+ var url = target.parentNode.childNodes[2].value;
+ window.open(url, "_blank", "chrome");
+}
+
+function onBeginLinkDrag(event, urlField, descField)
+{
+ if (event.originalTarget.localName != "treechildren")
+ return;
+
+ var tree = event.target;
+ if (!("treeBoxObject" in tree))
+ tree = tree.parentNode;
+
+ var row = tree.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (row == -1)
+ return;
+
+ // Adding URL flavor
+ var col = tree.columns[urlField];
+ var url = tree.view.getCellText(row, col);
+ col = tree.columns[descField];
+ var desc = tree.view.getCellText(row, col);
+
+ var dt = event.dataTransfer;
+ dt.setData("text/x-moz-url", url + "\n" + desc);
+ dt.setData("text/url-list", url);
+ dt.setData("text/plain", url);
+}
+
+// Image Stuff
+function getSelectedRows(tree)
+{
+ var start = { };
+ var end = { };
+ var numRanges = tree.view.selection.getRangeCount();
+
+ var rowArray = [ ];
+ for (var t = 0; t < numRanges; t++) {
+ tree.view.selection.getRangeAt(t, start, end);
+ for (var v = start.value; v <= end.value; v++)
+ rowArray.push(v);
+ }
+
+ return rowArray;
+}
+
+function getSelectedRow(tree)
+{
+ var rows = getSelectedRows(tree);
+ return (rows.length == 1) ? rows[0] : -1;
+}
+
+function selectSaveFolder(aCallback)
+{
+ const nsILocalFile = Components.interfaces.nsILocalFile;
+ const nsIFilePicker = Components.interfaces.nsIFilePicker;
+ let titleText = gBundle.getString("mediaSelectFolder");
+ let fp = Components.classes["@mozilla.org/filepicker;1"].
+ createInstance(nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult == nsIFilePicker.returnOK) {
+ aCallback(fp.file.QueryInterface(nsILocalFile));
+ } else {
+ aCallback(null);
+ }
+ };
+
+ fp.init(window, titleText, nsIFilePicker.modeGetFolder);
+ fp.appendFilters(nsIFilePicker.filterAll);
+ try {
+ let prefs = Components.classes[PREFERENCES_CONTRACTID].
+ getService(Components.interfaces.nsIPrefBranch);
+ let initialDir = prefs.getComplexValue("browser.download.dir", nsILocalFile);
+ if (initialDir) {
+ fp.displayDirectory = initialDir;
+ }
+ } catch (ex) {
+ }
+ fp.open(fpCallback);
+}
+
+function saveMedia()
+{
+ var tree = document.getElementById("imagetree");
+ var rowArray = getSelectedRows(tree);
+ if (rowArray.length == 1) {
+ var row = rowArray[0];
+ var item = gImageView.data[row][COL_IMAGE_NODE];
+ var url = gImageView.data[row][COL_IMAGE_ADDRESS];
+
+ if (url) {
+ var titleKey = "SaveImageTitle";
+
+ if (item instanceof HTMLVideoElement)
+ titleKey = "SaveVideoTitle";
+ else if (item instanceof HTMLAudioElement)
+ titleKey = "SaveAudioTitle";
+
+ saveURL(url, null, titleKey, false, false, makeURI(item.baseURI),
+ null, gDocInfo.isContentWindowPrivate);
+ }
+ } else {
+ selectSaveFolder(function(aDirectory) {
+ if (aDirectory) {
+ var saveAnImage = function(aURIString, aChosenData, aBaseURI) {
+ uniqueFile(aChosenData.file);
+ internalSave(aURIString, null, null, null, null, false, "SaveImageTitle",
+ aChosenData, aBaseURI, null, false, null, gDocInfo.isContentWindowPrivate);
+ };
+
+ for (var i = 0; i < rowArray.length; i++) {
+ var v = rowArray[i];
+ var dir = aDirectory.clone();
+ var item = gImageView.data[v][COL_IMAGE_NODE];
+ var uriString = gImageView.data[v][COL_IMAGE_ADDRESS];
+ var uri = makeURI(uriString);
+
+ try {
+ uri.QueryInterface(Components.interfaces.nsIURL);
+ dir.append(decodeURIComponent(uri.fileName));
+ } catch (ex) {
+ // data:/blob: uris
+ // Supply a dummy filename, otherwise Download Manager
+ // will try to delete the base directory on failure.
+ dir.append(gImageView.data[v][COL_IMAGE_TYPE]);
+ }
+
+ if (i == 0) {
+ saveAnImage(uriString, new AutoChosen(dir, uri), makeURI(item.baseURI));
+ } else {
+ // This delay is a hack which prevents the download manager
+ // from opening many times. See bug 377339.
+ setTimeout(saveAnImage, 200, uriString, new AutoChosen(dir, uri),
+ makeURI(item.baseURI));
+ }
+ }
+ }
+ });
+ }
+}
+
+function onBlockImage()
+{
+ var permissionManager = Components.classes[PERMISSION_CONTRACTID]
+ .getService(nsIPermissionManager);
+
+ var checkbox = document.getElementById("blockImage");
+ var uri = makeURI(document.getElementById("imageurltext").value);
+ if (checkbox.checked)
+ permissionManager.add(uri, "image", nsIPermissionManager.DENY_ACTION);
+ else
+ permissionManager.remove(uri, "image");
+}
+
+function onImageSelect()
+{
+ var previewBox = document.getElementById("mediaPreviewBox");
+ var mediaSaveBox = document.getElementById("mediaSaveBox");
+ var splitter = document.getElementById("mediaSplitter");
+ var tree = document.getElementById("imagetree");
+ var count = tree.view.selection.count;
+ if (count == 0) {
+ previewBox.collapsed = true;
+ mediaSaveBox.collapsed = true;
+ splitter.collapsed = true;
+ tree.flex = 1;
+ }
+ else if (count > 1) {
+ splitter.collapsed = true;
+ previewBox.collapsed = true;
+ mediaSaveBox.collapsed = false;
+ tree.flex = 1;
+ }
+ else {
+ mediaSaveBox.collapsed = true;
+ splitter.collapsed = false;
+ previewBox.collapsed = false;
+ tree.flex = 0;
+ makePreview(getSelectedRows(tree)[0]);
+ }
+}
+
+// Makes the media preview (image, video, etc) for the selected row on the media tab.
+function makePreview(row)
+{
+ var item = gImageView.data[row][COL_IMAGE_NODE];
+ var url = gImageView.data[row][COL_IMAGE_ADDRESS];
+ var isBG = gImageView.data[row][COL_IMAGE_BG];
+ var isAudio = false;
+
+ setItemValue("imageurltext", url);
+ setItemValue("imagetext", item.imageText);
+ setItemValue("imagelongdesctext", item.longDesc);
+
+ // get cache info
+ var cacheKey = url.replace(/#.*$/, "");
+ openCacheEntry(cacheKey, function(cacheEntry) {
+ // find out the file size
+ var sizeText;
+ if (cacheEntry) {
+ let imageSize = cacheEntry.dataSize;
+ var kbSize = Math.round(imageSize / 1024 * 100) / 100;
+ sizeText = gBundle.getFormattedString("generalSize",
+ [formatNumber(kbSize), formatNumber(imageSize)]);
+ }
+ else
+ sizeText = gBundle.getString("mediaUnknownNotCached");
+ setItemValue("imagesizetext", sizeText);
+
+ var mimeType = item.mimeType || this.getContentTypeFromHeaders(cacheEntry);
+ var numFrames = item.numFrames;
+
+ var imageType;
+ if (mimeType) {
+ // We found the type, try to display it nicely
+ let imageMimeType = /^image\/(.*)/i.exec(mimeType);
+ if (imageMimeType) {
+ imageType = imageMimeType[1].toUpperCase();
+ if (numFrames > 1)
+ imageType = gBundle.getFormattedString("mediaAnimatedImageType",
+ [imageType, numFrames]);
+ else
+ imageType = gBundle.getFormattedString("mediaImageType", [imageType]);
+ }
+ else {
+ // the MIME type doesn't begin with image/, display the raw type
+ imageType = mimeType;
+ }
+ }
+ else {
+ // We couldn't find the type, fall back to the value in the treeview
+ imageType = gImageView.data[row][COL_IMAGE_TYPE];
+ }
+ setItemValue("imagetypetext", imageType);
+
+ var imageContainer = document.getElementById("theimagecontainer");
+ var oldImage = document.getElementById("thepreviewimage");
+
+ var isProtocolAllowed = checkProtocol(gImageView.data[row]);
+
+ var newImage = new Image;
+ newImage.id = "thepreviewimage";
+ var physWidth = 0, physHeight = 0;
+ var width = 0, height = 0;
+
+ if ((item.HTMLLinkElement || item.HTMLInputElement ||
+ item.HTMLImageElement || item.SVGImageElement ||
+ (item.HTMLObjectElement && mimeType && mimeType.startsWith("image/")) ||
+ isBG) && isProtocolAllowed) {
+ newImage.setAttribute("src", url);
+ physWidth = newImage.width || 0;
+ physHeight = newImage.height || 0;
+
+ // "width" and "height" attributes must be set to newImage,
+ // even if there is no "width" or "height attribute in item;
+ // otherwise, the preview image cannot be displayed correctly.
+ // Since the image might have been loaded out-of-process, we expect
+ // the item to tell us its width / height dimensions. Failing that
+ // the item should tell us the natural dimensions of the image. Finally
+ // failing that, we'll assume that the image was never loaded in the
+ // other process (this can be true for favicons, for example), and so
+ // we'll assume that we can use the natural dimensions of the newImage
+ // we just created. If the natural dimensions of newImage are not known
+ // then the image is probably broken.
+ if (!isBG) {
+ newImage.width = ("width" in item && item.width) || newImage.naturalWidth;
+ newImage.height = ("height" in item && item.height) || newImage.naturalHeight;
+ }
+ else {
+ // the Width and Height of an HTML tag should not be used for its background image
+ // (for example, "table" can have "width" or "height" attributes)
+ newImage.width = item.naturalWidth || newImage.naturalWidth;
+ newImage.height = item.naturalHeight || newImage.naturalHeight;
+ }
+
+ if (item.SVGImageElement) {
+ newImage.width = item.SVGImageElementWidth;
+ newImage.height = item.SVGImageElementHeight;
+ }
+
+ width = newImage.width;
+ height = newImage.height;
+
+ document.getElementById("theimagecontainer").collapsed = false
+ document.getElementById("brokenimagecontainer").collapsed = true;
+ }
+ else if (item.HTMLVideoElement && isProtocolAllowed) {
+ newImage = document.createElementNS("http://www.w3.org/1999/xhtml", "video");
+ newImage.id = "thepreviewimage";
+ newImage.src = url;
+ newImage.controls = true;
+ width = physWidth = item.videoWidth;
+ height = physHeight = item.videoHeight;
+
+ document.getElementById("theimagecontainer").collapsed = false;
+ document.getElementById("brokenimagecontainer").collapsed = true;
+ }
+ else if (item.HTMLAudioElement && isProtocolAllowed) {
+ newImage = new Audio;
+ newImage.id = "thepreviewimage";
+ newImage.src = url;
+ newImage.controls = true;
+ isAudio = true;
+
+ document.getElementById("theimagecontainer").collapsed = false;
+ document.getElementById("brokenimagecontainer").collapsed = true;
+ }
+ else {
+ // fallback image for protocols not allowed (e.g., javascript:)
+ // or elements not [yet] handled (e.g., object, embed).
+ document.getElementById("brokenimagecontainer").collapsed = false;
+ document.getElementById("theimagecontainer").collapsed = true;
+ }
+
+ let imageSize = "";
+ if (url && !isAudio) {
+ if (width != physWidth || height != physHeight) {
+ imageSize = gBundle.getFormattedString("mediaDimensionsScaled",
+ [formatNumber(physWidth),
+ formatNumber(physHeight),
+ formatNumber(width),
+ formatNumber(height)]);
+ }
+ else {
+ imageSize = gBundle.getFormattedString("mediaDimensions",
+ [formatNumber(width),
+ formatNumber(height)]);
+ }
+ }
+ setItemValue("imagedimensiontext", imageSize);
+
+ makeBlockImage(url);
+
+ imageContainer.removeChild(oldImage);
+ imageContainer.appendChild(newImage);
+ });
+}
+
+function makeBlockImage(url)
+{
+ var permissionManager = Components.classes[PERMISSION_CONTRACTID]
+ .getService(nsIPermissionManager);
+ var prefs = Components.classes[PREFERENCES_CONTRACTID]
+ .getService(Components.interfaces.nsIPrefBranch);
+
+ var checkbox = document.getElementById("blockImage");
+ var imagePref = prefs.getIntPref("permissions.default.image");
+ if (!(/^https?:/.test(url)) || imagePref == 2)
+ // We can't block the images from this host because either is is not
+ // for http(s) or we don't load images at all
+ checkbox.hidden = true;
+ else {
+ var uri = makeURI(url);
+ if (uri.host) {
+ checkbox.hidden = false;
+ checkbox.label = gBundle.getFormattedString("mediaBlockImage", [uri.host]);
+ var perm = permissionManager.testPermission(uri, "image");
+ checkbox.checked = perm == nsIPermissionManager.DENY_ACTION;
+ }
+ else
+ checkbox.hidden = true;
+ }
+}
+
+var imagePermissionObserver = {
+ observe: function (aSubject, aTopic, aData)
+ {
+ if (document.getElementById("mediaPreviewBox").collapsed)
+ return;
+
+ if (aTopic == "perm-changed") {
+ var permission = aSubject.QueryInterface(Components.interfaces.nsIPermission);
+ if (permission.type == "image") {
+ var imageTree = document.getElementById("imagetree");
+ var row = getSelectedRow(imageTree);
+ var url = gImageView.data[row][COL_IMAGE_ADDRESS];
+ if (permission.matchesURI(makeURI(url), true)) {
+ makeBlockImage(url);
+ }
+ }
+ }
+ }
+}
+
+function getContentTypeFromHeaders(cacheEntryDescriptor)
+{
+ if (!cacheEntryDescriptor)
+ return null;
+
+ let headers = cacheEntryDescriptor.getMetaDataElement("response-head");
+ let type = /^Content-Type:\s*(.*?)\s*(?:\;|$)/mi.exec(headers);
+ return type && type[1];
+}
+
+function setItemValue(id, value)
+{
+ var item = document.getElementById(id);
+ if (value) {
+ item.parentNode.collapsed = false;
+ item.value = value;
+ }
+ else
+ item.parentNode.collapsed = true;
+}
+
+function formatNumber(number)
+{
+ return (+number).toLocaleString(); // coerce number to a numeric value before calling toLocaleString()
+}
+
+function formatDate(datestr, unknown)
+{
+ var date = new Date(datestr);
+ if (!date.valueOf())
+ return unknown;
+
+ 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);
+}
+
+function doCopy()
+{
+ if (!gClipboardHelper)
+ return;
+
+ var elem = document.commandDispatcher.focusedElement;
+
+ if (elem && "treeBoxObject" in elem) {
+ var view = elem.view;
+ var selection = view.selection;
+ var text = [], tmp = '';
+ var min = {}, max = {};
+
+ var count = selection.getRangeCount();
+
+ for (var i = 0; i < count; i++) {
+ selection.getRangeAt(i, min, max);
+
+ for (var row = min.value; row <= max.value; row++) {
+ view.performActionOnRow("copy", row);
+
+ tmp = elem.getAttribute("copybuffer");
+ if (tmp)
+ text.push(tmp);
+ elem.removeAttribute("copybuffer");
+ }
+ }
+ gClipboardHelper.copyString(text.join("\n"));
+ }
+}
+
+function doSelectAllMedia()
+{
+ var tree = document.getElementById("imagetree");
+
+ if (tree)
+ tree.view.selection.selectAll();
+}
+
+function doSelectAll()
+{
+ var elem = document.commandDispatcher.focusedElement;
+
+ if (elem && "treeBoxObject" in elem)
+ elem.view.selection.selectAll();
+}
+
+function selectImage()
+{
+ if (!gImageElement)
+ return;
+
+ var tree = document.getElementById("imagetree");
+ for (var i = 0; i < tree.view.rowCount; i++) {
+ // If the image row element is the image selected from the "View Image Info" context menu item.
+ let image = gImageView.data[i][COL_IMAGE_NODE];
+ if (!gImageView.data[i][COL_IMAGE_BG] &&
+ gImageElement.currentSrc == gImageView.data[i][COL_IMAGE_ADDRESS] &&
+ gImageElement.width == image.width &&
+ gImageElement.height == image.height &&
+ gImageElement.imageText == image.imageText) {
+ tree.view.selection.select(i);
+ tree.treeBoxObject.ensureRowIsVisible(i);
+ tree.focus();
+ return;
+ }
+ }
+}
+
+function checkProtocol(img)
+{
+ var url = img[COL_IMAGE_ADDRESS];
+ return /^data:image\//i.test(url) ||
+ /^(https?|ftp|file|about|chrome|resource):/.test(url);
+}
diff --git a/browser/base/content/pageinfo/pageInfo.xml b/browser/base/content/pageinfo/pageInfo.xml
new file mode 100644
index 000000000..62c699bf2
--- /dev/null
+++ b/browser/base/content/pageinfo/pageInfo.xml
@@ -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/. -->
+
+
+<bindings id="pageInfoBindings"
+ 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">
+
+ <!-- based on preferences.xml paneButton -->
+ <binding id="viewbutton" extends="chrome://global/content/bindings/radio.xml#radio" role="xullistitem">
+ <content>
+ <xul:image class="viewButtonIcon" xbl:inherits="src"/>
+ <xul:label class="viewButtonLabel" xbl:inherits="value=label"/>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/browser/base/content/pageinfo/pageInfo.xul b/browser/base/content/pageinfo/pageInfo.xul
new file mode 100644
index 000000000..8352a8aa7
--- /dev/null
+++ b/browser/base/content/pageinfo/pageInfo.xul
@@ -0,0 +1,438 @@
+<?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://browser/content/pageinfo/pageInfo.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/pageInfo.css" type="text/css"?>
+
+<!DOCTYPE window [
+ <!ENTITY % pageInfoDTD SYSTEM "chrome://browser/locale/pageInfo.dtd">
+ %pageInfoDTD;
+]>
+
+#ifdef XP_MACOSX
+<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?>
+#endif
+
+<window id="main-window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="Browser:page-info"
+ onload="onLoadPageInfo()"
+ onunload="onUnloadPageInfo()"
+ align="stretch"
+ screenX="10" screenY="10"
+ width="&pageInfoWindow.width;" height="&pageInfoWindow.height;"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+ <script type="application/javascript" src="chrome://global/content/treeUtils.js"/>
+ <script type="application/javascript" src="chrome://browser/content/pageinfo/pageInfo.js"/>
+ <script type="application/javascript" src="chrome://browser/content/pageinfo/feeds.js"/>
+ <script type="application/javascript" src="chrome://browser/content/pageinfo/permissions.js"/>
+ <script type="application/javascript" src="chrome://browser/content/pageinfo/security.js"/>
+ <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
+
+ <stringbundleset id="pageinfobundleset">
+ <stringbundle id="pageinfobundle" src="chrome://browser/locale/pageInfo.properties"/>
+ <stringbundle id="pkiBundle" src="chrome://pippki/locale/pippki.properties"/>
+ <stringbundle id="browserBundle" src="chrome://browser/locale/browser.properties"/>
+ </stringbundleset>
+
+ <commandset id="pageInfoCommandSet">
+ <command id="cmd_close" oncommand="window.close();"/>
+ <command id="cmd_help" oncommand="doHelpButton();"/>
+ <command id="cmd_copy" oncommand="doCopy();"/>
+ <command id="cmd_selectall" oncommand="doSelectAll();"/>
+
+ <!-- permissions tab -->
+ <command id="cmd_pluginsDef" oncommand="onCheckboxClick('plugins');"/>
+ <command id="cmd_pluginsToggle" oncommand="onPluginRadioClick(event);"/>
+ </commandset>
+
+ <keyset id="pageInfoKeySet">
+ <key key="&closeWindow.key;" modifiers="accel" command="cmd_close"/>
+ <key keycode="VK_ESCAPE" command="cmd_close"/>
+#ifdef XP_MACOSX
+ <key key="." modifiers="meta" command="cmd_close"/>
+#else
+ <key keycode="VK_F1" command="cmd_help"/>
+#endif
+ <key key="&copy.key;" modifiers="accel" command="cmd_copy"/>
+ <key key="&selectall.key;" modifiers="accel" command="cmd_selectall"/>
+ <key key="&selectall.key;" modifiers="alt" command="cmd_selectall"/>
+ </keyset>
+
+ <menupopup id="picontext">
+ <menuitem id="menu_selectall" label="&selectall.label;" command="cmd_selectall" accesskey="&selectall.accesskey;"/>
+ <menuitem id="menu_copy" label="&copy.label;" command="cmd_copy" accesskey="&copy.accesskey;"/>
+ </menupopup>
+
+ <windowdragbox id="topBar" class="viewGroupWrapper">
+ <radiogroup id="viewGroup" class="chromeclass-toolbar" orient="horizontal">
+ <radio id="generalTab" label="&generalTab;" accesskey="&generalTab.accesskey;"
+ oncommand="showTab('general');"/>
+ <radio id="mediaTab" label="&mediaTab;" accesskey="&mediaTab.accesskey;"
+ oncommand="showTab('media');" hidden="true"/>
+ <radio id="feedTab" label="&feedTab;" accesskey="&feedTab.accesskey;"
+ oncommand="showTab('feed');" hidden="true"/>
+ <radio id="permTab" label="&permTab;" accesskey="&permTab.accesskey;"
+ oncommand="showTab('perm');"/>
+ <radio id="securityTab" label="&securityTab;" accesskey="&securityTab.accesskey;"
+ oncommand="showTab('security');"/>
+ <!-- Others added by overlay -->
+ </radiogroup>
+ </windowdragbox>
+
+ <deck id="mainDeck" flex="1">
+ <!-- General page information -->
+ <vbox id="generalPanel">
+ <grid id="generalGrid">
+ <columns>
+ <column/>
+ <column class="gridSeparator"/>
+ <column flex="1"/>
+ </columns>
+ <rows id="generalRows">
+ <row id="generalTitle">
+ <label control="titletext" value="&generalTitle;"/>
+ <separator/>
+ <textbox readonly="true" id="titletext"/>
+ </row>
+ <row id="generalURLRow">
+ <label control="urltext" value="&generalURL;"/>
+ <separator/>
+ <textbox readonly="true" id="urltext"/>
+ </row>
+ <row id="generalSeparatorRow1">
+ <separator class="thin"/>
+ </row>
+ <row id="generalTypeRow">
+ <label control="typetext" value="&generalType;"/>
+ <separator/>
+ <textbox readonly="true" id="typetext"/>
+ </row>
+ <row id="generalModeRow">
+ <label control="modetext" value="&generalMode;"/>
+ <separator/>
+ <textbox readonly="true" crop="end" id="modetext"/>
+ </row>
+ <row id="generalEncodingRow">
+ <label control="encodingtext" value="&generalEncoding2;"/>
+ <separator/>
+ <textbox readonly="true" id="encodingtext"/>
+ </row>
+ <row id="generalSizeRow">
+ <label control="sizetext" value="&generalSize;"/>
+ <separator/>
+ <textbox readonly="true" id="sizetext"/>
+ </row>
+ <row id="generalReferrerRow">
+ <label control="refertext" value="&generalReferrer;"/>
+ <separator/>
+ <textbox readonly="true" id="refertext"/>
+ </row>
+ <row id="generalSeparatorRow2">
+ <separator class="thin"/>
+ </row>
+ <row id="generalModifiedRow">
+ <label control="modifiedtext" value="&generalModified;"/>
+ <separator/>
+ <textbox readonly="true" id="modifiedtext"/>
+ </row>
+ </rows>
+ </grid>
+ <separator class="thin"/>
+ <groupbox id="metaTags" flex="1" class="collapsable treebox">
+ <caption id="metaTagsCaption" onclick="toggleGroupbox('metaTags');"/>
+ <tree id="metatree" flex="1" hidecolumnpicker="true" contextmenu="picontext">
+ <treecols>
+ <treecol id="meta-name" label="&generalMetaName;"
+ persist="width" flex="1"
+ onclick="gMetaView.onPageMediaSort('meta-name');"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="meta-content" label="&generalMetaContent;"
+ persist="width" flex="4"
+ onclick="gMetaView.onPageMediaSort('meta-content');"/>
+ </treecols>
+ <treechildren id="metatreechildren" flex="1"/>
+ </tree>
+ </groupbox>
+ <hbox pack="end">
+ <button command="cmd_help" label="&helpButton.label;" dlgtype="help"/>
+ </hbox>
+ </vbox>
+
+ <!-- Media information -->
+ <vbox id="mediaPanel">
+ <tree id="imagetree" onselect="onImageSelect();" contextmenu="picontext"
+ ondragstart="onBeginLinkDrag(event,'image-address','image-alt')">
+ <treecols>
+ <treecol sortSeparators="true" primary="true" persist="width" flex="10"
+ width="10" id="image-address" label="&mediaAddress;"
+ onclick="gImageView.onPageMediaSort('image-address');"/>
+ <splitter class="tree-splitter"/>
+ <treecol sortSeparators="true" persist="hidden width" flex="2"
+ width="2" id="image-type" label="&mediaType;"
+ onclick="gImageView.onPageMediaSort('image-type');"/>
+ <splitter class="tree-splitter"/>
+ <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="2"
+ width="2" id="image-size" label="&mediaSize;" value="size"
+ onclick="gImageView.onPageMediaSort('image-size');"/>
+ <splitter class="tree-splitter"/>
+ <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="4"
+ width="4" id="image-alt" label="&mediaAltHeader;"
+ onclick="gImageView.onPageMediaSort('image-alt');"/>
+ <splitter class="tree-splitter"/>
+ <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="1"
+ width="1" id="image-count" label="&mediaCount;"
+ onclick="gImageView.onPageMediaSort('image-count');"/>
+ </treecols>
+ <treechildren id="imagetreechildren" flex="1"/>
+ </tree>
+ <splitter orient="vertical" id="mediaSplitter"/>
+ <vbox flex="1" id="mediaPreviewBox" collapsed="true">
+ <grid id="mediaGrid">
+ <columns>
+ <column id="mediaLabelColumn"/>
+ <column class="gridSeparator"/>
+ <column flex="1"/>
+ </columns>
+ <rows id="mediaRows">
+ <row id="mediaLocationRow">
+ <label control="imageurltext" value="&mediaLocation;"/>
+ <separator/>
+ <textbox readonly="true" id="imageurltext"/>
+ </row>
+ <row id="mediaTypeRow">
+ <label control="imagetypetext" value="&generalType;"/>
+ <separator/>
+ <textbox readonly="true" id="imagetypetext"/>
+ </row>
+ <row id="mediaSizeRow">
+ <label control="imagesizetext" value="&generalSize;"/>
+ <separator/>
+ <textbox readonly="true" id="imagesizetext"/>
+ </row>
+ <row id="mediaDimensionRow">
+ <label control="imagedimensiontext" value="&mediaDimension;"/>
+ <separator/>
+ <textbox readonly="true" id="imagedimensiontext"/>
+ </row>
+ <row id="mediaTextRow">
+ <label control="imagetext" value="&mediaText;"/>
+ <separator/>
+ <textbox readonly="true" id="imagetext"/>
+ </row>
+ <row id="mediaLongdescRow">
+ <label control="imagelongdesctext" value="&mediaLongdesc;"/>
+ <separator/>
+ <textbox readonly="true" id="imagelongdesctext"/>
+ </row>
+ </rows>
+ </grid>
+ <hbox id="imageSaveBox" align="end">
+ <vbox id="blockImageBox">
+ <checkbox id="blockImage" hidden="true" oncommand="onBlockImage()"
+ accesskey="&mediaBlockImage.accesskey;"/>
+ <label control="thepreviewimage" value="&mediaPreview;" class="header"/>
+ </vbox>
+ <spacer id="imageSaveBoxSpacer" flex="1"/>
+ <button label="&selectall.label;" accesskey="&selectall.accesskey;"
+ id="selectallbutton"
+ oncommand="doSelectAllMedia();"/>
+ <button label="&mediaSaveAs;" accesskey="&mediaSaveAs.accesskey;"
+ icon="save" id="imagesaveasbutton"
+ oncommand="saveMedia();"/>
+ </hbox>
+ <vbox id="imagecontainerbox" class="inset iframe" flex="1" pack="center">
+ <hbox id="theimagecontainer" pack="center">
+ <image id="thepreviewimage"/>
+ </hbox>
+ <hbox id="brokenimagecontainer" pack="center" collapsed="true">
+ <image id="brokenimage" src="resource://gre-resources/broken-image.png"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ <hbox id="mediaSaveBox" collapsed="true">
+ <spacer id="mediaSaveBoxSpacer" flex="1"/>
+ <button label="&mediaSaveAs;" accesskey="&mediaSaveAs2.accesskey;"
+ icon="save" id="mediasaveasbutton"
+ oncommand="saveMedia();"/>
+ </hbox>
+ <hbox pack="end">
+ <button command="cmd_help" label="&helpButton.label;" dlgtype="help"/>
+ </hbox>
+ </vbox>
+
+ <!-- Feeds -->
+ <vbox id="feedPanel">
+ <richlistbox id="feedListbox" flex="1"/>
+ </vbox>
+
+ <!-- Permissions -->
+ <vbox id="permPanel">
+ <hbox id="permHostBox">
+ <label value="&permissionsFor;" control="hostText" />
+ <textbox id="hostText" class="header" readonly="true"
+ crop="end" flex="1"/>
+ </hbox>
+
+ <vbox id="permList" flex="1">
+ <hbox id="perm-indexedDB-extras">
+ <spacer flex="1"/>
+ <vbox id="permIndexedDBStatusBox" pack="center">
+ <label id="indexedDBStatus" control="indexedDBClear" hidden="true"/>
+ </vbox>
+ <button id="indexedDBClear" label="&permClearStorage;" hidden="true"
+ accesskey="&permClearStorage.accesskey;" onclick="onIndexedDBClear();"/>
+ </hbox>
+ <vbox class="permission" id="perm-plugins-row">
+ <label class="permissionLabel" id="permPluginsLabel"
+ value="&permPlugins;" control="pluginsRadioGroup"/>
+ <hbox id="permPluginTemplate" role="group" aria-labelledby="permPluginsLabel" align="baseline">
+ <label class="permPluginTemplateLabel"/>
+ <spacer flex="1"/>
+ <radiogroup class="permPluginTemplateRadioGroup" orient="horizontal" command="cmd_pluginsToggle">
+ <radio class="permPluginTemplateRadioDefault" label="&permUseDefault;"/>
+ <radio class="permPluginTemplateRadioAsk" label="&permAskAlways;"/>
+ <radio class="permPluginTemplateRadioAllow" label="&permAllow;"/>
+ <radio class="permPluginTemplateRadioBlock" label="&permBlock;"/>
+ </radiogroup>
+ </hbox>
+ </vbox>
+ </vbox>
+ <hbox pack="end">
+ <button command="cmd_help" label="&helpButton.label;" dlgtype="help"/>
+ </hbox>
+ </vbox>
+
+ <!-- Security & Privacy -->
+ <vbox id="securityPanel">
+ <!-- Identity Section -->
+ <groupbox id="security-identity-groupbox" flex="1">
+ <caption id="security-identity" label="&securityView.identity.header;"/>
+ <grid id="security-identity-grid" flex="1">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows id="security-identity-rows">
+ <!-- Domain -->
+ <row id="security-identity-domain-row">
+ <label id="security-identity-domain-label"
+ class="fieldLabel"
+ value="&securityView.identity.domain;"
+ control="security-identity-domain-value"/>
+ <textbox id="security-identity-domain-value"
+ class="fieldValue" readonly="true"/>
+ </row>
+ <!-- Owner -->
+ <row id="security-identity-owner-row">
+ <label id="security-identity-owner-label"
+ class="fieldLabel"
+ value="&securityView.identity.owner;"
+ control="security-identity-owner-value"/>
+ <textbox id="security-identity-owner-value"
+ class="fieldValue" readonly="true"/>
+ </row>
+ <!-- Verifier -->
+ <row id="security-identity-verifier-row">
+ <label id="security-identity-verifier-label"
+ class="fieldLabel"
+ value="&securityView.identity.verifier;"
+ control="security-identity-verifier-value"/>
+ <textbox id="security-identity-verifier-value"
+ class="fieldValue" readonly="true" />
+ </row>
+ </rows>
+ </grid>
+ <spacer flex="1"/>
+ <!-- Cert button -->
+ <hbox id="security-view-cert-box" pack="end">
+ <button id="security-view-cert" label="&securityView.certView;"
+ accesskey="&securityView.accesskey;"
+ oncommand="security.viewCert();"/>
+ </hbox>
+ </groupbox>
+
+ <!-- Privacy & History section -->
+ <groupbox id="security-privacy-groupbox" flex="1">
+ <caption id="security-privacy" label="&securityView.privacy.header;" />
+ <grid id="security-privacy-grid">
+ <columns>
+ <column flex="1"/>
+ <column flex="1"/>
+ </columns>
+ <rows id="security-privacy-rows">
+ <!-- History -->
+ <row id="security-privacy-history-row">
+ <label id="security-privacy-history-label"
+ control="security-privacy-history-value"
+ class="fieldLabel">&securityView.privacy.history;</label>
+ <textbox id="security-privacy-history-value"
+ class="fieldValue"
+ value="&securityView.unknown;"
+ readonly="true"/>
+ </row>
+ <!-- Cookies -->
+ <row id="security-privacy-cookies-row">
+ <label id="security-privacy-cookies-label"
+ control="security-privacy-cookies-value"
+ class="fieldLabel">&securityView.privacy.cookies;</label>
+ <hbox id="security-privacy-cookies-box" align="center">
+ <textbox id="security-privacy-cookies-value"
+ class="fieldValue"
+ value="&securityView.unknown;"
+ flex="1"
+ readonly="true"/>
+ <button id="security-view-cookies"
+ label="&securityView.privacy.viewCookies;"
+ accesskey="&securityView.privacy.viewCookies.accessKey;"
+ oncommand="security.viewCookies();"/>
+ </hbox>
+ </row>
+ <!-- Passwords -->
+ <row id="security-privacy-passwords-row">
+ <label id="security-privacy-passwords-label"
+ control="security-privacy-passwords-value"
+ class="fieldLabel">&securityView.privacy.passwords;</label>
+ <hbox id="security-privacy-passwords-box" align="center">
+ <textbox id="security-privacy-passwords-value"
+ class="fieldValue"
+ value="&securityView.unknown;"
+ flex="1"
+ readonly="true"/>
+ <button id="security-view-password"
+ label="&securityView.privacy.viewPasswords;"
+ accesskey="&securityView.privacy.viewPasswords.accessKey;"
+ oncommand="security.viewPasswords();"/>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <!-- Technical Details section -->
+ <groupbox id="security-technical-groupbox" flex="1">
+ <caption id="security-technical" label="&securityView.technical.header;" />
+ <vbox id="security-technical-box" flex="1">
+ <label id="security-technical-shortform" class="fieldValue"/>
+ <description id="security-technical-longform1" class="fieldLabel"/>
+ <description id="security-technical-longform2" class="fieldLabel"/>
+ <description id="security-technical-certificate-transparency" class="fieldLabel"/>
+ </vbox>
+ </groupbox>
+ <hbox pack="end">
+ <button command="cmd_help" label="&helpButton.label;" dlgtype="help"/>
+ </hbox>
+ </vbox>
+ <!-- Others added by overlay -->
+ </deck>
+
+#ifdef XP_MACOSX
+#include ../browserMountPoints.inc
+#endif
+
+</window>
diff --git a/browser/base/content/pageinfo/permissions.js b/browser/base/content/pageinfo/permissions.js
new file mode 100644
index 000000000..0e6b9cba1
--- /dev/null
+++ b/browser/base/content/pageinfo/permissions.js
@@ -0,0 +1,334 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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:///modules/SitePermissions.jsm");
+Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+
+const nsIQuotaManagerService = Components.interfaces.nsIQuotaManagerService;
+
+var gPermURI;
+var gPermPrincipal;
+var gUsageRequest;
+
+// Array of permissionIDs sorted alphabetically by label.
+var gPermissions = SitePermissions.listPermissions().sort((a, b) => {
+ let firstLabel = SitePermissions.getPermissionLabel(a);
+ let secondLabel = SitePermissions.getPermissionLabel(b);
+ return firstLabel.localeCompare(secondLabel);
+});
+gPermissions.push("plugins");
+
+var permissionObserver = {
+ observe: function (aSubject, aTopic, aData)
+ {
+ if (aTopic == "perm-changed") {
+ var permission = aSubject.QueryInterface(Components.interfaces.nsIPermission);
+ if (permission.matchesURI(gPermURI, true)) {
+ if (gPermissions.indexOf(permission.type) > -1)
+ initRow(permission.type);
+ else if (permission.type.startsWith("plugin"))
+ setPluginsRadioState();
+ }
+ }
+ }
+};
+
+function onLoadPermission(uri, principal)
+{
+ var permTab = document.getElementById("permTab");
+ if (SitePermissions.isSupportedURI(uri)) {
+ gPermURI = uri;
+ gPermPrincipal = principal;
+ var hostText = document.getElementById("hostText");
+ hostText.value = gPermURI.prePath;
+
+ for (var i of gPermissions)
+ initRow(i);
+ var os = Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+ os.addObserver(permissionObserver, "perm-changed", false);
+ onUnloadRegistry.push(onUnloadPermission);
+ permTab.hidden = false;
+ }
+ else
+ permTab.hidden = true;
+}
+
+function onUnloadPermission()
+{
+ var os = Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+ os.removeObserver(permissionObserver, "perm-changed");
+
+ if (gUsageRequest) {
+ gUsageRequest.cancel();
+ gUsageRequest = null;
+ }
+}
+
+function initRow(aPartId)
+{
+ if (aPartId == "plugins") {
+ initPluginsRow();
+ return;
+ }
+
+ createRow(aPartId);
+
+ var checkbox = document.getElementById(aPartId + "Def");
+ var command = document.getElementById("cmd_" + aPartId + "Toggle");
+ var perm = SitePermissions.get(gPermURI, aPartId);
+
+ if (perm) {
+ checkbox.checked = false;
+ command.removeAttribute("disabled");
+ }
+ else {
+ checkbox.checked = true;
+ command.setAttribute("disabled", "true");
+ perm = SitePermissions.getDefault(aPartId);
+ }
+ setRadioState(aPartId, perm);
+
+ if (aPartId == "indexedDB") {
+ initIndexedDBRow();
+ }
+}
+
+function createRow(aPartId) {
+ let rowId = "perm-" + aPartId + "-row";
+ if (document.getElementById(rowId))
+ return;
+
+ let commandId = "cmd_" + aPartId + "Toggle";
+ let labelId = "perm-" + aPartId + "-label";
+ let radiogroupId = aPartId + "RadioGroup";
+
+ let command = document.createElement("command");
+ command.setAttribute("id", commandId);
+ command.setAttribute("oncommand", "onRadioClick('" + aPartId + "');");
+ document.getElementById("pageInfoCommandSet").appendChild(command);
+
+ let row = document.createElement("vbox");
+ row.setAttribute("id", rowId);
+ row.setAttribute("class", "permission");
+
+ let label = document.createElement("label");
+ label.setAttribute("id", labelId);
+ label.setAttribute("control", radiogroupId);
+ label.setAttribute("value", SitePermissions.getPermissionLabel(aPartId));
+ label.setAttribute("class", "permissionLabel");
+ row.appendChild(label);
+
+ let controls = document.createElement("hbox");
+ controls.setAttribute("role", "group");
+ controls.setAttribute("aria-labelledby", labelId);
+
+ let checkbox = document.createElement("checkbox");
+ checkbox.setAttribute("id", aPartId + "Def");
+ checkbox.setAttribute("oncommand", "onCheckboxClick('" + aPartId + "');");
+ checkbox.setAttribute("label", gBundle.getString("permissions.useDefault"));
+ controls.appendChild(checkbox);
+
+ let spacer = document.createElement("spacer");
+ spacer.setAttribute("flex", "1");
+ controls.appendChild(spacer);
+
+ let radiogroup = document.createElement("radiogroup");
+ radiogroup.setAttribute("id", radiogroupId);
+ radiogroup.setAttribute("orient", "horizontal");
+ for (let state of SitePermissions.getAvailableStates(aPartId)) {
+ let radio = document.createElement("radio");
+ radio.setAttribute("id", aPartId + "#" + state);
+ radio.setAttribute("label", SitePermissions.getStateLabel(aPartId, state));
+ radio.setAttribute("command", commandId);
+ radiogroup.appendChild(radio);
+ }
+ controls.appendChild(radiogroup);
+
+ row.appendChild(controls);
+
+ document.getElementById("permList").appendChild(row);
+}
+
+function onCheckboxClick(aPartId)
+{
+ var command = document.getElementById("cmd_" + aPartId + "Toggle");
+ var checkbox = document.getElementById(aPartId + "Def");
+ if (checkbox.checked) {
+ SitePermissions.remove(gPermURI, aPartId);
+ command.setAttribute("disabled", "true");
+ var perm = SitePermissions.getDefault(aPartId);
+ setRadioState(aPartId, perm);
+ }
+ else {
+ onRadioClick(aPartId);
+ command.removeAttribute("disabled");
+ }
+}
+
+function onPluginRadioClick(aEvent) {
+ onRadioClick(aEvent.originalTarget.getAttribute("id").split('#')[0]);
+}
+
+function onRadioClick(aPartId)
+{
+ var radioGroup = document.getElementById(aPartId + "RadioGroup");
+ var id = radioGroup.selectedItem.id;
+ var permission = id.split('#')[1];
+ SitePermissions.set(gPermURI, aPartId, permission);
+}
+
+function setRadioState(aPartId, aValue)
+{
+ var radio = document.getElementById(aPartId + "#" + aValue);
+ if (radio) {
+ radio.radioGroup.selectedItem = radio;
+ }
+}
+
+function initIndexedDBRow()
+{
+ let row = document.getElementById("perm-indexedDB-row");
+ let extras = document.getElementById("perm-indexedDB-extras");
+
+ row.appendChild(extras);
+
+ var quotaManagerService =
+ Components.classes["@mozilla.org/dom/quota-manager-service;1"]
+ .getService(nsIQuotaManagerService);
+ gUsageRequest =
+ quotaManagerService.getUsageForPrincipal(gPermPrincipal,
+ onIndexedDBUsageCallback);
+
+ var status = document.getElementById("indexedDBStatus");
+ var button = document.getElementById("indexedDBClear");
+
+ status.value = "";
+ status.setAttribute("hidden", "true");
+ button.setAttribute("hidden", "true");
+}
+
+function onIndexedDBClear()
+{
+ Components.classes["@mozilla.org/dom/quota-manager-service;1"]
+ .getService(nsIQuotaManagerService)
+ .clearStoragesForPrincipal(gPermPrincipal);
+
+ Components.classes["@mozilla.org/serviceworkers/manager;1"]
+ .getService(Components.interfaces.nsIServiceWorkerManager)
+ .removeAndPropagate(gPermURI.host);
+
+ SitePermissions.remove(gPermURI, "indexedDB");
+ initIndexedDBRow();
+}
+
+function onIndexedDBUsageCallback(request)
+{
+ let uri = request.principal.URI;
+ if (!uri.equals(gPermURI)) {
+ throw new Error("Callback received for bad URI: " + uri);
+ }
+
+ let usage = request.result.usage;
+ if (usage) {
+ if (!("DownloadUtils" in window)) {
+ Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
+ }
+
+ var status = document.getElementById("indexedDBStatus");
+ var button = document.getElementById("indexedDBClear");
+
+ status.value =
+ gBundle.getFormattedString("indexedDBUsage",
+ DownloadUtils.convertByteUnits(usage));
+ status.removeAttribute("hidden");
+ button.removeAttribute("hidden");
+ }
+}
+
+function fillInPluginPermissionTemplate(aPluginName, aPermissionString) {
+ let permPluginTemplate = document.getElementById("permPluginTemplate").cloneNode(true);
+ permPluginTemplate.setAttribute("permString", aPermissionString);
+ let attrs = [
+ [ ".permPluginTemplateLabel", "value", aPluginName ],
+ [ ".permPluginTemplateRadioGroup", "id", aPermissionString + "RadioGroup" ],
+ [ ".permPluginTemplateRadioDefault", "id", aPermissionString + "#0" ],
+ [ ".permPluginTemplateRadioAsk", "id", aPermissionString + "#3" ],
+ [ ".permPluginTemplateRadioAllow", "id", aPermissionString + "#1" ],
+ [ ".permPluginTemplateRadioBlock", "id", aPermissionString + "#2" ]
+ ];
+
+ for (let attr of attrs) {
+ permPluginTemplate.querySelector(attr[0]).setAttribute(attr[1], attr[2]);
+ }
+
+ return permPluginTemplate;
+}
+
+function clearPluginPermissionTemplate() {
+ let permPluginTemplate = document.getElementById("permPluginTemplate");
+ permPluginTemplate.hidden = true;
+ permPluginTemplate.removeAttribute("permString");
+ document.querySelector(".permPluginTemplateLabel").removeAttribute("value");
+ document.querySelector(".permPluginTemplateRadioGroup").removeAttribute("id");
+ document.querySelector(".permPluginTemplateRadioAsk").removeAttribute("id");
+ document.querySelector(".permPluginTemplateRadioAllow").removeAttribute("id");
+ document.querySelector(".permPluginTemplateRadioBlock").removeAttribute("id");
+}
+
+function initPluginsRow() {
+ let vulnerableLabel = document.getElementById("browserBundle").getString("pluginActivateVulnerable.label");
+ let pluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+ let permissionMap = new Map();
+
+ for (let plugin of pluginHost.getPluginTags()) {
+ if (plugin.disabled) {
+ continue;
+ }
+ for (let mimeType of plugin.getMimeTypes()) {
+ let permString = pluginHost.getPermissionStringForType(mimeType);
+ if (!permissionMap.has(permString)) {
+ let name = BrowserUtils.makeNicePluginName(plugin.name);
+ if (permString.startsWith("plugin-vulnerable:")) {
+ name += " \u2014 " + vulnerableLabel;
+ }
+ permissionMap.set(permString, name);
+ }
+ }
+ }
+
+ let entries = Array.from(permissionMap, item => ({ name: item[1], permission: item[0] }));
+
+ entries.sort(function(a, b) {
+ return a.name.localeCompare(b.name);
+ });
+
+ let permissionEntries = entries.map(p => fillInPluginPermissionTemplate(p.name, p.permission));
+
+ let permPluginsRow = document.getElementById("perm-plugins-row");
+ clearPluginPermissionTemplate();
+ if (permissionEntries.length < 1) {
+ permPluginsRow.hidden = true;
+ return;
+ }
+
+ for (let permissionEntry of permissionEntries) {
+ permPluginsRow.appendChild(permissionEntry);
+ }
+
+ setPluginsRadioState();
+}
+
+function setPluginsRadioState() {
+ let box = document.getElementById("perm-plugins-row");
+ for (let permissionEntry of box.childNodes) {
+ if (permissionEntry.hasAttribute("permString")) {
+ let permString = permissionEntry.getAttribute("permString");
+ let permission = SitePermissions.get(gPermURI, permString);
+ setRadioState(permString, permission);
+ }
+ }
+}
diff --git a/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js
new file mode 100644
index 000000000..5295a8fe6
--- /dev/null
+++ b/browser/base/content/pageinfo/security.js
@@ -0,0 +1,388 @@
+/* -*- 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/. */
+
+Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+var security = {
+ init: function(uri, windowInfo) {
+ this.uri = uri;
+ this.windowInfo = windowInfo;
+ },
+
+ // Display the server certificate (static)
+ viewCert : function () {
+ var cert = security._cert;
+ viewCertHelper(window, cert);
+ },
+
+ _getSecurityInfo : function() {
+ const nsISSLStatusProvider = Components.interfaces.nsISSLStatusProvider;
+ const nsISSLStatus = Components.interfaces.nsISSLStatus;
+
+ // We don't have separate info for a frame, return null until further notice
+ // (see bug 138479)
+ if (!this.windowInfo.isTopWindow)
+ return null;
+
+ var hostName = this.windowInfo.hostName;
+
+ var ui = security._getSecurityUI();
+ if (!ui)
+ return null;
+
+ var isBroken =
+ (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IS_BROKEN);
+ var isMixed =
+ (ui.state & (Components.interfaces.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT |
+ Components.interfaces.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT));
+ var isInsecure =
+ (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IS_INSECURE);
+ var isEV =
+ (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL);
+ ui.QueryInterface(nsISSLStatusProvider);
+ var status = ui.SSLStatus;
+
+ if (!isInsecure && status) {
+ status.QueryInterface(nsISSLStatus);
+ var cert = status.serverCert;
+ var issuerName =
+ this.mapIssuerOrganization(cert.issuerOrganization) || cert.issuerName;
+
+ var retval = {
+ hostName : hostName,
+ cAName : issuerName,
+ encryptionAlgorithm : undefined,
+ encryptionStrength : undefined,
+ version: undefined,
+ isBroken : isBroken,
+ isMixed : isMixed,
+ isEV : isEV,
+ cert : cert,
+ certificateTransparency : undefined
+ };
+
+ var version;
+ try {
+ retval.encryptionAlgorithm = status.cipherName;
+ retval.encryptionStrength = status.secretKeyLength;
+ version = status.protocolVersion;
+ }
+ catch (e) {
+ }
+
+ switch (version) {
+ case nsISSLStatus.SSL_VERSION_3:
+ retval.version = "SSL 3";
+ break;
+ case nsISSLStatus.TLS_VERSION_1:
+ retval.version = "TLS 1.0";
+ break;
+ case nsISSLStatus.TLS_VERSION_1_1:
+ retval.version = "TLS 1.1";
+ break;
+ case nsISSLStatus.TLS_VERSION_1_2:
+ retval.version = "TLS 1.2"
+ break;
+ case nsISSLStatus.TLS_VERSION_1_3:
+ retval.version = "TLS 1.3"
+ break;
+ }
+
+ // Select status text to display for Certificate Transparency.
+ switch (status.certificateTransparencyStatus) {
+ case nsISSLStatus.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE:
+ // CT compliance checks were not performed,
+ // do not display any status text.
+ retval.certificateTransparency = null;
+ break;
+ case nsISSLStatus.CERTIFICATE_TRANSPARENCY_NONE:
+ retval.certificateTransparency = "None";
+ break;
+ case nsISSLStatus.CERTIFICATE_TRANSPARENCY_OK:
+ retval.certificateTransparency = "OK";
+ break;
+ case nsISSLStatus.CERTIFICATE_TRANSPARENCY_UNKNOWN_LOG:
+ retval.certificateTransparency = "UnknownLog";
+ break;
+ case nsISSLStatus.CERTIFICATE_TRANSPARENCY_INVALID:
+ retval.certificateTransparency = "Invalid";
+ break;
+ }
+
+ return retval;
+ }
+ return {
+ hostName : hostName,
+ cAName : "",
+ encryptionAlgorithm : "",
+ encryptionStrength : 0,
+ version: "",
+ isBroken : isBroken,
+ isMixed : isMixed,
+ isEV : isEV,
+ cert : null,
+ certificateTransparency : null
+ };
+ },
+
+ // Find the secureBrowserUI object (if present)
+ _getSecurityUI : function() {
+ if (window.opener.gBrowser)
+ return window.opener.gBrowser.securityUI;
+ return null;
+ },
+
+ // Interface for mapping a certificate issuer organization to
+ // the value to be displayed.
+ // Bug 82017 - this implementation should be moved to pipnss C++ code
+ mapIssuerOrganization: function(name) {
+ if (!name) return null;
+
+ if (name == "RSA Data Security, Inc.") return "Verisign, Inc.";
+
+ // No mapping required
+ return name;
+ },
+
+ /**
+ * Open the cookie manager window
+ */
+ viewCookies : function()
+ {
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("Browser:Cookies");
+ var eTLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"].
+ getService(Components.interfaces.nsIEffectiveTLDService);
+
+ var eTLD;
+ try {
+ eTLD = eTLDService.getBaseDomain(this.uri);
+ }
+ catch (e) {
+ // getBaseDomain will fail if the host is an IP address or is empty
+ eTLD = this.uri.asciiHost;
+ }
+
+ if (win) {
+ win.gCookiesWindow.setFilter(eTLD);
+ win.focus();
+ }
+ else
+ window.openDialog("chrome://browser/content/preferences/cookies.xul",
+ "Browser:Cookies", "", {filterString : eTLD});
+ },
+
+ /**
+ * Open the login manager window
+ */
+ viewPasswords : function() {
+ LoginHelper.openPasswordManager(window, this._getSecurityInfo().hostName);
+ },
+
+ _cert : null
+};
+
+function securityOnLoad(uri, windowInfo) {
+ security.init(uri, windowInfo);
+
+ var info = security._getSecurityInfo();
+ if (!info) {
+ document.getElementById("securityTab").hidden = true;
+ return;
+ }
+ document.getElementById("securityTab").hidden = false;
+
+ const pageInfoBundle = document.getElementById("pageinfobundle");
+
+ /* Set Identity section text */
+ setText("security-identity-domain-value", info.hostName);
+
+ var owner, verifier;
+ if (info.cert && !info.isBroken) {
+ // Try to pull out meaningful values. Technically these fields are optional
+ // so we'll employ fallbacks where appropriate. The EV spec states that Org
+ // fields must be specified for subject and issuer so that case is simpler.
+ if (info.isEV) {
+ owner = info.cert.organization;
+ verifier = security.mapIssuerOrganization(info.cAName);
+ }
+ else {
+ // Technically, a non-EV cert might specify an owner in the O field or not,
+ // depending on the CA's issuing policies. However we don't have any programmatic
+ // way to tell those apart, and no policy way to establish which organization
+ // vetting standards are good enough (that's what EV is for) so we default to
+ // treating these certs as domain-validated only.
+ owner = pageInfoBundle.getString("securityNoOwner");
+ verifier = security.mapIssuerOrganization(info.cAName ||
+ info.cert.issuerCommonName ||
+ info.cert.issuerName);
+ }
+ }
+ else {
+ // We don't have valid identity credentials.
+ owner = pageInfoBundle.getString("securityNoOwner");
+ verifier = pageInfoBundle.getString("notset");
+ }
+
+ setText("security-identity-owner-value", owner);
+ setText("security-identity-verifier-value", verifier);
+
+ /* Manage the View Cert button*/
+ var viewCert = document.getElementById("security-view-cert");
+ if (info.cert) {
+ security._cert = info.cert;
+ viewCert.collapsed = false;
+ }
+ else
+ viewCert.collapsed = true;
+
+ /* Set Privacy & History section text */
+ var yesStr = pageInfoBundle.getString("yes");
+ var noStr = pageInfoBundle.getString("no");
+
+ setText("security-privacy-cookies-value",
+ hostHasCookies(uri) ? yesStr : noStr);
+ setText("security-privacy-passwords-value",
+ realmHasPasswords(uri) ? yesStr : noStr);
+
+ var visitCount = previousVisitCount(info.hostName);
+ if (visitCount > 1) {
+ setText("security-privacy-history-value",
+ pageInfoBundle.getFormattedString("securityNVisits", [visitCount.toLocaleString()]));
+ }
+ else if (visitCount == 1) {
+ setText("security-privacy-history-value",
+ pageInfoBundle.getString("securityOneVisit"));
+ }
+ else {
+ setText("security-privacy-history-value", noStr);
+ }
+
+ /* Set the Technical Detail section messages */
+ const pkiBundle = document.getElementById("pkiBundle");
+ var hdr;
+ var msg1;
+ var msg2;
+
+ if (info.isBroken) {
+ if (info.isMixed) {
+ hdr = pkiBundle.getString("pageInfo_MixedContent");
+ msg1 = pkiBundle.getString("pageInfo_MixedContent2");
+ } else {
+ hdr = pkiBundle.getFormattedString("pageInfo_BrokenEncryption",
+ [info.encryptionAlgorithm,
+ info.encryptionStrength + "",
+ info.version]);
+ msg1 = pkiBundle.getString("pageInfo_WeakCipher");
+ }
+ msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
+ }
+ else if (info.encryptionStrength > 0) {
+ hdr = pkiBundle.getFormattedString("pageInfo_EncryptionWithBitsAndProtocol",
+ [info.encryptionAlgorithm,
+ info.encryptionStrength + "",
+ info.version]);
+ msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1");
+ msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
+ security._cert = info.cert;
+ }
+ else {
+ hdr = pkiBundle.getString("pageInfo_NoEncryption");
+ if (info.hostName != null)
+ msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [info.hostName]);
+ else
+ msg1 = pkiBundle.getString("pageInfo_Privacy_None4");
+ msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
+ }
+ setText("security-technical-shortform", hdr);
+ setText("security-technical-longform1", msg1);
+ setText("security-technical-longform2", msg2);
+
+ const ctStatus =
+ document.getElementById("security-technical-certificate-transparency");
+ if (info.certificateTransparency) {
+ ctStatus.hidden = false;
+ ctStatus.value = pkiBundle.getString(
+ "pageInfo_CertificateTransparency_" + info.certificateTransparency);
+ } else {
+ ctStatus.hidden = true;
+ }
+}
+
+function setText(id, value)
+{
+ var element = document.getElementById(id);
+ if (!element)
+ return;
+ if (element.localName == "textbox" || element.localName == "label")
+ element.value = value;
+ else {
+ if (element.hasChildNodes())
+ element.removeChild(element.firstChild);
+ var textNode = document.createTextNode(value);
+ element.appendChild(textNode);
+ }
+}
+
+function viewCertHelper(parent, cert)
+{
+ if (!cert)
+ return;
+
+ var cd = Components.classes[CERTIFICATEDIALOGS_CONTRACTID].getService(nsICertificateDialogs);
+ cd.viewCert(parent, cert);
+}
+
+/**
+ * Return true iff we have cookies for uri
+ */
+function hostHasCookies(uri) {
+ var cookieManager = Components.classes["@mozilla.org/cookiemanager;1"]
+ .getService(Components.interfaces.nsICookieManager2);
+
+ return cookieManager.countCookiesFromHost(uri.asciiHost) > 0;
+}
+
+/**
+ * Return true iff realm (proto://host:port) (extracted from uri) has
+ * saved passwords
+ */
+function realmHasPasswords(uri) {
+ var passwordManager = Components.classes["@mozilla.org/login-manager;1"]
+ .getService(Components.interfaces.nsILoginManager);
+ return passwordManager.countLogins(uri.prePath, "", "") > 0;
+}
+
+/**
+ * Return the number of previous visits recorded for host before today.
+ *
+ * @param host - the domain name to look for in history
+ */
+function previousVisitCount(host, endTimeReference) {
+ if (!host)
+ return false;
+
+ var historyService = Components.classes["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Components.interfaces.nsINavHistoryService);
+
+ var options = historyService.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_VISIT;
+
+ // Search for visits to this host before today
+ var query = historyService.getNewQuery();
+ query.endTimeReference = query.TIME_RELATIVE_TODAY;
+ query.endTime = 0;
+ query.domain = host;
+
+ var result = historyService.executeQuery(query, options);
+ result.root.containerOpen = true;
+ var cc = result.root.childCount;
+ result.root.containerOpen = false;
+ return cc;
+}
diff --git a/browser/base/content/popup-notifications.inc b/browser/base/content/popup-notifications.inc
new file mode 100644
index 000000000..bdc2d0bd3
--- /dev/null
+++ b/browser/base/content/popup-notifications.inc
@@ -0,0 +1,81 @@
+# to be included inside a popupset element
+
+ <panel id="notification-popup"
+ type="arrow"
+ position="after_start"
+ hidden="true"
+ orient="vertical"
+ noautofocus="true"
+ role="alert"/>
+
+ <popupnotification id="webRTC-shareDevices-notification" hidden="true">
+ <popupnotificationcontent id="webRTC-selectCamera" orient="vertical">
+ <label value="&getUserMedia.selectCamera.label;"
+ accesskey="&getUserMedia.selectCamera.accesskey;"
+ control="webRTC-selectCamera-menulist"/>
+ <menulist id="webRTC-selectCamera-menulist">
+ <menupopup id="webRTC-selectCamera-menupopup"/>
+ </menulist>
+ </popupnotificationcontent>
+
+ <popupnotificationcontent id="webRTC-selectWindowOrScreen" orient="vertical">
+ <label id="webRTC-selectWindow-label"
+ control="webRTC-selectWindow-menulist"/>
+ <menulist id="webRTC-selectWindow-menulist"
+ oncommand="webrtcUI.updateMainActionLabel(this);">
+ <menupopup id="webRTC-selectWindow-menupopup"/>
+ </menulist>
+ <description id="webRTC-all-windows-shared" hidden="true">&getUserMedia.allWindowsShared.message;</description>
+ </popupnotificationcontent>
+
+ <popupnotificationcontent id="webRTC-preview" hidden="true">
+ <html:video id="webRTC-previewVideo"/>
+ <vbox id="webRTC-previewWarningBox">
+ <spacer flex="1"/>
+ <description id="webRTC-previewWarning"/>
+ </vbox>
+ </popupnotificationcontent>
+
+ <popupnotificationcontent id="webRTC-selectMicrophone" orient="vertical">
+ <label value="&getUserMedia.selectMicrophone.label;"
+ accesskey="&getUserMedia.selectMicrophone.accesskey;"
+ control="webRTC-selectMicrophone-menulist"/>
+ <menulist id="webRTC-selectMicrophone-menulist">
+ <menupopup id="webRTC-selectMicrophone-menupopup"/>
+ </menulist>
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="servicesInstall-notification" hidden="true">
+ <popupnotificationcontent orient="vertical" align="start">
+ <!-- XXX bug 974146, tests are looking for this, can't remove yet. -->
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="password-notification" hidden="true">
+ <popupnotificationcontent orient="vertical">
+ <textbox id="password-notification-username"/>
+ <textbox id="password-notification-password" type="password" show-content=""/>
+ <checkbox id="password-notification-visibilityToggle" hidden="true"/>
+ </popupnotificationcontent>
+ </popupnotification>
+
+
+ <popupnotification id="addon-progress-notification" hidden="true">
+ <popupnotificationcontent orient="vertical">
+ <progressmeter id="addon-progress-notification-progressmeter"/>
+ <label id="addon-progress-notification-progresstext" crop="end"/>
+ </popupnotificationcontent>
+ <button id="addon-progress-cancel"
+ oncommand="this.parentNode.cancel();"/>
+ <button id="addon-progress-accept" disabled="true"/>
+ </popupnotification>
+
+ <popupnotification id="addon-install-confirmation-notification" hidden="true">
+ <popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
+ <button id="addon-install-confirmation-cancel"
+ oncommand="PopupNotifications.getNotification('addon-install-confirmation').remove();"/>
+ <button id="addon-install-confirmation-accept"
+ oncommand="gXPInstallObserver.acceptInstallation();
+ PopupNotifications.getNotification('addon-install-confirmation').remove();"/>
+ </popupnotification>
diff --git a/browser/base/content/report-phishing-overlay.xul b/browser/base/content/report-phishing-overlay.xul
new file mode 100644
index 000000000..712079f82
--- /dev/null
+++ b/browser/base/content/report-phishing-overlay.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/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % reportphishDTD SYSTEM "chrome://browser/locale/safebrowsing/report-phishing.dtd">
+%reportphishDTD;
+<!ENTITY % safebrowsingDTD SYSTEM "chrome://browser/locale/safebrowsing/phishing-afterload-warning-message.dtd">
+%safebrowsingDTD;
+]>
+
+<overlay id="reportPhishingMenuOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <broadcasterset id="mainBroadcasterSet">
+ <broadcaster id="reportPhishingBroadcaster" disabled="true"/>
+ <broadcaster id="reportPhishingErrorBroadcaster" disabled="true"/>
+ </broadcasterset>
+ <menupopup id="menu_HelpPopup">
+ <menuitem id="menu_HelpPopup_reportPhishingtoolmenu"
+ label="&reportDeceptiveSiteMenu.title;"
+ accesskey="&reportDeceptiveSiteMenu.accesskey;"
+ insertbefore="aboutSeparator"
+ observes="reportPhishingBroadcaster"
+ oncommand="openUILink(gSafeBrowsing.getReportURL('Phish'), event);"
+ onclick="checkForMiddleClick(this, event);"/>
+ <menuitem id="menu_HelpPopup_reportPhishingErrortoolmenu"
+ label="&safeb.palm.notdeceptive.label;"
+ accesskey="&safeb.palm.notdeceptive.accesskey;"
+ insertbefore="aboutSeparator"
+ observes="reportPhishingErrorBroadcaster"
+ oncommand="openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');"
+ onclick="checkForMiddleClick(this, event);"/>
+ </menupopup>
+</overlay>
diff --git a/browser/base/content/safeMode.css b/browser/base/content/safeMode.css
new file mode 100644
index 000000000..4f093a452
--- /dev/null
+++ b/browser/base/content/safeMode.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/. */
+
+#resetProfileFooter {
+ font-weight: bold;
+}
+
diff --git a/browser/base/content/safeMode.js b/browser/base/content/safeMode.js
new file mode 100644
index 000000000..7f34c2c58
--- /dev/null
+++ b/browser/base/content/safeMode.js
@@ -0,0 +1,82 @@
+/* -*- 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 Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+const appStartup = Services.startup;
+
+Cu.import("resource://gre/modules/ResetProfile.jsm");
+
+var defaultToReset = false;
+
+function restartApp() {
+ appStartup.quit(appStartup.eForceQuit | appStartup.eRestart);
+}
+
+function resetProfile() {
+ // Set the reset profile environment variable.
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ env.set("MOZ_RESET_PROFILE_RESTART", "1");
+}
+
+function showResetDialog() {
+ // Prompt the user to confirm the reset.
+ let retVals = {
+ reset: false,
+ };
+ window.openDialog("chrome://global/content/resetProfile.xul", null,
+ "chrome,modal,centerscreen,titlebar,dialog=yes", retVals);
+ if (!retVals.reset)
+ return;
+ resetProfile();
+ restartApp();
+}
+
+function onDefaultButton() {
+ if (defaultToReset) {
+ // Restart to reset the profile.
+ resetProfile();
+ restartApp();
+ // Return false to prevent starting into safe mode while restarting.
+ return false;
+ }
+ // Continue in safe mode. No restart needed.
+ return true;
+}
+
+function onCancel() {
+ appStartup.quit(appStartup.eForceQuit);
+}
+
+function onExtra1() {
+ if (defaultToReset) {
+ // Continue in safe mode
+ window.close();
+ return true;
+ }
+ // The reset dialog will handle starting the reset process if the user confirms.
+ showResetDialog();
+ return false;
+}
+
+function onLoad() {
+ if (appStartup.automaticSafeModeNecessary) {
+ document.getElementById("autoSafeMode").hidden = false;
+ document.getElementById("safeMode").hidden = true;
+ if (ResetProfile.resetSupported()) {
+ document.getElementById("resetProfile").hidden = false;
+ } else {
+ // Hide the reset button is it's not supported.
+ document.documentElement.getButton("extra1").hidden = true;
+ }
+ } else if (!ResetProfile.resetSupported()) {
+ // Hide the reset button and text if it's not supported.
+ document.documentElement.getButton("extra1").hidden = true;
+ document.getElementById("resetProfileInstead").hidden = true;
+ }
+}
diff --git a/browser/base/content/safeMode.xul b/browser/base/content/safeMode.xul
new file mode 100644
index 000000000..a94de5fba
--- /dev/null
+++ b/browser/base/content/safeMode.xul
@@ -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 prefwindow [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % safeModeDTD SYSTEM "chrome://browser/locale/safeMode.dtd" >
+%safeModeDTD;
+<!ENTITY % resetProfileDTD SYSTEM "chrome://global/locale/resetProfile.dtd" >
+%resetProfileDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://browser/content/safeMode.css"?>
+
+<dialog id="safeModeDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&safeModeDialog.title;"
+ buttons="accept,extra1"
+ buttonlabelaccept="&startSafeMode.label;"
+ buttonlabelextra1="&refreshProfile.label;"
+ maxwidth="&window.maxWidth;"
+ ondialogaccept="return onDefaultButton()"
+ ondialogcancel="onCancel();"
+ ondialogextra1="return onExtra1()"
+ onload="onLoad()">
+
+ <script type="application/javascript" src="chrome://global/content/resetProfile.js"/>
+ <script type="application/javascript" src="chrome://browser/content/safeMode.js"/>
+
+ <vbox id="autoSafeMode" hidden="true">
+ <description>&autoSafeModeDescription3.label;</description>
+ </vbox>
+
+ <vbox id="safeMode">
+ <label>&safeModeDescription3.label;</label>
+ <separator class="thin"/>
+ <label>&safeModeDescription4.label;</label>
+ <separator class="thin"/>
+ <label id="resetProfileInstead">&refreshProfileInstead.label;</label>
+ </vbox>
+
+ <vbox id="resetProfile" hidden="true">
+ <label id="resetProfileInstead">&refreshProfileInstead.label;</label>
+ </vbox>
+
+ <separator class="thin"/>
+</dialog>
diff --git a/browser/base/content/sanitize.js b/browser/base/content/sanitize.js
new file mode 100644
index 000000000..841376580
--- /dev/null
+++ b/browser/base/content/sanitize.js
@@ -0,0 +1,910 @@
+// -*- 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
+ "resource:///modules/DownloadsCommon.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+ "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+
+
+XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+ "@mozilla.org/serviceworkers/manager;1",
+ "nsIServiceWorkerManager");
+XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+ "@mozilla.org/dom/quota-manager-service;1",
+ "nsIQuotaManagerService");
+
+var {classes: Cc, interfaces: Ci, results: Cr} = Components;
+
+/**
+ * A number of iterations after which to yield time back
+ * to the system.
+ */
+const YIELD_PERIOD = 10;
+
+function Sanitizer() {
+}
+Sanitizer.prototype = {
+ // warning to the caller: this one may raise an exception (e.g. bug #265028)
+ clearItem: function (aItemName)
+ {
+ this.items[aItemName].clear();
+ },
+
+ prefDomain: "",
+
+ getNameFromPreference: function (aPreferenceName)
+ {
+ return aPreferenceName.substr(this.prefDomain.length);
+ },
+
+ /**
+ * Deletes privacy sensitive data in a batch, according to user preferences.
+ * Returns a promise which is resolved if no errors occurred. If an error
+ * occurs, a message is reported to the console and all other items are still
+ * cleared before the promise is finally rejected.
+ *
+ * @param [optional] itemsToClear
+ * Array of items to be cleared. if specified only those
+ * items get cleared, irrespectively of the preference settings.
+ * @param [optional] options
+ * Object whose properties are options for this sanitization.
+ * TODO (bug 1167238) document options here.
+ */
+ sanitize: Task.async(function*(itemsToClear = null, options = {}) {
+ let progress = options.progress || {};
+ let promise = this._sanitize(itemsToClear, progress);
+
+ // Depending on preferences, the sanitizer may perform asynchronous
+ // work before it starts cleaning up the Places database (e.g. closing
+ // windows). We need to make sure that the connection to that database
+ // hasn't been closed by the time we use it.
+ // Though, if this is a sanitize on shutdown, we already have a blocker.
+ if (!progress.isShutdown) {
+ let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsPIPlacesDatabase)
+ .shutdownClient
+ .jsclient;
+ shutdownClient.addBlocker("sanitize.js: Sanitize",
+ promise,
+ {
+ fetchState: () => ({ progress })
+ }
+ );
+ }
+
+ try {
+ yield promise;
+ } finally {
+ Services.obs.notifyObservers(null, "sanitizer-sanitization-complete", "");
+ }
+ }),
+
+ _sanitize: Task.async(function*(aItemsToClear, progress = {}) {
+ let seenError = false;
+ let itemsToClear;
+ if (Array.isArray(aItemsToClear)) {
+ // Shallow copy the array, as we are going to modify
+ // it in place later.
+ itemsToClear = [...aItemsToClear];
+ } else {
+ let branch = Services.prefs.getBranch(this.prefDomain);
+ itemsToClear = Object.keys(this.items).filter(itemName => {
+ try {
+ return branch.getBoolPref(itemName);
+ } catch (ex) {
+ return false;
+ }
+ });
+ }
+
+ // Store the list of items to clear, in case we are killed before we
+ // get a chance to complete.
+ Preferences.set(Sanitizer.PREF_SANITIZE_IN_PROGRESS,
+ JSON.stringify(itemsToClear));
+
+ // Store the list of items to clear, for debugging/forensics purposes
+ for (let k of itemsToClear) {
+ progress[k] = "ready";
+ }
+
+ // Ensure open windows get cleared first, if they're in our list, so that they don't stick
+ // around in the recently closed windows list, and so we can cancel the whole thing
+ // if the user selects to keep a window open from a beforeunload prompt.
+ let openWindowsIndex = itemsToClear.indexOf("openWindows");
+ if (openWindowsIndex != -1) {
+ itemsToClear.splice(openWindowsIndex, 1);
+ yield this.items.openWindows.clear();
+ progress.openWindows = "cleared";
+ }
+
+ // Cache the range of times to clear
+ let range = null;
+ // If we ignore timespan, clear everything,
+ // otherwise, pick a range.
+ if (!this.ignoreTimespan) {
+ range = this.range || Sanitizer.getClearRange();
+ }
+
+ // For performance reasons we start all the clear tasks at once, then wait
+ // for their promises later.
+ // Some of the clear() calls may raise exceptions (for example bug 265028),
+ // we catch and store them, but continue to sanitize as much as possible.
+ // Callers should check returned errors and give user feedback
+ // about items that could not be sanitized
+ let refObj = {};
+ TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj);
+
+ let annotateError = (name, ex) => {
+ progress[name] = "failed";
+ seenError = true;
+ console.error("Error sanitizing " + name, ex);
+ };
+
+ // Array of objects in form { name, promise }.
+ // `name` is the item's name and `promise` may be a promise, if the
+ // sanitization is asynchronous, or the function return value, otherwise.
+ let handles = [];
+ for (let itemName of itemsToClear) {
+ // Workaround for bug 449811.
+ let name = itemName;
+ let item = this.items[name];
+ try {
+ // Catch errors here, so later we can just loop through these.
+ handles.push({ name,
+ promise: item.clear(range)
+ .then(() => progress[name] = "cleared",
+ ex => annotateError(name, ex))
+ });
+ } catch (ex) {
+ annotateError(name, ex);
+ }
+ }
+ for (let handle of handles) {
+ progress[handle.name] = "blocking";
+ yield handle.promise;
+ }
+
+ // Sanitization is complete.
+ TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
+ // Reset the inProgress preference since we were not killed during
+ // sanitization.
+ Preferences.reset(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
+ progress = {};
+ if (seenError) {
+ throw new Error("Error sanitizing");
+ }
+ }),
+
+ // Time span only makes sense in certain cases. Consumers who want
+ // to only clear some private data can opt in by setting this to false,
+ // and can optionally specify a specific range. If timespan is not ignored,
+ // and range is not set, sanitize() will use the value of the timespan
+ // pref to determine a range
+ ignoreTimespan : true,
+ range : null,
+
+ items: {
+ cache: {
+ clear: Task.async(function* (range) {
+ let seenException;
+ let refObj = {};
+ TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
+
+ try {
+ // Cache doesn't consult timespan, nor does it have the
+ // facility for timespan-based eviction. Wipe it.
+ let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+ .getService(Ci.nsICacheStorageService);
+ cache.clear();
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ let imageCache = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools)
+ .getImgCacheForDocument(null);
+ imageCache.clearCache(false); // true=chrome, false=content
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ TelemetryStopwatch.finish("FX_SANITIZE_CACHE", refObj);
+ if (seenException) {
+ throw seenException;
+ }
+ })
+ },
+
+ cookies: {
+ clear: Task.async(function* (range) {
+ let seenException;
+ let yieldCounter = 0;
+ let refObj = {};
+
+ // Clear cookies.
+ TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj);
+ try {
+ let cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"]
+ .getService(Ci.nsICookieManager);
+ if (range) {
+ // Iterate through the cookies and delete any created after our cutoff.
+ let cookiesEnum = cookieMgr.enumerator;
+ while (cookiesEnum.hasMoreElements()) {
+ let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
+
+ if (cookie.creationTime > range[0]) {
+ // This cookie was created after our cutoff, clear it
+ cookieMgr.remove(cookie.host, cookie.name, cookie.path,
+ false, cookie.originAttributes);
+
+ if (++yieldCounter % YIELD_PERIOD == 0) {
+ yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
+ }
+ }
+ }
+ }
+ else {
+ // Remove everything
+ cookieMgr.removeAll();
+ yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
+ }
+ } catch (ex) {
+ seenException = ex;
+ } finally {
+ TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj);
+ }
+
+ // Clear deviceIds. Done asynchronously (returns before complete).
+ try {
+ let mediaMgr = Components.classes["@mozilla.org/mediaManagerService;1"]
+ .getService(Ci.nsIMediaManagerService);
+ mediaMgr.sanitizeDeviceIds(range && range[0]);
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ // Clear plugin data.
+ // As evidenced in bug 1253204, clearing plugin data can sometimes be
+ // very, very long, for mysterious reasons. Unfortunately, this is not
+ // something actionable by Mozilla, so crashing here serves no purpose.
+ //
+ // For this reason, instead of waiting for sanitization to always
+ // complete, we introduce a soft timeout. Once this timeout has
+ // elapsed, we proceed with the shutdown of Firefox.
+ let promiseClearPluginCookies;
+ try {
+ // We don't want to wait for this operation to complete...
+ promiseClearPluginCookies = this.promiseClearPluginCookies(range);
+
+ // ... at least, not for more than 10 seconds.
+ yield Promise.race([
+ promiseClearPluginCookies,
+ new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */))
+ ]);
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ // Detach waiting for plugin cookies to be cleared.
+ promiseClearPluginCookies.catch(() => {
+ // If this exception is raised before the soft timeout, it
+ // will appear in `seenException`. Otherwise, it's too late
+ // to do anything about it.
+ });
+
+ if (seenException) {
+ throw seenException;
+ }
+ }),
+
+ promiseClearPluginCookies: Task.async(function* (range) {
+ const FLAG_CLEAR_ALL = Ci.nsIPluginHost.FLAG_CLEAR_ALL;
+ let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+
+ // Determine age range in seconds. (-1 means clear all.) We don't know
+ // that range[1] is actually now, so we compute age range based
+ // on the lower bound. If range results in a negative age, do nothing.
+ let age = range ? (Date.now() / 1000 - range[0] / 1000000) : -1;
+ if (!range || age >= 0) {
+ let tags = ph.getPluginTags();
+ for (let tag of tags) {
+ let refObj = {};
+ let probe = "";
+ if (/\bFlash\b/.test(tag.name)) {
+ probe = tag.loaded ? "FX_SANITIZE_LOADED_FLASH"
+ : "FX_SANITIZE_UNLOADED_FLASH";
+ TelemetryStopwatch.start(probe, refObj);
+ }
+ try {
+ let rv = yield new Promise(resolve =>
+ ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve)
+ );
+ // If the plugin doesn't support clearing by age, clear everything.
+ if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
+ yield new Promise(resolve =>
+ ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve)
+ );
+ }
+ if (probe) {
+ TelemetryStopwatch.finish(probe, refObj);
+ }
+ } catch (ex) {
+ // Ignore errors from plug-ins
+ if (probe) {
+ TelemetryStopwatch.cancel(probe, refObj);
+ }
+ }
+ }
+ }
+ })
+ },
+
+ offlineApps: {
+ clear: Task.async(function* (range) {
+ // AppCache
+ Components.utils.import("resource:///modules/offlineAppCache.jsm");
+ // This doesn't wait for the cleanup to be complete.
+ OfflineAppCacheHelper.clear();
+
+ // LocalStorage
+ Services.obs.notifyObservers(null, "extension:purge-localStorage", null);
+
+ // ServiceWorkers
+ let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+ for (let i = 0; i < serviceWorkers.length; i++) {
+ let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+ let host = sw.principal.URI.host;
+ serviceWorkerManager.removeAndPropagate(host);
+ }
+
+ // QuotaManager
+ let promises = [];
+ yield new Promise(resolve => {
+ quotaManagerService.getUsage(request => {
+ if (request.resultCode != Cr.NS_OK) {
+ // We are probably shutting down. We don't want to propagate the
+ // error, rejecting the promise.
+ resolve();
+ return;
+ }
+
+ for (let item of request.result) {
+ let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
+ let uri = principal.URI;
+ if (uri.scheme == "http" || uri.scheme == "https" || uri.scheme == "file") {
+ promises.push(new Promise(r => {
+ let req = quotaManagerService.clearStoragesForPrincipal(principal, null, true);
+ req.callback = () => { r(); };
+ }));
+ }
+ }
+ resolve();
+ });
+ });
+
+ yield Promise.all(promises);
+ })
+ },
+
+ history: {
+ clear: Task.async(function* (range) {
+ let seenException;
+ let refObj = {};
+ TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj);
+ try {
+ if (range) {
+ yield PlacesUtils.history.removeVisitsByFilter({
+ beginDate: new Date(range[0] / 1000),
+ endDate: new Date(range[1] / 1000)
+ });
+ } else {
+ // Remove everything.
+ yield PlacesUtils.history.clear();
+ }
+ } catch (ex) {
+ seenException = ex;
+ } finally {
+ TelemetryStopwatch.finish("FX_SANITIZE_HISTORY", refObj);
+ }
+
+ try {
+ let clearStartingTime = range ? String(range[0]) : "";
+ Services.obs.notifyObservers(null, "browser:purge-session-history", clearStartingTime);
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ let predictor = Components.classes["@mozilla.org/network/predictor;1"]
+ .getService(Components.interfaces.nsINetworkPredictor);
+ predictor.reset();
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ if (seenException) {
+ throw seenException;
+ }
+ })
+ },
+
+ formdata: {
+ clear: Task.async(function* (range) {
+ let seenException;
+ let refObj = {};
+ TelemetryStopwatch.start("FX_SANITIZE_FORMDATA", refObj);
+ try {
+ // Clear undo history of all searchBars
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let currentWindow = windows.getNext();
+ let currentDocument = currentWindow.document;
+ let searchBar = currentDocument.getElementById("searchbar");
+ if (searchBar)
+ searchBar.textbox.reset();
+ let tabBrowser = currentWindow.gBrowser;
+ if (!tabBrowser) {
+ // No tab browser? This means that it's too early during startup (typically,
+ // Session Restore hasn't completed yet). Since we don't have find
+ // bars at that stage and since Session Restore will not restore
+ // find bars further down during startup, we have nothing to clear.
+ continue;
+ }
+ for (let tab of tabBrowser.tabs) {
+ if (tabBrowser.isFindBarInitialized(tab))
+ tabBrowser.getFindBar(tab).clear();
+ }
+ // Clear any saved find value
+ tabBrowser._lastFindValue = "";
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ let change = { op: "remove" };
+ if (range) {
+ [ change.firstUsedStart, change.firstUsedEnd ] = range;
+ }
+ yield new Promise(resolve => {
+ FormHistory.update(change, {
+ handleError(e) {
+ seenException = new Error("Error " + e.result + ": " + e.message);
+ },
+ handleCompletion() {
+ resolve();
+ }
+ });
+ });
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA", refObj);
+ if (seenException) {
+ throw seenException;
+ }
+ })
+ },
+
+ downloads: {
+ clear: Task.async(function* (range) {
+ let refObj = {};
+ TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
+ try {
+ let filterByTime = null;
+ if (range) {
+ // Convert microseconds back to milliseconds for date comparisons.
+ let rangeBeginMs = range[0] / 1000;
+ let rangeEndMs = range[1] / 1000;
+ filterByTime = download => download.startTime >= rangeBeginMs &&
+ download.startTime <= rangeEndMs;
+ }
+
+ // Clear all completed/cancelled downloads
+ let list = yield Downloads.getList(Downloads.ALL);
+ list.removeFinished(filterByTime);
+ } finally {
+ TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
+ }
+ })
+ },
+
+ sessions: {
+ clear: Task.async(function* (range) {
+ let refObj = {};
+ TelemetryStopwatch.start("FX_SANITIZE_SESSIONS", refObj);
+
+ try {
+ // clear all auth tokens
+ let sdr = Components.classes["@mozilla.org/security/sdr;1"]
+ .getService(Components.interfaces.nsISecretDecoderRing);
+ sdr.logoutAndTeardown();
+
+ // clear FTP and plain HTTP auth sessions
+ Services.obs.notifyObservers(null, "net:clear-active-logins", null);
+ } finally {
+ TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS", refObj);
+ }
+ })
+ },
+
+ siteSettings: {
+ clear: Task.async(function* (range) {
+ let seenException;
+ let refObj = {};
+ TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS", refObj);
+
+ let startDateMS = range ? range[0] / 1000 : null;
+
+ try {
+ // Clear site-specific permissions like "Allow this site to open popups"
+ // we ignore the "end" range and hope it is now() - none of the
+ // interfaces used here support a true range anyway.
+ if (startDateMS == null) {
+ Services.perms.removeAll();
+ } else {
+ Services.perms.removeAllSince(startDateMS);
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ // Clear site-specific settings like page-zoom level
+ let cps = Components.classes["@mozilla.org/content-pref/service;1"]
+ .getService(Components.interfaces.nsIContentPrefService2);
+ if (startDateMS == null) {
+ cps.removeAllDomains(null);
+ } else {
+ cps.removeAllDomainsSince(startDateMS, null);
+ }
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ try {
+ // Clear site security settings - no support for ranges in this
+ // interface either, so we clearAll().
+ let sss = Cc["@mozilla.org/ssservice;1"]
+ .getService(Ci.nsISiteSecurityService);
+ sss.clearAll();
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ // Clear all push notification subscriptions
+ try {
+ yield new Promise((resolve, reject) => {
+ let push = Cc["@mozilla.org/push/Service;1"]
+ .getService(Ci.nsIPushService);
+ push.clearForDomain("*", status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(new Error("Error clearing push subscriptions: " +
+ status));
+ }
+ });
+ });
+ } catch (ex) {
+ seenException = ex;
+ }
+
+ TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj);
+ if (seenException) {
+ throw seenException;
+ }
+ })
+ },
+
+ openWindows: {
+ privateStateForNewWindow: "non-private",
+ _canCloseWindow: function(aWindow) {
+ if (aWindow.CanCloseWindow()) {
+ // We already showed PermitUnload for the window, so let's
+ // make sure we don't do it again when we actually close the
+ // window.
+ aWindow.skipNextCanClose = true;
+ return true;
+ }
+ return false;
+ },
+ _resetAllWindowClosures: function(aWindowList) {
+ for (let win of aWindowList) {
+ win.skipNextCanClose = false;
+ }
+ },
+ clear: Task.async(function* () {
+ // NB: this closes all *browser* windows, not other windows like the library, about window,
+ // browser console, etc.
+
+ // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload
+ // dialogs
+ let existingWindow = Services.appShell.hiddenDOMWindow;
+ let startDate = existingWindow.performance.now();
+
+ // First check if all these windows are OK with being closed:
+ let windowEnumerator = Services.wm.getEnumerator("navigator:browser");
+ let windowList = [];
+ while (windowEnumerator.hasMoreElements()) {
+ let someWin = windowEnumerator.getNext();
+ windowList.push(someWin);
+ // If someone says "no" to a beforeunload prompt, we abort here:
+ if (!this._canCloseWindow(someWin)) {
+ this._resetAllWindowClosures(windowList);
+ throw new Error("Sanitize could not close windows: cancelled by user");
+ }
+
+ // ...however, beforeunload prompts spin the event loop, and so the code here won't get
+ // hit until the prompt has been dismissed. If more than 1 minute has elapsed since we
+ // started prompting, stop, because the user might not even remember initiating the
+ // 'forget', and the timespans will be all wrong by now anyway:
+ if (existingWindow.performance.now() > (startDate + 60 * 1000)) {
+ this._resetAllWindowClosures(windowList);
+ throw new Error("Sanitize could not close windows: timeout");
+ }
+ }
+
+ // If/once we get here, we should actually be able to close all windows.
+
+ let refObj = {};
+ TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS", refObj);
+
+ // First create a new window. We do this first so that on non-mac, we don't
+ // accidentally close the app by closing all the windows.
+ let handler = Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler);
+ let defaultArgs = handler.defaultArgs;
+ let features = "chrome,all,dialog=no," + this.privateStateForNewWindow;
+ let newWindow = existingWindow.openDialog("chrome://browser/content/", "_blank",
+ features, defaultArgs);
+
+ let onFullScreen = null;
+ if (AppConstants.platform == "macosx") {
+ onFullScreen = function(e) {
+ newWindow.removeEventListener("fullscreen", onFullScreen);
+ let docEl = newWindow.document.documentElement;
+ let sizemode = docEl.getAttribute("sizemode");
+ if (!newWindow.fullScreen && sizemode == "fullscreen") {
+ docEl.setAttribute("sizemode", "normal");
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ return undefined;
+ }
+ newWindow.addEventListener("fullscreen", onFullScreen);
+ }
+
+ let promiseReady = new Promise(resolve => {
+ // Window creation and destruction is asynchronous. We need to wait
+ // until all existing windows are fully closed, and the new window is
+ // fully open, before continuing. Otherwise the rest of the sanitizer
+ // could run too early (and miss new cookies being set when a page
+ // closes) and/or run too late (and not have a fully-formed window yet
+ // in existence). See bug 1088137.
+ let newWindowOpened = false;
+ let onWindowOpened = function(subject, topic, data) {
+ if (subject != newWindow)
+ return;
+
+ Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished");
+ if (AppConstants.platform == "macosx") {
+ newWindow.removeEventListener("fullscreen", onFullScreen);
+ }
+ newWindowOpened = true;
+ // If we're the last thing to happen, invoke callback.
+ if (numWindowsClosing == 0) {
+ TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj);
+ resolve();
+ }
+ }
+
+ let numWindowsClosing = windowList.length;
+ let onWindowClosed = function() {
+ numWindowsClosing--;
+ if (numWindowsClosing == 0) {
+ Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
+ // If we're the last thing to happen, invoke callback.
+ if (newWindowOpened) {
+ TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj);
+ resolve();
+ }
+ }
+ }
+ Services.obs.addObserver(onWindowOpened, "browser-delayed-startup-finished", false);
+ Services.obs.addObserver(onWindowClosed, "xul-window-destroyed", false);
+ });
+
+ // Start the process of closing windows
+ while (windowList.length) {
+ windowList.pop().close();
+ }
+ newWindow.focus();
+ yield promiseReady;
+ })
+ },
+ }
+};
+
+// The preferences branch for the sanitizer.
+Sanitizer.PREF_DOMAIN = "privacy.sanitize.";
+// Whether we should sanitize on shutdown.
+Sanitizer.PREF_SANITIZE_ON_SHUTDOWN = "privacy.sanitize.sanitizeOnShutdown";
+// During a sanitization this is set to a json containing the array of items
+// being sanitized, then cleared once the sanitization is complete.
+// This allows to retry a sanitization on startup in case it was interrupted
+// by a crash.
+Sanitizer.PREF_SANITIZE_IN_PROGRESS = "privacy.sanitize.sanitizeInProgress";
+// Whether the previous shutdown sanitization completed successfully.
+// This is used to detect cases where we were supposed to sanitize on shutdown
+// but due to a crash we were unable to. In such cases there may not be any
+// sanitization in progress, cause we didn't have a chance to start it yet.
+Sanitizer.PREF_SANITIZE_DID_SHUTDOWN = "privacy.sanitize.didShutdownSanitize";
+
+// Time span constants corresponding to values of the privacy.sanitize.timeSpan
+// pref. Used to determine how much history to clear, for various items
+Sanitizer.TIMESPAN_EVERYTHING = 0;
+Sanitizer.TIMESPAN_HOUR = 1;
+Sanitizer.TIMESPAN_2HOURS = 2;
+Sanitizer.TIMESPAN_4HOURS = 3;
+Sanitizer.TIMESPAN_TODAY = 4;
+Sanitizer.TIMESPAN_5MIN = 5;
+Sanitizer.TIMESPAN_24HOURS = 6;
+
+// Return a 2 element array representing the start and end times,
+// in the uSec-since-epoch format that PRTime likes. If we should
+// clear everything, return null. Use ts if it is defined; otherwise
+// use the timeSpan pref.
+Sanitizer.getClearRange = function (ts) {
+ if (ts === undefined)
+ ts = Sanitizer.prefs.getIntPref("timeSpan");
+ if (ts === Sanitizer.TIMESPAN_EVERYTHING)
+ return null;
+
+ // PRTime is microseconds while JS time is milliseconds
+ var endDate = Date.now() * 1000;
+ switch (ts) {
+ case Sanitizer.TIMESPAN_5MIN :
+ var startDate = endDate - 300000000; // 5*60*1000000
+ break;
+ case Sanitizer.TIMESPAN_HOUR :
+ startDate = endDate - 3600000000; // 1*60*60*1000000
+ break;
+ case Sanitizer.TIMESPAN_2HOURS :
+ startDate = endDate - 7200000000; // 2*60*60*1000000
+ break;
+ case Sanitizer.TIMESPAN_4HOURS :
+ startDate = endDate - 14400000000; // 4*60*60*1000000
+ break;
+ case Sanitizer.TIMESPAN_TODAY :
+ var d = new Date(); // Start with today
+ d.setHours(0); // zero us back to midnight...
+ d.setMinutes(0);
+ d.setSeconds(0);
+ startDate = d.valueOf() * 1000; // convert to epoch usec
+ break;
+ case Sanitizer.TIMESPAN_24HOURS :
+ startDate = endDate - 86400000000; // 24*60*60*1000000
+ break;
+ default:
+ throw "Invalid time span for clear private data: " + ts;
+ }
+ return [startDate, endDate];
+};
+
+Sanitizer._prefs = null;
+Sanitizer.__defineGetter__("prefs", function()
+{
+ return Sanitizer._prefs ? Sanitizer._prefs
+ : Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService)
+ .getBranch(Sanitizer.PREF_DOMAIN);
+});
+
+// Shows sanitization UI
+Sanitizer.showUI = function(aParentWindow)
+{
+ let win = AppConstants.platform == "macosx" ?
+ null: // make this an app-modal window on Mac
+ aParentWindow;
+ Services.ww.openWindow(win,
+ "chrome://browser/content/sanitize.xul",
+ "Sanitize",
+ "chrome,titlebar,dialog,centerscreen,modal",
+ null);
+};
+
+/**
+ * Deletes privacy sensitive data in a batch, optionally showing the
+ * sanitize UI, according to user preferences
+ */
+Sanitizer.sanitize = function(aParentWindow)
+{
+ Sanitizer.showUI(aParentWindow);
+};
+
+Sanitizer.onStartup = Task.async(function*() {
+ // Check if we were interrupted during the last shutdown sanitization.
+ let shutownSanitizationWasInterrupted =
+ Preferences.get(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false) &&
+ !Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
+
+ if (Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) {
+ // Reset the pref, so that if we crash before having a chance to
+ // sanitize on shutdown, we will do at the next startup.
+ // Flushing prefs has a cost, so do this only if necessary.
+ Preferences.reset(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
+ Services.prefs.savePrefFile(null);
+ }
+
+ // Make sure that we are triggered during shutdown.
+ let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsPIPlacesDatabase)
+ .shutdownClient
+ .jsclient;
+ // We need to pass to sanitize() (through sanitizeOnShutdown) a state object
+ // that tracks the status of the shutdown blocker. This `progress` object
+ // will be updated during sanitization and reported with the crash in case of
+ // a shutdown timeout.
+ // We use the `options` argument to pass the `progress` object to sanitize().
+ let progress = { isShutdown: true };
+ shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
+ () => sanitizeOnShutdown({ progress }),
+ {
+ fetchState: () => ({ progress })
+ }
+ );
+
+ // Check if Firefox crashed during a sanitization.
+ let lastInterruptedSanitization = Preferences.get(Sanitizer.PREF_SANITIZE_IN_PROGRESS, "");
+ if (lastInterruptedSanitization) {
+ let s = new Sanitizer();
+ // If the json is invalid this will just throw and reject the Task.
+ let itemsToClear = JSON.parse(lastInterruptedSanitization);
+ yield s.sanitize(itemsToClear);
+ } else if (shutownSanitizationWasInterrupted) {
+ // Otherwise, could be we were supposed to sanitize on shutdown but we
+ // didn't have a chance, due to an earlier crash.
+ // In such a case, just redo a shutdown sanitize now, during startup.
+ yield sanitizeOnShutdown();
+ }
+});
+
+var sanitizeOnShutdown = Task.async(function*(options = {}) {
+ if (!Preferences.get(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN)) {
+ return;
+ }
+ // Need to sanitize upon shutdown
+ let s = new Sanitizer();
+ s.prefDomain = "privacy.clearOnShutdown.";
+ yield s.sanitize(null, options);
+ // We didn't crash during shutdown sanitization, so annotate it to avoid
+ // sanitizing again on startup.
+ Preferences.set(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true);
+ Services.prefs.savePrefFile(null);
+});
diff --git a/browser/base/content/sanitize.xul b/browser/base/content/sanitize.xul
new file mode 100644
index 000000000..c00c6cda7
--- /dev/null
+++ b/browser/base/content/sanitize.xul
@@ -0,0 +1,183 @@
+<?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/"?>
+<?xml-stylesheet href="chrome://browser/skin/sanitizeDialog.css"?>
+
+#ifdef CRH_DIALOG_TREE_VIEW
+<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
+#endif
+
+<?xml-stylesheet href="chrome://browser/content/sanitizeDialog.css"?>
+
+<!DOCTYPE prefwindow [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ <!ENTITY % sanitizeDTD SYSTEM "chrome://browser/locale/sanitize.dtd">
+ %brandDTD;
+ %sanitizeDTD;
+]>
+
+<prefwindow id="SanitizeDialog" type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ dlgbuttons="accept,cancel"
+ title="&sanitizeDialog2.title;"
+ noneverythingtitle="&sanitizeDialog2.title;"
+ style="width: &sanitizeDialog2.width;;"
+ ondialogaccept="return gSanitizePromptDialog.sanitize();">
+
+ <prefpane id="SanitizeDialogPane" onpaneload="gSanitizePromptDialog.init();">
+ <stringbundle id="bundleBrowser"
+ src="chrome://browser/locale/browser.properties"/>
+
+ <script type="application/javascript"
+ src="chrome://browser/content/sanitize.js"/>
+
+#ifdef CRH_DIALOG_TREE_VIEW
+ <script type="application/javascript"
+ src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/places/treeView.js"/>
+ <script type="application/javascript"><![CDATA[
+ Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+ Components.utils.import("resource:///modules/PlacesUIUtils.jsm");
+ ]]></script>
+#endif
+
+ <script type="application/javascript"
+ src="chrome://browser/content/sanitizeDialog.js"/>
+
+ <preferences id="sanitizePreferences">
+ <preference id="privacy.cpd.history" name="privacy.cpd.history" type="bool"/>
+ <preference id="privacy.cpd.formdata" name="privacy.cpd.formdata" type="bool"/>
+ <preference id="privacy.cpd.downloads" name="privacy.cpd.downloads" type="bool" disabled="true"/>
+ <preference id="privacy.cpd.cookies" name="privacy.cpd.cookies" type="bool"/>
+ <preference id="privacy.cpd.cache" name="privacy.cpd.cache" type="bool"/>
+ <preference id="privacy.cpd.sessions" name="privacy.cpd.sessions" type="bool"/>
+ <preference id="privacy.cpd.offlineApps" name="privacy.cpd.offlineApps" type="bool"/>
+ <preference id="privacy.cpd.siteSettings" name="privacy.cpd.siteSettings" type="bool"/>
+ </preferences>
+
+ <preferences id="nonItemPreferences">
+ <preference id="privacy.sanitize.timeSpan"
+ name="privacy.sanitize.timeSpan"
+ type="int"/>
+ </preferences>
+
+ <hbox id="SanitizeDurationBox" align="center">
+ <label value="&clearTimeDuration.label;"
+ accesskey="&clearTimeDuration.accesskey;"
+ control="sanitizeDurationChoice"
+ id="sanitizeDurationLabel"/>
+ <menulist id="sanitizeDurationChoice"
+ preference="privacy.sanitize.timeSpan"
+ onselect="gSanitizePromptDialog.selectByTimespan();"
+ flex="1">
+ <menupopup id="sanitizeDurationPopup">
+#ifdef CRH_DIALOG_TREE_VIEW
+ <menuitem label="" value="-1" id="sanitizeDurationCustom"/>
+#endif
+ <menuitem label="&clearTimeDuration.lastHour;" value="1"/>
+ <menuitem label="&clearTimeDuration.last2Hours;" value="2"/>
+ <menuitem label="&clearTimeDuration.last4Hours;" value="3"/>
+ <menuitem label="&clearTimeDuration.today;" value="4"/>
+ <menuseparator/>
+ <menuitem label="&clearTimeDuration.everything;" value="0"/>
+ </menupopup>
+ </menulist>
+ <label id="sanitizeDurationSuffixLabel"
+ value="&clearTimeDuration.suffix;"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+#ifdef CRH_DIALOG_TREE_VIEW
+ <deck id="durationDeck">
+ <tree id="placesTree" flex="1" hidecolumnpicker="true" rows="10"
+ disabled="true" disableKeyNavigation="true">
+ <treecols>
+ <treecol id="date" label="&clearTimeDuration.dateColumn;" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="title" label="&clearTimeDuration.nameColumn;" flex="5"/>
+ </treecols>
+ <treechildren id="placesTreechildren"
+ ondragstart="gSanitizePromptDialog.grippyMoved('ondragstart', event);"
+ ondragover="gSanitizePromptDialog.grippyMoved('ondragover', event);"
+ onkeypress="gSanitizePromptDialog.grippyMoved('onkeypress', event);"
+ onmousedown="gSanitizePromptDialog.grippyMoved('onmousedown', event);"/>
+ </tree>
+#endif
+
+ <vbox id="sanitizeEverythingWarningBox">
+ <spacer flex="1"/>
+ <hbox align="center">
+ <image id="sanitizeEverythingWarningIcon"/>
+ <vbox id="sanitizeEverythingWarningDescBox" flex="1">
+ <description id="sanitizeEverythingWarning"/>
+ <description id="sanitizeEverythingUndoWarning">&sanitizeEverythingUndoWarning;</description>
+ </vbox>
+ </hbox>
+ <spacer flex="1"/>
+ </vbox>
+
+#ifdef CRH_DIALOG_TREE_VIEW
+ </deck>
+#endif
+
+ <separator class="thin"/>
+
+ <hbox id="detailsExpanderWrapper" align="center">
+ <button type="image"
+ id="detailsExpander"
+ class="expander-down"
+ persist="class"
+ oncommand="gSanitizePromptDialog.toggleItemList();"/>
+ <label id="detailsExpanderLabel"
+ value="&detailsProgressiveDisclosure.label;"
+ accesskey="&detailsProgressiveDisclosure.accesskey;"
+ control="detailsExpander"/>
+ </hbox>
+ <listbox id="itemList" rows="7" collapsed="true" persist="collapsed">
+ <listitem label="&itemHistoryAndDownloads.label;"
+ type="checkbox"
+ accesskey="&itemHistoryAndDownloads.accesskey;"
+ preference="privacy.cpd.history"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <listitem label="&itemFormSearchHistory.label;"
+ type="checkbox"
+ accesskey="&itemFormSearchHistory.accesskey;"
+ preference="privacy.cpd.formdata"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <listitem label="&itemCookies.label;"
+ type="checkbox"
+ accesskey="&itemCookies.accesskey;"
+ preference="privacy.cpd.cookies"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <listitem label="&itemCache.label;"
+ type="checkbox"
+ accesskey="&itemCache.accesskey;"
+ preference="privacy.cpd.cache"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <listitem label="&itemActiveLogins.label;"
+ type="checkbox"
+ accesskey="&itemActiveLogins.accesskey;"
+ preference="privacy.cpd.sessions"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <listitem label="&itemOfflineApps.label;"
+ type="checkbox"
+ accesskey="&itemOfflineApps.accesskey;"
+ preference="privacy.cpd.offlineApps"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ <listitem label="&itemSitePreferences.label;"
+ type="checkbox"
+ accesskey="&itemSitePreferences.accesskey;"
+ preference="privacy.cpd.siteSettings"
+ noduration="true"
+ onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/>
+ </listbox>
+
+ </prefpane>
+</prefwindow>
diff --git a/browser/base/content/sanitizeDialog.css b/browser/base/content/sanitizeDialog.css
new file mode 100644
index 000000000..a7c17f094
--- /dev/null
+++ b/browser/base/content/sanitizeDialog.css
@@ -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/. */
+
+/* Places tree */
+
+#placesTreechildren {
+ -moz-user-focus: normal;
+}
+
+#placesTreechildren::-moz-tree-cell(grippyRow),
+#placesTreechildren::-moz-tree-cell-text(grippyRow),
+#placesTreechildren::-moz-tree-image(grippyRow) {
+ cursor: grab;
+}
+
+
+/* Sanitize everything warnings */
+
+#sanitizeEverythingWarning,
+#sanitizeEverythingUndoWarning {
+ white-space: pre-wrap;
+}
diff --git a/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js
new file mode 100644
index 000000000..279f1efd6
--- /dev/null
+++ b/browser/base/content/sanitizeDialog.js
@@ -0,0 +1,889 @@
+/* -*- 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 Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+var {Sanitizer} = Cu.import("resource:///modules/Sanitizer.jsm", {});
+
+var gSanitizePromptDialog = {
+
+ get bundleBrowser()
+ {
+ if (!this._bundleBrowser)
+ this._bundleBrowser = document.getElementById("bundleBrowser");
+ return this._bundleBrowser;
+ },
+
+ get selectedTimespan()
+ {
+ var durList = document.getElementById("sanitizeDurationChoice");
+ return parseInt(durList.value);
+ },
+
+ get sanitizePreferences()
+ {
+ if (!this._sanitizePreferences) {
+ this._sanitizePreferences =
+ document.getElementById("sanitizePreferences");
+ }
+ return this._sanitizePreferences;
+ },
+
+ get warningBox()
+ {
+ return document.getElementById("sanitizeEverythingWarningBox");
+ },
+
+ init: function ()
+ {
+ // This is used by selectByTimespan() to determine if the window has loaded.
+ this._inited = true;
+
+ var s = new Sanitizer();
+ s.prefDomain = "privacy.cpd.";
+
+ document.documentElement.getButton("accept").label =
+ this.bundleBrowser.getString("sanitizeButtonOK");
+
+ if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
+ this.prepareWarning();
+ this.warningBox.hidden = false;
+ document.title =
+ this.bundleBrowser.getString("sanitizeDialog2.everything.title");
+ }
+ else
+ this.warningBox.hidden = true;
+ },
+
+ selectByTimespan: function ()
+ {
+ // This method is the onselect handler for the duration dropdown. As a
+ // result it's called a couple of times before onload calls init().
+ if (!this._inited)
+ return;
+
+ var warningBox = this.warningBox;
+
+ // If clearing everything
+ if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
+ this.prepareWarning();
+ if (warningBox.hidden) {
+ warningBox.hidden = false;
+ window.resizeBy(0, warningBox.boxObject.height);
+ }
+ window.document.title =
+ this.bundleBrowser.getString("sanitizeDialog2.everything.title");
+ return;
+ }
+
+ // If clearing a specific time range
+ if (!warningBox.hidden) {
+ window.resizeBy(0, -warningBox.boxObject.height);
+ warningBox.hidden = true;
+ }
+ window.document.title =
+ window.document.documentElement.getAttribute("noneverythingtitle");
+ },
+
+ sanitize: function ()
+ {
+ // Update pref values before handing off to the sanitizer (bug 453440)
+ this.updatePrefs();
+ var s = new Sanitizer();
+ s.prefDomain = "privacy.cpd.";
+
+ s.range = Sanitizer.getClearRange(this.selectedTimespan);
+ s.ignoreTimespan = !s.range;
+
+ // As the sanitize is async, we disable the buttons, update the label on
+ // the 'accept' button to indicate things are happening and return false -
+ // once the async operation completes (either with or without errors)
+ // we close the window.
+ let docElt = document.documentElement;
+ let acceptButton = docElt.getButton("accept");
+ acceptButton.disabled = true;
+ acceptButton.setAttribute("label",
+ this.bundleBrowser.getString("sanitizeButtonClearing"));
+ docElt.getButton("cancel").disabled = true;
+
+ try {
+ s.sanitize().then(null, Components.utils.reportError)
+ .then(() => window.close())
+ .then(null, Components.utils.reportError);
+ } catch (er) {
+ Components.utils.reportError("Exception during sanitize: " + er);
+ return true; // We *do* want to close immediately on error.
+ }
+ },
+
+ /**
+ * If the panel that displays a warning when the duration is "Everything" is
+ * not set up, sets it up. Otherwise does nothing.
+ *
+ * @param aDontShowItemList Whether only the warning message should be updated.
+ * True means the item list visibility status should not
+ * be changed.
+ */
+ prepareWarning: function (aDontShowItemList) {
+ // If the date and time-aware locale warning string is ever used again,
+ // initialize it here. Currently we use the no-visits warning string,
+ // which does not include date and time. See bug 480169 comment 48.
+
+ var warningStringID;
+ if (this.hasNonSelectedItems()) {
+ warningStringID = "sanitizeSelectedWarning";
+ if (!aDontShowItemList)
+ this.showItemList();
+ }
+ else {
+ warningStringID = "sanitizeEverythingWarning2";
+ }
+
+ var warningDesc = document.getElementById("sanitizeEverythingWarning");
+ warningDesc.textContent =
+ this.bundleBrowser.getString(warningStringID);
+ },
+
+ /**
+ * Called when the value of a preference element is synced from the actual
+ * pref. Enables or disables the OK button appropriately.
+ */
+ onReadGeneric: function ()
+ {
+ var found = false;
+
+ // Find any other pref that's checked and enabled.
+ var i = 0;
+ while (!found && i < this.sanitizePreferences.childNodes.length) {
+ var preference = this.sanitizePreferences.childNodes[i];
+
+ found = !!preference.value &&
+ !preference.disabled;
+ i++;
+ }
+
+ try {
+ document.documentElement.getButton("accept").disabled = !found;
+ }
+ catch (e) { }
+
+ // Update the warning prompt if needed
+ this.prepareWarning(true);
+
+ return undefined;
+ },
+
+ /**
+ * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date.
+ * Because the type of this prefwindow is "child" -- and that's needed because
+ * without it the dialog has no OK and Cancel buttons -- the prefs are not
+ * updated on dialogaccept on platforms that don't support instant-apply
+ * (i.e., Windows). We must therefore manually set the prefs from their
+ * corresponding preference elements.
+ */
+ updatePrefs : function ()
+ {
+ var tsPref = document.getElementById("privacy.sanitize.timeSpan");
+ Sanitizer.prefs.setIntPref("timeSpan", this.selectedTimespan);
+
+ // Keep the pref for the download history in sync with the history pref.
+ document.getElementById("privacy.cpd.downloads").value =
+ document.getElementById("privacy.cpd.history").value;
+
+ // Now manually set the prefs from their corresponding preference
+ // elements.
+ var prefs = this.sanitizePreferences.rootBranch;
+ for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) {
+ var p = this.sanitizePreferences.childNodes[i];
+ prefs.setBoolPref(p.name, p.value);
+ }
+ },
+
+ /**
+ * Check if all of the history items have been selected like the default status.
+ */
+ hasNonSelectedItems: function () {
+ let checkboxes = document.querySelectorAll("#itemList > [preference]");
+ for (let i = 0; i < checkboxes.length; ++i) {
+ let pref = document.getElementById(checkboxes[i].getAttribute("preference"));
+ if (!pref.value)
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Show the history items list.
+ */
+ showItemList: function () {
+ var itemList = document.getElementById("itemList");
+ var expanderButton = document.getElementById("detailsExpander");
+
+ if (itemList.collapsed) {
+ expanderButton.className = "expander-up";
+ itemList.setAttribute("collapsed", "false");
+ if (document.documentElement.boxObject.height)
+ window.resizeBy(0, itemList.boxObject.height);
+ }
+ },
+
+ /**
+ * Hide the history items list.
+ */
+ hideItemList: function () {
+ var itemList = document.getElementById("itemList");
+ var expanderButton = document.getElementById("detailsExpander");
+
+ if (!itemList.collapsed) {
+ expanderButton.className = "expander-down";
+ window.resizeBy(0, -itemList.boxObject.height);
+ itemList.setAttribute("collapsed", "true");
+ }
+ },
+
+ /**
+ * Called by the item list expander button to toggle the list's visibility.
+ */
+ toggleItemList: function ()
+ {
+ var itemList = document.getElementById("itemList");
+
+ if (itemList.collapsed)
+ this.showItemList();
+ else
+ this.hideItemList();
+ },
+
+#ifdef CRH_DIALOG_TREE_VIEW
+ // A duration value; used in the same context as Sanitizer.TIMESPAN_HOUR,
+ // Sanitizer.TIMESPAN_2HOURS, et al. This should match the value attribute
+ // of the sanitizeDurationCustom menuitem.
+ get TIMESPAN_CUSTOM()
+ {
+ return -1;
+ },
+
+ get placesTree()
+ {
+ if (!this._placesTree)
+ this._placesTree = document.getElementById("placesTree");
+ return this._placesTree;
+ },
+
+ init: function ()
+ {
+ // This is used by selectByTimespan() to determine if the window has loaded.
+ this._inited = true;
+
+ var s = new Sanitizer();
+ s.prefDomain = "privacy.cpd.";
+
+ document.documentElement.getButton("accept").label =
+ this.bundleBrowser.getString("sanitizeButtonOK");
+
+ this.selectByTimespan();
+ },
+
+ /**
+ * Sets up the hashes this.durationValsToRows, which maps duration values
+ * to rows in the tree, this.durationRowsToVals, which maps rows in
+ * the tree to duration values, and this.durationStartTimes, which maps
+ * duration values to their corresponding start times.
+ */
+ initDurationDropdown: function ()
+ {
+ // First, calculate the start times for each duration.
+ this.durationStartTimes = {};
+ var durVals = [];
+ var durPopup = document.getElementById("sanitizeDurationPopup");
+ var durMenuitems = durPopup.childNodes;
+ for (let i = 0; i < durMenuitems.length; i++) {
+ let durMenuitem = durMenuitems[i];
+ let durVal = parseInt(durMenuitem.value);
+ if (durMenuitem.localName === "menuitem" &&
+ durVal !== Sanitizer.TIMESPAN_EVERYTHING &&
+ durVal !== this.TIMESPAN_CUSTOM) {
+ durVals.push(durVal);
+ let durTimes = Sanitizer.getClearRange(durVal);
+ this.durationStartTimes[durVal] = durTimes[0];
+ }
+ }
+
+ // Sort the duration values ascending. Because one tree index can map to
+ // more than one duration, this ensures that this.durationRowsToVals maps
+ // a row index to the largest duration possible in the code below.
+ durVals.sort();
+
+ // Now calculate the rows in the tree of the durations' start times. For
+ // each duration, we are looking for the node in the tree whose time is the
+ // smallest time greater than or equal to the duration's start time.
+ this.durationRowsToVals = {};
+ this.durationValsToRows = {};
+ var view = this.placesTree.view;
+ // For all rows in the tree except the grippy row...
+ for (let i = 0; i < view.rowCount - 1; i++) {
+ let unfoundDurVals = [];
+ let nodeTime = view.QueryInterface(Ci.nsINavHistoryResultTreeViewer).
+ nodeForTreeIndex(i).time;
+ // For all durations whose rows have not yet been found in the tree, see
+ // if index i is their index. An index may map to more than one duration,
+ // in which case the final duration (the largest) wins.
+ for (let j = 0; j < durVals.length; j++) {
+ let durVal = durVals[j];
+ let durStartTime = this.durationStartTimes[durVal];
+ if (nodeTime < durStartTime) {
+ this.durationValsToRows[durVal] = i - 1;
+ this.durationRowsToVals[i - 1] = durVal;
+ }
+ else
+ unfoundDurVals.push(durVal);
+ }
+ durVals = unfoundDurVals;
+ }
+
+ // If any durations were not found above, then every node in the tree has a
+ // time greater than or equal to the duration. In other words, those
+ // durations include the entire tree (except the grippy row).
+ for (let i = 0; i < durVals.length; i++) {
+ let durVal = durVals[i];
+ this.durationValsToRows[durVal] = view.rowCount - 2;
+ this.durationRowsToVals[view.rowCount - 2] = durVal;
+ }
+ },
+
+ /**
+ * If the Places tree is not set up, sets it up. Otherwise does nothing.
+ */
+ ensurePlacesTreeIsInited: function ()
+ {
+ if (this._placesTreeIsInited)
+ return;
+
+ this._placesTreeIsInited = true;
+
+ // Either "Last Four Hours" or "Today" will have the most history. If
+ // it's been more than 4 hours since today began, "Today" will. Otherwise
+ // "Last Four Hours" will.
+ var times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_TODAY);
+
+ // If it's been less than 4 hours since today began, use the past 4 hours.
+ if (times[1] - times[0] < 14400000000) { // 4*60*60*1000000
+ times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_4HOURS);
+ }
+
+ var histServ = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var query = histServ.getNewQuery();
+ query.beginTimeReference = query.TIME_RELATIVE_EPOCH;
+ query.beginTime = times[0];
+ query.endTimeReference = query.TIME_RELATIVE_EPOCH;
+ query.endTime = times[1];
+ var opts = histServ.getNewQueryOptions();
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ opts.queryType = opts.QUERY_TYPE_HISTORY;
+ var result = histServ.executeQuery(query, opts);
+
+ var view = gContiguousSelectionTreeHelper.setTree(this.placesTree,
+ new PlacesTreeView());
+ result.addObserver(view, false);
+ this.initDurationDropdown();
+ },
+
+ /**
+ * Called on select of the duration dropdown and when grippyMoved() sets a
+ * duration based on the location of the grippy row. Selects all the nodes in
+ * the tree that are contained in the selected duration. If clearing
+ * everything, the warning panel is shown instead.
+ */
+ selectByTimespan: function ()
+ {
+ // This method is the onselect handler for the duration dropdown. As a
+ // result it's called a couple of times before onload calls init().
+ if (!this._inited)
+ return;
+
+ var durDeck = document.getElementById("durationDeck");
+ var durList = document.getElementById("sanitizeDurationChoice");
+ var durVal = parseInt(durList.value);
+ var durCustom = document.getElementById("sanitizeDurationCustom");
+
+ // If grippy row is not at a duration boundary, show the custom menuitem;
+ // otherwise, hide it. Since the user cannot specify a custom duration by
+ // using the dropdown, this conditional is true only when this method is
+ // called onselect from grippyMoved(), so no selection need be made.
+ if (durVal === this.TIMESPAN_CUSTOM) {
+ durCustom.hidden = false;
+ return;
+ }
+ durCustom.hidden = true;
+
+ // If clearing everything, show the warning and change the dialog's title.
+ if (durVal === Sanitizer.TIMESPAN_EVERYTHING) {
+ this.prepareWarning();
+ durDeck.selectedIndex = 1;
+ window.document.title =
+ this.bundleBrowser.getString("sanitizeDialog2.everything.title");
+ document.documentElement.getButton("accept").disabled = false;
+ return;
+ }
+
+ // Otherwise -- if clearing a specific time range -- select that time range
+ // in the tree.
+ this.ensurePlacesTreeIsInited();
+ durDeck.selectedIndex = 0;
+ window.document.title =
+ window.document.documentElement.getAttribute("noneverythingtitle");
+ var durRow = this.durationValsToRows[durVal];
+ gContiguousSelectionTreeHelper.rangedSelect(durRow);
+ gContiguousSelectionTreeHelper.scrollToGrippy();
+
+ // If duration is empty (there are no selected rows), disable the dialog's
+ // OK button.
+ document.documentElement.getButton("accept").disabled = durRow < 0;
+ },
+
+ sanitize: function ()
+ {
+ // Update pref values before handing off to the sanitizer (bug 453440)
+ this.updatePrefs();
+ var s = new Sanitizer();
+ s.prefDomain = "privacy.cpd.";
+
+ var durList = document.getElementById("sanitizeDurationChoice");
+ var durValue = parseInt(durList.value);
+ s.ignoreTimespan = durValue === Sanitizer.TIMESPAN_EVERYTHING;
+
+ // Set the sanitizer's time range if we're not clearing everything.
+ if (!s.ignoreTimespan) {
+ // If user selected a custom timespan, use that.
+ if (durValue === this.TIMESPAN_CUSTOM) {
+ var view = this.placesTree.view;
+ var now = Date.now() * 1000;
+ // We disable the dialog's OK button if there's no selection, but we'll
+ // handle that case just in... case.
+ if (view.selection.getRangeCount() === 0)
+ s.range = [now, now];
+ else {
+ var startIndexRef = {};
+ // Tree sorted by visit date DEscending, so start time time comes last.
+ view.selection.getRangeAt(0, {}, startIndexRef);
+ view.QueryInterface(Ci.nsINavHistoryResultTreeViewer);
+ var startNode = view.nodeForTreeIndex(startIndexRef.value);
+ s.range = [startNode.time, now];
+ }
+ }
+ // Otherwise use the predetermined range.
+ else
+ s.range = [this.durationStartTimes[durValue], Date.now() * 1000];
+ }
+
+ try {
+ s.sanitize(); // We ignore the resulting Promise
+ } catch (er) {
+ Components.utils.reportError("Exception during sanitize: " + er);
+ }
+ return true;
+ },
+
+ /**
+ * In order to mark the custom Places tree view and its nsINavHistoryResult
+ * for garbage collection, we need to break the reference cycle between the
+ * two.
+ */
+ unload: function ()
+ {
+ let result = this.placesTree.getResult();
+ result.removeObserver(this.placesTree.view);
+ this.placesTree.view = null;
+ },
+
+ /**
+ * Called when the user moves the grippy by dragging it, clicking in the tree,
+ * or on keypress. Updates the duration dropdown so that it displays the
+ * appropriate specific or custom duration.
+ *
+ * @param aEventName
+ * The name of the event whose handler called this method, e.g.,
+ * "ondragstart", "onkeypress", etc.
+ * @param aEvent
+ * The event captured in the event handler.
+ */
+ grippyMoved: function (aEventName, aEvent)
+ {
+ gContiguousSelectionTreeHelper[aEventName](aEvent);
+ var lastSelRow = gContiguousSelectionTreeHelper.getGrippyRow() - 1;
+ var durList = document.getElementById("sanitizeDurationChoice");
+ var durValue = parseInt(durList.value);
+
+ // Multiple durations can map to the same row. Don't update the dropdown
+ // if the current duration is valid for lastSelRow.
+ if ((durValue !== this.TIMESPAN_CUSTOM ||
+ lastSelRow in this.durationRowsToVals) &&
+ (durValue === this.TIMESPAN_CUSTOM ||
+ this.durationValsToRows[durValue] !== lastSelRow)) {
+ // Setting durList.value causes its onselect handler to fire, which calls
+ // selectByTimespan().
+ if (lastSelRow in this.durationRowsToVals)
+ durList.value = this.durationRowsToVals[lastSelRow];
+ else
+ durList.value = this.TIMESPAN_CUSTOM;
+ }
+
+ // If there are no selected rows, disable the dialog's OK button.
+ document.documentElement.getButton("accept").disabled = lastSelRow < 0;
+ }
+#endif
+
+};
+
+
+#ifdef CRH_DIALOG_TREE_VIEW
+/**
+ * A helper for handling contiguous selection in the tree.
+ */
+var gContiguousSelectionTreeHelper = {
+
+ /**
+ * Gets the tree associated with this helper.
+ */
+ get tree()
+ {
+ return this._tree;
+ },
+
+ /**
+ * Sets the tree that this module handles. The tree is assigned a new view
+ * that is equipped to handle contiguous selection. You can pass in an
+ * object that will be used as the prototype of the new view. Otherwise
+ * the tree's current view is used as the prototype.
+ *
+ * @param aTreeElement
+ * The tree element
+ * @param aProtoTreeView
+ * If defined, this will be used as the prototype of the tree's new
+ * view
+ * @return The new view
+ */
+ setTree: function CSTH_setTree(aTreeElement, aProtoTreeView)
+ {
+ this._tree = aTreeElement;
+ var newView = this._makeTreeView(aProtoTreeView || aTreeElement.view);
+ aTreeElement.view = newView;
+ return newView;
+ },
+
+ /**
+ * The index of the row that the grippy occupies. Note that the index of the
+ * last selected row is getGrippyRow() - 1. If getGrippyRow() is 0, then
+ * no selection exists.
+ *
+ * @return The row index of the grippy
+ */
+ getGrippyRow: function CSTH_getGrippyRow()
+ {
+ var sel = this.tree.view.selection;
+ var rangeCount = sel.getRangeCount();
+ if (rangeCount === 0)
+ return 0;
+ if (rangeCount !== 1) {
+ throw "contiguous selection tree helper: getGrippyRow called with " +
+ "multiple selection ranges";
+ }
+ var max = {};
+ sel.getRangeAt(0, {}, max);
+ return max.value + 1;
+ },
+
+ /**
+ * Helper function for the dragover event. Your dragover listener should
+ * call this. It updates the selection in the tree under the mouse.
+ *
+ * @param aEvent
+ * The observed dragover event
+ */
+ ondragover: function CSTH_ondragover(aEvent)
+ {
+ // Without this when dragging on Windows the mouse cursor is a "no" sign.
+ // This makes it a drop symbol.
+ var ds = Cc["@mozilla.org/widget/dragservice;1"].
+ getService(Ci.nsIDragService).
+ getCurrentSession();
+ ds.canDrop = true;
+ ds.dragAction = 0;
+
+ var tbo = this.tree.treeBoxObject;
+ aEvent.QueryInterface(Ci.nsIDOMMouseEvent);
+ var hoverRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
+
+ if (hoverRow < 0)
+ return;
+
+ this.rangedSelect(hoverRow - 1);
+ },
+
+ /**
+ * Helper function for the dragstart event. Your dragstart listener should
+ * call this. It starts a drag session.
+ *
+ * @param aEvent
+ * The observed dragstart event
+ */
+ ondragstart: function CSTH_ondragstart(aEvent)
+ {
+ var tbo = this.tree.treeBoxObject;
+ var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
+
+ if (clickedRow !== this.getGrippyRow())
+ return;
+
+ // This part is a hack. What we really want is a grab and slide, not
+ // drag and drop. Start a move drag session with dummy data and a
+ // dummy region. Set the region's coordinates to (Infinity, Infinity)
+ // so it's drawn offscreen and its size to (1, 1).
+ var arr = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ var trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(null);
+ trans.setTransferData('dummy-flavor', null, 0);
+ arr.appendElement(trans, /* weak = */ false);
+ var reg = Cc["@mozilla.org/gfx/region;1"].
+ createInstance(Ci.nsIScriptableRegion);
+ reg.setToRect(Infinity, Infinity, 1, 1);
+ var ds = Cc["@mozilla.org/widget/dragservice;1"].
+ getService(Ci.nsIDragService);
+ ds.invokeDragSession(aEvent.target, arr, reg, ds.DRAGDROP_ACTION_MOVE);
+ },
+
+ /**
+ * Helper function for the keypress event. Your keypress listener should
+ * call this. Users can use Up, Down, Page Up/Down, Home, and End to move
+ * the bottom of the selection window.
+ *
+ * @param aEvent
+ * The observed keypress event
+ */
+ onkeypress: function CSTH_onkeypress(aEvent)
+ {
+ var grippyRow = this.getGrippyRow();
+ var tbo = this.tree.treeBoxObject;
+ var rangeEnd;
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_VK_HOME:
+ rangeEnd = 0;
+ break;
+ case aEvent.DOM_VK_PAGE_UP:
+ rangeEnd = grippyRow - tbo.getPageLength();
+ break;
+ case aEvent.DOM_VK_UP:
+ rangeEnd = grippyRow - 2;
+ break;
+ case aEvent.DOM_VK_DOWN:
+ rangeEnd = grippyRow;
+ break;
+ case aEvent.DOM_VK_PAGE_DOWN:
+ rangeEnd = grippyRow + tbo.getPageLength();
+ break;
+ case aEvent.DOM_VK_END:
+ rangeEnd = this.tree.view.rowCount - 2;
+ break;
+ default:
+ return;
+ break;
+ }
+
+ aEvent.stopPropagation();
+
+ // First, clip rangeEnd. this.rangedSelect() doesn't clip the range if we
+ // select past the ends of the tree.
+ if (rangeEnd < 0)
+ rangeEnd = -1;
+ else if (this.tree.view.rowCount - 2 < rangeEnd)
+ rangeEnd = this.tree.view.rowCount - 2;
+
+ // Next, (de)select.
+ this.rangedSelect(rangeEnd);
+
+ // Finally, scroll the tree. We always want one row above and below the
+ // grippy row to be visible if possible.
+ if (rangeEnd < grippyRow) // moved up
+ tbo.ensureRowIsVisible(rangeEnd < 0 ? 0 : rangeEnd);
+ else { // moved down
+ if (rangeEnd + 2 < this.tree.view.rowCount)
+ tbo.ensureRowIsVisible(rangeEnd + 2);
+ else if (rangeEnd + 1 < this.tree.view.rowCount)
+ tbo.ensureRowIsVisible(rangeEnd + 1);
+ }
+ },
+
+ /**
+ * Helper function for the mousedown event. Your mousedown listener should
+ * call this. Users can click on individual rows to make the selection
+ * jump to them immediately.
+ *
+ * @param aEvent
+ * The observed mousedown event
+ */
+ onmousedown: function CSTH_onmousedown(aEvent)
+ {
+ var tbo = this.tree.treeBoxObject;
+ var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
+
+ if (clickedRow < 0 || clickedRow >= this.tree.view.rowCount)
+ return;
+
+ if (clickedRow < this.getGrippyRow())
+ this.rangedSelect(clickedRow);
+ else if (clickedRow > this.getGrippyRow())
+ this.rangedSelect(clickedRow - 1);
+ },
+
+ /**
+ * Selects range [0, aEndRow] in the tree. The grippy row will then be at
+ * index aEndRow + 1. aEndRow may be -1, in which case the selection is
+ * cleared and the grippy row will be at index 0.
+ *
+ * @param aEndRow
+ * The range [0, aEndRow] will be selected.
+ */
+ rangedSelect: function CSTH_rangedSelect(aEndRow)
+ {
+ var tbo = this.tree.treeBoxObject;
+ if (aEndRow < 0)
+ this.tree.view.selection.clearSelection();
+ else
+ this.tree.view.selection.rangedSelect(0, aEndRow, false);
+ tbo.invalidateRange(tbo.getFirstVisibleRow(), tbo.getLastVisibleRow());
+ },
+
+ /**
+ * Scrolls the tree so that the grippy row is in the center of the view.
+ */
+ scrollToGrippy: function CSTH_scrollToGrippy()
+ {
+ var rowCount = this.tree.view.rowCount;
+ var tbo = this.tree.treeBoxObject;
+ var pageLen = tbo.getPageLength() ||
+ parseInt(this.tree.getAttribute("rows")) ||
+ 10;
+
+ // All rows fit on a single page.
+ if (rowCount <= pageLen)
+ return;
+
+ var scrollToRow = this.getGrippyRow() - Math.ceil(pageLen / 2.0);
+
+ // Grippy row is in first half of first page.
+ if (scrollToRow < 0)
+ scrollToRow = 0;
+
+ // Grippy row is in last half of last page.
+ else if (rowCount < scrollToRow + pageLen)
+ scrollToRow = rowCount - pageLen;
+
+ tbo.scrollToRow(scrollToRow);
+ },
+
+ /**
+ * Creates a new tree view suitable for contiguous selection. If
+ * aProtoTreeView is specified, it's used as the new view's prototype.
+ * Otherwise the tree's current view is used as the prototype.
+ *
+ * @param aProtoTreeView
+ * Used as the new view's prototype if specified
+ */
+ _makeTreeView: function CSTH__makeTreeView(aProtoTreeView)
+ {
+ var view = aProtoTreeView;
+ var that = this;
+
+ //XXXadw: When Alex gets the grippy icon done, this may or may not change,
+ // depending on how we style it.
+ view.isSeparator = function CSTH_View_isSeparator(aRow)
+ {
+ return aRow === that.getGrippyRow();
+ };
+
+ // rowCount includes the grippy row.
+ view.__defineGetter__("_rowCount", view.__lookupGetter__("rowCount"));
+ view.__defineGetter__("rowCount",
+ function CSTH_View_rowCount()
+ {
+ return this._rowCount + 1;
+ });
+
+ // This has to do with visual feedback in the view itself, e.g., drawing
+ // a small line underneath the dropzone. Not what we want.
+ view.canDrop = function CSTH_View_canDrop() { return false; };
+
+ // No clicking headers to sort the tree or sort feedback on columns.
+ view.cycleHeader = function CSTH_View_cycleHeader() {};
+ view.sortingChanged = function CSTH_View_sortingChanged() {};
+
+ // Override a bunch of methods to account for the grippy row.
+
+ view._getCellProperties = view.getCellProperties;
+ view.getCellProperties =
+ function CSTH_View_getCellProperties(aRow, aCol)
+ {
+ var grippyRow = that.getGrippyRow();
+ if (aRow === grippyRow)
+ return "grippyRow";
+ if (aRow < grippyRow)
+ return this._getCellProperties(aRow, aCol);
+
+ return this._getCellProperties(aRow - 1, aCol);
+ };
+
+ view._getRowProperties = view.getRowProperties;
+ view.getRowProperties =
+ function CSTH_View_getRowProperties(aRow)
+ {
+ var grippyRow = that.getGrippyRow();
+ if (aRow === grippyRow)
+ return "grippyRow";
+
+ if (aRow < grippyRow)
+ return this._getRowProperties(aRow);
+
+ return this._getRowProperties(aRow - 1);
+ };
+
+ view._getCellText = view.getCellText;
+ view.getCellText =
+ function CSTH_View_getCellText(aRow, aCol)
+ {
+ var grippyRow = that.getGrippyRow();
+ if (aRow === grippyRow)
+ return "";
+ aRow = aRow < grippyRow ? aRow : aRow - 1;
+ return this._getCellText(aRow, aCol);
+ };
+
+ view._getImageSrc = view.getImageSrc;
+ view.getImageSrc =
+ function CSTH_View_getImageSrc(aRow, aCol)
+ {
+ var grippyRow = that.getGrippyRow();
+ if (aRow === grippyRow)
+ return "";
+ aRow = aRow < grippyRow ? aRow : aRow - 1;
+ return this._getImageSrc(aRow, aCol);
+ };
+
+ view.isContainer = function CSTH_View_isContainer(aRow) { return false; };
+ view.getParentIndex = function CSTH_View_getParentIndex(aRow) { return -1; };
+ view.getLevel = function CSTH_View_getLevel(aRow) { return 0; };
+ view.hasNextSibling = function CSTH_View_hasNextSibling(aRow, aAfterIndex)
+ {
+ return aRow < this.rowCount - 1;
+ };
+
+ return view;
+ }
+};
+#endif
diff --git a/browser/base/content/social-content.js b/browser/base/content/social-content.js
new file mode 100644
index 000000000..b5fa6a5c4
--- /dev/null
+++ b/browser/base/content/social-content.js
@@ -0,0 +1,172 @@
+/* -*- 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 content script is intended for use by iframes in the share panel. */
+
+var {interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// social frames are always treated as app tabs
+docShell.isAppTab = true;
+
+addEventListener("DOMContentLoaded", function(event) {
+ if (event.target != content.document)
+ return;
+ // Some share panels (e.g. twitter and facebook) check content.opener, and if
+ // it doesn't exist they act like they are in a browser tab. We want them to
+ // act like they are in a dialog (which is the typical case).
+ if (content && !content.opener) {
+ content.opener = content;
+ }
+ hookWindowClose();
+ disableDialogs();
+});
+
+addMessageListener("Social:OpenGraphData", (message) => {
+ let ev = new content.CustomEvent("OpenGraphData", { detail: JSON.stringify(message.data) });
+ content.dispatchEvent(ev);
+});
+
+addMessageListener("Social:ClearFrame", () => {
+ docShell.createAboutBlankContentViewer(null);
+});
+
+addEventListener("DOMWindowClose", (evt) => {
+ // preventDefault stops the default window.close() function being called,
+ // which doesn't actually close anything but causes things to get into
+ // a bad state (an internal 'closed' flag is set and debug builds start
+ // asserting as the window is used.).
+ // None of the windows we inject this API into are suitable for this
+ // default close behaviour, so even if we took no action above, we avoid
+ // the default close from doing anything.
+ evt.preventDefault();
+
+ // Tells the SocialShare class to close the panel
+ sendAsyncMessage("Social:DOMWindowClose");
+});
+
+function hookWindowClose() {
+ // Allow scripts to close the "window". Because we are in a panel and not
+ // in a full dialog, the DOMWindowClose listener above will only receive the
+ // event if we do this.
+ let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ dwu.allowScriptsToClose();
+}
+
+function disableDialogs() {
+ let windowUtils = content.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.disableDialogs();
+}
+
+// Error handling class used to listen for network errors in the social frames
+// and replace them with a social-specific error page
+const SocialErrorListener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+
+ defaultTemplate: "about:socialerror?mode=tryAgainOnly&url=%{url}&origin=%{origin}",
+ urlTemplate: null,
+
+ init() {
+ addMessageListener("Social:SetErrorURL", this);
+ let webProgress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebProgress);
+ webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+ },
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "Social:SetErrorURL":
+ // Either a url or null to reset to default template.
+ this.urlTemplate = message.data.template;
+ break;
+ }
+ },
+
+ setErrorPage() {
+ // if this is about:providerdirectory, use the directory iframe
+ let frame = docShell.chromeEventHandler;
+ let origin = frame.getAttribute("origin");
+ let src = frame.getAttribute("src");
+ if (src == "about:providerdirectory") {
+ frame = content.document.getElementById("activation-frame");
+ src = frame.getAttribute("src");
+ }
+
+ let url = this.urlTemplate || this.defaultTemplate;
+ url = url.replace("%{url}", encodeURIComponent(src));
+ url = url.replace("%{origin}", encodeURIComponent(origin));
+ if (frame != docShell.chromeEventHandler) {
+ // Unable to access frame.docShell here. This is our own frame and doesn't
+ // provide reload, so we'll just set the src.
+ frame.setAttribute("src", url);
+ } else {
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(url, null, null, null, null);
+ }
+ sendAsyncMessage("Social:ErrorPageNotify", {
+ origin: origin,
+ url: src
+ });
+ },
+
+ onStateChange(aWebProgress, aRequest, aState, aStatus) {
+ let failure = false;
+ if ((aState & Ci.nsIWebProgressListener.STATE_IS_REQUEST))
+ return;
+ if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
+ if (aRequest instanceof Ci.nsIHttpChannel) {
+ try {
+ // Change the frame to an error page on 4xx (client errors)
+ // and 5xx (server errors). responseStatus throws if it is not set.
+ failure = aRequest.responseStatus >= 400 &&
+ aRequest.responseStatus < 600;
+ } catch (e) {
+ failure = aStatus != Components.results.NS_OK;
+ }
+ }
+ }
+
+ // Calling cancel() will raise some OnStateChange notifications by itself,
+ // so avoid doing that more than once
+ if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
+ // if tp is enabled and we get a failure, ignore failures (ie. STATE_STOP)
+ // on child resources since they *may* have been blocked. We don't have an
+ // easy way to know if a particular url is blocked by TP, only that
+ // something was.
+ if (docShell.hasTrackingContentBlocked) {
+ let frame = docShell.chromeEventHandler;
+ let src = frame.getAttribute("src");
+ if (aRequest && aRequest.name != src) {
+ Cu.reportError("SocialErrorListener ignoring blocked content error for " + aRequest.name);
+ return;
+ }
+ }
+
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ this.setErrorPage();
+ }
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ this.setErrorPage();
+ }
+ },
+
+ onProgressChange() {},
+ onStatusChange() {},
+ onSecurityChange() {},
+};
+
+SocialErrorListener.init();
diff --git a/browser/base/content/softwareUpdateOverlay.xul b/browser/base/content/softwareUpdateOverlay.xul
new file mode 100644
index 000000000..01170e46c
--- /dev/null
+++ b/browser/base/content/softwareUpdateOverlay.xul
@@ -0,0 +1,18 @@
+<?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/.
+
+<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?>
+
+<overlay id="softwareUpdateOverlay"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<window id="updates">
+
+#include browserMountPoints.inc
+
+</window>
+
+</overlay>
diff --git a/browser/base/content/sync/aboutSyncTabs-bindings.xml b/browser/base/content/sync/aboutSyncTabs-bindings.xml
new file mode 100644
index 000000000..e6108209a
--- /dev/null
+++ b/browser/base/content/sync/aboutSyncTabs-bindings.xml
@@ -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/. -->
+
+<bindings id="tabBindings"
+ 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="tab-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="start">
+ <xul:image class="tabIcon"
+ xbl:inherits="src=icon"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:label xbl:inherits="value=title,selected"
+ crop="end" flex="1" class="title"/>
+ <xul:label xbl:inherits="value=url,selected"
+ crop="end" flex="1" class="url"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ <handlers>
+ <handler event="dblclick" button="0">
+ <![CDATA[
+ RemoteTabViewer.openSelected();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="client-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:hbox pack="start" align="center" onfocus="event.target.blur()" onselect="return false;">
+ <xul:image/>
+ <xul:label xbl:inherits="value=clientName"
+ class="clientName"
+ crop="center" flex="1"/>
+ </xul:hbox>
+ </content>
+ </binding>
+</bindings>
diff --git a/browser/base/content/sync/aboutSyncTabs.css b/browser/base/content/sync/aboutSyncTabs.css
new file mode 100644
index 000000000..5a353175b
--- /dev/null
+++ b/browser/base/content/sync/aboutSyncTabs.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="tab"] {
+ -moz-binding: url(chrome://browser/content/sync/aboutSyncTabs-bindings.xml#tab-listing);
+}
+
+richlistitem[type="client"] {
+ -moz-binding: url(chrome://browser/content/sync/aboutSyncTabs-bindings.xml#client-listing);
+}
diff --git a/browser/base/content/sync/aboutSyncTabs.js b/browser/base/content/sync/aboutSyncTabs.js
new file mode 100644
index 000000000..0c5dbb2d8
--- /dev/null
+++ b/browser/base/content/sync/aboutSyncTabs.js
@@ -0,0 +1,361 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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://services-common/utils.js");
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource:///modules/PlacesUIUtils.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
+ XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
+ "resource://gre/modules/CloudSync.jsm");
+}
+
+var RemoteTabViewer = {
+ _tabsList: null,
+
+ init: function () {
+ Services.obs.addObserver(this, "weave:service:login:finish", false);
+ Services.obs.addObserver(this, "weave:engine:sync:finish", false);
+
+ Services.obs.addObserver(this, "cloudsync:tabs:update", false);
+
+ this._tabsList = document.getElementById("tabsList");
+
+ this.buildList(true);
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "weave:service:login:finish");
+ Services.obs.removeObserver(this, "weave:engine:sync:finish");
+
+ Services.obs.removeObserver(this, "cloudsync:tabs:update");
+ },
+
+ createItem: function (attrs) {
+ let item = document.createElement("richlistitem");
+
+ // Copy the attributes from the argument into the item.
+ for (let attr in attrs) {
+ item.setAttribute(attr, attrs[attr]);
+ }
+
+ if (attrs["type"] == "tab") {
+ item.label = attrs.title != "" ? attrs.title : attrs.url;
+ }
+
+ return item;
+ },
+
+ filterTabs: function (event) {
+ let val = event.target.value.toLowerCase();
+ let numTabs = this._tabsList.getRowCount();
+ let clientTabs = 0;
+ let currentClient = null;
+
+ for (let i = 0; i < numTabs; i++) {
+ let item = this._tabsList.getItemAtIndex(i);
+ let hide = false;
+ if (item.getAttribute("type") == "tab") {
+ if (!item.getAttribute("url").toLowerCase().includes(val) &&
+ !item.getAttribute("title").toLowerCase().includes(val)) {
+ hide = true;
+ } else {
+ clientTabs++;
+ }
+ }
+ else if (item.getAttribute("type") == "client") {
+ if (currentClient) {
+ if (clientTabs == 0) {
+ currentClient.hidden = true;
+ }
+ }
+ currentClient = item;
+ clientTabs = 0;
+ }
+ item.hidden = hide;
+ }
+ if (clientTabs == 0) {
+ currentClient.hidden = true;
+ }
+ },
+
+ openSelected: function () {
+ let items = this._tabsList.selectedItems;
+ let urls = [];
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].getAttribute("type") == "tab") {
+ urls.push(items[i].getAttribute("url"));
+ let index = this._tabsList.getIndexOfItem(items[i]);
+ this._tabsList.removeItemAt(index);
+ }
+ }
+ if (urls.length) {
+ getTopWin().gBrowser.loadTabs(urls);
+ this._tabsList.clearSelection();
+ }
+ },
+
+ bookmarkSingleTab: function () {
+ let item = this._tabsList.selectedItems[0];
+ let uri = Weave.Utils.makeURI(item.getAttribute("url"));
+ let title = item.getAttribute("title");
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "bookmark"
+ , uri: uri
+ , title: title
+ , hiddenRows: [ "description"
+ , "location"
+ , "loadInSidebar"
+ , "keyword" ]
+ }, window.top);
+ },
+
+ bookmarkSelectedTabs: function () {
+ let items = this._tabsList.selectedItems;
+ let URIs = [];
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].getAttribute("type") == "tab") {
+ let uri = Weave.Utils.makeURI(items[i].getAttribute("url"));
+ if (!uri) {
+ continue;
+ }
+
+ URIs.push(uri);
+ }
+ }
+ if (URIs.length) {
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "folder"
+ , URIList: URIs
+ , hiddenRows: [ "description" ]
+ }, window.top);
+ }
+ },
+
+ getIcon: function (iconUri, defaultIcon) {
+ try {
+ let iconURI = Weave.Utils.makeURI(iconUri);
+ return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ // Just give the provided default icon or the system's default.
+ return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec;
+ },
+
+ _waitingForBuildList: false,
+
+ _buildListRequested: false,
+
+ buildList: function (forceSync) {
+ if (this._waitingForBuildList) {
+ this._buildListRequested = true;
+ return;
+ }
+
+ this._waitingForBuildList = true;
+ this._buildListRequested = false;
+
+ this._clearTabList();
+
+ if (Weave.Service.isLoggedIn) {
+ this._refetchTabs(forceSync);
+ this._generateWeaveTabList();
+ } else {
+ // XXXzpao We should say something about not being logged in & not having data
+ // or tell the appropriate condition. (bug 583344)
+ }
+
+ let complete = () => {
+ this._waitingForBuildList = false;
+ if (this._buildListRequested) {
+ CommonUtils.nextTick(this.buildList, this);
+ }
+ }
+
+ if (CloudSync && CloudSync.ready && CloudSync().tabsReady && CloudSync().tabs.hasRemoteTabs()) {
+ this._generateCloudSyncTabList()
+ .then(complete, complete);
+ } else {
+ complete();
+ }
+ },
+
+ _clearTabList: function () {
+ let list = this._tabsList;
+
+ // Clear out existing richlistitems.
+ let count = list.getRowCount();
+ if (count > 0) {
+ for (let i = count - 1; i >= 0; i--) {
+ list.removeItemAt(i);
+ }
+ }
+ },
+
+ _generateWeaveTabList: function () {
+ let engine = Weave.Service.engineManager.get("tabs");
+ let list = this._tabsList;
+
+ let seenURLs = new Set();
+ let localURLs = engine.getOpenURLs();
+
+ for (let [, client] of Object.entries(engine.getAllClients())) {
+ // Create the client node, but don't add it in-case we don't show any tabs
+ let appendClient = true;
+
+ client.tabs.forEach(function({title, urlHistory, icon}) {
+ let url = urlHistory[0];
+ if (!url || localURLs.has(url) || seenURLs.has(url)) {
+ return;
+ }
+ seenURLs.add(url);
+
+ if (appendClient) {
+ let attrs = {
+ type: "client",
+ clientName: client.clientName,
+ class: Weave.Service.clientsEngine.isMobile(client.id) ? "mobile" : "desktop"
+ };
+ let clientEnt = this.createItem(attrs);
+ list.appendChild(clientEnt);
+ appendClient = false;
+ clientEnt.disabled = true;
+ }
+ let attrs = {
+ type: "tab",
+ title: title || url,
+ url: url,
+ icon: this.getIcon(icon),
+ }
+ let tab = this.createItem(attrs);
+ list.appendChild(tab);
+ }, this);
+ }
+ },
+
+ _generateCloudSyncTabList: function () {
+ let updateTabList = function (remoteTabs) {
+ let list = this._tabsList;
+
+ for (let client of remoteTabs) {
+ let clientAttrs = {
+ type: "client",
+ clientName: client.name,
+ };
+
+ let clientEnt = this.createItem(clientAttrs);
+ list.appendChild(clientEnt);
+
+ for (let tab of client.tabs) {
+ let tabAttrs = {
+ type: "tab",
+ title: tab.title,
+ url: tab.url,
+ icon: this.getIcon(tab.icon),
+ };
+ let tabEnt = this.createItem(tabAttrs);
+ list.appendChild(tabEnt);
+ }
+ }
+ }.bind(this);
+
+ return CloudSync().tabs.getRemoteTabs()
+ .then(updateTabList, Promise.reject.bind(Promise));
+ },
+
+ adjustContextMenu: function (event) {
+ let mode = "all";
+ switch (this._tabsList.selectedItems.length) {
+ case 0:
+ break;
+ case 1:
+ mode = "single"
+ break;
+ default:
+ mode = "multiple";
+ break;
+ }
+
+ let menu = document.getElementById("tabListContext");
+ let el = menu.firstChild;
+ while (el) {
+ let showFor = el.getAttribute("showFor");
+ if (showFor) {
+ el.hidden = showFor != mode && showFor != "all";
+ }
+
+ el = el.nextSibling;
+ }
+ },
+
+ _refetchTabs: function (force) {
+ if (!force) {
+ // Don't bother refetching tabs if we already did so recently
+ let lastFetch = 0;
+ try {
+ lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch");
+ }
+ catch (e) {
+ /* Just use the default value of 0 */
+ }
+
+ let now = Math.floor(Date.now() / 1000);
+ if (now - lastFetch < 30) {
+ return false;
+ }
+ }
+
+ // Ask Sync to just do the tabs engine if it can.
+ Weave.Service.sync(["tabs"]);
+ Services.prefs.setIntPref("services.sync.lastTabFetch",
+ Math.floor(Date.now() / 1000));
+
+ return true;
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "weave:service:login:finish":
+ // A login has finished, which means that a Sync is about to start and
+ // we will eventually get to the "tabs" engine - but try and force the
+ // tab engine to sync first by passing |true| for the forceSync param.
+ this.buildList(true);
+ break;
+ case "weave:engine:sync:finish":
+ if (data == "tabs") {
+ // The tabs engine just finished, so re-build the list without
+ // forcing a new sync of the tabs engine.
+ this.buildList(false);
+ }
+ break;
+ case "cloudsync:tabs:update":
+ this.buildList(false);
+ break;
+ }
+ },
+
+ handleClick: function (event) {
+ if (event.target.getAttribute("type") != "tab") {
+ return;
+ }
+
+ if (event.button == 1) {
+ let url = event.target.getAttribute("url");
+ openUILink(url, event);
+ let index = this._tabsList.getIndexOfItem(event.target);
+ this._tabsList.removeItemAt(index);
+ }
+ }
+}
diff --git a/browser/base/content/sync/aboutSyncTabs.xul b/browser/base/content/sync/aboutSyncTabs.xul
new file mode 100644
index 000000000..a4aa0032f
--- /dev/null
+++ b/browser/base/content/sync/aboutSyncTabs.xul
@@ -0,0 +1,68 @@
+<?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/. -->
+
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/aboutSyncTabs.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/sync/aboutSyncTabs.css" type="text/css"?>
+
+<!DOCTYPE window [
+ <!ENTITY % aboutSyncTabsDTD SYSTEM "chrome://browser/locale/aboutSyncTabs.dtd">
+ %aboutSyncTabsDTD;
+]>
+
+<window id="tabs-display"
+ onload="RemoteTabViewer.init()"
+ onunload="RemoteTabViewer.uninit()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="&tabs.otherDevices.label;">
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/sync/aboutSyncTabs.js"/>
+ <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/>
+ <html:head>
+ <html:link rel="icon" href="chrome://browser/skin/sync-16.png"/>
+ </html:head>
+
+ <popupset id="contextmenus">
+ <menupopup id="tabListContext">
+ <menuitem label="&tabs.context.openTab.label;"
+ accesskey="&tabs.context.openTab.accesskey;"
+ oncommand="RemoteTabViewer.openSelected()"
+ showFor="single"/>
+ <menuitem label="&tabs.context.bookmarkSingleTab.label;"
+ accesskey="&tabs.context.bookmarkSingleTab.accesskey;"
+ oncommand="RemoteTabViewer.bookmarkSingleTab(event)"
+ showFor="single"/>
+ <menuitem label="&tabs.context.openMultipleTabs.label;"
+ accesskey="&tabs.context.openMultipleTabs.accesskey;"
+ oncommand="RemoteTabViewer.openSelected()"
+ showFor="multiple"/>
+ <menuitem label="&tabs.context.bookmarkMultipleTabs.label;"
+ accesskey="&tabs.context.bookmarkMultipleTabs.accesskey;"
+ oncommand="RemoteTabViewer.bookmarkSelectedTabs()"
+ showFor="multiple"/>
+ <menuseparator/>
+ <menuitem label="&tabs.context.refreshList.label;"
+ accesskey="&tabs.context.refreshList.accesskey;"
+ oncommand="RemoteTabViewer.buildList()"
+ showFor="all"/>
+ </menupopup>
+ </popupset>
+ <richlistbox context="tabListContext" id="tabsList" seltype="multiple"
+ align="center" flex="1"
+ onclick="RemoteTabViewer.handleClick(event)"
+ oncontextmenu="RemoteTabViewer.adjustContextMenu(event)">
+ <hbox id="headers" align="center">
+ <label id="tabsListHeading"
+ value="&tabs.otherDevices.label;"/>
+ <spacer flex="1"/>
+ <textbox type="search"
+ emptytext="&tabs.searchText.label;"
+ oncommand="RemoteTabViewer.filterTabs(event)"/>
+ </hbox>
+
+ </richlistbox>
+</window>
+
diff --git a/browser/base/content/sync/addDevice.js b/browser/base/content/sync/addDevice.js
new file mode 100644
index 000000000..0390d4397
--- /dev/null
+++ b/browser/base/content/sync/addDevice.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const PIN_PART_LENGTH = 4;
+
+const ADD_DEVICE_PAGE = 0;
+const SYNC_KEY_PAGE = 1;
+const DEVICE_CONNECTED_PAGE = 2;
+
+var gSyncAddDevice = {
+
+ init: function init() {
+ this.pin1.setAttribute("maxlength", PIN_PART_LENGTH);
+ this.pin2.setAttribute("maxlength", PIN_PART_LENGTH);
+ this.pin3.setAttribute("maxlength", PIN_PART_LENGTH);
+
+ this.nextFocusEl = {pin1: this.pin2,
+ pin2: this.pin3,
+ pin3: this.wizard.getButton("next")};
+
+ this.throbber = document.getElementById("pairDeviceThrobber");
+ this.errorRow = document.getElementById("errorRow");
+
+ // Kick off a sync. That way the server will have the most recent data from
+ // this computer and it will show up immediately on the new device.
+ Weave.Service.scheduler.scheduleNextSync(0);
+ },
+
+ onPageShow: function onPageShow() {
+ this.wizard.getButton("back").hidden = true;
+
+ switch (this.wizard.pageIndex) {
+ case ADD_DEVICE_PAGE:
+ this.onTextBoxInput();
+ this.wizard.canRewind = false;
+ this.wizard.getButton("next").hidden = false;
+ this.pin1.focus();
+ break;
+ case SYNC_KEY_PAGE:
+ this.wizard.canAdvance = false;
+ this.wizard.canRewind = true;
+ this.wizard.getButton("back").hidden = false;
+ this.wizard.getButton("next").hidden = true;
+ document.getElementById("weavePassphrase").value =
+ Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey);
+ break;
+ case DEVICE_CONNECTED_PAGE:
+ this.wizard.canAdvance = true;
+ this.wizard.canRewind = false;
+ this.wizard.getButton("cancel").hidden = true;
+ break;
+ }
+ },
+
+ onWizardAdvance: function onWizardAdvance() {
+ switch (this.wizard.pageIndex) {
+ case ADD_DEVICE_PAGE:
+ this.startTransfer();
+ return false;
+ case DEVICE_CONNECTED_PAGE:
+ window.close();
+ return false;
+ }
+ return true;
+ },
+
+ startTransfer: function startTransfer() {
+ this.errorRow.hidden = true;
+ // When onAbort is called, Weave may already be gone.
+ const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
+
+ let self = this;
+ let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({
+ onPaired: function onPaired() {
+ let credentials = {account: Weave.Service.identity.account,
+ password: Weave.Service.identity.basicPassword,
+ synckey: Weave.Service.identity.syncKey,
+ serverURL: Weave.Service.serverURL};
+ jpakeclient.sendAndComplete(credentials);
+ },
+ onComplete: function onComplete() {
+ delete self._jpakeclient;
+ self.wizard.pageIndex = DEVICE_CONNECTED_PAGE;
+
+ // Schedule a Sync for soonish to fetch the data uploaded by the
+ // device with which we just paired.
+ Weave.Service.scheduler.scheduleNextSync(Weave.Service.scheduler.activeInterval);
+ },
+ onAbort: function onAbort(error) {
+ delete self._jpakeclient;
+
+ // Aborted by user, ignore.
+ if (error == JPAKE_ERROR_USERABORT) {
+ return;
+ }
+
+ self.errorRow.hidden = false;
+ self.throbber.hidden = true;
+ self.pin1.value = self.pin2.value = self.pin3.value = "";
+ self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false;
+ self.pin1.focus();
+ }
+ });
+ this.throbber.hidden = false;
+ this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true;
+ this.wizard.canAdvance = false;
+
+ let pin = this.pin1.value + this.pin2.value + this.pin3.value;
+ let expectDelay = false;
+ jpakeclient.pairWithPIN(pin, expectDelay);
+ },
+
+ onWizardBack: function onWizardBack() {
+ if (this.wizard.pageIndex != SYNC_KEY_PAGE)
+ return true;
+
+ this.wizard.pageIndex = ADD_DEVICE_PAGE;
+ return false;
+ },
+
+ onWizardCancel: function onWizardCancel() {
+ if (this._jpakeclient) {
+ this._jpakeclient.abort();
+ delete this._jpakeclient;
+ }
+ return true;
+ },
+
+ onTextBoxInput: function onTextBoxInput(textbox) {
+ if (textbox && textbox.value.length == PIN_PART_LENGTH)
+ this.nextFocusEl[textbox.id].focus();
+
+ this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH
+ && this.pin2.value.length == PIN_PART_LENGTH
+ && this.pin3.value.length == PIN_PART_LENGTH);
+ },
+
+ goToSyncKeyPage: function goToSyncKeyPage() {
+ this.wizard.pageIndex = SYNC_KEY_PAGE;
+ }
+
+};
+// onWizardAdvance() and onPageShow() are run before init() so we'll set
+// these up as lazy getters.
+["wizard", "pin1", "pin2", "pin3"].forEach(function (id) {
+ XPCOMUtils.defineLazyGetter(gSyncAddDevice, id, function() {
+ return document.getElementById(id);
+ });
+});
diff --git a/browser/base/content/sync/addDevice.xul b/browser/base/content/sync/addDevice.xul
new file mode 100644
index 000000000..83c3b7b3c
--- /dev/null
+++ b/browser/base/content/sync/addDevice.xul
@@ -0,0 +1,129 @@
+<?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://browser/skin/syncSetup.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
+<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd">
+%brandDTD;
+%syncBrandDTD;
+%syncSetupDTD;
+]>
+<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ id="wizard"
+ title="&pairDevice.title.label;"
+ windowtype="Sync:AddDevice"
+ persist="screenX screenY"
+ onwizardnext="return gSyncAddDevice.onWizardAdvance();"
+ onwizardback="return gSyncAddDevice.onWizardBack();"
+ onwizardcancel="gSyncAddDevice.onWizardCancel();"
+ onload="gSyncAddDevice.init();">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/sync/addDevice.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/sync/utils.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/utilityOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/printUtils.js"/>
+
+ <wizardpage id="addDevicePage"
+ label="&pairDevice.title.label;"
+ onpageshow="gSyncAddDevice.onPageShow();">
+ <description>
+ &pairDevice.dialog.description.label;
+ <label class="text-link"
+ value="&addDevice.showMeHow.label;"
+ href="https://services.mozilla.com/sync/help/add-device"/>
+ </description>
+ <separator class="groove-thin"/>
+ <description>
+ &addDevice.dialog.enterCode.label;
+ </description>
+ <separator class="groove-thin"/>
+ <vbox align="center">
+ <textbox id="pin1"
+ class="pin"
+ oninput="gSyncAddDevice.onTextBoxInput(this);"
+ onfocus="this.select();"
+ />
+ <textbox id="pin2"
+ class="pin"
+ oninput="gSyncAddDevice.onTextBoxInput(this);"
+ onfocus="this.select();"
+ />
+ <textbox id="pin3"
+ class="pin"
+ oninput="gSyncAddDevice.onTextBoxInput(this);"
+ onfocus="this.select();"
+ />
+ </vbox>
+ <separator class="groove-thin"/>
+ <vbox id="pairDeviceThrobber" align="center" hidden="true">
+ <image/>
+ </vbox>
+ <hbox id="errorRow" pack="center" hidden="true">
+ <image class="statusIcon" status="error"/>
+ <label class="status"
+ value="&addDevice.dialog.tryAgain.label;"/>
+ </hbox>
+ <spacer flex="3"/>
+ <label class="text-link"
+ value="&addDevice.dontHaveDevice.label;"
+ onclick="gSyncAddDevice.goToSyncKeyPage();"/>
+ </wizardpage>
+
+ <!-- Need a non-empty label here, otherwise we get a default label on Mac -->
+ <wizardpage id="syncKeyPage"
+ label=" "
+ onpageshow="gSyncAddDevice.onPageShow();">
+ <description>
+ &addDevice.dialog.recoveryKey.label;
+ </description>
+ <spacer/>
+
+ <groupbox>
+ <label value="&recoveryKeyEntry.label;"
+ accesskey="&recoveryKeyEntry.accesskey;"
+ control="weavePassphrase"/>
+ <textbox id="weavePassphrase"
+ readonly="true"/>
+ </groupbox>
+
+ <groupbox align="center">
+ <description>&recoveryKeyBackup.description;</description>
+ <hbox>
+ <button id="printSyncKeyButton"
+ label="&button.syncKeyBackup.print.label;"
+ accesskey="&button.syncKeyBackup.print.accesskey;"
+ oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/>
+ <button id="saveSyncKeyButton"
+ label="&button.syncKeyBackup.save.label;"
+ accesskey="&button.syncKeyBackup.save.accesskey;"
+ oncommand="gSyncUtils.passphraseSave('weavePassphrase');"/>
+ </hbox>
+ </groupbox>
+ </wizardpage>
+
+ <wizardpage id="deviceConnectedPage"
+ label="&addDevice.dialog.connected.label;"
+ onpageshow="gSyncAddDevice.onPageShow();">
+ <vbox align="center">
+ <image id="successPageIcon"/>
+ </vbox>
+ <separator/>
+ <description class="normal">
+ &addDevice.dialog.successful.label;
+ </description>
+ </wizardpage>
+
+</wizard>
diff --git a/browser/base/content/sync/customize.css b/browser/base/content/sync/customize.css
new file mode 100644
index 000000000..2bb62595d
--- /dev/null
+++ b/browser/base/content/sync/customize.css
@@ -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/. */
+
+:root {
+ font-size: 80%;
+}
+
+#sync-customize-pane {
+ padding-inline-start: 74px;
+ background: top left url(chrome://browser/skin/sync-128.png) no-repeat;
+ background-size: 64px;
+}
+
+#sync-customize-title {
+ margin-inline-start: 0;
+ padding-bottom: 0.5em;
+ font-weight: bold;
+}
+
+#sync-customize-subtitle {
+ font-size: 90%;
+}
+
+checkbox {
+ margin: 0;
+ padding: 0.5em 0 0;
+}
diff --git a/browser/base/content/sync/customize.js b/browser/base/content/sync/customize.js
new file mode 100644
index 000000000..f431ac58c
--- /dev/null
+++ b/browser/base/content/sync/customize.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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+addEventListener("dialogaccept", function () {
+ let pane = document.getElementById("sync-customize-pane");
+ // First determine what the preference for the "global" sync enabled pref
+ // should be based on the engines selected.
+ let prefElts = pane.querySelectorAll("preferences > preference");
+ let syncEnabled = false;
+ for (let elt of prefElts) {
+ if (elt.name.startsWith("services.sync.") && elt.value) {
+ syncEnabled = true;
+ break;
+ }
+ }
+ Services.prefs.setBoolPref("services.sync.enabled", syncEnabled);
+ // and write the individual prefs.
+ pane.writePreferences(true);
+ window.arguments[0].accepted = true;
+});
diff --git a/browser/base/content/sync/customize.xul b/browser/base/content/sync/customize.xul
new file mode 100644
index 000000000..d95536d9a
--- /dev/null
+++ b/browser/base/content/sync/customize.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://browser/content/sync/customize.css" type="text/css"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % syncCustomizeDTD SYSTEM "chrome://browser/locale/syncCustomize.dtd">
+%syncCustomizeDTD;
+]>
+<dialog id="sync-customize"
+ windowtype="Sync:Customize"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="&syncCustomize.dialog.title;"
+ buttonlabelaccept="&syncCustomize.acceptButton.label;"
+ buttons="accept">
+
+ <prefpane id="sync-customize-pane">
+ <preferences>
+ <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/>
+ <preference id="engine.history" name="services.sync.engine.history" type="bool"/>
+ <preference id="engine.tabs" name="services.sync.engine.tabs" type="bool"/>
+ <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/>
+ <preference id="engine.addons" name="services.sync.engine.addons" type="bool"/>
+ <preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/>
+ </preferences>
+
+ <label id="sync-customize-title" value="&syncCustomize.title;"/>
+ <description id="sync-customize-subtitle"
+#ifdef XP_UNIX
+ value="&syncCustomizeUnix.description;"
+#else
+ value="&syncCustomize.description;"
+#endif
+ />
+
+ <vbox align="start">
+ <checkbox label="&engine.tabs.label;"
+ accesskey="&engine.tabs.accesskey;"
+ preference="engine.tabs"/>
+ <checkbox label="&engine.bookmarks.label;"
+ accesskey="&engine.bookmarks.accesskey;"
+ preference="engine.bookmarks"/>
+ <checkbox label="&engine.passwords.label;"
+ accesskey="&engine.passwords.accesskey;"
+ preference="engine.passwords"/>
+ <checkbox label="&engine.history.label;"
+ accesskey="&engine.history.accesskey;"
+ preference="engine.history"/>
+ <checkbox label="&engine.addons.label;"
+ accesskey="&engine.addons.accesskey;"
+ preference="engine.addons"/>
+ <checkbox label="&engine.prefs.label;"
+ accesskey="&engine.prefs.accesskey;"
+ preference="engine.prefs"/>
+ </vbox>
+
+ </prefpane>
+
+ <script type="application/javascript"
+ src="chrome://browser/content/sync/customize.js" />
+
+</dialog>
diff --git a/browser/base/content/sync/genericChange.js b/browser/base/content/sync/genericChange.js
new file mode 100644
index 000000000..51a74f1b1
--- /dev/null
+++ b/browser/base/content/sync/genericChange.js
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+Components.utils.import("resource://services-sync/main.js");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var Change = {
+ _dialog: null,
+ _dialogType: null,
+ _status: null,
+ _statusIcon: null,
+ _firstBox: null,
+ _secondBox: null,
+
+ get _passphraseBox() {
+ delete this._passphraseBox;
+ return this._passphraseBox = document.getElementById("passphraseBox");
+ },
+
+ get _currentPasswordInvalid() {
+ return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ },
+
+ get _updatingPassphrase() {
+ return this._dialogType == "UpdatePassphrase";
+ },
+
+ onLoad: function Change_onLoad() {
+ /* Load labels */
+ let introText = document.getElementById("introText");
+ let warningText = document.getElementById("warningText");
+
+ // load some other elements & info from the window
+ this._dialog = document.getElementById("change-dialog");
+ this._dialogType = window.arguments[0];
+ this._duringSetup = window.arguments[1];
+ this._status = document.getElementById("status");
+ this._statusIcon = document.getElementById("statusIcon");
+ this._statusRow = document.getElementById("statusRow");
+ this._firstBox = document.getElementById("textBox1");
+ this._secondBox = document.getElementById("textBox2");
+
+ this._dialog.getButton("finish").disabled = true;
+ this._dialog.getButton("back").hidden = true;
+
+ this._stringBundle =
+ Services.strings.createBundle("chrome://browser/locale/syncGenericChange.properties");
+
+ switch (this._dialogType) {
+ case "UpdatePassphrase":
+ case "ResetPassphrase":
+ document.getElementById("textBox1Row").hidden = true;
+ document.getElementById("textBox2Row").hidden = true;
+ document.getElementById("passphraseLabel").value
+ = this._str("new.recoverykey.label");
+ document.getElementById("passphraseSpacer").hidden = false;
+
+ if (this._updatingPassphrase) {
+ document.getElementById("passphraseHelpBox").hidden = false;
+ document.title = this._str("new.recoverykey.title");
+ introText.textContent = this._str("new.recoverykey.introText");
+ this._dialog.getButton("finish").label
+ = this._str("new.recoverykey.acceptButton");
+ }
+ else {
+ document.getElementById("generatePassphraseButton").hidden = false;
+ document.getElementById("passphraseBackupButtons").hidden = false;
+ this._passphraseBox.setAttribute("readonly", "true");
+ let pp = Weave.Service.identity.syncKey;
+ if (Weave.Utils.isPassphrase(pp))
+ pp = Weave.Utils.hyphenatePassphrase(pp);
+ this._passphraseBox.value = pp;
+ this._passphraseBox.focus();
+ document.title = this._str("change.recoverykey.title");
+ introText.textContent = this._str("change.synckey.introText2");
+ warningText.textContent = this._str("change.recoverykey.warningText");
+ this._dialog.getButton("finish").label
+ = this._str("change.recoverykey.acceptButton");
+ if (this._duringSetup) {
+ this._dialog.getButton("finish").disabled = false;
+ }
+ }
+ break;
+ case "ChangePassword":
+ document.getElementById("passphraseRow").hidden = true;
+ let box1label = document.getElementById("textBox1Label");
+ let box2label = document.getElementById("textBox2Label");
+ box1label.value = this._str("new.password.label");
+
+ if (this._currentPasswordInvalid) {
+ document.title = this._str("new.password.title");
+ introText.textContent = this._str("new.password.introText");
+ this._dialog.getButton("finish").label
+ = this._str("new.password.acceptButton");
+ document.getElementById("textBox2Row").hidden = true;
+ }
+ else {
+ document.title = this._str("change.password.title");
+ box2label.value = this._str("new.password.confirm");
+ introText.textContent = this._str("change.password3.introText");
+ warningText.textContent = this._str("change.password.warningText");
+ this._dialog.getButton("finish").label
+ = this._str("change.password.acceptButton");
+ }
+ break;
+ }
+ document.getElementById("change-page")
+ .setAttribute("label", document.title);
+ },
+
+ _clearStatus: function _clearStatus() {
+ this._status.value = "";
+ this._statusIcon.removeAttribute("status");
+ },
+
+ _updateStatus: function Change__updateStatus(str, state) {
+ this._updateStatusWithString(this._str(str), state);
+ },
+
+ _updateStatusWithString: function Change__updateStatusWithString(string, state) {
+ this._statusRow.hidden = false;
+ this._status.value = string;
+ this._statusIcon.setAttribute("status", state);
+
+ let error = state == "error";
+ this._dialog.getButton("cancel").disabled = !error;
+ this._dialog.getButton("finish").disabled = !error;
+ document.getElementById("printSyncKeyButton").disabled = !error;
+ document.getElementById("saveSyncKeyButton").disabled = !error;
+
+ if (state == "success")
+ window.setTimeout(window.close, 1500);
+ },
+
+ onDialogAccept: function() {
+ switch (this._dialogType) {
+ case "UpdatePassphrase":
+ case "ResetPassphrase":
+ return this.doChangePassphrase();
+ case "ChangePassword":
+ return this.doChangePassword();
+ }
+ return undefined;
+ },
+
+ doGeneratePassphrase: function () {
+ let passphrase = Weave.Utils.generatePassphrase();
+ this._passphraseBox.value = Weave.Utils.hyphenatePassphrase(passphrase);
+ this._dialog.getButton("finish").disabled = false;
+ },
+
+ doChangePassphrase: function Change_doChangePassphrase() {
+ let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value);
+ if (this._updatingPassphrase) {
+ Weave.Service.identity.syncKey = pp;
+ if (Weave.Service.login()) {
+ this._updateStatus("change.recoverykey.success", "success");
+ Weave.Service.persistLogin();
+ Weave.Service.scheduler.delayedAutoConnect(0);
+ }
+ else {
+ this._updateStatus("new.passphrase.status.incorrect", "error");
+ }
+ }
+ else {
+ this._updateStatus("change.recoverykey.label", "active");
+
+ if (Weave.Service.changePassphrase(pp))
+ this._updateStatus("change.recoverykey.success", "success");
+ else
+ this._updateStatus("change.recoverykey.error", "error");
+ }
+
+ return false;
+ },
+
+ doChangePassword: function Change_doChangePassword() {
+ if (this._currentPasswordInvalid) {
+ Weave.Service.identity.basicPassword = this._firstBox.value;
+ if (Weave.Service.login()) {
+ this._updateStatus("change.password.status.success", "success");
+ Weave.Service.persistLogin();
+ }
+ else {
+ this._updateStatus("new.password.status.incorrect", "error");
+ }
+ }
+ else {
+ this._updateStatus("change.password.status.active", "active");
+
+ if (Weave.Service.changePassword(this._firstBox.value))
+ this._updateStatus("change.password.status.success", "success");
+ else
+ this._updateStatus("change.password.status.error", "error");
+ }
+
+ return false;
+ },
+
+ validate: function (event) {
+ let valid = false;
+ let errorString = "";
+
+ if (this._dialogType == "ChangePassword") {
+ if (this._currentPasswordInvalid)
+ [valid, errorString] = gSyncUtils.validatePassword(this._firstBox);
+ else
+ [valid, errorString] = gSyncUtils.validatePassword(this._firstBox, this._secondBox);
+ }
+ else {
+ if (!this._updatingPassphrase)
+ return;
+
+ valid = this._passphraseBox.value != "";
+ }
+
+ if (errorString == "")
+ this._clearStatus();
+ else
+ this._updateStatusWithString(errorString, "error");
+
+ this._statusRow.hidden = valid;
+ this._dialog.getButton("finish").disabled = !valid;
+ },
+
+ _str: function Change__string(str) {
+ return this._stringBundle.GetStringFromName(str);
+ }
+};
diff --git a/browser/base/content/sync/genericChange.xul b/browser/base/content/sync/genericChange.xul
new file mode 100644
index 000000000..db74a1b31
--- /dev/null
+++ b/browser/base/content/sync/genericChange.xul
@@ -0,0 +1,123 @@
+<?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://browser/skin/syncSetup.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
+<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd">
+%brandDTD;
+%syncBrandDTD;
+%syncSetupDTD;
+]>
+<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ id="change-dialog"
+ windowtype="Weave:ChangeSomething"
+ persist="screenX screenY"
+ onwizardnext="Change.onLoad()"
+ onwizardfinish="return Change.onDialogAccept();">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/sync/genericChange.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/sync/utils.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/printUtils.js"/>
+
+ <wizardpage id="change-page"
+ label="">
+
+ <description id="introText">
+ </description>
+
+ <separator class="thin"/>
+
+ <groupbox>
+ <grid>
+ <columns>
+ <column align="right"/>
+ <column flex="3"/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row id="textBox1Row" align="center">
+ <label id="textBox1Label" control="textBox1"/>
+ <textbox id="textBox1" type="password" oninput="Change.validate()"/>
+ <spacer/>
+ </row>
+ <row id="textBox2Row" align="center">
+ <label id="textBox2Label" control="textBox2"/>
+ <textbox id="textBox2" type="password" oninput="Change.validate()"/>
+ <spacer/>
+ </row>
+ </rows>
+ </grid>
+
+ <vbox id="passphraseRow">
+ <hbox flex="1">
+ <label id="passphraseLabel" control="passphraseBox"/>
+ <spacer flex="1"/>
+ <label id="generatePassphraseButton"
+ hidden="true"
+ value="&syncGenerateNewKey.label;"
+ class="text-link"
+ onclick="event.stopPropagation();
+ Change.doGeneratePassphrase();"/>
+ </hbox>
+ <textbox id="passphraseBox"
+ flex="1"
+ onfocus="this.select()"
+ oninput="Change.validate()"/>
+ </vbox>
+
+ <vbox id="feedback" pack="center">
+ <hbox id="statusRow" align="center">
+ <image id="statusIcon" class="statusIcon"/>
+ <label id="status" class="status" value=" "/>
+ </hbox>
+ </vbox>
+ </groupbox>
+
+ <separator class="thin"/>
+
+ <hbox id="passphraseBackupButtons"
+ hidden="true"
+ pack="center">
+ <button id="printSyncKeyButton"
+ label="&button.syncKeyBackup.print.label;"
+ accesskey="&button.syncKeyBackup.print.accesskey;"
+ oncommand="gSyncUtils.passphrasePrint('passphraseBox');"/>
+ <button id="saveSyncKeyButton"
+ label="&button.syncKeyBackup.save.label;"
+ accesskey="&button.syncKeyBackup.save.accesskey;"
+ oncommand="gSyncUtils.passphraseSave('passphraseBox');"/>
+ </hbox>
+
+ <vbox id="passphraseHelpBox"
+ hidden="true">
+ <description>
+ &existingRecoveryKey.description;
+ <label class="text-link"
+ href="https://services.mozilla.com/sync/help/manual-setup">
+ &addDevice.showMeHow.label;
+ </label>
+ </description>
+ </vbox>
+
+ <spacer id="passphraseSpacer"
+ flex="1"
+ hidden="true"/>
+
+ <description id="warningText" class="data">
+ </description>
+
+ <spacer flex="1"/>
+ </wizardpage>
+</wizard>
diff --git a/browser/base/content/sync/key.xhtml b/browser/base/content/sync/key.xhtml
new file mode 100644
index 000000000..1363132e7
--- /dev/null
+++ b/browser/base/content/sync/key.xhtml
@@ -0,0 +1,54 @@
+<?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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
+ %syncBrandDTD;
+ <!ENTITY % syncKeyDTD SYSTEM "chrome://browser/locale/syncKey.dtd">
+ %syncKeyDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+ %globalDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&syncKey.page.title;</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <meta name="robots" content="noindex"/>
+ <style type="text/css">
+ #synckey { font-size: 150% }
+ footer { font-size: 70% }
+ /* Bug 575675: Need to have an a:visited rule in a chrome document. */
+ a:visited { color: purple; }
+ </style>
+</head>
+
+<body dir="&locale.dir;">
+<h1>&syncKey.page.title;</h1>
+
+<p id="synckey" dir="ltr">SYNCKEY</p>
+
+<p>&syncKey.page.description2;</p>
+
+<div id="column1">
+ <h2>&syncKey.keepItSecret.heading;</h2>
+ <p>&syncKey.keepItSecret.description;</p>
+</div>
+
+<div id="column2">
+ <h2>&syncKey.keepItSafe.heading;</h2>
+ <p><em>&syncKey.keepItSafe1.description;</em>&syncKey.keepItSafe2.description;<em>&syncKey.keepItSafe3.description;</em>&syncKey.keepItSafe4a.description;</p>
+</div>
+
+<p>&syncKey.findOutMore1.label;<a href="https://services.mozilla.com">https://services.mozilla.com</a>&syncKey.findOutMore2.label;</p>
+
+<footer>
+ &syncKey.footer1.label;<a id="tosLink" href="termsURL">termsURL</a>&syncKey.footer2.label;<a id="ppLink" href="privacyURL">privacyURL</a>&syncKey.footer3.label;
+</footer>
+
+</body>
+</html>
diff --git a/browser/base/content/sync/setup.js b/browser/base/content/sync/setup.js
new file mode 100644
index 000000000..f9dae1bd4
--- /dev/null
+++ b/browser/base/content/sync/setup.js
@@ -0,0 +1,1060 @@
+// -*- 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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+// page consts
+
+const PAIR_PAGE = 0;
+const INTRO_PAGE = 1;
+const NEW_ACCOUNT_START_PAGE = 2;
+const EXISTING_ACCOUNT_CONNECT_PAGE = 3;
+const EXISTING_ACCOUNT_LOGIN_PAGE = 4;
+const OPTIONS_PAGE = 5;
+const OPTIONS_CONFIRM_PAGE = 6;
+
+// Broader than we'd like, but after this changed from api-secure.recaptcha.net
+// we had no choice. At least we only do this for the duration of setup.
+// See discussion in Bugs 508112 and 653307.
+const RECAPTCHA_DOMAIN = "https://www.google.com";
+
+const PIN_PART_LENGTH = 4;
+
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/PluralForm.jsm");
+
+
+function setVisibility(element, visible) {
+ element.style.visibility = visible ? "visible" : "hidden";
+}
+
+var gSyncSetup = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+
+ captchaBrowser: null,
+ wizard: null,
+ _disabledSites: [],
+
+ status: {
+ password: false,
+ email: false,
+ server: false
+ },
+
+ get _remoteSites() {
+ return [Weave.Service.serverURL, RECAPTCHA_DOMAIN];
+ },
+
+ get _usingMainServers() {
+ if (this._settingUpNew)
+ return document.getElementById("server").selectedIndex == 0;
+ return document.getElementById("existingServer").selectedIndex == 0;
+ },
+
+ init: function () {
+ let obs = [
+ ["weave:service:change-passphrase", "onResetPassphrase"],
+ ["weave:service:login:start", "onLoginStart"],
+ ["weave:service:login:error", "onLoginEnd"],
+ ["weave:service:login:finish", "onLoginEnd"]];
+
+ // Add the observers now and remove them on unload
+ let self = this;
+ let addRem = function(add) {
+ obs.forEach(function([topic, func]) {
+ // XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
+ // of `this`. Fix in a followup. (bug 583347)
+ if (add)
+ Weave.Svc.Obs.add(topic, self[func], self);
+ else
+ Weave.Svc.Obs.remove(topic, self[func], self);
+ });
+ };
+ addRem(true);
+ window.addEventListener("unload", () => addRem(false), false);
+
+ window.setTimeout(function () {
+ // Force Service to be loaded so that engines are registered.
+ // See Bug 670082.
+ Weave.Service;
+ }, 0);
+
+ this.captchaBrowser = document.getElementById("captcha");
+
+ this.wizardType = null;
+ if (window.arguments && window.arguments[0]) {
+ this.wizardType = window.arguments[0];
+ }
+ switch (this.wizardType) {
+ case null:
+ this.wizard.pageIndex = INTRO_PAGE;
+ // Fall through!
+ case "pair":
+ this.captchaBrowser.addProgressListener(this);
+ Weave.Svc.Prefs.set("firstSync", "notReady");
+ break;
+ case "reset":
+ this._resettingSync = true;
+ this.wizard.pageIndex = OPTIONS_PAGE;
+ break;
+ }
+
+ this.wizard.getButton("extra1").label =
+ this._stringBundle.GetStringFromName("button.syncOptions.label");
+
+ // Remember these values because the options pages change them temporarily.
+ this._nextButtonLabel = this.wizard.getButton("next").label;
+ this._nextButtonAccesskey = this.wizard.getButton("next")
+ .getAttribute("accesskey");
+ this._backButtonLabel = this.wizard.getButton("back").label;
+ this._backButtonAccesskey = this.wizard.getButton("back")
+ .getAttribute("accesskey");
+ },
+
+ startNewAccountSetup: function () {
+ if (!Weave.Utils.ensureMPUnlocked())
+ return;
+ this._settingUpNew = true;
+ this.wizard.pageIndex = NEW_ACCOUNT_START_PAGE;
+ },
+
+ useExistingAccount: function () {
+ if (!Weave.Utils.ensureMPUnlocked())
+ return;
+ this._settingUpNew = false;
+ if (this.wizardType == "pair") {
+ // We're already pairing, so there's no point in pairing again.
+ // Go straight to the manual login page.
+ this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE;
+ } else {
+ this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE;
+ }
+ },
+
+ resetPassphrase: function resetPassphrase() {
+ // Apply the existing form fields so that
+ // Weave.Service.changePassphrase() has the necessary credentials.
+ Weave.Service.identity.account = document.getElementById("existingAccountName").value;
+ Weave.Service.identity.basicPassword = document.getElementById("existingPassword").value;
+
+ // Generate a new passphrase so that Weave.Service.login() will
+ // actually do something.
+ let passphrase = Weave.Utils.generatePassphrase();
+ Weave.Service.identity.syncKey = passphrase;
+
+ // Only open the dialog if username + password are actually correct.
+ Weave.Service.login();
+ if ([Weave.LOGIN_FAILED_INVALID_PASSPHRASE,
+ Weave.LOGIN_FAILED_NO_PASSPHRASE,
+ Weave.LOGIN_SUCCEEDED].indexOf(Weave.Status.login) == -1) {
+ return;
+ }
+
+ // Hide any errors about the passphrase, we know it's not right.
+ let feedback = document.getElementById("existingPassphraseFeedbackRow");
+ feedback.hidden = true;
+ let el = document.getElementById("existingPassphrase");
+ el.value = Weave.Utils.hyphenatePassphrase(passphrase);
+
+ // changePassphrase() will sync, make sure we set the "firstSync" pref
+ // according to the user's pref.
+ Weave.Svc.Prefs.reset("firstSync");
+ this.setupInitialSync();
+ gSyncUtils.resetPassphrase(true);
+ },
+
+ onResetPassphrase: function () {
+ document.getElementById("existingPassphrase").value =
+ Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey);
+ this.checkFields();
+ this.wizard.advance();
+ },
+
+ onLoginStart: function () {
+ this.toggleLoginFeedback(false);
+ },
+
+ onLoginEnd: function () {
+ this.toggleLoginFeedback(true);
+ },
+
+ sendCredentialsAfterSync: function () {
+ let send = function() {
+ Services.obs.removeObserver("weave:service:sync:finish", send);
+ Services.obs.removeObserver("weave:service:sync:error", send);
+ let credentials = {account: Weave.Service.identity.account,
+ password: Weave.Service.identity.basicPassword,
+ synckey: Weave.Service.identity.syncKey,
+ serverURL: Weave.Service.serverURL};
+ this._jpakeclient.sendAndComplete(credentials);
+ }.bind(this);
+ Services.obs.addObserver("weave:service:sync:finish", send, false);
+ Services.obs.addObserver("weave:service:sync:error", send, false);
+ },
+
+ toggleLoginFeedback: function (stop) {
+ document.getElementById("login-throbber").hidden = stop;
+ let password = document.getElementById("existingPasswordFeedbackRow");
+ let server = document.getElementById("existingServerFeedbackRow");
+ let passphrase = document.getElementById("existingPassphraseFeedbackRow");
+
+ if (!stop || (Weave.Status.login == Weave.LOGIN_SUCCEEDED)) {
+ password.hidden = server.hidden = passphrase.hidden = true;
+ return;
+ }
+
+ let feedback;
+ switch (Weave.Status.login) {
+ case Weave.LOGIN_FAILED_NETWORK_ERROR:
+ case Weave.LOGIN_FAILED_SERVER_ERROR:
+ feedback = server;
+ break;
+ case Weave.LOGIN_FAILED_LOGIN_REJECTED:
+ case Weave.LOGIN_FAILED_NO_USERNAME:
+ case Weave.LOGIN_FAILED_NO_PASSWORD:
+ feedback = password;
+ break;
+ case Weave.LOGIN_FAILED_INVALID_PASSPHRASE:
+ feedback = passphrase;
+ break;
+ }
+ this._setFeedbackMessage(feedback, false, Weave.Status.login);
+ },
+
+ setupInitialSync: function () {
+ let action = document.getElementById("mergeChoiceRadio").selectedItem.id;
+ switch (action) {
+ case "resetClient":
+ // if we're not resetting sync, we don't need to explicitly
+ // call resetClient
+ if (!this._resettingSync)
+ return;
+ // otherwise, fall through
+ case "wipeClient":
+ case "wipeRemote":
+ Weave.Svc.Prefs.set("firstSync", action);
+ break;
+ }
+ },
+
+ // fun with validation!
+ checkFields: function () {
+ this.wizard.canAdvance = this.readyToAdvance();
+ },
+
+ readyToAdvance: function () {
+ switch (this.wizard.pageIndex) {
+ case INTRO_PAGE:
+ return false;
+ case NEW_ACCOUNT_START_PAGE:
+ for (let i in this.status) {
+ if (!this.status[i])
+ return false;
+ }
+ if (this._usingMainServers)
+ return document.getElementById("tos").checked;
+
+ return true;
+ case EXISTING_ACCOUNT_LOGIN_PAGE:
+ let hasUser = document.getElementById("existingAccountName").value != "";
+ let hasPass = document.getElementById("existingPassword").value != "";
+ let hasKey = document.getElementById("existingPassphrase").value != "";
+
+ if (hasUser && hasPass && hasKey) {
+ if (this._usingMainServers)
+ return true;
+
+ if (this._validateServer(document.getElementById("existingServer"))) {
+ return true;
+ }
+ }
+ return false;
+ }
+ // Default, e.g. wizard's special page -1 etc.
+ return true;
+ },
+
+ onPINInput: function onPINInput(textbox) {
+ if (textbox && textbox.value.length == PIN_PART_LENGTH) {
+ this.nextFocusEl[textbox.id].focus();
+ }
+ this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH &&
+ this.pin2.value.length == PIN_PART_LENGTH &&
+ this.pin3.value.length == PIN_PART_LENGTH);
+ },
+
+ onEmailInput: function () {
+ // Check account validity when the user stops typing for 1 second.
+ if (this._checkAccountTimer)
+ window.clearTimeout(this._checkAccountTimer);
+ this._checkAccountTimer = window.setTimeout(function () {
+ gSyncSetup.checkAccount();
+ }, 1000);
+ },
+
+ checkAccount: function() {
+ delete this._checkAccountTimer;
+ let value = Weave.Utils.normalizeAccount(
+ document.getElementById("weaveEmail").value);
+ if (!value) {
+ this.status.email = false;
+ this.checkFields();
+ return;
+ }
+
+ let re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ let feedback = document.getElementById("emailFeedbackRow");
+ let valid = re.test(value);
+
+ let str = "";
+ if (!valid) {
+ str = "invalidEmail.label";
+ } else {
+ let availCheck = Weave.Service.checkAccount(value);
+ valid = availCheck == "available";
+ if (!valid) {
+ if (availCheck == "notAvailable")
+ str = "usernameNotAvailable.label";
+ else
+ str = availCheck;
+ }
+ }
+
+ this._setFeedbackMessage(feedback, valid, str);
+ this.status.email = valid;
+ if (valid)
+ Weave.Service.identity.account = value;
+ this.checkFields();
+ },
+
+ onPasswordChange: function () {
+ let password = document.getElementById("weavePassword");
+ let pwconfirm = document.getElementById("weavePasswordConfirm");
+ let [valid, errorString] = gSyncUtils.validatePassword(password, pwconfirm);
+
+ let feedback = document.getElementById("passwordFeedbackRow");
+ this._setFeedback(feedback, valid, errorString);
+
+ this.status.password = valid;
+ this.checkFields();
+ },
+
+ onPageShow: function() {
+ switch (this.wizard.pageIndex) {
+ case PAIR_PAGE:
+ this.wizard.getButton("back").hidden = true;
+ this.wizard.getButton("extra1").hidden = true;
+ this.onPINInput();
+ this.pin1.focus();
+ break;
+ case INTRO_PAGE:
+ // We may not need the captcha in the Existing Account branch of the
+ // wizard. However, we want to preload it to avoid any flickering while
+ // the Create Account page is shown.
+ this.loadCaptcha();
+ this.wizard.getButton("next").hidden = true;
+ this.wizard.getButton("back").hidden = true;
+ this.wizard.getButton("extra1").hidden = true;
+ this.checkFields();
+ break;
+ case NEW_ACCOUNT_START_PAGE:
+ this.wizard.getButton("extra1").hidden = false;
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("back").hidden = false;
+ this.onServerCommand();
+ this.wizard.canRewind = true;
+ this.checkFields();
+ break;
+ case EXISTING_ACCOUNT_CONNECT_PAGE:
+ Weave.Svc.Prefs.set("firstSync", "existingAccount");
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("back").hidden = false;
+ this.wizard.getButton("extra1").hidden = false;
+ this.wizard.canAdvance = false;
+ this.wizard.canRewind = true;
+ this.startEasySetup();
+ break;
+ case EXISTING_ACCOUNT_LOGIN_PAGE:
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("back").hidden = false;
+ this.wizard.getButton("extra1").hidden = false;
+ this.wizard.canRewind = true;
+ this.checkFields();
+ break;
+ case OPTIONS_PAGE:
+ this.wizard.canRewind = false;
+ this.wizard.canAdvance = true;
+ if (!this._resettingSync) {
+ this.wizard.getButton("next").label =
+ this._stringBundle.GetStringFromName("button.syncOptionsDone.label");
+ this.wizard.getButton("next").removeAttribute("accesskey");
+ }
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("back").hidden = true;
+ this.wizard.getButton("cancel").hidden = !this._resettingSync;
+ this.wizard.getButton("extra1").hidden = true;
+ document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
+ document.getElementById("syncOptions").collapsed = this._resettingSync;
+ document.getElementById("mergeOptions").collapsed = this._settingUpNew;
+ break;
+ case OPTIONS_CONFIRM_PAGE:
+ this.wizard.canRewind = true;
+ this.wizard.canAdvance = true;
+ this.wizard.getButton("back").label =
+ this._stringBundle.GetStringFromName("button.syncOptionsCancel.label");
+ this.wizard.getButton("back").removeAttribute("accesskey");
+ this.wizard.getButton("back").hidden = this._resettingSync;
+ this.wizard.getButton("next").hidden = false;
+ this.wizard.getButton("finish").hidden = true;
+ break;
+ }
+ },
+
+ onWizardAdvance: function () {
+ // Check pageIndex so we don't prompt before the Sync setup wizard appears.
+ // This is a fallback in case the Master Password gets locked mid-wizard.
+ if ((this.wizard.pageIndex >= 0) &&
+ !Weave.Utils.ensureMPUnlocked()) {
+ return false;
+ }
+
+ switch (this.wizard.pageIndex) {
+ case PAIR_PAGE:
+ this.startPairing();
+ return false;
+ case NEW_ACCOUNT_START_PAGE:
+ // If the user selects Next (e.g. by hitting enter) when we haven't
+ // executed the delayed checks yet, execute them immediately.
+ if (this._checkAccountTimer) {
+ this.checkAccount();
+ }
+ if (this._checkServerTimer) {
+ this.checkServer();
+ }
+ if (!this.wizard.canAdvance) {
+ return false;
+ }
+
+ let doc = this.captchaBrowser.contentDocument;
+ let getField = function getField(field) {
+ let node = doc.getElementById("recaptcha_" + field + "_field");
+ return node && node.value;
+ };
+
+ // Display throbber
+ let feedback = document.getElementById("captchaFeedback");
+ let image = feedback.firstChild;
+ let label = image.nextSibling;
+ image.setAttribute("status", "active");
+ label.value = this._stringBundle.GetStringFromName("verifying.label");
+ setVisibility(feedback, true);
+
+ let password = document.getElementById("weavePassword").value;
+ let email = Weave.Utils.normalizeAccount(
+ document.getElementById("weaveEmail").value);
+ let challenge = getField("challenge");
+ let response = getField("response");
+
+ let error = Weave.Service.createAccount(email, password,
+ challenge, response);
+
+ if (error == null) {
+ Weave.Service.identity.account = email;
+ Weave.Service.identity.basicPassword = password;
+ Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase();
+ this._handleNoScript(false);
+ Weave.Svc.Prefs.set("firstSync", "newAccount");
+ this.wizardFinish();
+ return false;
+ }
+
+ image.setAttribute("status", "error");
+ label.value = Weave.Utils.getErrorString(error);
+ return false;
+ case EXISTING_ACCOUNT_LOGIN_PAGE:
+ Weave.Service.identity.account = Weave.Utils.normalizeAccount(
+ document.getElementById("existingAccountName").value);
+ Weave.Service.identity.basicPassword =
+ document.getElementById("existingPassword").value;
+ let pp = document.getElementById("existingPassphrase").value;
+ Weave.Service.identity.syncKey = Weave.Utils.normalizePassphrase(pp);
+ if (Weave.Service.login()) {
+ this.wizardFinish();
+ }
+ return false;
+ case OPTIONS_PAGE:
+ let desc = document.getElementById("mergeChoiceRadio").selectedIndex;
+ // No confirmation needed on new account setup or merge option
+ // with existing account.
+ if (this._settingUpNew || (!this._resettingSync && desc == 0))
+ return this.returnFromOptions();
+ return this._handleChoice();
+ case OPTIONS_CONFIRM_PAGE:
+ if (this._resettingSync) {
+ this.wizardFinish();
+ return false;
+ }
+ return this.returnFromOptions();
+ }
+ return true;
+ },
+
+ onWizardBack: function () {
+ switch (this.wizard.pageIndex) {
+ case NEW_ACCOUNT_START_PAGE:
+ this.wizard.pageIndex = INTRO_PAGE;
+ return false;
+ case EXISTING_ACCOUNT_CONNECT_PAGE:
+ this.abortEasySetup();
+ this.wizard.pageIndex = INTRO_PAGE;
+ return false;
+ case EXISTING_ACCOUNT_LOGIN_PAGE:
+ // If we were already pairing on entry, we went straight to the manual
+ // login page. If subsequently we go back, return to the page that lets
+ // us choose whether we already have an account.
+ if (this.wizardType == "pair") {
+ this.wizard.pageIndex = INTRO_PAGE;
+ return false;
+ }
+ return true;
+ case OPTIONS_CONFIRM_PAGE:
+ // Backing up from the confirmation page = resetting first sync to merge.
+ document.getElementById("mergeChoiceRadio").selectedIndex = 0;
+ return this.returnFromOptions();
+ }
+ return true;
+ },
+
+ wizardFinish: function () {
+ this.setupInitialSync();
+
+ if (this.wizardType == "pair") {
+ this.completePairing();
+ }
+
+ if (!this._resettingSync) {
+ function isChecked(element) {
+ return document.getElementById(element).hasAttribute("checked");
+ }
+
+ let prefs = ["engine.bookmarks", "engine.passwords", "engine.history",
+ "engine.tabs", "engine.prefs", "engine.addons"];
+ for (let i = 0;i < prefs.length;i++) {
+ Weave.Svc.Prefs.set(prefs[i], isChecked(prefs[i]));
+ }
+ this._handleNoScript(false);
+ if (Weave.Svc.Prefs.get("firstSync", "") == "notReady")
+ Weave.Svc.Prefs.reset("firstSync");
+
+ Weave.Service.persistLogin();
+ Weave.Svc.Obs.notify("weave:service:setup-complete");
+ }
+ Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
+ window.close();
+ },
+
+ onWizardCancel: function () {
+ if (this._resettingSync)
+ return;
+
+ this.abortEasySetup();
+ this._handleNoScript(false);
+ Weave.Service.startOver();
+ },
+
+ onSyncOptions: function () {
+ this._beforeOptionsPage = this.wizard.pageIndex;
+ this.wizard.pageIndex = OPTIONS_PAGE;
+ },
+
+ returnFromOptions: function() {
+ this.wizard.getButton("next").label = this._nextButtonLabel;
+ this.wizard.getButton("next").setAttribute("accesskey",
+ this._nextButtonAccesskey);
+ this.wizard.getButton("back").label = this._backButtonLabel;
+ this.wizard.getButton("back").setAttribute("accesskey",
+ this._backButtonAccesskey);
+ this.wizard.getButton("cancel").hidden = false;
+ this.wizard.getButton("extra1").hidden = false;
+ this.wizard.pageIndex = this._beforeOptionsPage;
+ return false;
+ },
+
+ startPairing: function startPairing() {
+ this.pairDeviceErrorRow.hidden = true;
+ // When onAbort is called, Weave may already be gone.
+ const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
+
+ let self = this;
+ let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({
+ onPaired: function onPaired() {
+ self.wizard.pageIndex = INTRO_PAGE;
+ },
+ onComplete: function onComplete() {
+ // This method will never be called since SendCredentialsController
+ // will take over after the wizard completes.
+ },
+ onAbort: function onAbort(error) {
+ delete self._jpakeclient;
+
+ // Aborted by user, ignore. The window is almost certainly going to close
+ // or is already closed.
+ if (error == JPAKE_ERROR_USERABORT) {
+ return;
+ }
+
+ self.pairDeviceErrorRow.hidden = false;
+ self.pairDeviceThrobber.hidden = true;
+ self.pin1.value = self.pin2.value = self.pin3.value = "";
+ self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false;
+ if (self.wizard.pageIndex == PAIR_PAGE) {
+ self.pin1.focus();
+ }
+ }
+ });
+ this.pairDeviceThrobber.hidden = false;
+ this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true;
+ this.wizard.canAdvance = false;
+
+ let pin = this.pin1.value + this.pin2.value + this.pin3.value;
+ let expectDelay = true;
+ jpakeclient.pairWithPIN(pin, expectDelay);
+ },
+
+ completePairing: function completePairing() {
+ if (!this._jpakeclient) {
+ // The channel was aborted while we were setting up the account
+ // locally. XXX TODO should we do anything here, e.g. tell
+ // the user on the last wizard page that it's ok, they just
+ // have to pair again?
+ return;
+ }
+ let controller = new Weave.SendCredentialsController(this._jpakeclient,
+ Weave.Service);
+ this._jpakeclient.controller = controller;
+ },
+
+ startEasySetup: function () {
+ // Don't do anything if we have a client already (e.g. we went to
+ // Sync Options and just came back).
+ if (this._jpakeclient)
+ return;
+
+ // When onAbort is called, Weave may already be gone
+ const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
+
+ let self = this;
+ this._jpakeclient = new Weave.JPAKEClient({
+ displayPIN: function displayPIN(pin) {
+ document.getElementById("easySetupPIN1").value = pin.slice(0, 4);
+ document.getElementById("easySetupPIN2").value = pin.slice(4, 8);
+ document.getElementById("easySetupPIN3").value = pin.slice(8);
+ },
+
+ onPairingStart: function onPairingStart() {},
+
+ onComplete: function onComplete(credentials) {
+ Weave.Service.identity.account = credentials.account;
+ Weave.Service.identity.basicPassword = credentials.password;
+ Weave.Service.identity.syncKey = credentials.synckey;
+ Weave.Service.serverURL = credentials.serverURL;
+ gSyncSetup.wizardFinish();
+ },
+
+ onAbort: function onAbort(error) {
+ delete self._jpakeclient;
+
+ // Ignore if wizard is aborted.
+ if (error == JPAKE_ERROR_USERABORT)
+ return;
+
+ // Automatically go to manual setup if we couldn't acquire a channel.
+ if (error == Weave.JPAKE_ERROR_CHANNEL) {
+ self.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE;
+ return;
+ }
+
+ // Restart on all other errors.
+ self.startEasySetup();
+ }
+ });
+ this._jpakeclient.receiveNoPIN();
+ },
+
+ abortEasySetup: function () {
+ document.getElementById("easySetupPIN1").value = "";
+ document.getElementById("easySetupPIN2").value = "";
+ document.getElementById("easySetupPIN3").value = "";
+ if (!this._jpakeclient)
+ return;
+
+ this._jpakeclient.abort();
+ delete this._jpakeclient;
+ },
+
+ manualSetup: function () {
+ this.abortEasySetup();
+ this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE;
+ },
+
+ // _handleNoScript is needed because it blocks the captcha. So we temporarily
+ // allow the necessary sites so that we can verify the user is in fact a human.
+ // This was done with the help of Giorgio (NoScript author). See bug 508112.
+ _handleNoScript: function (addExceptions) {
+ // if NoScript isn't installed, or is disabled, bail out.
+ let ns = Cc["@maone.net/noscript-service;1"];
+ if (ns == null)
+ return;
+
+ ns = ns.getService().wrappedJSObject;
+ if (addExceptions) {
+ this._remoteSites.forEach(function(site) {
+ site = ns.getSite(site);
+ if (!ns.isJSEnabled(site)) {
+ this._disabledSites.push(site); // save status
+ ns.setJSEnabled(site, true); // allow site
+ }
+ }, this);
+ }
+ else {
+ this._disabledSites.forEach(function(site) {
+ ns.setJSEnabled(site, false);
+ });
+ this._disabledSites = [];
+ }
+ },
+
+ onExistingServerCommand: function () {
+ let control = document.getElementById("existingServer");
+ if (control.selectedIndex == 0) {
+ control.removeAttribute("editable");
+ Weave.Svc.Prefs.reset("serverURL");
+ } else {
+ control.setAttribute("editable", "true");
+ // Force a style flush to ensure that the binding is attached.
+ control.clientTop;
+ control.value = "";
+ control.inputField.focus();
+ }
+ document.getElementById("existingServerFeedbackRow").hidden = true;
+ this.checkFields();
+ },
+
+ onExistingServerInput: function () {
+ // Check custom server validity when the user stops typing for 1 second.
+ if (this._existingServerTimer)
+ window.clearTimeout(this._existingServerTimer);
+ this._existingServerTimer = window.setTimeout(function () {
+ gSyncSetup.checkFields();
+ }, 1000);
+ },
+
+ onServerCommand: function () {
+ setVisibility(document.getElementById("TOSRow"), this._usingMainServers);
+ let control = document.getElementById("server");
+ if (!this._usingMainServers) {
+ control.setAttribute("editable", "true");
+ // Force a style flush to ensure that the binding is attached.
+ control.clientTop;
+ control.value = "";
+ control.inputField.focus();
+ // checkServer() will call checkAccount() and checkFields().
+ this.checkServer();
+ return;
+ }
+ control.removeAttribute("editable");
+ Weave.Svc.Prefs.reset("serverURL");
+ if (this._settingUpNew) {
+ this.loadCaptcha();
+ }
+ this.checkAccount();
+ this.status.server = true;
+ document.getElementById("serverFeedbackRow").hidden = true;
+ this.checkFields();
+ },
+
+ onServerInput: function () {
+ // Check custom server validity when the user stops typing for 1 second.
+ if (this._checkServerTimer)
+ window.clearTimeout(this._checkServerTimer);
+ this._checkServerTimer = window.setTimeout(function () {
+ gSyncSetup.checkServer();
+ }, 1000);
+ },
+
+ checkServer: function () {
+ delete this._checkServerTimer;
+ let el = document.getElementById("server");
+ let valid = false;
+ let feedback = document.getElementById("serverFeedbackRow");
+ if (el.value) {
+ valid = this._validateServer(el);
+ let str = valid ? "" : "serverInvalid.label";
+ this._setFeedbackMessage(feedback, valid, str);
+ }
+ else
+ this._setFeedbackMessage(feedback, true);
+
+ // Recheck account against the new server.
+ if (valid)
+ this.checkAccount();
+
+ this.status.server = valid;
+ this.checkFields();
+ },
+
+ _validateServer: function (element) {
+ let valid = false;
+ let val = element.value;
+ if (!val)
+ return false;
+
+ let uri = Weave.Utils.makeURI(val);
+
+ if (!uri)
+ uri = Weave.Utils.makeURI("https://" + val);
+
+ if (uri && this._settingUpNew) {
+ function isValid(uri) {
+ Weave.Service.serverURL = uri.spec;
+ let check = Weave.Service.checkAccount("a");
+ return (check == "available" || check == "notAvailable");
+ }
+
+ if (uri.schemeIs("http")) {
+ uri.scheme = "https";
+ if (isValid(uri))
+ valid = true;
+ else
+ // setting the scheme back to http
+ uri.scheme = "http";
+ }
+ if (!valid)
+ valid = isValid(uri);
+
+ if (valid) {
+ this.loadCaptcha();
+ }
+ }
+ else if (uri) {
+ valid = true;
+ Weave.Service.serverURL = uri.spec;
+ }
+
+ if (valid)
+ element.value = Weave.Service.serverURL;
+ else
+ Weave.Svc.Prefs.reset("serverURL");
+
+ return valid;
+ },
+
+ _handleChoice: function () {
+ let desc = document.getElementById("mergeChoiceRadio").selectedIndex;
+ document.getElementById("chosenActionDeck").selectedIndex = desc;
+ switch (desc) {
+ case 1:
+ if (this._case1Setup)
+ break;
+
+ let places_db = PlacesUtils.history
+ .QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ if (Weave.Service.engineManager.get("history").enabled) {
+ let daysOfHistory = 0;
+ let stm = places_db.createStatement(
+ "SELECT ROUND(( " +
+ "strftime('%s','now','localtime','utc') - " +
+ "( " +
+ "SELECT visit_date FROM moz_historyvisits " +
+ "ORDER BY visit_date ASC LIMIT 1 " +
+ ")/1000000 " +
+ ")/86400) AS daysOfHistory ");
+
+ if (stm.step())
+ daysOfHistory = stm.getInt32(0);
+ // Support %S for historical reasons (see bug 600141)
+ document.getElementById("historyCount").value =
+ PluralForm.get(daysOfHistory,
+ this._stringBundle.GetStringFromName("historyDaysCount.label"))
+ .replace("%S", daysOfHistory)
+ .replace("#1", daysOfHistory);
+ } else {
+ document.getElementById("historyCount").hidden = true;
+ }
+
+ if (Weave.Service.engineManager.get("bookmarks").enabled) {
+ let bookmarks = 0;
+ let stm = places_db.createStatement(
+ "SELECT count(*) AS bookmarks " +
+ "FROM moz_bookmarks b " +
+ "LEFT JOIN moz_bookmarks t ON " +
+ "b.parent = t.id WHERE b.type = 1 AND t.parent <> :tag");
+ stm.params.tag = PlacesUtils.tagsFolderId;
+ if (stm.executeStep())
+ bookmarks = stm.row.bookmarks;
+ // Support %S for historical reasons (see bug 600141)
+ document.getElementById("bookmarkCount").value =
+ PluralForm.get(bookmarks,
+ this._stringBundle.GetStringFromName("bookmarksCount.label"))
+ .replace("%S", bookmarks)
+ .replace("#1", bookmarks);
+ } else {
+ document.getElementById("bookmarkCount").hidden = true;
+ }
+
+ if (Weave.Service.engineManager.get("passwords").enabled) {
+ let logins = Services.logins.getAllLogins({});
+ // Support %S for historical reasons (see bug 600141)
+ document.getElementById("passwordCount").value =
+ PluralForm.get(logins.length,
+ this._stringBundle.GetStringFromName("passwordsCount.label"))
+ .replace("%S", logins.length)
+ .replace("#1", logins.length);
+ } else {
+ document.getElementById("passwordCount").hidden = true;
+ }
+
+ if (!Weave.Service.engineManager.get("prefs").enabled) {
+ document.getElementById("prefsWipe").hidden = true;
+ }
+
+ let addonsEngine = Weave.Service.engineManager.get("addons");
+ if (addonsEngine.enabled) {
+ let ids = addonsEngine._store.getAllIDs();
+ let blessedcount = Object.keys(ids).filter(id => ids[id]).length;
+ // bug 600141 does not apply, as this does not have to support existing strings
+ document.getElementById("addonCount").value =
+ PluralForm.get(blessedcount,
+ this._stringBundle.GetStringFromName("addonsCount.label"))
+ .replace("#1", blessedcount);
+ } else {
+ document.getElementById("addonCount").hidden = true;
+ }
+
+ this._case1Setup = true;
+ break;
+ case 2:
+ if (this._case2Setup)
+ break;
+ let count = 0;
+ function appendNode(label) {
+ let box = document.getElementById("clientList");
+ let node = document.createElement("label");
+ node.setAttribute("value", label);
+ node.setAttribute("class", "data indent");
+ box.appendChild(node);
+ }
+
+ for (let name of Weave.Service.clientsEngine.stats.names) {
+ // Don't list the current client
+ if (name == Weave.Service.clientsEngine.localName)
+ continue;
+
+ // Only show the first several client names
+ if (++count <= 5)
+ appendNode(name);
+ }
+ if (count > 5) {
+ // Support %S for historical reasons (see bug 600141)
+ let label =
+ PluralForm.get(count - 5,
+ this._stringBundle.GetStringFromName("additionalClientCount.label"))
+ .replace("%S", count - 5)
+ .replace("#1", count - 5);
+ appendNode(label);
+ }
+ this._case2Setup = true;
+ break;
+ }
+
+ return true;
+ },
+
+ // sets class and string on a feedback element
+ // if no property string is passed in, we clear label/style
+ _setFeedback: function (element, success, string) {
+ element.hidden = success || !string;
+ let classname = success ? "success" : "error";
+ let image = element.getElementsByAttribute("class", "statusIcon")[0];
+ image.setAttribute("status", classname);
+ let label = element.getElementsByAttribute("class", "status")[0];
+ label.value = string;
+ },
+
+ // shim
+ _setFeedbackMessage: function (element, success, string) {
+ let str = "";
+ if (string) {
+ try {
+ str = this._stringBundle.GetStringFromName(string);
+ } catch (e) {}
+
+ if (!str)
+ str = Weave.Utils.getErrorString(string);
+ }
+ this._setFeedback(element, success, str);
+ },
+
+ loadCaptcha: function loadCaptcha() {
+ let captchaURI = Weave.Service.miscAPI + "captcha_html";
+ // First check for NoScript and whitelist the right sites.
+ this._handleNoScript(true);
+ if (this.captchaBrowser.currentURI.spec != captchaURI) {
+ this.captchaBrowser.loadURI(captchaURI);
+ }
+ },
+
+ onStateChange: function(webProgress, request, stateFlags, status) {
+ // We're only looking for the end of the frame load
+ if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) == 0)
+ return;
+ if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) == 0)
+ return;
+ if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) == 0)
+ return;
+
+ // If we didn't find a captcha, assume it's not needed and don't show it.
+ let responseStatus = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+ setVisibility(this.captchaBrowser, responseStatus != 404);
+ // XXX TODO we should really log any responseStatus other than 200
+ },
+ onProgressChange: function() {},
+ onStatusChange: function() {},
+ onSecurityChange: function() {},
+ onLocationChange: function () {}
+};
+
+// Define lazy getters for various XUL elements.
+//
+// onWizardAdvance() and onPageShow() are run before init(), so we'll even
+// define things that will almost certainly be used (like 'wizard') as a lazy
+// getter here.
+["wizard",
+ "pin1",
+ "pin2",
+ "pin3",
+ "pairDeviceErrorRow",
+ "pairDeviceThrobber"].forEach(function (id) {
+ XPCOMUtils.defineLazyGetter(gSyncSetup, id, function() {
+ return document.getElementById(id);
+ });
+});
+XPCOMUtils.defineLazyGetter(gSyncSetup, "nextFocusEl", function () {
+ return {pin1: this.pin2,
+ pin2: this.pin3,
+ pin3: this.wizard.getButton("next")};
+});
+XPCOMUtils.defineLazyGetter(gSyncSetup, "_stringBundle", function() {
+ return Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
+});
diff --git a/browser/base/content/sync/setup.xul b/browser/base/content/sync/setup.xul
new file mode 100644
index 000000000..11c085931
--- /dev/null
+++ b/browser/base/content/sync/setup.xul
@@ -0,0 +1,490 @@
+<?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://browser/skin/syncSetup.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
+<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd">
+%brandDTD;
+%syncBrandDTD;
+%syncSetupDTD;
+]>
+<wizard id="wizard"
+ title="&accountSetupTitle.label;"
+ windowtype="Weave:AccountSetup"
+ persist="screenX screenY"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onwizardnext="return gSyncSetup.onWizardAdvance()"
+ onwizardback="return gSyncSetup.onWizardBack()"
+ onwizardcancel="gSyncSetup.onWizardCancel()"
+ onload="gSyncSetup.init()">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/sync/setup.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/sync/utils.js"/>
+ <script type="application/javascript"
+ src="chrome://browser/content/utilityOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/printUtils.js"/>
+
+ <wizardpage id="addDevicePage"
+ label="&pairDevice.title.label;"
+ onpageshow="gSyncSetup.onPageShow()">
+ <description>
+ &pairDevice.dialog.description.label;
+ <label class="text-link"
+ value="&addDevice.showMeHow.label;"
+ href="https://services.mozilla.com/sync/help/add-device"/>
+ </description>
+ <separator class="groove-thin"/>
+ <description>
+ &addDevice.dialog.enterCode.label;
+ </description>
+ <separator class="groove-thin"/>
+ <vbox align="center">
+ <textbox id="pin1"
+ class="pin"
+ oninput="gSyncSetup.onPINInput(this);"
+ onfocus="this.select();"
+ />
+ <textbox id="pin2"
+ class="pin"
+ oninput="gSyncSetup.onPINInput(this);"
+ onfocus="this.select();"
+ />
+ <textbox id="pin3"
+ class="pin"
+ oninput="gSyncSetup.onPINInput(this);"
+ onfocus="this.select();"
+ />
+ </vbox>
+ <separator class="groove-thin"/>
+ <vbox id="pairDeviceThrobber" align="center" hidden="true">
+ <image/>
+ </vbox>
+ <hbox id="pairDeviceErrorRow" pack="center" hidden="true">
+ <image class="statusIcon" status="error"/>
+ <label class="status"
+ value="&addDevice.dialog.tryAgain.label;"/>
+ </hbox>
+ </wizardpage>
+
+ <wizardpage id="pickSetupType"
+ label="&syncBrand.fullName.label;"
+ onpageshow="gSyncSetup.onPageShow()">
+ <vbox align="center" flex="1">
+ <description style="padding: 0 7em;">
+ &setup.pickSetupType.description2;
+ </description>
+ <spacer flex="3"/>
+ <button id="newAccount"
+ class="accountChoiceButton"
+ label="&button.createNewAccount.label;"
+ oncommand="gSyncSetup.startNewAccountSetup()"
+ align="center"/>
+ <spacer flex="1"/>
+ </vbox>
+ <separator class="groove"/>
+ <vbox align="center" flex="1">
+ <spacer flex="1"/>
+ <button id="existingAccount"
+ class="accountChoiceButton"
+ label="&button.haveAccount.label;"
+ oncommand="gSyncSetup.useExistingAccount()"/>
+ <spacer flex="3"/>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage label="&setup.newAccountDetailsPage.title.label;"
+ id="newAccountStart"
+ onextra1="gSyncSetup.onSyncOptions()"
+ onpageshow="gSyncSetup.onPageShow();">
+ <grid>
+ <columns>
+ <column/>
+ <column class="inputColumn" flex="1"/>
+ </columns>
+ <rows>
+ <row id="emailRow" align="center">
+ <label value="&setup.emailAddress.label;"
+ accesskey="&setup.emailAddress.accesskey;"
+ control="weaveEmail"/>
+ <textbox id="weaveEmail"
+ oninput="gSyncSetup.onEmailInput()"/>
+ </row>
+ <row id="emailFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </row>
+ <row id="passwordRow" align="center">
+ <label value="&setup.choosePassword.label;"
+ accesskey="&setup.choosePassword.accesskey;"
+ control="weavePassword"/>
+ <textbox id="weavePassword"
+ type="password"
+ onchange="gSyncSetup.onPasswordChange()"/>
+ </row>
+ <row id="confirmRow" align="center">
+ <label value="&setup.confirmPassword.label;"
+ accesskey="&setup.confirmPassword.accesskey;"
+ control="weavePasswordConfirm"/>
+ <textbox id="weavePasswordConfirm"
+ type="password"
+ onchange="gSyncSetup.onPasswordChange()"/>
+ </row>
+ <row id="passwordFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </row>
+ <row align="center">
+ <label control="server"
+ value="&server.label;"/>
+ <menulist id="server"
+ oncommand="gSyncSetup.onServerCommand()"
+ oninput="gSyncSetup.onServerInput()">
+ <menupopup>
+ <menuitem label="&serverType.default.label;"
+ value="main"/>
+ <menuitem label="&serverType.custom2.label;"
+ value="custom"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row id="serverFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </row>
+ <row id="TOSRow" align="center">
+ <spacer/>
+ <hbox align="center">
+ <checkbox id="tos"
+ accesskey="&setup.tosAgree1.accesskey;"
+ oncommand="this.focus(); gSyncSetup.checkFields();"/>
+ <description id="tosDesc"
+ flex="1"
+ onclick="document.getElementById('tos').focus();
+ document.getElementById('tos').click()">
+ &setup.tosAgree1.label;
+ <label class="text-link"
+ onclick="event.stopPropagation();gSyncUtils.openToS();">
+ &setup.tosLink.label;
+ </label>
+ &setup.tosAgree2.label;
+ <label class="text-link"
+ onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();">
+ &setup.ppLink.label;
+ </label>
+ &setup.tosAgree3.label;
+ </description>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ <spacer flex="1"/>
+ <vbox flex="1" align="center">
+ <browser height="150"
+ width="500"
+ id="captcha"
+ type="content"
+ disablehistory="true"/>
+ <spacer flex="1"/>
+ <hbox id="captchaFeedback">
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="addDevice"
+ label="&pairDevice.title.label;"
+ onextra1="gSyncSetup.onSyncOptions()"
+ onpageshow="gSyncSetup.onPageShow()">
+ <description>
+ &pairDevice.setup.description.label;
+ <label class="text-link"
+ value="&addDevice.showMeHow.label;"
+ href="https://services.mozilla.com/sync/help/easy-setup"/>
+ </description>
+ <label value="&addDevice.setup.enterCode.label;"
+ control="easySetupPIN1"/>
+ <spacer flex="1"/>
+ <vbox align="center" flex="1">
+ <textbox id="easySetupPIN1"
+ class="pin"
+ value=""
+ readonly="true"
+ />
+ <textbox id="easySetupPIN2"
+ class="pin"
+ value=""
+ readonly="true"
+ />
+ <textbox id="easySetupPIN3"
+ class="pin"
+ value=""
+ readonly="true"
+ />
+ </vbox>
+ <spacer flex="3"/>
+ <label class="text-link"
+ value="&addDevice.dontHaveDevice.label;"
+ onclick="gSyncSetup.manualSetup();"/>
+ </wizardpage>
+
+ <wizardpage id="existingAccount"
+ label="&setup.signInPage.title.label;"
+ onextra1="gSyncSetup.onSyncOptions()"
+ onpageshow="gSyncSetup.onPageShow()">
+ <grid>
+ <columns>
+ <column/>
+ <column class="inputColumn" flex="1"/>
+ </columns>
+ <rows>
+ <row id="existingAccountRow" align="center">
+ <label id="existingAccountLabel"
+ value="&signIn.account2.label;"
+ accesskey="&signIn.account2.accesskey;"
+ control="existingAccount"/>
+ <textbox id="existingAccountName"
+ oninput="gSyncSetup.checkFields(event)"
+ onchange="gSyncSetup.checkFields(event)"/>
+ </row>
+ <row id="existingPasswordRow" align="center">
+ <label id="existingPasswordLabel"
+ value="&signIn.password.label;"
+ accesskey="&signIn.password.accesskey;"
+ control="existingPassword"/>
+ <textbox id="existingPassword"
+ type="password"
+ onkeyup="gSyncSetup.checkFields(event)"
+ onchange="gSyncSetup.checkFields(event)"/>
+ </row>
+ <row id="existingPasswordFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </row>
+ <row align="center">
+ <spacer/>
+ <label class="text-link"
+ value="&resetPassword.label;"
+ onclick="gSyncUtils.resetPassword(); return false;"/>
+ </row>
+ <row align="center">
+ <label control="existingServer"
+ value="&server.label;"/>
+ <menulist id="existingServer"
+ oncommand="gSyncSetup.onExistingServerCommand()"
+ oninput="gSyncSetup.onExistingServerInput()">
+ <menupopup>
+ <menuitem label="&serverType.default.label;"
+ value="main"/>
+ <menuitem label="&serverType.custom2.label;"
+ value="custom"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row id="existingServerFeedbackRow" align="center" hidden="true">
+ <spacer/>
+ <hbox>
+ <image class="statusIcon"/>
+ <vbox>
+ <label class="status" value=" "/>
+ </vbox>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+
+ <groupbox>
+ <label id="existingPassphraseLabel"
+ value="&signIn.recoveryKey.label;"
+ accesskey="&signIn.recoveryKey.accesskey;"
+ control="existingPassphrase"/>
+ <textbox id="existingPassphrase"
+ oninput="gSyncSetup.checkFields()"/>
+ <hbox id="login-throbber" hidden="true">
+ <image/>
+ <label value="&verifying.label;"/>
+ </hbox>
+ <vbox align="left" id="existingPassphraseFeedbackRow" hidden="true">
+ <hbox>
+ <image class="statusIcon"/>
+ <label class="status" value=" "/>
+ </hbox>
+ </vbox>
+ </groupbox>
+
+ <vbox id="passphraseHelpBox">
+ <description>
+ &existingRecoveryKey.description;
+ <label class="text-link"
+ href="https://services.mozilla.com/sync/help/manual-setup">
+ &addDevice.showMeHow.label;
+ </label>
+ <spacer id="passphraseHelpSpacer"/>
+ <label class="text-link"
+ onclick="gSyncSetup.resetPassphrase(); return false;">
+ &resetSyncKey.label;
+ </label>
+ </description>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="syncOptionsPage"
+ label="&setup.optionsPage.title;"
+ onpageshow="gSyncSetup.onPageShow()">
+ <groupbox id="syncOptions">
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1" style="margin-inline-end: 2px"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label value="&syncDeviceName.label;"
+ accesskey="&syncDeviceName.accesskey;"
+ control="syncComputerName"/>
+ <textbox id="syncComputerName" flex="1"
+ onchange="gSyncUtils.changeName(this)"/>
+ </row>
+ <row>
+ <label value="&syncMy.label;" />
+ <vbox>
+ <checkbox label="&engine.addons.label;"
+ accesskey="&engine.addons.accesskey;"
+ id="engine.addons"
+ checked="true"/>
+ <checkbox label="&engine.bookmarks.label;"
+ accesskey="&engine.bookmarks.accesskey;"
+ id="engine.bookmarks"
+ checked="true"/>
+ <checkbox label="&engine.passwords.label;"
+ accesskey="&engine.passwords.accesskey;"
+ id="engine.passwords"
+ checked="true"/>
+ <checkbox label="&engine.prefs.label;"
+ accesskey="&engine.prefs.accesskey;"
+ id="engine.prefs"
+ checked="true"/>
+ <checkbox label="&engine.history.label;"
+ accesskey="&engine.history.accesskey;"
+ id="engine.history"
+ checked="true"/>
+ <checkbox label="&engine.tabs.label;"
+ accesskey="&engine.tabs.accesskey;"
+ id="engine.tabs"
+ checked="true"/>
+ </vbox>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <groupbox id="mergeOptions">
+ <radiogroup id="mergeChoiceRadio" pack="start">
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows flex="1">
+ <row align="center">
+ <radio id="resetClient"
+ class="mergeChoiceButton"
+ aria-labelledby="resetClientLabel"/>
+ <label id="resetClientLabel" control="resetClient">
+ <html:strong>&choice2.merge.recommended.label;</html:strong>
+ &choice2a.merge.main.label;
+ </label>
+ </row>
+ <row align="center">
+ <radio id="wipeClient"
+ class="mergeChoiceButton"
+ aria-labelledby="wipeClientLabel"/>
+ <label id="wipeClientLabel"
+ control="wipeClient">
+ &choice2a.client.main.label;
+ </label>
+ </row>
+ <row align="center">
+ <radio id="wipeRemote"
+ class="mergeChoiceButton"
+ aria-labelledby="wipeRemoteLabel"/>
+ <label id="wipeRemoteLabel"
+ control="wipeRemote">
+ &choice2a.server.main.label;
+ </label>
+ </row>
+ </rows>
+ </grid>
+ </radiogroup>
+ </groupbox>
+ </wizardpage>
+
+ <wizardpage id="syncOptionsConfirm"
+ label="&setup.optionsConfirmPage.title;"
+ onpageshow="gSyncSetup.onPageShow()">
+ <deck id="chosenActionDeck">
+ <vbox id="chosenActionMerge" class="confirm">
+ <description class="normal">
+ &confirm.merge2.label;
+ </description>
+ </vbox>
+ <vbox id="chosenActionWipeClient" class="confirm">
+ <description class="normal">
+ &confirm.client3.label;
+ </description>
+ <separator class="thin"/>
+ <vbox id="dataList">
+ <label class="data indent" id="bookmarkCount"/>
+ <label class="data indent" id="historyCount"/>
+ <label class="data indent" id="passwordCount"/>
+ <label class="data indent" id="addonCount"/>
+ <label class="data indent" id="prefsWipe"
+ value="&engine.prefs.label;"/>
+ </vbox>
+ <separator class="thin"/>
+ <description class="normal">
+ &confirm.client2.moreinfo.label;
+ </description>
+ </vbox>
+ <vbox id="chosenActionWipeServer" class="confirm">
+ <description class="normal">
+ &confirm.server2.label;
+ </description>
+ <separator class="thin"/>
+ <vbox id="clientList">
+ </vbox>
+ </vbox>
+ </deck>
+ </wizardpage>
+ <!-- In terms of the wizard flow shown to the user, the 'syncOptionsConfirm'
+ page above is not the last wizard page. To prevent the wizard binding from
+ assuming that it is, we're inserting this dummy page here. This also means
+ that the wizard needs to always be closed manually via wizardFinish(). -->
+ <wizardpage>
+ </wizardpage>
+</wizard>
+
diff --git a/browser/base/content/sync/utils.js b/browser/base/content/sync/utils.js
new file mode 100644
index 000000000..92981f7b4
--- /dev/null
+++ b/browser/base/content/sync/utils.js
@@ -0,0 +1,231 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Equivalent to 0o600 permissions; used for saved Sync Recovery Key.
+// This constant can be replaced when the equivalent values are available to
+// chrome JS; see Bug 433295 and Bug 757351.
+const PERMISSIONS_RWUSR = 0x180;
+
+// Weave should always exist before before this file gets included.
+var gSyncUtils = {
+ get bundle() {
+ delete this.bundle;
+ return this.bundle = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
+ },
+
+ get fxAccountsEnabled() {
+ let service = Components.classes["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ return service.fxAccountsEnabled;
+ },
+
+ // opens in a new window if we're in a modal prefwindow world, in a new tab otherwise
+ _openLink: function (url) {
+ let thisDocEl = document.documentElement,
+ openerDocEl = window.opener && window.opener.document.documentElement;
+ if (thisDocEl.id == "accountSetup" && window.opener &&
+ openerDocEl.id == "BrowserPreferences" && !openerDocEl.instantApply)
+ openUILinkIn(url, "window");
+ else if (thisDocEl.id == "BrowserPreferences" && !thisDocEl.instantApply)
+ openUILinkIn(url, "window");
+ else if (document.documentElement.id == "change-dialog")
+ Services.wm.getMostRecentWindow("navigator:browser")
+ .openUILinkIn(url, "tab");
+ else
+ openUILinkIn(url, "tab");
+ },
+
+ changeName: function changeName(input) {
+ // Make sure to update to a modified name, e.g., empty-string -> default
+ Weave.Service.clientsEngine.localName = input.value;
+ input.value = Weave.Service.clientsEngine.localName;
+ },
+
+ openChange: function openChange(type, duringSetup) {
+ // Just re-show the dialog if it's already open
+ let openedDialog = Services.wm.getMostRecentWindow("Sync:" + type);
+ if (openedDialog != null) {
+ openedDialog.focus();
+ return;
+ }
+
+ // Open up the change dialog
+ let changeXUL = "chrome://browser/content/sync/genericChange.xul";
+ let changeOpt = "centerscreen,chrome,resizable=no";
+ Services.ww.activeWindow.openDialog(changeXUL, "", changeOpt,
+ type, duringSetup);
+ },
+
+ changePassword: function () {
+ if (Weave.Utils.ensureMPUnlocked())
+ this.openChange("ChangePassword");
+ },
+
+ resetPassphrase: function (duringSetup) {
+ if (Weave.Utils.ensureMPUnlocked())
+ this.openChange("ResetPassphrase", duringSetup);
+ },
+
+ updatePassphrase: function () {
+ if (Weave.Utils.ensureMPUnlocked())
+ this.openChange("UpdatePassphrase");
+ },
+
+ resetPassword: function () {
+ this._openLink(Weave.Service.pwResetURL);
+ },
+
+ get tosURL() {
+ let root = this.fxAccountsEnabled ? "fxa." : "";
+ return Weave.Svc.Prefs.get(root + "termsURL");
+ },
+
+ openToS: function () {
+ this._openLink(this.tosURL);
+ },
+
+ get privacyPolicyURL() {
+ let root = this.fxAccountsEnabled ? "fxa." : "";
+ return Weave.Svc.Prefs.get(root + "privacyURL");
+ },
+
+ openPrivacyPolicy: function () {
+ this._openLink(this.privacyPolicyURL);
+ },
+
+ /**
+ * Prepare an invisible iframe with the passphrase backup document.
+ * Used by both the print and saving methods.
+ *
+ * @param elid : ID of the form element containing the passphrase.
+ * @param callback : Function called once the iframe has loaded.
+ */
+ _preparePPiframe: function(elid, callback) {
+ let pp = document.getElementById(elid).value;
+
+ // Create an invisible iframe whose contents we can print.
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "chrome://browser/content/sync/key.xhtml");
+ iframe.collapsed = true;
+ document.documentElement.appendChild(iframe);
+ iframe.contentWindow.addEventListener("load", function() {
+ iframe.contentWindow.removeEventListener("load", arguments.callee, false);
+
+ // Insert the Sync Key into the page.
+ let el = iframe.contentDocument.getElementById("synckey");
+ el.firstChild.nodeValue = pp;
+
+ // Insert the TOS and Privacy Policy URLs into the page.
+ let termsURL = Weave.Svc.Prefs.get("termsURL");
+ el = iframe.contentDocument.getElementById("tosLink");
+ el.setAttribute("href", termsURL);
+ el.firstChild.nodeValue = termsURL;
+
+ let privacyURL = Weave.Svc.Prefs.get("privacyURL");
+ el = iframe.contentDocument.getElementById("ppLink");
+ el.setAttribute("href", privacyURL);
+ el.firstChild.nodeValue = privacyURL;
+
+ callback(iframe);
+ }, false);
+ },
+
+ /**
+ * Print passphrase backup document.
+ *
+ * @param elid : ID of the form element containing the passphrase.
+ */
+ passphrasePrint: function(elid) {
+ this._preparePPiframe(elid, function(iframe) {
+ let webBrowserPrint = iframe.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebBrowserPrint);
+ let printSettings = PrintUtils.getPrintSettings();
+
+ // Display no header/footer decoration except for the date.
+ printSettings.headerStrLeft
+ = printSettings.headerStrCenter
+ = printSettings.headerStrRight
+ = printSettings.footerStrLeft
+ = printSettings.footerStrCenter = "";
+ printSettings.footerStrRight = "&D";
+
+ try {
+ webBrowserPrint.print(printSettings, null);
+ } catch (ex) {
+ // print()'s return codes are expressed as exceptions. Ignore.
+ }
+ });
+ },
+
+ /**
+ * Save passphrase backup document to disk as HTML file.
+ *
+ * @param elid : ID of the form element containing the passphrase.
+ */
+ passphraseSave: function(elid) {
+ let dialogTitle = this.bundle.GetStringFromName("save.recoverykey.title");
+ let defaultSaveName = this.bundle.GetStringFromName("save.recoverykey.defaultfilename");
+ this._preparePPiframe(elid, function(iframe) {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let fpCallback = function fpCallback_done(aResult) {
+ if (aResult == Ci.nsIFilePicker.returnOK ||
+ aResult == Ci.nsIFilePicker.returnReplace) {
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ stream.init(fp.file, -1, PERMISSIONS_RWUSR, 0);
+
+ let serializer = new XMLSerializer();
+ let output = serializer.serializeToString(iframe.contentDocument);
+ output = output.replace(/<!DOCTYPE (.|\n)*?]>/,
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' +
+ '"DTD/xhtml1-strict.dtd">');
+ output = Weave.Utils.encodeUTF8(output);
+ stream.write(output, output.length);
+ }
+ };
+
+ fp.init(window, dialogTitle, Ci.nsIFilePicker.modeSave);
+ fp.appendFilters(Ci.nsIFilePicker.filterHTML);
+ fp.defaultString = defaultSaveName;
+ fp.open(fpCallback);
+ return false;
+ });
+ },
+
+ /**
+ * validatePassword
+ *
+ * @param el1 : the first textbox element in the form
+ * @param el2 : the second textbox element, if omitted it's an update form
+ *
+ * returns [valid, errorString]
+ */
+ validatePassword: function (el1, el2) {
+ let valid = false;
+ let val1 = el1.value;
+ let val2 = el2 ? el2.value : "";
+ let error = "";
+
+ if (!el2)
+ valid = val1.length >= Weave.MIN_PASS_LENGTH;
+ else if (val1 && val1 == Weave.Service.identity.username)
+ error = "change.password.pwSameAsUsername";
+ else if (val1 && val1 == Weave.Service.identity.account)
+ error = "change.password.pwSameAsEmail";
+ else if (val1 && val1 == Weave.Service.identity.basicPassword)
+ error = "change.password.pwSameAsPassword";
+ else if (val1 && val2) {
+ if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH)
+ valid = true;
+ else if (val1.length < Weave.MIN_PASS_LENGTH)
+ error = "change.password.tooShort";
+ else if (val1 != val2)
+ error = "change.password.mismatch";
+ }
+ let errorString = error ? Weave.Utils.getErrorString(error) : "";
+ return [valid, errorString];
+ }
+};
diff --git a/browser/base/content/tab-content.js b/browser/base/content/tab-content.js
new file mode 100644
index 000000000..06fa3d9cc
--- /dev/null
+++ b/browser/base/content/tab-content.js
@@ -0,0 +1,947 @@
+/* -*- 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 content script contains code that requires a tab browser. */
+
+var {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/ExtensionContent.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
+ "resource:///modules/E10SUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AboutReader",
+ "resource://gre/modules/AboutReader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
+ "resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyGetter(this, "SimpleServiceDiscovery", function() {
+ let ssdp = Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm", {}).SimpleServiceDiscovery;
+ // Register targets
+ ssdp.registerDevice({
+ id: "roku:ecp",
+ target: "roku:ecp",
+ factory: function(aService) {
+ Cu.import("resource://gre/modules/RokuApp.jsm");
+ return new RokuApp(aService);
+ },
+ types: ["video/mp4"],
+ extensions: ["mp4"]
+ });
+ return ssdp;
+});
+
+// TabChildGlobal
+var global = this;
+
+
+addEventListener("MozDOMPointerLock:Entered", function(aEvent) {
+ sendAsyncMessage("PointerLock:Entered", {
+ originNoSuffix: aEvent.target.nodePrincipal.originNoSuffix
+ });
+});
+
+addEventListener("MozDOMPointerLock:Exited", function(aEvent) {
+ sendAsyncMessage("PointerLock:Exited");
+});
+
+
+addMessageListener("Browser:HideSessionRestoreButton", function (message) {
+ // Hide session restore button on about:home
+ let doc = content.document;
+ let container;
+ if (doc.documentURI.toLowerCase() == "about:home" &&
+ (container = doc.getElementById("sessionRestoreContainer"))) {
+ container.hidden = true;
+ }
+});
+
+
+addMessageListener("Browser:Reload", function(message) {
+ /* First, we'll try to use the session history object to reload so
+ * that framesets are handled properly. If we're in a special
+ * window (such as view-source) that has no session history, fall
+ * back on using the web navigation's reload method.
+ */
+
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ try {
+ let sh = webNav.sessionHistory;
+ if (sh)
+ webNav = sh.QueryInterface(Ci.nsIWebNavigation);
+ } catch (e) {
+ }
+
+ let reloadFlags = message.data.flags;
+ try {
+ E10SUtils.wrapHandlingUserInput(content, message.data.handlingUserInput,
+ () => webNav.reload(reloadFlags));
+ } catch (e) {
+ }
+});
+
+addMessageListener("MixedContent:ReenableProtection", function() {
+ docShell.mixedContentChannel = null;
+});
+
+addMessageListener("SecondScreen:tab-mirror", function(message) {
+ if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
+ return;
+ }
+ let app = SimpleServiceDiscovery.findAppForService(message.data.service);
+ if (app) {
+ let width = content.innerWidth;
+ let height = content.innerHeight;
+ let viewport = {cssWidth: width, cssHeight: height, width: width, height: height};
+ app.mirror(function() {}, content, viewport, function() {}, content);
+ }
+});
+
+var AboutHomeListener = {
+ init: function(chromeGlobal) {
+ chromeGlobal.addEventListener('AboutHomeLoad', this, false, true);
+ },
+
+ get isAboutHome() {
+ return content.document.documentURI.toLowerCase() == "about:home";
+ },
+
+ handleEvent: function(aEvent) {
+ if (!this.isAboutHome) {
+ return;
+ }
+ switch (aEvent.type) {
+ case "AboutHomeLoad":
+ this.onPageLoad();
+ break;
+ case "click":
+ this.onClick(aEvent);
+ break;
+ case "pagehide":
+ this.onPageHide(aEvent);
+ break;
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ if (!this.isAboutHome) {
+ return;
+ }
+ switch (aMessage.name) {
+ case "AboutHome:Update":
+ this.onUpdate(aMessage.data);
+ break;
+ }
+ },
+
+ onUpdate: function(aData) {
+ let doc = content.document;
+ if (aData.showRestoreLastSession && !PrivateBrowsingUtils.isContentWindowPrivate(content))
+ doc.getElementById("launcher").setAttribute("session", "true");
+
+ // Inject search engine and snippets URL.
+ let docElt = doc.documentElement;
+ // Set snippetsVersion last, which triggers to show the snippets when it's set.
+ docElt.setAttribute("snippetsURL", aData.snippetsURL);
+ if (aData.showKnowYourRights)
+ docElt.setAttribute("showKnowYourRights", "true");
+ docElt.setAttribute("snippetsVersion", aData.snippetsVersion);
+ },
+
+ onPageLoad: function() {
+ addMessageListener("AboutHome:Update", this);
+ addEventListener("click", this, true);
+ addEventListener("pagehide", this, true);
+
+ sendAsyncMessage("AboutHome:MaybeShowAutoMigrationUndoNotification");
+ sendAsyncMessage("AboutHome:RequestUpdate");
+ },
+
+ onClick: function(aEvent) {
+ if (!aEvent.isTrusted || // Don't trust synthetic events
+ aEvent.button == 2 || aEvent.target.localName != "button") {
+ return;
+ }
+
+ let originalTarget = aEvent.originalTarget;
+ let ownerDoc = originalTarget.ownerDocument;
+ if (ownerDoc.documentURI != "about:home") {
+ // This shouldn't happen, but we're being defensive.
+ return;
+ }
+
+ let elmId = originalTarget.getAttribute("id");
+
+ switch (elmId) {
+ case "restorePreviousSession":
+ sendAsyncMessage("AboutHome:RestorePreviousSession");
+ ownerDoc.getElementById("launcher").removeAttribute("session");
+ break;
+
+ case "downloads":
+ sendAsyncMessage("AboutHome:Downloads");
+ break;
+
+ case "bookmarks":
+ sendAsyncMessage("AboutHome:Bookmarks");
+ break;
+
+ case "history":
+ sendAsyncMessage("AboutHome:History");
+ break;
+
+ case "addons":
+ sendAsyncMessage("AboutHome:Addons");
+ break;
+
+ case "sync":
+ sendAsyncMessage("AboutHome:Sync");
+ break;
+
+ case "settings":
+ sendAsyncMessage("AboutHome:Settings");
+ break;
+ }
+ },
+
+ onPageHide: function(aEvent) {
+ if (aEvent.target.defaultView.frameElement) {
+ return;
+ }
+ removeMessageListener("AboutHome:Update", this);
+ removeEventListener("click", this, true);
+ removeEventListener("pagehide", this, true);
+ },
+};
+AboutHomeListener.init(this);
+
+var AboutPrivateBrowsingListener = {
+ init(chromeGlobal) {
+ chromeGlobal.addEventListener("AboutPrivateBrowsingOpenWindow", this,
+ false, true);
+ chromeGlobal.addEventListener("AboutPrivateBrowsingToggleTrackingProtection", this,
+ false, true);
+ chromeGlobal.addEventListener("AboutPrivateBrowsingDontShowIntroPanelAgain", this,
+ false, true);
+ },
+
+ get isAboutPrivateBrowsing() {
+ return content.document.documentURI.toLowerCase() == "about:privatebrowsing";
+ },
+
+ handleEvent(aEvent) {
+ if (!this.isAboutPrivateBrowsing) {
+ return;
+ }
+ switch (aEvent.type) {
+ case "AboutPrivateBrowsingOpenWindow":
+ sendAsyncMessage("AboutPrivateBrowsing:OpenPrivateWindow");
+ break;
+ case "AboutPrivateBrowsingToggleTrackingProtection":
+ sendAsyncMessage("AboutPrivateBrowsing:ToggleTrackingProtection");
+ break;
+ case "AboutPrivateBrowsingDontShowIntroPanelAgain":
+ sendAsyncMessage("AboutPrivateBrowsing:DontShowIntroPanelAgain");
+ break;
+ }
+ },
+};
+AboutPrivateBrowsingListener.init(this);
+
+var AboutReaderListener = {
+
+ _articlePromise: null,
+
+ _isLeavingReaderMode: false,
+
+ init: function() {
+ addEventListener("AboutReaderContentLoaded", this, false, true);
+ addEventListener("DOMContentLoaded", this, false);
+ addEventListener("pageshow", this, false);
+ addEventListener("pagehide", this, false);
+ addMessageListener("Reader:ToggleReaderMode", this);
+ addMessageListener("Reader:PushState", this);
+ },
+
+ receiveMessage: function(message) {
+ switch (message.name) {
+ case "Reader:ToggleReaderMode":
+ if (!this.isAboutReader) {
+ this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError);
+ ReaderMode.enterReaderMode(docShell, content);
+ } else {
+ this._isLeavingReaderMode = true;
+ ReaderMode.leaveReaderMode(docShell, content);
+ }
+ break;
+
+ case "Reader:PushState":
+ this.updateReaderButton(!!(message.data && message.data.isArticle));
+ break;
+ }
+ },
+
+ get isAboutReader() {
+ if (!content) {
+ return false;
+ }
+ return content.document.documentURI.startsWith("about:reader");
+ },
+
+ handleEvent: function(aEvent) {
+ if (aEvent.originalTarget.defaultView != content) {
+ return;
+ }
+
+ switch (aEvent.type) {
+ case "AboutReaderContentLoaded":
+ if (!this.isAboutReader) {
+ return;
+ }
+
+ if (content.document.body) {
+ // Update the toolbar icon to show the "reader active" icon.
+ sendAsyncMessage("Reader:UpdateReaderButton");
+ new AboutReader(global, content, this._articlePromise);
+ this._articlePromise = null;
+ }
+ break;
+
+ case "pagehide":
+ this.cancelPotentialPendingReadabilityCheck();
+ // this._isLeavingReaderMode is used here to keep the Reader Mode icon
+ // visible in the location bar when transitioning from reader-mode page
+ // back to the source page.
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: this._isLeavingReaderMode });
+ if (this._isLeavingReaderMode) {
+ this._isLeavingReaderMode = false;
+ }
+ break;
+
+ case "pageshow":
+ // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
+ // event, so we need to rely on "pageshow" in this case.
+ if (aEvent.persisted) {
+ this.updateReaderButton();
+ }
+ break;
+ case "DOMContentLoaded":
+ this.updateReaderButton();
+ break;
+
+ }
+ },
+
+ /**
+ * NB: this function will update the state of the reader button asynchronously
+ * after the next mozAfterPaint call (assuming reader mode is enabled and
+ * this is a suitable document). Calling it on things which won't be
+ * painted is not going to work.
+ */
+ updateReaderButton: function(forceNonArticle) {
+ if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader ||
+ !content || !(content.document instanceof content.HTMLDocument) ||
+ content.document.mozSyntheticDocument) {
+ return;
+ }
+
+ this.scheduleReadabilityCheckPostPaint(forceNonArticle);
+ },
+
+ cancelPotentialPendingReadabilityCheck: function() {
+ if (this._pendingReadabilityCheck) {
+ removeEventListener("MozAfterPaint", this._pendingReadabilityCheck);
+ delete this._pendingReadabilityCheck;
+ }
+ },
+
+ scheduleReadabilityCheckPostPaint: function(forceNonArticle) {
+ if (this._pendingReadabilityCheck) {
+ // We need to stop this check before we re-add one because we don't know
+ // if forceNonArticle was true or false last time.
+ this.cancelPotentialPendingReadabilityCheck();
+ }
+ this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(this, forceNonArticle);
+ addEventListener("MozAfterPaint", this._pendingReadabilityCheck);
+ },
+
+ onPaintWhenWaitedFor: function(forceNonArticle, event) {
+ // In non-e10s, we'll get called for paints other than ours, and so it's
+ // possible that this page hasn't been laid out yet, in which case we
+ // should wait until we get an event that does relate to our layout. We
+ // determine whether any of our content got painted by checking if there
+ // are any painted rects.
+ if (!event.clientRects.length) {
+ return;
+ }
+
+ this.cancelPotentialPendingReadabilityCheck();
+ // Only send updates when there are articles; there's no point updating with
+ // |false| all the time.
+ if (ReaderMode.isProbablyReaderable(content.document)) {
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
+ } else if (forceNonArticle) {
+ sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
+ }
+ },
+};
+AboutReaderListener.init();
+
+
+var ContentSearchMediator = {
+
+ whitelist: new Set([
+ "about:home",
+ "about:newtab",
+ ]),
+
+ init: function (chromeGlobal) {
+ chromeGlobal.addEventListener("ContentSearchClient", this, true, true);
+ addMessageListener("ContentSearch", this);
+ },
+
+ handleEvent: function (event) {
+ if (this._contentWhitelisted) {
+ this._sendMsg(event.detail.type, event.detail.data);
+ }
+ },
+
+ receiveMessage: function (msg) {
+ if (msg.data.type == "AddToWhitelist") {
+ for (let uri of msg.data.data) {
+ this.whitelist.add(uri);
+ }
+ this._sendMsg("AddToWhitelistAck");
+ return;
+ }
+ if (this._contentWhitelisted) {
+ this._fireEvent(msg.data.type, msg.data.data);
+ }
+ },
+
+ get _contentWhitelisted() {
+ return this.whitelist.has(content.document.documentURI);
+ },
+
+ _sendMsg: function (type, data=null) {
+ sendAsyncMessage("ContentSearch", {
+ type: type,
+ data: data,
+ });
+ },
+
+ _fireEvent: function (type, data=null) {
+ let event = Cu.cloneInto({
+ detail: {
+ type: type,
+ data: data,
+ },
+ }, content);
+ content.dispatchEvent(new content.CustomEvent("ContentSearchService",
+ event));
+ },
+};
+ContentSearchMediator.init(this);
+
+var PageStyleHandler = {
+ init: function() {
+ addMessageListener("PageStyle:Switch", this);
+ addMessageListener("PageStyle:Disable", this);
+ addEventListener("pageshow", () => this.sendStyleSheetInfo());
+ },
+
+ get markupDocumentViewer() {
+ return docShell.contentViewer;
+ },
+
+ sendStyleSheetInfo: function() {
+ let filteredStyleSheets = this._filterStyleSheets(this.getAllStyleSheets());
+
+ sendAsyncMessage("PageStyle:StyleSheets", {
+ filteredStyleSheets: filteredStyleSheets,
+ authorStyleDisabled: this.markupDocumentViewer.authorStyleDisabled,
+ preferredStyleSheetSet: content.document.preferredStyleSheetSet
+ });
+ },
+
+ getAllStyleSheets: function(frameset = content) {
+ let selfSheets = Array.slice(frameset.document.styleSheets);
+ let subSheets = Array.map(frameset.frames, frame => this.getAllStyleSheets(frame));
+ return selfSheets.concat(...subSheets);
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "PageStyle:Switch":
+ this.markupDocumentViewer.authorStyleDisabled = false;
+ this._stylesheetSwitchAll(content, msg.data.title);
+ break;
+
+ case "PageStyle:Disable":
+ this.markupDocumentViewer.authorStyleDisabled = true;
+ break;
+ }
+
+ this.sendStyleSheetInfo();
+ },
+
+ _stylesheetSwitchAll: function (frameset, title) {
+ if (!title || this._stylesheetInFrame(frameset, title)) {
+ this._stylesheetSwitchFrame(frameset, title);
+ }
+
+ for (let i = 0; i < frameset.frames.length; i++) {
+ // Recurse into sub-frames.
+ this._stylesheetSwitchAll(frameset.frames[i], title);
+ }
+ },
+
+ _stylesheetSwitchFrame: function (frame, title) {
+ var docStyleSheets = frame.document.styleSheets;
+
+ for (let i = 0; i < docStyleSheets.length; ++i) {
+ let docStyleSheet = docStyleSheets[i];
+ if (docStyleSheet.title) {
+ docStyleSheet.disabled = (docStyleSheet.title != title);
+ } else if (docStyleSheet.disabled) {
+ docStyleSheet.disabled = false;
+ }
+ }
+ },
+
+ _stylesheetInFrame: function (frame, title) {
+ return Array.some(frame.document.styleSheets, (styleSheet) => styleSheet.title == title);
+ },
+
+ _filterStyleSheets: function(styleSheets) {
+ let result = [];
+
+ for (let currentStyleSheet of styleSheets) {
+ if (!currentStyleSheet.title)
+ continue;
+
+ // Skip any stylesheets that don't match the screen media type.
+ if (currentStyleSheet.media.length > 0) {
+ let mediaQueryList = currentStyleSheet.media.mediaText;
+ if (!content.matchMedia(mediaQueryList).matches) {
+ continue;
+ }
+ }
+
+ let URI;
+ try {
+ if (!currentStyleSheet.ownerNode ||
+ // special-case style nodes, which have no href
+ currentStyleSheet.ownerNode.nodeName.toLowerCase() != "style") {
+ URI = Services.io.newURI(currentStyleSheet.href, null, null);
+ }
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_MALFORMED_URI) {
+ throw e;
+ }
+ continue;
+ }
+
+ // We won't send data URIs all of the way up to the parent, as these
+ // can be arbitrarily large.
+ let sentURI = (!URI || URI.scheme == "data") ? null : URI.spec;
+
+ result.push({
+ title: currentStyleSheet.title,
+ disabled: currentStyleSheet.disabled,
+ href: sentURI,
+ });
+ }
+
+ return result;
+ },
+};
+PageStyleHandler.init();
+
+// Keep a reference to the translation content handler to avoid it it being GC'ed.
+var trHandler = null;
+if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) {
+ Cu.import("resource:///modules/translation/TranslationContentHandler.jsm");
+ trHandler = new TranslationContentHandler(global, docShell);
+}
+
+function gKeywordURIFixup(fixupInfo) {
+ fixupInfo.QueryInterface(Ci.nsIURIFixupInfo);
+ if (!fixupInfo.consumer) {
+ return;
+ }
+
+ // Ignore info from other docshells
+ let parent = fixupInfo.consumer.QueryInterface(Ci.nsIDocShellTreeItem).sameTypeRootTreeItem;
+ if (parent != docShell)
+ return;
+
+ let data = {};
+ for (let f of Object.keys(fixupInfo)) {
+ if (f == "consumer" || typeof fixupInfo[f] == "function")
+ continue;
+
+ if (fixupInfo[f] && fixupInfo[f] instanceof Ci.nsIURI) {
+ data[f] = fixupInfo[f].spec;
+ } else {
+ data[f] = fixupInfo[f];
+ }
+ }
+
+ sendAsyncMessage("Browser:URIFixup", data);
+}
+Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup", false);
+addEventListener("unload", () => {
+ Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup");
+}, false);
+
+addMessageListener("Browser:AppTab", function(message) {
+ if (docShell) {
+ docShell.isAppTab = message.data.isAppTab;
+ }
+});
+
+var WebBrowserChrome = {
+ onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
+ return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab);
+ },
+
+ // Check whether this URI should load in the current process
+ shouldLoadURI: function(aDocShell, aURI, aReferrer) {
+ if (!E10SUtils.shouldLoadURI(aDocShell, aURI, aReferrer)) {
+ E10SUtils.redirectLoad(aDocShell, aURI, aReferrer);
+ return false;
+ }
+
+ return true;
+ },
+
+ // Try to reload the currently active or currently loading page in a new process.
+ reloadInFreshProcess: function(aDocShell, aURI, aReferrer) {
+ E10SUtils.redirectLoad(aDocShell, aURI, aReferrer, true);
+ return true;
+ }
+};
+
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsITabChild);
+ tabchild.webBrowserChrome = WebBrowserChrome;
+}
+
+
+var DOMFullscreenHandler = {
+
+ init: function() {
+ addMessageListener("DOMFullscreen:Entered", this);
+ addMessageListener("DOMFullscreen:CleanUp", this);
+ addEventListener("MozDOMFullscreen:Request", this);
+ addEventListener("MozDOMFullscreen:Entered", this);
+ addEventListener("MozDOMFullscreen:NewOrigin", this);
+ addEventListener("MozDOMFullscreen:Exit", this);
+ addEventListener("MozDOMFullscreen:Exited", this);
+ },
+
+ get _windowUtils() {
+ if (!content) {
+ return null;
+ }
+ return content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ },
+
+ receiveMessage: function(aMessage) {
+ let windowUtils = this._windowUtils;
+ switch (aMessage.name) {
+ case "DOMFullscreen:Entered": {
+ this._lastTransactionId = windowUtils.lastTransactionId;
+ if (!windowUtils.handleFullscreenRequests() &&
+ !content.document.fullscreenElement) {
+ // If we don't actually have any pending fullscreen request
+ // to handle, neither we have been in fullscreen, tell the
+ // parent to just exit.
+ sendAsyncMessage("DOMFullscreen:Exit");
+ }
+ break;
+ }
+ case "DOMFullscreen:CleanUp": {
+ // If we've exited fullscreen at this point, no need to record
+ // transaction id or call exit fullscreen. This is especially
+ // important for non-e10s, since in that case, it is possible
+ // that no more paint would be triggered after this point.
+ if (content.document.fullscreenElement && windowUtils) {
+ this._lastTransactionId = windowUtils.lastTransactionId;
+ windowUtils.exitFullscreen();
+ }
+ break;
+ }
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "MozDOMFullscreen:Request": {
+ sendAsyncMessage("DOMFullscreen:Request");
+ break;
+ }
+ case "MozDOMFullscreen:NewOrigin": {
+ sendAsyncMessage("DOMFullscreen:NewOrigin", {
+ originNoSuffix: aEvent.target.nodePrincipal.originNoSuffix,
+ });
+ break;
+ }
+ case "MozDOMFullscreen:Exit": {
+ sendAsyncMessage("DOMFullscreen:Exit");
+ break;
+ }
+ case "MozDOMFullscreen:Entered":
+ case "MozDOMFullscreen:Exited": {
+ addEventListener("MozAfterPaint", this);
+ if (!content || !content.document.fullscreenElement) {
+ // If we receive any fullscreen change event, and find we are
+ // actually not in fullscreen, also ask the parent to exit to
+ // ensure that the parent always exits fullscreen when we do.
+ sendAsyncMessage("DOMFullscreen:Exit");
+ }
+ break;
+ }
+ case "MozAfterPaint": {
+ // Only send Painted signal after we actually finish painting
+ // the transition for the fullscreen change.
+ // Note that this._lastTransactionId is not set when in non-e10s
+ // mode, so we need to check that explicitly.
+ if (!this._lastTransactionId ||
+ aEvent.transactionId > this._lastTransactionId) {
+ removeEventListener("MozAfterPaint", this);
+ sendAsyncMessage("DOMFullscreen:Painted");
+ }
+ break;
+ }
+ }
+ }
+};
+DOMFullscreenHandler.init();
+
+var RefreshBlocker = {
+ PREF: "accessibility.blockautorefresh",
+
+ // Bug 1247100 - When a refresh is caused by an HTTP header,
+ // onRefreshAttempted will be fired before onLocationChange.
+ // When a refresh is caused by a <meta> tag in the document,
+ // onRefreshAttempted will be fired after onLocationChange.
+ //
+ // We only ever want to send a message to the parent after
+ // onLocationChange has fired, since the parent uses the
+ // onLocationChange update to clear transient notifications.
+ // Sending the message before onLocationChange will result in
+ // us creating the notification, and then clearing it very
+ // soon after.
+ //
+ // To account for both cases (onRefreshAttempted before
+ // onLocationChange, and onRefreshAttempted after onLocationChange),
+ // we'll hold a mapping of DOM Windows that we see get
+ // sent through both onLocationChange and onRefreshAttempted.
+ // When either run, they'll check the WeakMap for the existence
+ // of the DOM Window. If it doesn't exist, it'll add it. If
+ // it finds it, it'll know that it's safe to send the message
+ // to the parent, since we know that both have fired.
+ //
+ // The DOM Window is removed from blockedWindows when we notice
+ // the nsIWebProgress change state to STATE_STOP for the
+ // STATE_IS_WINDOW case.
+ //
+ // DOM Windows are mapped to a JS object that contains the data
+ // to be sent to the parent to show the notification. Since that
+ // data is only known when onRefreshAttempted is fired, it's only
+ // ever stashed in the map if onRefreshAttempted fires first -
+ // otherwise, null is set as the value of the mapping.
+ blockedWindows: new WeakMap(),
+
+ init() {
+ if (Services.prefs.getBoolPref(this.PREF)) {
+ this.enable();
+ }
+
+ Services.prefs.addObserver(this.PREF, this, false);
+ },
+
+ uninit() {
+ if (Services.prefs.getBoolPref(this.PREF)) {
+ this.disable();
+ }
+
+ Services.prefs.removeObserver(this.PREF, this);
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed" && data == this.PREF) {
+ if (Services.prefs.getBoolPref(this.PREF)) {
+ this.enable();
+ } else {
+ this.disable();
+ }
+ }
+ },
+
+ enable() {
+ this._filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
+ .createInstance(Ci.nsIWebProgress);
+ this._filter.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this._filter, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ addMessageListener("RefreshBlocker:Refresh", this);
+ },
+
+ disable() {
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.removeProgressListener(this._filter);
+
+ this._filter.removeProgressListener(this);
+ this._filter = null;
+
+ removeMessageListener("RefreshBlocker:Refresh", this);
+ },
+
+ send(data) {
+ sendAsyncMessage("RefreshBlocker:Blocked", data);
+ },
+
+ /**
+ * Notices when the nsIWebProgress transitions to STATE_STOP for
+ * the STATE_IS_WINDOW case, which will clear any mappings from
+ * blockedWindows.
+ */
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ this.blockedWindows.delete(aWebProgress.DOMWindow);
+ }
+ },
+
+ /**
+ * Notices when the location has changed. If, when running,
+ * onRefreshAttempted has already fired for this DOM Window, will
+ * send the appropriate refresh blocked data to the parent.
+ */
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ let win = aWebProgress.DOMWindow;
+ if (this.blockedWindows.has(win)) {
+ let data = this.blockedWindows.get(win);
+ if (data) {
+ // We saw onRefreshAttempted before onLocationChange, so
+ // send the message to the parent to show the notification.
+ this.send(data);
+ }
+ } else {
+ this.blockedWindows.set(win, null);
+ }
+ },
+
+ /**
+ * Notices when a refresh / reload was attempted. If, when running,
+ * onLocationChange has not yet run, will stash the appropriate data
+ * into the blockedWindows map to be sent when onLocationChange fires.
+ */
+ onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
+ let win = aWebProgress.DOMWindow;
+ let outerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+
+ let data = {
+ URI: aURI.spec,
+ originCharset: aURI.originCharset,
+ delay: aDelay,
+ sameURI: aSameURI,
+ outerWindowID,
+ };
+
+ if (this.blockedWindows.has(win)) {
+ // onLocationChange must have fired before, so we can tell the
+ // parent to show the notification.
+ this.send(data);
+ } else {
+ // onLocationChange hasn't fired yet, so stash the data in the
+ // map so that onLocationChange can send it when it fires.
+ this.blockedWindows.set(win, data);
+ }
+
+ return false;
+ },
+
+ receiveMessage(message) {
+ let data = message.data;
+
+ if (message.name == "RefreshBlocker:Refresh") {
+ let win = Services.wm.getOuterWindowWithId(data.outerWindowID);
+ let refreshURI = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIRefreshURI);
+
+ let URI = BrowserUtils.makeURI(data.URI, data.originCharset, null);
+
+ refreshURI.forceRefreshURI(URI, data.delay, true);
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener2,
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+};
+
+RefreshBlocker.init();
+
+var UserContextIdNotifier = {
+ init() {
+ addEventListener("DOMWindowCreated", this);
+ },
+
+ uninit() {
+ removeEventListener("DOMWindowCreated", this);
+ },
+
+ handleEvent(aEvent) {
+ // When the window is created, we want to inform the tabbrowser about
+ // the userContextId in use in order to update the UI correctly.
+ // Just because we cannot change the userContextId from an active docShell,
+ // we don't need to check DOMContentLoaded again.
+ this.uninit();
+
+ // We use the docShell because content.document can have been loaded before
+ // setting the originAttributes.
+ let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
+ let userContextId = loadContext.originAttributes.userContextId;
+
+ sendAsyncMessage("Browser:WindowCreated", { userContextId });
+ }
+};
+
+UserContextIdNotifier.init();
+
+ExtensionContent.init(this);
+addEventListener("unload", () => {
+ ExtensionContent.uninit(this);
+ RefreshBlocker.uninit();
+});
+
+addMessageListener("AllowScriptsToClose", () => {
+ content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .allowScriptsToClose();
+});
+
+addEventListener("MozAfterPaint", function onFirstPaint() {
+ removeEventListener("MozAfterPaint", onFirstPaint);
+ sendAsyncMessage("Browser:FirstPaint");
+});
diff --git a/browser/base/content/tab-shape.inc.svg b/browser/base/content/tab-shape.inc.svg
new file mode 100644
index 000000000..f97889389
--- /dev/null
+++ b/browser/base/content/tab-shape.inc.svg
@@ -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/. -->
+
+<svg:clipPath id="tab-curve-clip-path-start" clipPathUnits="objectBoundingBox">
+ <svg:path d="m 1,0.0625 0.05,0 0,0.938 -1,0 0,-0.028 C 0.32082458,0.95840561 0.4353096,0.81970962 0.48499998,0.5625 0.51819998,0.3905 0.535,0.0659 1,0.0625 z"/>
+</svg:clipPath>
+
+<svg:clipPath id="tab-curve-clip-path-end" clipPathUnits="objectBoundingBox">
+ <svg:path d="m 0,0.0625 -0.05,0 0,0.938 1,0 0,-0.028 C 0.67917542,0.95840561 0.56569036,0.81970962 0.51599998,0.5625 0.48279998,0.3905 0.465,0.0659 0,0.0625 z"/>
+</svg:clipPath>
diff --git a/browser/base/content/tabbrowser.css b/browser/base/content/tabbrowser.css
new file mode 100644
index 000000000..9085304f6
--- /dev/null
+++ b/browser/base/content/tabbrowser.css
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.tabbrowser-tabbox {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabbox");
+}
+
+.tabbrowser-tabpanels {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabpanels");
+}
+
+.tabbrowser-arrowscrollbox {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-arrowscrollbox");
+}
+
+.tab-close-button {
+ -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-close-tab-button");
+}
+
+.tab-close-button[pinned],
+.tabbrowser-tabs[closebuttons="activetab"] > * > * > * > .tab-close-button:not([selected="true"]),
+.tab-icon-image:not([src]):not([pinned]):not([crashed])[selected],
+.tab-icon-image:not([src]):not([pinned]):not([crashed]):not([sharing]),
+.tab-icon-image[busy],
+.tab-throbber:not([busy]),
+.tab-icon-sound:not([soundplaying]):not([muted]):not([blocked]),
+.tab-icon-sound[pinned],
+.tab-sharing-icon-overlay,
+.tab-icon-overlay {
+ display: none;
+}
+
+.tab-sharing-icon-overlay[sharing]:not([selected]),
+.tab-icon-overlay[soundplaying][pinned],
+.tab-icon-overlay[muted][pinned],
+.tab-icon-overlay[blocked][pinned],
+.tab-icon-overlay[crashed] {
+ display: -moz-box;
+}
+
+.tab-label[pinned] {
+ width: 0;
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+}
+
+.tab-stack {
+ vertical-align: top; /* for pinned tabs */
+}
+
+tabpanels {
+ background-color: transparent;
+}
+
+.tab-drop-indicator {
+ position: relative;
+ z-index: 2;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ .tab-icon-image {
+ image-rendering: -moz-crisp-edges;
+ }
+}
+
+.closing-tabs-spacer {
+ pointer-events: none;
+}
+
+.tabbrowser-tabs:not(:hover) > .tabbrowser-arrowscrollbox > .closing-tabs-spacer {
+ transition: width .15s ease-out;
+}
+
+/**
+ * Optimization for tabs that are restored lazily. We can save a good amount of
+ * memory that to-be-restored tabs would otherwise consume simply by setting
+ * their browsers to 'display: none' as that will prevent them from having to
+ * create a presentation and the like.
+ */
+browser[pending] {
+ display: none;
+}
+
+browser[pendingpaint] {
+ opacity: 0;
+}
+
+tabbrowser[pendingpaint] {
+ background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-color: #f9f9f9 !important;
+ background-size: 30px;
+}
diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml
new file mode 100644
index 000000000..3f4c3518e
--- /dev/null
+++ b/browser/base/content/tabbrowser.xml
@@ -0,0 +1,7417 @@
+<?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="tabBrowserBindings"
+ 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="tabbrowser">
+ <resources>
+ <stylesheet src="chrome://browser/content/tabbrowser.css"/>
+ </resources>
+
+ <content>
+ <xul:stringbundle anonid="tbstringbundle" src="chrome://browser/locale/tabbrowser.properties"/>
+ <xul:tabbox anonid="tabbox" class="tabbrowser-tabbox"
+ flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown"
+ onselect="if (event.target.localName == 'tabpanels') this.parentNode.updateCurrentBrowser();">
+ <xul:tabpanels flex="1" class="plain" selectedIndex="0" anonid="panelcontainer">
+ <xul:notificationbox flex="1" notificationside="top">
+ <xul:hbox flex="1" class="browserSidebarContainer">
+ <xul:vbox flex="1" class="browserContainer">
+ <xul:stack flex="1" class="browserStack" anonid="browserStack">
+ <xul:browser anonid="initialBrowser" type="content-primary" message="true" messagemanagergroup="browsers"
+ xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,selectmenulist,datetimepicker"/>
+ </xul:stack>
+ </xul:vbox>
+ </xul:hbox>
+ </xul:notificationbox>
+ </xul:tabpanels>
+ </xul:tabbox>
+ <children/>
+ </content>
+ <implementation implements="nsIDOMEventListener, nsIMessageListener, nsIObserver">
+
+ <property name="tabContextMenu" readonly="true"
+ onget="return this.tabContainer.contextMenu;"/>
+
+ <field name="tabContainer" readonly="true">
+ document.getElementById(this.getAttribute("tabcontainer"));
+ </field>
+ <field name="tabs" readonly="true">
+ this.tabContainer.childNodes;
+ </field>
+
+ <property name="visibleTabs" readonly="true">
+ <getter><![CDATA[
+ if (!this._visibleTabs)
+ this._visibleTabs = Array.filter(this.tabs,
+ tab => !tab.hidden && !tab.closing);
+ return this._visibleTabs;
+ ]]></getter>
+ </property>
+
+ <field name="closingTabsEnum" readonly="true">({ ALL: 0, OTHER: 1, TO_END: 2 });</field>
+
+ <field name="_visibleTabs">null</field>
+
+ <field name="mURIFixup" readonly="true">
+ Components.classes["@mozilla.org/docshell/urifixup;1"]
+ .getService(Components.interfaces.nsIURIFixup);
+ </field>
+ <field name="_unifiedComplete" readonly="true">
+ Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
+ .getService(Components.interfaces.mozIPlacesAutoComplete);
+ </field>
+ <field name="AppConstants" readonly="true">
+ (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
+ </field>
+ <field name="mTabBox" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "tabbox");
+ </field>
+ <field name="mPanelContainer" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer");
+ </field>
+ <field name="mStringBundle">
+ document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle");
+ </field>
+ <field name="mCurrentTab">
+ null
+ </field>
+ <field name="_lastRelatedTab">
+ null
+ </field>
+ <field name="mCurrentBrowser">
+ null
+ </field>
+ <field name="mProgressListeners">
+ []
+ </field>
+ <field name="mActiveResizeDisplayportSuppression">
+ null
+ </field>
+ <field name="mTabsProgressListeners">
+ []
+ </field>
+ <field name="_tabListeners">
+ new Map()
+ </field>
+ <field name="_tabFilters">
+ new Map()
+ </field>
+ <field name="mIsBusy">
+ false
+ </field>
+ <field name="_outerWindowIDBrowserMap">
+ new Map();
+ </field>
+ <field name="arrowKeysShouldWrap" readonly="true">
+ this.AppConstants.platform == "macosx";
+ </field>
+
+ <field name="_autoScrollPopup">
+ null
+ </field>
+
+ <field name="_previewMode">
+ false
+ </field>
+
+ <field name="_lastFindValue">
+ ""
+ </field>
+
+ <field name="_contentWaitingCount">
+ 0
+ </field>
+
+ <property name="_numPinnedTabs" readonly="true">
+ <getter><![CDATA[
+ for (var i = 0; i < this.tabs.length; i++) {
+ if (!this.tabs[i].pinned)
+ break;
+ }
+ return i;
+ ]]></getter>
+ </property>
+
+ <property name="popupAnchor" readonly="true">
+ <getter><![CDATA[
+ if (this.mCurrentTab._popupAnchor) {
+ return this.mCurrentTab._popupAnchor;
+ }
+ let stack = this.mCurrentBrowser.parentNode;
+ // Create an anchor for the popup
+ const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let popupAnchor = document.createElementNS(NS_XUL, "hbox");
+ popupAnchor.className = "popup-anchor";
+ popupAnchor.hidden = true;
+ stack.appendChild(popupAnchor);
+ return this.mCurrentTab._popupAnchor = popupAnchor;
+ ]]></getter>
+ </property>
+
+ <method name="isFindBarInitialized">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ return (aTab || this.selectedTab)._findBar != undefined;
+ ]]></body>
+ </method>
+
+ <method name="getFindBar">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (!aTab)
+ aTab = this.selectedTab;
+
+ if (aTab._findBar)
+ return aTab._findBar;
+
+ let findBar = document.createElementNS(this.namespaceURI, "findbar");
+ let browser = this.getBrowserForTab(aTab);
+ let browserContainer = this.getBrowserContainer(browser);
+ browserContainer.appendChild(findBar);
+
+ // Force a style flush to ensure that our binding is attached.
+ findBar.clientTop;
+
+ findBar.browser = browser;
+ findBar._findField.value = this._lastFindValue;
+
+ aTab._findBar = findBar;
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabFindInitialized", true, false);
+ aTab.dispatchEvent(event);
+
+ return findBar;
+ ]]></body>
+ </method>
+
+ <method name="getStatusPanel">
+ <body><![CDATA[
+ if (!this._statusPanel) {
+ this._statusPanel = document.createElementNS(this.namespaceURI, "statuspanel");
+ this._statusPanel.setAttribute("inactive", "true");
+ this._statusPanel.setAttribute("layer", "true");
+ this._appendStatusPanel();
+ }
+ return this._statusPanel;
+ ]]></body>
+ </method>
+
+ <method name="_appendStatusPanel">
+ <body><![CDATA[
+ if (this._statusPanel) {
+ let browser = this.selectedBrowser;
+ let browserContainer = this.getBrowserContainer(browser);
+ browserContainer.insertBefore(this._statusPanel, browser.parentNode.nextSibling);
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateWindowResizers">
+ <body><![CDATA[
+ if (!window.gShowPageResizers)
+ return;
+
+ var show = window.windowState == window.STATE_NORMAL;
+ for (let i = 0; i < this.browsers.length; i++) {
+ this.browsers[i].showWindowResizer = show;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_setCloseKeyState">
+ <parameter name="aEnabled"/>
+ <body><![CDATA[
+ let keyClose = document.getElementById("key_close");
+ let closeKeyEnabled = keyClose.getAttribute("disabled") != "true";
+ if (closeKeyEnabled == aEnabled)
+ return;
+
+ if (aEnabled)
+ keyClose.removeAttribute("disabled");
+ else
+ keyClose.setAttribute("disabled", "true");
+
+ // We also want to remove the keyboard shortcut from the file menu
+ // when the shortcut is disabled, and bring it back when it's
+ // renabled.
+ //
+ // Fixing bug 630826 could make that happen automatically.
+ // Fixing bug 630830 could avoid the ugly hack below.
+
+ let closeMenuItem = document.getElementById("menu_close");
+ let parentPopup = closeMenuItem.parentNode;
+ let nextItem = closeMenuItem.nextSibling;
+ let clonedItem = closeMenuItem.cloneNode(true);
+
+ parentPopup.removeChild(closeMenuItem);
+
+ if (aEnabled)
+ clonedItem.setAttribute("key", "key_close");
+ else
+ clonedItem.removeAttribute("key");
+
+ parentPopup.insertBefore(clonedItem, nextItem);
+ ]]></body>
+ </method>
+
+ <method name="pinTab">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (aTab.pinned)
+ return;
+
+ if (aTab.hidden)
+ this.showTab(aTab);
+
+ this.moveTabTo(aTab, this._numPinnedTabs);
+ aTab.setAttribute("pinned", "true");
+ this.tabContainer._unlockTabSizing();
+ this.tabContainer._positionPinnedTabs();
+ this.tabContainer.adjustTabstrip();
+
+ this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: true })
+
+ if (aTab.selected)
+ this._setCloseKeyState(false);
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabPinned", true, false);
+ aTab.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="unpinTab">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (!aTab.pinned)
+ return;
+
+ this.moveTabTo(aTab, this._numPinnedTabs - 1);
+ aTab.removeAttribute("pinned");
+ aTab.style.marginInlineStart = "";
+ this.tabContainer._unlockTabSizing();
+ this.tabContainer._positionPinnedTabs();
+ this.tabContainer.adjustTabstrip();
+
+ this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: false })
+
+ if (aTab.selected)
+ this._setCloseKeyState(true);
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabUnpinned", true, false);
+ aTab.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="previewTab">
+ <parameter name="aTab"/>
+ <parameter name="aCallback"/>
+ <body>
+ <![CDATA[
+ let currentTab = this.selectedTab;
+ try {
+ // Suppress focus, ownership and selected tab changes
+ this._previewMode = true;
+ this.selectedTab = aTab;
+ aCallback();
+ } finally {
+ this.selectedTab = currentTab;
+ this._previewMode = false;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserAtIndex">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ return this.browsers[aIndex];
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserIndexForDocument">
+ <parameter name="aDocument"/>
+ <body>
+ <![CDATA[
+ var tab = this._getTabForContentWindow(aDocument.defaultView);
+ return tab ? tab._tPos : -1;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserForDocument">
+ <parameter name="aDocument"/>
+ <body>
+ <![CDATA[
+ var tab = this._getTabForContentWindow(aDocument.defaultView);
+ return tab ? tab.linkedBrowser : null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserForContentWindow">
+ <parameter name="aWindow"/>
+ <body>
+ <![CDATA[
+ var tab = this._getTabForContentWindow(aWindow);
+ return tab ? tab.linkedBrowser : null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserForOuterWindowID">
+ <parameter name="aID"/>
+ <body>
+ <![CDATA[
+ return this._outerWindowIDBrowserMap.get(aID);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getTabForContentWindow">
+ <parameter name="aWindow"/>
+ <body>
+ <![CDATA[
+ // When not using remote browsers, we can take a fast path by getting
+ // directly from the content window to the browser without looping
+ // over all browsers.
+ if (!gMultiProcessBrowser) {
+ let browser = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ return this.getTabForBrowser(browser);
+ }
+
+ for (let i = 0; i < this.browsers.length; i++) {
+ // NB: We use contentWindowAsCPOW so that this code works both
+ // for remote browsers as well. aWindow may be a CPOW.
+ if (this.browsers[i].contentWindowAsCPOW == aWindow)
+ return this.tabs[i];
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <!-- Binding from browser to tab -->
+ <field name="_tabForBrowser" readonly="true">
+ <![CDATA[
+ new WeakMap();
+ ]]>
+ </field>
+
+ <method name="_getTabForBrowser">
+ <parameter name="aBrowser" />
+ <body>
+ <![CDATA[
+ let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+ let text = "_getTabForBrowser` is now deprecated, please use `getTabForBrowser";
+ let url = "https://developer.mozilla.org/docs/Mozilla/Tech/XUL/Method/getTabForBrowser";
+ Deprecated.warning(text, url);
+ return this.getTabForBrowser(aBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getTabForBrowser">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ return this._tabForBrowser.get(aBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getNotificationBox">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ return this.getSidebarContainer(aBrowser).parentNode;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getSidebarContainer">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ return this.getBrowserContainer(aBrowser).parentNode;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserContainer">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ return (aBrowser || this.mCurrentBrowser).parentNode.parentNode;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getTabModalPromptBox">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ let browser = (aBrowser || this.mCurrentBrowser);
+ if (!browser.tabModalPromptBox) {
+ browser.tabModalPromptBox = new TabModalPromptBox(browser);
+ }
+ return browser.tabModalPromptBox;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getTabFromAudioEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
+ !aEvent.isTrusted) {
+ return null;
+ }
+
+ var browser = aEvent.originalTarget;
+ var tab = this.getTabForBrowser(browser);
+ return tab;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_callProgressListeners">
+ <parameter name="aBrowser"/>
+ <parameter name="aMethod"/>
+ <parameter name="aArguments"/>
+ <parameter name="aCallGlobalListeners"/>
+ <parameter name="aCallTabsListeners"/>
+ <body><![CDATA[
+ var rv = true;
+
+ function callListeners(listeners, args) {
+ for (let p of listeners) {
+ if (aMethod in p) {
+ try {
+ if (!p[aMethod].apply(p, args))
+ rv = false;
+ } catch (e) {
+ // don't inhibit other listeners
+ Components.utils.reportError(e);
+ }
+ }
+ }
+ }
+
+ if (!aBrowser)
+ aBrowser = this.mCurrentBrowser;
+
+ if (aCallGlobalListeners != false &&
+ aBrowser == this.mCurrentBrowser) {
+ callListeners(this.mProgressListeners, aArguments);
+ }
+
+ if (aCallTabsListeners != false) {
+ aArguments.unshift(aBrowser);
+
+ callListeners(this.mTabsProgressListeners, aArguments);
+ }
+
+ return rv;
+ ]]></body>
+ </method>
+
+ <!-- A web progress listener object definition for a given tab. -->
+ <method name="mTabProgressListener">
+ <parameter name="aTab"/>
+ <parameter name="aBrowser"/>
+ <parameter name="aStartsBlank"/>
+ <parameter name="aWasPreloadedBrowser"/>
+ <parameter name="aOrigStateFlags"/>
+ <body>
+ <![CDATA[
+ let stateFlags = aOrigStateFlags || 0;
+ // Initialize mStateFlags to non-zero e.g. when creating a progress
+ // listener for preloaded browsers as there was no progress listener
+ // around when the content started loading. If the content didn't
+ // quite finish loading yet, mStateFlags will very soon be overridden
+ // with the correct value and end up at STATE_STOP again.
+ if (aWasPreloadedBrowser) {
+ stateFlags = Ci.nsIWebProgressListener.STATE_STOP |
+ Ci.nsIWebProgressListener.STATE_IS_REQUEST;
+ }
+
+ return ({
+ mTabBrowser: this,
+ mTab: aTab,
+ mBrowser: aBrowser,
+ mBlank: aStartsBlank,
+
+ // cache flags for correct status UI update after tab switching
+ mStateFlags: stateFlags,
+ mStatus: 0,
+ mMessage: "",
+ mTotalProgress: 0,
+
+ // count of open requests (should always be 0 or 1)
+ mRequestCount: 0,
+
+ destroy: function () {
+ delete this.mTab;
+ delete this.mBrowser;
+ delete this.mTabBrowser;
+ },
+
+ _callProgressListeners: function () {
+ Array.unshift(arguments, this.mBrowser);
+ return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments);
+ },
+
+ _shouldShowProgress: function (aRequest) {
+ if (this.mBlank)
+ return false;
+
+ // Don't show progress indicators in tabs for about: URIs
+ // pointing to local resources.
+ if ((aRequest instanceof Ci.nsIChannel) &&
+ aRequest.originalURI.schemeIs("about") &&
+ (aRequest.URI.schemeIs("jar") || aRequest.URI.schemeIs("file")))
+ return false;
+
+ return true;
+ },
+
+ _isForInitialAboutBlank: function (aWebProgress, aLocation) {
+ if (!this.mBlank || !aWebProgress.isTopLevel) {
+ return false;
+ }
+
+ let location = aLocation ? aLocation.spec : "";
+ return location == "about:blank";
+ },
+
+ onProgressChange: function (aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0;
+
+ if (!this._shouldShowProgress(aRequest))
+ return;
+
+ if (this.mTotalProgress)
+ this.mTab.setAttribute("progress", "true");
+
+ this._callProgressListeners("onProgressChange",
+ [aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress]);
+ },
+
+ onProgressChange64: function (aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ return this.onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress);
+ },
+
+ onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (!aRequest)
+ return;
+
+ const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ const nsIChannel = Components.interfaces.nsIChannel;
+ let location, originalLocation;
+ try {
+ aRequest.QueryInterface(nsIChannel)
+ location = aRequest.URI;
+ originalLocation = aRequest.originalURI;
+ } catch (ex) {}
+
+ let ignoreBlank = this._isForInitialAboutBlank(aWebProgress, location);
+ // If we were ignoring some messages about the initial about:blank, and we
+ // got the STATE_STOP for it, we'll want to pay attention to those messages
+ // from here forward. Similarly, if we conclude that this state change
+ // is one that we shouldn't be ignoring, then stop ignoring.
+ if ((ignoreBlank &&
+ aStateFlags & nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) ||
+ !ignoreBlank && this.mBlank) {
+ this.mBlank = false;
+ }
+
+ if (aStateFlags & nsIWebProgressListener.STATE_START) {
+ this.mRequestCount++;
+ }
+ else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
+ const NS_ERROR_UNKNOWN_HOST = 2152398878;
+ if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
+ // to prevent bug 235825: wait for the request handled
+ // by the automatic keyword resolver
+ return;
+ }
+ // since we (try to) only handle STATE_STOP of the last request,
+ // the count of open requests should now be 0
+ this.mRequestCount = 0;
+ }
+
+ if (aStateFlags & nsIWebProgressListener.STATE_START &&
+ aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
+ if (aWebProgress.isTopLevel) {
+ // Need to use originalLocation rather than location because things
+ // like about:home and about:privatebrowsing arrive with nsIRequest
+ // pointing to their resolved jar: or file: URIs.
+ if (!(originalLocation && gInitialPages.includes(originalLocation.spec) &&
+ originalLocation != "about:blank" &&
+ this.mBrowser.initialPageLoadedFromURLBar != originalLocation.spec &&
+ this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) {
+ // Indicating that we started a load will allow the location
+ // bar to be cleared when the load finishes.
+ // In order to not overwrite user-typed content, we avoid it
+ // (see if condition above) in a very specific case:
+ // If the load is of an 'initial' page (e.g. about:privatebrowsing,
+ // about:newtab, etc.), was not explicitly typed in the location
+ // bar by the user, is not about:blank (because about:blank can be
+ // loaded by websites under their principal), and the current
+ // page in the browser is about:blank (indicating it is a newly
+ // created or re-created browser, e.g. because it just switched
+ // remoteness or is a new tab/window).
+ this.mBrowser.urlbarChangeTracker.startedLoad();
+ }
+ delete this.mBrowser.initialPageLoadedFromURLBar;
+ // If the browser is loading it must not be crashed anymore
+ this.mTab.removeAttribute("crashed");
+ }
+
+ if (this._shouldShowProgress(aRequest)) {
+ if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) {
+ this.mTab.setAttribute("busy", "true");
+
+ if (aWebProgress.isTopLevel &&
+ !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD))
+ this.mTabBrowser.setTabTitleLoading(this.mTab);
+ }
+
+ if (this.mTab.selected)
+ this.mTabBrowser.mIsBusy = true;
+ }
+ }
+ else if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
+
+ if (this.mTab.hasAttribute("busy")) {
+ this.mTab.removeAttribute("busy");
+ this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]);
+ if (!this.mTab.selected)
+ this.mTab.setAttribute("unread", "true");
+ }
+ this.mTab.removeAttribute("progress");
+
+ if (aWebProgress.isTopLevel) {
+ let isSuccessful = Components.isSuccessCode(aStatus);
+ if (!isSuccessful && !isTabEmpty(this.mTab)) {
+ // Restore the current document's location in case the
+ // request was stopped (possibly from a content script)
+ // before the location changed.
+
+ this.mBrowser.userTypedValue = null;
+
+ let inLoadURI = this.mBrowser.inLoadURI;
+ if (this.mTab.selected && gURLBar && !inLoadURI) {
+ URLBarSetURI();
+ }
+ } else if (isSuccessful) {
+ this.mBrowser.urlbarChangeTracker.finishedLoad();
+ }
+
+ if (!this.mBrowser.mIconURL)
+ this.mTabBrowser.useDefaultIcon(this.mTab);
+ }
+
+ // For keyword URIs clear the user typed value since they will be changed into real URIs
+ if (location.scheme == "keyword")
+ this.mBrowser.userTypedValue = null;
+
+ if (this.mTab.label == this.mTabBrowser.mStringBundle.getString("tabs.connecting"))
+ this.mTabBrowser.setTabTitle(this.mTab);
+
+ if (this.mTab.selected)
+ this.mTabBrowser.mIsBusy = false;
+ }
+
+ if (ignoreBlank) {
+ this._callProgressListeners("onUpdateCurrentBrowser",
+ [aStateFlags, aStatus, "", 0],
+ true, false);
+ } else {
+ this._callProgressListeners("onStateChange",
+ [aWebProgress, aRequest, aStateFlags, aStatus],
+ true, false);
+ }
+
+ this._callProgressListeners("onStateChange",
+ [aWebProgress, aRequest, aStateFlags, aStatus],
+ false);
+
+ if (aStateFlags & (nsIWebProgressListener.STATE_START |
+ nsIWebProgressListener.STATE_STOP)) {
+ // reset cached temporary values at beginning and end
+ this.mMessage = "";
+ this.mTotalProgress = 0;
+ }
+ this.mStateFlags = aStateFlags;
+ this.mStatus = aStatus;
+ },
+
+ onLocationChange: function (aWebProgress, aRequest, aLocation,
+ aFlags) {
+ // OnLocationChange is called for both the top-level content
+ // and the subframes.
+ let topLevel = aWebProgress.isTopLevel;
+
+ if (topLevel) {
+ let isSameDocument =
+ !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
+ // We need to clear the typed value
+ // if the document failed to load, to make sure the urlbar reflects the
+ // failed URI (particularly for SSL errors). However, don't clear the value
+ // if the error page's URI is about:blank, because that causes complete
+ // loss of urlbar contents for invalid URI errors (see bug 867957).
+ // Another reason to clear the userTypedValue is if this was an anchor
+ // navigation initiated by the user.
+ if (this.mBrowser.didStartLoadSinceLastUserTyping() ||
+ ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
+ aLocation.spec != "about:blank") ||
+ (isSameDocument && this.mBrowser.inLoadURI)) {
+ this.mBrowser.userTypedValue = null;
+ }
+
+ // If the browser was playing audio, we should remove the playing state.
+ if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) {
+ clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
+ this.mTab._soundPlayingAttrRemovalTimer = 0;
+ this.mTab.removeAttribute("soundplaying");
+ this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
+ }
+
+ // If the browser was previously muted, we should restore the muted state.
+ if (this.mTab.hasAttribute("muted")) {
+ this.mTab.linkedBrowser.mute();
+ }
+
+ if (this.mTabBrowser.isFindBarInitialized(this.mTab)) {
+ let findBar = this.mTabBrowser.getFindBar(this.mTab);
+
+ // Close the Find toolbar if we're in old-style TAF mode
+ if (findBar.findMode != findBar.FIND_NORMAL) {
+ findBar.close();
+ }
+ }
+
+ // Don't clear the favicon if this onLocationChange was
+ // triggered by a pushState or a replaceState (bug 550565) or
+ // a hash change (bug 408415).
+ if (aWebProgress.isLoadingDocument && !isSameDocument) {
+ this.mBrowser.mIconURL = null;
+ }
+
+ let unifiedComplete = this.mTabBrowser._unifiedComplete;
+ let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
+ if (this.mBrowser.registeredOpenURI) {
+ unifiedComplete.unregisterOpenPage(this.mBrowser.registeredOpenURI,
+ userContextId);
+ delete this.mBrowser.registeredOpenURI;
+ }
+ // Tabs in private windows aren't registered as "Open" so
+ // that they don't appear as switch-to-tab candidates.
+ if (!isBlankPageURL(aLocation.spec) &&
+ (!PrivateBrowsingUtils.isWindowPrivate(window) ||
+ PrivateBrowsingUtils.permanentPrivateBrowsing)) {
+ unifiedComplete.registerOpenPage(aLocation, userContextId);
+ this.mBrowser.registeredOpenURI = aLocation;
+ }
+ }
+
+ if (!this.mBlank) {
+ this._callProgressListeners("onLocationChange",
+ [aWebProgress, aRequest, aLocation,
+ aFlags]);
+ }
+
+ if (topLevel) {
+ this.mBrowser.lastURI = aLocation;
+ this.mBrowser.lastLocationChange = Date.now();
+ }
+ },
+
+ onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) {
+ if (this.mBlank)
+ return;
+
+ this._callProgressListeners("onStatusChange",
+ [aWebProgress, aRequest, aStatus, aMessage]);
+
+ this.mMessage = aMessage;
+ },
+
+ onSecurityChange: function (aWebProgress, aRequest, aState) {
+ this._callProgressListeners("onSecurityChange",
+ [aWebProgress, aRequest, aState]);
+ },
+
+ onRefreshAttempted: function (aWebProgress, aURI, aDelay, aSameURI) {
+ return this._callProgressListeners("onRefreshAttempted",
+ [aWebProgress, aURI, aDelay, aSameURI]);
+ },
+
+ QueryInterface: function (aIID) {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsIWebProgressListener2) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ }
+ });
+ ]]>
+ </body>
+ </method>
+
+ <field name="serializationHelper">
+ Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ </field>
+
+ <field name="mIconLoadingPrincipal">
+ null
+ </field>
+
+ <method name="setIcon">
+ <parameter name="aTab"/>
+ <parameter name="aURI"/>
+ <parameter name="aLoadingPrincipal"/>
+ <body>
+ <![CDATA[
+ let browser = this.getBrowserForTab(aTab);
+ browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let loadingPrincipal = aLoadingPrincipal
+ ? aLoadingPrincipal
+ : Services.scriptSecurityManager.getSystemPrincipal();
+
+ if (aURI) {
+ if (!(aURI instanceof Ci.nsIURI)) {
+ aURI = makeURI(aURI);
+ }
+ PlacesUIUtils.loadFavicon(browser, loadingPrincipal, aURI);
+ }
+
+ let sizedIconUrl = browser.mIconURL || "";
+ if (sizedIconUrl != aTab.getAttribute("image")) {
+ if (sizedIconUrl) {
+ aTab.setAttribute("image", sizedIconUrl);
+ if (!browser.mIconLoadingPrincipal ||
+ !browser.mIconLoadingPrincipal.equals(loadingPrincipal)) {
+ aTab.setAttribute("iconLoadingPrincipal",
+ this.serializationHelper.serializeToString(loadingPrincipal));
+ browser.mIconLoadingPrincipal = loadingPrincipal;
+ }
+ }
+ else {
+ aTab.removeAttribute("image");
+ aTab.removeAttribute("iconLoadingPrincipal");
+ delete browser.mIconLoadingPrincipal;
+ }
+ this._tabAttrModified(aTab, ["image"]);
+ }
+
+ this._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getIcon">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
+ return browser.mIconURL;
+ ]]>
+ </body>
+ </method>
+
+ <method name="shouldLoadFavIcon">
+ <parameter name="aURI"/>
+ <body>
+ <![CDATA[
+ return (aURI &&
+ Services.prefs.getBoolPref("browser.chrome.site_icons") &&
+ Services.prefs.getBoolPref("browser.chrome.favicons") &&
+ ("schemeIs" in aURI) && (aURI.schemeIs("http") || aURI.schemeIs("https")));
+ ]]>
+ </body>
+ </method>
+
+ <method name="useDefaultIcon">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ var browser = this.getBrowserForTab(aTab);
+ var documentURI = browser.documentURI;
+ var icon = null;
+
+ if (browser.imageDocument) {
+ if (Services.prefs.getBoolPref("browser.chrome.site_icons")) {
+ let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size");
+ if (browser.imageDocument.width <= sz &&
+ browser.imageDocument.height <= sz) {
+ icon = browser.currentURI;
+ }
+ }
+ }
+
+ // Use documentURIObject in the check for shouldLoadFavIcon so that we
+ // do the right thing with about:-style error pages. Bug 453442
+ if (!icon && this.shouldLoadFavIcon(documentURI)) {
+ let url = documentURI.prePath + "/favicon.ico";
+ if (!this.isFailedIcon(url))
+ icon = url;
+ }
+ this.setIcon(aTab, icon, browser.contentPrincipal);
+ ]]>
+ </body>
+ </method>
+
+ <method name="isFailedIcon">
+ <parameter name="aURI"/>
+ <body>
+ <![CDATA[
+ if (!(aURI instanceof Ci.nsIURI))
+ aURI = makeURI(aURI);
+ return PlacesUtils.favicons.isFailedFavicon(aURI);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getWindowTitleForBrowser">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ var newTitle = "";
+ var docElement = this.ownerDocument.documentElement;
+ var sep = docElement.getAttribute("titlemenuseparator");
+
+ // Strip out any null bytes in the content title, since the
+ // underlying widget implementations of nsWindow::SetTitle pass
+ // null-terminated strings to system APIs.
+ var docTitle = aBrowser.contentTitle.replace(/\0/g, "");
+
+ if (!docTitle)
+ docTitle = docElement.getAttribute("titledefault");
+
+ var modifier = docElement.getAttribute("titlemodifier");
+ if (docTitle) {
+ newTitle += docElement.getAttribute("titlepreface");
+ newTitle += docTitle;
+ if (modifier)
+ newTitle += sep;
+ }
+ newTitle += modifier;
+
+ // If location bar is hidden and the URL type supports a host,
+ // add the scheme and host to the title to prevent spoofing.
+ // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
+ try {
+ if (docElement.getAttribute("chromehidden").includes("location")) {
+ var uri = this.mURIFixup.createExposableURI(
+ aBrowser.currentURI);
+ if (uri.scheme == "about")
+ newTitle = uri.spec + sep + newTitle;
+ else
+ newTitle = uri.prePath + sep + newTitle;
+ }
+ } catch (e) {}
+
+ return newTitle;
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateTitlebar">
+ <body>
+ <![CDATA[
+ this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <!-- Holds a unique ID for the tab change that's currently being timed.
+ Used to make sure that multiple, rapid tab switches do not try to
+ create overlapping timers. -->
+ <field name="_tabSwitchID">null</field>
+
+ <method name="updateCurrentBrowser">
+ <parameter name="aForceUpdate"/>
+ <body>
+ <![CDATA[
+ var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
+ if (this.mCurrentBrowser == newBrowser && !aForceUpdate)
+ return;
+
+ if (!aForceUpdate) {
+ TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
+ if (!gMultiProcessBrowser) {
+ // old way of measuring tab paint which is not valid with e10s.
+ // Waiting until the next MozAfterPaint ensures that we capture
+ // the time it takes to paint, upload the textures to the compositor,
+ // and then composite.
+ if (this._tabSwitchID) {
+ TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_MS");
+ }
+
+ let tabSwitchID = Symbol();
+
+ TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS");
+ this._tabSwitchID = tabSwitchID;
+
+ let onMozAfterPaint = () => {
+ if (this._tabSwitchID === tabSwitchID) {
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS");
+ this._tabSwitchID = null;
+ }
+ window.removeEventListener("MozAfterPaint", onMozAfterPaint);
+ }
+ window.addEventListener("MozAfterPaint", onMozAfterPaint);
+ }
+ }
+
+ var oldTab = this.mCurrentTab;
+
+ // Preview mode should not reset the owner
+ if (!this._previewMode && !oldTab.selected)
+ oldTab.owner = null;
+
+ if (this._lastRelatedTab) {
+ if (!this._lastRelatedTab.selected)
+ this._lastRelatedTab.owner = null;
+ this._lastRelatedTab = null;
+ }
+
+ var oldBrowser = this.mCurrentBrowser;
+
+ if (!gMultiProcessBrowser) {
+ oldBrowser.setAttribute("type", "content-targetable");
+ oldBrowser.docShellIsActive = false;
+ newBrowser.setAttribute("type", "content-primary");
+ newBrowser.docShellIsActive =
+ (window.windowState != window.STATE_MINIMIZED);
+ }
+
+ var updateBlockedPopups = false;
+ if ((oldBrowser.blockedPopups && !newBrowser.blockedPopups) ||
+ (!oldBrowser.blockedPopups && newBrowser.blockedPopups))
+ updateBlockedPopups = true;
+
+ this.mCurrentBrowser = newBrowser;
+ this.mCurrentTab = this.tabContainer.selectedItem;
+ this.showTab(this.mCurrentTab);
+
+ var forwardButtonContainer = document.getElementById("urlbar-wrapper");
+ if (forwardButtonContainer) {
+ forwardButtonContainer.setAttribute("switchingtabs", "true");
+ window.addEventListener("MozAfterPaint", function removeSwitchingtabsAttr() {
+ window.removeEventListener("MozAfterPaint", removeSwitchingtabsAttr);
+ forwardButtonContainer.removeAttribute("switchingtabs");
+ });
+ }
+
+ this._appendStatusPanel();
+
+ if (updateBlockedPopups)
+ this.mCurrentBrowser.updateBlockedPopups();
+
+ // Update the URL bar.
+ var loc = this.mCurrentBrowser.currentURI;
+
+ var webProgress = this.mCurrentBrowser.webProgress;
+ var securityUI = this.mCurrentBrowser.securityUI;
+
+ this._callProgressListeners(null, "onLocationChange",
+ [webProgress, null, loc, 0], true,
+ false);
+
+ if (securityUI) {
+ // Include the true final argument to indicate that this event is
+ // simulated (instead of being observed by the webProgressListener).
+ this._callProgressListeners(null, "onSecurityChange",
+ [webProgress, null, securityUI.state, true],
+ true, false);
+ }
+
+ var listener = this._tabListeners.get(this.mCurrentTab);
+ if (listener && listener.mStateFlags) {
+ this._callProgressListeners(null, "onUpdateCurrentBrowser",
+ [listener.mStateFlags, listener.mStatus,
+ listener.mMessage, listener.mTotalProgress],
+ true, false);
+ }
+
+ if (!this._previewMode) {
+ this._recordTabAccess(this.mCurrentTab);
+
+ this.mCurrentTab.updateLastAccessed();
+ this.mCurrentTab.removeAttribute("unread");
+ oldTab.updateLastAccessed();
+
+ let oldFindBar = oldTab._findBar;
+ if (oldFindBar &&
+ oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
+ !oldFindBar.hidden)
+ this._lastFindValue = oldFindBar._findField.value;
+
+ this.updateTitlebar();
+
+ this.mCurrentTab.removeAttribute("titlechanged");
+ this.mCurrentTab.removeAttribute("attention");
+ }
+
+ // If the new tab is busy, and our current state is not busy, then
+ // we need to fire a start to all progress listeners.
+ const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ if (this.mCurrentTab.hasAttribute("busy") && !this.mIsBusy) {
+ this.mIsBusy = true;
+ this._callProgressListeners(null, "onStateChange",
+ [webProgress, null,
+ nsIWebProgressListener.STATE_START |
+ nsIWebProgressListener.STATE_IS_NETWORK, 0],
+ true, false);
+ }
+
+ // If the new tab is not busy, and our current state is busy, then
+ // we need to fire a stop to all progress listeners.
+ if (!this.mCurrentTab.hasAttribute("busy") && this.mIsBusy) {
+ this.mIsBusy = false;
+ this._callProgressListeners(null, "onStateChange",
+ [webProgress, null,
+ nsIWebProgressListener.STATE_STOP |
+ nsIWebProgressListener.STATE_IS_NETWORK, 0],
+ true, false);
+ }
+
+ this._setCloseKeyState(!this.mCurrentTab.pinned);
+
+ // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
+ // that might rely upon the other changes suppressed.
+ // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
+ if (!this._previewMode) {
+ // We've selected the new tab, so go ahead and notify listeners.
+ let event = new CustomEvent("TabSelect", {
+ bubbles: true,
+ cancelable: false,
+ detail: {
+ previousTab: oldTab
+ }
+ });
+ this.mCurrentTab.dispatchEvent(event);
+
+ this._tabAttrModified(oldTab, ["selected"]);
+ this._tabAttrModified(this.mCurrentTab, ["selected"]);
+
+ if (oldBrowser != newBrowser &&
+ oldBrowser.getInPermitUnload) {
+ oldBrowser.getInPermitUnload(inPermitUnload => {
+ if (!inPermitUnload) {
+ return;
+ }
+ // Since the user is switching away from a tab that has
+ // a beforeunload prompt active, we remove the prompt.
+ // This prevents confusing user flows like the following:
+ // 1. User attempts to close Firefox
+ // 2. User switches tabs (ingoring a beforeunload prompt)
+ // 3. User returns to tab, presses "Leave page"
+ let promptBox = this.getTabModalPromptBox(oldBrowser);
+ let prompts = promptBox.listPrompts();
+ // There might not be any prompts here if the tab was closed
+ // while in an onbeforeunload prompt, which will have
+ // destroyed aforementioned prompt already, so check there's
+ // something to remove, first:
+ if (prompts.length) {
+ // NB: This code assumes that the beforeunload prompt
+ // is the top-most prompt on the tab.
+ prompts[prompts.length - 1].abortPrompt();
+ }
+ });
+ }
+
+ oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
+ if (this.isFindBarInitialized(oldTab)) {
+ let findBar = this.getFindBar(oldTab);
+ oldTab._findBarFocused = (!findBar.hidden &&
+ findBar._findField.getAttribute("focused") == "true");
+ }
+
+ // If focus is in the tab bar, retain it there.
+ if (document.activeElement == oldTab) {
+ // We need to explicitly focus the new tab, because
+ // tabbox.xml does this only in some cases.
+ this.mCurrentTab.focus();
+ } else if (gMultiProcessBrowser && document.activeElement !== newBrowser) {
+ // Clear focus so that _adjustFocusAfterTabSwitch can detect if
+ // some element has been focused and respect that.
+ document.activeElement.blur();
+ }
+
+ if (!gMultiProcessBrowser)
+ this._adjustFocusAfterTabSwitch(this.mCurrentTab);
+ }
+
+ updateUserContextUIIndicator();
+ gIdentityHandler.updateSharingIndicator();
+
+ this.tabContainer._setPositionalAttributes();
+
+ if (!gMultiProcessBrowser) {
+ let event = new CustomEvent("TabSwitchDone", {
+ bubbles: true,
+ cancelable: true
+ });
+ this.dispatchEvent(event);
+ }
+
+ if (!aForceUpdate)
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
+ ]]>
+ </body>
+ </method>
+
+ <method name="_adjustFocusAfterTabSwitch">
+ <parameter name="newTab"/>
+ <body><![CDATA[
+ // Don't steal focus from the tab bar.
+ if (document.activeElement == newTab)
+ return;
+
+ let newBrowser = this.getBrowserForTab(newTab);
+
+ // If there's a tabmodal prompt showing, focus it.
+ if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
+ let XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let prompts = newBrowser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
+ let prompt = prompts[prompts.length - 1];
+ prompt.Dialog.setDefaultFocus();
+ return;
+ }
+
+ // Focus the location bar if it was previously focused for that tab.
+ // In full screen mode, only bother making the location bar visible
+ // if the tab is a blank one.
+ if (newBrowser._urlbarFocused && gURLBar) {
+ // Explicitly close the popup if the URL bar retains focus
+ gURLBar.closePopup();
+
+ if (!window.fullScreen) {
+ gURLBar.focus();
+ return;
+ }
+
+ if (isTabEmpty(this.mCurrentTab)) {
+ focusAndSelectUrlBar();
+ return;
+ }
+ }
+
+ // Focus the find bar if it was previously focused for that tab.
+ if (gFindBarInitialized && !gFindBar.hidden &&
+ this.selectedTab._findBarFocused) {
+ gFindBar._findField.focus();
+ return;
+ }
+
+ // Don't focus the content area if something has been focused after the
+ // tab switch was initiated.
+ if (gMultiProcessBrowser &&
+ document.activeElement != document.documentElement)
+ return;
+
+ // We're now committed to focusing the content area.
+ let fm = Services.focus;
+ let focusFlags = fm.FLAG_NOSCROLL;
+
+ if (!gMultiProcessBrowser) {
+ let newFocusedElement = fm.getFocusedElementForWindow(window.content, true, {});
+
+ // for anchors, use FLAG_SHOWRING so that it is clear what link was
+ // last clicked when switching back to that tab
+ if (newFocusedElement &&
+ (newFocusedElement instanceof HTMLAnchorElement ||
+ newFocusedElement.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple"))
+ focusFlags |= fm.FLAG_SHOWRING;
+ }
+
+ fm.setFocus(newBrowser, focusFlags);
+ ]]></body>
+ </method>
+
+ <!--
+ This function assumes we have an LRU cache of tabs (either
+ images of tab content or their layers). The goal is to find
+ out how far into the cache we need to look in order to find
+ aTab. We record this number in telemetry and also move aTab to
+ the front of the cache.
+
+ A newly created tab has position Infinity in the cache.
+ If a tab is closed, it has no effect on the position of other
+ tabs in the cache since we assume that closing a tab doesn't
+ cause us to load in any other tabs.
+
+ We ignore the effect of dragging tabs between windows.
+ -->
+ <method name="_recordTabAccess">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (!Services.telemetry.canRecordExtended) {
+ return;
+ }
+
+ let tabs = Array.from(this.visibleTabs);
+
+ let pos = aTab.cachePosition;
+ for (let i = 0; i < tabs.length; i++) {
+ // If aTab is moving to the front, everything that was
+ // previously in front of it is bumped up one position.
+ if (tabs[i].cachePosition < pos) {
+ tabs[i].cachePosition++;
+ }
+ }
+ aTab.cachePosition = 0;
+
+ if (isFinite(pos)) {
+ Services.telemetry.getHistogramById("TAB_SWITCH_CACHE_POSITION").add(pos);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_tabAttrModified">
+ <parameter name="aTab"/>
+ <parameter name="aChanged"/>
+ <body><![CDATA[
+ if (aTab.closing)
+ return;
+
+ let event = new CustomEvent("TabAttrModified", {
+ bubbles: true,
+ cancelable: false,
+ detail: {
+ changed: aChanged,
+ }
+ });
+ aTab.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="setBrowserSharing">
+ <parameter name="aBrowser"/>
+ <parameter name="aState"/>
+ <body><![CDATA[
+ let tab = this.getTabForBrowser(aBrowser);
+ if (!tab)
+ return;
+
+ let sharing;
+ if (aState.screen) {
+ sharing = "screen";
+ } else if (aState.camera) {
+ sharing = "camera";
+ } else if (aState.microphone) {
+ sharing = "microphone";
+ }
+
+ if (sharing) {
+ tab.setAttribute("sharing", sharing);
+ tab._sharingState = aState;
+ } else {
+ tab.removeAttribute("sharing");
+ tab._sharingState = null;
+ }
+ this._tabAttrModified(tab, ["sharing"]);
+
+ if (aBrowser == this.mCurrentBrowser)
+ gIdentityHandler.updateSharingIndicator();
+ ]]></body>
+ </method>
+
+
+ <method name="setTabTitleLoading">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ aTab.label = this.mStringBundle.getString("tabs.connecting");
+ aTab.crop = "end";
+ this._tabAttrModified(aTab, ["label", "crop"]);
+ ]]>
+ </body>
+ </method>
+
+ <method name="setTabTitle">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ var browser = this.getBrowserForTab(aTab);
+ var crop = "end";
+ var title = browser.contentTitle;
+
+ if (!title) {
+ if (browser.currentURI.spec) {
+ try {
+ title = this.mURIFixup.createExposableURI(browser.currentURI).spec;
+ } catch (ex) {
+ title = browser.currentURI.spec;
+ }
+ }
+
+ if (title && !isBlankPageURL(title)) {
+ // At this point, we now have a URI.
+ // Let's try to unescape it using a character set
+ // in case the URI is not ASCII.
+ try {
+ var characterSet = browser.characterSet;
+ const textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
+ .getService(Components.interfaces.nsITextToSubURI);
+ title = textToSubURI.unEscapeNonAsciiURI(characterSet, title);
+ } catch (ex) { /* Do nothing. */ }
+
+ crop = "center";
+
+ } else if (aTab.hasAttribute("customizemode")) {
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+ title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle",
+ [ brandShortName ]);
+ } else // Still no title? Fall back to our untitled string.
+ title = this.mStringBundle.getString("tabs.emptyTabTitle");
+ }
+
+ if (aTab.label == title &&
+ aTab.crop == crop)
+ return false;
+
+ aTab.label = title;
+ aTab.crop = crop;
+ this._tabAttrModified(aTab, ["label", "crop"]);
+
+ if (aTab.selected)
+ this.updateTitlebar();
+
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="loadOneTab">
+ <parameter name="aURI"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <parameter name="aLoadInBackground"/>
+ <parameter name="aAllowThirdPartyFixup"/>
+ <body>
+ <![CDATA[
+ var aReferrerPolicy;
+ var aFromExternal;
+ var aRelatedToCurrent;
+ var aAllowMixedContent;
+ var aSkipAnimation;
+ var aForceNotRemote;
+ var aNoReferrer;
+ var aUserContextId;
+ var aRelatedBrowser;
+ var aOriginPrincipal;
+ var aOpener;
+ if (arguments.length == 2 &&
+ typeof arguments[1] == "object" &&
+ !(arguments[1] instanceof Ci.nsIURI)) {
+ let params = arguments[1];
+ aReferrerURI = params.referrerURI;
+ aReferrerPolicy = params.referrerPolicy;
+ aCharset = params.charset;
+ aPostData = params.postData;
+ aLoadInBackground = params.inBackground;
+ aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ aFromExternal = params.fromExternal;
+ aRelatedToCurrent = params.relatedToCurrent;
+ aAllowMixedContent = params.allowMixedContent;
+ aSkipAnimation = params.skipAnimation;
+ aForceNotRemote = params.forceNotRemote;
+ aNoReferrer = params.noReferrer;
+ aUserContextId = params.userContextId;
+ aRelatedBrowser = params.relatedBrowser;
+ aOriginPrincipal = params.originPrincipal;
+ aOpener = params.opener;
+ }
+
+ var bgLoad = (aLoadInBackground != null) ? aLoadInBackground :
+ Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+ var owner = bgLoad ? null : this.selectedTab;
+ var tab = this.addTab(aURI, {
+ referrerURI: aReferrerURI,
+ referrerPolicy: aReferrerPolicy,
+ charset: aCharset,
+ postData: aPostData,
+ ownerTab: owner,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ fromExternal: aFromExternal,
+ relatedToCurrent: aRelatedToCurrent,
+ skipAnimation: aSkipAnimation,
+ allowMixedContent: aAllowMixedContent,
+ forceNotRemote: aForceNotRemote,
+ noReferrer: aNoReferrer,
+ userContextId: aUserContextId,
+ originPrincipal: aOriginPrincipal,
+ relatedBrowser: aRelatedBrowser,
+ opener: aOpener });
+ if (!bgLoad)
+ this.selectedTab = tab;
+
+ return tab;
+ ]]>
+ </body>
+ </method>
+
+ <method name="loadTabs">
+ <parameter name="aURIs"/>
+ <parameter name="aLoadInBackground"/>
+ <parameter name="aReplace"/>
+ <body><![CDATA[
+ let aAllowThirdPartyFixup;
+ let aTargetTab;
+ let aNewIndex = -1;
+ let aPostDatas = [];
+ let aUserContextId;
+ if (arguments.length == 2 &&
+ typeof arguments[1] == "object") {
+ let params = arguments[1];
+ aLoadInBackground = params.inBackground;
+ aReplace = params.replace;
+ aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ aTargetTab = params.targetTab;
+ aNewIndex = typeof params.newIndex === "number" ?
+ params.newIndex : aNewIndex;
+ aPostDatas = params.postDatas || aPostDatas;
+ aUserContextId = params.userContextId;
+ }
+
+ if (!aURIs.length)
+ return;
+
+ // The tab selected after this new tab is closed (i.e. the new tab's
+ // "owner") is the next adjacent tab (i.e. not the previously viewed tab)
+ // when several urls are opened here (i.e. closing the first should select
+ // the next of many URLs opened) or if the pref to have UI links opened in
+ // the background is set (i.e. the link is not being opened modally)
+ //
+ // i.e.
+ // Number of URLs Load UI Links in BG Focus Last Viewed?
+ // == 1 false YES
+ // == 1 true NO
+ // > 1 false/true NO
+ var multiple = aURIs.length > 1;
+ var owner = multiple || aLoadInBackground ? null : this.selectedTab;
+ var firstTabAdded = null;
+ var targetTabIndex = -1;
+
+ if (aReplace) {
+ let browser;
+ if (aTargetTab) {
+ browser = this.getBrowserForTab(aTargetTab);
+ targetTabIndex = aTargetTab._tPos;
+ } else {
+ browser = this.mCurrentBrowser;
+ targetTabIndex = this.tabContainer.selectedIndex;
+ }
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (aAllowThirdPartyFixup) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
+ Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ try {
+ browser.loadURIWithFlags(aURIs[0], {
+ flags, postData: aPostDatas[0]
+ });
+ } catch (e) {
+ // Ignore failure in case a URI is wrong, so we can continue
+ // opening the next ones.
+ }
+ } else {
+ firstTabAdded = this.addTab(aURIs[0], {
+ ownerTab: owner,
+ skipAnimation: multiple,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ postData: aPostDatas[0],
+ userContextId: aUserContextId
+ });
+ if (aNewIndex !== -1) {
+ this.moveTabTo(firstTabAdded, aNewIndex);
+ targetTabIndex = firstTabAdded._tPos;
+ }
+ }
+
+ let tabNum = targetTabIndex;
+ for (let i = 1; i < aURIs.length; ++i) {
+ let tab = this.addTab(aURIs[i], {
+ skipAnimation: true,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ postData: aPostDatas[i],
+ userContextId: aUserContextId
+ });
+ if (targetTabIndex !== -1)
+ this.moveTabTo(tab, ++tabNum);
+ }
+
+ if (!aLoadInBackground) {
+ if (firstTabAdded) {
+ // .selectedTab setter focuses the content area
+ this.selectedTab = firstTabAdded;
+ }
+ else
+ this.selectedBrowser.focus();
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateBrowserRemoteness">
+ <parameter name="aBrowser"/>
+ <parameter name="aShouldBeRemote"/>
+ <parameter name="aOpener"/>
+ <parameter name="aFreshProcess"/>
+ <body>
+ <![CDATA[
+ let isRemote = aBrowser.getAttribute("remote") == "true";
+
+ // If we are passed an opener, we must be making the browser non-remote, and
+ // if the browser is _currently_ non-remote, we need the openers to match,
+ // because it is already too late to change it.
+ if (aOpener) {
+ if (aShouldBeRemote) {
+ throw new Exception("Cannot set an opener on a browser which should be remote!");
+ }
+ if (!isRemote && aBrowser.contentWindow.opener != aOpener) {
+ throw new Exception("Cannot change opener on an already non-remote browser!");
+ }
+ }
+
+ // Abort if we're not going to change anything
+ if (isRemote == aShouldBeRemote && !aFreshProcess) {
+ return false;
+ }
+
+ let tab = this.getTabForBrowser(aBrowser);
+ let evt = document.createEvent("Events");
+ evt.initEvent("BeforeTabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+
+ let wasActive = document.activeElement == aBrowser;
+
+ // Unmap the old outerWindowID.
+ this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID);
+
+ // Unhook our progress listener.
+ let filter = this._tabFilters.get(tab);
+ let listener = this._tabListeners.get(tab);
+ aBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(listener);
+
+ // We'll be creating a new listener, so destroy the old one.
+ listener.destroy();
+
+ let oldUserTypedValue = aBrowser.userTypedValue;
+ let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
+
+ // Make sure the browser is destroyed so it unregisters from observer notifications
+ aBrowser.destroy();
+
+ // Make sure to restore the original droppedLinkHandler and
+ // relatedBrowser.
+ let droppedLinkHandler = aBrowser.droppedLinkHandler;
+ let relatedBrowser = aBrowser.relatedBrowser;
+
+ // Change the "remote" attribute.
+ let parent = aBrowser.parentNode;
+ parent.removeChild(aBrowser);
+ aBrowser.setAttribute("remote", aShouldBeRemote ? "true" : "false");
+
+ // NB: This works with the hack in the browser constructor that
+ // turns this normal property into a field.
+ aBrowser.relatedBrowser = relatedBrowser;
+
+ // Set the opener window on the browser, such that when the frame
+ // loader is created the opener is set correctly.
+ aBrowser.presetOpenerWindow(aOpener);
+
+ // Set the freshProcess attribute so that the frameloader knows to
+ // create a new process
+ if (aFreshProcess) {
+ aBrowser.setAttribute("freshProcess", "true");
+ }
+
+ parent.appendChild(aBrowser);
+
+ // Remove the freshProcess attribute if we set it, as we don't
+ // want it to apply for the next time the frameloader is created
+ aBrowser.removeAttribute("freshProcess");
+
+ aBrowser.userTypedValue = oldUserTypedValue;
+ if (hadStartedLoad) {
+ aBrowser.urlbarChangeTracker.startedLoad();
+ }
+
+ aBrowser.droppedLinkHandler = droppedLinkHandler;
+
+ // Switching a browser's remoteness will create a new frameLoader.
+ // As frameLoaders start out with an active docShell we have to
+ // deactivate it if this is not the selected tab's browser or the
+ // browser window is minimized.
+ aBrowser.docShellIsActive = this.shouldActivateDocShell(aBrowser);
+
+ // Create a new tab progress listener for the new browser we just injected,
+ // since tab progress listeners have logic for handling the initial about:blank
+ // load
+ listener = this.mTabProgressListener(tab, aBrowser, true, false);
+ this._tabListeners.set(tab, listener);
+ filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ // Restore the progress listener.
+ aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ // Restore the securityUI state.
+ let securityUI = aBrowser.securityUI;
+ let state = securityUI ? securityUI.state
+ : Ci.nsIWebProgressListener.STATE_IS_INSECURE;
+ // Include the true final argument to indicate that this event is
+ // simulated (instead of being observed by the webProgressListener).
+ this._callProgressListeners(aBrowser, "onSecurityChange",
+ [aBrowser.webProgress, null, state, true],
+ true, false);
+
+ if (aShouldBeRemote) {
+ // Switching the browser to be remote will connect to a new child
+ // process so the browser can no longer be considered to be
+ // crashed.
+ tab.removeAttribute("crashed");
+ } else {
+ aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned })
+
+ // Register the new outerWindowID.
+ this._outerWindowIDBrowserMap.set(aBrowser.outerWindowID, aBrowser);
+ }
+
+ if (wasActive)
+ aBrowser.focus();
+
+ // If the findbar has been initialised, reset its browser reference.
+ if (this.isFindBarInitialized(tab)) {
+ this.getFindBar(tab).browser = aBrowser;
+ }
+
+ evt = document.createEvent("Events");
+ evt.initEvent("TabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="switchBrowserIntoFreshProcess">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ if (!gMultiProcessBrowser) {
+ return this.updateBrowserRemoteness(aBrowser, false);
+ }
+
+ return this.updateBrowserRemoteness(aBrowser,
+ /* aShouldBeRemote */ true,
+ /* aOpener */ null,
+ /* aFreshProcess */ true);
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateBrowserRemotenessByURL">
+ <parameter name="aBrowser"/>
+ <parameter name="aURL"/>
+ <body>
+ <![CDATA[
+ if (!gMultiProcessBrowser)
+ return this.updateBrowserRemoteness(aBrowser, false);
+
+ let process = aBrowser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
+ : Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+ // If this URL can't load in the browser's current process then flip
+ // it to the other process
+ if (!E10SUtils.canLoadURIInProcess(aURL, process))
+ return this.updateBrowserRemoteness(aBrowser, !aBrowser.isRemoteBrowser);
+
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <field name="_preloadedBrowser">null</field>
+ <method name="_getPreloadedBrowser">
+ <body>
+ <![CDATA[
+ if (!this._isPreloadingEnabled()) {
+ return null;
+ }
+
+ // The preloaded browser might be null.
+ let browser = this._preloadedBrowser;
+
+ // Consume the browser.
+ this._preloadedBrowser = null;
+
+ // Attach the nsIFormFillController now that we know the browser
+ // will be used. If we do that before and the preloaded browser
+ // won't be consumed until shutdown then we leak a docShell.
+ // Also, we do not need to take care of attaching nsIFormFillControllers
+ // in the case that the browser is remote, as remote browsers take
+ // care of that themselves.
+ if (browser && this.hasAttribute("autocompletepopup")) {
+ browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
+ }
+
+ return browser;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_isPreloadingEnabled">
+ <body>
+ <![CDATA[
+ // Preloading for the newtab page is enabled when the pref is true
+ // and the URL is "about:newtab". We do not support preloading for
+ // custom newtab URLs.
+ return Services.prefs.getBoolPref("browser.newtab.preload") &&
+ !aboutNewTabService.overridden;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_createPreloadBrowser">
+ <body>
+ <![CDATA[
+ // Do nothing if we have a preloaded browser already
+ // or preloading of newtab pages is disabled.
+ if (this._preloadedBrowser || !this._isPreloadingEnabled()) {
+ return;
+ }
+
+ let remote = gMultiProcessBrowser &&
+ E10SUtils.canLoadURIInProcess(BROWSER_NEW_TAB_URL, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);
+ let browser = this._createBrowser({isPreloadBrowser: true, remote: remote});
+ this._preloadedBrowser = browser;
+
+ let notificationbox = this.getNotificationBox(browser);
+ this.mPanelContainer.appendChild(notificationbox);
+
+ if (remote) {
+ // For remote browsers, we need to make sure that the webProgress is
+ // instantiated, otherwise the parent won't get informed about the state
+ // of the preloaded browser until it gets attached to a tab.
+ browser.webProgress;
+ }
+
+ browser.loadURI(BROWSER_NEW_TAB_URL);
+ browser.docShellIsActive = false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_createBrowser">
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ // Supported parameters:
+ // userContextId, remote, isPreloadBrowser, uriIsAboutBlank, permanentKey
+
+ const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ let b = document.createElementNS(NS_XUL, "browser");
+ b.permanentKey = aParams.permanentKey || {};
+ b.setAttribute("type", "content-targetable");
+ b.setAttribute("message", "true");
+ b.setAttribute("messagemanagergroup", "browsers");
+ b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
+ b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
+
+ if (aParams.userContextId) {
+ b.setAttribute("usercontextid", aParams.userContextId);
+ }
+
+ if (aParams.remote) {
+ b.setAttribute("remote", "true");
+ }
+
+ if (aParams.opener) {
+ if (aParams.remote) {
+ throw new Exception("Cannot set opener window on a remote browser!");
+ }
+ b.QueryInterface(Ci.nsIFrameLoaderOwner).presetOpenerWindow(aParams.opener);
+ }
+
+ if (window.gShowPageResizers && window.windowState == window.STATE_NORMAL) {
+ b.setAttribute("showresizer", "true");
+ }
+
+ if (!aParams.isPreloadBrowser && this.hasAttribute("autocompletepopup")) {
+ b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
+ }
+
+ if (this.hasAttribute("selectmenulist"))
+ b.setAttribute("selectmenulist", this.getAttribute("selectmenulist"));
+
+ if (this.hasAttribute("datetimepicker")) {
+ b.setAttribute("datetimepicker", this.getAttribute("datetimepicker"));
+ }
+
+ b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
+
+ if (aParams.relatedBrowser) {
+ b.relatedBrowser = aParams.relatedBrowser;
+ }
+
+ // Create the browserStack container
+ var stack = document.createElementNS(NS_XUL, "stack");
+ stack.className = "browserStack";
+ stack.appendChild(b);
+ stack.setAttribute("flex", "1");
+
+ // Create the browserContainer
+ var browserContainer = document.createElementNS(NS_XUL, "vbox");
+ browserContainer.className = "browserContainer";
+ browserContainer.appendChild(stack);
+ browserContainer.setAttribute("flex", "1");
+
+ // Create the sidebar container
+ var browserSidebarContainer = document.createElementNS(NS_XUL,
+ "hbox");
+ browserSidebarContainer.className = "browserSidebarContainer";
+ browserSidebarContainer.appendChild(browserContainer);
+ browserSidebarContainer.setAttribute("flex", "1");
+
+ // Add the Message and the Browser to the box
+ var notificationbox = document.createElementNS(NS_XUL,
+ "notificationbox");
+ notificationbox.setAttribute("flex", "1");
+ notificationbox.setAttribute("notificationside", "top");
+ notificationbox.appendChild(browserSidebarContainer);
+
+ // Prevent the superfluous initial load of a blank document
+ // if we're going to load something other than about:blank.
+ if (!aParams.uriIsAboutBlank) {
+ b.setAttribute("nodefaultsrc", "true");
+ }
+
+ return b;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_linkBrowserToTab">
+ <parameter name="aTab"/>
+ <parameter name="aURI"/>
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ "use strict";
+
+ // Supported parameters:
+ // forceNotRemote, userContextId
+
+ let uriIsAboutBlank = !aURI || aURI == "about:blank";
+
+ // The new browser should be remote if this is an e10s window and
+ // the uri to load can be loaded remotely.
+ let remote = gMultiProcessBrowser &&
+ !aParams.forceNotRemote &&
+ E10SUtils.canLoadURIInProcess(aURI, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);
+
+ let browser;
+ let usingPreloadedContent = false;
+
+ // If we open a new tab with the newtab URL in the default
+ // userContext, check if there is a preloaded browser ready.
+ // Private windows are not included because both the label and the
+ // icon for the tab would be set incorrectly (see bug 1195981).
+ if (aURI == BROWSER_NEW_TAB_URL &&
+ !aParams.userContextId &&
+ !PrivateBrowsingUtils.isWindowPrivate(window)) {
+ browser = this._getPreloadedBrowser();
+ if (browser) {
+ usingPreloadedContent = true;
+ aTab.permanentKey = browser.permanentKey;
+ }
+ }
+
+ if (!browser) {
+ // No preloaded browser found, create one.
+ browser = this._createBrowser({permanentKey: aTab.permanentKey,
+ remote: remote,
+ uriIsAboutBlank: uriIsAboutBlank,
+ userContextId: aParams.userContextId,
+ relatedBrowser: aParams.relatedBrowser,
+ opener: aParams.opener});
+ }
+
+ let notificationbox = this.getNotificationBox(browser);
+ let uniqueId = this._generateUniquePanelID();
+ notificationbox.id = uniqueId;
+ aTab.linkedPanel = uniqueId;
+ aTab.linkedBrowser = browser;
+ aTab.hasBrowser = true;
+ this._tabForBrowser.set(browser, aTab);
+
+ // Inject the <browser> into the DOM if necessary.
+ if (!notificationbox.parentNode) {
+ // NB: this appendChild call causes us to run constructors for the
+ // browser element, which fires off a bunch of notifications. Some
+ // of those notifications can cause code to run that inspects our
+ // state, so it is important that the tab element is fully
+ // initialized by this point.
+ this.mPanelContainer.appendChild(notificationbox);
+ }
+
+ // wire up a progress listener for the new browser object.
+ let tabListener = this.mTabProgressListener(aTab, browser, uriIsAboutBlank, usingPreloadedContent);
+ const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
+ .createInstance(Ci.nsIWebProgress);
+ filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
+ this._tabListeners.set(aTab, tabListener);
+ this._tabFilters.set(aTab, filter);
+
+ browser.droppedLinkHandler = handleDroppedLink;
+
+ // We start our browsers out as inactive, and then maintain
+ // activeness in the tab switcher.
+ browser.docShellIsActive = false;
+
+ // When addTab() is called with an URL that is not "about:blank" we
+ // set the "nodefaultsrc" attribute that prevents a frameLoader
+ // from being created as soon as the linked <browser> is inserted
+ // into the DOM. We thus have to register the new outerWindowID
+ // for non-remote browsers after we have called browser.loadURI().
+ if (!remote) {
+ this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
+ }
+
+ var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: {} });
+ aTab.dispatchEvent(evt);
+
+ return { usingPreloadedContent: usingPreloadedContent };
+ ]]>
+ </body>
+ </method>
+
+ <method name="addTab">
+ <parameter name="aURI"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <parameter name="aOwner"/>
+ <parameter name="aAllowThirdPartyFixup"/>
+ <body>
+ <![CDATA[
+ "use strict";
+
+ const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var aReferrerPolicy;
+ var aFromExternal;
+ var aRelatedToCurrent;
+ var aSkipAnimation;
+ var aAllowMixedContent;
+ var aForceNotRemote;
+ var aNoReferrer;
+ var aUserContextId;
+ var aEventDetail;
+ var aRelatedBrowser;
+ var aOriginPrincipal;
+ var aOpener;
+ if (arguments.length == 2 &&
+ typeof arguments[1] == "object" &&
+ !(arguments[1] instanceof Ci.nsIURI)) {
+ let params = arguments[1];
+ aReferrerURI = params.referrerURI;
+ aReferrerPolicy = params.referrerPolicy;
+ aCharset = params.charset;
+ aPostData = params.postData;
+ aOwner = params.ownerTab;
+ aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ aFromExternal = params.fromExternal;
+ aRelatedToCurrent = params.relatedToCurrent;
+ aSkipAnimation = params.skipAnimation;
+ aAllowMixedContent = params.allowMixedContent;
+ aForceNotRemote = params.forceNotRemote;
+ aNoReferrer = params.noReferrer;
+ aUserContextId = params.userContextId;
+ aEventDetail = params.eventDetail;
+ aRelatedBrowser = params.relatedBrowser;
+ aOriginPrincipal = params.originPrincipal;
+ aOpener = params.opener;
+ }
+
+ // if we're adding tabs, we're past interrupt mode, ditch the owner
+ if (this.mCurrentTab.owner)
+ this.mCurrentTab.owner = null;
+
+ var t = document.createElementNS(NS_XUL, "tab");
+
+ var uriIsAboutBlank = !aURI || aURI == "about:blank";
+ let aURIObject = null;
+ try {
+ aURIObject = Services.io.newURI(aURI || "about:blank");
+ } catch (ex) { /* we'll try to fix up this URL later */ }
+
+ if (!aURI || isBlankPageURL(aURI)) {
+ t.setAttribute("label", this.mStringBundle.getString("tabs.emptyTabTitle"));
+ } else if (aURI.toLowerCase().startsWith("javascript:")) {
+ // This can go away when bug 672618 or bug 55696 are fixed.
+ t.setAttribute("label", aURI);
+ }
+
+ if (aUserContextId) {
+ t.setAttribute("usercontextid", aUserContextId);
+ ContextualIdentityService.setTabStyle(t);
+ }
+
+ t.setAttribute("crop", "end");
+ t.setAttribute("onerror", "this.removeAttribute('image');");
+ t.className = "tabbrowser-tab";
+
+ this.tabContainer._unlockTabSizing();
+
+ // When overflowing, new tabs are scrolled into view smoothly, which
+ // doesn't go well together with the width transition. So we skip the
+ // transition in that case.
+ let animate = !aSkipAnimation &&
+ this.tabContainer.getAttribute("overflow") != "true" &&
+ Services.prefs.getBoolPref("browser.tabs.animate");
+ if (!animate) {
+ t.setAttribute("fadein", "true");
+ setTimeout(function (tabContainer) {
+ tabContainer._handleNewTab(t);
+ }, 0, this.tabContainer);
+ }
+
+ // invalidate cache
+ this._visibleTabs = null;
+
+ this.tabContainer.appendChild(t);
+
+ // If this new tab is owned by another, assert that relationship
+ if (aOwner)
+ t.owner = aOwner;
+
+ var position = this.tabs.length - 1;
+ t._tPos = position;
+ t.permanentKey = {};
+ this.tabContainer._setPositionalAttributes();
+
+ this.tabContainer.updateVisibility();
+
+ // Currently in this incarnation of bug 906076, we are forcing the
+ // browser to immediately be linked. In future incarnations of this
+ // bug this will be removed so we can leave the tab in its "lazy"
+ // state to be exploited for startup optimization. Note that for
+ // now this must occur before "TabOpen" event is fired, as that will
+ // trigger SessionStore.jsm to run code that expects the existence
+ // of tab.linkedBrowser.
+ let browserParams = {
+ forceNotRemote: aForceNotRemote,
+ userContextId: aUserContextId,
+ relatedBrowser: aRelatedBrowser,
+ opener: aOpener,
+ };
+ let { usingPreloadedContent } = this._linkBrowserToTab(t, aURI, browserParams);
+ let b = t.linkedBrowser;
+
+ // Dispatch a new tab notification. We do this once we're
+ // entirely done, so that things are in a consistent state
+ // even if the event listener opens or closes tabs.
+ var detail = aEventDetail || {};
+ var evt = new CustomEvent("TabOpen", { bubbles: true, detail });
+ t.dispatchEvent(evt);
+
+ if (!usingPreloadedContent && aOriginPrincipal && aURI) {
+ let {URI_INHERITS_SECURITY_CONTEXT} = Ci.nsIProtocolHandler;
+ // Unless we know for sure we're not inheriting principals,
+ // force the about:blank viewer to have the right principal:
+ if (!aURIObject ||
+ (Services.io.getProtocolFlags(aURIObject.scheme) & URI_INHERITS_SECURITY_CONTEXT)) {
+ b.createAboutBlankContentViewer(aOriginPrincipal);
+ }
+ }
+
+ // If we didn't swap docShells with a preloaded browser
+ // then let's just continue loading the page normally.
+ if (!usingPreloadedContent && !uriIsAboutBlank) {
+ // pretend the user typed this so it'll be available till
+ // the document successfully loads
+ if (aURI && gInitialPages.indexOf(aURI) == -1)
+ b.userTypedValue = aURI;
+
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (aAllowThirdPartyFixup) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ if (aFromExternal)
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
+ if (aAllowMixedContent)
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
+ try {
+ b.loadURIWithFlags(aURI, {
+ flags,
+ referrerURI: aNoReferrer ? null: aReferrerURI,
+ referrerPolicy: aReferrerPolicy,
+ charset: aCharset,
+ postData: aPostData,
+ });
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+
+ // Check if we're opening a tab related to the current tab and
+ // move it to after the current tab.
+ // aReferrerURI is null or undefined if the tab is opened from
+ // an external application or bookmark, i.e. somewhere other
+ // than the current tab.
+ if ((aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent) &&
+ Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) {
+ let newTabPos = (this._lastRelatedTab ||
+ this.selectedTab)._tPos + 1;
+ if (this._lastRelatedTab)
+ this._lastRelatedTab.owner = null;
+ else
+ t.owner = this.selectedTab;
+ this.moveTabTo(t, newTabPos);
+ this._lastRelatedTab = t;
+ }
+
+ if (animate) {
+ requestAnimationFrame(function () {
+ this.tabContainer._handleTabTelemetryStart(t, aURI);
+
+ // kick the animation off
+ t.setAttribute("fadein", "true");
+ }.bind(this));
+ }
+
+ return t;
+ ]]>
+ </body>
+ </method>
+
+ <method name="warnAboutClosingTabs">
+ <parameter name="aCloseTabs"/>
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ var tabsToClose;
+ switch (aCloseTabs) {
+ case this.closingTabsEnum.ALL:
+ tabsToClose = this.tabs.length - this._removingTabs.length -
+ gBrowser._numPinnedTabs;
+ break;
+ case this.closingTabsEnum.OTHER:
+ tabsToClose = this.visibleTabs.length - 1 - gBrowser._numPinnedTabs;
+ break;
+ case this.closingTabsEnum.TO_END:
+ if (!aTab)
+ throw new Error("Required argument missing: aTab");
+
+ tabsToClose = this.getTabsToTheEndFrom(aTab).length;
+ break;
+ default:
+ throw new Error("Invalid argument: " + aCloseTabs);
+ }
+
+ if (tabsToClose <= 1)
+ return true;
+
+ const pref = aCloseTabs == this.closingTabsEnum.ALL ?
+ "browser.tabs.warnOnClose" : "browser.tabs.warnOnCloseOtherTabs";
+ var shouldPrompt = Services.prefs.getBoolPref(pref);
+ if (!shouldPrompt)
+ return true;
+
+ var ps = Services.prompt;
+
+ // default to true: if it were false, we wouldn't get this far
+ var warnOnClose = { value: true };
+ var bundle = this.mStringBundle;
+
+ // focus the window before prompting.
+ // this will raise any minimized window, which will
+ // make it obvious which window the prompt is for and will
+ // solve the problem of windows "obscuring" the prompt.
+ // see bug #350299 for more details
+ window.focus();
+ var warningMessage =
+ PluralForm.get(tabsToClose, bundle.getString("tabs.closeWarningMultiple"))
+ .replace("#1", tabsToClose);
+ var buttonPressed =
+ ps.confirmEx(window,
+ bundle.getString("tabs.closeWarningTitle"),
+ warningMessage,
+ (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0)
+ + (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1),
+ bundle.getString("tabs.closeButtonMultiple"),
+ null, null,
+ aCloseTabs == this.closingTabsEnum.ALL ?
+ bundle.getString("tabs.closeWarningPromptMe") : null,
+ warnOnClose);
+ var reallyClose = (buttonPressed == 0);
+
+ // don't set the pref unless they press OK and it's false
+ if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value)
+ Services.prefs.setBoolPref(pref, false);
+
+ return reallyClose;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getTabsToTheEndFrom">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ var tabsToEnd = [];
+ let tabs = this.visibleTabs;
+ for (let i = tabs.length - 1; tabs[i] != aTab && i >= 0; --i) {
+ tabsToEnd.push(tabs[i]);
+ }
+ return tabsToEnd.reverse();
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeTabsToTheEndFrom">
+ <parameter name="aTab"/>
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ if (this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab)) {
+ let tabs = this.getTabsToTheEndFrom(aTab);
+ for (let i = tabs.length - 1; i >= 0; --i) {
+ this.removeTab(tabs[i], aParams);
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeAllTabsBut">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (aTab.pinned)
+ return;
+
+ if (this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) {
+ let tabs = this.visibleTabs;
+ this.selectedTab = aTab;
+
+ for (let i = tabs.length - 1; i >= 0; --i) {
+ if (tabs[i] != aTab && !tabs[i].pinned)
+ this.removeTab(tabs[i], {animate: true});
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeCurrentTab">
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ this.removeTab(this.mCurrentTab, aParams);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_removingTabs">
+ []
+ </field>
+
+ <method name="removeTab">
+ <parameter name="aTab"/>
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ if (aParams) {
+ var animate = aParams.animate;
+ var byMouse = aParams.byMouse;
+ var skipPermitUnload = aParams.skipPermitUnload;
+ }
+
+ // Handle requests for synchronously removing an already
+ // asynchronously closing tab.
+ if (!animate &&
+ aTab.closing) {
+ this._endRemoveTab(aTab);
+ return;
+ }
+
+ var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
+
+ if (!this._beginRemoveTab(aTab, null, null, true, skipPermitUnload))
+ return;
+
+ if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse)
+ this.tabContainer._lockTabSizing(aTab);
+ else
+ this.tabContainer._unlockTabSizing();
+
+ if (!animate /* the caller didn't opt in */ ||
+ isLastTab ||
+ aTab.pinned ||
+ aTab.hidden ||
+ this._removingTabs.length > 3 /* don't want lots of concurrent animations */ ||
+ aTab.getAttribute("fadein") != "true" /* fade-in transition hasn't been triggered yet */ ||
+ window.getComputedStyle(aTab).maxWidth == "0.1px" /* fade-in transition hasn't moved yet */ ||
+ !Services.prefs.getBoolPref("browser.tabs.animate")) {
+ this._endRemoveTab(aTab);
+ return;
+ }
+
+ this.tabContainer._handleTabTelemetryStart(aTab);
+
+ this._blurTab(aTab);
+ aTab.style.maxWidth = ""; // ensure that fade-out transition happens
+ aTab.removeAttribute("fadein");
+
+ setTimeout(function (tab, tabbrowser) {
+ if (tab.parentNode &&
+ window.getComputedStyle(tab).maxWidth == "0.1px") {
+ NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)");
+ tabbrowser._endRemoveTab(tab);
+ }
+ }, 3000, aTab, this);
+ ]]>
+ </body>
+ </method>
+
+ <!-- Tab close requests are ignored if the window is closing anyway,
+ e.g. when holding Ctrl+W. -->
+ <field name="_windowIsClosing">
+ false
+ </field>
+
+ <method name="_beginRemoveTab">
+ <parameter name="aTab"/>
+ <parameter name="aAdoptedByTab"/>
+ <parameter name="aCloseWindowWithLastTab"/>
+ <parameter name="aCloseWindowFastpath"/>
+ <parameter name="aSkipPermitUnload"/>
+ <body>
+ <![CDATA[
+ if (aTab.closing ||
+ this._windowIsClosing)
+ return false;
+
+ var browser = this.getBrowserForTab(aTab);
+
+ if (!aTab._pendingPermitUnload && !aAdoptedByTab && !aSkipPermitUnload) {
+ // We need to block while calling permitUnload() because it
+ // processes the event queue and may lead to another removeTab()
+ // call before permitUnload() returns.
+ aTab._pendingPermitUnload = true;
+ let {permitUnload, timedOut} = browser.permitUnload();
+ delete aTab._pendingPermitUnload;
+ // If we were closed during onbeforeunload, we return false now
+ // so we don't (try to) close the same tab again. Of course, we
+ // also stop if the unload was cancelled by the user:
+ if (aTab.closing || (!timedOut && !permitUnload)) {
+ // NB: deliberately keep the _closedDuringPermitUnload set to
+ // true so we keep exiting early in case of multiple calls.
+ return false;
+ }
+ }
+
+
+ var closeWindow = false;
+ var newTab = false;
+ if (this.tabs.length - this._removingTabs.length == 1) {
+ closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
+ !window.toolbar.visible ||
+ Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
+
+ if (closeWindow) {
+ // We've already called beforeunload on all the relevant tabs if we get here,
+ // so avoid calling it again:
+ window.skipNextCanClose = true;
+ }
+
+ // Closing the tab and replacing it with a blank one is notably slower
+ // than closing the window right away. If the caller opts in, take
+ // the fast path.
+ if (closeWindow &&
+ aCloseWindowFastpath &&
+ this._removingTabs.length == 0) {
+ // This call actually closes the window, unless the user
+ // cancels the operation. We are finished here in both cases.
+ this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow);
+ return null;
+ }
+
+ newTab = true;
+ }
+
+ aTab.closing = true;
+ this._removingTabs.push(aTab);
+ this._visibleTabs = null; // invalidate cache
+
+ // Invalidate hovered tab state tracking for this closing tab.
+ if (this.tabContainer._hoveredTab == aTab)
+ aTab._mouseleave();
+
+ if (newTab)
+ this.addTab(BROWSER_NEW_TAB_URL, {skipAnimation: true});
+ else
+ this.tabContainer.updateVisibility();
+
+ // We're committed to closing the tab now.
+ // Dispatch a notification.
+ // We dispatch it before any teardown so that event listeners can
+ // inspect the tab that's about to close.
+ var evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: aAdoptedByTab } });
+ aTab.dispatchEvent(evt);
+
+ if (!aAdoptedByTab && !gMultiProcessBrowser) {
+ // Prevent this tab from showing further dialogs, since we're closing it
+ var windowUtils = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.disableDialogs();
+ }
+
+ // Remove the tab's filter and progress listener.
+ const filter = this._tabFilters.get(aTab);
+
+ browser.webProgress.removeProgressListener(filter);
+
+ const listener = this._tabListeners.get(aTab);
+ filter.removeProgressListener(listener);
+ listener.destroy();
+
+ if (browser.registeredOpenURI && !aAdoptedByTab) {
+ this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
+ browser.getAttribute("usercontextid") || 0);
+ delete browser.registeredOpenURI;
+ }
+
+ // We are no longer the primary content area.
+ browser.setAttribute("type", "content-targetable");
+
+ // Remove this tab as the owner of any other tabs, since it's going away.
+ for (let tab of this.tabs) {
+ if ("owner" in tab && tab.owner == aTab)
+ // |tab| is a child of the tab we're removing, make it an orphan
+ tab.owner = null;
+ }
+
+ aTab._endRemoveArgs = [closeWindow, newTab];
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_endRemoveTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!aTab || !aTab._endRemoveArgs)
+ return;
+
+ var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
+ aTab._endRemoveArgs = null;
+
+ if (this._windowIsClosing) {
+ aCloseWindow = false;
+ aNewTab = false;
+ }
+
+ this._lastRelatedTab = null;
+
+ // update the UI early for responsiveness
+ aTab.collapsed = true;
+ this.tabContainer._fillTrailingGap();
+ this._blurTab(aTab);
+
+ this._removingTabs.splice(this._removingTabs.indexOf(aTab), 1);
+
+ if (aCloseWindow) {
+ this._windowIsClosing = true;
+ while (this._removingTabs.length)
+ this._endRemoveTab(this._removingTabs[0]);
+ } else if (!this._windowIsClosing) {
+ if (aNewTab)
+ focusAndSelectUrlBar();
+
+ // workaround for bug 345399
+ this.tabContainer.mTabstrip._updateScrollButtonsDisabledState();
+ }
+
+ // We're going to remove the tab and the browser now.
+ this._tabFilters.delete(aTab);
+ this._tabListeners.delete(aTab);
+
+ var browser = this.getBrowserForTab(aTab);
+ this._outerWindowIDBrowserMap.delete(browser.outerWindowID);
+
+ // Because of the way XBL works (fields just set JS
+ // properties on the element) and the code we have in place
+ // to preserve the JS objects for any elements that have
+ // JS properties set on them, the browser element won't be
+ // destroyed until the document goes away. So we force a
+ // cleanup ourselves.
+ // This has to happen before we remove the child so that the
+ // XBL implementation of nsIObserver still works.
+ browser.destroy();
+
+ var wasPinned = aTab.pinned;
+
+ // Remove the tab ...
+ this.tabContainer.removeChild(aTab);
+
+ // ... and fix up the _tPos properties immediately.
+ for (let i = aTab._tPos; i < this.tabs.length; i++)
+ this.tabs[i]._tPos = i;
+
+ if (!this._windowIsClosing) {
+ if (wasPinned)
+ this.tabContainer._positionPinnedTabs();
+
+ // update tab close buttons state
+ this.tabContainer.adjustTabstrip();
+
+ setTimeout(function(tabs) {
+ tabs._lastTabClosedByMouse = false;
+ }, 0, this.tabContainer);
+ }
+
+ // update tab positional properties and attributes
+ this.selectedTab._selected = true;
+ this.tabContainer._setPositionalAttributes();
+
+ // Removing the panel requires fixing up selectedPanel immediately
+ // (see below), which would be hindered by the potentially expensive
+ // browser removal. So we remove the browser and the panel in two
+ // steps.
+
+ var panel = this.getNotificationBox(browser);
+
+ // In the multi-process case, it's possible an asynchronous tab switch
+ // is still underway. If so, then it's possible that the last visible
+ // browser is the one we're in the process of removing. There's the
+ // risk of displaying preloaded browsers that are at the end of the
+ // deck if we remove the browser before the switch is complete, so
+ // we alert the switcher in order to show a spinner instead.
+ if (this._switcher) {
+ this._switcher.onTabRemoved(aTab);
+ }
+
+ // This will unload the document. An unload handler could remove
+ // dependant tabs, so it's important that the tabbrowser is now in
+ // a consistent state (tab removed, tab positions updated, etc.).
+ browser.parentNode.removeChild(browser);
+
+ // Release the browser in case something is erroneously holding a
+ // reference to the tab after its removal.
+ this._tabForBrowser.delete(aTab.linkedBrowser);
+ aTab.linkedBrowser = null;
+
+ // As the browser is removed, the removal of a dependent document can
+ // cause the whole window to close. So at this point, it's possible
+ // that the binding is destructed.
+ if (this.mTabBox) {
+ this.mPanelContainer.removeChild(panel);
+ }
+
+ if (aCloseWindow)
+ this._windowIsClosing = closeWindow(true, window.warnAboutClosingWindow);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_blurTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!aTab.selected)
+ return;
+
+ if (aTab.owner &&
+ !aTab.owner.hidden &&
+ !aTab.owner.closing &&
+ Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) {
+ this.selectedTab = aTab.owner;
+ return;
+ }
+
+ // Switch to a visible tab unless there aren't any others remaining
+ let remainingTabs = this.visibleTabs;
+ let numTabs = remainingTabs.length;
+ if (numTabs == 0 || numTabs == 1 && remainingTabs[0] == aTab) {
+ remainingTabs = Array.filter(this.tabs, function(tab) {
+ return !tab.closing;
+ }, this);
+ }
+
+ // Try to find a remaining tab that comes after the given tab
+ var tab = aTab;
+ do {
+ tab = tab.nextSibling;
+ } while (tab && remainingTabs.indexOf(tab) == -1);
+
+ if (!tab) {
+ tab = aTab;
+
+ do {
+ tab = tab.previousSibling;
+ } while (tab && remainingTabs.indexOf(tab) == -1);
+ }
+
+ this.selectedTab = tab;
+ ]]>
+ </body>
+ </method>
+
+ <method name="swapBrowsersAndCloseOther">
+ <parameter name="aOurTab"/>
+ <parameter name="aOtherTab"/>
+ <body>
+ <![CDATA[
+ // Do not allow transfering a private tab to a non-private window
+ // and vice versa.
+ if (PrivateBrowsingUtils.isWindowPrivate(window) !=
+ PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerDocument.defaultView))
+ return;
+
+ let ourBrowser = this.getBrowserForTab(aOurTab);
+ let otherBrowser = aOtherTab.linkedBrowser;
+
+ // Can't swap between chrome and content processes.
+ if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser)
+ return;
+
+ // Keep the userContextId if set on other browser
+ if (otherBrowser.hasAttribute("usercontextid")) {
+ ourBrowser.setAttribute("usercontextid", otherBrowser.getAttribute("usercontextid"));
+ }
+
+ // That's gBrowser for the other window, not the tab's browser!
+ var remoteBrowser = aOtherTab.ownerDocument.defaultView.gBrowser;
+ var isPending = aOtherTab.hasAttribute("pending");
+
+ let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
+ let stateFlags = otherTabListener.mStateFlags;
+
+ // Expedite the removal of the icon if it was already scheduled.
+ if (aOtherTab._soundPlayingAttrRemovalTimer) {
+ clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
+ aOtherTab._soundPlayingAttrRemovalTimer = 0;
+ aOtherTab.removeAttribute("soundplaying");
+ remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
+ }
+
+ // First, start teardown of the other browser. Make sure to not
+ // fire the beforeunload event in the process. Close the other
+ // window if this was its last tab.
+ if (!remoteBrowser._beginRemoveTab(aOtherTab, aOurTab, true))
+ return;
+
+ let modifiedAttrs = [];
+ if (aOtherTab.hasAttribute("muted")) {
+ aOurTab.setAttribute("muted", "true");
+ aOurTab.muteReason = aOtherTab.muteReason;
+ ourBrowser.mute();
+ modifiedAttrs.push("muted");
+ }
+ if (aOtherTab.hasAttribute("soundplaying")) {
+ aOurTab.setAttribute("soundplaying", "true");
+ modifiedAttrs.push("soundplaying");
+ }
+ if (aOtherTab.hasAttribute("usercontextid")) {
+ aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
+ modifiedAttrs.push("usercontextid");
+ }
+ if (aOtherTab.hasAttribute("sharing")) {
+ aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
+ modifiedAttrs.push("sharing");
+ aOurTab._sharingState = aOtherTab._sharingState;
+ webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
+ }
+
+ // If the other tab is pending (i.e. has not been restored, yet)
+ // then do not switch docShells but retrieve the other tab's state
+ // and apply it to our tab.
+ if (isPending) {
+ SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
+
+ // Make sure to unregister any open URIs.
+ this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
+ } else {
+ // Workarounds for bug 458697
+ // Icon might have been set on DOMLinkAdded, don't override that.
+ if (!ourBrowser.mIconURL && otherBrowser.mIconURL)
+ this.setIcon(aOurTab, otherBrowser.mIconURL, otherBrowser.contentPrincipal);
+ var isBusy = aOtherTab.hasAttribute("busy");
+ if (isBusy) {
+ aOurTab.setAttribute("busy", "true");
+ modifiedAttrs.push("busy");
+ if (aOurTab.selected)
+ this.mIsBusy = true;
+ }
+
+ this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags);
+ }
+
+ // Handle findbar data (if any)
+ let otherFindBar = aOtherTab._findBar;
+ if (otherFindBar &&
+ otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
+ let ourFindBar = this.getFindBar(aOurTab);
+ ourFindBar._findField.value = otherFindBar._findField.value;
+ if (!otherFindBar.hidden)
+ ourFindBar.onFindCommand();
+ }
+
+ // Finish tearing down the tab that's going away.
+ remoteBrowser._endRemoveTab(aOtherTab);
+
+ if (isBusy)
+ this.setTabTitleLoading(aOurTab);
+ else
+ this.setTabTitle(aOurTab);
+
+ // If the tab was already selected (this happpens in the scenario
+ // of replaceTabWithWindow), notify onLocationChange, etc.
+ if (aOurTab.selected)
+ this.updateCurrentBrowser(true);
+
+ if (modifiedAttrs.length) {
+ this._tabAttrModified(aOurTab, modifiedAttrs);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_swapBrowserDocShells">
+ <parameter name="aOurTab"/>
+ <parameter name="aOtherBrowser"/>
+ <parameter name="aStateFlags"/>
+ <body>
+ <![CDATA[
+ // Unhook our progress listener
+ const filter = this._tabFilters.get(aOurTab);
+ let tabListener = this._tabListeners.get(aOurTab);
+ let ourBrowser = this.getBrowserForTab(aOurTab);
+ ourBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(tabListener);
+
+ // Make sure to unregister any open URIs.
+ this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
+
+ // Unmap old outerWindowIDs.
+ this._outerWindowIDBrowserMap.delete(ourBrowser.outerWindowID);
+ let remoteBrowser = aOtherBrowser.ownerDocument.defaultView.gBrowser;
+ if (remoteBrowser) {
+ remoteBrowser._outerWindowIDBrowserMap.delete(aOtherBrowser.outerWindowID);
+ }
+
+ // If switcher is active, it will intercept swap events and
+ // react as needed.
+ if (!this._switcher) {
+ aOtherBrowser.docShellIsActive = this.shouldActivateDocShell(ourBrowser);
+ }
+
+ // Swap the docshells
+ ourBrowser.swapDocShells(aOtherBrowser);
+
+ if (ourBrowser.isRemoteBrowser) {
+ // Switch outerWindowIDs for remote browsers.
+ let ourOuterWindowID = ourBrowser._outerWindowID;
+ ourBrowser._outerWindowID = aOtherBrowser._outerWindowID;
+ aOtherBrowser._outerWindowID = ourOuterWindowID;
+ }
+
+ // Register new outerWindowIDs.
+ this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser);
+ if (remoteBrowser) {
+ remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser);
+ }
+
+ // Swap permanentKey properties.
+ let ourPermanentKey = ourBrowser.permanentKey;
+ ourBrowser.permanentKey = aOtherBrowser.permanentKey;
+ aOtherBrowser.permanentKey = ourPermanentKey;
+ aOurTab.permanentKey = ourBrowser.permanentKey;
+ if (remoteBrowser) {
+ let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
+ if (otherTab) {
+ otherTab.permanentKey = aOtherBrowser.permanentKey;
+ }
+ }
+
+ // Restore the progress listener
+ tabListener = this.mTabProgressListener(aOurTab, ourBrowser, false, false,
+ aStateFlags);
+ this._tabListeners.set(aOurTab, tabListener);
+
+ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
+ filter.addProgressListener(tabListener, notifyAll);
+ ourBrowser.webProgress.addProgressListener(filter, notifyAll);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_swapRegisteredOpenURIs">
+ <parameter name="aOurBrowser"/>
+ <parameter name="aOtherBrowser"/>
+ <body>
+ <![CDATA[
+ // If the current URI is registered as open remove it from the list.
+ if (aOurBrowser.registeredOpenURI) {
+ this._unifiedComplete.unregisterOpenPage(aOurBrowser.registeredOpenURI,
+ aOurBrowser.getAttribute("usercontextid") || 0);
+ delete aOurBrowser.registeredOpenURI;
+ }
+
+ // If the other/new URI is registered as open then copy it over.
+ if (aOtherBrowser.registeredOpenURI) {
+ aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
+ delete aOtherBrowser.registeredOpenURI;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="reloadAllTabs">
+ <body>
+ <![CDATA[
+ let tabs = this.visibleTabs;
+ let l = tabs.length;
+ for (var i = 0; i < l; i++) {
+ try {
+ this.getBrowserForTab(tabs[i]).reload();
+ } catch (e) {
+ // ignore failure to reload so others will be reloaded
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="reloadTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ this.getBrowserForTab(aTab).reload();
+ ]]>
+ </body>
+ </method>
+
+ <method name="addProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ if (arguments.length != 1) {
+ Components.utils.reportError("gBrowser.addProgressListener was " +
+ "called with a second argument, " +
+ "which is not supported. See bug " +
+ "608628. Call stack: " + new Error().stack);
+ }
+
+ this.mProgressListeners.push(aListener);
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ this.mProgressListeners =
+ this.mProgressListeners.filter(l => l != aListener);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addTabsProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ this.mTabsProgressListeners.push(aListener);
+ </body>
+ </method>
+
+ <method name="removeTabsProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ this.mTabsProgressListeners =
+ this.mTabsProgressListeners.filter(l => l != aListener);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserForTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ return aTab.linkedBrowser;
+ ]]>
+ </body>
+ </method>
+
+ <method name="showOnlyTheseTabs">
+ <parameter name="aTabs"/>
+ <body>
+ <![CDATA[
+ for (let tab of this.tabs) {
+ if (aTabs.indexOf(tab) == -1)
+ this.hideTab(tab);
+ else
+ this.showTab(tab);
+ }
+
+ this.tabContainer._handleTabSelect(false);
+ ]]>
+ </body>
+ </method>
+
+ <method name="showTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (aTab.hidden) {
+ aTab.removeAttribute("hidden");
+ this._visibleTabs = null; // invalidate cache
+
+ this.tabContainer.adjustTabstrip();
+
+ this.tabContainer._setPositionalAttributes();
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabShow", true, false);
+ aTab.dispatchEvent(event);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="hideTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
+ !aTab.closing) {
+ aTab.setAttribute("hidden", "true");
+ this._visibleTabs = null; // invalidate cache
+
+ this.tabContainer.adjustTabstrip();
+
+ this.tabContainer._setPositionalAttributes();
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabHide", true, false);
+ aTab.dispatchEvent(event);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="selectTabAtIndex">
+ <parameter name="aIndex"/>
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ let tabs = this.visibleTabs;
+
+ // count backwards for aIndex < 0
+ if (aIndex < 0) {
+ aIndex += tabs.length;
+ // clamp at index 0 if still negative.
+ if (aIndex < 0)
+ aIndex = 0;
+ } else if (aIndex >= tabs.length) {
+ // clamp at right-most tab if out of range.
+ aIndex = tabs.length - 1;
+ }
+
+ this.selectedTab = tabs[aIndex];
+
+ if (aEvent) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="selectedTab">
+ <getter>
+ return this.mCurrentTab;
+ </getter>
+ <setter>
+ <![CDATA[
+ if (gNavToolbox.collapsed) {
+ return this.mTabBox.selectedTab;
+ }
+ // Update the tab
+ this.mTabBox.selectedTab = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedBrowser"
+ onget="return this.mCurrentBrowser;"
+ readonly="true"/>
+
+ <field name="browsers" readonly="true">
+ <![CDATA[
+ // This defines a proxy which allows us to access browsers by
+ // index without actually creating a full array of browsers.
+ new Proxy([], {
+ has: (target, name) => {
+ if (typeof name == "string" && Number.isInteger(parseInt(name))) {
+ return (name in this.tabs);
+ }
+ return false;
+ },
+ get: (target, name) => {
+ if (name == "length") {
+ return this.tabs.length;
+ }
+ if (typeof name == "string" && Number.isInteger(parseInt(name))) {
+ if (!(name in this.tabs)) {
+ return undefined;
+ }
+ return this.tabs[name].linkedBrowser;
+ }
+ return target[name];
+ }
+ });
+ ]]>
+ </field>
+
+ <!-- Moves a tab to a new browser window, unless it's already the only tab
+ in the current window, in which case this will do nothing. -->
+ <method name="replaceTabWithWindow">
+ <parameter name="aTab"/>
+ <parameter name="aOptions"/>
+ <body>
+ <![CDATA[
+ if (this.tabs.length == 1)
+ return null;
+
+ var options = "chrome,dialog=no,all";
+ for (var name in aOptions)
+ options += "," + name + "=" + aOptions[name];
+
+ // tell a new window to take the "dropped" tab
+ return window.openDialog(getBrowserURL(), "_blank", options, aTab);
+ ]]>
+ </body>
+ </method>
+
+ <!-- Opens a given tab to a non-remote window. -->
+ <method name="openNonRemoteWindow">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!this.AppConstants.E10S_TESTING_ONLY) {
+ throw "This method is intended only for e10s testing!";
+ }
+ let url = aTab.linkedBrowser.currentURI.spec;
+ return window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no,non-remote", url);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabTo">
+ <parameter name="aTab"/>
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ var oldPosition = aTab._tPos;
+ if (oldPosition == aIndex)
+ return;
+
+ // Don't allow mixing pinned and unpinned tabs.
+ if (aTab.pinned)
+ aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
+ else
+ aIndex = Math.max(aIndex, this._numPinnedTabs);
+ if (oldPosition == aIndex)
+ return;
+
+ this._lastRelatedTab = null;
+
+ let wasFocused = (document.activeElement == this.mCurrentTab);
+
+ aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1;
+
+ // invalidate cache
+ this._visibleTabs = null;
+
+ // use .item() instead of [] because dragging to the end of the strip goes out of
+ // bounds: .item() returns null (so it acts like appendChild), but [] throws
+ this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex));
+
+ for (let i = 0; i < this.tabs.length; i++) {
+ this.tabs[i]._tPos = i;
+ this.tabs[i]._selected = false;
+ }
+
+ // If we're in the midst of an async tab switch while calling
+ // moveTabTo, we can get into a case where _visuallySelected
+ // is set to true on two different tabs.
+ //
+ // What we want to do in moveTabTo is to remove logical selection
+ // from all tabs, and then re-add logical selection to mCurrentTab
+ // (and visual selection as well if we're not running with e10s, which
+ // setting _selected will do automatically).
+ //
+ // If we're running with e10s, then the visual selection will not
+ // be changed, which is fine, since if we weren't in the midst of a
+ // tab switch, the previously visually selected tab should still be
+ // correct, and if we are in the midst of a tab switch, then the async
+ // tab switcher will set the visually selected tab once the tab switch
+ // has completed.
+ this.mCurrentTab._selected = true;
+
+ if (wasFocused)
+ this.mCurrentTab.focus();
+
+ this.tabContainer._handleTabSelect(false);
+
+ if (aTab.pinned)
+ this.tabContainer._positionPinnedTabs();
+
+ this.tabContainer._setPositionalAttributes();
+
+ var evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabMove", true, false, window, oldPosition);
+ aTab.dispatchEvent(evt);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabForward">
+ <body>
+ <![CDATA[
+ let nextTab = this.mCurrentTab.nextSibling;
+ while (nextTab && nextTab.hidden)
+ nextTab = nextTab.nextSibling;
+
+ if (nextTab)
+ this.moveTabTo(this.mCurrentTab, nextTab._tPos);
+ else if (this.arrowKeysShouldWrap)
+ this.moveTabToStart();
+ ]]>
+ </body>
+ </method>
+
+ <!-- Adopts a tab from another browser window, and inserts it at aIndex -->
+ <method name="adoptTab">
+ <parameter name="aTab"/>
+ <parameter name="aIndex"/>
+ <parameter name="aSelectTab"/>
+ <body>
+ <![CDATA[
+ // Swap the dropped tab with a new one we create and then close
+ // it in the other window (making it seem to have moved between
+ // windows).
+ let params = { eventDetail: { adoptedTab: aTab } };
+ if (aTab.hasAttribute("usercontextid")) {
+ // new tab must have the same usercontextid as the old one
+ params.userContextId = aTab.getAttribute("usercontextid");
+ }
+ let newTab = this.addTab("about:blank", params);
+ let newBrowser = this.getBrowserForTab(newTab);
+ let newURL = aTab.linkedBrowser.currentURI.spec;
+
+ // If we're an e10s browser window, an exception will be thrown
+ // if we attempt to drag a non-remote browser in, so we need to
+ // ensure that the remoteness of the newly created browser is
+ // appropriate for the URL of the tab being dragged in.
+ this.updateBrowserRemotenessByURL(newBrowser, newURL);
+
+ // Stop the about:blank load.
+ newBrowser.stop();
+ // Make sure it has a docshell.
+ newBrowser.docShell;
+
+ let numPinned = this._numPinnedTabs;
+ if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
+ this.pinTab(newTab);
+ }
+
+ this.moveTabTo(newTab, aIndex);
+
+ // We need to select the tab before calling swapBrowsersAndCloseOther
+ // so that window.content in chrome windows points to the right tab
+ // when pagehide/show events are fired. This is no longer necessary
+ // for any exiting browser code, but it may be necessary for add-on
+ // compatibility.
+ if (aSelectTab) {
+ this.selectedTab = newTab;
+ }
+
+ aTab.parentNode._finishAnimateTabMove();
+ this.swapBrowsersAndCloseOther(newTab, aTab);
+
+ if (aSelectTab) {
+ // Call updateCurrentBrowser to make sure the URL bar is up to date
+ // for our new tab after we've done swapBrowsersAndCloseOther.
+ this.updateCurrentBrowser(true);
+ }
+
+ return newTab;
+ ]]>
+ </body>
+ </method>
+
+
+ <method name="moveTabBackward">
+ <body>
+ <![CDATA[
+ let previousTab = this.mCurrentTab.previousSibling;
+ while (previousTab && previousTab.hidden)
+ previousTab = previousTab.previousSibling;
+
+ if (previousTab)
+ this.moveTabTo(this.mCurrentTab, previousTab._tPos);
+ else if (this.arrowKeysShouldWrap)
+ this.moveTabToEnd();
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabToStart">
+ <body>
+ <![CDATA[
+ var tabPos = this.mCurrentTab._tPos;
+ if (tabPos > 0)
+ this.moveTabTo(this.mCurrentTab, 0);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabToEnd">
+ <body>
+ <![CDATA[
+ var tabPos = this.mCurrentTab._tPos;
+ if (tabPos < this.browsers.length - 1)
+ this.moveTabTo(this.mCurrentTab, this.browsers.length - 1);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabOver">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ var direction = window.getComputedStyle(this.parentNode, null).direction;
+ if ((direction == "ltr" && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
+ (direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT))
+ this.moveTabForward();
+ else
+ this.moveTabBackward();
+ ]]>
+ </body>
+ </method>
+
+ <method name="duplicateTab">
+ <parameter name="aTab"/><!-- can be from a different window as well -->
+ <parameter name="aRestoreTabImmediately"/><!-- can defer loading of the tab contents -->
+ <body>
+ <![CDATA[
+ return SessionStore.duplicateTab(window, aTab, 0, aRestoreTabImmediately);
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ List of browsers whose docshells must be active in order for print preview
+ to work.
+ -->
+ <field name="_printPreviewBrowsers">
+ new Set()
+ </field>
+
+ <method name="activateBrowserForPrintPreview">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ this._printPreviewBrowsers.add(aBrowser);
+ if (this._switcher) {
+ this._switcher.activateBrowserForPrintPreview(aBrowser);
+ }
+ aBrowser.docShellIsActive = true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="deactivatePrintPreviewBrowsers">
+ <body>
+ <![CDATA[
+ let browsers = this._printPreviewBrowsers;
+ this._printPreviewBrowsers = new Set();
+ for (let browser of browsers) {
+ browser.docShellIsActive = this.shouldActivateDocShell(browser);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ Returns true if a given browser's docshell should be active.
+ -->
+ <method name="shouldActivateDocShell">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ if (this._switcher) {
+ return this._switcher.shouldActivateDocShell(aBrowser);
+ }
+ return (aBrowser == this.selectedBrowser &&
+ window.windowState != window.STATE_MINIMIZED) ||
+ this._printPreviewBrowsers.has(aBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ The tab switcher is responsible for asynchronously switching
+ tabs in e10s. It waits until the new tab is ready (i.e., the
+ layer tree is available) before switching to it. Then it
+ unloads the layer tree for the old tab.
+
+ The tab switcher is a state machine. For each tab, it
+ maintains state about whether the layer tree for the tab is
+ available, being loaded, being unloaded, or unavailable. It
+ also keeps track of the tab currently being displayed, the tab
+ it's trying to load, and the tab the user has asked to switch
+ to. The switcher object is created upon tab switch. It is
+ released when there are no pending tabs to load or unload.
+
+ The following general principles have guided the design:
+
+ 1. We only request one layer tree at a time. If the user
+ switches to a different tab while waiting, we don't request
+ the new layer tree until the old tab has loaded or timed out.
+
+ 2. If loading the layers for a tab times out, we show the
+ spinner and possibly request the layer tree for another tab if
+ the user has requested one.
+
+ 3. We discard layer trees on a delay. This way, if the user is
+ switching among the same tabs frequently, we don't continually
+ load the same tabs.
+
+ It's important that we always show either the spinner or a tab
+ whose layers are available. Otherwise the compositor will draw
+ an entirely black frame, which is very jarring. To ensure this
+ never happens when switching away from a tab, we assume the
+ old tab might still be drawn until a MozAfterPaint event
+ occurs. Because layout and compositing happen asynchronously,
+ we don't have any other way of knowing when the switch
+ actually takes place. Therefore, we don't unload the old tab
+ until the next MozAfterPaint event.
+ -->
+ <field name="_switcher">null</field>
+ <method name="_getSwitcher">
+ <body><![CDATA[
+ if (this._switcher) {
+ return this._switcher;
+ }
+
+ let switcher = {
+ // How long to wait for a tab's layers to load. After this
+ // time elapses, we're free to put up the spinner and start
+ // trying to load a different tab.
+ TAB_SWITCH_TIMEOUT: 400 /* ms */,
+
+ // When the user hasn't switched tabs for this long, we unload
+ // layers for all tabs that aren't in use.
+ UNLOAD_DELAY: 300 /* ms */,
+
+ // The next three tabs form the principal state variables.
+ // See the assertions in postActions for their invariants.
+
+ // Tab the user requested most recently.
+ requestedTab: this.selectedTab,
+
+ // Tab we're currently trying to load.
+ loadingTab: null,
+
+ // We show this tab in case the requestedTab hasn't loaded yet.
+ lastVisibleTab: this.selectedTab,
+
+ // Auxilliary state variables:
+
+ visibleTab: this.selectedTab, // Tab that's on screen.
+ spinnerTab: null, // Tab showing a spinner.
+ originalTab: this.selectedTab, // Tab that we started on.
+
+ tabbrowser: this, // Reference to gBrowser.
+ loadTimer: null, // TAB_SWITCH_TIMEOUT nsITimer instance.
+ unloadTimer: null, // UNLOAD_DELAY nsITimer instance.
+
+ // Map from tabs to STATE_* (below).
+ tabState: new Map(),
+
+ // True if we're in the midst of switching tabs.
+ switchInProgress: false,
+
+ // Keep an exact list of content processes (tabParent) in which
+ // we're actively suppressing the display port. This gives a robust
+ // way to make sure we don't forget to un-suppress.
+ activeSuppressDisplayport: new Set(),
+
+ // Set of tabs that might be visible right now. We maintain
+ // this set because we can't be sure when a tab is actually
+ // drawn. A tab is added to this set when we ask to make it
+ // visible. All tabs but the most recently shown tab are
+ // removed from the set upon MozAfterPaint.
+ maybeVisibleTabs: new Set([this.selectedTab]),
+
+ STATE_UNLOADED: 0,
+ STATE_LOADING: 1,
+ STATE_LOADED: 2,
+ STATE_UNLOADING: 3,
+
+ // re-entrancy guard:
+ _processing: false,
+
+ // Wraps nsITimer. Must not use the vanilla setTimeout and
+ // clearTimeout, because they will be blocked by nsIPromptService
+ // dialogs.
+ setTimer: function(callback, timeout) {
+ let event = {
+ notify: callback
+ };
+
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+ return timer;
+ },
+
+ clearTimer: function(timer) {
+ timer.cancel();
+ },
+
+ getTabState: function(tab) {
+ let state = this.tabState.get(tab);
+ if (state === undefined) {
+ return this.STATE_UNLOADED;
+ }
+ return state;
+ },
+
+ setTabStateNoAction(tab, state) {
+ if (state == this.STATE_UNLOADED) {
+ this.tabState.delete(tab);
+ } else {
+ this.tabState.set(tab, state);
+ }
+ },
+
+ setTabState: function(tab, state) {
+ this.setTabStateNoAction(tab, state);
+
+ let browser = tab.linkedBrowser;
+ let {tabParent} = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ if (state == this.STATE_LOADING) {
+ this.assert(!this.minimized);
+ browser.docShellIsActive = true;
+ if (!tabParent) {
+ this.onLayersReady(browser);
+ }
+ } else if (state == this.STATE_UNLOADING) {
+ browser.docShellIsActive = false;
+ if (!tabParent) {
+ this.onLayersCleared(browser);
+ }
+ }
+ },
+
+ get minimized() {
+ return window.windowState == window.STATE_MINIMIZED;
+ },
+
+ init: function() {
+ this.log("START");
+
+ // If we minimized the window before the switcher was activated,
+ // we might have set the preserveLayers flag for the current
+ // browser. Let's clear it.
+ this.tabbrowser.mCurrentBrowser.preserveLayers(false);
+
+ window.addEventListener("MozAfterPaint", this);
+ window.addEventListener("MozLayerTreeReady", this);
+ window.addEventListener("MozLayerTreeCleared", this);
+ window.addEventListener("TabRemotenessChange", this);
+ window.addEventListener("sizemodechange", this);
+ window.addEventListener("SwapDocShells", this, true);
+ window.addEventListener("EndSwapDocShells", this, true);
+ if (!this.minimized) {
+ this.setTabState(this.requestedTab, this.STATE_LOADED);
+ }
+ },
+
+ destroy: function() {
+ if (this.unloadTimer) {
+ this.clearTimer(this.unloadTimer);
+ this.unloadTimer = null;
+ }
+ if (this.loadTimer) {
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ window.removeEventListener("MozAfterPaint", this);
+ window.removeEventListener("MozLayerTreeReady", this);
+ window.removeEventListener("MozLayerTreeCleared", this);
+ window.removeEventListener("TabRemotenessChange", this);
+ window.removeEventListener("sizemodechange", this);
+ window.removeEventListener("SwapDocShells", this, true);
+ window.removeEventListener("EndSwapDocShells", this, true);
+
+ this.tabbrowser._switcher = null;
+
+ this.activeSuppressDisplayport.forEach(function(tabParent) {
+ tabParent.suppressDisplayport(false);
+ });
+ this.activeSuppressDisplayport.clear();
+ },
+
+ finish: function() {
+ this.log("FINISH");
+
+ this.assert(this.tabbrowser._switcher);
+ this.assert(this.tabbrowser._switcher === this);
+ this.assert(!this.spinnerTab);
+ this.assert(!this.loadTimer);
+ this.assert(!this.loadingTab);
+ this.assert(this.lastVisibleTab === this.requestedTab);
+ this.assert(this.minimized || this.getTabState(this.requestedTab) == this.STATE_LOADED);
+
+ this.destroy();
+
+ let toBrowser = this.requestedTab.linkedBrowser;
+ toBrowser.setAttribute("type", "content-primary");
+
+ this.tabbrowser._adjustFocusAfterTabSwitch(this.requestedTab);
+
+ let fromBrowser = this.originalTab.linkedBrowser;
+ // It's possible that the tab we're switching from closed
+ // before we were able to finalize, in which case, fromBrowser
+ // doesn't exist.
+ if (fromBrowser) {
+ fromBrowser.setAttribute("type", "content-targetable");
+ }
+
+ let event = new CustomEvent("TabSwitchDone", {
+ bubbles: true,
+ cancelable: true
+ });
+ this.tabbrowser.dispatchEvent(event);
+ },
+
+ // This function is called after all the main state changes to
+ // make sure we display the right tab.
+ updateDisplay: function() {
+ // Figure out which tab we actually want visible right now.
+ let showTab = null;
+ if (this.getTabState(this.requestedTab) != this.STATE_LOADED &&
+ this.lastVisibleTab && this.loadTimer) {
+ // If we can't show the requestedTab, and lastVisibleTab is
+ // available, show it.
+ showTab = this.lastVisibleTab;
+ } else {
+ // Show the requested tab. If it's not available, we'll show the spinner.
+ showTab = this.requestedTab;
+ }
+
+ // Show or hide the spinner as needed.
+ let needSpinner = this.getTabState(showTab) != this.STATE_LOADED && !this.minimized;
+ if (!needSpinner && this.spinnerTab) {
+ this.spinnerHidden();
+ this.tabbrowser.removeAttribute("pendingpaint");
+ this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
+ this.spinnerTab = null;
+ } else if (needSpinner && this.spinnerTab !== showTab) {
+ if (this.spinnerTab) {
+ this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
+ } else {
+ this.spinnerDisplayed();
+ }
+ this.spinnerTab = showTab;
+ this.tabbrowser.setAttribute("pendingpaint", "true");
+ this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
+ }
+
+ // Switch to the tab we've decided to make visible.
+ if (this.visibleTab !== showTab) {
+ this.visibleTab = showTab;
+
+ this.maybeVisibleTabs.add(showTab);
+
+ let tabs = this.tabbrowser.mTabBox.tabs;
+ let tabPanel = this.tabbrowser.mPanelContainer;
+ let showPanel = tabs.getRelatedElement(showTab);
+ let index = Array.indexOf(tabPanel.childNodes, showPanel);
+ if (index != -1) {
+ this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
+ tabPanel.setAttribute("selectedIndex", index);
+ if (showTab === this.requestedTab) {
+ this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
+ }
+ }
+
+ // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
+ if (this.lastVisibleTab)
+ this.lastVisibleTab._visuallySelected = false;
+
+ this.visibleTab._visuallySelected = true;
+ }
+
+ this.lastVisibleTab = this.visibleTab;
+ },
+
+ assert: function(cond) {
+ if (!cond) {
+ dump("Assertion failure\n" + Error().stack);
+
+ // Don't break a user's browser if an assertion fails.
+ if (this.tabbrowser.AppConstants.DEBUG) {
+ throw new Error("Assertion failure");
+ }
+ }
+ },
+
+ // We've decided to try to load requestedTab.
+ loadRequestedTab: function() {
+ this.assert(!this.loadTimer);
+ this.assert(!this.minimized);
+
+ // loadingTab can be non-null here if we timed out loading the current tab.
+ // In that case we just overwrite it with a different tab; it's had its chance.
+ this.loadingTab = this.requestedTab;
+ this.log("Loading tab " + this.tinfo(this.loadingTab));
+
+ this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
+ this.setTabState(this.requestedTab, this.STATE_LOADING);
+ },
+
+ // This function runs before every event. It fixes up the state
+ // to account for closed tabs.
+ preActions: function() {
+ this.assert(this.tabbrowser._switcher);
+ this.assert(this.tabbrowser._switcher === this);
+
+ for (let [tab, ] of this.tabState) {
+ if (!tab.linkedBrowser) {
+ this.tabState.delete(tab);
+ }
+ }
+
+ if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
+ this.lastVisibleTab = null;
+ }
+ if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
+ this.spinnerHidden();
+ this.spinnerTab = null;
+ }
+ if (this.loadingTab && !this.loadingTab.linkedBrowser) {
+ this.loadingTab = null;
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ }
+ },
+
+ // This code runs after we've responded to an event or requested a new
+ // tab. It's expected that we've already updated all the principal
+ // state variables. This function takes care of updating any auxilliary
+ // state.
+ postActions: function() {
+ // Once we finish loading loadingTab, we null it out. So the state should
+ // always be LOADING.
+ this.assert(!this.loadingTab ||
+ this.getTabState(this.loadingTab) == this.STATE_LOADING);
+
+ // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
+ // the timer is set only when we're loading something.
+ this.assert(!this.loadTimer || this.loadingTab);
+ this.assert(!this.loadingTab || this.loadTimer);
+
+ // If we're not loading anything, try loading the requested tab.
+ let requestedState = this.getTabState(this.requestedTab);
+ if (!this.loadTimer && !this.minimized &&
+ (requestedState == this.STATE_UNLOADED ||
+ requestedState == this.STATE_UNLOADING)) {
+ this.loadRequestedTab();
+ }
+
+ // See how many tabs still have work to do.
+ let numPending = 0;
+ for (let [tab, state] of this.tabState) {
+ // Skip print preview browsers since they shouldn't affect tab switching.
+ if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ continue;
+ }
+
+ if (state == this.STATE_LOADED && tab !== this.requestedTab) {
+ numPending++;
+ }
+ if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
+ numPending++;
+ }
+ }
+
+ this.updateDisplay();
+
+ // It's possible for updateDisplay to trigger one of our own event
+ // handlers, which might cause finish() to already have been called.
+ // Check for that before calling finish() again.
+ if (!this.tabbrowser._switcher) {
+ return;
+ }
+
+ if (numPending == 0) {
+ this.finish();
+ }
+
+ this.logState("done");
+ },
+
+ // Fires when we're ready to unload unused tabs.
+ onUnloadTimeout: function() {
+ this.logState("onUnloadTimeout");
+ this.unloadTimer = null;
+ this.preActions();
+
+ let numPending = 0;
+
+ // Unload any tabs that can be unloaded.
+ for (let [tab, state] of this.tabState) {
+ if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ continue;
+ }
+
+ if (state == this.STATE_LOADED &&
+ !this.maybeVisibleTabs.has(tab) &&
+ tab !== this.lastVisibleTab &&
+ tab !== this.loadingTab &&
+ tab !== this.requestedTab)
+ {
+ this.setTabState(tab, this.STATE_UNLOADING);
+ }
+
+ if (state != this.STATE_UNLOADED && tab !== this.requestedTab) {
+ numPending++;
+ }
+ }
+
+ if (numPending) {
+ // Keep the timer going since there may be more tabs to unload.
+ this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
+ }
+
+ this.postActions();
+ },
+
+ // Fires when an ongoing load has taken too long.
+ onLoadTimeout: function() {
+ this.logState("onLoadTimeout");
+ this.preActions();
+ this.loadTimer = null;
+ this.loadingTab = null;
+ this.postActions();
+ },
+
+ // Fires when the layers become available for a tab.
+ onLayersReady: function(browser) {
+ let tab = this.tabbrowser.getTabForBrowser(browser);
+ this.logState(`onLayersReady(${tab._tPos})`);
+
+ this.assert(this.getTabState(tab) == this.STATE_LOADING ||
+ this.getTabState(tab) == this.STATE_LOADED);
+ this.setTabState(tab, this.STATE_LOADED);
+
+ this.maybeFinishTabSwitch();
+
+ if (this.loadingTab === tab) {
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ this.loadingTab = null;
+ }
+ },
+
+ // Fires when we paint the screen. Any tab switches we initiated
+ // previously are done, so there's no need to keep the old layers
+ // around.
+ onPaint: function() {
+ this.maybeVisibleTabs.clear();
+ this.maybeFinishTabSwitch();
+ },
+
+ // Called when we're done clearing the layers for a tab.
+ onLayersCleared: function(browser) {
+ let tab = this.tabbrowser.getTabForBrowser(browser);
+ if (tab) {
+ this.logState(`onLayersCleared(${tab._tPos})`);
+ this.assert(this.getTabState(tab) == this.STATE_UNLOADING ||
+ this.getTabState(tab) == this.STATE_UNLOADED);
+ this.setTabState(tab, this.STATE_UNLOADED);
+ }
+ },
+
+ // Called when a tab switches from remote to non-remote. In this case
+ // a MozLayerTreeReady notification that we requested may never fire,
+ // so we need to simulate it.
+ onRemotenessChange: function(tab) {
+ this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`);
+ if (!tab.linkedBrowser.isRemoteBrowser) {
+ if (this.getTabState(tab) == this.STATE_LOADING) {
+ this.onLayersReady(tab.linkedBrowser);
+ } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
+ this.onLayersCleared(tab.linkedBrowser);
+ }
+ }
+ },
+
+ // Called when a tab has been removed, and the browser node is
+ // about to be removed from the DOM.
+ onTabRemoved: function(tab) {
+ if (this.lastVisibleTab == tab) {
+ // The browser that was being presented to the user is
+ // going to be removed during this tick of the event loop.
+ // This will cause us to show a tab spinner instead.
+ this.preActions();
+ this.lastVisibleTab = null;
+ this.postActions();
+ }
+ },
+
+ onSizeModeChange() {
+ if (this.minimized) {
+ for (let [tab, state] of this.tabState) {
+ // Skip print preview browsers since they shouldn't affect tab switching.
+ if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ continue;
+ }
+
+ if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
+ this.setTabState(tab, this.STATE_UNLOADING);
+ }
+ }
+ if (this.loadTimer) {
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ }
+ this.loadingTab = null;
+ } else {
+ // Do nothing. We'll automatically start loading the requested tab in
+ // postActions.
+ }
+ },
+
+ onSwapDocShells(ourBrowser, otherBrowser) {
+ // This event fires before the swap. ourBrowser is from
+ // our window. We save the state of otherBrowser since ourBrowser
+ // needs to take on that state at the end of the swap.
+
+ let otherTabbrowser = otherBrowser.ownerDocument.defaultView.gBrowser;
+ let otherState;
+ if (otherTabbrowser && otherTabbrowser._switcher) {
+ let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
+ otherState = otherTabbrowser._switcher.getTabState(otherTab);
+ } else {
+ otherState = (otherBrowser.docShellIsActive
+ ? this.STATE_LOADED
+ : this.STATE_UNLOADED);
+ }
+
+ if (!this.swapMap) {
+ this.swapMap = new WeakMap();
+ }
+ this.swapMap.set(otherBrowser, otherState);
+ },
+
+ onEndSwapDocShells(ourBrowser, otherBrowser) {
+ // The swap has happened. We reset the loadingTab in
+ // case it has been swapped. We also set ourBrowser's state
+ // to whatever otherBrowser's state was before the swap.
+
+ if (this.loadTimer) {
+ // Clearing the load timer means that we will
+ // immediately display a spinner if ourBrowser isn't
+ // ready yet. Typically it will already be ready
+ // though. If it's not, we're probably in a new window,
+ // in which case we have no other tabs to display anyway.
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ }
+ this.loadingTab = null;
+
+ let otherState = this.swapMap.get(otherBrowser);
+ this.swapMap.delete(otherBrowser);
+
+ let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
+ if (ourTab) {
+ this.setTabStateNoAction(ourTab, otherState);
+ }
+ },
+
+ shouldActivateDocShell(browser) {
+ let tab = this.tabbrowser.getTabForBrowser(browser);
+ let state = this.getTabState(tab);
+ return state == this.STATE_LOADING || state == this.STATE_LOADED;
+ },
+
+ activateBrowserForPrintPreview(browser) {
+ let tab = this.tabbrowser.getTabForBrowser(browser);
+ this.setTabState(tab, this.STATE_LOADING);
+ },
+
+ // Called when the user asks to switch to a given tab.
+ requestTab: function(tab) {
+ if (tab === this.requestedTab) {
+ return;
+ }
+
+ this.logState("requestTab " + this.tinfo(tab));
+ this.startTabSwitch();
+
+ this.requestedTab = tab;
+
+ let browser = this.requestedTab.linkedBrowser;
+ let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) {
+ fl.tabParent.suppressDisplayport(true);
+ this.activeSuppressDisplayport.add(fl.tabParent);
+ }
+
+ this.preActions();
+
+ if (this.unloadTimer) {
+ this.clearTimer(this.unloadTimer);
+ }
+ this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
+
+ this.postActions();
+ },
+
+ handleEvent: function(event, delayed = false) {
+ if (this._processing) {
+ this.setTimer(() => this.handleEvent(event, true), 0);
+ return;
+ }
+ if (delayed && this.tabbrowser._switcher != this) {
+ // if we delayed processing this event, we might be out of date, in which
+ // case we drop the delayed events
+ return;
+ }
+ this._processing = true;
+ this.preActions();
+
+ if (event.type == "MozLayerTreeReady") {
+ this.onLayersReady(event.originalTarget);
+ } if (event.type == "MozAfterPaint") {
+ this.onPaint();
+ } else if (event.type == "MozLayerTreeCleared") {
+ this.onLayersCleared(event.originalTarget);
+ } else if (event.type == "TabRemotenessChange") {
+ this.onRemotenessChange(event.target);
+ } else if (event.type == "sizemodechange") {
+ this.onSizeModeChange();
+ } else if (event.type == "SwapDocShells") {
+ this.onSwapDocShells(event.originalTarget, event.detail);
+ } else if (event.type == "EndSwapDocShells") {
+ this.onEndSwapDocShells(event.originalTarget, event.detail);
+ }
+
+ this.postActions();
+ this._processing = false;
+ },
+
+ /*
+ * Telemetry and Profiler related helpers for recording tab switch
+ * timing.
+ */
+
+ startTabSwitch: function () {
+ TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
+ TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
+ this.addMarker("AsyncTabSwitch:Start");
+ this.switchInProgress = true;
+ },
+
+ /**
+ * Something has occurred that might mean that we've completed
+ * the tab switch (layers are ready, paints are done, spinners
+ * are hidden). This checks to make sure all conditions are
+ * satisfied, and then records the tab switch as finished.
+ */
+ maybeFinishTabSwitch: function () {
+ if (this.switchInProgress && this.requestedTab &&
+ this.getTabState(this.requestedTab) == this.STATE_LOADED) {
+ // After this point the tab has switched from the content thread's point of view.
+ // The changes will be visible after the next refresh driver tick + composite.
+ let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
+ if (time != -1) {
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
+ this.log("DEBUG: tab switch time = " + time);
+ this.addMarker("AsyncTabSwitch:Finish");
+ }
+ this.switchInProgress = false;
+ }
+ },
+
+ spinnerDisplayed: function () {
+ this.assert(!this.spinnerTab);
+ TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
+ // We have a second, similar probe for capturing recordings of
+ // when the spinner is displayed for very long periods.
+ TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
+ this.addMarker("AsyncTabSwitch:SpinnerShown");
+ },
+
+ spinnerHidden: function () {
+ this.assert(this.spinnerTab);
+ this.log("DEBUG: spinner time = " +
+ TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window));
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
+ this.addMarker("AsyncTabSwitch:SpinnerHidden");
+ // we do not get a onPaint after displaying the spinner
+ this.maybeFinishTabSwitch();
+ },
+
+ addMarker: function(marker) {
+ if (Services.profiler) {
+ Services.profiler.AddMarker(marker);
+ }
+ },
+
+ /*
+ * Debug related logging for switcher.
+ */
+
+ _useDumpForLogging: false,
+ _logInit: false,
+
+ logging: function () {
+ if (this._useDumpForLogging)
+ return true;
+ if (this._logInit)
+ return this._shouldLog;
+ let result = false;
+ try {
+ result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming");
+ } catch (ex) {
+ }
+ this._shouldLog = result;
+ this._logInit = true;
+ return this._shouldLog;
+ },
+
+ tinfo: function(tab) {
+ if (tab) {
+ return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
+ }
+ return "null";
+ },
+
+ log: function(s) {
+ if (!this.logging())
+ return;
+ if (this._useDumpForLogging) {
+ dump(s + "\n");
+ } else {
+ Services.console.logStringMessage(s);
+ }
+ },
+
+ logState: function(prefix) {
+ if (!this.logging())
+ return;
+
+ let accum = prefix + " ";
+ for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
+ let tab = this.tabbrowser.tabs[i];
+ let state = this.getTabState(tab);
+
+ accum += i + ":";
+ if (tab === this.lastVisibleTab) accum += "V";
+ if (tab === this.loadingTab) accum += "L";
+ if (tab === this.requestedTab) accum += "R";
+ if (state == this.STATE_LOADED) accum += "(+)";
+ if (state == this.STATE_LOADING) accum += "(+?)";
+ if (state == this.STATE_UNLOADED) accum += "(-)";
+ if (state == this.STATE_UNLOADING) accum += "(-?)";
+ accum += " ";
+ }
+ if (this._useDumpForLogging) {
+ dump(accum + "\n");
+ } else {
+ Services.console.logStringMessage(accum);
+ }
+ },
+ };
+ this._switcher = switcher;
+ switcher.init();
+ return switcher;
+ ]]></body>
+ </method>
+
+ <!-- BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
+ MAKE SURE TO ADD IT HERE AS WELL. -->
+ <property name="canGoBack"
+ onget="return this.mCurrentBrowser.canGoBack;"
+ readonly="true"/>
+
+ <property name="canGoForward"
+ onget="return this.mCurrentBrowser.canGoForward;"
+ readonly="true"/>
+
+ <method name="goBack">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.goBack();
+ ]]>
+ </body>
+ </method>
+
+ <method name="goForward">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.goForward();
+ ]]>
+ </body>
+ </method>
+
+ <method name="reload">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.reload();
+ ]]>
+ </body>
+ </method>
+
+ <method name="reloadWithFlags">
+ <parameter name="aFlags"/>
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.reloadWithFlags(aFlags);
+ ]]>
+ </body>
+ </method>
+
+ <method name="stop">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.stop();
+ ]]>
+ </body>
+ </method>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURI">
+ <parameter name="aURI"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.loadURI(aURI, aReferrerURI, aCharset);
+ ]]>
+ </body>
+ </method>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURIWithFlags">
+ <parameter name="aURI"/>
+ <parameter name="aFlags"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <body>
+ <![CDATA[
+ // Note - the callee understands both:
+ // (a) loadURIWithFlags(aURI, aFlags, ...)
+ // (b) loadURIWithFlags(aURI, { flags: aFlags, ... })
+ // Forwarding it as (a) here actually supports both (a) and (b),
+ // so you can call us either way too.
+ return this.mCurrentBrowser.loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData);
+ ]]>
+ </body>
+ </method>
+
+ <method name="goHome">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.goHome();
+ ]]>
+ </body>
+ </method>
+
+ <property name="homePage">
+ <getter>
+ <![CDATA[
+ return this.mCurrentBrowser.homePage;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this.mCurrentBrowser.homePage = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="gotoIndex">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.gotoIndex(aIndex);
+ ]]>
+ </body>
+ </method>
+
+ <property name="currentURI"
+ onget="return this.mCurrentBrowser.currentURI;"
+ readonly="true"/>
+
+ <property name="finder"
+ onget="return this.mCurrentBrowser.finder"
+ readonly="true"/>
+
+ <property name="docShell"
+ onget="return this.mCurrentBrowser.docShell"
+ readonly="true"/>
+
+ <property name="webNavigation"
+ onget="return this.mCurrentBrowser.webNavigation"
+ readonly="true"/>
+
+ <property name="webBrowserFind"
+ readonly="true"
+ onget="return this.mCurrentBrowser.webBrowserFind"/>
+
+ <property name="webProgress"
+ readonly="true"
+ onget="return this.mCurrentBrowser.webProgress"/>
+
+ <property name="contentWindow"
+ readonly="true"
+ onget="return this.mCurrentBrowser.contentWindow"/>
+
+ <property name="contentWindowAsCPOW"
+ readonly="true"
+ onget="return this.mCurrentBrowser.contentWindowAsCPOW"/>
+
+ <property name="sessionHistory"
+ onget="return this.mCurrentBrowser.sessionHistory;"
+ readonly="true"/>
+
+ <property name="markupDocumentViewer"
+ onget="return this.mCurrentBrowser.markupDocumentViewer;"
+ readonly="true"/>
+
+ <property name="contentViewerEdit"
+ onget="return this.mCurrentBrowser.contentViewerEdit;"
+ readonly="true"/>
+
+ <property name="contentViewerFile"
+ onget="return this.mCurrentBrowser.contentViewerFile;"
+ readonly="true"/>
+
+ <property name="contentDocument"
+ onget="return this.mCurrentBrowser.contentDocument;"
+ readonly="true"/>
+
+ <property name="contentDocumentAsCPOW"
+ onget="return this.mCurrentBrowser.contentDocumentAsCPOW;"
+ readonly="true"/>
+
+ <property name="contentTitle"
+ onget="return this.mCurrentBrowser.contentTitle;"
+ readonly="true"/>
+
+ <property name="contentPrincipal"
+ onget="return this.mCurrentBrowser.contentPrincipal;"
+ readonly="true"/>
+
+ <property name="securityUI"
+ onget="return this.mCurrentBrowser.securityUI;"
+ readonly="true"/>
+
+ <property name="fullZoom"
+ onget="return this.mCurrentBrowser.fullZoom;"
+ onset="this.mCurrentBrowser.fullZoom = val;"/>
+
+ <property name="textZoom"
+ onget="return this.mCurrentBrowser.textZoom;"
+ onset="this.mCurrentBrowser.textZoom = val;"/>
+
+ <property name="isSyntheticDocument"
+ onget="return this.mCurrentBrowser.isSyntheticDocument;"
+ readonly="true"/>
+
+ <method name="_handleKeyDownEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (!aEvent.isTrusted) {
+ // Don't let untrusted events mess with tabs.
+ return;
+ }
+
+ if (aEvent.altKey)
+ return;
+
+ // Don't check if the event was already consumed because tab
+ // navigation should always work for better user experience.
+
+ if (aEvent.ctrlKey && aEvent.shiftKey && !aEvent.metaKey) {
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_VK_PAGE_UP:
+ this.moveTabBackward();
+ aEvent.preventDefault();
+ return;
+ case aEvent.DOM_VK_PAGE_DOWN:
+ this.moveTabForward();
+ aEvent.preventDefault();
+ return;
+ }
+ }
+
+ if (this.AppConstants.platform != "macosx") {
+ if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey &&
+ aEvent.keyCode == KeyEvent.DOM_VK_F4 &&
+ !this.mCurrentTab.pinned) {
+ this.removeCurrentTab({animate: true});
+ aEvent.preventDefault();
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_handleKeyPressEventMac">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (!aEvent.isTrusted) {
+ // Don't let untrusted events mess with tabs.
+ return;
+ }
+
+ if (aEvent.altKey)
+ return;
+
+ if (this.AppConstants.platform == "macosx") {
+ if (!aEvent.metaKey)
+ return;
+
+ var offset = 1;
+ switch (aEvent.charCode) {
+ case '}'.charCodeAt(0):
+ offset = -1;
+ case '{'.charCodeAt(0):
+ if (window.getComputedStyle(this, null).direction == "ltr")
+ offset *= -1;
+ this.tabContainer.advanceSelectedTab(offset, true);
+ aEvent.preventDefault();
+ }
+ }
+ ]]></body>
+ </method>
+
+ <property name="userTypedValue"
+ onget="return this.mCurrentBrowser.userTypedValue;"
+ onset="return this.mCurrentBrowser.userTypedValue = val;"/>
+
+ <method name="createTooltip">
+ <parameter name="event"/>
+ <body><![CDATA[
+ event.stopPropagation();
+ var tab = document.tooltipNode;
+ if (tab.localName != "tab") {
+ event.preventDefault();
+ return;
+ }
+
+ let stringWithShortcut = (stringId, keyElemId) => {
+ let keyElem = document.getElementById(keyElemId);
+ let shortcut = ShortcutUtils.prettifyShortcut(keyElem);
+ return this.mStringBundle.getFormattedString(stringId, [shortcut]);
+ };
+
+ var label;
+ if (tab.mOverCloseButton) {
+ label = tab.selected ?
+ stringWithShortcut("tabs.closeSelectedTab.tooltip", "key_close") :
+ this.mStringBundle.getString("tabs.closeTab.tooltip");
+ } else if (tab._overPlayingIcon) {
+ let stringID;
+ if (tab.selected) {
+ stringID = tab.linkedBrowser.audioMuted ?
+ "tabs.unmuteAudio.tooltip" :
+ "tabs.muteAudio.tooltip";
+ label = stringWithShortcut(stringID, "key_toggleMute");
+ } else {
+ if (tab.linkedBrowser.audioBlocked) {
+ stringID = "tabs.unblockAudio.tooltip";
+ } else {
+ stringID = tab.linkedBrowser.audioMuted ?
+ "tabs.unmuteAudio.background.tooltip" :
+ "tabs.muteAudio.background.tooltip";
+ }
+
+ label = this.mStringBundle.getString(stringID);
+ }
+ } else {
+ label = tab.getAttribute("label") +
+ (this.AppConstants.E10S_TESTING_ONLY && tab.linkedBrowser && tab.linkedBrowser.isRemoteBrowser ? " - e10s" : "");
+ }
+ event.target.setAttribute("label", label);
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.type) {
+ case "keydown":
+ this._handleKeyDownEvent(aEvent);
+ break;
+ case "keypress":
+ this._handleKeyPressEventMac(aEvent);
+ break;
+ case "sizemodechange":
+ if (aEvent.target == window && !this._switcher) {
+ this.mCurrentBrowser.preserveLayers(window.windowState == window.STATE_MINIMIZED);
+ this.mCurrentBrowser.docShellIsActive = this.shouldActivateDocShell(this.mCurrentBrowser);
+ }
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="receiveMessage">
+ <parameter name="aMessage"/>
+ <body><![CDATA[
+ let data = aMessage.data;
+ let browser = aMessage.target;
+
+ switch (aMessage.name) {
+ case "DOMTitleChanged": {
+ let tab = this.getTabForBrowser(browser);
+ if (!tab || tab.hasAttribute("pending"))
+ return undefined;
+ let titleChanged = this.setTabTitle(tab);
+ if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
+ tab.setAttribute("titlechanged", "true");
+ break;
+ }
+ case "DOMWindowClose": {
+ if (this.tabs.length == 1) {
+ // We already did PermitUnload in the content process
+ // for this tab (the only one in the window). So we don't
+ // need to do it again for any tabs.
+ window.skipNextCanClose = true;
+ window.close();
+ return undefined;
+ }
+
+ let tab = this.getTabForBrowser(browser);
+ if (tab) {
+ // Skip running PermitUnload since it already happened in
+ // the content process.
+ this.removeTab(tab, {skipPermitUnload: true});
+ }
+ break;
+ }
+ case "contextmenu": {
+ let spellInfo = data.spellInfo;
+ if (spellInfo)
+ spellInfo.target = aMessage.target.messageManager;
+ let documentURIObject = makeURI(data.docLocation,
+ data.charSet,
+ makeURI(data.baseURI));
+ gContextMenuContentData = { isRemote: true,
+ event: aMessage.objects.event,
+ popupNode: aMessage.objects.popupNode,
+ browser: browser,
+ editFlags: data.editFlags,
+ spellInfo: spellInfo,
+ principal: data.principal,
+ customMenuItems: data.customMenuItems,
+ addonInfo: data.addonInfo,
+ documentURIObject: documentURIObject,
+ docLocation: data.docLocation,
+ charSet: data.charSet,
+ referrer: data.referrer,
+ referrerPolicy: data.referrerPolicy,
+ contentType: data.contentType,
+ contentDisposition: data.contentDisposition,
+ frameOuterWindowID: data.frameOuterWindowID,
+ selectionInfo: data.selectionInfo,
+ disableSetDesktopBackground: data.disableSetDesktopBg,
+ loginFillInfo: data.loginFillInfo,
+ parentAllowsMixedContent: data.parentAllowsMixedContent,
+ userContextId: data.userContextId,
+ };
+ let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
+ let event = gContextMenuContentData.event;
+ popup.openPopupAtScreen(event.screenX, event.screenY, true);
+ break;
+ }
+ case "DOMServiceWorkerFocusClient":
+ case "DOMWebNotificationClicked": {
+ let tab = this.getTabForBrowser(browser);
+ if (!tab)
+ return undefined;
+ this.selectedTab = tab;
+ window.focus();
+ break;
+ }
+ case "Browser:Init": {
+ let tab = this.getTabForBrowser(browser);
+ if (!tab)
+ return undefined;
+
+ this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
+ browser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned })
+ break;
+ }
+ case "Browser:WindowCreated": {
+ let tab = this.getTabForBrowser(browser);
+ if (tab && data.userContextId) {
+ ContextualIdentityService.telemetry(data.userContextId);
+ tab.setUserContextId(data.userContextId);
+ }
+
+ // We don't want to update the container icon and identifier if
+ // this is not the selected browser.
+ if (browser == gBrowser.selectedBrowser) {
+ updateUserContextUIIndicator();
+ }
+
+ break;
+ }
+ case "Findbar:Keypress": {
+ let tab = this.getTabForBrowser(browser);
+ // If the find bar for this tab is not yet alive, only initialize
+ // it if there's a possibility FindAsYouType will be used.
+ // There's no point in doing it for most random keypresses.
+ if (!this.isFindBarInitialized(tab) &&
+ data.shouldFastFind) {
+ let shouldFastFind = this._findAsYouType;
+ if (!shouldFastFind) {
+ // Please keep in sync with toolkit/content/widgets/findbar.xml
+ const FAYT_LINKS_KEY = "'";
+ const FAYT_TEXT_KEY = "/";
+ let charCode = data.fakeEvent.charCode;
+ let key = charCode ? String.fromCharCode(charCode) : null;
+ shouldFastFind = key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY;
+ }
+ if (shouldFastFind) {
+ // Make sure we return the result.
+ return this.getFindBar(tab).receiveMessage(aMessage);
+ }
+ }
+ break;
+ }
+ case "RefreshBlocker:Blocked": {
+ let event = new CustomEvent("RefreshBlocked", {
+ bubbles: true,
+ cancelable: false,
+ detail: data,
+ });
+
+ browser.dispatchEvent(event);
+
+ break;
+ }
+
+ }
+ return undefined;
+ ]]></body>
+ </method>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body><![CDATA[
+ let browser;
+ switch (aTopic) {
+ case "live-resize-start":
+ browser = this.mCurrentTab.linkedBrowser;
+ let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ if (fl && fl.tabParent && !this.mActiveResizeDisplayportSuppression) {
+ fl.tabParent.suppressDisplayport(true);
+ this.mActiveResizeDisplayportSuppression = browser;
+ }
+ break;
+ case "live-resize-end":
+ browser = this.mActiveResizeDisplayportSuppression;
+ if (browser) {
+ let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ if (fl && fl.tabParent) {
+ fl.tabParent.suppressDisplayport(false);
+ this.mActiveResizeDisplayportSuppression = null;
+ }
+ }
+ break;
+ case "nsPref:changed":
+ // This is the only pref observed.
+ this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <constructor>
+ <![CDATA[
+ this.mCurrentBrowser = document.getAnonymousElementByAttribute(this, "anonid", "initialBrowser");
+ this.mCurrentBrowser.permanentKey = {};
+
+ Services.obs.addObserver(this, "live-resize-start", false);
+ Services.obs.addObserver(this, "live-resize-end", false);
+
+ this.mCurrentTab = this.tabContainer.firstChild;
+ const nsIEventListenerService =
+ Components.interfaces.nsIEventListenerService;
+ let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
+ .getService(nsIEventListenerService);
+ els.addSystemEventListener(document, "keydown", this, false);
+ if (this.AppConstants.platform == "macosx") {
+ els.addSystemEventListener(document, "keypress", this, false);
+ }
+ window.addEventListener("sizemodechange", this, false);
+
+ var uniqueId = this._generateUniquePanelID();
+ this.mPanelContainer.childNodes[0].id = uniqueId;
+ this.mCurrentTab.linkedPanel = uniqueId;
+ this.mCurrentTab.permanentKey = this.mCurrentBrowser.permanentKey;
+ this.mCurrentTab._tPos = 0;
+ this.mCurrentTab._fullyOpen = true;
+ this.mCurrentTab.cachePosition = 0;
+ this.mCurrentTab.linkedBrowser = this.mCurrentBrowser;
+ this.mCurrentTab.hasBrowser = true;
+ this._tabForBrowser.set(this.mCurrentBrowser, this.mCurrentTab);
+
+ // set up the shared autoscroll popup
+ this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
+ this._autoScrollPopup.id = "autoscroller";
+ this.appendChild(this._autoScrollPopup);
+ this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
+ this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
+ this.updateWindowResizers();
+
+ // Hook up the event listeners to the first browser
+ var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true, false);
+ const nsIWebProgress = Components.interfaces.nsIWebProgress;
+ const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
+ .createInstance(nsIWebProgress);
+ filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
+ this._tabListeners.set(this.mCurrentTab, tabListener);
+ this._tabFilters.set(this.mCurrentTab, filter);
+ this.webProgress.addProgressListener(filter, nsIWebProgress.NOTIFY_ALL);
+
+ this.style.backgroundColor =
+ Services.prefs.getBoolPref("browser.display.use_system_colors") ?
+ "-moz-default-background-color" :
+ Services.prefs.getCharPref("browser.display.background_color");
+
+ let remote = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext)
+ .useRemoteTabs;
+ if (remote) {
+ messageManager.addMessageListener("DOMTitleChanged", this);
+ messageManager.addMessageListener("DOMWindowClose", this);
+ messageManager.addMessageListener("contextmenu", this);
+ messageManager.addMessageListener("Browser:Init", this);
+
+ // If this window has remote tabs, switch to our tabpanels fork
+ // which does asynchronous tab switching.
+ this.mPanelContainer.classList.add("tabbrowser-tabpanels");
+ } else {
+ this._outerWindowIDBrowserMap.set(this.mCurrentBrowser.outerWindowID,
+ this.mCurrentBrowser);
+ }
+ messageManager.addMessageListener("DOMWebNotificationClicked", this);
+ messageManager.addMessageListener("DOMServiceWorkerFocusClient", this);
+ messageManager.addMessageListener("RefreshBlocker:Blocked", this);
+ messageManager.addMessageListener("Browser:WindowCreated", this);
+
+ // To correctly handle keypresses for potential FindAsYouType, while
+ // the tab's find bar is not yet initialized.
+ this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
+ Services.prefs.addObserver("accessibility.typeaheadfind", this, false);
+ messageManager.addMessageListener("Findbar:Keypress", this);
+ ]]>
+ </constructor>
+
+ <method name="_generateUniquePanelID">
+ <body><![CDATA[
+ if (!this._uniquePanelIDCounter) {
+ this._uniquePanelIDCounter = 0;
+ }
+
+ let outerID = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+
+ // We want panel IDs to be globally unique, that's why we include the
+ // window ID. We switched to a monotonic counter as Date.now() lead
+ // to random failures because of colliding IDs.
+ return "panel-" + outerID + "-" + (++this._uniquePanelIDCounter);
+ ]]></body>
+ </method>
+
+ <destructor>
+ <![CDATA[
+ Services.obs.removeObserver(this, "live-resize-start", false);
+ Services.obs.removeObserver(this, "live-resize-end", false);
+
+ for (let tab of this.tabs) {
+ let browser = tab.linkedBrowser;
+ if (browser.registeredOpenURI) {
+ this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
+ browser.getAttribute("usercontextid") || 0);
+ delete browser.registeredOpenURI;
+ }
+ let filter = this._tabFilters.get(tab);
+ let listener = this._tabListeners.get(tab);
+
+ browser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(listener);
+ listener.destroy();
+
+ this._tabFilters.delete(tab);
+ this._tabListeners.delete(tab);
+ }
+ const nsIEventListenerService =
+ Components.interfaces.nsIEventListenerService;
+ let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
+ .getService(nsIEventListenerService);
+ els.removeSystemEventListener(document, "keydown", this, false);
+ if (this.AppConstants.platform == "macosx") {
+ els.removeSystemEventListener(document, "keypress", this, false);
+ }
+ window.removeEventListener("sizemodechange", this, false);
+
+ if (gMultiProcessBrowser) {
+ messageManager.removeMessageListener("DOMTitleChanged", this);
+ messageManager.removeMessageListener("contextmenu", this);
+
+ if (this._switcher) {
+ this._switcher.destroy();
+ }
+ }
+
+ Services.prefs.removeObserver("accessibility.typeaheadfind", this);
+ ]]>
+ </destructor>
+
+ <!-- Deprecated stuff, implemented for backwards compatibility. -->
+ <method name="enterTabbedMode">
+ <body>
+ Services.console.logStringMessage("enterTabbedMode is an obsolete method and " +
+ "will be removed in a future release.");
+ </body>
+ </method>
+ <field name="mTabbedMode" readonly="true">true</field>
+ <method name="setStripVisibilityTo">
+ <parameter name="aShow"/>
+ <body>
+ this.tabContainer.visible = aShow;
+ </body>
+ </method>
+ <method name="getStripVisibility">
+ <body>
+ return this.tabContainer.visible;
+ </body>
+ </method>
+
+ <property name="mContextTab" readonly="true"
+ onget="return TabContextMenu.contextTab;"/>
+ <property name="mPrefs" readonly="true"
+ onget="return Services.prefs;"/>
+ <property name="mTabContainer" readonly="true"
+ onget="return this.tabContainer;"/>
+ <property name="mTabs" readonly="true"
+ onget="return this.tabs;"/>
+ <!--
+ - Compatibility hack: several extensions depend on this property to
+ - access the tab context menu or tab container, so keep that working for
+ - now. Ideally we can remove this once extensions are using
+ - tabbrowser.tabContextMenu and tabbrowser.tabContainer directly.
+ -->
+ <property name="mStrip" readonly="true">
+ <getter>
+ <![CDATA[
+ return ({
+ self: this,
+ childNodes: [null, this.tabContextMenu, this.tabContainer],
+ firstChild: { nextSibling: this.tabContextMenu },
+ getElementsByAttribute: function (attr, attrValue) {
+ if (attr == "anonid" && attrValue == "tabContextMenu")
+ return [this.self.tabContextMenu];
+ return [];
+ },
+ // Also support adding event listeners (forward to the tab container)
+ addEventListener: function (a, b, c) { this.self.tabContainer.addEventListener(a, b, c); },
+ removeEventListener: function (a, b, c) { this.self.tabContainer.removeEventListener(a, b, c); }
+ });
+ ]]>
+ </getter>
+ </property>
+ <field name="_soundPlayingAttrRemovalTimer">0</field>
+ </implementation>
+
+ <handlers>
+ <handler event="DOMWindowClose" phase="capturing">
+ <![CDATA[
+ if (!event.isTrusted)
+ return;
+
+ if (this.tabs.length == 1) {
+ // We already did PermitUnload in nsGlobalWindow::Close
+ // for this tab. There are no other tabs we need to do
+ // PermitUnload for.
+ window.skipNextCanClose = true;
+ return;
+ }
+
+ var tab = this._getTabForContentWindow(event.target);
+ if (tab) {
+ // Skip running PermitUnload since it already happened.
+ this.removeTab(tab, {skipPermitUnload: true});
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+ <handler event="DOMWillOpenModalDialog" phase="capturing">
+ <![CDATA[
+ if (!event.isTrusted)
+ return;
+
+ let targetIsWindow = event.target instanceof Window;
+
+ // We're about to open a modal dialog, so figure out for which tab:
+ // If this is a same-process modal dialog, then we're given its DOM
+ // window as the event's target. For remote dialogs, we're given the
+ // browser, but that's in the originalTarget and not the target,
+ // because it's across the tabbrowser's XBL boundary.
+ let tabForEvent = targetIsWindow ?
+ this._getTabForContentWindow(event.target.top) :
+ this.getTabForBrowser(event.originalTarget);
+
+ // Don't need to act if the tab is already selected:
+ if (tabForEvent.selected)
+ return;
+
+ // If this is a tabprompt, we won't switch tabs, unless:
+ // - this is a beforeunload prompt
+ // - this behaviour has been disabled entirely using the pref
+ if (event.detail && event.detail.tabPrompt &&
+ !event.detail.inPermitUnload &&
+ Services.prefs.getBoolPref("browser.tabs.dontfocusfordialogs")) {
+ let docPrincipal = targetIsWindow ? event.target.document.nodePrincipal : null;
+ // At least one of these should/will be non-null:
+ let promptPrincipal = event.detail.promptPrincipal || docPrincipal ||
+ tabForEvent.linkedBrowser.contentPrincipal;
+ // For null principals, we bail immediately and don't show the checkbox:
+ if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
+ tabForEvent.setAttribute("attention", "true");
+ return;
+ }
+
+ // For non-system/expanded principals, we bail and show the checkbox
+ if (promptPrincipal.URI &&
+ !Services.scriptSecurityManager.isSystemPrincipal(promptPrincipal)) {
+ let permission = Services.perms.testPermissionFromPrincipal(promptPrincipal,
+ "focus-tab-by-prompt");
+ if (permission != Services.perms.ALLOW_ACTION) {
+ // Tell the prompt box we want to show the user a checkbox:
+ let tabPrompt = this.getTabModalPromptBox(tabForEvent.linkedBrowser);
+ tabPrompt.onNextPromptShowAllowFocusCheckboxFor(promptPrincipal);
+ tabForEvent.setAttribute("attention", "true");
+ return;
+ }
+ }
+ // ... so system and expanded principals, as well as permitted "normal"
+ // URI-based principals, always get to steal focus for the tab when prompting.
+ }
+
+ // if prefs/permissions/origins so dictate, bring tab to the front:
+ this.selectedTab = tabForEvent;
+ ]]>
+ </handler>
+ <handler event="DOMTitleChanged">
+ <![CDATA[
+ if (!event.isTrusted)
+ return;
+
+ var contentWin = event.target.defaultView;
+ if (contentWin != contentWin.top)
+ return;
+
+ var tab = this._getTabForContentWindow(contentWin);
+ if (!tab || tab.hasAttribute("pending"))
+ return;
+
+ var titleChanged = this.setTabTitle(tab);
+ if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
+ tab.setAttribute("titlechanged", "true");
+ ]]>
+ </handler>
+ <handler event="oop-browser-crashed">
+ <![CDATA[
+ if (!event.isTrusted)
+ return;
+
+ let browser = event.originalTarget;
+ let icon = browser.mIconURL;
+ let tab = this.getTabForBrowser(browser);
+
+ if (this.selectedBrowser == browser) {
+ TabCrashHandler.onSelectedBrowserCrash(browser);
+ } else {
+ this.updateBrowserRemoteness(browser, false);
+ SessionStore.reviveCrashedTab(tab);
+ }
+
+ tab.removeAttribute("soundplaying");
+ this.setIcon(tab, icon, browser.contentPrincipal);
+ ]]>
+ </handler>
+ <handler event="DOMAudioPlaybackStarted">
+ <![CDATA[
+ var tab = getTabFromAudioEvent(event)
+ if (!tab) {
+ return;
+ }
+
+ clearTimeout(tab._soundPlayingAttrRemovalTimer);
+ tab._soundPlayingAttrRemovalTimer = 0;
+
+ let modifiedAttrs = [];
+ if (tab.hasAttribute("soundplaying-scheduledremoval")) {
+ tab.removeAttribute("soundplaying-scheduledremoval");
+ modifiedAttrs.push("soundplaying-scheduledremoval");
+ }
+
+ if (!tab.hasAttribute("soundplaying")) {
+ tab.setAttribute("soundplaying", true);
+ modifiedAttrs.push("soundplaying");
+ }
+
+ this._tabAttrModified(tab, modifiedAttrs);
+ ]]>
+ </handler>
+ <handler event="DOMAudioPlaybackStopped">
+ <![CDATA[
+ var tab = getTabFromAudioEvent(event)
+ if (!tab) {
+ return;
+ }
+
+ if (tab.hasAttribute("soundplaying")) {
+ let removalDelay = Services.prefs.getIntPref("browser.tabs.delayHidingAudioPlayingIconMS");
+
+ tab.style.setProperty("--soundplaying-removal-delay", `${removalDelay - 300}ms`);
+ tab.setAttribute("soundplaying-scheduledremoval", "true");
+ this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);
+
+ tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
+ tab.removeAttribute("soundplaying-scheduledremoval");
+ tab.removeAttribute("soundplaying");
+ this._tabAttrModified(tab, ["soundplaying", "soundplaying-scheduledremoval"]);
+ }, removalDelay);
+ }
+ ]]>
+ </handler>
+ <handler event="DOMAudioPlaybackBlockStarted">
+ <![CDATA[
+ var tab = getTabFromAudioEvent(event)
+ if (!tab) {
+ return;
+ }
+
+ if (!tab.hasAttribute("blocked")) {
+ tab.setAttribute("blocked", true);
+ this._tabAttrModified(tab, ["blocked"]);
+ }
+ ]]>
+ </handler>
+ <handler event="DOMAudioPlaybackBlockStopped">
+ <![CDATA[
+ var tab = getTabFromAudioEvent(event)
+ if (!tab) {
+ return;
+ }
+
+ if (tab.hasAttribute("blocked")) {
+ tab.removeAttribute("blocked");
+ this._tabAttrModified(tab, ["blocked"]);
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabbrowser-tabbox"
+ extends="chrome://global/content/bindings/tabbox.xml#tabbox">
+ <implementation>
+ <property name="tabs" readonly="true"
+ onget="return document.getBindingParent(this).tabContainer;"/>
+ </implementation>
+ </binding>
+
+ <binding id="tabbrowser-arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll">
+ <implementation>
+ <!-- Override scrollbox.xml method, since our scrollbox's children are
+ inherited from the binding parent -->
+ <method name="_getScrollableElements">
+ <body><![CDATA[
+ return Array.filter(document.getBindingParent(this).childNodes,
+ this._canScrollToElement, this);
+ ]]></body>
+ </method>
+ <method name="_canScrollToElement">
+ <parameter name="tab"/>
+ <body><![CDATA[
+ return !tab.pinned && !tab.hidden;
+ ]]></body>
+ </method>
+ <field name="_tabMarginLeft">null</field>
+ <field name="_tabMarginRight">null</field>
+ <method name="_calcTabMargins">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (this._tabMarginLeft === null || this._tabMarginRight === null) {
+ let tabMiddle = document.getAnonymousElementByAttribute(aTab, "class", "tab-background-middle");
+ let tabMiddleStyle = window.getComputedStyle(tabMiddle, null);
+ this._tabMarginLeft = parseFloat(tabMiddleStyle.marginLeft);
+ this._tabMarginRight = parseFloat(tabMiddleStyle.marginRight);
+ }
+ ]]></body>
+ </method>
+ <method name="_adjustElementStartAndEnd">
+ <parameter name="aTab"/>
+ <parameter name="tabStart"/>
+ <parameter name="tabEnd"/>
+ <body><![CDATA[
+ this._calcTabMargins(aTab);
+ if (this._tabMarginLeft < 0) {
+ tabStart = tabStart + this._tabMarginLeft;
+ }
+ if (this._tabMarginRight < 0) {
+ tabEnd = tabEnd - this._tabMarginRight;
+ }
+ return [tabStart, tabEnd];
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="underflow" phase="capturing"><![CDATA[
+ if (event.detail == 0)
+ return; // Ignore vertical events
+
+ var tabs = document.getBindingParent(this);
+ tabs.removeAttribute("overflow");
+
+ if (tabs._lastTabClosedByMouse)
+ tabs._expandSpacerBy(this._scrollButtonDown.clientWidth);
+
+ for (let tab of Array.from(tabs.tabbrowser._removingTabs))
+ tabs.tabbrowser.removeTab(tab);
+
+ tabs._positionPinnedTabs();
+ ]]></handler>
+ <handler event="overflow"><![CDATA[
+ if (event.detail == 0)
+ return; // Ignore vertical events
+
+ var tabs = document.getBindingParent(this);
+ tabs.setAttribute("overflow", "true");
+ tabs._positionPinnedTabs();
+ tabs._handleTabSelect(false);
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabbrowser-tabs"
+ extends="chrome://global/content/bindings/tabbox.xml#tabs">
+ <resources>
+ <stylesheet src="chrome://browser/content/tabbrowser.css"/>
+ </resources>
+
+ <content>
+ <xul:hbox align="end">
+ <xul:image class="tab-drop-indicator" anonid="tab-drop-indicator" collapsed="true"/>
+ </xul:hbox>
+ <xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1"
+ style="min-width: 1px;"
+ class="tabbrowser-arrowscrollbox">
+<!--
+ This is a hack to circumvent bug 472020, otherwise the tabs show up on the
+ right of the newtab button.
+-->
+ <children includes="tab"/>
+<!--
+ This is to ensure anything extensions put here will go before the newtab
+ button, necessary due to the previous hack.
+-->
+ <children/>
+ <xul:toolbarbutton class="tabs-newtab-button"
+ anonid="tabs-newtab-button"
+ command="cmd_newNavigatorTab"
+ onclick="checkForMiddleClick(this, event);"
+ onmouseover="document.getBindingParent(this)._enterNewTab();"
+ onmouseout="document.getBindingParent(this)._leaveNewTab();"
+ tooltip="dynamic-shortcut-tooltip"/>
+ <xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer"
+ style="width: 0;"/>
+ </xul:arrowscrollbox>
+ </content>
+
+ <implementation implements="nsIDOMEventListener, nsIObserver">
+ <constructor>
+ <![CDATA[
+ this.mTabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
+
+ var tab = this.firstChild;
+ tab.label = this.tabbrowser.mStringBundle.getString("tabs.emptyTabTitle");
+ tab.setAttribute("crop", "end");
+ tab.setAttribute("onerror", "this.removeAttribute('image');");
+
+ window.addEventListener("resize", this, false);
+ window.addEventListener("load", this, false);
+
+ try {
+ this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled");
+ } catch (ex) {
+ this._tabAnimationLoggingEnabled = false;
+ }
+ this._browserNewtabpageEnabled = Services.prefs.getBoolPref("browser.newtabpage.enabled");
+ this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
+ Services.prefs.addObserver("privacy.userContext.enabled", this, false);
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ Services.prefs.removeObserver("privacy.userContext.enabled", this);
+ ]]>
+ </destructor>
+
+ <field name="tabbrowser" readonly="true">
+ document.getElementById(this.getAttribute("tabbrowser"));
+ </field>
+
+ <field name="tabbox" readonly="true">
+ this.tabbrowser.mTabBox;
+ </field>
+
+ <field name="contextMenu" readonly="true">
+ document.getElementById("tabContextMenu");
+ </field>
+
+ <field name="mTabstripWidth">0</field>
+
+ <field name="mTabstrip">
+ document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox");
+ </field>
+
+ <field name="_firstTab">null</field>
+ <field name="_lastTab">null</field>
+ <field name="_afterSelectedTab">null</field>
+ <field name="_beforeHoveredTab">null</field>
+ <field name="_afterHoveredTab">null</field>
+ <field name="_hoveredTab">null</field>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body><![CDATA[
+ switch (aTopic) {
+ case "nsPref:changed":
+ // This is the only pref observed.
+ let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
+
+ const newTab = document.getElementById("new-tab-button");
+ const newTab2 = document.getAnonymousElementByAttribute(this, "anonid", "tabs-newtab-button")
+
+ if (containersEnabled) {
+ for (let parent of [newTab, newTab2]) {
+ if (!parent)
+ continue;
+ let popup = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "menupopup");
+ if (parent.id) {
+ popup.id = "newtab-popup";
+ } else {
+ popup.setAttribute("anonid", "newtab-popup");
+ }
+ popup.className = "new-tab-popup";
+ popup.setAttribute("position", "after_end");
+ parent.appendChild(popup);
+
+ gClickAndHoldListenersOnElement.add(parent);
+ parent.setAttribute("type", "menu");
+ }
+ } else {
+ for (let parent of [newTab, newTab2]) {
+ if (!parent)
+ continue;
+ gClickAndHoldListenersOnElement.remove(parent);
+ parent.removeAttribute("type");
+ if (!parent.firstChild)
+ continue;
+ parent.firstChild.remove();
+ }
+ }
+
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <property name="_isCustomizing" readonly="true">
+ <getter>
+ let root = document.documentElement;
+ return root.getAttribute("customizing") == "true" ||
+ root.getAttribute("customize-exiting") == "true";
+ </getter>
+ </property>
+
+ <method name="_setPositionalAttributes">
+ <body><![CDATA[
+ let visibleTabs = this.tabbrowser.visibleTabs;
+
+ if (!visibleTabs.length)
+ return;
+
+ let selectedIndex = visibleTabs.indexOf(this.selectedItem);
+
+ let lastVisible = visibleTabs.length - 1;
+
+ if (this._afterSelectedTab)
+ this._afterSelectedTab.removeAttribute("afterselected-visible");
+ if (this.selectedItem.closing || selectedIndex == lastVisible) {
+ this._afterSelectedTab = null;
+ } else {
+ this._afterSelectedTab = visibleTabs[selectedIndex + 1];
+ this._afterSelectedTab.setAttribute("afterselected-visible",
+ "true");
+ }
+
+ if (this._firstTab)
+ this._firstTab.removeAttribute("first-visible-tab");
+ this._firstTab = visibleTabs[0];
+ this._firstTab.setAttribute("first-visible-tab", "true");
+ if (this._lastTab)
+ this._lastTab.removeAttribute("last-visible-tab");
+ this._lastTab = visibleTabs[lastVisible];
+ this._lastTab.setAttribute("last-visible-tab", "true");
+
+ let hoveredTab = this._hoveredTab;
+ if (hoveredTab) {
+ hoveredTab._mouseleave();
+ }
+ hoveredTab = this.querySelector("tab:hover");
+ if (hoveredTab) {
+ hoveredTab._mouseenter();
+ }
+ ]]></body>
+ </method>
+
+ <field name="_blockDblClick">false</field>
+
+ <field name="_tabDropIndicator">
+ document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator");
+ </field>
+
+ <field name="_dragOverDelay">350</field>
+ <field name="_dragTime">0</field>
+
+ <field name="_container" readonly="true"><![CDATA[
+ this.parentNode && this.parentNode.localName == "toolbar" ? this.parentNode : this;
+ ]]></field>
+
+ <field name="_propagatedVisibilityOnce">false</field>
+
+ <property name="visible"
+ onget="return !this._container.collapsed;">
+ <setter><![CDATA[
+ if (val == this.visible &&
+ this._propagatedVisibilityOnce)
+ return val;
+
+ this._container.collapsed = !val;
+
+ this._propagateVisibility();
+ this._propagatedVisibilityOnce = true;
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="_enterNewTab">
+ <body><![CDATA[
+ let visibleTabs = this.tabbrowser.visibleTabs;
+ let candidate = visibleTabs[visibleTabs.length - 1];
+ if (!candidate.selected) {
+ this._beforeHoveredTab = candidate;
+ candidate.setAttribute("beforehovered", "true");
+ }
+ ]]></body>
+ </method>
+
+ <method name="_leaveNewTab">
+ <body><![CDATA[
+ if (this._beforeHoveredTab) {
+ this._beforeHoveredTab.removeAttribute("beforehovered");
+ this._beforeHoveredTab = null;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_propagateVisibility">
+ <body><![CDATA[
+ let visible = this.visible;
+
+ document.getElementById("menu_closeWindow").hidden = !visible;
+ document.getElementById("menu_close").setAttribute("label",
+ this.tabbrowser.mStringBundle.getString(visible ? "tabs.closeTab" : "tabs.close"));
+
+ TabsInTitlebar.allowedBy("tabs-visible", visible);
+ ]]></body>
+ </method>
+
+ <method name="updateVisibility">
+ <body><![CDATA[
+ if (this.childNodes.length - this.tabbrowser._removingTabs.length == 1)
+ this.visible = window.toolbar.visible;
+ else
+ this.visible = true;
+ ]]></body>
+ </method>
+
+ <method name="adjustTabstrip">
+ <body><![CDATA[
+ let numTabs = this.childNodes.length -
+ this.tabbrowser._removingTabs.length;
+ if (numTabs > 2) {
+ // This is an optimization to avoid layout flushes by calling
+ // getBoundingClientRect() when we just opened a second tab. In
+ // this case it's highly unlikely that the tab width is smaller
+ // than mTabClipWidth and the tab close button obscures too much
+ // of the tab's label. In the edge case of the window being too
+ // narrow (or if tabClipWidth has been set to a way higher value),
+ // we'll correct the 'closebuttons' attribute after the tabopen
+ // animation has finished.
+
+ let tab = this.tabbrowser.visibleTabs[this.tabbrowser._numPinnedTabs];
+ if (tab && tab.getBoundingClientRect().width <= this.mTabClipWidth) {
+ this.setAttribute("closebuttons", "activetab");
+ return;
+ }
+ }
+ this.removeAttribute("closebuttons");
+ ]]></body>
+ </method>
+
+ <method name="_handleTabSelect">
+ <parameter name="aSmoothScroll"/>
+ <body><![CDATA[
+ if (this.getAttribute("overflow") == "true")
+ this.mTabstrip.ensureElementIsVisible(this.selectedItem, aSmoothScroll);
+ ]]></body>
+ </method>
+
+ <method name="_fillTrailingGap">
+ <body><![CDATA[
+ try {
+ // if we're at the right side (and not the logical end,
+ // which is why this works for both LTR and RTL)
+ // of the tabstrip, we need to ensure that we stay
+ // completely scrolled to the right side
+ var tabStrip = this.mTabstrip;
+ if (tabStrip.scrollPosition + tabStrip.scrollClientSize >
+ tabStrip.scrollSize)
+ tabStrip.scrollByPixels(-1);
+ } catch (e) {}
+ ]]></body>
+ </method>
+
+ <field name="_closingTabsSpacer">
+ document.getAnonymousElementByAttribute(this, "anonid", "closing-tabs-spacer");
+ </field>
+
+ <field name="_tabDefaultMaxWidth">NaN</field>
+ <field name="_lastTabClosedByMouse">false</field>
+ <field name="_hasTabTempMaxWidth">false</field>
+
+ <!-- Try to keep the active tab's close button under the mouse cursor -->
+ <method name="_lockTabSizing">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ var tabs = this.tabbrowser.visibleTabs;
+ if (!tabs.length)
+ return;
+
+ var isEndTab = (aTab._tPos > tabs[tabs.length-1]._tPos);
+ var tabWidth = aTab.getBoundingClientRect().width;
+
+ if (!this._tabDefaultMaxWidth)
+ this._tabDefaultMaxWidth =
+ parseFloat(window.getComputedStyle(aTab).maxWidth);
+ this._lastTabClosedByMouse = true;
+
+ if (this.getAttribute("overflow") == "true") {
+ // Don't need to do anything if we're in overflow mode and aren't scrolled
+ // all the way to the right, or if we're closing the last tab.
+ if (isEndTab || !this.mTabstrip._scrollButtonDown.disabled)
+ return;
+
+ // If the tab has an owner that will become the active tab, the owner will
+ // be to the left of it, so we actually want the left tab to slide over.
+ // This can't be done as easily in non-overflow mode, so we don't bother.
+ if (aTab.owner)
+ return;
+
+ this._expandSpacerBy(tabWidth);
+ } else { // non-overflow mode
+ // Locking is neither in effect nor needed, so let tabs expand normally.
+ if (isEndTab && !this._hasTabTempMaxWidth)
+ return;
+
+ let numPinned = this.tabbrowser._numPinnedTabs;
+ // Force tabs to stay the same width, unless we're closing the last tab,
+ // which case we need to let them expand just enough so that the overall
+ // tabbar width is the same.
+ if (isEndTab) {
+ let numNormalTabs = tabs.length - numPinned;
+ tabWidth = tabWidth * (numNormalTabs + 1) / numNormalTabs;
+ if (tabWidth > this._tabDefaultMaxWidth)
+ tabWidth = this._tabDefaultMaxWidth;
+ }
+ tabWidth += "px";
+ for (let i = numPinned; i < tabs.length; i++) {
+ let tab = tabs[i];
+ tab.style.setProperty("max-width", tabWidth, "important");
+ if (!isEndTab) { // keep tabs the same width
+ tab.style.transition = "none";
+ tab.clientTop; // flush styles to skip animation; see bug 649247
+ tab.style.transition = "";
+ }
+ }
+ this._hasTabTempMaxWidth = true;
+ this.tabbrowser.addEventListener("mousemove", this, false);
+ window.addEventListener("mouseout", this, false);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_expandSpacerBy">
+ <parameter name="pixels"/>
+ <body><![CDATA[
+ let spacer = this._closingTabsSpacer;
+ spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
+ this.setAttribute("using-closing-tabs-spacer", "true");
+ this.tabbrowser.addEventListener("mousemove", this, false);
+ window.addEventListener("mouseout", this, false);
+ ]]></body>
+ </method>
+
+ <method name="_unlockTabSizing">
+ <body><![CDATA[
+ this.tabbrowser.removeEventListener("mousemove", this, false);
+ window.removeEventListener("mouseout", this, false);
+
+ if (this._hasTabTempMaxWidth) {
+ this._hasTabTempMaxWidth = false;
+ let tabs = this.tabbrowser.visibleTabs;
+ for (let i = 0; i < tabs.length; i++)
+ tabs[i].style.maxWidth = "";
+ }
+
+ if (this.hasAttribute("using-closing-tabs-spacer")) {
+ this.removeAttribute("using-closing-tabs-spacer");
+ this._closingTabsSpacer.style.width = 0;
+ }
+ ]]></body>
+ </method>
+
+ <field name="_lastNumPinned">0</field>
+ <method name="_positionPinnedTabs">
+ <body><![CDATA[
+ var numPinned = this.tabbrowser._numPinnedTabs;
+ var doPosition = this.getAttribute("overflow") == "true" &&
+ numPinned > 0;
+
+ if (doPosition) {
+ this.setAttribute("positionpinnedtabs", "true");
+
+ let scrollButtonWidth = this.mTabstrip._scrollButtonDown.getBoundingClientRect().width;
+ let paddingStart = this.mTabstrip.scrollboxPaddingStart;
+ let width = 0;
+
+ for (let i = numPinned - 1; i >= 0; i--) {
+ let tab = this.childNodes[i];
+ width += tab.getBoundingClientRect().width;
+ tab.style.marginInlineStart = - (width + scrollButtonWidth + paddingStart) + "px";
+ }
+
+ this.style.paddingInlineStart = width + paddingStart + "px";
+
+ } else {
+ this.removeAttribute("positionpinnedtabs");
+
+ for (let i = 0; i < numPinned; i++) {
+ let tab = this.childNodes[i];
+ tab.style.marginInlineStart = "";
+ }
+
+ this.style.paddingInlineStart = "";
+ }
+
+ if (this._lastNumPinned != numPinned) {
+ this._lastNumPinned = numPinned;
+ this._handleTabSelect(false);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_animateTabMove">
+ <parameter name="event"/>
+ <body><![CDATA[
+ let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+
+ if (this.getAttribute("movingtab") != "true") {
+ this.setAttribute("movingtab", "true");
+ this.selectedItem = draggedTab;
+ }
+
+ if (!("animLastScreenX" in draggedTab._dragData))
+ draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
+
+ let screenX = event.screenX;
+ if (screenX == draggedTab._dragData.animLastScreenX)
+ return;
+
+ draggedTab._dragData.animLastScreenX = screenX;
+
+ let rtl = (window.getComputedStyle(this).direction == "rtl");
+ let pinned = draggedTab.pinned;
+ let numPinned = this.tabbrowser._numPinnedTabs;
+ let tabs = this.tabbrowser.visibleTabs
+ .slice(pinned ? 0 : numPinned,
+ pinned ? numPinned : undefined);
+ if (rtl)
+ tabs.reverse();
+ let tabWidth = draggedTab.getBoundingClientRect().width;
+
+ // Move the dragged tab based on the mouse position.
+
+ let leftTab = tabs[0];
+ let rightTab = tabs[tabs.length - 1];
+ let tabScreenX = draggedTab.boxObject.screenX;
+ let translateX = screenX - draggedTab._dragData.screenX;
+ if (!pinned)
+ translateX += this.mTabstrip.scrollPosition - draggedTab._dragData.scrollX;
+ let leftBound = leftTab.boxObject.screenX - tabScreenX;
+ let rightBound = (rightTab.boxObject.screenX + rightTab.boxObject.width) -
+ (tabScreenX + tabWidth);
+ translateX = Math.max(translateX, leftBound);
+ translateX = Math.min(translateX, rightBound);
+ draggedTab.style.transform = "translateX(" + translateX + "px)";
+
+ // Determine what tab we're dragging over.
+ // * Point of reference is the center of the dragged tab. If that
+ // point touches a background tab, the dragged tab would take that
+ // tab's position when dropped.
+ // * We're doing a binary search in order to reduce the amount of
+ // tabs we need to check.
+
+ let tabCenter = tabScreenX + translateX + tabWidth / 2;
+ let newIndex = -1;
+ let oldIndex = "animDropIndex" in draggedTab._dragData ?
+ draggedTab._dragData.animDropIndex : draggedTab._tPos;
+ let low = 0;
+ let high = tabs.length - 1;
+ while (low <= high) {
+ let mid = Math.floor((low + high) / 2);
+ if (tabs[mid] == draggedTab &&
+ ++mid > high)
+ break;
+ let boxObject = tabs[mid].boxObject;
+ let screenX = boxObject.screenX + getTabShift(tabs[mid], oldIndex);
+ if (screenX > tabCenter) {
+ high = mid - 1;
+ } else if (screenX + boxObject.width < tabCenter) {
+ low = mid + 1;
+ } else {
+ newIndex = tabs[mid]._tPos;
+ break;
+ }
+ }
+ if (newIndex >= oldIndex)
+ newIndex++;
+ if (newIndex < 0 || newIndex == oldIndex)
+ return;
+ draggedTab._dragData.animDropIndex = newIndex;
+
+ // Shift background tabs to leave a gap where the dragged tab
+ // would currently be dropped.
+
+ for (let tab of tabs) {
+ if (tab != draggedTab) {
+ let shift = getTabShift(tab, newIndex);
+ tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
+ }
+ }
+
+ function getTabShift(tab, dropIndex) {
+ if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex)
+ return rtl ? -tabWidth : tabWidth;
+ if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex)
+ return rtl ? tabWidth : -tabWidth;
+ return 0;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_finishAnimateTabMove">
+ <body><![CDATA[
+ if (this.getAttribute("movingtab") != "true")
+ return;
+
+ for (let tab of this.tabbrowser.visibleTabs)
+ tab.style.transform = "";
+
+ this.removeAttribute("movingtab");
+
+ this._handleTabSelect();
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.type) {
+ case "load":
+ this.updateVisibility();
+ TabsInTitlebar.init();
+ break;
+ case "resize":
+ if (aEvent.target != window)
+ break;
+
+ TabsInTitlebar.updateAppearance();
+
+ var width = this.mTabstrip.boxObject.width;
+ if (width != this.mTabstripWidth) {
+ this.adjustTabstrip();
+ this._fillTrailingGap();
+ this._handleTabSelect();
+ this.mTabstripWidth = width;
+ }
+
+ this.tabbrowser.updateWindowResizers();
+ break;
+ case "mouseout":
+ // If the "related target" (the node to which the pointer went) is not
+ // a child of the current document, the mouse just left the window.
+ let relatedTarget = aEvent.relatedTarget;
+ if (relatedTarget && relatedTarget.ownerDocument == document)
+ break;
+ case "mousemove":
+ if (document.getElementById("tabContextMenu").state != "open")
+ this._unlockTabSizing();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <field name="_animateElement">
+ this.mTabstrip._scrollButtonDown;
+ </field>
+
+ <method name="_notifyBackgroundTab">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (aTab.pinned || aTab.hidden)
+ return;
+
+ var scrollRect = this.mTabstrip.scrollClientRect;
+ var tab = aTab.getBoundingClientRect();
+ this.mTabstrip._calcTabMargins(aTab);
+
+ // DOMRect left/right properties are immutable.
+ tab = {left: tab.left, right: tab.right};
+
+ // Is the new tab already completely visible?
+ if (scrollRect.left <= tab.left && tab.right <= scrollRect.right)
+ return;
+
+ if (this.mTabstrip.smoothScroll) {
+ let selected = !this.selectedItem.pinned &&
+ this.selectedItem.getBoundingClientRect();
+ if (selected) {
+ selected = {left: selected.left, right: selected.right};
+ // Need to take in to account the width of the left/right margins on tabs.
+ selected.left = selected.left + this.mTabstrip._tabMarginLeft;
+ selected.right = selected.right - this.mTabstrip._tabMarginRight;
+ }
+
+ tab.left += this.mTabstrip._tabMarginLeft;
+ tab.right -= this.mTabstrip._tabMarginRight;
+
+ // Can we make both the new tab and the selected tab completely visible?
+ if (!selected ||
+ Math.max(tab.right - selected.left, selected.right - tab.left) <=
+ scrollRect.width) {
+ this.mTabstrip.ensureElementIsVisible(aTab);
+ return;
+ }
+
+ this.mTabstrip._smoothScrollByPixels(this.mTabstrip._isRTLScrollbox ?
+ selected.right - scrollRect.right :
+ selected.left - scrollRect.left);
+ }
+
+ if (!this._animateElement.hasAttribute("notifybgtab")) {
+ this._animateElement.setAttribute("notifybgtab", "true");
+ setTimeout(function (ele) {
+ ele.removeAttribute("notifybgtab");
+ }, 150, this._animateElement);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_getDragTargetTab">
+ <parameter name="event"/>
+ <parameter name="isLink"/>
+ <body><![CDATA[
+ let tab = event.target.localName == "tab" ? event.target : null;
+ if (tab && isLink) {
+ let boxObject = tab.boxObject;
+ if (event.screenX < boxObject.screenX + boxObject.width * .25 ||
+ event.screenX > boxObject.screenX + boxObject.width * .75)
+ return null;
+ }
+ return tab;
+ ]]></body>
+ </method>
+
+ <method name="_getDropIndex">
+ <parameter name="event"/>
+ <parameter name="isLink"/>
+ <body><![CDATA[
+ var tabs = this.childNodes;
+ var tab = this._getDragTargetTab(event, isLink);
+ if (window.getComputedStyle(this, null).direction == "ltr") {
+ for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
+ if (event.screenX < tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2)
+ return i;
+ } else {
+ for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
+ if (event.screenX > tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2)
+ return i;
+ }
+ return tabs.length;
+ ]]></body>
+ </method>
+
+ <method name="_getDropEffectForTabDrag">
+ <parameter name="event"/>
+ <body><![CDATA[
+ var dt = event.dataTransfer;
+ if (dt.mozItemCount == 1) {
+ var types = dt.mozTypesAt(0);
+ // tabs are always added as the first type
+ if (types[0] == TAB_DROP_TYPE) {
+ let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (sourceNode instanceof XULElement &&
+ sourceNode.localName == "tab" &&
+ sourceNode.ownerDocument.defaultView instanceof ChromeWindow &&
+ sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" &&
+ sourceNode.ownerDocument.defaultView.gBrowser.tabContainer == sourceNode.parentNode) {
+ // Do not allow transfering a private tab to a non-private window
+ // and vice versa.
+ if (PrivateBrowsingUtils.isWindowPrivate(window) !=
+ PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerDocument.defaultView))
+ return "none";
+
+ if (window.gMultiProcessBrowser !=
+ sourceNode.ownerDocument.defaultView.gMultiProcessBrowser)
+ return "none";
+
+ return dt.dropEffect == "copy" ? "copy" : "move";
+ }
+ }
+ }
+
+ if (browserDragAndDrop.canDropLink(event)) {
+ return "link";
+ }
+ return "none";
+ ]]></body>
+ </method>
+
+ <method name="_handleNewTab">
+ <parameter name="tab"/>
+ <body><![CDATA[
+ if (tab.parentNode != this)
+ return;
+ tab._fullyOpen = true;
+
+ this.adjustTabstrip();
+
+ if (tab.getAttribute("selected") == "true") {
+ this._fillTrailingGap();
+ this._handleTabSelect();
+ } else {
+ this._notifyBackgroundTab(tab);
+ }
+
+ // XXXmano: this is a temporary workaround for bug 345399
+ // We need to manually update the scroll buttons disabled state
+ // if a tab was inserted to the overflow area or removed from it
+ // without any scrolling and when the tabbar has already
+ // overflowed.
+ this.mTabstrip._updateScrollButtonsDisabledState();
+
+ // Preload the next about:newtab if there isn't one already.
+ this.tabbrowser._createPreloadBrowser();
+ ]]></body>
+ </method>
+
+ <method name="_canAdvanceToTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ return !aTab.closing;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_handleTabTelemetryStart">
+ <parameter name="aTab"/>
+ <parameter name="aURI"/>
+ <body>
+ <![CDATA[
+ // Animation-smoothness telemetry/logging
+ if (Services.telemetry.canRecordExtended || this._tabAnimationLoggingEnabled) {
+ if (aURI == "about:newtab" && (aTab._tPos == 1 || aTab._tPos == 2)) {
+ // Indicate newtab page animation where other tabs are unaffected
+ // (for which case, the 2nd or 3rd tabs are good representatives, even if not absolute)
+ aTab._recordingTabOpenPlain = true;
+ }
+ aTab._recordingHandle = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .startFrameTimeRecording();
+ }
+
+ // Overall animation duration
+ aTab._animStartTime = Date.now();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_handleTabTelemetryEnd">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!aTab._animStartTime) {
+ return;
+ }
+
+ aTab._animStartTime = 0;
+
+ // Handle tab animation smoothness telemetry/logging of frame intervals and paint times
+ if (!("_recordingHandle" in aTab)) {
+ return;
+ }
+
+ let intervals = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .stopFrameTimeRecording(aTab._recordingHandle);
+ delete aTab._recordingHandle;
+ let frameCount = intervals.length;
+
+ if (this._tabAnimationLoggingEnabled) {
+ let msg = "Tab " + (aTab.closing ? "close" : "open") + " (Frame-interval):\n";
+ for (let i = 0; i < frameCount; i++) {
+ msg += Math.round(intervals[i]) + "\n";
+ }
+ Services.console.logStringMessage(msg);
+ }
+
+ // For telemetry, the first frame interval is not useful since it may represent an interval
+ // to a relatively old frame (prior to recording start). So we'll ignore it for the average.
+ if (frameCount > 1) {
+ let averageInterval = 0;
+ for (let i = 1; i < frameCount; i++) {
+ averageInterval += intervals[i];
+ }
+ averageInterval = averageInterval / (frameCount - 1);
+
+ Services.telemetry.getHistogramById("FX_TAB_ANIM_ANY_FRAME_INTERVAL_MS").add(averageInterval);
+
+ if (aTab._recordingTabOpenPlain) {
+ delete aTab._recordingTabOpenPlain;
+ // While we do have a telemetry probe NEWTAB_PAGE_ENABLED to monitor newtab preview, it'll be
+ // easier to overview the data without slicing by it. Hence the additional histograms with _PREVIEW.
+ let preview = this._browserNewtabpageEnabled ? "_PREVIEW" : "";
+ Services.telemetry.getHistogramById("FX_TAB_ANIM_OPEN" + preview + "_FRAME_INTERVAL_MS").add(averageInterval);
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- Deprecated stuff, implemented for backwards compatibility. -->
+ <property name="mAllTabsPopup" readonly="true"
+ onget="return document.getElementById('alltabs-popup');"/>
+ </implementation>
+
+ <handlers>
+ <handler event="TabSelect" action="this._handleTabSelect();"/>
+
+ <handler event="transitionend"><![CDATA[
+ if (event.propertyName != "max-width")
+ return;
+
+ var tab = event.target;
+
+ this._handleTabTelemetryEnd(tab);
+
+ if (tab.getAttribute("fadein") == "true") {
+ if (tab._fullyOpen)
+ this.adjustTabstrip();
+ else
+ this._handleNewTab(tab);
+ } else if (tab.closing) {
+ this.tabbrowser._endRemoveTab(tab);
+ }
+ ]]></handler>
+
+ <handler event="dblclick"><![CDATA[
+ // When the tabbar has an unified appearance with the titlebar
+ // and menubar, a double-click in it should have the same behavior
+ // as double-clicking the titlebar
+ if (TabsInTitlebar.enabled || this.parentNode._dragBindingAlive)
+ return;
+
+ if (event.button != 0 ||
+ event.originalTarget.localName != "box")
+ return;
+
+ // See hack note in the tabbrowser-close-tab-button binding
+ if (!this._blockDblClick)
+ BrowserOpenTab();
+
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="click" button="0" phase="capturing"><![CDATA[
+ /* Catches extra clicks meant for the in-tab close button.
+ * Placed here to avoid leaking (a temporary handler added from the
+ * in-tab close button binding would close over the tab and leak it
+ * until the handler itself was removed). (bug 897751)
+ *
+ * The only sequence in which a second click event (i.e. dblclik)
+ * can be dispatched on an in-tab close button is when it is shown
+ * after the first click (i.e. the first click event was dispatched
+ * on the tab). This happens when we show the close button only on
+ * the active tab. (bug 352021)
+ * The only sequence in which a third click event can be dispatched
+ * on an in-tab close button is when the tab was opened with a
+ * double click on the tabbar. (bug 378344)
+ * In both cases, it is most likely that the close button area has
+ * been accidentally clicked, therefore we do not close the tab.
+ *
+ * We don't want to ignore processing of more than one click event,
+ * though, since the user might actually be repeatedly clicking to
+ * close many tabs at once.
+ */
+ let target = event.originalTarget;
+ if (target.classList.contains('tab-close-button')) {
+ // We preemptively set this to allow the closing-multiple-tabs-
+ // in-a-row case.
+ if (this._blockDblClick) {
+ target._ignoredCloseButtonClicks = true;
+ } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
+ target._ignoredCloseButtonClicks = true;
+ event.stopPropagation();
+ return;
+ } else {
+ // Reset the "ignored click" flag
+ target._ignoredCloseButtonClicks = false;
+ }
+ }
+
+ /* Protects from close-tab-button errant doubleclick:
+ * Since we're removing the event target, if the user
+ * double-clicks the button, the dblclick event will be dispatched
+ * with the tabbar as its event target (and explicit/originalTarget),
+ * which treats that as a mouse gesture for opening a new tab.
+ * In this context, we're manually blocking the dblclick event
+ * (see tabbrowser-close-tab-button dblclick handler).
+ */
+ if (this._blockDblClick) {
+ if (!("_clickedTabBarOnce" in this)) {
+ this._clickedTabBarOnce = true;
+ return;
+ }
+ delete this._clickedTabBarOnce;
+ this._blockDblClick = false;
+ }
+ ]]></handler>
+
+ <handler event="click"><![CDATA[
+ if (event.button != 1)
+ return;
+
+ if (event.target.localName == "tab") {
+ this.tabbrowser.removeTab(event.target, {animate: true,
+ byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE});
+ } else if (event.originalTarget.localName == "box") {
+ // The user middleclicked an open space on the tabstrip. This could
+ // be because they intend to open a new tab, but it could also be
+ // because they just removed a tab and they now middleclicked on the
+ // resulting space while that tab is closing. In that case, we don't
+ // want to open a tab. So if we're removing one or more tabs, and
+ // the tab click is before the end of the last visible tab, we do
+ // nothing.
+ if (this.tabbrowser._removingTabs.length) {
+ let visibleTabs = this.tabbrowser.visibleTabs;
+ let ltr = (window.getComputedStyle(this, null).direction == "ltr");
+ let lastTab = visibleTabs[visibleTabs.length - 1];
+ let endOfTab = lastTab.getBoundingClientRect()[ltr ? "right" : "left"];
+ if ((ltr && event.clientX > endOfTab) ||
+ (!ltr && event.clientX < endOfTab)) {
+ BrowserOpenTab();
+ }
+ } else {
+ BrowserOpenTab();
+ }
+ } else {
+ return;
+ }
+
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="keydown" group="system"><![CDATA[
+ if (event.altKey || event.shiftKey)
+ return;
+
+ let wrongModifiers;
+ if (this.tabbrowser.AppConstants.platform == "macosx") {
+ wrongModifiers = !event.metaKey;
+ } else {
+ wrongModifiers = !event.ctrlKey || event.metaKey;
+ }
+
+ if (wrongModifiers)
+ return;
+
+ // Don't check if the event was already consumed because tab navigation
+ // should work always for better user experience.
+
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_UP:
+ this.tabbrowser.moveTabBackward();
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ this.tabbrowser.moveTabForward();
+ break;
+ case KeyEvent.DOM_VK_RIGHT:
+ case KeyEvent.DOM_VK_LEFT:
+ this.tabbrowser.moveTabOver(event);
+ break;
+ case KeyEvent.DOM_VK_HOME:
+ this.tabbrowser.moveTabToStart();
+ break;
+ case KeyEvent.DOM_VK_END:
+ this.tabbrowser.moveTabToEnd();
+ break;
+ default:
+ // Consume the keydown event for the above keyboard
+ // shortcuts only.
+ return;
+ }
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="dragstart"><![CDATA[
+ var tab = this._getDragTargetTab(event, false);
+ if (!tab || this._isCustomizing)
+ return;
+
+ let dt = event.dataTransfer;
+ dt.mozSetDataAt(TAB_DROP_TYPE, tab, 0);
+ let browser = tab.linkedBrowser;
+
+ // We must not set text/x-moz-url or text/plain data here,
+ // otherwise trying to deatch the tab by dropping it on the desktop
+ // may result in an "internet shortcut"
+ dt.mozSetDataAt("text/x-moz-text-internal", browser.currentURI.spec, 0);
+
+ // Set the cursor to an arrow during tab drags.
+ dt.mozCursor = "default";
+
+ // Create a canvas to which we capture the current tab.
+ // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
+ // canvas size (in CSS pixels) to the window's backing resolution in order
+ // to get a full-resolution drag image for use on HiDPI displays.
+ let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils);
+ let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
+ let canvas = this._dndCanvas ? this._dndCanvas
+ : document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.mozOpaque = true;
+ canvas.width = 160 * scale;
+ canvas.height = 90 * scale;
+ let toDrag;
+ let dragImageOffset = -16;
+ if (gMultiProcessBrowser) {
+ var context = canvas.getContext('2d');
+ context.fillStyle = "white";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ // Create a panel to use it in setDragImage
+ // which will tell xul to render a panel that follows
+ // the pointer while a dnd session is on.
+ if (!this._dndPanel) {
+ this._dndCanvas = canvas;
+ this._dndPanel = document.createElement("panel");
+ this._dndPanel.className = "dragfeedback-tab";
+ this._dndPanel.setAttribute("type", "drag");
+ let wrapper = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ wrapper.style.width = "160px";
+ wrapper.style.height = "90px";
+ wrapper.appendChild(canvas);
+ canvas.style.width = "100%";
+ canvas.style.height = "100%";
+ this._dndPanel.appendChild(wrapper);
+ document.documentElement.appendChild(this._dndPanel);
+ }
+ // PageThumb is async with e10s but that's fine
+ // since we can update the panel during the dnd.
+ PageThumbs.captureToCanvas(browser, canvas);
+ toDrag = this._dndPanel;
+ } else {
+ // For the non e10s case we can just use PageThumbs
+ // sync. No need for xul magic, the native dnd will
+ // be fine, so let's use the canvas for setDragImage.
+ PageThumbs.captureToCanvas(browser, canvas);
+ toDrag = canvas;
+ dragImageOffset = dragImageOffset * scale;
+ }
+ dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
+
+ // _dragData.offsetX/Y give the coordinates that the mouse should be
+ // positioned relative to the corner of the new window created upon
+ // dragend such that the mouse appears to have the same position
+ // relative to the corner of the dragged tab.
+ function clientX(ele) {
+ return ele.getBoundingClientRect().left;
+ }
+ let tabOffsetX = clientX(tab) - clientX(this);
+ tab._dragData = {
+ offsetX: event.screenX - window.screenX - tabOffsetX,
+ offsetY: event.screenY - window.screenY,
+ scrollX: this.mTabstrip.scrollPosition,
+ screenX: event.screenX
+ };
+
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragover"><![CDATA[
+ var effects = this._getDropEffectForTabDrag(event);
+
+ var ind = this._tabDropIndicator;
+ if (effects == "" || effects == "none") {
+ ind.collapsed = true;
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+
+ var tabStrip = this.mTabstrip;
+ var ltr = (window.getComputedStyle(this, null).direction == "ltr");
+
+ // autoscroll the tab strip if we drag over the scroll
+ // buttons, even if we aren't dragging a tab, but then
+ // return to avoid drawing the drop indicator
+ var pixelsToScroll = 0;
+ if (this.getAttribute("overflow") == "true") {
+ var targetAnonid = event.originalTarget.getAttribute("anonid");
+ switch (targetAnonid) {
+ case "scrollbutton-up":
+ pixelsToScroll = tabStrip.scrollIncrement * -1;
+ break;
+ case "scrollbutton-down":
+ pixelsToScroll = tabStrip.scrollIncrement;
+ break;
+ }
+ if (pixelsToScroll)
+ tabStrip.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll);
+ }
+
+ if (effects == "move" &&
+ this == event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0).parentNode) {
+ ind.collapsed = true;
+ this._animateTabMove(event);
+ return;
+ }
+
+ this._finishAnimateTabMove();
+
+ if (effects == "link") {
+ let tab = this._getDragTargetTab(event, true);
+ if (tab) {
+ if (!this._dragTime)
+ this._dragTime = Date.now();
+ if (Date.now() >= this._dragTime + this._dragOverDelay)
+ this.selectedItem = tab;
+ ind.collapsed = true;
+ return;
+ }
+ }
+
+ var rect = tabStrip.getBoundingClientRect();
+ var newMargin;
+ if (pixelsToScroll) {
+ // if we are scrolling, put the drop indicator at the edge
+ // so that it doesn't jump while scrolling
+ let scrollRect = tabStrip.scrollClientRect;
+ let minMargin = scrollRect.left - rect.left;
+ let maxMargin = Math.min(minMargin + scrollRect.width,
+ scrollRect.right);
+ if (!ltr)
+ [minMargin, maxMargin] = [this.clientWidth - maxMargin,
+ this.clientWidth - minMargin];
+ newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin;
+ }
+ else {
+ let newIndex = this._getDropIndex(event, effects == "link");
+ if (newIndex == this.childNodes.length) {
+ let tabRect = this.childNodes[newIndex-1].getBoundingClientRect();
+ if (ltr)
+ newMargin = tabRect.right - rect.left;
+ else
+ newMargin = rect.right - tabRect.left;
+ }
+ else {
+ let tabRect = this.childNodes[newIndex].getBoundingClientRect();
+ if (ltr)
+ newMargin = tabRect.left - rect.left;
+ else
+ newMargin = rect.right - tabRect.right;
+ }
+ }
+
+ ind.collapsed = false;
+
+ newMargin += ind.clientWidth / 2;
+ if (!ltr)
+ newMargin *= -1;
+
+ ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
+ ind.style.marginInlineStart = (-ind.clientWidth) + "px";
+ ]]></handler>
+
+ <handler event="drop"><![CDATA[
+ var dt = event.dataTransfer;
+ var dropEffect = dt.dropEffect;
+ var draggedTab;
+ if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move
+ draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ // not our drop then
+ if (!draggedTab)
+ return;
+ }
+
+ this._tabDropIndicator.collapsed = true;
+ event.stopPropagation();
+ if (draggedTab && dropEffect == "copy") {
+ // copy the dropped tab (wherever it's from)
+ let newIndex = this._getDropIndex(event, false);
+ let newTab = this.tabbrowser.duplicateTab(draggedTab);
+ this.tabbrowser.moveTabTo(newTab, newIndex);
+ if (draggedTab.parentNode != this || event.shiftKey)
+ this.selectedItem = newTab;
+ } else if (draggedTab && draggedTab.parentNode == this) {
+ this._finishAnimateTabMove();
+
+ // actually move the dragged tab
+ if ("animDropIndex" in draggedTab._dragData) {
+ let newIndex = draggedTab._dragData.animDropIndex;
+ if (newIndex > draggedTab._tPos)
+ newIndex--;
+ this.tabbrowser.moveTabTo(draggedTab, newIndex);
+ }
+ } else if (draggedTab) {
+ let newIndex = this._getDropIndex(event, false);
+ this.tabbrowser.adoptTab(draggedTab, newIndex, true);
+ } else {
+ // Pass true to disallow dropping javascript: or data: urls
+ let links;
+ try {
+ links = browserDragAndDrop.dropLinks(event, true);
+ } catch (ex) {}
+
+ if (!links || links.length === 0)
+ return;
+
+ let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+
+ if (event.shiftKey)
+ inBackground = !inBackground;
+
+ let targetTab = this._getDragTargetTab(event, true);
+ let userContextId = this.selectedItem.getAttribute("usercontextid");
+ let replace = !!targetTab;
+ let newIndex = this._getDropIndex(event, true);
+ let urls = links.map(link => link.url);
+ this.tabbrowser.loadTabs(urls, {
+ inBackground,
+ replace,
+ allowThirdPartyFixup: true,
+ targetTab,
+ newIndex,
+ userContextId,
+ });
+ }
+
+ if (draggedTab) {
+ delete draggedTab._dragData;
+ }
+ ]]></handler>
+
+ <handler event="dragend"><![CDATA[
+ // Note: while this case is correctly handled here, this event
+ // isn't dispatched when the tab is moved within the tabstrip,
+ // see bug 460801.
+
+ this._finishAnimateTabMove();
+
+ var dt = event.dataTransfer;
+ var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (dt.mozUserCancelled || dt.dropEffect != "none" || this._isCustomizing) {
+ delete draggedTab._dragData;
+ return;
+ }
+
+ // Disable detach within the browser toolbox
+ var eX = event.screenX;
+ var eY = event.screenY;
+ var wX = window.screenX;
+ // check if the drop point is horizontally within the window
+ if (eX > wX && eX < (wX + window.outerWidth)) {
+ let bo = this.mTabstrip.boxObject;
+ // also avoid detaching if the the tab was dropped too close to
+ // the tabbar (half a tab)
+ let endScreenY = bo.screenY + 1.5 * bo.height;
+ if (eY < endScreenY && eY > window.screenY)
+ return;
+ }
+
+ // screen.availLeft et. al. only check the screen that this window is on,
+ // but we want to look at the screen the tab is being dropped onto.
+ var screen = Cc["@mozilla.org/gfx/screenmanager;1"]
+ .getService(Ci.nsIScreenManager)
+ .screenForRect(eX, eY, 1, 1);
+ var fullX = {}, fullY = {}, fullWidth = {}, fullHeight = {};
+ var availX = {}, availY = {}, availWidth = {}, availHeight = {};
+ // get full screen rect and available rect, both in desktop pix
+ screen.GetRectDisplayPix(fullX, fullY, fullWidth, fullHeight);
+ screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
+
+ // scale factor to convert desktop pixels to CSS px
+ var scaleFactor =
+ screen.contentsScaleFactor / screen.defaultCSSScaleFactor;
+ // synchronize CSS-px top-left coordinates with the screen's desktop-px
+ // coordinates, to ensure uniqueness across multiple screens
+ // (compare the equivalent adjustments in nsGlobalWindow::GetScreenXY()
+ // and related methods)
+ availX.value = (availX.value - fullX.value) * scaleFactor + fullX.value;
+ availY.value = (availY.value - fullY.value) * scaleFactor + fullY.value;
+ availWidth.value *= scaleFactor;
+ availHeight.value *= scaleFactor;
+
+ // ensure new window entirely within screen
+ var winWidth = Math.min(window.outerWidth, availWidth.value);
+ var winHeight = Math.min(window.outerHeight, availHeight.value);
+ var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, availX.value),
+ availX.value + availWidth.value - winWidth);
+ var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, availY.value),
+ availY.value + availHeight.value - winHeight);
+
+ delete draggedTab._dragData;
+
+ if (this.tabbrowser.tabs.length == 1) {
+ // resize _before_ move to ensure the window fits the new screen. if
+ // the window is too large for its screen, the window manager may do
+ // automatic repositioning.
+ window.resizeTo(winWidth, winHeight);
+ window.moveTo(left, top);
+ window.focus();
+ } else {
+ let props = { screenX: left, screenY: top };
+ if (this.tabbrowser.AppConstants.platform != "win") {
+ props.outerWidth = winWidth;
+ props.outerHeight = winHeight;
+ }
+ this.tabbrowser.replaceTabWithWindow(draggedTab, props);
+ }
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragexit"><![CDATA[
+ this._dragTime = 0;
+
+ // This does not work at all (see bug 458613)
+ var target = event.relatedTarget;
+ while (target && target != this)
+ target = target.parentNode;
+ if (target)
+ return;
+
+ this._tabDropIndicator.collapsed = true;
+ event.stopPropagation();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <!-- close-tab-button binding
+ This binding relies on the structure of the tabbrowser binding.
+ Therefore it should only be used as a child of the tab or the tabs
+ element (in both cases, when they are anonymous nodes of <tabbrowser>).
+ -->
+ <binding id="tabbrowser-close-tab-button"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image">
+ <handlers>
+ <handler event="click" button="0"><![CDATA[
+ var bindingParent = document.getBindingParent(this);
+ var tabContainer = bindingParent.parentNode;
+ tabContainer.tabbrowser.removeTab(bindingParent, {animate: true,
+ byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE});
+ // This enables double-click protection for the tab container
+ // (see tabbrowser-tabs 'click' handler).
+ tabContainer._blockDblClick = true;
+ ]]></handler>
+
+ <handler event="dblclick" button="0" phase="capturing">
+ // for the one-close-button case
+ event.stopPropagation();
+ </handler>
+
+ <handler event="dragstart">
+ event.stopPropagation();
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabbrowser-tab" display="xul:hbox"
+ extends="chrome://global/content/bindings/tabbox.xml#tab">
+ <resources>
+ <stylesheet src="chrome://browser/content/tabbrowser.css"/>
+ </resources>
+
+ <content context="tabContextMenu">
+ <xul:stack class="tab-stack" flex="1">
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected,fadein"
+ class="tab-background">
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected"
+ class="tab-background-start"/>
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected"
+ class="tab-background-middle"/>
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected"
+ class="tab-background-end"/>
+ </xul:hbox>
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected,titlechanged,attention"
+ class="tab-content" align="center">
+ <xul:image xbl:inherits="fadein,pinned,busy,progress,selected=visuallyselected"
+ class="tab-throbber"
+ role="presentation"
+ layer="true" />
+ <xul:image xbl:inherits="src=image,loadingprincipal=iconLoadingPrincipal,fadein,pinned,selected=visuallyselected,busy,crashed,sharing"
+ anonid="tab-icon-image"
+ class="tab-icon-image"
+ validate="never"
+ role="presentation"/>
+ <xul:image xbl:inherits="sharing,selected=visuallyselected"
+ anonid="sharing-icon"
+ class="tab-sharing-icon-overlay"
+ role="presentation"/>
+ <xul:image xbl:inherits="crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected"
+ anonid="overlay-icon"
+ class="tab-icon-overlay"
+ role="presentation"/>
+ <xul:label flex="1"
+ xbl:inherits="value=label,crop,accesskey,fadein,pinned,selected=visuallyselected,attention"
+ class="tab-text tab-label"
+ role="presentation"/>
+ <xul:image xbl:inherits="soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected"
+ anonid="soundplaying-icon"
+ class="tab-icon-sound"
+ role="presentation"/>
+ <xul:toolbarbutton anonid="close-button"
+ xbl:inherits="fadein,pinned,selected=visuallyselected"
+ class="tab-close-button close-icon"/>
+ </xul:hbox>
+ </xul:stack>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ if (!("_lastAccessed" in this)) {
+ this.updateLastAccessed();
+ }
+ ]]></constructor>
+
+ <property name="_visuallySelected">
+ <setter>
+ <![CDATA[
+ if (val)
+ this.setAttribute("visuallyselected", "true");
+ else
+ this.removeAttribute("visuallyselected");
+ this.parentNode.tabbrowser._tabAttrModified(this, ["visuallyselected"]);
+
+ this._setPositionAttributes(val);
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="_selected">
+ <setter>
+ <![CDATA[
+ // in e10s we want to only pseudo-select a tab before its rendering is done, so that
+ // the rest of the system knows that the tab is selected, but we don't want to update its
+ // visual status to selected until after we receive confirmation that its content has painted.
+ if (val)
+ this.setAttribute("selected", "true");
+ else
+ this.removeAttribute("selected");
+
+ // If we're non-e10s we should update the visual selection as well at the same time,
+ // *or* if we're e10s and the visually selected tab isn't changing, in which case the
+ // tab switcher code won't run and update anything else (like the before- and after-
+ // selected attributes).
+ if (!gMultiProcessBrowser || (val && this.hasAttribute("visuallyselected"))) {
+ this._visuallySelected = val;
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="pinned" readonly="true">
+ <getter>
+ return this.getAttribute("pinned") == "true";
+ </getter>
+ </property>
+ <property name="hidden" readonly="true">
+ <getter>
+ return this.getAttribute("hidden") == "true";
+ </getter>
+ </property>
+ <property name="muted" readonly="true">
+ <getter>
+ return this.getAttribute("muted") == "true";
+ </getter>
+ </property>
+ <property name="blocked" readonly="true">
+ <getter>
+ return this.getAttribute("blocked") == "true";
+ </getter>
+ </property>
+ <!--
+ Describes how the tab ended up in this mute state. May be any of:
+
+ - undefined: The tabs mute state has never changed.
+ - null: The mute state was last changed through the UI.
+ - Any string: The ID was changed through an extension API. The string
+ must be the ID of the extension which changed it.
+ -->
+ <field name="muteReason">undefined</field>
+
+ <property name="userContextId" readonly="true">
+ <getter>
+ return this.hasAttribute("usercontextid")
+ ? parseInt(this.getAttribute("usercontextid"))
+ : 0;
+ </getter>
+ </property>
+
+ <property name="soundPlaying" readonly="true">
+ <getter>
+ return this.getAttribute("soundplaying") == "true";
+ </getter>
+ </property>
+
+ <property name="lastAccessed">
+ <getter>
+ return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
+ </getter>
+ </property>
+ <method name="updateLastAccessed">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ this._lastAccessed = this.selected ? Infinity : (aDate || Date.now());
+ ]]></body>
+ </method>
+
+ <field name="cachePosition">Infinity</field>
+
+ <field name="mOverCloseButton">false</field>
+ <property name="_overPlayingIcon" readonly="true">
+ <getter><![CDATA[
+ let iconVisible = this.hasAttribute("soundplaying") ||
+ this.hasAttribute("muted") ||
+ this.hasAttribute("blocked");
+ let soundPlayingIcon =
+ document.getAnonymousElementByAttribute(this, "anonid", "soundplaying-icon");
+ let overlayIcon =
+ document.getAnonymousElementByAttribute(this, "anonid", "overlay-icon");
+
+ return soundPlayingIcon && soundPlayingIcon.matches(":hover") ||
+ (overlayIcon && overlayIcon.matches(":hover") && iconVisible);
+ ]]></getter>
+ </property>
+ <field name="mCorrespondingMenuitem">null</field>
+
+ <!--
+ While it would make sense to track this in a field, the field will get nuked
+ once the node is gone from the DOM, which causes us to think the tab is not
+ closed, which causes us to make wrong decisions. So we use an expando instead.
+ <field name="closing">false</field>
+ -->
+
+ <method name="_mouseenter">
+ <body><![CDATA[
+ if (this.hidden || this.closing)
+ return;
+
+ let tabContainer = this.parentNode;
+ let visibleTabs = tabContainer.tabbrowser.visibleTabs;
+ let tabIndex = visibleTabs.indexOf(this);
+ if (tabIndex == 0) {
+ tabContainer._beforeHoveredTab = null;
+ } else {
+ let candidate = visibleTabs[tabIndex - 1];
+ if (!candidate.selected) {
+ tabContainer._beforeHoveredTab = candidate;
+ candidate.setAttribute("beforehovered", "true");
+ }
+ }
+
+ if (tabIndex == visibleTabs.length - 1) {
+ tabContainer._afterHoveredTab = null;
+ } else {
+ let candidate = visibleTabs[tabIndex + 1];
+ if (!candidate.selected) {
+ tabContainer._afterHoveredTab = candidate;
+ candidate.setAttribute("afterhovered", "true");
+ }
+ }
+
+ tabContainer._hoveredTab = this;
+ ]]></body>
+ </method>
+
+ <method name="_mouseleave">
+ <body><![CDATA[
+ let tabContainer = this.parentNode;
+ if (tabContainer._beforeHoveredTab) {
+ tabContainer._beforeHoveredTab.removeAttribute("beforehovered");
+ tabContainer._beforeHoveredTab = null;
+ }
+ if (tabContainer._afterHoveredTab) {
+ tabContainer._afterHoveredTab.removeAttribute("afterhovered");
+ tabContainer._afterHoveredTab = null;
+ }
+
+ tabContainer._hoveredTab = null;
+ ]]></body>
+ </method>
+
+ <method name="toggleMuteAudio">
+ <parameter name="aMuteReason"/>
+ <body>
+ <![CDATA[
+ let tabContainer = this.parentNode;
+ let browser = this.linkedBrowser;
+ let modifiedAttrs = [];
+ if (browser.audioBlocked) {
+ this.removeAttribute("blocked");
+ modifiedAttrs.push("blocked");
+
+ // We don't want sound icon flickering between "blocked", "none" and
+ // "sound-playing", here adding the "soundplaying" is to keep the
+ // transition smoothly.
+ if (!this.hasAttribute("soundplaying")) {
+ this.setAttribute("soundplaying", true);
+ modifiedAttrs.push("soundplaying");
+ }
+
+ browser.resumeMedia();
+ } else {
+ if (browser.audioMuted) {
+ browser.unmute();
+ this.removeAttribute("muted");
+ BrowserUITelemetry.countTabMutingEvent("unmute", aMuteReason);
+ } else {
+ browser.mute();
+ this.setAttribute("muted", "true");
+ BrowserUITelemetry.countTabMutingEvent("mute", aMuteReason);
+ }
+ this.muteReason = aMuteReason || null;
+ modifiedAttrs.push("muted");
+ }
+ tabContainer.tabbrowser._tabAttrModified(this, modifiedAttrs);
+ ]]>
+ </body>
+ </method>
+
+ <method name="setUserContextId">
+ <parameter name="aUserContextId"/>
+ <body>
+ <![CDATA[
+ if (aUserContextId) {
+ if (this.linkedBrowser) {
+ this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
+ }
+ this.setAttribute("usercontextid", aUserContextId);
+ } else {
+ if (this.linkedBrowser) {
+ this.linkedBrowser.removeAttribute("usercontextid");
+ }
+ this.removeAttribute("usercontextid");
+ }
+
+ ContextualIdentityService.setTabStyle(this);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mouseover"><![CDATA[
+ let anonid = event.originalTarget.getAttribute("anonid");
+ if (anonid == "close-button")
+ this.mOverCloseButton = true;
+
+ this._mouseenter();
+ ]]></handler>
+ <handler event="mouseout"><![CDATA[
+ let anonid = event.originalTarget.getAttribute("anonid");
+ if (anonid == "close-button")
+ this.mOverCloseButton = false;
+
+ this._mouseleave();
+ ]]></handler>
+ <handler event="dragstart" phase="capturing">
+ this.style.MozUserFocus = '';
+ </handler>
+ <handler event="mousedown" phase="capturing">
+ <![CDATA[
+ if (this.selected) {
+ this.style.MozUserFocus = 'ignore';
+ this.clientTop; // just using this to flush style updates
+ } else if (this.mOverCloseButton ||
+ this._overPlayingIcon) {
+ // Prevent tabbox.xml from selecting the tab.
+ event.stopPropagation();
+ }
+ ]]>
+ </handler>
+ <handler event="mouseup">
+ this.style.MozUserFocus = '';
+ </handler>
+ <handler event="click">
+ <![CDATA[
+ if (event.button != 0) {
+ return;
+ }
+
+ if (this._overPlayingIcon) {
+ this.toggleMuteAudio();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabbrowser-alltabs-popup"
+ extends="chrome://global/content/bindings/popup.xml#popup">
+ <implementation implements="nsIDOMEventListener">
+ <method name="_tabOnAttrModified">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var tab = aEvent.target;
+ if (tab.mCorrespondingMenuitem)
+ this._setMenuitemAttributes(tab.mCorrespondingMenuitem, tab);
+ ]]></body>
+ </method>
+
+ <method name="_tabOnTabClose">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var tab = aEvent.target;
+ if (tab.mCorrespondingMenuitem)
+ this.removeChild(tab.mCorrespondingMenuitem);
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.type) {
+ case "TabAttrModified":
+ this._tabOnAttrModified(aEvent);
+ break;
+ case "TabClose":
+ this._tabOnTabClose(aEvent);
+ break;
+ case "scroll":
+ this._updateTabsVisibilityStatus();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_updateTabsVisibilityStatus">
+ <body><![CDATA[
+ var tabContainer = gBrowser.tabContainer;
+ // We don't want menu item decoration unless there is overflow.
+ if (tabContainer.getAttribute("overflow") != "true")
+ return;
+
+ var tabstripBO = tabContainer.mTabstrip.scrollBoxObject;
+ for (var i = 0; i < this.childNodes.length; i++) {
+ let curTab = this.childNodes[i].tab;
+ if (!curTab) // "Undo close tab", menuseparator, or entries put here by addons.
+ continue;
+ let curTabBO = curTab.boxObject;
+ if (curTabBO.screenX >= tabstripBO.screenX &&
+ curTabBO.screenX + curTabBO.width <= tabstripBO.screenX + tabstripBO.width)
+ this.childNodes[i].setAttribute("tabIsVisible", "true");
+ else
+ this.childNodes[i].removeAttribute("tabIsVisible");
+ }
+ ]]></body>
+ </method>
+
+ <method name="_createTabMenuItem">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ var menuItem = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "menuitem");
+
+ menuItem.setAttribute("class", "menuitem-iconic alltabs-item menuitem-with-favicon");
+
+ this._setMenuitemAttributes(menuItem, aTab);
+
+ aTab.mCorrespondingMenuitem = menuItem;
+ menuItem.tab = aTab;
+
+ this.appendChild(menuItem);
+ ]]></body>
+ </method>
+
+ <method name="_setMenuitemAttributes">
+ <parameter name="aMenuitem"/>
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ aMenuitem.setAttribute("label", aTab.label);
+ aMenuitem.setAttribute("crop", aTab.getAttribute("crop"));
+
+ if (aTab.hasAttribute("busy")) {
+ aMenuitem.setAttribute("busy", aTab.getAttribute("busy"));
+ aMenuitem.removeAttribute("image");
+ } else {
+ aMenuitem.setAttribute("image", aTab.getAttribute("image"));
+ aMenuitem.removeAttribute("busy");
+ }
+
+ if (aTab.hasAttribute("pending"))
+ aMenuitem.setAttribute("pending", aTab.getAttribute("pending"));
+ else
+ aMenuitem.removeAttribute("pending");
+
+ if (aTab.selected)
+ aMenuitem.setAttribute("selected", "true");
+ else
+ aMenuitem.removeAttribute("selected");
+
+ function addEndImage() {
+ let endImage = document.createElement("image");
+ endImage.setAttribute("class", "alltabs-endimage");
+ let endImageContainer = document.createElement("hbox");
+ endImageContainer.setAttribute("align", "center");
+ endImageContainer.setAttribute("pack", "center");
+ endImageContainer.appendChild(endImage);
+ aMenuitem.appendChild(endImageContainer);
+ return endImage;
+ }
+
+ if (aMenuitem.firstChild)
+ aMenuitem.firstChild.remove();
+ if (aTab.hasAttribute("muted"))
+ addEndImage().setAttribute("muted", "true");
+ else if (aTab.hasAttribute("soundplaying"))
+ addEndImage().setAttribute("soundplaying", "true");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing">
+ <![CDATA[
+ if (event.target.getAttribute("id") == "alltabs_containersMenuTab") {
+ createUserContextMenu(event);
+ return;
+ }
+
+ let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
+
+ if (event.target.getAttribute("anonid") == "newtab-popup" ||
+ event.target.id == "newtab-popup") {
+ createUserContextMenu(event);
+ } else {
+ document.getElementById("alltabs-popup-separator-1").hidden = !containersEnabled;
+ let containersTab = document.getElementById("alltabs_containersTab");
+
+ containersTab.hidden = !containersEnabled;
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ containersTab.setAttribute("disabled", "true");
+ }
+
+ document.getElementById("alltabs_undoCloseTab").disabled =
+ SessionStore.getClosedTabCount(window) == 0;
+
+ var tabcontainer = gBrowser.tabContainer;
+
+ // Listen for changes in the tab bar.
+ tabcontainer.addEventListener("TabAttrModified", this, false);
+ tabcontainer.addEventListener("TabClose", this, false);
+ tabcontainer.mTabstrip.addEventListener("scroll", this, false);
+
+ let tabs = gBrowser.visibleTabs;
+ for (var i = 0; i < tabs.length; i++) {
+ if (!tabs[i].pinned)
+ this._createTabMenuItem(tabs[i]);
+ }
+ this._updateTabsVisibilityStatus();
+ }
+ ]]></handler>
+
+ <handler event="popuphidden">
+ <![CDATA[
+ if (event.target.getAttribute("id") == "alltabs_containersMenuTab") {
+ return;
+ }
+
+ // clear out the menu popup and remove the listeners
+ for (let i = this.childNodes.length - 1; i > 0; i--) {
+ let menuItem = this.childNodes[i];
+ if (menuItem.tab) {
+ menuItem.tab.mCorrespondingMenuitem = null;
+ this.removeChild(menuItem);
+ }
+ if (menuItem.hasAttribute("usercontextid")) {
+ this.removeChild(menuItem);
+ }
+ }
+ var tabcontainer = gBrowser.tabContainer;
+ tabcontainer.mTabstrip.removeEventListener("scroll", this, false);
+ tabcontainer.removeEventListener("TabAttrModified", this, false);
+ tabcontainer.removeEventListener("TabClose", this, false);
+ ]]></handler>
+
+ <handler event="DOMMenuItemActive">
+ <![CDATA[
+ var tab = event.target.tab;
+ if (tab) {
+ let overLink = tab.linkedBrowser.currentURI.spec;
+ if (overLink == "about:blank")
+ overLink = "";
+ XULBrowserWindow.setOverLink(overLink, null);
+ }
+ ]]></handler>
+
+ <handler event="DOMMenuItemInactive">
+ <![CDATA[
+ XULBrowserWindow.setOverLink("", null);
+ ]]></handler>
+
+ <handler event="command"><![CDATA[
+ if (event.target.tab)
+ gBrowser.selectedTab = event.target.tab;
+ ]]></handler>
+
+ </handlers>
+ </binding>
+
+ <binding id="statuspanel" display="xul:hbox">
+ <content>
+ <xul:hbox class="statuspanel-inner">
+ <xul:label class="statuspanel-label"
+ role="status"
+ aria-live="off"
+ xbl:inherits="value=label,crop,mirror"
+ flex="1"
+ crop="end"/>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMEventListener">
+ <constructor><![CDATA[
+ window.addEventListener("resize", this, false);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ window.removeEventListener("resize", this, false);
+ MousePosTracker.removeListener(this);
+ ]]></destructor>
+
+ <property name="label">
+ <setter><![CDATA[
+ if (!this.label) {
+ this.removeAttribute("mirror");
+ this.removeAttribute("sizelimit");
+ }
+
+ this.style.minWidth = this.getAttribute("type") == "status" &&
+ this.getAttribute("previoustype") == "status"
+ ? getComputedStyle(this).width : "";
+
+ if (val) {
+ this.setAttribute("label", val);
+ this.removeAttribute("inactive");
+ this._calcMouseTargetRect();
+ MousePosTracker.addListener(this);
+ } else {
+ this.setAttribute("inactive", "true");
+ MousePosTracker.removeListener(this);
+ }
+
+ return val;
+ ]]></setter>
+ <getter>
+ return this.hasAttribute("inactive") ? "" : this.getAttribute("label");
+ </getter>
+ </property>
+
+ <method name="getMouseTargetRect">
+ <body><![CDATA[
+ return this._mouseTargetRect;
+ ]]></body>
+ </method>
+
+ <method name="onMouseEnter">
+ <body>
+ this._mirror();
+ </body>
+ </method>
+
+ <method name="onMouseLeave">
+ <body>
+ this._mirror();
+ </body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="event"/>
+ <body><![CDATA[
+ if (!this.label)
+ return;
+
+ switch (event.type) {
+ case "resize":
+ this._calcMouseTargetRect();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_calcMouseTargetRect">
+ <body><![CDATA[
+ let container = this.parentNode;
+ let alignRight = (getComputedStyle(container).direction == "rtl");
+ let panelRect = this.getBoundingClientRect();
+ let containerRect = container.getBoundingClientRect();
+
+ this._mouseTargetRect = {
+ top: panelRect.top,
+ bottom: panelRect.bottom,
+ left: alignRight ? containerRect.right - panelRect.width : containerRect.left,
+ right: alignRight ? containerRect.right : containerRect.left + panelRect.width
+ };
+ ]]></body>
+ </method>
+
+ <method name="_mirror">
+ <body>
+ if (this.hasAttribute("mirror"))
+ this.removeAttribute("mirror");
+ else
+ this.setAttribute("mirror", "true");
+
+ if (!this.hasAttribute("sizelimit")) {
+ this.setAttribute("sizelimit", "true");
+ this._calcMouseTargetRect();
+ }
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="tabbrowser-tabpanels"
+ extends="chrome://global/content/bindings/tabbox.xml#tabpanels">
+ <implementation>
+ <field name="_selectedIndex">0</field>
+
+ <property name="selectedIndex">
+ <getter>
+ <![CDATA[
+ return this._selectedIndex;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ if (val < 0 || val >= this.childNodes.length)
+ return val;
+
+ let toTab = this.getRelatedElement(this.childNodes[val]);
+
+ gBrowser._getSwitcher().requestTab(toTab);
+
+ var panel = this._selectedPanel;
+ var newPanel = this.childNodes[val];
+ this._selectedPanel = newPanel;
+ if (this._selectedPanel != panel) {
+ var event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.dispatchEvent(event);
+
+ this._selectedIndex = val;
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="tabbrowser-browser"
+ extends="chrome://global/content/bindings/browser.xml#browser">
+ <implementation>
+ <field name="tabModalPromptBox">null</field>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURIWithFlags">
+ <parameter name="aURI"/>
+ <parameter name="aFlags"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <body>
+ <![CDATA[
+ var params = arguments[1];
+ if (typeof(params) == "number") {
+ params = {
+ flags: aFlags,
+ referrerURI: aReferrerURI,
+ charset: aCharset,
+ postData: aPostData,
+ };
+ }
+ _loadURIWithFlags(this, aURI, params);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="tabbrowser-remote-browser"
+ extends="chrome://global/content/bindings/remote-browser.xml#remote-browser">
+ <implementation>
+ <field name="tabModalPromptBox">null</field>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURIWithFlags">
+ <parameter name="aURI"/>
+ <parameter name="aFlags"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <body>
+ <![CDATA[
+ var params = arguments[1];
+ if (typeof(params) == "number") {
+ params = {
+ flags: aFlags,
+ referrerURI: aReferrerURI,
+ charset: aCharset,
+ postData: aPostData,
+ };
+ }
+ _loadURIWithFlags(this, aURI, params);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/browser/base/content/test/alerts/.eslintrc.js b/browser/base/content/test/alerts/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/alerts/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/alerts/browser.ini b/browser/base/content/test/alerts/browser.ini
new file mode 100644
index 000000000..07fcf5253
--- /dev/null
+++ b/browser/base/content/test/alerts/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files =
+ head.js
+ file_dom_notifications.html
+
+[browser_notification_close.js]
+[browser_notification_do_not_disturb.js]
+[browser_notification_open_settings.js]
+[browser_notification_remove_permission.js]
+[browser_notification_permission_migration.js]
+[browser_notification_replace.js]
+[browser_notification_tab_switching.js]
diff --git a/browser/base/content/test/alerts/browser_notification_close.js b/browser/base/content/test/alerts/browser_notification_close.js
new file mode 100644
index 000000000..bbd444212
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_close.js
@@ -0,0 +1,71 @@
+"use strict";
+
+const {PlacesTestUtils} =
+ Cu.import("resource://testing-common/PlacesTestUtils.jsm", {});
+
+let notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+let oldShowFavicons;
+
+add_task(function* test_notificationClose() {
+ let pm = Services.perms;
+ let notificationURI = makeURI(notificationURL);
+ pm.add(notificationURI, "desktop-notification", pm.ALLOW_ACTION);
+
+ oldShowFavicons = Services.prefs.getBoolPref("alerts.showFavicons");
+ Services.prefs.setBoolPref("alerts.showFavicons", true);
+
+ yield PlacesTestUtils.addVisits(notificationURI);
+ let faviconURI = yield new Promise(resolve => {
+ let faviconURI = makeURI("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC");
+ PlacesUtils.favicons.setAndFetchFaviconForPage(notificationURI, faviconURI,
+ true, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ (faviconURI, iconSize, iconData, mimeType) => resolve(faviconURI),
+ Services.scriptSecurityManager.getSystemPrincipal());
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: notificationURL
+ }, function* dummyTabTask(aBrowser) {
+ yield openNotification(aBrowser, "showNotification2");
+
+ info("Notification alert showing");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ yield closeNotification(aBrowser);
+ return;
+ }
+
+ let alertTitleLabel = alertWindow.document.getElementById("alertTitleLabel");
+ is(alertTitleLabel.value, "Test title", "Title text of notification should be present");
+ let alertTextLabel = alertWindow.document.getElementById("alertTextLabel");
+ is(alertTextLabel.textContent, "Test body 2", "Body text of notification should be present");
+ let alertIcon = alertWindow.document.getElementById("alertIcon");
+ is(alertIcon.src, faviconURI.spec, "Icon of notification should be present");
+
+ let alertCloseButton = alertWindow.document.querySelector(".alertCloseButton");
+ is(alertCloseButton.localName, "toolbarbutton", "close button found");
+ let promiseBeforeUnloadEvent =
+ BrowserTestUtils.waitForEvent(alertWindow, "beforeunload");
+ let closedTime = alertWindow.Date.now();
+ alertCloseButton.click();
+ info("Clicked on close button");
+ yield promiseBeforeUnloadEvent;
+
+ ok(true, "Alert should close when the close button is clicked");
+ let currentTime = alertWindow.Date.now();
+ // The notification will self-close at 12 seconds, so this checks
+ // that the notification closed before the timeout.
+ ok(currentTime - closedTime < 5000,
+ "Close requested at " + closedTime + ", actually closed at " + currentTime);
+ });
+});
+
+add_task(function* cleanup() {
+ Services.perms.remove(makeURI(notificationURL), "desktop-notification");
+ if (typeof oldShowFavicons == "boolean") {
+ Services.prefs.setBoolPref("alerts.showFavicons", oldShowFavicons);
+ }
+});
diff --git a/browser/base/content/test/alerts/browser_notification_do_not_disturb.js b/browser/base/content/test/alerts/browser_notification_do_not_disturb.js
new file mode 100644
index 000000000..92c689fd2
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_do_not_disturb.js
@@ -0,0 +1,80 @@
+"use strict";
+
+var tab;
+var notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+
+const ALERT_SERVICE = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIAlertsDoNotDisturb);
+
+function test () {
+ waitForExplicitFinish();
+
+ try {
+ // Only run the test if the do-not-disturb
+ // interface has been implemented.
+ ALERT_SERVICE.manualDoNotDisturb;
+ ok(true, "Alert service implements do-not-disturb interface");
+ } catch (e) {
+ ok(true, "Alert service doesn't implement do-not-disturb interface, exiting test");
+ finish();
+ return;
+ }
+
+ let pm = Services.perms;
+ registerCleanupFunction(function() {
+ ALERT_SERVICE.manualDoNotDisturb = false;
+ pm.remove(makeURI(notificationURL), "desktop-notification");
+ gBrowser.removeTab(tab);
+ window.restore();
+ });
+
+ pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
+
+ // Make sure that do-not-disturb is not enabled.
+ ok(!ALERT_SERVICE.manualDoNotDisturb, "Alert service should not be disabled when test starts");
+ ALERT_SERVICE.manualDoNotDisturb = false;
+
+ tab = gBrowser.addTab(notificationURL);
+ gBrowser.selectedTab = tab;
+ tab.linkedBrowser.addEventListener("load", onLoad, true);
+}
+
+function onLoad() {
+ tab.linkedBrowser.removeEventListener("load", onLoad, true);
+ openNotification(tab.linkedBrowser, "showNotification2").then(onAlertShowing);
+}
+
+function onAlertShowing() {
+ info("Notification alert showing");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ closeNotification(tab.linkedBrowser).then(finish);
+ return;
+ }
+ let doNotDisturbMenuItem = alertWindow.document.getElementById("doNotDisturbMenuItem");
+ is(doNotDisturbMenuItem.localName, "menuitem", "menuitem found");
+ alertWindow.addEventListener("beforeunload", onAlertClosing);
+ doNotDisturbMenuItem.click();
+ info("Clicked on do-not-disturb menuitem");
+}
+
+function onAlertClosing(event) {
+ event.target.removeEventListener("beforeunload", onAlertClosing);
+
+ ok(ALERT_SERVICE.manualDoNotDisturb, "Alert service should be disabled after clicking menuitem");
+
+ // The notification should not appear, but there is
+ // no way from the client-side to know that it was
+ // blocked, except for waiting some time and realizing
+ // that the "onshow" event never fired.
+ openNotification(tab.linkedBrowser, "showNotification2", 2000)
+ .then(onAlert2Showing, finish);
+}
+
+function onAlert2Showing() {
+ ok(false, "the second alert should not have been shown");
+ closeNotification(tab.linkedBrowser).then(finish);
+}
diff --git a/browser/base/content/test/alerts/browser_notification_open_settings.js b/browser/base/content/test/alerts/browser_notification_open_settings.js
new file mode 100644
index 000000000..5306fd90a
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_open_settings.js
@@ -0,0 +1,58 @@
+"use strict";
+
+var notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+
+add_task(function* test_settingsOpen_observer() {
+ info("Opening a dummy tab so openPreferences=>switchToTabHavingURI doesn't use the blank tab.");
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "about:robots"
+ }, function* dummyTabTask(aBrowser) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:preferences#content");
+ info("simulate a notifications-open-settings notification");
+ let uri = NetUtil.newURI("https://example.com");
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+ Services.obs.notifyObservers(principal, "notifications-open-settings", null);
+ let tab = yield tabPromise;
+ ok(tab, "The notification settings tab opened");
+ yield BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(function* test_settingsOpen_button() {
+ let pm = Services.perms;
+ info("Adding notification permission");
+ pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
+
+ try {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: notificationURL
+ }, function* tabTask(aBrowser) {
+ info("Waiting for notification");
+ yield openNotification(aBrowser, "showNotification2");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ yield closeNotification(aBrowser);
+ return;
+ }
+
+ let closePromise = promiseWindowClosed(alertWindow);
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:preferences#content");
+ let openSettingsMenuItem = alertWindow.document.getElementById("openSettingsMenuItem");
+ openSettingsMenuItem.click();
+
+ info("Waiting for notification settings tab");
+ let tab = yield tabPromise;
+ ok(tab, "The notification settings tab opened");
+
+ yield closePromise;
+ yield BrowserTestUtils.removeTab(tab);
+ });
+ } finally {
+ info("Removing notification permission");
+ pm.remove(makeURI(notificationURL), "desktop-notification");
+ }
+});
diff --git a/browser/base/content/test/alerts/browser_notification_permission_migration.js b/browser/base/content/test/alerts/browser_notification_permission_migration.js
new file mode 100644
index 000000000..b015e59a7
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_permission_migration.js
@@ -0,0 +1,45 @@
+const UI_VERSION = 32;
+
+var gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"]
+ .getService(Ci.nsIObserver);
+var notificationURI = makeURI("http://example.org");
+var pm = Services.perms;
+var currentUIVersion;
+
+add_task(function* setup() {
+ currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ pm.add(notificationURI, "desktop-notification", pm.ALLOW_ACTION);
+});
+
+add_task(function* test_permissionMigration() {
+ if ("@mozilla.org/system-alerts-service;1" in Cc) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ return;
+ }
+
+ info("Waiting for migration notification");
+ let alertWindowPromise = promiseAlertWindow();
+ gBrowserGlue.observe(null, "browser-glue-test", "force-ui-migration");
+ let alertWindow = yield alertWindowPromise;
+
+ info("Clicking on notification");
+ let url =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "push#w_upgraded-notifications";
+ let closePromise = promiseWindowClosed(alertWindow);
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url);
+ EventUtils.synthesizeMouseAtCenter(alertWindow.document.getElementById("alertTitleLabel"), {}, alertWindow);
+
+ info("Waiting for migration info tab");
+ let tab = yield tabPromise;
+ ok(tab, "The migration info tab opened");
+
+ yield closePromise;
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* cleanup() {
+ Services.prefs.setIntPref("browser.migration.version", currentUIVersion);
+ pm.remove(notificationURI, "desktop-notification");
+});
diff --git a/browser/base/content/test/alerts/browser_notification_remove_permission.js b/browser/base/content/test/alerts/browser_notification_remove_permission.js
new file mode 100644
index 000000000..bd36faeae
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_remove_permission.js
@@ -0,0 +1,72 @@
+"use strict";
+
+var tab;
+var notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+var alertWindowClosed = false;
+var permRemoved = false;
+
+function test () {
+ waitForExplicitFinish();
+
+ let pm = Services.perms;
+ registerCleanupFunction(function() {
+ pm.remove(makeURI(notificationURL), "desktop-notification");
+ gBrowser.removeTab(tab);
+ window.restore();
+ });
+
+ pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
+
+ tab = gBrowser.addTab(notificationURL);
+ gBrowser.selectedTab = tab;
+ tab.linkedBrowser.addEventListener("load", onLoad, true);
+}
+
+function onLoad() {
+ tab.linkedBrowser.removeEventListener("load", onLoad, true);
+ openNotification(tab.linkedBrowser, "showNotification2").then(onAlertShowing);
+}
+
+function onAlertShowing() {
+ info("Notification alert showing");
+
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ closeNotification(tab.linkedBrowser).then(finish);
+ return;
+ }
+ ok(Services.perms.testExactPermission(makeURI(notificationURL), "desktop-notification"),
+ "Permission should exist prior to removal");
+ let disableForOriginMenuItem = alertWindow.document.getElementById("disableForOriginMenuItem");
+ is(disableForOriginMenuItem.localName, "menuitem", "menuitem found");
+ Services.obs.addObserver(permObserver, "perm-changed", false);
+ alertWindow.addEventListener("beforeunload", onAlertClosing);
+ disableForOriginMenuItem.click();
+ info("Clicked on disable-for-origin menuitem")
+}
+
+function permObserver(subject, topic, data) {
+ if (topic != "perm-changed") {
+ return;
+ }
+
+ let permission = subject.QueryInterface(Ci.nsIPermission);
+ is(permission.type, "desktop-notification", "desktop-notification permission changed");
+ is(data, "deleted", "desktop-notification permission deleted");
+
+ Services.obs.removeObserver(permObserver, "perm-changed");
+ permRemoved = true;
+ if (alertWindowClosed) {
+ finish();
+ }
+}
+
+function onAlertClosing(event) {
+ event.target.removeEventListener("beforeunload", onAlertClosing);
+
+ alertWindowClosed = true;
+ if (permRemoved) {
+ finish();
+ }
+}
diff --git a/browser/base/content/test/alerts/browser_notification_replace.js b/browser/base/content/test/alerts/browser_notification_replace.js
new file mode 100644
index 000000000..e678dc438
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_replace.js
@@ -0,0 +1,38 @@
+"use strict";
+
+let notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+
+add_task(function* test_notificationReplace() {
+ let pm = Services.perms;
+ pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: notificationURL
+ }, function* dummyTabTask(aBrowser) {
+ yield ContentTask.spawn(aBrowser, {}, function* () {
+ let win = content.window.wrappedJSObject;
+ let notification = win.showNotification1();
+ let promiseCloseEvent = ContentTaskUtils.waitForEvent(notification, "close");
+
+ let showEvent = yield ContentTaskUtils.waitForEvent(notification, "show");
+ Assert.equal(showEvent.target.body, "Test body 1", "Showed tagged notification");
+
+ let newNotification = win.showNotification2();
+ let newShowEvent = yield ContentTaskUtils.waitForEvent(newNotification, "show");
+ Assert.equal(newShowEvent.target.body, "Test body 2", "Showed new notification with same tag");
+
+ let closeEvent = yield promiseCloseEvent;
+ Assert.equal(closeEvent.target.body, "Test body 1", "Closed previous tagged notification");
+
+ let promiseNewCloseEvent = ContentTaskUtils.waitForEvent(newNotification, "close");
+ newNotification.close();
+ let newCloseEvent = yield promiseNewCloseEvent;
+ Assert.equal(newCloseEvent.target.body, "Test body 2", "Closed new notification");
+ });
+ });
+});
+
+add_task(function* cleanup() {
+ Services.perms.remove(makeURI(notificationURL), "desktop-notification");
+});
diff --git a/browser/base/content/test/alerts/browser_notification_tab_switching.js b/browser/base/content/test/alerts/browser_notification_tab_switching.js
new file mode 100644
index 000000000..7e46c0722
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_tab_switching.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+var tab;
+var notification;
+var notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+var newWindowOpenedFromTab;
+
+add_task(function* test_notificationPreventDefaultAndSwitchTabs() {
+ let pm = Services.perms;
+ pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
+
+ let originalTab = gBrowser.selectedTab;
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: notificationURL
+ }, function* dummyTabTask(aBrowser) {
+ // Put new tab in background so it is obvious when it is re-focused.
+ yield BrowserTestUtils.switchTab(gBrowser, originalTab);
+ isnot(gBrowser.selectedBrowser, aBrowser, "Notification page loaded as a background tab");
+
+ // First, show a notification that will be have the tab-switching prevented.
+ function promiseNotificationEvent(evt) {
+ return ContentTask.spawn(aBrowser, evt, function* (evt) {
+ return yield new Promise(resolve => {
+ let notification = content.wrappedJSObject._notification;
+ notification.addEventListener(evt, function l(event) {
+ notification.removeEventListener(evt, l);
+ resolve({ defaultPrevented: event.defaultPrevented });
+ });
+ });
+ });
+ }
+ yield openNotification(aBrowser, "showNotification1");
+ info("Notification alert showing");
+ let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ if (!alertWindow) {
+ ok(true, "Notifications don't use XUL windows on all platforms.");
+ yield closeNotification(aBrowser);
+ return;
+ }
+ info("Clicking on notification");
+ let promiseClickEvent = promiseNotificationEvent("click");
+
+ // NB: This executeSoon is needed to allow the non-e10s runs of this test
+ // a chance to set the event listener on the page. Otherwise, we
+ // synchronously fire the click event before we listen for the event.
+ executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(alertWindow.document.getElementById("alertTitleLabel"),
+ {}, alertWindow);
+ });
+ let clickEvent = yield promiseClickEvent;
+ ok(clickEvent.defaultPrevented, "The event handler for the first notification cancels the event");
+ isnot(gBrowser.selectedBrowser, aBrowser, "Notification page still a background tab");
+ let notificationClosed = promiseNotificationEvent("close");
+ yield closeNotification(aBrowser);
+ yield notificationClosed;
+
+ // Second, show a notification that will cause the tab to get switched.
+ yield openNotification(aBrowser, "showNotification2");
+ alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+ let promiseTabSelect = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabSelect");
+ EventUtils.synthesizeMouseAtCenter(alertWindow.document.getElementById("alertTitleLabel"),
+ {},
+ alertWindow);
+ yield promiseTabSelect;
+ is(gBrowser.selectedBrowser.currentURI.spec, notificationURL,
+ "Clicking on the second notification should select its originating tab");
+ notificationClosed = promiseNotificationEvent("close");
+ yield closeNotification(aBrowser);
+ yield notificationClosed;
+ });
+});
+
+add_task(function* cleanup() {
+ Services.perms.remove(makeURI(notificationURL), "desktop-notification");
+});
diff --git a/browser/base/content/test/alerts/file_dom_notifications.html b/browser/base/content/test/alerts/file_dom_notifications.html
new file mode 100644
index 000000000..6deede8fc
--- /dev/null
+++ b/browser/base/content/test/alerts/file_dom_notifications.html
@@ -0,0 +1,39 @@
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+"use strict";
+
+function showNotification1() {
+ var options = {
+ dir: undefined,
+ lang: undefined,
+ body: "Test body 1",
+ tag: "Test tag",
+ icon: undefined,
+ };
+ var n = new Notification("Test title", options);
+ n.addEventListener("click", function(event) {
+ event.preventDefault();
+ });
+ return n;
+}
+
+function showNotification2() {
+ var options = {
+ dir: undefined,
+ lang: undefined,
+ body: "Test body 2",
+ tag: "Test tag",
+ icon: undefined,
+ };
+ return new Notification("Test title", options);
+}
+</script>
+</head>
+<body>
+<form id="notificationForm" onsubmit="showNotification();">
+ <input type="submit" value="Show notification" id="submit"/>
+</form>
+</body>
+</html>
diff --git a/browser/base/content/test/alerts/head.js b/browser/base/content/test/alerts/head.js
new file mode 100644
index 000000000..21257de31
--- /dev/null
+++ b/browser/base/content/test/alerts/head.js
@@ -0,0 +1,71 @@
+function promiseAlertWindow() {
+ return new Promise(function(resolve) {
+ let listener = {
+ onOpenWindow(window) {
+ let alertWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ alertWindow.addEventListener("load", function onLoad() {
+ alertWindow.removeEventListener("load", onLoad);
+ let windowType = alertWindow.document.documentElement.getAttribute("windowtype");
+ if (windowType != "alert:alert") {
+ return;
+ }
+ Services.wm.removeListener(listener);
+ resolve(alertWindow);
+ });
+ },
+ };
+ Services.wm.addListener(listener);
+ });
+}
+
+/**
+ * Similar to `BrowserTestUtils.closeWindow`, but
+ * doesn't call `window.close()`.
+ */
+function promiseWindowClosed(window) {
+ return new Promise(function(resolve) {
+ Services.ww.registerNotification(function observer(subject, topic, data) {
+ if (topic == "domwindowclosed" && subject == window) {
+ Services.ww.unregisterNotification(observer);
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * These two functions work with file_dom_notifications.html to open the
+ * notification and close it.
+ *
+ * |fn| can be showNotification1 or showNotification2.
+ * if |timeout| is passed, then the promise returned from this function is
+ * rejected after the requested number of miliseconds.
+ */
+function openNotification(aBrowser, fn, timeout) {
+ return ContentTask.spawn(aBrowser, { fn, timeout }, function* ({ fn, timeout }) {
+ let win = content.wrappedJSObject;
+ let notification = win[fn]();
+ win._notification = notification;
+ yield new Promise((resolve, reject) => {
+ function listener() {
+ notification.removeEventListener("show", listener);
+ resolve();
+ }
+
+ notification.addEventListener("show", listener);
+
+ if (timeout) {
+ content.setTimeout(() => {
+ notification.removeEventListener("show", listener);
+ reject("timed out");
+ }, timeout);
+ }
+ });
+ });
+}
+
+function closeNotification(aBrowser) {
+ return ContentTask.spawn(aBrowser, null, function() {
+ content.wrappedJSObject._notification.close();
+ });
+}
diff --git a/browser/base/content/test/captivePortal/browser.ini b/browser/base/content/test/captivePortal/browser.ini
new file mode 100644
index 000000000..cfdbc5c2f
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_CaptivePortalWatcher.js]
+skip-if = os == "win" # Bug 1313894
+[browser_CaptivePortalWatcher_1.js]
+skip-if = os == "win" # Bug 1313894
+[browser_captivePortal_certErrorUI.js]
diff --git a/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
new file mode 100644
index 000000000..e9c0fad6d
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
@@ -0,0 +1,119 @@
+"use strict";
+
+add_task(setupPrefsAndRecentWindowBehavior);
+
+// Each of the test cases below is run twice: once for login-success and once
+// for login-abort (aSuccess set to true and false respectively).
+let testCasesForBothSuccessAndAbort = [
+ /**
+ * A portal is detected when there's no browser window, then a browser
+ * window is opened, then the portal is freed.
+ * The portal tab should be added and focused when the window is
+ * opened, and closed automatically when the success event is fired.
+ * The captive portal notification should be shown when the window is
+ * opened, and closed automatically when the success event is fired.
+ */
+ function* test_detectedWithNoBrowserWindow_Open(aSuccess) {
+ yield portalDetected();
+ let win = yield focusWindowAndWaitForPortalUI();
+ yield freePortal(aSuccess);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ yield closeWindowAndWaitForXulWindowVisible(win);
+ },
+
+ /**
+ * A portal is detected when multiple browser windows are open but none
+ * have focus. A brower window is focused, then the portal is freed.
+ * The portal tab should be added and focused when the window is
+ * focused, and closed automatically when the success event is fired.
+ * The captive portal notification should be shown in all windows upon
+ * detection, and closed automatically when the success event is fired.
+ */
+ function* test_detectedWithNoBrowserWindow_Focused(aSuccess) {
+ let win1 = yield openWindowAndWaitForFocus();
+ let win2 = yield openWindowAndWaitForFocus();
+ // Defocus both windows.
+ yield SimpleTest.promiseFocus(window);
+
+ yield portalDetected();
+
+ // Notification should be shown in both windows.
+ ensurePortalNotification(win1);
+ ensureNoPortalTab(win1);
+ ensurePortalNotification(win2);
+ ensureNoPortalTab(win2);
+
+ yield focusWindowAndWaitForPortalUI(false, win2);
+
+ yield freePortal(aSuccess);
+
+ ensureNoPortalNotification(win1);
+ ensureNoPortalTab(win2);
+ ensureNoPortalNotification(win2);
+
+ yield closeWindowAndWaitForXulWindowVisible(win2);
+ // No need to wait for xul-window-visible: after win2 is closed, focus
+ // is restored to the default window and win1 remains in the background.
+ yield BrowserTestUtils.closeWindow(win1);
+ },
+
+ /**
+ * A portal is detected when there's no browser window, then a browser
+ * window is opened, then the portal is freed.
+ * The recheck triggered when the browser window is opened takes a
+ * long time. No portal tab should be added.
+ * The captive portal notification should be shown when the window is
+ * opened, and closed automatically when the success event is fired.
+ */
+ function* test_detectedWithNoBrowserWindow_LongRecheck(aSuccess) {
+ yield portalDetected();
+ let win = yield focusWindowAndWaitForPortalUI(true);
+ yield freePortal(aSuccess);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ yield closeWindowAndWaitForXulWindowVisible(win);
+ },
+
+ /**
+ * A portal is detected when there's no browser window, and the
+ * portal is freed before a browser window is opened. No portal
+ * UI should be shown when a browser window is opened.
+ */
+ function* test_detectedWithNoBrowserWindow_GoneBeforeOpen(aSuccess) {
+ yield portalDetected();
+ yield freePortal(aSuccess);
+ let win = yield openWindowAndWaitForFocus();
+ // Wait for a while to make sure no UI is shown.
+ yield new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ });
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ yield closeWindowAndWaitForXulWindowVisible(win);
+ },
+
+ /**
+ * A portal is detected when a browser window has focus. No portal tab should
+ * be opened. A notification bar should be displayed in all browser windows.
+ */
+ function* test_detectedWithFocus(aSuccess) {
+ let win1 = yield openWindowAndWaitForFocus();
+ let win2 = yield openWindowAndWaitForFocus();
+ yield portalDetected();
+ ensureNoPortalTab(win1);
+ ensureNoPortalTab(win2);
+ ensurePortalNotification(win1);
+ ensurePortalNotification(win2);
+ yield freePortal(aSuccess);
+ ensureNoPortalNotification(win1);
+ ensureNoPortalNotification(win2);
+ yield closeWindowAndWaitForXulWindowVisible(win2);
+ yield closeWindowAndWaitForXulWindowVisible(win1);
+ },
+];
+
+for (let testcase of testCasesForBothSuccessAndAbort) {
+ add_task(testcase.bind(null, true));
+ add_task(testcase.bind(null, false));
+}
diff --git a/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js
new file mode 100644
index 000000000..71b12c32a
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher_1.js
@@ -0,0 +1,91 @@
+"use strict";
+
+add_task(setupPrefsAndRecentWindowBehavior);
+
+let testcases = [
+ /**
+ * A portal is detected when there's no browser window,
+ * then a browser window is opened, and the portal is logged into
+ * and redirects to a different page. The portal tab should be added
+ * and focused when the window is opened, and left open after login
+ * since it redirected.
+ */
+ function* test_detectedWithNoBrowserWindow_Redirect() {
+ yield portalDetected();
+ let win = yield focusWindowAndWaitForPortalUI();
+ let browser = win.gBrowser.selectedTab.linkedBrowser;
+ let loadPromise =
+ BrowserTestUtils.browserLoaded(browser, false, CANONICAL_URL_REDIRECTED);
+ BrowserTestUtils.loadURI(browser, CANONICAL_URL_REDIRECTED);
+ yield loadPromise;
+ yield freePortal(true);
+ ensurePortalTab(win);
+ ensureNoPortalNotification(win);
+ yield closeWindowAndWaitForXulWindowVisible(win);
+ },
+
+ /**
+ * Test the various expected behaviors of the "Show Login Page" button
+ * in the captive portal notification. The button should be visible for
+ * all tabs except the captive portal tab, and when clicked, should
+ * ensure a captive portal tab is open and select it.
+ */
+ function* test_showLoginPageButton() {
+ let win = yield openWindowAndWaitForFocus();
+ yield portalDetected();
+ let notification = ensurePortalNotification(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+
+ function testPortalTabSelectedAndButtonNotVisible() {
+ is(win.gBrowser.selectedTab, tab, "The captive portal tab should be selected.");
+ testShowLoginPageButtonVisibility(notification, "hidden");
+ }
+
+ let button = notification.querySelector("button.notification-button");
+ function* clickButtonAndExpectNewPortalTab() {
+ let p = BrowserTestUtils.waitForNewTab(win.gBrowser, CANONICAL_URL);
+ button.click();
+ let tab = yield p;
+ is(win.gBrowser.selectedTab, tab, "The captive portal tab should be selected.");
+ return tab;
+ }
+
+ // Simulate clicking the button. The portal tab should be opened and
+ // selected and the button should hide.
+ let tab = yield clickButtonAndExpectNewPortalTab();
+ testPortalTabSelectedAndButtonNotVisible();
+
+ // Close the tab. The button should become visible.
+ yield BrowserTestUtils.removeTab(tab);
+ ensureNoPortalTab(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+
+ // When the button is clicked, a new portal tab should be opened and
+ // selected.
+ tab = yield clickButtonAndExpectNewPortalTab();
+
+ // Open another arbitrary tab. The button should become visible. When it's clicked,
+ // the portal tab should be selected.
+ let anotherTab = yield BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ testShowLoginPageButtonVisibility(notification, "visible");
+ button.click();
+ is(win.gBrowser.selectedTab, tab, "The captive portal tab should be selected.");
+
+ // Close the portal tab and select the arbitrary tab. The button should become
+ // visible and when it's clicked, a new portal tab should be opened.
+ yield BrowserTestUtils.removeTab(tab);
+ win.gBrowser.selectedTab = anotherTab;
+ testShowLoginPageButtonVisibility(notification, "visible");
+ tab = yield clickButtonAndExpectNewPortalTab();
+
+ yield BrowserTestUtils.removeTab(anotherTab);
+ yield freePortal(true);
+ ensureNoPortalTab(win);
+ ensureNoPortalNotification(win);
+ yield closeWindowAndWaitForXulWindowVisible(win);
+ },
+];
+
+for (let testcase of testcases) {
+ add_task(testcase);
+}
diff --git a/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
new file mode 100644
index 000000000..6b97e19a3
--- /dev/null
+++ b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BAD_CERT_PAGE = "https://expired.example.com/";
+
+// This tests the alternate cert error UI when we are behind a captive portal.
+
+add_task(function* checkCaptivePortalCertErrorUI() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["captivedetect.canonicalURL", CANONICAL_URL],
+ ["captivedetect.canonicalContent", CANONICAL_CONTENT]],
+ });
+
+ let captivePortalStatePropagated = TestUtils.topicObserved("ipc:network:captive-portal-set-state");
+
+ info("Checking that the alternate about:certerror UI is shown when we are behind a captive portal.");
+ Services.obs.notifyObservers(null, "captive-portal-login", null);
+
+ info("Waiting for captive portal state to be propagated to the content process.");
+ yield captivePortalStatePropagated;
+
+ // Open a page with a cert error.
+ let browser;
+ let certErrorLoaded;
+ let errorTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ let tab = gBrowser.addTab(BAD_CERT_PAGE);
+ gBrowser.selectedTab = tab;
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = waitForCertErrorLoad(browser);
+ return tab;
+ }, false);
+
+ info("Waiting for cert error page to load.")
+ yield certErrorLoaded;
+
+ let portalTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, CANONICAL_URL);
+
+ yield ContentTask.spawn(browser, null, () => {
+ let doc = content.document;
+ ok(doc.body.classList.contains("captiveportal"),
+ "Captive portal error page UI is visible.");
+
+ info("Clicking the Open Login Page button.");
+ let loginButton = doc.getElementById("openPortalLoginPageButton");
+ is(loginButton.getAttribute("autofocus"), "true", "openPortalLoginPageButton has autofocus");
+ loginButton.click();
+ });
+
+ let portalTab = yield portalTabPromise;
+ is(gBrowser.selectedTab, portalTab, "Login page should be open in a new foreground tab.");
+
+ // Make sure clicking the "Open Login Page" button again focuses the existing portal tab.
+ yield BrowserTestUtils.switchTab(gBrowser, errorTab);
+ // Passing an empty function to BrowserTestUtils.switchTab lets us wait for an arbitrary
+ // tab switch.
+ portalTabPromise = BrowserTestUtils.switchTab(gBrowser, () => {});
+ yield ContentTask.spawn(browser, null, () => {
+ info("Clicking the Open Login Page button.");
+ content.document.getElementById("openPortalLoginPageButton").click();
+ });
+
+ let portalTab2 = yield portalTabPromise;
+ is(portalTab2, portalTab, "The existing portal tab should be focused.");
+
+ let portalTabRemoved = BrowserTestUtils.removeTab(portalTab, {dontRemove: true});
+ let errorTabReloaded = waitForCertErrorLoad(browser);
+
+ Services.obs.notifyObservers(null, "captive-portal-login-success", null);
+ yield portalTabRemoved;
+
+ info("Waiting for error tab to be reloaded after the captive portal was freed.");
+ yield errorTabReloaded;
+ yield ContentTask.spawn(browser, null, () => {
+ let doc = content.document;
+ ok(!doc.body.classList.contains("captiveportal"),
+ "Captive portal error page UI is not visible.");
+ });
+
+ yield BrowserTestUtils.removeTab(errorTab);
+});
diff --git a/browser/base/content/test/captivePortal/head.js b/browser/base/content/test/captivePortal/head.js
new file mode 100644
index 000000000..e40b5a325
--- /dev/null
+++ b/browser/base/content/test/captivePortal/head.js
@@ -0,0 +1,181 @@
+Components.utils.import("resource:///modules/RecentWindow.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CaptivePortalWatcher",
+ "resource:///modules/CaptivePortalWatcher.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cps",
+ "@mozilla.org/network/captive-portal-service;1",
+ "nsICaptivePortalService");
+
+const CANONICAL_CONTENT = "success";
+const CANONICAL_URL = "data:text/plain;charset=utf-8," + CANONICAL_CONTENT;
+const CANONICAL_URL_REDIRECTED = "data:text/plain;charset=utf-8,redirected";
+const PORTAL_NOTIFICATION_VALUE = "captive-portal-detected";
+
+function* setupPrefsAndRecentWindowBehavior() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["captivedetect.canonicalURL", CANONICAL_URL],
+ ["captivedetect.canonicalContent", CANONICAL_CONTENT]],
+ });
+ // We need to test behavior when a portal is detected when there is no browser
+ // window, but we can't close the default window opened by the test harness.
+ // Instead, we deactivate CaptivePortalWatcher in the default window and
+ // exclude it from RecentWindow.getMostRecentBrowserWindow in an attempt to
+ // mask its presence.
+ window.CaptivePortalWatcher.uninit();
+ let getMostRecentBrowserWindowCopy = RecentWindow.getMostRecentBrowserWindow;
+ let defaultWindow = window;
+ RecentWindow.getMostRecentBrowserWindow = () => {
+ let win = getMostRecentBrowserWindowCopy();
+ if (win == defaultWindow) {
+ return null;
+ }
+ return win;
+ };
+
+ registerCleanupFunction(function* cleanUp() {
+ RecentWindow.getMostRecentBrowserWindow = getMostRecentBrowserWindowCopy;
+ window.CaptivePortalWatcher.init();
+ });
+}
+
+function* portalDetected() {
+ Services.obs.notifyObservers(null, "captive-portal-login", null);
+ yield BrowserTestUtils.waitForCondition(() => {
+ return cps.state == cps.LOCKED_PORTAL;
+ }, "Waiting for Captive Portal Service to update state after portal detected.");
+}
+
+function* freePortal(aSuccess) {
+ Services.obs.notifyObservers(null,
+ "captive-portal-login-" + (aSuccess ? "success" : "abort"), null);
+ yield BrowserTestUtils.waitForCondition(() => {
+ return cps.state != cps.LOCKED_PORTAL;
+ }, "Waiting for Captive Portal Service to update state after portal freed.");
+}
+
+// If a window is provided, it will be focused. Otherwise, a new window
+// will be opened and focused.
+function* focusWindowAndWaitForPortalUI(aLongRecheck, win) {
+ // CaptivePortalWatcher triggers a recheck when a window gains focus. If
+ // the time taken for the check to complete is under PORTAL_RECHECK_DELAY_MS,
+ // a tab with the login page is opened and selected. If it took longer,
+ // no tab is opened. It's not reliable to time things in an async test,
+ // so use a delay threshold of -1 to simulate a long recheck (so that any
+ // amount of time is considered excessive), and a very large threshold to
+ // simulate a short recheck.
+ Preferences.set("captivedetect.portalRecheckDelayMS", aLongRecheck ? -1 : 1000000);
+
+ if (!win) {
+ win = yield BrowserTestUtils.openNewBrowserWindow();
+ }
+ yield SimpleTest.promiseFocus(win);
+
+ // After a new window is opened, CaptivePortalWatcher asks for a recheck, and
+ // waits for it to complete. We need to manually tell it a recheck completed.
+ yield BrowserTestUtils.waitForCondition(() => {
+ return win.CaptivePortalWatcher._waitingForRecheck;
+ }, "Waiting for CaptivePortalWatcher to trigger a recheck.");
+ Services.obs.notifyObservers(null, "captive-portal-check-complete", null);
+
+ let notification = ensurePortalNotification(win);
+
+ if (aLongRecheck) {
+ ensureNoPortalTab(win);
+ testShowLoginPageButtonVisibility(notification, "visible");
+ return win;
+ }
+
+ let tab = win.gBrowser.tabs[1];
+ if (tab.linkedBrowser.currentURI.spec != CANONICAL_URL) {
+ // The tab should load the canonical URL, wait for it.
+ yield BrowserTestUtils.waitForLocationChange(win.gBrowser, CANONICAL_URL);
+ }
+ is(win.gBrowser.selectedTab, tab,
+ "The captive portal tab should be open and selected in the new window.");
+ testShowLoginPageButtonVisibility(notification, "hidden");
+ return win;
+}
+
+function ensurePortalTab(win) {
+ // For the tests that call this function, it's enough to ensure there
+ // are two tabs in the window - the default tab and the portal tab.
+ is(win.gBrowser.tabs.length, 2,
+ "There should be a captive portal tab in the window.");
+}
+
+function ensurePortalNotification(win) {
+ let notificationBox =
+ win.document.getElementById("high-priority-global-notificationbox");
+ let notification = notificationBox.getNotificationWithValue(PORTAL_NOTIFICATION_VALUE)
+ isnot(notification, null,
+ "There should be a captive portal notification in the window.");
+ return notification;
+}
+
+// Helper to test whether the "Show Login Page" is visible in the captive portal
+// notification (it should be hidden when the portal tab is selected).
+function testShowLoginPageButtonVisibility(notification, visibility) {
+ let showLoginPageButton = notification.querySelector("button.notification-button");
+ // If the visibility property was never changed from default, it will be
+ // an empty string, so we pretend it's "visible" (effectively the same).
+ is(showLoginPageButton.style.visibility || "visible", visibility,
+ "The \"Show Login Page\" button should be " + visibility + ".");
+}
+
+function ensureNoPortalTab(win) {
+ is(win.gBrowser.tabs.length, 1,
+ "There should be no captive portal tab in the window.");
+}
+
+function ensureNoPortalNotification(win) {
+ let notificationBox =
+ win.document.getElementById("high-priority-global-notificationbox");
+ is(notificationBox.getNotificationWithValue(PORTAL_NOTIFICATION_VALUE), null,
+ "There should be no captive portal notification in the window.");
+}
+
+/**
+ * Some tests open a new window and close it later. When the window is closed,
+ * the original window opened by mochitest gains focus, generating a
+ * xul-window-visible notification. If the next test also opens a new window
+ * before this notification has a chance to fire, CaptivePortalWatcher picks
+ * up the first one instead of the one from the new window. To avoid this
+ * unfortunate intermittent timing issue, we wait for the notification from
+ * the original window every time we close a window that we opened.
+ */
+function waitForXulWindowVisible() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observe() {
+ Services.obs.removeObserver(observe, "xul-window-visible");
+ resolve();
+ }, "xul-window-visible", false);
+ });
+}
+
+function* closeWindowAndWaitForXulWindowVisible(win) {
+ let p = waitForXulWindowVisible();
+ yield BrowserTestUtils.closeWindow(win);
+ yield p;
+}
+
+/**
+ * BrowserTestUtils.openNewBrowserWindow() does not guarantee the newly
+ * opened window has received focus when the promise resolves, so we
+ * have to manually wait every time.
+ */
+function* openWindowAndWaitForFocus() {
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+ yield SimpleTest.promiseFocus(win);
+ return win;
+}
+
+function waitForCertErrorLoad(browser) {
+ return new Promise(resolve => {
+ info("Waiting for DOMContentLoaded event");
+ browser.addEventListener("DOMContentLoaded", function load() {
+ browser.removeEventListener("DOMContentLoaded", load, false, true);
+ resolve();
+ }, false, true);
+ });
+}
diff --git a/browser/base/content/test/chrome/.eslintrc.js b/browser/base/content/test/chrome/.eslintrc.js
new file mode 100644
index 000000000..8c0f4f574
--- /dev/null
+++ b/browser/base/content/test/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/chrome/chrome.ini b/browser/base/content/test/chrome/chrome.ini
new file mode 100644
index 000000000..15035fc0c
--- /dev/null
+++ b/browser/base/content/test/chrome/chrome.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[test_aboutCrashed.xul]
diff --git a/browser/base/content/test/chrome/test_aboutCrashed.xul b/browser/base/content/test/chrome/test_aboutCrashed.xul
new file mode 100644
index 000000000..7a68076f1
--- /dev/null
+++ b/browser/base/content/test/chrome/test_aboutCrashed.xul
@@ -0,0 +1,86 @@
+<?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://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <iframe type="content" id="frame1"/>
+ <iframe type="content" id="frame2" onload="doTest()"/>
+ <script type="application/javascript"><![CDATA[
+ const Ci = Components.interfaces;
+ const Cu = Components.utils;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+ Cu.import("resource://gre/modules/Promise.jsm");
+ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Load error pages do not fire "load" events, so let's use a progressListener.
+ function waitForErrorPage(frame) {
+ let errorPageDeferred = Promise.defer();
+
+ let progressListener = {
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress)
+ .removeProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+
+ errorPageDeferred.resolve();
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+ };
+
+ frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress)
+ .addProgressListener(progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+
+ return errorPageDeferred.promise;
+ }
+
+ function doTest() {
+ Task.spawn(function test_aboutCrashed() {
+ let frame1 = document.getElementById("frame1");
+ let frame2 = document.getElementById("frame2");
+ let uri1 = Services.io.newURI("http://www.example.com/1", null, null);
+ let uri2 = Services.io.newURI("http://www.example.com/2", null, null);
+
+ let errorPageReady = waitForErrorPage(frame1);
+ frame1.docShell.chromeEventHandler.setAttribute("crashedPageTitle", "pageTitle");
+ frame1.docShell.displayLoadError(Components.results.NS_ERROR_CONTENT_CRASHED, uri1, null);
+
+ yield errorPageReady;
+ frame1.docShell.chromeEventHandler.removeAttribute("crashedPageTitle");
+
+ SimpleTest.is(frame1.contentDocument.documentURI,
+ "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/1&c=UTF-8&f=regular&d=pageTitle",
+ "Correct about:tabcrashed displayed for page with title.");
+
+ errorPageReady = waitForErrorPage(frame2);
+ frame2.docShell.displayLoadError(Components.results.NS_ERROR_CONTENT_CRASHED, uri2, null);
+
+ yield errorPageReady;
+
+ SimpleTest.is(frame2.contentDocument.documentURI,
+ "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/2&c=UTF-8&f=regular&d=%20",
+ "Correct about:tabcrashed displayed for page with no title.");
+
+ SimpleTest.finish();
+ });
+ }
+ ]]></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" />
+</window>
diff --git a/browser/base/content/test/general/.eslintrc.js b/browser/base/content/test/general/.eslintrc.js
new file mode 100644
index 000000000..11abd6140
--- /dev/null
+++ b/browser/base/content/test/general/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js",
+ "../../../../../testing/mochitest/mochitest.eslintrc.js",
+ ]
+};
diff --git a/browser/base/content/test/general/POSTSearchEngine.xml b/browser/base/content/test/general/POSTSearchEngine.xml
new file mode 100644
index 000000000..30567d92f
--- /dev/null
+++ b/browser/base/content/test/general/POSTSearchEngine.xml
@@ -0,0 +1,6 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>POST Search</ShortName>
+ <Url type="text/html" method="POST" template="http://mochi.test:8888/browser/browser/base/content/test/general/print_postdata.sjs">
+ <Param name="searchterms" value="{searchTerms}"/>
+ </Url>
+</OpenSearchDescription>
diff --git a/browser/base/content/test/general/aboutHome_content_script.js b/browser/base/content/test/general/aboutHome_content_script.js
new file mode 100644
index 000000000..28d0e617e
--- /dev/null
+++ b/browser/base/content/test/general/aboutHome_content_script.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+addMessageListener("AboutHome:SearchTriggered", function (msg) {
+ sendAsyncMessage("AboutHomeTest:CheckRecordedSearch", msg.data);
+});
diff --git a/browser/base/content/test/general/accounts_testRemoteCommands.html b/browser/base/content/test/general/accounts_testRemoteCommands.html
new file mode 100644
index 000000000..517317aff
--- /dev/null
+++ b/browser/base/content/test/general/accounts_testRemoteCommands.html
@@ -0,0 +1,83 @@
+<html>
+ <head>
+ <meta charset="utf-8">
+
+<script type="text/javascript;version=1.8">
+
+function init() {
+ window.addEventListener("message", function process(e) {doTest(e)}, false);
+ // unless we relinquish the eventloop,
+ // tests will run before the chrome event handlers are ready
+ setTimeout(doTest, 0);
+}
+
+function checkStatusValue(payload, expectedValue) {
+ return payload.status == expectedValue;
+}
+
+let tests = [
+{
+ info: "Check account log in",
+ event: "login",
+ data: {
+ email: "foo@example.com",
+ uid: "1234@lcip.org",
+ assertion: "foobar",
+ sessionToken: "dead",
+ kA: "beef",
+ kB: "cafe",
+ verified: true
+ },
+ payloadType: "message",
+ validateResponse: function(payload) {
+ return checkStatusValue(payload, "login");
+ },
+},
+];
+
+let currentTest = -1;
+function doTest(evt) {
+ if (evt) {
+ if (currentTest < 0 || !evt.data.content)
+ return; // not yet testing
+
+ let test = tests[currentTest];
+ if (evt.data.type != test.payloadType)
+ return; // skip unrequested events
+
+ let error = JSON.stringify(evt.data.content);
+ let pass = false;
+ try {
+ pass = test.validateResponse(evt.data.content)
+ } catch (e) {}
+ reportResult(test.info, pass, error);
+ }
+ // start the next test if there are any left
+ if (tests[++currentTest])
+ sendToBrowser(tests[currentTest].event, tests[currentTest].data);
+ else
+ reportFinished();
+}
+
+function reportResult(info, pass, error) {
+ let data = {type: "testResult", info: info, pass: pass, error: error};
+ let event = new CustomEvent("FirefoxAccountsTestResponse", {detail: {data: data}, bubbles: true});
+ document.dispatchEvent(event);
+}
+
+function reportFinished(cmd) {
+ let data = {type: "testsComplete", count: tests.length};
+ let event = new CustomEvent("FirefoxAccountsTestResponse", {detail: {data: data}, bubbles: true});
+ document.dispatchEvent(event);
+}
+
+function sendToBrowser(type, data) {
+ let event = new CustomEvent("FirefoxAccountsCommand", {detail: {command: type, data: data}, bubbles: true});
+ document.dispatchEvent(event);
+}
+
+</script>
+ </head>
+ <body onload="init()">
+ </body>
+</html>
diff --git a/browser/base/content/test/general/alltabslistener.html b/browser/base/content/test/general/alltabslistener.html
new file mode 100644
index 000000000..166c31037
--- /dev/null
+++ b/browser/base/content/test/general/alltabslistener.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Test page for bug 463387</title>
+</head>
+<body>
+<p>Test page for bug 463387</p>
+</body>
+</html>
diff --git a/browser/base/content/test/general/app_bug575561.html b/browser/base/content/test/general/app_bug575561.html
new file mode 100644
index 000000000..a60c7c87e
--- /dev/null
+++ b/browser/base/content/test/general/app_bug575561.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=575561
+-->
+ <head>
+ <title>Test for links in app tabs</title>
+ </head>
+ <body>
+ <a href="http://example.com/browser/browser/base/content/test/general/dummy_page.html">same domain</a>
+ <a href="http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html">same domain (different subdomain)</a>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html">different domain</a>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html" target="foo">different domain (with target)</a>
+ <a href="http://www.example.com/browser/browser/base/content/test/general/dummy_page.html">same domain (www prefix)</a>
+ <a href="data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>">data: URI</a>
+ <iframe src="app_subframe_bug575561.html"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/app_subframe_bug575561.html b/browser/base/content/test/general/app_subframe_bug575561.html
new file mode 100644
index 000000000..8690497ff
--- /dev/null
+++ b/browser/base/content/test/general/app_subframe_bug575561.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=575561
+-->
+ <head>
+ <title>Test for links in app tab subframes</title>
+ </head>
+ <body>
+ <a href="http://example.org/browser/browser/base/content/test/general/dummy_page.html">different domain</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/audio.ogg b/browser/base/content/test/general/audio.ogg
new file mode 100644
index 000000000..477544875
--- /dev/null
+++ b/browser/base/content/test/general/audio.ogg
Binary files differ
diff --git a/browser/base/content/test/general/benignPage.html b/browser/base/content/test/general/benignPage.html
new file mode 100644
index 000000000..8e9429acd
--- /dev/null
+++ b/browser/base/content/test/general/benignPage.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <iframe src="http://not-tracking.example.com/"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini
new file mode 100644
index 000000000..96e591ffe
--- /dev/null
+++ b/browser/base/content/test/general/browser.ini
@@ -0,0 +1,494 @@
+[DEFAULT]
+support-files =
+ POSTSearchEngine.xml
+ accounts_testRemoteCommands.html
+ alltabslistener.html
+ app_bug575561.html
+ app_subframe_bug575561.html
+ aboutHome_content_script.js
+ audio.ogg
+ browser_bug479408_sample.html
+ browser_bug678392-1.html
+ browser_bug678392-2.html
+ browser_bug970746.xhtml
+ browser_fxa_oauth.html
+ browser_fxa_oauth_with_keys.html
+ browser_fxa_web_channel.html
+ browser_registerProtocolHandler_notification.html
+ browser_star_hsts.sjs
+ browser_tab_dragdrop2_frame1.xul
+ browser_web_channel.html
+ browser_web_channel_iframe.html
+ bug1262648_string_with_newlines.dtd
+ bug592338.html
+ bug792517-2.html
+ bug792517.html
+ bug792517.sjs
+ bug839103.css
+ clipboard_pastefile.html
+ contextmenu_common.js
+ ctxmenu-image.png
+ discovery.html
+ download_page.html
+ dummy_page.html
+ feed_tab.html
+ file_generic_favicon.ico
+ file_with_favicon.html
+ file_bug822367_1.html
+ file_bug822367_1.js
+ file_bug822367_2.html
+ file_bug822367_3.html
+ file_bug822367_4.html
+ file_bug822367_4.js
+ file_bug822367_4B.html
+ file_bug822367_5.html
+ file_bug822367_6.html
+ file_bug902156.js
+ file_bug902156_1.html
+ file_bug902156_2.html
+ file_bug902156_3.html
+ file_bug906190_1.html
+ file_bug906190_2.html
+ file_bug906190_3_4.html
+ file_bug906190_redirected.html
+ file_bug906190.js
+ file_bug906190.sjs
+ file_mediaPlayback.html
+ file_mixedContentFromOnunload.html
+ file_mixedContentFromOnunload_test1.html
+ file_mixedContentFromOnunload_test2.html
+ file_mixedContentFramesOnHttp.html
+ file_mixedPassiveContent.html
+ file_bug970276_popup1.html
+ file_bug970276_popup2.html
+ file_bug970276_favicon1.ico
+ file_bug970276_favicon2.ico
+ file_documentnavigation_frameset.html
+ file_double_close_tab.html
+ file_favicon_change.html
+ file_favicon_change_not_in_document.html
+ file_fullscreen-window-open.html
+ head.js
+ healthreport_pingData.js
+ healthreport_testRemoteCommands.html
+ moz.png
+ navigating_window_with_download.html
+ offlineQuotaNotification.cacheManifest
+ offlineQuotaNotification.html
+ page_style_sample.html
+ parsingTestHelpers.jsm
+ pinning_headers.sjs
+ ssl_error_reports.sjs
+ print_postdata.sjs
+ searchSuggestionEngine.sjs
+ searchSuggestionEngine.xml
+ searchSuggestionEngine2.xml
+ subtst_contextmenu.html
+ subtst_contextmenu_input.html
+ subtst_contextmenu_xul.xul
+ test-mixedcontent-securityerrors.html
+ test_bug435035.html
+ test_bug462673.html
+ test_bug628179.html
+ test_bug839103.html
+ test_bug959531.html
+ test_process_flags_chrome.html
+ title_test.svg
+ unknownContentType_file.pif
+ unknownContentType_file.pif^headers^
+ video.ogg
+ web_video.html
+ web_video1.ogv
+ web_video1.ogv^headers^
+ zoom_test.html
+ test_no_mcb_on_http_site_img.html
+ test_no_mcb_on_http_site_img.css
+ test_no_mcb_on_http_site_font.html
+ test_no_mcb_on_http_site_font.css
+ test_no_mcb_on_http_site_font2.html
+ test_no_mcb_on_http_site_font2.css
+ test_mcb_redirect.html
+ test_mcb_redirect_image.html
+ test_mcb_double_redirect_image.html
+ test_mcb_redirect.js
+ test_mcb_redirect.sjs
+ file_bug1045809_1.html
+ file_bug1045809_2.html
+ file_csp_block_all_mixedcontent.html
+ file_csp_block_all_mixedcontent.js
+ !/image/test/mochitest/blue.png
+ !/toolkit/components/passwordmgr/test/browser/form_basic.html
+ !/toolkit/components/passwordmgr/test/browser/insecure_test.html
+ !/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
+ !/toolkit/content/tests/browser/common/mockTransfer.js
+ !/toolkit/modules/tests/browser/metadata_*.html
+ !/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
+ !/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/redirect.sjs
+ !/toolkit/mozapps/extensions/test/xpinstall/restartless-unsigned.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
+
+[browser_aboutAccounts.js]
+skip-if = os == "linux" # Bug 958026
+support-files =
+ content_aboutAccounts.js
+[browser_aboutCertError.js]
+[browser_aboutNetError.js]
+[browser_aboutSupport_newtab_security_state.js]
+[browser_aboutHealthReport.js]
+skip-if = os == "linux" # Bug 924307
+[browser_aboutHome.js]
+[browser_aboutHome_wrapsCorrectly.js]
+[browser_addKeywordSearch.js]
+[browser_alltabslistener.js]
+[browser_audioTabIcon.js]
+tags = audiochannel
+[browser_backButtonFitts.js]
+skip-if = os == "mac" # The Fitt's Law back button is not supported on OS X
+[browser_beforeunload_duplicate_dialogs.js]
+[browser_blob-channelname.js]
+[browser_bookmark_popup.js]
+skip-if = (os == "linux" && debug) # mouseover not reliable on linux debug builds
+[browser_bookmark_titles.js]
+skip-if = toolkit == "windows" # Disabled on Windows due to frequent failures (bugs 825739, 841341)
+[browser_bug321000.js]
+subsuite = clipboard
+skip-if = true # browser_bug321000.js is disabled because newline handling is shaky (bug 592528)
+[browser_bug356571.js]
+[browser_bug380960.js]
+[browser_bug386835.js]
+[browser_bug406216.js]
+[browser_bug408415.js]
+[browser_bug409481.js]
+[browser_bug409624.js]
+[browser_bug413915.js]
+[browser_bug416661.js]
+[browser_bug417483.js]
+[browser_bug419612.js]
+[browser_bug422590.js]
+[browser_bug423833.js]
+skip-if = true # bug 428712
+[browser_bug424101.js]
+[browser_bug427559.js]
+[browser_bug431826.js]
+[browser_bug432599.js]
+[browser_bug435035.js]
+[browser_bug435325.js]
+[browser_bug441778.js]
+[browser_bug455852.js]
+[browser_bug460146.js]
+[browser_bug462289.js]
+skip-if = toolkit == "cocoa"
+[browser_bug462673.js]
+[browser_bug477014.js]
+[browser_bug479408.js]
+[browser_bug481560.js]
+[browser_bug484315.js]
+[browser_bug491431.js]
+[browser_bug495058.js]
+[browser_bug517902.js]
+skip-if = (os == 'linux' && e10s) # bug 1161699
+[browser_bug519216.js]
+[browser_bug520538.js]
+[browser_bug521216.js]
+[browser_bug533232.js]
+[browser_bug537013.js]
+subsuite = clipboard
+skip-if = e10s # Bug 1134458 - Find bar doesn't work correctly in a detached tab
+[browser_bug537474.js]
+[browser_bug550565.js]
+[browser_bug553455.js]
+[browser_bug555224.js]
+[browser_bug555767.js]
+[browser_bug559991.js]
+[browser_bug561636.js]
+skip-if = true # bug 1057615
+[browser_bug563588.js]
+[browser_bug565575.js]
+[browser_bug567306.js]
+subsuite = clipboard
+[browser_bug1261299.js]
+subsuite = clipboard
+skip-if = toolkit != "cocoa" # Because of tests for supporting Service Menu of macOS, bug 1261299
+[browser_bug1297539.js]
+skip-if = toolkit != "cocoa" # Because of tests for supporting pasting from Service Menu of macOS, bug 1297539
+[browser_bug575561.js]
+[browser_bug575830.js]
+[browser_bug577121.js]
+[browser_bug578534.js]
+[browser_bug579872.js]
+[browser_bug580638.js]
+[browser_bug580956.js]
+[browser_bug581242.js]
+[browser_bug581253.js]
+[browser_bug585558.js]
+[browser_bug585785.js]
+[browser_bug585830.js]
+[browser_bug590206.js]
+[browser_bug592338.js]
+[browser_bug594131.js]
+[browser_bug595507.js]
+skip-if = true # bug 1057615
+[browser_bug596687.js]
+[browser_bug597218.js]
+[browser_bug609700.js]
+[browser_bug623893.js]
+[browser_bug624734.js]
+[browser_bug633691.js]
+[browser_bug647886.js]
+[browser_bug655584.js]
+[browser_bug664672.js]
+[browser_bug676619.js]
+skip-if = os == "mac" # mac: Intermittent failures, bug 925225
+[browser_bug678392.js]
+skip-if = os == "mac" # Bug 1102331 - does focus things on the content window which break in e10s mode (still causes orange on Mac 10.10)
+[browser_bug710878.js]
+[browser_bug719271.js]
+[browser_bug724239.js]
+[browser_bug734076.js]
+[browser_bug735471.js]
+[browser_bug749738.js]
+[browser_bug763468_perwindowpb.js]
+[browser_bug767836_perwindowpb.js]
+[browser_bug817947.js]
+[browser_bug822367.js]
+tags = mcb
+[browser_bug832435.js]
+[browser_bug839103.js]
+[browser_bug882977.js]
+[browser_bug902156.js]
+tags = mcb
+[browser_bug906190.js]
+tags = mcb
+[browser_mixedContentFromOnunload.js]
+tags = mcb
+[browser_mixedContentFramesOnHttp.js]
+tags = mcb
+[browser_bug970746.js]
+[browser_bug1015721.js]
+skip-if = os == 'win'
+[browser_bug1064280_changeUrlInPinnedTab.js]
+[browser_accesskeys.js]
+[browser_clipboard.js]
+subsuite = clipboard
+[browser_clipboard_pastefile.js]
+skip-if = true # Disabled due to the clipboard not supporting real file types yet (bug 1288773)
+[browser_contentAreaClick.js]
+skip-if = e10s # Clicks in content don't go through contentAreaClick with e10s.
+[browser_contentAltClick.js]
+[browser_contextmenu.js]
+subsuite = clipboard
+tags = fullscreen
+skip-if = toolkit == "gtk2" || toolkit == "gtk3" # disabled on Linux due to bug 513558
+[browser_contextmenu_input.js]
+skip-if = toolkit == "gtk2" || toolkit == "gtk3" # disabled on Linux due to bug 513558
+[browser_ctrlTab.js]
+[browser_datachoices_notification.js]
+skip-if = !datareporting
+[browser_decoderDoctor.js]
+skip-if = os == "mac" # decoder doctor isn't implemented on osx
+[browser_devedition.js]
+[browser_discovery.js]
+[browser_double_close_tab.js]
+[browser_documentnavigation.js]
+[browser_duplicateIDs.js]
+[browser_drag.js]
+skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
+[browser_favicon_change.js]
+[browser_favicon_change_not_in_document.js]
+[browser_findbarClose.js]
+[browser_focusonkeydown.js]
+[browser_fullscreen-window-open.js]
+tags = fullscreen
+skip-if = os == "linux" # Linux: Intermittent failures - bug 941575.
+[browser_fxaccounts.js]
+support-files = fxa_profile_handler.sjs
+[browser_fxa_migrate.js]
+[browser_fxa_oauth.js]
+[browser_fxa_web_channel.js]
+[browser_gestureSupport.js]
+skip-if = e10s # Bug 863514 - no gesture support.
+[browser_getshortcutoruri.js]
+[browser_hide_removing.js]
+[browser_homeDrop.js]
+[browser_identity_UI.js]
+[browser_insecureLoginForms.js]
+support-files = insecure_opener.html
+[browser_invalid_uri_back_forward_manipulation.js]
+[browser_keywordBookmarklets.js]
+[browser_keywordSearch.js]
+[browser_keywordSearch_postData.js]
+[browser_lastAccessedTab.js]
+skip-if = toolkit == "windows" # Disabled on Windows due to frequent failures (bug 969405)
+[browser_menuButtonFitts.js]
+skip-if = os != "win" # The Fitts Law menu button is only supported on Windows (bug 969376)
+[browser_middleMouse_noJSPaste.js]
+subsuite = clipboard
+[browser_minimize.js]
+[browser_misused_characters_in_strings.js]
+[browser_mixed_content_cert_override.js]
+[browser_mixedcontent_securityflags.js]
+tags = mcb
+[browser_modifiedclick_inherit_principal.js]
+[browser_offlineQuotaNotification.js]
+skip-if = os == "linux" && !debug # bug 1304273
+[browser_feed_discovery.js]
+support-files = feed_discovery.html
+[browser_gZipOfflineChild.js]
+support-files = test_offline_gzip.html gZipOfflineChild.cacheManifest gZipOfflineChild.cacheManifest^headers^ gZipOfflineChild.html gZipOfflineChild.html^headers^
+[browser_overflowScroll.js]
+[browser_pageInfo.js]
+[browser_pageinfo_svg_image.js]
+support-files =
+ svg_image.html
+[browser_page_style_menu.js]
+[browser_page_style_menu_update.js]
+[browser_parsable_css.js]
+skip-if = (debug || asan) # no point in running on both opt and debug, and will likely intermittently timeout on debug
+[browser_parsable_script.js]
+skip-if = asan || (os == 'linux' && !debug && (bits == 32)) # disabled on asan because of timeouts, and bug 1172468 for the linux 32-bit pgo issue.
+[browser_permissions.js]
+support-files =
+ permissions.html
+[browser_pinnedTabs.js]
+[browser_plainTextLinks.js]
+[browser_printpreview.js]
+[browser_private_browsing_window.js]
+[browser_private_no_prompt.js]
+[browser_purgehistory_clears_sh.js]
+[browser_PageMetaData_pushstate.js]
+[browser_refreshBlocker.js]
+support-files =
+ refresh_header.sjs
+ refresh_meta.sjs
+[browser_relatedTabs.js]
+[browser_remoteTroubleshoot.js]
+support-files =
+ test_remoteTroubleshoot.html
+[browser_remoteWebNavigation_postdata.js]
+[browser_removeTabsToTheEnd.js]
+[browser_restore_isAppTab.js]
+[browser_sanitize-passwordDisabledHosts.js]
+[browser_sanitize-sitepermissions.js]
+[browser_sanitize-timespans.js]
+[browser_sanitizeDialog.js]
+[browser_save_link-perwindowpb.js]
+skip-if = e10s && debug && os == "win" # Bug 1280505
+[browser_save_private_link_perwindowpb.js]
+[browser_save_link_when_window_navigates.js]
+[browser_save_video.js]
+[browser_save_video_frame.js]
+[browser_scope.js]
+[browser_contentSearchUI.js]
+support-files =
+ contentSearchUI.html
+ contentSearchUI.js
+[browser_selectpopup.js]
+run-if = e10s
+[browser_selectTabAtIndex.js]
+[browser_ssl_error_reports.js]
+[browser_star_hsts.js]
+[browser_subframe_favicons_not_used.js]
+[browser_syncui.js]
+[browser_tab_close_dependent_window.js]
+[browser_tabDrop.js]
+[browser_tabReorder.js]
+[browser_tab_detach_restore.js]
+[browser_tab_drag_drop_perwindow.js]
+[browser_tab_dragdrop.js]
+skip-if = buildapp == 'mulet' || (e10s && (debug || os == 'linux')) # Bug 1312436
+[browser_tab_dragdrop2.js]
+[browser_tabbar_big_widgets.js]
+skip-if = os == "linux" || os == "mac" # No tabs in titlebar on linux
+ # Disabled on OS X because of bug 967917
+[browser_tabfocus.js]
+[browser_tabkeynavigation.js]
+skip-if = (os == "mac" && !e10s) # Bug 1237713 - OSX eats keypresses for some reason
+[browser_tabopen_reflows.js]
+[browser_tabs_close_beforeunload.js]
+support-files =
+ close_beforeunload_opens_second_tab.html
+ close_beforeunload.html
+[browser_tabs_isActive.js]
+[browser_tabs_owner.js]
+[browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js]
+run-if = e10s
+[browser_trackingUI_1.js]
+tags = trackingprotection
+support-files =
+ trackingPage.html
+ benignPage.html
+[browser_trackingUI_2.js]
+tags = trackingprotection
+support-files =
+ trackingPage.html
+ benignPage.html
+[browser_trackingUI_3.js]
+tags = trackingprotection
+[browser_trackingUI_4.js]
+tags = trackingprotection
+support-files =
+ trackingPage.html
+ benignPage.html
+[browser_trackingUI_5.js]
+tags = trackingprotection
+support-files =
+ trackingPage.html
+[browser_trackingUI_6.js]
+tags = trackingprotection
+support-files =
+ file_trackingUI_6.html
+ file_trackingUI_6.js
+ file_trackingUI_6.js^headers^
+[browser_trackingUI_telemetry.js]
+tags = trackingprotection
+support-files =
+ trackingPage.html
+[browser_typeAheadFind.js]
+[browser_unknownContentType_title.js]
+[browser_unloaddialogs.js]
+[browser_utilityOverlay.js]
+[browser_viewSourceInTabOnViewSource.js]
+[browser_visibleFindSelection.js]
+[browser_visibleTabs.js]
+[browser_visibleTabs_bookmarkAllPages.js]
+skip-if = true # Bug 1005420 - fails intermittently. also with e10s enabled: bizarre problem with hidden tab having _mouseenter called, via _setPositionalAttributes, and tab not being found resulting in 'candidate is undefined'
+[browser_visibleTabs_bookmarkAllTabs.js]
+[browser_visibleTabs_contextMenu.js]
+[browser_visibleTabs_tabPreview.js]
+skip-if = (os == "win" && !debug)
+[browser_web_channel.js]
+[browser_windowopen_reflows.js]
+[browser_zbug569342.js]
+skip-if = e10s || debug # Bug 1094240 - has findbar-related failures
+[browser_registerProtocolHandler_notification.js]
+[browser_no_mcb_on_http_site.js]
+tags = mcb
+[browser_addCertException.js]
+[browser_bug1045809.js]
+tags = mcb
+[browser_e10s_switchbrowser.js]
+[browser_e10s_about_process.js]
+[browser_e10s_chrome_process.js]
+[browser_e10s_javascript.js]
+[browser_blockHPKP.js]
+tags = psm
+[browser_mcb_redirect.js]
+tags = mcb
+[browser_windowactivation.js]
+[browser_contextmenu_childprocess.js]
+[browser_bug963945.js]
+[browser_domFullscreen_fullscreenMode.js]
+tags = fullscreen
+[browser_menuButtonBadgeManager.js]
+[browser_newTabDrop.js]
+[browser_newWindowDrop.js]
+[browser_csp_block_all_mixedcontent.js]
+tags = mcb
+[browser_newwindow_focus.js]
+skip-if = (os == "linux" && !e10s) # Bug 1263254 - Perma fails on Linux without e10s for some reason.
+[browser_bug1299667.js]
diff --git a/browser/base/content/test/general/browser_PageMetaData_pushstate.js b/browser/base/content/test/general/browser_PageMetaData_pushstate.js
new file mode 100644
index 000000000..6f71c57a3
--- /dev/null
+++ b/browser/base/content/test/general/browser_PageMetaData_pushstate.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(function* () {
+ let rooturi = "https://example.com/browser/toolkit/modules/tests/browser/";
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, rooturi + "metadata_simple.html");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { rooturi }, function* (args) {
+ let result = PageMetadata.getData(content.document);
+ // Result should have description.
+ Assert.equal(result.url, args.rooturi + "metadata_simple.html", "metadata url is correct");
+ Assert.equal(result.title, "Test Title", "metadata title is correct");
+ Assert.equal(result.description, "A very simple test page", "description is correct");
+
+ content.history.pushState({}, "2", "2.html");
+ result = PageMetadata.getData(content.document);
+ // Result should not have description.
+ Assert.equal(result.url, args.rooturi + "2.html", "metadata url is correct");
+ Assert.equal(result.title, "Test Title", "metadata title is correct");
+ Assert.ok(!result.description, "description is undefined");
+
+ Assert.equal(content.document.documentURI, args.rooturi + "2.html",
+ "content.document has correct url");
+ });
+
+ is(gBrowser.currentURI.spec, rooturi + "2.html", "gBrowser has correct url");
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/general/browser_aboutAccounts.js b/browser/base/content/test/general/browser_aboutAccounts.js
new file mode 100644
index 000000000..fd72a1608
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutAccounts.js
@@ -0,0 +1,499 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: window.location is null");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
+// Preference helpers.
+var changedPrefs = new Set();
+
+function setPref(name, value) {
+ changedPrefs.add(name);
+ Services.prefs.setCharPref(name, value);
+}
+
+registerCleanupFunction(function() {
+ // Ensure we don't pollute prefs for next tests.
+ for (let name of changedPrefs) {
+ Services.prefs.clearUserPref(name);
+ }
+});
+
+var gTests = [
+{
+ desc: "Test the remote commands",
+ teardown: function* () {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ run: function* ()
+ {
+ setPref("identity.fxaccounts.remote.signup.uri",
+ "https://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
+ let tab = yield promiseNewTabLoadEvent("about:accounts");
+ let mm = tab.linkedBrowser.messageManager;
+
+ let deferred = Promise.defer();
+
+ // We'll get a message when openPrefs() is called, which this test should
+ // arrange.
+ let promisePrefsOpened = promiseOneMessage(tab, "test:openPrefsCalled");
+ let results = 0;
+ try {
+ mm.addMessageListener("test:response", function responseHandler(msg) {
+ let data = msg.data.data;
+ if (data.type == "testResult") {
+ ok(data.pass, data.info);
+ results++;
+ } else if (data.type == "testsComplete") {
+ is(results, data.count, "Checking number of results received matches the number of tests that should have run");
+ mm.removeMessageListener("test:response", responseHandler);
+ deferred.resolve();
+ }
+ });
+ } catch (e) {
+ ok(false, "Failed to get all commands");
+ deferred.reject();
+ }
+ yield deferred.promise;
+ yield promisePrefsOpened;
+ }
+},
+{
+ desc: "Test action=signin - no user logged in",
+ teardown: () => gBrowser.removeCurrentTab(),
+ run: function* ()
+ {
+ // When this loads with no user logged-in, we expect the "normal" URL
+ const expected_url = "https://example.com/?is_sign_in";
+ setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+ let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+ is(url, expected_url, "action=signin got the expected URL");
+ // we expect the remote iframe to be shown.
+ yield checkVisibilities(tab, {
+ stage: false, // parent of 'manage' and 'intro'
+ manage: false,
+ intro: false, // this is "get started"
+ remote: true,
+ networkError: false
+ });
+ }
+},
+{
+ desc: "Test action=signin - user logged in",
+ teardown: function* () {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ run: function* ()
+ {
+ // When this loads with a user logged-in, we expect the normal URL to
+ // have been ignored and the "manage" page to be shown.
+ const expected_url = "https://example.com/?is_sign_in";
+ setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+ yield setSignedInUser();
+ let tab = yield promiseNewTabLoadEvent("about:accounts?action=signin");
+ // about:accounts initializes after fetching the current user from Fxa -
+ // so we also request it - by the time we get it we know it should have
+ // done its thing.
+ yield fxAccounts.getSignedInUser();
+ // we expect "manage" to be shown.
+ yield checkVisibilities(tab, {
+ stage: true, // parent of 'manage' and 'intro'
+ manage: true,
+ intro: false, // this is "get started"
+ remote: false,
+ networkError: false
+ });
+ }
+},
+{
+ desc: "Test action=signin - captive portal",
+ teardown: () => gBrowser.removeCurrentTab(),
+ run: function* ()
+ {
+ const signinUrl = "https://redirproxy.example.com/test";
+ setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
+ let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+ yield checkVisibilities(tab, {
+ stage: true, // parent of 'manage' and 'intro'
+ manage: false,
+ intro: false, // this is "get started"
+ remote: false,
+ networkError: true
+ });
+ }
+},
+{
+ desc: "Test action=signin - offline",
+ teardown: () => {
+ gBrowser.removeCurrentTab();
+ BrowserOffline.toggleOfflineStatus();
+ },
+ run: function* ()
+ {
+ BrowserOffline.toggleOfflineStatus();
+ Services.cache2.clear();
+
+ const signinUrl = "https://unknowndomain.cow";
+ setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
+ let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+ yield checkVisibilities(tab, {
+ stage: true, // parent of 'manage' and 'intro'
+ manage: false,
+ intro: false, // this is "get started"
+ remote: false,
+ networkError: true
+ });
+ }
+},
+{
+ desc: "Test action=signup - no user logged in",
+ teardown: () => gBrowser.removeCurrentTab(),
+ run: function* ()
+ {
+ const expected_url = "https://example.com/?is_sign_up";
+ setPref("identity.fxaccounts.remote.signup.uri", expected_url);
+ let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signup");
+ is(url, expected_url, "action=signup got the expected URL");
+ // we expect the remote iframe to be shown.
+ yield checkVisibilities(tab, {
+ stage: false, // parent of 'manage' and 'intro'
+ manage: false,
+ intro: false, // this is "get started"
+ remote: true,
+ networkError: false
+ });
+ },
+},
+{
+ desc: "Test action=signup - user logged in",
+ teardown: () => gBrowser.removeCurrentTab(),
+ run: function* ()
+ {
+ const expected_url = "https://example.com/?is_sign_up";
+ setPref("identity.fxaccounts.remote.signup.uri", expected_url);
+ yield setSignedInUser();
+ let tab = yield promiseNewTabLoadEvent("about:accounts?action=signup");
+ yield fxAccounts.getSignedInUser();
+ // we expect "manage" to be shown.
+ yield checkVisibilities(tab, {
+ stage: true, // parent of 'manage' and 'intro'
+ manage: true,
+ intro: false, // this is "get started"
+ remote: false,
+ networkError: false
+ });
+ },
+},
+{
+ desc: "Test action=reauth",
+ teardown: function* () {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ run: function* ()
+ {
+ const expected_url = "https://example.com/?is_force_auth";
+ setPref("identity.fxaccounts.remote.force_auth.uri", expected_url);
+
+ yield setSignedInUser();
+ let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=reauth");
+ // The current user will be appended to the url
+ let expected = expected_url + "&email=foo%40example.com";
+ is(url, expected, "action=reauth got the expected URL");
+ },
+},
+{
+ desc: "Test with migrateToDevEdition enabled (success)",
+ teardown: function* () {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ run: function* ()
+ {
+ let fxAccountsCommon = {};
+ Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+ const pref = "identity.fxaccounts.migrateToDevEdition";
+ changedPrefs.add(pref);
+ Services.prefs.setBoolPref(pref, true);
+
+ // Create the signedInUser.json file that will be used as the source of
+ // migrated user data.
+ let signedInUser = {
+ version: 1,
+ accountData: {
+ email: "foo@example.com",
+ uid: "1234@lcip.org",
+ sessionToken: "dead",
+ verified: true
+ }
+ };
+ // We use a sub-dir of the real profile dir as the "pretend" profile dir
+ // for this test.
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let mockDir = profD.clone();
+ mockDir.append("about-accounts-mock-profd");
+ mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ let fxAccountsStorage = OS.Path.join(mockDir.path, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
+ yield OS.File.writeAtomic(fxAccountsStorage, JSON.stringify(signedInUser));
+ info("Wrote file " + fxAccountsStorage);
+
+ // this is a little subtle - we load about:robots so we get a non-remote
+ // tab, then we send a message which does both (a) load the URL we want and
+ // (b) mocks the default profile path used by about:accounts.
+ let tab = yield promiseNewTabLoadEvent("about:robots");
+ let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
+
+ let mm = tab.linkedBrowser.messageManager;
+ mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
+ url: "about:accounts",
+ profilePath: mockDir.path,
+ });
+
+ let response = yield readyPromise;
+ // We are expecting the iframe to be on the "force reauth" URL
+ let expected = yield fxAccounts.promiseAccountsForceSigninURI();
+ is(response.data.url, expected);
+
+ let userData = yield fxAccounts.getSignedInUser();
+ SimpleTest.isDeeply(userData, signedInUser.accountData, "All account data were migrated");
+ // The migration pref will have been switched off by now.
+ is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
+
+ yield OS.File.remove(fxAccountsStorage);
+ yield OS.File.removeEmptyDir(mockDir.path);
+ },
+},
+{
+ desc: "Test with migrateToDevEdition enabled (no user to migrate)",
+ teardown: function* () {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ run: function* ()
+ {
+ const pref = "identity.fxaccounts.migrateToDevEdition";
+ changedPrefs.add(pref);
+ Services.prefs.setBoolPref(pref, true);
+
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let mockDir = profD.clone();
+ mockDir.append("about-accounts-mock-profd");
+ mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ // but leave it empty, so we don't think a user is logged in.
+
+ let tab = yield promiseNewTabLoadEvent("about:robots");
+ let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
+
+ let mm = tab.linkedBrowser.messageManager;
+ mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
+ url: "about:accounts",
+ profilePath: mockDir.path,
+ });
+
+ let response = yield readyPromise;
+ // We are expecting the iframe to be on the "signup" URL
+ let expected = yield fxAccounts.promiseAccountsSignUpURI();
+ is(response.data.url, expected);
+
+ // and expect no signed in user.
+ let userData = yield fxAccounts.getSignedInUser();
+ is(userData, null);
+ // The migration pref should have still been switched off.
+ is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
+ yield OS.File.removeEmptyDir(mockDir.path);
+ },
+},
+{
+ desc: "Test observers about:accounts",
+ teardown: function() {
+ gBrowser.removeCurrentTab();
+ },
+ run: function* () {
+ setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
+ yield setSignedInUser();
+ let tab = yield promiseNewTabLoadEvent("about:accounts");
+ // sign the user out - the tab should have action=signin
+ yield signOut();
+ // wait for the new load.
+ yield promiseOneMessage(tab, "test:document:load");
+ is(tab.linkedBrowser.contentDocument.location.href, "about:accounts?action=signin");
+ }
+},
+{
+ desc: "Test entrypoint query string, no action, no user logged in",
+ teardown: () => gBrowser.removeCurrentTab(),
+ run: function* () {
+ // When this loads with no user logged-in, we expect the "normal" URL
+ setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
+ let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome");
+ is(url, "https://example.com/?entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+ },
+},
+{
+ desc: "Test entrypoint query string for signin",
+ teardown: () => gBrowser.removeCurrentTab(),
+ run: function* () {
+ // When this loads with no user logged-in, we expect the "normal" URL
+ const expected_url = "https://example.com/?is_sign_in";
+ setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+ let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin&entrypoint=abouthome");
+ is(url, expected_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+ },
+},
+{
+ desc: "Test entrypoint query string for signup",
+ teardown: () => gBrowser.removeCurrentTab(),
+ run: function* () {
+ // When this loads with no user logged-in, we expect the "normal" URL
+ const sign_up_url = "https://example.com/?is_sign_up";
+ setPref("identity.fxaccounts.remote.signup.uri", sign_up_url);
+ let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome&action=signup");
+ is(url, sign_up_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+ },
+},
+{
+ desc: "about:accounts URL params should be copied to remote URL params " +
+ "when remote URL has no URL params, except for 'action'",
+ teardown() {
+ gBrowser.removeCurrentTab();
+ },
+ run: function* () {
+ let signupURL = "https://example.com/";
+ setPref("identity.fxaccounts.remote.signup.uri", signupURL);
+ let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
+ let [, url] =
+ yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
+ "&action=action");
+ is(url, signupURL + "?" + queryStr, "URL params are copied to signup URL");
+ },
+},
+{
+ desc: "about:accounts URL params should be copied to remote URL params " +
+ "when remote URL already has some URL params, except for 'action'",
+ teardown() {
+ gBrowser.removeCurrentTab();
+ },
+ run: function* () {
+ let signupURL = "https://example.com/?param";
+ setPref("identity.fxaccounts.remote.signup.uri", signupURL);
+ let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
+ let [, url] =
+ yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
+ "&action=action");
+ is(url, signupURL + "&" + queryStr, "URL params are copied to signup URL");
+ },
+},
+]; // gTests
+
+function test()
+{
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ for (let testCase of gTests) {
+ info(testCase.desc);
+ try {
+ yield testCase.run();
+ } finally {
+ yield testCase.teardown();
+ }
+ }
+
+ finish();
+ });
+}
+
+function promiseOneMessage(tab, messageName) {
+ let mm = tab.linkedBrowser.messageManager;
+ let deferred = Promise.defer();
+ mm.addMessageListener(messageName, function onmessage(message) {
+ mm.removeMessageListener(messageName, onmessage);
+ deferred.resolve(message);
+ });
+ return deferred.promise;
+}
+
+function promiseNewTabLoadEvent(aUrl)
+{
+ let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+ let browser = tab.linkedBrowser;
+ let mm = browser.messageManager;
+
+ // give it an e10s-friendly content script to help with our tests.
+ mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
+ // and wait for it to tell us about the load.
+ return promiseOneMessage(tab, "test:document:load").then(
+ () => tab
+ );
+}
+
+// Returns a promise which is resolved with the iframe's URL after a new
+// tab is created and the iframe in that tab loads.
+function promiseNewTabWithIframeLoadEvent(aUrl) {
+ let deferred = Promise.defer();
+ let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+ let browser = tab.linkedBrowser;
+ let mm = browser.messageManager;
+
+ // give it an e10s-friendly content script to help with our tests.
+ mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
+ // and wait for it to tell us about the iframe load.
+ mm.addMessageListener("test:iframe:load", function onFrameLoad(message) {
+ mm.removeMessageListener("test:iframe:load", onFrameLoad);
+ deferred.resolve([tab, message.data.url]);
+ });
+ return deferred.promise;
+}
+
+function checkVisibilities(tab, data) {
+ let ids = Object.keys(data);
+ let mm = tab.linkedBrowser.messageManager;
+ let deferred = Promise.defer();
+ mm.addMessageListener("test:check-visibilities-response", function onResponse(message) {
+ mm.removeMessageListener("test:check-visibilities-response", onResponse);
+ for (let id of ids) {
+ is(message.data[id], data[id], "Element '" + id + "' has correct visibility");
+ }
+ deferred.resolve();
+ });
+ mm.sendAsyncMessage("test:check-visibilities", {ids: ids});
+ return deferred.promise;
+}
+
+// watch out - these will fire observers which if you aren't careful, may
+// interfere with the tests.
+function setSignedInUser(data) {
+ if (!data) {
+ data = {
+ email: "foo@example.com",
+ uid: "1234@lcip.org",
+ assertion: "foobar",
+ sessionToken: "dead",
+ kA: "beef",
+ kB: "cafe",
+ verified: true
+ }
+ }
+ return fxAccounts.setSignedInUser(data);
+}
+
+function signOut() {
+ // we always want a "localOnly" signout here...
+ return fxAccounts.signOut(true);
+}
diff --git a/browser/base/content/test/general/browser_aboutCertError.js b/browser/base/content/test/general/browser_aboutCertError.js
new file mode 100644
index 000000000..0e335066c
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutCertError.js
@@ -0,0 +1,409 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This is testing the aboutCertError page (Bug 1207107).
+
+const GOOD_PAGE = "https://example.com/";
+const BAD_CERT = "https://expired.example.com/";
+const UNKNOWN_ISSUER = "https://self-signed.example.com ";
+const BAD_STS_CERT = "https://badchain.include-subdomains.pinning.example.com:443";
+const {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+
+add_task(function* checkReturnToAboutHome() {
+ info("Loading a bad cert page directly and making sure 'return to previous page' goes to about:home");
+ let browser;
+ let certErrorLoaded;
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ gBrowser.selectedTab = gBrowser.addTab(BAD_CERT);
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = waitForCertErrorLoad(browser);
+ }, false);
+
+ info("Loading and waiting for the cert error");
+ yield certErrorLoaded;
+
+ is(browser.webNavigation.canGoBack, false, "!webNavigation.canGoBack");
+ is(browser.webNavigation.canGoForward, false, "!webNavigation.canGoForward");
+
+ // Populate the shistory entries manually, since it happens asynchronously
+ // and the following tests will be too soon otherwise.
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is one shistory entry");
+
+ info("Clicking the go back button on about:certerror");
+ yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let returnButton = doc.getElementById("returnButton");
+ is(returnButton.getAttribute("autofocus"), "true", "returnButton has autofocus");
+ returnButton.click();
+
+ yield ContentTaskUtils.waitForEvent(this, "pageshow", true);
+ });
+
+ is(browser.webNavigation.canGoBack, true, "webNavigation.canGoBack");
+ is(browser.webNavigation.canGoForward, false, "!webNavigation.canGoForward");
+ is(gBrowser.currentURI.spec, "about:home", "Went back");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(function* checkReturnToPreviousPage() {
+ info("Loading a bad cert page and making sure 'return to previous page' goes back");
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, GOOD_PAGE);
+ let browser = gBrowser.selectedBrowser;
+
+ info("Loading and waiting for the cert error");
+ let certErrorLoaded = waitForCertErrorLoad(browser);
+ BrowserTestUtils.loadURI(browser, BAD_CERT);
+ yield certErrorLoaded;
+
+ is(browser.webNavigation.canGoBack, true, "webNavigation.canGoBack");
+ is(browser.webNavigation.canGoForward, false, "!webNavigation.canGoForward");
+
+ // Populate the shistory entries manually, since it happens asynchronously
+ // and the following tests will be too soon otherwise.
+ yield TabStateFlusher.flush(browser);
+ let {entries} = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 2, "there are two shistory entries");
+
+ info("Clicking the go back button on about:certerror");
+ yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let returnButton = doc.getElementById("returnButton");
+ returnButton.click();
+
+ yield ContentTaskUtils.waitForEvent(this, "pageshow", true);
+ });
+
+ is(browser.webNavigation.canGoBack, false, "!webNavigation.canGoBack");
+ is(browser.webNavigation.canGoForward, true, "webNavigation.canGoForward");
+ is(gBrowser.currentURI.spec, GOOD_PAGE, "Went back");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(function* checkBadStsCert() {
+ info("Loading a badStsCert and making sure exception button doesn't show up");
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, GOOD_PAGE);
+ let browser = gBrowser.selectedBrowser;
+
+ info("Loading and waiting for the cert error");
+ let certErrorLoaded = waitForCertErrorLoad(browser);
+ BrowserTestUtils.loadURI(browser, BAD_STS_CERT);
+ yield certErrorLoaded;
+
+ let exceptionButtonHidden = yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let exceptionButton = doc.getElementById("exceptionDialogButton");
+ return exceptionButton.hidden;
+ });
+ ok(exceptionButtonHidden, "Exception button is hidden");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
+
+add_task(function* checkWrongSystemTimeWarning() {
+ function* setUpPage() {
+ let browser;
+ let certErrorLoaded;
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ gBrowser.selectedTab = gBrowser.addTab(BAD_CERT);
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = waitForCertErrorLoad(browser);
+ }, false);
+
+ info("Loading and waiting for the cert error");
+ yield certErrorLoaded;
+
+ return yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let div = doc.getElementById("wrongSystemTimePanel");
+ let systemDateDiv = doc.getElementById("wrongSystemTime_systemDate");
+ let actualDateDiv = doc.getElementById("wrongSystemTime_actualDate");
+ let learnMoreLink = doc.getElementById("learnMoreLink");
+
+ return {
+ divDisplay: content.getComputedStyle(div).display,
+ text: div.textContent,
+ systemDate: systemDateDiv.textContent,
+ actualDate: actualDateDiv.textContent,
+ learnMoreLink: learnMoreLink.href
+ };
+ });
+ }
+
+ let formatter = new Intl.DateTimeFormat();
+
+ // pretend we have a positively skewed (ahead) system time
+ let serverDate = new Date("2015/10/27");
+ let serverDateFmt = formatter.format(serverDate);
+ let localDateFmt = formatter.format(new Date());
+
+ let skew = Math.floor((Date.now() - serverDate.getTime()) / 1000);
+ yield new Promise(r => SpecialPowers.pushPrefEnv({set:
+ [[PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, skew]]}, r));
+
+ info("Loading a bad cert page with a skewed clock");
+ let message = yield Task.spawn(setUpPage);
+
+ isnot(message.divDisplay, "none", "Wrong time message information is visible");
+ ok(message.text.includes("because your clock appears to show the wrong time"),
+ "Correct error message found");
+ ok(message.text.includes("expired.example.com"), "URL found in error message");
+ ok(message.systemDate.includes(localDateFmt), "correct local date displayed");
+ ok(message.actualDate.includes(serverDateFmt), "correct server date displayed");
+ ok(message.learnMoreLink.includes("time-errors"), "time-errors in the Learn More URL");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // pretend we have a negatively skewed (behind) system time
+ serverDate = new Date();
+ serverDate.setYear(serverDate.getFullYear() + 1);
+ serverDateFmt = formatter.format(serverDate);
+
+ skew = Math.floor((Date.now() - serverDate.getTime()) / 1000);
+ yield new Promise(r => SpecialPowers.pushPrefEnv({set:
+ [[PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, skew]]}, r));
+
+ info("Loading a bad cert page with a skewed clock");
+ message = yield Task.spawn(setUpPage);
+
+ isnot(message.divDisplay, "none", "Wrong time message information is visible");
+ ok(message.text.includes("because your clock appears to show the wrong time"),
+ "Correct error message found");
+ ok(message.text.includes("expired.example.com"), "URL found in error message");
+ ok(message.systemDate.includes(localDateFmt), "correct local date displayed");
+ ok(message.actualDate.includes(serverDateFmt), "correct server date displayed");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // pretend we only have a slightly skewed system time, four hours
+ skew = 60 * 60 * 4;
+ yield new Promise(r => SpecialPowers.pushPrefEnv({set:
+ [[PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, skew]]}, r));
+
+ info("Loading a bad cert page with an only slightly skewed clock");
+ message = yield Task.spawn(setUpPage);
+
+ is(message.divDisplay, "none", "Wrong time message information is not visible");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ // now pretend we have no skewed system time
+ skew = 0;
+ yield new Promise(r => SpecialPowers.pushPrefEnv({set:
+ [[PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, skew]]}, r));
+
+ info("Loading a bad cert page with no skewed clock");
+ message = yield Task.spawn(setUpPage);
+
+ is(message.divDisplay, "none", "Wrong time message information is not visible");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(function* checkAdvancedDetails() {
+ info("Loading a bad cert page and verifying the main error and advanced details section");
+ let browser;
+ let certErrorLoaded;
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ gBrowser.selectedTab = gBrowser.addTab(BAD_CERT);
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = waitForCertErrorLoad(browser);
+ }, false);
+
+ info("Loading and waiting for the cert error");
+ yield certErrorLoaded;
+
+ let message = yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let shortDescText = doc.getElementById("errorShortDescText");
+ info("Main error text: " + shortDescText.textContent);
+ ok(shortDescText.textContent.includes("expired.example.com"),
+ "Should list hostname in error message.");
+
+ let advancedButton = doc.getElementById("advancedButton");
+ advancedButton.click();
+ let el = doc.getElementById("errorCode");
+ return { textContent: el.textContent, tagName: el.tagName };
+ });
+ is(message.textContent, "SEC_ERROR_EXPIRED_CERTIFICATE",
+ "Correct error message found");
+ is(message.tagName, "a", "Error message is a link");
+
+ message = yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let errorCode = doc.getElementById("errorCode");
+ errorCode.click();
+ let div = doc.getElementById("certificateErrorDebugInformation");
+ let text = doc.getElementById("certificateErrorText");
+
+ let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ let serializable = docShell.failedChannel.securityInfo
+ .QueryInterface(Ci.nsITransportSecurityInfo)
+ .QueryInterface(Ci.nsISerializable);
+ let serializedSecurityInfo = serhelper.serializeToString(serializable);
+ return {
+ divDisplay: content.getComputedStyle(div).display,
+ text: text.textContent,
+ securityInfoAsString: serializedSecurityInfo
+ };
+ });
+ isnot(message.divDisplay, "none", "Debug information is visible");
+ ok(message.text.includes(BAD_CERT), "Correct URL found");
+ ok(message.text.includes("Certificate has expired"),
+ "Correct error message found");
+ ok(message.text.includes("HTTP Strict Transport Security: false"),
+ "Correct HSTS value found");
+ ok(message.text.includes("HTTP Public Key Pinning: false"),
+ "Correct HPKP value found");
+ let certChain = getCertChain(message.securityInfoAsString);
+ ok(message.text.includes(certChain), "Found certificate chain");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(function* checkAdvancedDetailsForHSTS() {
+ info("Loading a bad STS cert page and verifying the advanced details section");
+ let browser;
+ let certErrorLoaded;
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ gBrowser.selectedTab = gBrowser.addTab(BAD_STS_CERT);
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = waitForCertErrorLoad(browser);
+ }, false);
+
+ info("Loading and waiting for the cert error");
+ yield certErrorLoaded;
+
+ let message = yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let advancedButton = doc.getElementById("advancedButton");
+ advancedButton.click();
+ let ec = doc.getElementById("errorCode");
+ let cdl = doc.getElementById("cert_domain_link");
+ return {
+ ecTextContent: ec.textContent,
+ ecTagName: ec.tagName,
+ cdlTextContent: cdl.textContent,
+ cdlTagName: cdl.tagName
+ };
+ });
+
+ const badStsUri = Services.io.newURI(BAD_STS_CERT, null, null);
+ is(message.ecTextContent, "SSL_ERROR_BAD_CERT_DOMAIN",
+ "Correct error message found");
+ is(message.ecTagName, "a", "Error message is a link");
+ const url = badStsUri.prePath.slice(badStsUri.prePath.indexOf(".") + 1);
+ is(message.cdlTextContent, url,
+ "Correct cert_domain_link contents found");
+ is(message.cdlTagName, "a", "cert_domain_link is a link");
+
+ message = yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let errorCode = doc.getElementById("errorCode");
+ errorCode.click();
+ let div = doc.getElementById("certificateErrorDebugInformation");
+ let text = doc.getElementById("certificateErrorText");
+
+ let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ let serializable = docShell.failedChannel.securityInfo
+ .QueryInterface(Ci.nsITransportSecurityInfo)
+ .QueryInterface(Ci.nsISerializable);
+ let serializedSecurityInfo = serhelper.serializeToString(serializable);
+ return {
+ divDisplay: content.getComputedStyle(div).display,
+ text: text.textContent,
+ securityInfoAsString: serializedSecurityInfo
+ };
+ });
+ isnot(message.divDisplay, "none", "Debug information is visible");
+ ok(message.text.includes(badStsUri.spec), "Correct URL found");
+ ok(message.text.includes("requested domain name does not match the server\u2019s certificate"),
+ "Correct error message found");
+ ok(message.text.includes("HTTP Strict Transport Security: false"),
+ "Correct HSTS value found");
+ ok(message.text.includes("HTTP Public Key Pinning: true"),
+ "Correct HPKP value found");
+ let certChain = getCertChain(message.securityInfoAsString);
+ ok(message.text.includes(certChain), "Found certificate chain");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(function* checkUnknownIssuerLearnMoreLink() {
+ info("Loading a cert error for self-signed pages and checking the correct link is shown");
+ let browser;
+ let certErrorLoaded;
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ gBrowser.selectedTab = gBrowser.addTab(UNKNOWN_ISSUER);
+ browser = gBrowser.selectedBrowser;
+ certErrorLoaded = waitForCertErrorLoad(browser);
+ }, false);
+
+ info("Loading and waiting for the cert error");
+ yield certErrorLoaded;
+
+ let href = yield ContentTask.spawn(browser, null, function* () {
+ let learnMoreLink = content.document.getElementById("learnMoreLink");
+ return learnMoreLink.href;
+ });
+ ok(href.endsWith("security-error"), "security-error in the Learn More URL");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+function waitForCertErrorLoad(browser) {
+ return new Promise(resolve => {
+ info("Waiting for DOMContentLoaded event");
+ browser.addEventListener("DOMContentLoaded", function load() {
+ browser.removeEventListener("DOMContentLoaded", load, false, true);
+ resolve();
+ }, false, true);
+ });
+}
+
+function getCertChain(securityInfoAsString) {
+ let certChain = "";
+ const serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ let securityInfo = serhelper.deserializeObject(securityInfoAsString);
+ securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ let certs = securityInfo.failedCertChain.getEnumerator();
+ while (certs.hasMoreElements()) {
+ let cert = certs.getNext();
+ cert.QueryInterface(Ci.nsIX509Cert);
+ certChain += getPEMString(cert);
+ }
+ return certChain;
+}
+
+function getDERString(cert)
+{
+ var length = {};
+ var derArray = cert.getRawDER(length);
+ var derString = '';
+ for (var i = 0; i < derArray.length; i++) {
+ derString += String.fromCharCode(derArray[i]);
+ }
+ return derString;
+}
+
+function getPEMString(cert)
+{
+ var derb64 = btoa(getDERString(cert));
+ // Wrap the Base64 string into lines of 64 characters,
+ // with CRLF line breaks (as specified in RFC 1421).
+ var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n");
+ return "-----BEGIN CERTIFICATE-----\r\n"
+ + wrapped
+ + "\r\n-----END CERTIFICATE-----\r\n";
+}
diff --git a/browser/base/content/test/general/browser_aboutHealthReport.js b/browser/base/content/test/general/browser_aboutHealthReport.js
new file mode 100644
index 000000000..0be184fb8
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutHealthReport.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
+const HTTPS_BASE = "https://example.com/browser/browser/base/content/test/general/";
+
+const TELEMETRY_LOG_PREF = "toolkit.telemetry.log.level";
+const telemetryOriginalLogPref = Preferences.get(TELEMETRY_LOG_PREF, null);
+
+const originalReportUrl = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrl");
+
+registerCleanupFunction(function() {
+ // Ensure we don't pollute prefs for next tests.
+ if (telemetryOriginalLogPref) {
+ Preferences.set(TELEMETRY_LOG_PREF, telemetryOriginalLogPref);
+ } else {
+ Preferences.reset(TELEMETRY_LOG_PREF);
+ }
+
+ try {
+ Services.prefs.setCharPref("datareporting.healthreport.about.reportUrl", originalReportUrl);
+ Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true);
+ } catch (ex) {}
+});
+
+function fakeTelemetryNow(...args) {
+ let date = new Date(...args);
+ let scope = {};
+ const modules = [
+ Cu.import("resource://gre/modules/TelemetrySession.jsm", scope),
+ Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", scope),
+ Cu.import("resource://gre/modules/TelemetryController.jsm", scope),
+ ];
+
+ for (let m of modules) {
+ m.Policy.now = () => new Date(date);
+ }
+
+ return date;
+}
+
+function* setupPingArchive() {
+ let scope = {};
+ Cu.import("resource://gre/modules/TelemetryController.jsm", scope);
+ Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript(CHROME_BASE + "healthreport_pingData.js", scope);
+
+ for (let p of scope.TEST_PINGS) {
+ fakeTelemetryNow(p.date);
+ p.id = yield scope.TelemetryController.submitExternalPing(p.type, p.payload);
+ }
+}
+
+var gTests = [
+
+{
+ desc: "Test the remote commands",
+ setup: Task.async(function*()
+ {
+ Preferences.set(TELEMETRY_LOG_PREF, "Trace");
+ yield setupPingArchive();
+ Preferences.set("datareporting.healthreport.about.reportUrl",
+ HTTPS_BASE + "healthreport_testRemoteCommands.html");
+ }),
+ run: function (iframe)
+ {
+ let deferred = Promise.defer();
+ let results = 0;
+ try {
+ iframe.contentWindow.addEventListener("FirefoxHealthReportTestResponse", function evtHandler(event) {
+ let data = event.detail.data;
+ if (data.type == "testResult") {
+ ok(data.pass, data.info);
+ results++;
+ }
+ else if (data.type == "testsComplete") {
+ is(results, data.count, "Checking number of results received matches the number of tests that should have run");
+ iframe.contentWindow.removeEventListener("FirefoxHealthReportTestResponse", evtHandler, true);
+ deferred.resolve();
+ }
+ }, true);
+
+ } catch (e) {
+ ok(false, "Failed to get all commands");
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+},
+
+]; // gTests
+
+function test()
+{
+ waitForExplicitFinish();
+
+ // xxxmpc leaving this here until we resolve bug 854038 and bug 854060
+ requestLongerTimeout(10);
+
+ Task.spawn(function* () {
+ for (let testCase of gTests) {
+ info(testCase.desc);
+ yield testCase.setup();
+
+ let iframe = yield promiseNewTabLoadEvent("about:healthreport");
+
+ yield testCase.run(iframe);
+
+ gBrowser.removeCurrentTab();
+ }
+
+ finish();
+ });
+}
+
+function promiseNewTabLoadEvent(aUrl, aEventType="load")
+{
+ let deferred = Promise.defer();
+ let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+ tab.linkedBrowser.addEventListener(aEventType, function load(event) {
+ tab.linkedBrowser.removeEventListener(aEventType, load, true);
+ let iframe = tab.linkedBrowser.contentDocument.getElementById("remote-report");
+ iframe.addEventListener("load", function frameLoad(e) {
+ if (iframe.contentWindow.location.href == "about:blank" ||
+ e.target != iframe) {
+ return;
+ }
+ iframe.removeEventListener("load", frameLoad, false);
+ deferred.resolve(iframe);
+ }, false);
+ }, true);
+ return deferred.promise;
+}
diff --git a/browser/base/content/test/general/browser_aboutHome.js b/browser/base/content/test/general/browser_aboutHome.js
new file mode 100644
index 000000000..f0e19e852
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutHome.js
@@ -0,0 +1,668 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This test needs to be split up. See bug 1258717.
+requestLongerTimeout(4);
+ignoreAllUncaughtExceptions();
+
+XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
+ "resource:///modules/AboutHome.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+
+const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/browser/base/" +
+ "content/test/general/aboutHome_content_script.js";
+var gRightsVersion = Services.prefs.getIntPref("browser.rights.version");
+
+registerCleanupFunction(function() {
+ // Ensure we don't pollute prefs for next tests.
+ Services.prefs.clearUserPref("network.cookies.cookieBehavior");
+ Services.prefs.clearUserPref("network.cookie.lifetimePolicy");
+ Services.prefs.clearUserPref("browser.rights.override");
+ Services.prefs.clearUserPref("browser.rights." + gRightsVersion + ".shown");
+});
+
+add_task(function* () {
+ info("Check that clearing cookies does not clear storage");
+
+ yield withSnippetsMap(
+ () => {
+ Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService)
+ .notifyObservers(null, "cookie-changed", "cleared");
+ },
+ function* () {
+ isnot(content.gSnippetsMap.get("snippets-last-update"), null,
+ "snippets-last-update should have a value");
+ });
+});
+
+add_task(function* () {
+ info("Check default snippets are shown");
+
+ yield withSnippetsMap(null, function* () {
+ let doc = content.document;
+ let snippetsElt = doc.getElementById("snippets");
+ ok(snippetsElt, "Found snippets element")
+ is(snippetsElt.getElementsByTagName("span").length, 1,
+ "A default snippet is present.");
+ });
+});
+
+add_task(function* () {
+ info("Check default snippets are shown if snippets are invalid xml");
+
+ yield withSnippetsMap(
+ // This must set some incorrect xhtml code.
+ snippetsMap => snippetsMap.set("snippets", "<p><b></p></b>"),
+ function* () {
+ let doc = content.document;
+ let snippetsElt = doc.getElementById("snippets");
+ ok(snippetsElt, "Found snippets element");
+ is(snippetsElt.getElementsByTagName("span").length, 1,
+ "A default snippet is present.");
+
+ content.gSnippetsMap.delete("snippets");
+ });
+});
+
+add_task(function* () {
+ info("Check that performing a search fires a search event and records to Telemetry.");
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ let currEngine = Services.search.currentEngine;
+ let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
+ // Make this actually work in healthreport by giving it an ID:
+ Object.defineProperty(engine.wrappedJSObject, "identifier",
+ { value: "org.mozilla.testsearchsuggestions" });
+
+ let p = promiseContentSearchChange(browser, engine.name);
+ Services.search.currentEngine = engine;
+ yield p;
+
+ yield ContentTask.spawn(browser, { expectedName: engine.name }, function* (args) {
+ let engineName = content.wrappedJSObject.gContentSearchController.defaultEngine.name;
+ is(engineName, args.expectedName, "Engine name in DOM should match engine we just added");
+ });
+
+ let numSearchesBefore = 0;
+ // Get the current number of recorded searches.
+ let histogramKey = engine.identifier + ".abouthome";
+ try {
+ let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
+ if (histogramKey in hs) {
+ numSearchesBefore = hs[histogramKey].sum;
+ }
+ } catch (ex) {
+ // No searches performed yet, not a problem, |numSearchesBefore| is 0.
+ }
+
+ let searchStr = "a search";
+
+ let expectedURL = Services.search.currentEngine
+ .getSubmission(searchStr, null, "homepage").uri.spec;
+ let promise = waitForDocLoadAndStopIt(expectedURL, browser);
+
+ // Perform a search to increase the SEARCH_COUNT histogram.
+ yield ContentTask.spawn(browser, { searchStr }, function* (args) {
+ let doc = content.document;
+ info("Perform a search.");
+ doc.getElementById("searchText").value = args.searchStr;
+ doc.getElementById("searchSubmit").click();
+ });
+
+ yield promise;
+
+ // Make sure the SEARCH_COUNTS histogram has the right key and count.
+ let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
+ Assert.ok(histogramKey in hs, "histogram with key should be recorded");
+ Assert.equal(hs[histogramKey].sum, numSearchesBefore + 1,
+ "histogram sum should be incremented");
+
+ Services.search.currentEngine = currEngine;
+ try {
+ Services.search.removeEngine(engine);
+ } catch (ex) {}
+ });
+});
+
+add_task(function* () {
+ info("Check snippets map is cleared if cached version is old");
+
+ yield withSnippetsMap(
+ snippetsMap => {
+ snippetsMap.set("snippets", "test");
+ snippetsMap.set("snippets-cached-version", 0);
+ },
+ function* () {
+ let snippetsMap = content.gSnippetsMap;
+ ok(!snippetsMap.has("snippets"), "snippets have been properly cleared");
+ ok(!snippetsMap.has("snippets-cached-version"),
+ "cached-version has been properly cleared");
+ });
+});
+
+add_task(function* () {
+ info("Check cached snippets are shown if cached version is current");
+
+ yield withSnippetsMap(
+ snippetsMap => snippetsMap.set("snippets", "test"),
+ function* (args) {
+ let doc = content.document;
+ let snippetsMap = content.gSnippetsMap
+
+ let snippetsElt = doc.getElementById("snippets");
+ ok(snippetsElt, "Found snippets element");
+ is(snippetsElt.innerHTML, "test", "Cached snippet is present.");
+
+ is(snippetsMap.get("snippets"), "test", "snippets still cached");
+ is(snippetsMap.get("snippets-cached-version"),
+ args.expectedVersion,
+ "cached-version is correct");
+ ok(snippetsMap.has("snippets-last-update"), "last-update still exists");
+ }, { expectedVersion: AboutHomeUtils.snippetsVersion });
+});
+
+add_task(function* () {
+ info("Check if the 'Know Your Rights' default snippet is shown when " +
+ "'browser.rights.override' pref is set and that its link works");
+
+ Services.prefs.setBoolPref("browser.rights.override", false);
+
+ ok(AboutHomeUtils.showKnowYourRights, "AboutHomeUtils.showKnowYourRights should be TRUE");
+
+ yield withSnippetsMap(null, function* () {
+ let doc = content.document;
+ let snippetsElt = doc.getElementById("snippets");
+ ok(snippetsElt, "Found snippets element");
+ let linkEl = snippetsElt.querySelector("a");
+ is(linkEl.href, "about:rights", "Snippet link is present.");
+ }, null, function* () {
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, "about:rights");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("a[href='about:rights']", {
+ button: 0
+ }, gBrowser.selectedBrowser);
+ yield loadPromise;
+ is(gBrowser.currentURI.spec, "about:rights", "about:rights should have opened.");
+ });
+
+
+ Services.prefs.clearUserPref("browser.rights.override");
+});
+
+add_task(function* () {
+ info("Check if the 'Know Your Rights' default snippet is NOT shown when " +
+ "'browser.rights.override' pref is NOT set");
+
+ Services.prefs.setBoolPref("browser.rights.override", true);
+
+ let rightsData = AboutHomeUtils.knowYourRightsData;
+ ok(!rightsData, "AboutHomeUtils.knowYourRightsData should be FALSE");
+
+ yield withSnippetsMap(null, function*() {
+ let doc = content.document;
+ let snippetsElt = doc.getElementById("snippets");
+ ok(snippetsElt, "Found snippets element");
+ ok(snippetsElt.getElementsByTagName("a")[0].href != "about:rights",
+ "Snippet link should not point to about:rights.");
+ });
+
+ Services.prefs.clearUserPref("browser.rights.override");
+});
+
+add_task(function* () {
+ info("Check POST search engine support");
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ return new Promise(resolve => {
+ let searchObserver = Task.async(function* search_observer(subject, topic, data) {
+ let currEngine = Services.search.defaultEngine;
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ info("Observer: " + data + " for " + engine.name);
+
+ if (data != "engine-added")
+ return;
+
+ if (engine.name != "POST Search")
+ return;
+
+ Services.obs.removeObserver(searchObserver, "browser-search-engine-modified");
+
+ // Ready to execute the tests!
+ let needle = "Search for something awesome.";
+
+ let p = promiseContentSearchChange(browser, engine.name);
+ Services.search.defaultEngine = engine;
+ yield p;
+
+ let promise = BrowserTestUtils.browserLoaded(browser);
+
+ yield ContentTask.spawn(browser, { needle }, function* (args) {
+ let doc = content.document;
+ doc.getElementById("searchText").value = args.needle;
+ doc.getElementById("searchSubmit").click();
+ });
+
+ yield promise;
+
+ // When the search results load, check them for correctness.
+ yield ContentTask.spawn(browser, { needle }, function* (args) {
+ let loadedText = content.document.body.textContent;
+ ok(loadedText, "search page loaded");
+ is(loadedText, "searchterms=" + escape(args.needle.replace(/\s/g, "+")),
+ "Search text should arrive correctly");
+ });
+
+ Services.search.defaultEngine = currEngine;
+ try {
+ Services.search.removeEngine(engine);
+ } catch (ex) {}
+ resolve();
+ });
+ Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false);
+ Services.search.addEngine("http://test:80/browser/browser/base/content/test/general/POSTSearchEngine.xml",
+ null, null, false);
+ });
+ });
+});
+
+add_task(function* () {
+ info("Make sure that a page can't imitate about:home");
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ let promise = BrowserTestUtils.browserLoaded(browser);
+ browser.loadURI("https://example.com/browser/browser/base/content/test/general/test_bug959531.html");
+ yield promise;
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let button = content.document.getElementById("settings");
+ ok(button, "Found settings button in test page");
+ button.click();
+ });
+
+ yield new Promise(resolve => {
+ // It may take a few turns of the event loop before the window
+ // is displayed, so we wait.
+ function check(n) {
+ let win = Services.wm.getMostRecentWindow("Browser:Preferences");
+ ok(!win, "Preferences window not showing");
+ if (win) {
+ win.close();
+ }
+
+ if (n > 0) {
+ executeSoon(() => check(n-1));
+ } else {
+ resolve();
+ }
+ }
+
+ check(5);
+ });
+ });
+});
+
+add_task(function* () {
+ // See browser_contentSearchUI.js for comprehensive content search UI tests.
+ info("Search suggestion smoke test");
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ // Add a test engine that provides suggestions and switch to it.
+ let currEngine = Services.search.currentEngine;
+ let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
+ let p = promiseContentSearchChange(browser, engine.name);
+ Services.search.currentEngine = engine;
+ yield p;
+
+ yield ContentTask.spawn(browser, null, function* () {
+ // Avoid intermittent failures.
+ content.wrappedJSObject.gContentSearchController.remoteTimeout = 5000;
+
+ // Type an X in the search input.
+ let input = content.document.getElementById("searchText");
+ input.focus();
+ });
+
+ yield BrowserTestUtils.synthesizeKey("x", {}, browser);
+
+ yield ContentTask.spawn(browser, null, function* () {
+ // Wait for the search suggestions to become visible.
+ let table = content.document.getElementById("searchSuggestionTable");
+ let input = content.document.getElementById("searchText");
+
+ yield new Promise(resolve => {
+ let observer = new content.MutationObserver(() => {
+ if (input.getAttribute("aria-expanded") == "true") {
+ observer.disconnect();
+ ok(!table.hidden, "Search suggestion table unhidden");
+ resolve();
+ }
+ });
+ observer.observe(input, {
+ attributes: true,
+ attributeFilter: ["aria-expanded"],
+ });
+ });
+ });
+
+ // Empty the search input, causing the suggestions to be hidden.
+ yield BrowserTestUtils.synthesizeKey("a", { accelKey: true }, browser);
+ yield BrowserTestUtils.synthesizeKey("VK_DELETE", {}, browser);
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let table = content.document.getElementById("searchSuggestionTable");
+ yield ContentTaskUtils.waitForCondition(() => table.hidden,
+ "Search suggestion table hidden");
+ });
+
+ Services.search.currentEngine = currEngine;
+ try {
+ Services.search.removeEngine(engine);
+ } catch (ex) { }
+ });
+});
+
+add_task(function* () {
+ info("Clicking suggestion list while composing");
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ // Add a test engine that provides suggestions and switch to it.
+ let currEngine = Services.search.currentEngine;
+ let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
+ let p = promiseContentSearchChange(browser, engine.name);
+ Services.search.currentEngine = engine;
+ yield p;
+
+ yield ContentTask.spawn(browser, null, function* () {
+ // Start composition and type "x"
+ let input = content.document.getElementById("searchText");
+ input.focus();
+ });
+
+ yield BrowserTestUtils.synthesizeComposition({
+ type: "compositionstart",
+ data: ""
+ }, browser);
+ yield BrowserTestUtils.synthesizeCompositionChange({
+ composition: {
+ string: "x",
+ clauses: [
+ { length: 1, attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE }
+ ]
+ },
+ caret: { start: 1, length: 0 }
+ }, browser);
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let searchController = content.wrappedJSObject.gContentSearchController;
+
+ // Wait for the search suggestions to become visible.
+ let table = searchController._suggestionsList;
+ let input = content.document.getElementById("searchText");
+
+ yield new Promise(resolve => {
+ let observer = new content.MutationObserver(() => {
+ if (input.getAttribute("aria-expanded") == "true") {
+ observer.disconnect();
+ ok(!table.hidden, "Search suggestion table unhidden");
+ resolve();
+ }
+ });
+ observer.observe(input, {
+ attributes: true,
+ attributeFilter: ["aria-expanded"],
+ });
+ });
+
+ let row = table.children[1];
+ row.setAttribute("id", "TEMPID");
+
+ // ContentSearchUIController looks at the current selectedIndex when
+ // performing a search. Synthesizing the mouse event on the suggestion
+ // doesn't actually mouseover the suggestion and trigger it to be flagged
+ // as selected, so we manually select it first.
+ searchController.selectedIndex = 1;
+ });
+
+ // Click the second suggestion.
+ let expectedURL = Services.search.currentEngine
+ .getSubmission("xbar", null, "homepage").uri.spec;
+ let loadPromise = waitForDocLoadAndStopIt(expectedURL);
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#TEMPID", {
+ button: 0
+ }, browser);
+ yield loadPromise;
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let input = content.document.getElementById("searchText");
+ ok(input.value == "x", "Input value did not change");
+
+ let row = content.document.getElementById("TEMPID");
+ if (row) {
+ row.removeAttribute("id");
+ }
+ });
+
+ Services.search.currentEngine = currEngine;
+ try {
+ Services.search.removeEngine(engine);
+ } catch (ex) { }
+ });
+});
+
+add_task(function* () {
+ info("Pressing any key should focus the search box in the page, and send the key to it");
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#brandLogo", {}, browser);
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ isnot(doc.getElementById("searchText"), doc.activeElement,
+ "Search input should not be the active element.");
+ });
+
+ yield BrowserTestUtils.synthesizeKey("a", {}, browser);
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ let searchInput = doc.getElementById("searchText");
+ yield ContentTaskUtils.waitForCondition(() => doc.activeElement === searchInput,
+ "Search input should be the active element.");
+ is(searchInput.value, "a", "Search input should be 'a'.");
+ });
+ });
+});
+
+add_task(function* () {
+ info("Cmd+k should focus the search box in the toolbar when it's present");
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#brandLogo", {}, browser);
+
+ let doc = window.document;
+ let searchInput = doc.getElementById("searchbar").textbox.inputField;
+ isnot(searchInput, doc.activeElement, "Search bar should not be the active element.");
+
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ yield promiseWaitForCondition(() => doc.activeElement === searchInput);
+ is(searchInput, doc.activeElement, "Search bar should be the active element.");
+ });
+});
+
+add_task(function* () {
+ info("Sync button should open about:preferences#sync");
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ let oldOpenPrefs = window.openPreferences;
+ let openPrefsPromise = new Promise(resolve => {
+ window.openPreferences = function (pane, params) {
+ resolve({ pane: pane, params: params });
+ };
+ });
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#sync", {}, browser);
+
+ let result = yield openPrefsPromise;
+ window.openPreferences = oldOpenPrefs;
+
+ is(result.pane, "paneSync", "openPreferences should be called with paneSync");
+ is(result.params.urlParams.entrypoint, "abouthome",
+ "openPreferences should be called with abouthome entrypoint");
+ });
+});
+
+add_task(function* () {
+ info("Pressing Space while the Addons button is focused should activate it");
+
+ // Skip this test on Mac, because Space doesn't activate the button there.
+ if (AppConstants.platform == "macosx") {
+ return;
+ }
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
+ info("Waiting for about:addons tab to open...");
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let addOnsButton = content.document.getElementById("addons");
+ addOnsButton.focus();
+ });
+ yield BrowserTestUtils.synthesizeKey(" ", {}, browser);
+
+ let tab = yield promiseTabOpened;
+ is(tab.linkedBrowser.currentURI.spec, "about:addons",
+ "Should have seen the about:addons tab");
+ yield BrowserTestUtils.removeTab(tab);
+ });
+});
+
+/**
+ * Cleans up snippets and ensures that by default we don't try to check for
+ * remote snippets since that may cause network bustage or slowness.
+ *
+ * @param aSetupFn
+ * The setup function to be run.
+ * @param testFn
+ * the content task to run
+ * @param testArgs (optional)
+ * the parameters to pass to the content task
+ * @param parentFn (optional)
+ * the function to run in the parent after the content task has completed.
+ * @return {Promise} resolved when the snippets are ready. Gets the snippets map.
+ */
+function* withSnippetsMap(setupFn, testFn, testArgs = null, parentFn = null) {
+ let setupFnSource;
+ if (setupFn) {
+ setupFnSource = setupFn.toSource();
+ }
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
+ let promiseAfterLocationChange = () => {
+ return ContentTask.spawn(browser, {
+ setupFnSource,
+ version: AboutHomeUtils.snippetsVersion,
+ }, function* (args) {
+ return new Promise(resolve => {
+ let document = content.document;
+ // We're not using Promise-based listeners, because they resolve asynchronously.
+ // The snippets test setup code relies on synchronous behaviour here.
+ document.addEventListener("AboutHomeLoadSnippets", function loadSnippets() {
+ document.removeEventListener("AboutHomeLoadSnippets", loadSnippets);
+
+ let updateSnippets;
+ if (args.setupFnSource) {
+ updateSnippets = eval(`(() => (${args.setupFnSource}))()`);
+ }
+
+ content.wrappedJSObject.ensureSnippetsMapThen(snippetsMap => {
+ snippetsMap = Cu.waiveXrays(snippetsMap);
+ info("Got snippets map: " +
+ "{ last-update: " + snippetsMap.get("snippets-last-update") +
+ ", cached-version: " + snippetsMap.get("snippets-cached-version") +
+ " }");
+ // Don't try to update.
+ snippetsMap.set("snippets-last-update", Date.now());
+ snippetsMap.set("snippets-cached-version", args.version);
+ // Clear snippets.
+ snippetsMap.delete("snippets");
+
+ if (updateSnippets) {
+ updateSnippets(snippetsMap);
+ }
+
+ // Tack it to the global object
+ content.gSnippetsMap = snippetsMap;
+
+ resolve();
+ });
+ });
+ });
+ });
+ };
+
+ // We'd like to listen to the 'AboutHomeLoadSnippets' event on a fresh
+ // document as soon as technically possible, so we use webProgress.
+ let promise = new Promise(resolve => {
+ let wpl = {
+ onLocationChange() {
+ gBrowser.removeProgressListener(wpl);
+ // Phase 2: retrieving the snippets map is the next promise on our agenda.
+ promiseAfterLocationChange().then(resolve);
+ },
+ onProgressChange() {},
+ onStatusChange() {},
+ onSecurityChange() {}
+ };
+ gBrowser.addProgressListener(wpl);
+ });
+
+ // Set the URL to 'about:home' here to allow capturing the 'AboutHomeLoadSnippets'
+ // event.
+ browser.loadURI("about:home");
+ // Wait for LocationChange.
+ yield promise;
+
+ yield ContentTask.spawn(browser, testArgs, testFn);
+ if (parentFn) {
+ yield parentFn();
+ }
+ });
+}
+
+function promiseContentSearchChange(browser, newEngineName) {
+ return ContentTask.spawn(browser, { newEngineName }, function* (args) {
+ return new Promise(resolve => {
+ content.addEventListener("ContentSearchService", function listener(aEvent) {
+ if (aEvent.detail.type == "CurrentState" &&
+ content.wrappedJSObject.gContentSearchController.defaultEngine.name == args.newEngineName) {
+ content.removeEventListener("ContentSearchService", listener);
+ resolve();
+ }
+ });
+ });
+ });
+}
+
+function promiseNewEngine(basename) {
+ info("Waiting for engine to be added: " + basename);
+ return new Promise((resolve, reject) => {
+ let url = getRootDirectory(gTestPath) + basename;
+ Services.search.addEngine(url, null, "", false, {
+ onSuccess: function (engine) {
+ info("Search engine added: " + basename);
+ registerCleanupFunction(() => {
+ try {
+ Services.search.removeEngine(engine);
+ } catch (ex) { /* Can't remove the engine more than once */ }
+ });
+ resolve(engine);
+ },
+ onError: function (errCode) {
+ ok(false, "addEngine failed with error code " + errCode);
+ reject();
+ },
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_aboutHome_wrapsCorrectly.js b/browser/base/content/test/general/browser_aboutHome_wrapsCorrectly.js
new file mode 100644
index 000000000..bfe0fe9c8
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutHome_wrapsCorrectly.js
@@ -0,0 +1,28 @@
+add_task(function* () {
+ let newWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ let resizedPromise = BrowserTestUtils.waitForEvent(newWindow, "resize");
+ newWindow.resizeTo(300, 300);
+ yield resizedPromise;
+
+ yield BrowserTestUtils.openNewForegroundTab(newWindow.gBrowser, "about:home");
+
+ yield ContentTask.spawn(newWindow.gBrowser.selectedBrowser, {}, function* () {
+ Assert.equal(content.document.body.getAttribute("narrow"), "true", "narrow mode");
+ });
+
+ resizedPromise = BrowserTestUtils.waitForContentEvent(newWindow.gBrowser.selectedBrowser, "resize");
+
+
+ yield ContentTask.spawn(newWindow.gBrowser.selectedBrowser, {}, function* () {
+ content.window.resizeTo(800, 800);
+ });
+
+ yield resizedPromise;
+
+ yield ContentTask.spawn(newWindow.gBrowser.selectedBrowser, {}, function* () {
+ Assert.equal(content.document.body.hasAttribute("narrow"), false, "non-narrow mode");
+ });
+
+ yield BrowserTestUtils.closeWindow(newWindow);
+});
diff --git a/browser/base/content/test/general/browser_aboutNetError.js b/browser/base/content/test/general/browser_aboutNetError.js
new file mode 100644
index 000000000..5185cbcaa
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutNetError.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Set ourselves up for TLS error
+Services.prefs.setIntPref("security.tls.version.max", 3);
+Services.prefs.setIntPref("security.tls.version.min", 3);
+
+const LOW_TLS_VERSION = "https://tls1.example.com/";
+const {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+
+add_task(function* checkReturnToPreviousPage() {
+ info("Loading a TLS page that isn't supported, ensure we have a fix button and clicking it then loads the page");
+ let browser;
+ let pageLoaded;
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ gBrowser.selectedTab = gBrowser.addTab(LOW_TLS_VERSION);
+ browser = gBrowser.selectedBrowser;
+ pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
+ }, false);
+
+ info("Loading and waiting for the net error");
+ yield pageLoaded;
+
+ // NB: This code assumes that the error page and the test page load in the
+ // same process. If this test starts to fail, it could be because they load
+ // in different processes.
+ yield ContentTask.spawn(browser, LOW_TLS_VERSION, function* (LOW_TLS_VERSION_) {
+ ok(content.document.getElementById("prefResetButton").getBoundingClientRect().left >= 0,
+ "Should have a visible button");
+
+ ok(content.document.documentURI.startsWith("about:neterror"), "Should be showing error page");
+
+ let doc = content.document;
+ let prefResetButton = doc.getElementById("prefResetButton");
+ is(prefResetButton.getAttribute("autofocus"), "true", "prefResetButton has autofocus");
+ prefResetButton.click();
+
+ yield ContentTaskUtils.waitForEvent(this, "pageshow", true);
+
+ is(content.document.documentURI, LOW_TLS_VERSION_, "Should not be showing page");
+ });
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/general/browser_aboutSupport_newtab_security_state.js b/browser/base/content/test/general/browser_aboutSupport_newtab_security_state.js
new file mode 100644
index 000000000..e574ba978
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutSupport_newtab_security_state.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: window.location is null");
+
+
+add_task(function* checkIdentityOfAboutSupport() {
+ let tab = gBrowser.loadOneTab("about:support", {
+ referrerURI: null,
+ inBackground: false,
+ allowThirdPartyFixup: false,
+ relatedToCurrent: false,
+ skipAnimation: true,
+ allowMixedContent: false
+ });
+
+ yield promiseTabLoaded(tab);
+ let identityBox = document.getElementById("identity-box");
+ is(identityBox.className, "chromeUI", "Should know that we're chrome.");
+ gBrowser.removeTab(tab);
+});
+
diff --git a/browser/base/content/test/general/browser_accesskeys.js b/browser/base/content/test/general/browser_accesskeys.js
new file mode 100644
index 000000000..56fe3995f
--- /dev/null
+++ b/browser/base/content/test/general/browser_accesskeys.js
@@ -0,0 +1,82 @@
+add_task(function *() {
+ yield pushPrefs(["ui.key.contentAccess", 5], ["ui.key.chromeAccess", 5]);
+
+ const gPageURL1 = "data:text/html,<body><p>" +
+ "<button id='button' accesskey='y'>Button</button>" +
+ "<input id='checkbox' type='checkbox' accesskey='z'>Checkbox" +
+ "</p></body>";
+ let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL1);
+ tab1.linkedBrowser.messageManager.loadFrameScript("data:,(" + childHandleFocus.toString() + ")();", false);
+
+ Services.focus.clearFocus(window);
+
+ // Press an accesskey in the child document while the chrome is focused.
+ let focusedId = yield performAccessKey("y");
+ is(focusedId, "button", "button accesskey");
+
+ // Press an accesskey in the child document while the content document is focused.
+ focusedId = yield performAccessKey("z");
+ is(focusedId, "checkbox", "checkbox accesskey");
+
+ // Add an element with an accesskey to the chrome and press its accesskey while the chrome is focused.
+ let newButton = document.createElement("button");
+ newButton.id = "chromebutton";
+ newButton.setAttribute("accesskey", "z");
+ document.documentElement.appendChild(newButton);
+
+ Services.focus.clearFocus(window);
+
+ focusedId = yield performAccessKeyForChrome("z");
+ is(focusedId, "chromebutton", "chromebutton accesskey");
+
+ // Add a second tab and ensure that accesskey from the first tab is not used.
+ const gPageURL2 = "data:text/html,<body>" +
+ "<button id='tab2button' accesskey='y'>Button in Tab 2</button>" +
+ "</body>";
+ let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, gPageURL2);
+ tab2.linkedBrowser.messageManager.loadFrameScript("data:,(" + childHandleFocus.toString() + ")();", false);
+
+ Services.focus.clearFocus(window);
+
+ focusedId = yield performAccessKey("y");
+ is(focusedId, "tab2button", "button accesskey in tab2");
+
+ // Press the accesskey for the chrome element while the content document is focused.
+ focusedId = yield performAccessKeyForChrome("z");
+ is(focusedId, "chromebutton", "chromebutton accesskey");
+
+ newButton.parentNode.removeChild(newButton);
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+});
+
+function childHandleFocus() {
+ content.document.body.firstChild.addEventListener("focus", function focused(event) {
+ let focusedElement = content.document.activeElement;
+ focusedElement.blur();
+ sendAsyncMessage("Test:FocusFromAccessKey", { focus: focusedElement.id })
+ }, true);
+}
+
+function performAccessKey(key)
+{
+ return new Promise(resolve => {
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.addMessageListener("Test:FocusFromAccessKey", function listenForFocus(msg) {
+ mm.removeMessageListener("Test:FocusFromAccessKey", listenForFocus);
+ resolve(msg.data.focus);
+ });
+
+ EventUtils.synthesizeKey(key, { altKey: true, shiftKey: true });
+ });
+}
+
+// This version is used when a chrome elemnt is expected to be found for an accesskey.
+function* performAccessKeyForChrome(key, inChild)
+{
+ let waitFocusChangePromise = BrowserTestUtils.waitForEvent(document, "focus", true);
+ EventUtils.synthesizeKey(key, { altKey: true, shiftKey: true });
+ yield waitFocusChangePromise;
+ return document.activeElement.id;
+}
diff --git a/browser/base/content/test/general/browser_addCertException.js b/browser/base/content/test/general/browser_addCertException.js
new file mode 100644
index 000000000..e2cf34b47
--- /dev/null
+++ b/browser/base/content/test/general/browser_addCertException.js
@@ -0,0 +1,50 @@
+/* -*- 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/. */
+
+// Test adding a certificate exception by attempting to browse to a site with
+// a bad certificate, being redirected to the internal about:certerror page,
+// using the button contained therein to load the certificate exception
+// dialog, using that to add an exception, and finally successfully visiting
+// the site, including showing the right identity box and control center icons.
+add_task(function* () {
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ yield loadBadCertPage("https://expired.example.com");
+ checkControlPanelIcons();
+ let certOverrideService = Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride("expired.example.com", -1);
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Check for the correct icons in the identity box and control center.
+function checkControlPanelIcons() {
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ gIdentityHandler._identityBox.click();
+ document.getElementById("identity-popup-security-expander").click();
+
+ is_element_visible(document.getElementById("connection-icon"), "Should see connection icon");
+ let connectionIconImage = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("connection-icon"), "")
+ .getPropertyValue("list-style-image");
+ let securityViewBG = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("identity-popup-securityView"), "")
+ .getPropertyValue("background-image");
+ let securityContentBG = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("identity-popup-security-content"), "")
+ .getPropertyValue("background-image");
+ is(connectionIconImage,
+ "url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg#icon\")",
+ "Using expected icon image in the identity block");
+ is(securityViewBG,
+ "url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg#icon\")",
+ "Using expected icon image in the Control Center main view");
+ is(securityContentBG,
+ "url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg#icon\")",
+ "Using expected icon image in the Control Center subview");
+
+ gIdentityHandler._identityPopup.hidden = true;
+}
+
diff --git a/browser/base/content/test/general/browser_addKeywordSearch.js b/browser/base/content/test/general/browser_addKeywordSearch.js
new file mode 100644
index 000000000..f38050b43
--- /dev/null
+++ b/browser/base/content/test/general/browser_addKeywordSearch.js
@@ -0,0 +1,81 @@
+var testData = [
+ { desc: "No path",
+ action: "http://example.com/",
+ param: "q",
+ },
+ { desc: "With path",
+ action: "http://example.com/new-path-here/",
+ param: "q",
+ },
+ { desc: "No action",
+ action: "",
+ param: "q",
+ },
+ { desc: "With Query String",
+ action: "http://example.com/search?oe=utf-8",
+ param: "q",
+ },
+];
+
+add_task(function*() {
+ const TEST_URL = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let count = 0;
+ for (let method of ["GET", "POST"]) {
+ for (let {desc, action, param } of testData) {
+ info(`Running ${method} keyword test '${desc}'`);
+ let id = `keyword-form-${count++}`;
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let contextMenuPromise =
+ BrowserTestUtils.waitForEvent(contextMenu, "popupshown")
+ .then(() => gContextMenuContentData.popupNode);
+
+ yield ContentTask.spawn(tab.linkedBrowser,
+ { action, param, method, id }, function* (args) {
+ let doc = content.document;
+ let form = doc.createElement("form");
+ form.id = args.id;
+ form.method = args.method;
+ form.action = args.action;
+ let element = doc.createElement("input");
+ element.setAttribute("type", "text");
+ element.setAttribute("name", args.param);
+ form.appendChild(element);
+ doc.body.appendChild(form);
+ });
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter(`#${id} > input`,
+ { type : "contextmenu", button : 2 },
+ tab.linkedBrowser);
+ let target = yield contextMenuPromise;
+
+ yield new Promise(resolve => {
+ let url = action || tab.linkedBrowser.currentURI.spec;
+ let mm = tab.linkedBrowser.messageManager;
+ let onMessage = (message) => {
+ mm.removeMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage);
+ if (method == "GET") {
+ ok(message.data.spec.endsWith(`${param}=%s`),
+ `Check expected url for field named ${param} and action ${action}`);
+ } else {
+ is(message.data.spec, url,
+ `Check expected url for field named ${param} and action ${action}`);
+ is(message.data.postData, `${param}%3D%25s`,
+ `Check expected POST data for field named ${param} and action ${action}`);
+ }
+ resolve();
+ };
+ mm.addMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage);
+
+ mm.sendAsyncMessage("ContextMenu:SearchFieldBookmarkData", null, { target });
+ });
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.hidePopup();
+ yield popupHiddenPromise;
+ }
+ }
+
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_alltabslistener.js b/browser/base/content/test/general/browser_alltabslistener.js
new file mode 100644
index 000000000..a56473ec9
--- /dev/null
+++ b/browser/base/content/test/general/browser_alltabslistener.js
@@ -0,0 +1,206 @@
+var Ci = Components.interfaces;
+
+const gCompleteState = Ci.nsIWebProgressListener.STATE_STOP +
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+
+var gFrontProgressListener = {
+ onProgressChange: function (aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ },
+
+ onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
+ var state = "onStateChange";
+ info("FrontProgress: " + state + " 0x" + aStateFlags.toString(16));
+ ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener");
+ is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener");
+ gFrontNotificationsPos++;
+ },
+
+ onLocationChange: function (aWebProgress, aRequest, aLocationURI, aFlags) {
+ var state = "onLocationChange";
+ info("FrontProgress: " + state + " " + aLocationURI.spec);
+ ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener");
+ is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener");
+ gFrontNotificationsPos++;
+ },
+
+ onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) {
+ },
+
+ onSecurityChange: function (aWebProgress, aRequest, aState) {
+ var state = "onSecurityChange";
+ info("FrontProgress: " + state + " 0x" + aState.toString(16));
+ ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener");
+ is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener");
+ gFrontNotificationsPos++;
+ }
+}
+
+var gAllProgressListener = {
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ var state = "onStateChange";
+ info("AllProgress: " + state + " 0x" + aStateFlags.toString(16));
+ ok(aBrowser == gTestBrowser, state + " notification came from the correct browser");
+ ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener");
+ is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener");
+ gAllNotificationsPos++;
+
+ if ((aStateFlags & gCompleteState) == gCompleteState) {
+ ok(gAllNotificationsPos == gAllNotifications.length, "Saw the expected number of notifications");
+ ok(gFrontNotificationsPos == gFrontNotifications.length, "Saw the expected number of frontnotifications");
+ executeSoon(gNextTest);
+ }
+ },
+
+ onLocationChange: function (aBrowser, aWebProgress, aRequest, aLocationURI,
+ aFlags) {
+ var state = "onLocationChange";
+ info("AllProgress: " + state + " " + aLocationURI.spec);
+ ok(aBrowser == gTestBrowser, state + " notification came from the correct browser");
+ ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener");
+ is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener");
+ gAllNotificationsPos++;
+ },
+
+ onStatusChange: function (aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
+ var state = "onStatusChange";
+ ok(aBrowser == gTestBrowser, state + " notification came from the correct browser");
+ },
+
+ onSecurityChange: function (aBrowser, aWebProgress, aRequest, aState) {
+ var state = "onSecurityChange";
+ info("AllProgress: " + state + " 0x" + aState.toString(16));
+ ok(aBrowser == gTestBrowser, state + " notification came from the correct browser");
+ ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener");
+ is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener");
+ gAllNotificationsPos++;
+ }
+}
+
+var gFrontNotifications, gAllNotifications, gFrontNotificationsPos, gAllNotificationsPos;
+var gBackgroundTab, gForegroundTab, gBackgroundBrowser, gForegroundBrowser, gTestBrowser;
+var gTestPage = "/browser/browser/base/content/test/general/alltabslistener.html";
+const kBasePage = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+var gNextTest;
+
+function test() {
+ waitForExplicitFinish();
+
+ gBackgroundTab = gBrowser.addTab();
+ gForegroundTab = gBrowser.addTab();
+ gBackgroundBrowser = gBrowser.getBrowserForTab(gBackgroundTab);
+ gForegroundBrowser = gBrowser.getBrowserForTab(gForegroundTab);
+ gBrowser.selectedTab = gForegroundTab;
+
+ // We must wait until a page has completed loading before
+ // starting tests or we get notifications from that
+ let promises = [
+ waitForDocLoadComplete(gBackgroundBrowser),
+ waitForDocLoadComplete(gForegroundBrowser)
+ ];
+ gBackgroundBrowser.loadURI(kBasePage);
+ gForegroundBrowser.loadURI(kBasePage);
+ Promise.all(promises).then(startTest1);
+}
+
+function runTest(browser, url, next) {
+ gFrontNotificationsPos = 0;
+ gAllNotificationsPos = 0;
+ gNextTest = next;
+ gTestBrowser = browser;
+ browser.loadURI(url);
+}
+
+function startTest1() {
+ info("\nTest 1");
+ gBrowser.addProgressListener(gFrontProgressListener);
+ gBrowser.addTabsProgressListener(gAllProgressListener);
+
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onStateChange"
+ ];
+ gFrontNotifications = gAllNotifications;
+ runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest2);
+}
+
+function startTest2() {
+ info("\nTest 2");
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onSecurityChange",
+ "onStateChange"
+ ];
+ gFrontNotifications = gAllNotifications;
+ runTest(gForegroundBrowser, "https://example.com" + gTestPage, startTest3);
+}
+
+function startTest3() {
+ info("\nTest 3");
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onStateChange"
+ ];
+ gFrontNotifications = [];
+ runTest(gBackgroundBrowser, "http://example.org" + gTestPage, startTest4);
+}
+
+function startTest4() {
+ info("\nTest 4");
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onSecurityChange",
+ "onStateChange"
+ ];
+ gFrontNotifications = [];
+ runTest(gBackgroundBrowser, "https://example.com" + gTestPage, startTest5);
+}
+
+function startTest5() {
+ info("\nTest 5");
+ // Switch the foreground browser
+ [gForegroundBrowser, gBackgroundBrowser] = [gBackgroundBrowser, gForegroundBrowser];
+ [gForegroundTab, gBackgroundTab] = [gBackgroundTab, gForegroundTab];
+ // Avoid the onLocationChange this will fire
+ gBrowser.removeProgressListener(gFrontProgressListener);
+ gBrowser.selectedTab = gForegroundTab;
+ gBrowser.addProgressListener(gFrontProgressListener);
+
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onStateChange"
+ ];
+ gFrontNotifications = gAllNotifications;
+ runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest6);
+}
+
+function startTest6() {
+ info("\nTest 6");
+ gAllNotifications = [
+ "onStateChange",
+ "onLocationChange",
+ "onSecurityChange",
+ "onStateChange"
+ ];
+ gFrontNotifications = [];
+ runTest(gBackgroundBrowser, "http://example.org" + gTestPage, finishTest);
+}
+
+function finishTest() {
+ gBrowser.removeProgressListener(gFrontProgressListener);
+ gBrowser.removeTabsProgressListener(gAllProgressListener);
+ gBrowser.removeTab(gBackgroundTab);
+ gBrowser.removeTab(gForegroundTab);
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_audioTabIcon.js b/browser/base/content/test/general/browser_audioTabIcon.js
new file mode 100644
index 000000000..4d7a7bbd8
--- /dev/null
+++ b/browser/base/content/test/general/browser_audioTabIcon.js
@@ -0,0 +1,504 @@
+const PAGE = "https://example.com/browser/browser/base/content/test/general/file_mediaPlayback.html";
+const TABATTR_REMOVAL_PREFNAME = "browser.tabs.delayHidingAudioPlayingIconMS";
+const INITIAL_TABATTR_REMOVAL_DELAY_MS = Services.prefs.getIntPref(TABATTR_REMOVAL_PREFNAME);
+
+function* wait_for_tab_playing_event(tab, expectPlaying) {
+ if (tab.soundPlaying == expectPlaying) {
+ ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+ return true;
+ }
+ return yield BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, (event) => {
+ if (event.detail.changed.includes("soundplaying")) {
+ is(tab.hasAttribute("soundplaying"), expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+ is(tab.soundPlaying, expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+ return true;
+ }
+ return false;
+ });
+}
+
+function* play(tab) {
+ let browser = tab.linkedBrowser;
+ yield ContentTask.spawn(browser, {}, function* () {
+ let audio = content.document.querySelector("audio");
+ audio.play();
+ });
+
+ yield wait_for_tab_playing_event(tab, true);
+}
+
+function* pause(tab, options) {
+ ok(tab.hasAttribute("soundplaying"), "The tab should have the soundplaying attribute when pause() is called");
+
+ let extendedDelay = options && options.extendedDelay;
+ if (extendedDelay) {
+ // Use 10s to remove possibility of race condition with attr removal.
+ Services.prefs.setIntPref(TABATTR_REMOVAL_PREFNAME, 10000);
+ }
+
+ try {
+ let browser = tab.linkedBrowser;
+ let awaitDOMAudioPlaybackStopped =
+ BrowserTestUtils.waitForEvent(browser, "DOMAudioPlaybackStopped", "DOMAudioPlaybackStopped event should get fired after pause");
+ let awaitTabPausedAttrModified =
+ wait_for_tab_playing_event(tab, false);
+ yield ContentTask.spawn(browser, {}, function* () {
+ let audio = content.document.querySelector("audio");
+ audio.pause();
+ });
+
+ if (extendedDelay) {
+ ok(tab.hasAttribute("soundplaying"), "The tab should still have the soundplaying attribute immediately after pausing");
+
+ yield awaitDOMAudioPlaybackStopped;
+ ok(tab.hasAttribute("soundplaying"), "The tab should still have the soundplaying attribute immediately after DOMAudioPlaybackStopped");
+ }
+
+ yield awaitTabPausedAttrModified;
+ ok(!tab.hasAttribute("soundplaying"), "The tab should not have the soundplaying attribute after the timeout has resolved");
+ } finally {
+ // Make sure other tests don't timeout if an exception gets thrown above.
+ // Need to use setIntPref instead of clearUserPref because prefs_general.js
+ // overrides the default value to help this and other tests run faster.
+ Services.prefs.setIntPref(TABATTR_REMOVAL_PREFNAME, INITIAL_TABATTR_REMOVAL_DELAY_MS);
+ }
+}
+
+function disable_non_test_mouse(disable) {
+ let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ utils.disableNonTestMouseEvents(disable);
+}
+
+function* hover_icon(icon, tooltip) {
+ disable_non_test_mouse(true);
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+ EventUtils.synthesizeMouse(icon, 1, 1, {type: "mouseover"});
+ EventUtils.synthesizeMouse(icon, 2, 2, {type: "mousemove"});
+ EventUtils.synthesizeMouse(icon, 3, 3, {type: "mousemove"});
+ EventUtils.synthesizeMouse(icon, 4, 4, {type: "mousemove"});
+ return popupShownPromise;
+}
+
+function leave_icon(icon) {
+ EventUtils.synthesizeMouse(icon, 0, 0, {type: "mouseout"});
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+
+ disable_non_test_mouse(false);
+}
+
+function* test_tooltip(icon, expectedTooltip, isActiveTab) {
+ let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+
+ yield hover_icon(icon, tooltip);
+ if (isActiveTab) {
+ // The active tab should have the keybinding shortcut in the tooltip.
+ // We check this by ensuring that the strings are not equal but the expected
+ // message appears in the beginning.
+ isnot(tooltip.getAttribute("label"), expectedTooltip, "Tooltips should not be equal");
+ is(tooltip.getAttribute("label").indexOf(expectedTooltip), 0, "Correct tooltip expected");
+ } else {
+ is(tooltip.getAttribute("label"), expectedTooltip, "Tooltips should not be equal");
+ }
+ leave_icon(icon);
+}
+
+// The set of tabs which have ever had their mute state changed.
+// Used to determine whether the tab should have a muteReason value.
+let everMutedTabs = new WeakSet();
+
+function get_wait_for_mute_promise(tab, expectMuted) {
+ return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => {
+ if (event.detail.changed.includes("muted")) {
+ is(tab.hasAttribute("muted"), expectMuted, "The tab should " + (expectMuted ? "" : "not ") + "be muted");
+ is(tab.muted, expectMuted, "The tab muted property " + (expectMuted ? "" : "not ") + "be true");
+
+ if (expectMuted || everMutedTabs.has(tab)) {
+ everMutedTabs.add(tab);
+ is(tab.muteReason, null, "The tab should have a null muteReason value");
+ } else {
+ is(tab.muteReason, undefined, "The tab should have an undefined muteReason value");
+ }
+ return true;
+ }
+ return false;
+ });
+}
+
+function* test_mute_tab(tab, icon, expectMuted) {
+ let mutedPromise = test_mute_keybinding(tab, expectMuted);
+
+ let activeTab = gBrowser.selectedTab;
+
+ let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+
+ yield hover_icon(icon, tooltip);
+ EventUtils.synthesizeMouseAtCenter(icon, {button: 0});
+ leave_icon(icon);
+
+ is(gBrowser.selectedTab, activeTab, "Clicking on mute should not change the currently selected tab");
+
+ return mutedPromise;
+}
+
+function get_tab_state(tab) {
+ const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ return JSON.parse(ss.getTabState(tab));
+}
+
+function* test_muting_using_menu(tab, expectMuted) {
+ // Show the popup menu
+ let contextMenu = document.getElementById("tabContextMenu");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(tab, {type: "contextmenu", button: 2});
+ yield popupShownPromise;
+
+ // Check the menu
+ let expectedLabel = expectMuted ? "Unmute Tab" : "Mute Tab";
+ let toggleMute = document.getElementById("context_toggleMuteTab");
+ is(toggleMute.label, expectedLabel, "Correct label expected");
+ is(toggleMute.accessKey, "M", "Correct accessKey expected");
+
+ is(toggleMute.hasAttribute("muted"), expectMuted, "Should have the correct state for the muted attribute");
+ ok(!toggleMute.hasAttribute("soundplaying"), "Should not have the soundplaying attribute");
+
+ yield play(tab);
+
+ is(toggleMute.hasAttribute("muted"), expectMuted, "Should have the correct state for the muted attribute");
+ ok(toggleMute.hasAttribute("soundplaying"), "Should have the soundplaying attribute");
+
+ yield pause(tab);
+
+ is(toggleMute.hasAttribute("muted"), expectMuted, "Should have the correct state for the muted attribute");
+ ok(!toggleMute.hasAttribute("soundplaying"), "Should not have the soundplaying attribute");
+
+ // Click on the menu and wait for the tab to be muted.
+ let mutedPromise = get_wait_for_mute_promise(tab, !expectMuted);
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ EventUtils.synthesizeMouseAtCenter(toggleMute, {});
+ yield popupHiddenPromise;
+ yield mutedPromise;
+}
+
+function* test_playing_icon_on_tab(tab, browser, isPinned) {
+ let icon = document.getAnonymousElementByAttribute(tab, "anonid",
+ isPinned ? "overlay-icon" : "soundplaying-icon");
+ let isActiveTab = tab === gBrowser.selectedTab;
+
+ yield play(tab);
+
+ yield test_tooltip(icon, "Mute tab", isActiveTab);
+
+ ok(!("muted" in get_tab_state(tab)), "No muted attribute should be persisted");
+ ok(!("muteReason" in get_tab_state(tab)), "No muteReason property should be persisted");
+
+ yield test_mute_tab(tab, icon, true);
+
+ ok("muted" in get_tab_state(tab), "Muted attribute should be persisted");
+ ok("muteReason" in get_tab_state(tab), "muteReason property should be persisted");
+
+ yield test_tooltip(icon, "Unmute tab", isActiveTab);
+
+ yield test_mute_tab(tab, icon, false);
+
+ ok(!("muted" in get_tab_state(tab)), "No muted attribute should be persisted");
+ ok(!("muteReason" in get_tab_state(tab)), "No muteReason property should be persisted");
+
+ yield test_tooltip(icon, "Mute tab", isActiveTab);
+
+ yield test_mute_tab(tab, icon, true);
+
+ yield pause(tab);
+
+ ok(tab.hasAttribute("muted") &&
+ !tab.hasAttribute("soundplaying"), "Tab should still be muted but not playing");
+ ok(tab.muted && !tab.soundPlaying, "Tab should still be muted but not playing");
+
+ yield test_tooltip(icon, "Unmute tab", isActiveTab);
+
+ yield test_mute_tab(tab, icon, false);
+
+ ok(!tab.hasAttribute("muted") &&
+ !tab.hasAttribute("soundplaying"), "Tab should not be be muted or playing");
+ ok(!tab.muted && !tab.soundPlaying, "Tab should not be be muted or playing");
+
+ // Make sure it's possible to mute using the context menu.
+ yield test_muting_using_menu(tab, false);
+
+ // Make sure it's possible to unmute using the context menu.
+ yield test_muting_using_menu(tab, true);
+}
+
+function* test_swapped_browser_while_playing(oldTab, newBrowser) {
+ ok(oldTab.hasAttribute("muted"), "Expected the correct muted attribute on the old tab");
+ is(oldTab.muteReason, null, "Expected the correct muteReason attribute on the old tab");
+ ok(oldTab.hasAttribute("soundplaying"), "Expected the correct soundplaying attribute on the old tab");
+
+ let newTab = gBrowser.getTabForBrowser(newBrowser);
+ let AttrChangePromise = BrowserTestUtils.waitForEvent(newTab, "TabAttrModified", false, event => {
+ return event.detail.changed.includes("soundplaying") &&
+ event.detail.changed.includes("muted");
+ });
+
+ gBrowser.swapBrowsersAndCloseOther(newTab, oldTab);
+ yield AttrChangePromise;
+
+ ok(newTab.hasAttribute("muted"), "Expected the correct muted attribute on the new tab");
+ is(newTab.muteReason, null, "Expected the correct muteReason property on the new tab");
+ ok(newTab.hasAttribute("soundplaying"), "Expected the correct soundplaying attribute on the new tab");
+
+ let icon = document.getAnonymousElementByAttribute(newTab, "anonid",
+ "soundplaying-icon");
+ yield test_tooltip(icon, "Unmute tab", true);
+}
+
+function* test_swapped_browser_while_not_playing(oldTab, newBrowser) {
+ ok(oldTab.hasAttribute("muted"), "Expected the correct muted attribute on the old tab");
+ is(oldTab.muteReason, null, "Expected the correct muteReason property on the old tab");
+ ok(!oldTab.hasAttribute("soundplaying"), "Expected the correct soundplaying attribute on the old tab");
+
+ let newTab = gBrowser.getTabForBrowser(newBrowser);
+ let AttrChangePromise = BrowserTestUtils.waitForEvent(newTab, "TabAttrModified", false, event => {
+ return event.detail.changed.includes("muted");
+ });
+
+ let AudioPlaybackPromise = new Promise(resolve => {
+ let observer = (subject, topic, data) => {
+ ok(false, "Should not see an audio-playback notification");
+ };
+ Services.obs.addObserver(observer, "audiochannel-activity-normal", false);
+ setTimeout(() => {
+ Services.obs.removeObserver(observer, "audiochannel-activity-normal");
+ resolve();
+ }, 100);
+ });
+
+ gBrowser.swapBrowsersAndCloseOther(newTab, oldTab);
+ yield AttrChangePromise;
+
+ ok(newTab.hasAttribute("muted"), "Expected the correct muted attribute on the new tab");
+ is(newTab.muteReason, null, "Expected the correct muteReason property on the new tab");
+ ok(!newTab.hasAttribute("soundplaying"), "Expected the correct soundplaying attribute on the new tab");
+
+ // Wait to see if an audio-playback event is dispatched.
+ yield AudioPlaybackPromise;
+
+ ok(newTab.hasAttribute("muted"), "Expected the correct muted attribute on the new tab");
+ is(newTab.muteReason, null, "Expected the correct muteReason property on the new tab");
+ ok(!newTab.hasAttribute("soundplaying"), "Expected the correct soundplaying attribute on the new tab");
+
+ let icon = document.getAnonymousElementByAttribute(newTab, "anonid",
+ "soundplaying-icon");
+ yield test_tooltip(icon, "Unmute tab", true);
+}
+
+function* test_browser_swapping(tab, browser) {
+ // First, test swapping with a playing but muted tab.
+ yield play(tab);
+
+ let icon = document.getAnonymousElementByAttribute(tab, "anonid",
+ "soundplaying-icon");
+ yield test_mute_tab(tab, icon, true);
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "about:blank",
+ }, function*(newBrowser) {
+ yield test_swapped_browser_while_playing(tab, newBrowser)
+
+ // Now, test swapping with a muted but not playing tab.
+ // Note that the tab remains muted, so we only need to pause playback.
+ tab = gBrowser.getTabForBrowser(newBrowser);
+ yield pause(tab);
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "about:blank",
+ }, secondAboutBlankBrowser => test_swapped_browser_while_not_playing(tab, secondAboutBlankBrowser));
+ });
+}
+
+function* test_click_on_pinned_tab_after_mute() {
+ function* taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ gBrowser.selectedTab = originallySelectedTab;
+ isnot(tab, gBrowser.selectedTab, "Sanity check, the tab should not be selected!");
+
+ // Steps to reproduce the bug:
+ // Pin the tab.
+ gBrowser.pinTab(tab);
+
+ // Start playback and wait for it to finish.
+ yield play(tab);
+
+ // Mute the tab.
+ let icon = document.getAnonymousElementByAttribute(tab, "anonid", "overlay-icon");
+ yield test_mute_tab(tab, icon, true);
+
+ // Pause playback and wait for it to finish.
+ yield pause(tab);
+
+ // Unmute tab.
+ yield test_mute_tab(tab, icon, false);
+
+ // Now click on the tab.
+ let image = document.getAnonymousElementByAttribute(tab, "anonid", "tab-icon-image");
+ EventUtils.synthesizeMouseAtCenter(image, {button: 0});
+
+ is(tab, gBrowser.selectedTab, "Tab switch should be successful");
+
+ // Cleanup.
+ gBrowser.unpinTab(tab);
+ gBrowser.selectedTab = originallySelectedTab;
+ }
+
+ let originallySelectedTab = gBrowser.selectedTab;
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE
+ }, taskFn);
+}
+
+// This test only does something useful in e10s!
+function* test_cross_process_load() {
+ function* taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Start playback and wait for it to finish.
+ yield play(tab);
+
+ let soundPlayingStoppedPromise = BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false,
+ event => event.detail.changed.includes("soundplaying")
+ );
+
+ // Go to a different process.
+ browser.loadURI("about:");
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ yield soundPlayingStoppedPromise;
+
+ ok(!tab.hasAttribute("soundplaying"), "Tab should not be playing sound any more");
+ ok(!tab.soundPlaying, "Tab should not be playing sound any more");
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE
+ }, taskFn);
+}
+
+function* test_mute_keybinding() {
+ function* test_muting_using_keyboard(tab) {
+ let mutedPromise = get_wait_for_mute_promise(tab, true);
+ EventUtils.synthesizeKey("m", {ctrlKey: true});
+ yield mutedPromise;
+ mutedPromise = get_wait_for_mute_promise(tab, false);
+ EventUtils.synthesizeKey("m", {ctrlKey: true});
+ yield mutedPromise;
+ }
+ function* taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Make sure it's possible to mute before the tab is playing.
+ yield test_muting_using_keyboard(tab);
+
+ // Start playback and wait for it to finish.
+ yield play(tab);
+
+ // Make sure it's possible to mute after the tab is playing.
+ yield test_muting_using_keyboard(tab);
+
+ // Pause playback and wait for it to finish.
+ yield pause(tab);
+
+ // Make sure things work if the tab is pinned.
+ gBrowser.pinTab(tab);
+
+ // Make sure it's possible to mute before the tab is playing.
+ yield test_muting_using_keyboard(tab);
+
+ // Start playback and wait for it to finish.
+ yield play(tab);
+
+ // Make sure it's possible to mute after the tab is playing.
+ yield test_muting_using_keyboard(tab);
+
+ gBrowser.unpinTab(tab);
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE
+ }, taskFn);
+}
+
+function* test_on_browser(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Test the icon in a normal tab.
+ yield test_playing_icon_on_tab(tab, browser, false);
+
+ gBrowser.pinTab(tab);
+
+ // Test the icon in a pinned tab.
+ yield test_playing_icon_on_tab(tab, browser, true);
+
+ gBrowser.unpinTab(tab);
+
+ // Retest with another browser in the foreground tab
+ if (gBrowser.selectedBrowser.currentURI.spec == PAGE) {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "data:text/html,test"
+ }, () => test_on_browser(browser));
+ } else {
+ yield test_browser_swapping(tab, browser);
+ }
+}
+
+function* test_delayed_tabattr_removal() {
+ function* taskFn(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ yield play(tab);
+
+ // Extend the delay to guarantee the soundplaying attribute
+ // is not removed from the tab when audio is stopped. Without
+ // the extended delay the attribute could be removed in the
+ // same tick and the test wouldn't catch that this broke.
+ yield pause(tab, {extendedDelay: true});
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE
+ }, taskFn);
+}
+
+add_task(function*() {
+ yield new Promise((resolve) => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["browser.tabs.showAudioPlayingIcon", true],
+ ]}, resolve);
+ });
+});
+
+requestLongerTimeout(2);
+add_task(function* test_page() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE
+ }, test_on_browser);
+});
+
+add_task(test_click_on_pinned_tab_after_mute);
+
+add_task(test_cross_process_load);
+
+add_task(test_mute_keybinding);
+
+add_task(test_delayed_tabattr_removal);
diff --git a/browser/base/content/test/general/browser_backButtonFitts.js b/browser/base/content/test/general/browser_backButtonFitts.js
new file mode 100644
index 000000000..0e8aeeaee
--- /dev/null
+++ b/browser/base/content/test/general/browser_backButtonFitts.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(function* () {
+ let firstLocation = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, firstLocation);
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ // Push the state before maximizing the window and clicking below.
+ content.history.pushState("page2", "page2", "page2");
+
+ // While in the child process, add a listener for the popstate event here. This
+ // event will fire when the mouse click happens.
+ content.addEventListener("popstate", function onPopState() {
+ content.removeEventListener("popstate", onPopState, false);
+ sendAsyncMessage("Test:PopStateOccurred", { location: content.document.location.href });
+ }, false);
+ });
+
+ window.maximize();
+
+ // Find where the nav-bar is vertically.
+ var navBar = document.getElementById("nav-bar");
+ var boundingRect = navBar.getBoundingClientRect();
+ var yPixel = boundingRect.top + Math.floor(boundingRect.height / 2);
+ var xPixel = 0; // Use the first pixel of the screen since it is maximized.
+
+ let resultLocation = yield new Promise(resolve => {
+ messageManager.addMessageListener("Test:PopStateOccurred", function statePopped(message) {
+ messageManager.removeMessageListener("Test:PopStateOccurred", statePopped);
+ resolve(message.data.location);
+ });
+
+ EventUtils.synthesizeMouseAtPoint(xPixel, yPixel, {}, window);
+ });
+
+ is(resultLocation, firstLocation, "Clicking the first pixel should have navigated back.");
+ window.restore();
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
new file mode 100644
index 000000000..91a4a7e9c
--- /dev/null
+++ b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
@@ -0,0 +1,76 @@
+const TEST_PAGE = "http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
+
+var expectingDialog = false;
+var wantToClose = true;
+var resolveDialogPromise;
+function onTabModalDialogLoaded(node) {
+ ok(expectingDialog, "Should be expecting this dialog.");
+ expectingDialog = false;
+ if (wantToClose) {
+ // This accepts the dialog, closing it
+ node.Dialog.ui.button0.click();
+ } else {
+ // This keeps the page open
+ node.Dialog.ui.button1.click();
+ }
+ if (resolveDialogPromise) {
+ resolveDialogPromise();
+ }
+}
+
+SpecialPowers.pushPrefEnv({"set": [["dom.require_user_interaction_for_beforeunload", false]]});
+
+// Listen for the dialog being created
+Services.obs.addObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded", false);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.tabs.warnOnClose");
+ Services.obs.removeObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
+});
+
+add_task(function* closeLastTabInWindow() {
+ let newWin = yield promiseOpenAndLoadWindow({}, true);
+ let firstTab = newWin.gBrowser.selectedTab;
+ yield promiseTabLoadEvent(firstTab, TEST_PAGE);
+ let windowClosedPromise = promiseWindowWillBeClosed(newWin);
+ expectingDialog = true;
+ // close tab:
+ document.getAnonymousElementByAttribute(firstTab, "anonid", "close-button").click();
+ yield windowClosedPromise;
+ ok(!expectingDialog, "There should have been a dialog.");
+ ok(newWin.closed, "Window should be closed.");
+});
+
+add_task(function* closeWindowWithMultipleTabsIncludingOneBeforeUnload() {
+ Services.prefs.setBoolPref("browser.tabs.warnOnClose", false);
+ let newWin = yield promiseOpenAndLoadWindow({}, true);
+ let firstTab = newWin.gBrowser.selectedTab;
+ yield promiseTabLoadEvent(firstTab, TEST_PAGE);
+ yield promiseTabLoadEvent(newWin.gBrowser.addTab(), "http://example.com/");
+ let windowClosedPromise = promiseWindowWillBeClosed(newWin);
+ expectingDialog = true;
+ newWin.BrowserTryToCloseWindow();
+ yield windowClosedPromise;
+ ok(!expectingDialog, "There should have been a dialog.");
+ ok(newWin.closed, "Window should be closed.");
+ Services.prefs.clearUserPref("browser.tabs.warnOnClose");
+});
+
+add_task(function* closeWindoWithSingleTabTwice() {
+ let newWin = yield promiseOpenAndLoadWindow({}, true);
+ let firstTab = newWin.gBrowser.selectedTab;
+ yield promiseTabLoadEvent(firstTab, TEST_PAGE);
+ let windowClosedPromise = promiseWindowWillBeClosed(newWin);
+ expectingDialog = true;
+ wantToClose = false;
+ let firstDialogShownPromise = new Promise((resolve, reject) => { resolveDialogPromise = resolve; });
+ document.getAnonymousElementByAttribute(firstTab, "anonid", "close-button").click();
+ yield firstDialogShownPromise;
+ info("Got initial dialog, now trying again");
+ expectingDialog = true;
+ wantToClose = true;
+ resolveDialogPromise = null;
+ document.getAnonymousElementByAttribute(firstTab, "anonid", "close-button").click();
+ yield windowClosedPromise;
+ ok(!expectingDialog, "There should have been a dialog.");
+ ok(newWin.closed, "Window should be closed.");
+});
diff --git a/browser/base/content/test/general/browser_blob-channelname.js b/browser/base/content/test/general/browser_blob-channelname.js
new file mode 100644
index 000000000..d87e4a896
--- /dev/null
+++ b/browser/base/content/test/general/browser_blob-channelname.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+function test() {
+ var file = new File([new Blob(['test'], {type: 'text/plain'})], "test-name");
+ var url = URL.createObjectURL(file);
+ var channel = NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true});
+
+ is(channel.contentDispositionFilename, 'test-name', "filename matches");
+}
diff --git a/browser/base/content/test/general/browser_blockHPKP.js b/browser/base/content/test/general/browser_blockHPKP.js
new file mode 100644
index 000000000..c0d1233ab
--- /dev/null
+++ b/browser/base/content/test/general/browser_blockHPKP.js
@@ -0,0 +1,101 @@
+/* -*- 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/. */
+
+// Test that visiting a site pinned with HPKP headers does not succeed when it
+// uses a certificate with a key not in the pinset. This should result in an
+// about:neterror page
+// Also verify that removal of the HPKP headers succeeds (via HPKP headers)
+// and that after removal the visit to the site with the previously
+// unauthorized pins succeeds.
+//
+// This test required three certs to be created in build/pgo/certs:
+// 1. A new trusted root:
+// a. certutil -S -s "Alternate trusted authority" -s "CN=Alternate Trusted Authority" -t "C,," -x -m 1 -v 120 -n "alternateTrustedAuthority" -Z SHA256 -g 2048 -2 -d .
+// b. (export) certutil -L -d . -n "alternateTrustedAuthority" -a -o alternateroot.ca
+// (files ended in .ca are added as trusted roots by the mochitest harness)
+// 2. A good pinning server cert (signed by the pgo root):
+// certutil -S -n "dynamicPinningGood" -s "CN=dynamic-pinning.example.com" -c "pgo temporary ca" -t "P,," -k rsa -g 2048 -Z SHA256 -m 8939454 -v 120 -8 "*.include-subdomains.pinning-dynamic.example.com,*.pinning-dynamic.example.com" -d .
+// 3. A certificate with a different issuer, so as to cause a key pinning violation."
+// certutil -S -n "dynamicPinningBad" -s "CN=bad.include-subdomains.pinning-dynamic.example.com" -c "alternateTrustedAuthority" -t "P,," -k rsa -g 2048 -Z SHA256 -m 893945439 -v 120 -8 "bad.include-subdomains.pinning-dynamic.example.com" -d .
+
+const gSSService = Cc["@mozilla.org/ssservice;1"]
+ .getService(Ci.nsISiteSecurityService);
+const gIOService = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+
+const kPinningDomain = "include-subdomains.pinning-dynamic.example.com";
+const khpkpPinninEnablePref = "security.cert_pinning.process_headers_from_non_builtin_roots";
+const kpkpEnforcementPref = "security.cert_pinning.enforcement_level";
+const kBadPinningDomain = "bad.include-subdomains.pinning-dynamic.example.com";
+const kURLPath = "/browser/browser/base/content/test/general/pinning_headers.sjs?";
+
+function test() {
+ waitForExplicitFinish();
+ // Enable enforcing strict pinning and processing headers from
+ // non-builtin roots.
+ Services.prefs.setIntPref(kpkpEnforcementPref, 2);
+ Services.prefs.setBoolPref(khpkpPinninEnablePref, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(kpkpEnforcementPref);
+ Services.prefs.clearUserPref(khpkpPinninEnablePref);
+ let uri = gIOService.newURI("https://" + kPinningDomain, null, null);
+ gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
+ });
+ whenNewTabLoaded(window, loadPinningPage);
+}
+
+// Start by making a successful connection to a domain that will pin a site
+function loadPinningPage() {
+
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "https://" + kPinningDomain + kURLPath + "valid").then(function() {
+ gBrowser.selectedBrowser.addEventListener("load",
+ successfulPinningPageListener,
+ true);
+ });
+}
+
+// After the site is pinned try to load with a subdomain site that should
+// fail to validate
+var successfulPinningPageListener = {
+ handleEvent: function() {
+ gBrowser.selectedBrowser.removeEventListener("load", this, true);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "https://" + kBadPinningDomain).then(function() {
+ return promiseErrorPageLoaded(gBrowser.selectedBrowser);
+ }).then(errorPageLoaded);
+ }
+};
+
+// The browser should load about:neterror, when this happens, proceed
+// to load the pinning domain again, this time removing the pinning information
+function errorPageLoaded() {
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let textElement = content.document.getElementById("errorShortDescText");
+ let text = textElement.innerHTML;
+ ok(text.indexOf("MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE") > 0,
+ "Got a pinning error page");
+ }).then(function() {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "https://" + kPinningDomain + kURLPath + "zeromaxagevalid").then(function() {
+ return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ }).then(pinningRemovalLoaded);
+ });
+}
+
+// After the pinning information has been removed (successful load) proceed
+// to load again with the invalid pin domain.
+function pinningRemovalLoaded() {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "https://" + kBadPinningDomain).then(function() {
+ return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ }).then(badPinningPageLoaded);
+}
+
+// Finally, we should successfully load
+// https://bad.include-subdomains.pinning-dynamic.example.com.
+function badPinningPageLoaded() {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab).then(function() {
+ ok(true, "load complete");
+ finish();
+ });
+}
diff --git a/browser/base/content/test/general/browser_bookmark_popup.js b/browser/base/content/test/general/browser_bookmark_popup.js
new file mode 100644
index 000000000..c1ddd725e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bookmark_popup.js
@@ -0,0 +1,431 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * Test opening and closing the bookmarks panel.
+ */
+
+let bookmarkPanel = document.getElementById("editBookmarkPanel");
+let bookmarkStar = document.getElementById("bookmarks-menu-button");
+let bookmarkPanelTitle = document.getElementById("editBookmarkPanelTitle");
+let editBookmarkPanelRemoveButtonRect;
+
+StarUI._closePanelQuickForTesting = true;
+
+function* test_bookmarks_popup({isNewBookmark, popupShowFn, popupEditFn,
+ shouldAutoClose, popupHideFn, isBookmarkRemoved}) {
+ yield BrowserTestUtils.withNewTab({gBrowser, url: "about:home"}, function*(browser) {
+ try {
+ if (!isNewBookmark) {
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "about:home",
+ title: "Home Page"
+ });
+ }
+
+ info(`BookmarkingUI.status is ${BookmarkingUI.status}`);
+ yield BrowserTestUtils.waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING,
+ "BookmarkingUI should not be updating");
+
+ is(bookmarkStar.hasAttribute("starred"), !isNewBookmark,
+ "Page should only be starred prior to popupshown if editing bookmark");
+ is(bookmarkPanel.state, "closed", "Panel should be 'closed' to start test");
+ let shownPromise = promisePopupShown(bookmarkPanel);
+ yield popupShowFn(browser);
+ yield shownPromise;
+ is(bookmarkPanel.state, "open", "Panel should be 'open' after shownPromise is resolved");
+
+ editBookmarkPanelRemoveButtonRect =
+ document.getElementById("editBookmarkPanelRemoveButton").getBoundingClientRect();
+
+ if (popupEditFn) {
+ yield popupEditFn();
+ }
+ let bookmarks = [];
+ yield PlacesUtils.bookmarks.fetch({url: "about:home"}, bm => bookmarks.push(bm));
+ is(bookmarks.length, 1, "Only one bookmark should exist");
+ is(bookmarkStar.getAttribute("starred"), "true", "Page is starred");
+ is(bookmarkPanelTitle.value,
+ isNewBookmark ?
+ gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
+ gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"),
+ "title should match isEditingBookmark state");
+
+ if (!shouldAutoClose) {
+ yield new Promise(resolve => setTimeout(resolve, 400));
+ is(bookmarkPanel.state, "open", "Panel should still be 'open' for non-autoclose");
+ }
+
+ let hiddenPromise = promisePopupHidden(bookmarkPanel);
+ if (popupHideFn) {
+ yield popupHideFn();
+ }
+ yield hiddenPromise;
+ is(bookmarkStar.hasAttribute("starred"), !isBookmarkRemoved,
+ "Page is starred after closing");
+ } finally {
+ let bookmark = yield PlacesUtils.bookmarks.fetch({url: "about:home"});
+ is(!!bookmark, !isBookmarkRemoved,
+ "bookmark should not be present if a panel action should've removed it");
+ if (bookmark) {
+ yield PlacesUtils.bookmarks.remove(bookmark);
+ }
+ }
+ });
+}
+
+add_task(function* panel_shown_for_new_bookmarks_and_autocloses() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ shouldAutoClose: true,
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* panel_shown_once_for_doubleclick_on_new_bookmark_star_and_autocloses() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ EventUtils.synthesizeMouse(bookmarkStar, 10, 10, { clickCount: 2 },
+ window);
+ },
+ shouldAutoClose: true,
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* panel_shown_once_for_slow_doubleclick_on_new_bookmark_star_and_autocloses() {
+ todo(false, "bug 1250267, may need to add some tracking state to " +
+ "browser-places.js for this.");
+ return;
+
+ /*
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ *popupShowFn() {
+ EventUtils.synthesizeMouse(bookmarkStar, 10, 10, window);
+ yield new Promise(resolve => setTimeout(resolve, 300));
+ EventUtils.synthesizeMouse(bookmarkStar, 10, 10, window);
+ },
+ shouldAutoClose: true,
+ isBookmarkRemoved: false,
+ });
+ */
+});
+
+add_task(function* panel_shown_for_keyboardshortcut_on_new_bookmark_star_and_autocloses() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ EventUtils.synthesizeKey("D", {accelKey: true}, window);
+ },
+ shouldAutoClose: true,
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* panel_shown_for_new_bookmarks_mousemove_mouseout() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ *popupEditFn() {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "mousemove");
+ EventUtils.synthesizeMouseAtCenter(bookmarkPanel, {type: "mousemove"});
+ info("Waiting for mousemove event");
+ yield mouseMovePromise;
+ info("Got mousemove event");
+
+ yield new Promise(resolve => setTimeout(resolve, 400));
+ is(bookmarkPanel.state, "open", "Panel should still be open on mousemove");
+ },
+ *popupHideFn() {
+ let mouseOutPromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "mouseout");
+ EventUtils.synthesizeMouse(bookmarkPanel, 0, 0, {type: "mouseout"});
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+ info("Waiting for mouseout event");
+ yield mouseOutPromise;
+ info("Got mouseout event, should autoclose now");
+ },
+ shouldAutoClose: false,
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* panel_shown_for_new_bookmark_no_autoclose_close_with_ESC() {
+ yield test_bookmarks_popup({
+ isNewBookmark: false,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {accelKey: true}, window);
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* panel_shown_for_editing_no_autoclose_close_with_ESC() {
+ yield test_bookmarks_popup({
+ isNewBookmark: false,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {accelKey: true}, window);
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* panel_shown_for_new_bookmark_keypress_no_autoclose() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ popupEditFn() {
+ EventUtils.sendChar("VK_TAB", window);
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ bookmarkPanel.hidePopup();
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+
+add_task(function* panel_shown_for_new_bookmark_compositionstart_no_autoclose() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ *popupEditFn() {
+ let compositionStartPromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "compositionstart");
+ EventUtils.synthesizeComposition({ type: "compositionstart" }, window);
+ info("Waiting for compositionstart event");
+ yield compositionStartPromise;
+ info("Got compositionstart event");
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeComposition({ type: "compositioncommitasis" });
+ bookmarkPanel.hidePopup();
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* panel_shown_for_new_bookmark_compositionstart_mouseout_no_autoclose() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ *popupEditFn() {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "mousemove");
+ EventUtils.synthesizeMouseAtCenter(bookmarkPanel, {type: "mousemove"});
+ info("Waiting for mousemove event");
+ yield mouseMovePromise;
+ info("Got mousemove event");
+
+ let compositionStartPromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "compositionstart");
+ EventUtils.synthesizeComposition({ type: "compositionstart" }, window);
+ info("Waiting for compositionstart event");
+ yield compositionStartPromise;
+ info("Got compositionstart event");
+
+ let mouseOutPromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "mouseout");
+ EventUtils.synthesizeMouse(bookmarkPanel, 0, 0, {type: "mouseout"});
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+ info("Waiting for mouseout event");
+ yield mouseOutPromise;
+ info("Got mouseout event, but shouldn't run autoclose");
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeComposition({ type: "compositioncommitasis" });
+ bookmarkPanel.hidePopup();
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* panel_shown_for_new_bookmark_compositionend_no_autoclose() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ *popupEditFn() {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "mousemove");
+ EventUtils.synthesizeMouseAtCenter(bookmarkPanel, {type: "mousemove"});
+ info("Waiting for mousemove event");
+ yield mouseMovePromise;
+ info("Got mousemove event");
+
+ EventUtils.synthesizeComposition({ type: "compositioncommit", data: "committed text" });
+ },
+ popupHideFn() {
+ bookmarkPanel.hidePopup();
+ },
+ shouldAutoClose: false,
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* contextmenu_new_bookmark_keypress_no_autoclose() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ *popupShowFn(browser) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu,
+ "popupshown");
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(contextMenu,
+ "popuphidden");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("body", {
+ type: "contextmenu",
+ button: 2
+ }, browser);
+ yield awaitPopupShown;
+ document.getElementById("context-bookmarkpage").click();
+ contextMenu.hidePopup();
+ yield awaitPopupHidden;
+ },
+ popupEditFn() {
+ EventUtils.sendChar("VK_TAB", window);
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ bookmarkPanel.hidePopup();
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(function* bookmarks_menu_new_bookmark_remove_bookmark() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn(browser) {
+ document.getElementById("menu_bookmarkThisPage").doCommand();
+ },
+ shouldAutoClose: true,
+ popupHideFn() {
+ document.getElementById("editBookmarkPanelRemoveButton").click();
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(function* ctrl_d_edit_bookmark_remove_bookmark() {
+ yield test_bookmarks_popup({
+ isNewBookmark: false,
+ popupShowFn(browser) {
+ EventUtils.synthesizeKey("D", {accelKey: true}, window);
+ },
+ shouldAutoClose: true,
+ popupHideFn() {
+ document.getElementById("editBookmarkPanelRemoveButton").click();
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(function* enter_on_remove_bookmark_should_remove_bookmark() {
+ if (AppConstants.platform == "macosx") {
+ // "Full Keyboard Access" is disabled by default, and thus doesn't allow
+ // keyboard navigation to the "Remove Bookmarks" button by default.
+ return;
+ }
+
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn(browser) {
+ EventUtils.synthesizeKey("D", {accelKey: true}, window);
+ },
+ shouldAutoClose: true,
+ popupHideFn() {
+ while (!document.activeElement ||
+ document.activeElement.id != "editBookmarkPanelRemoveButton") {
+ EventUtils.sendChar("VK_TAB", window);
+ }
+ EventUtils.sendChar("VK_RETURN", window);
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(function* ctrl_d_new_bookmark_mousedown_mouseout_no_autoclose() {
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn(browser) {
+ EventUtils.synthesizeKey("D", {accelKey: true}, window);
+ },
+ *popupEditFn() {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "mousemove");
+ EventUtils.synthesizeMouseAtCenter(bookmarkPanel, {type: "mousemove"});
+ info("Waiting for mousemove event");
+ yield mouseMovePromise;
+ info("Got mousemove event");
+
+ yield new Promise(resolve => setTimeout(resolve, 400));
+ is(bookmarkPanel.state, "open", "Panel should still be open on mousemove");
+
+ EventUtils.synthesizeMouseAtCenter(bookmarkPanelTitle, {button: 1, type: "mousedown"});
+
+ let mouseOutPromise = BrowserTestUtils.waitForEvent(bookmarkPanel, "mouseout");
+ EventUtils.synthesizeMouse(bookmarkPanel, 0, 0, {type: "mouseout"});
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+ info("Waiting for mouseout event");
+ yield mouseOutPromise;
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ document.getElementById("editBookmarkPanelRemoveButton").click();
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(function* mouse_hovering_panel_should_prevent_autoclose() {
+ if (AppConstants.platform != "win") {
+ // This test requires synthesizing native mouse movement which is
+ // best supported on Windows.
+ return;
+ }
+ yield test_bookmarks_popup({
+ isNewBookmark: true,
+ *popupShowFn(browser) {
+ yield new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(
+ document.documentElement,
+ editBookmarkPanelRemoveButtonRect.left,
+ editBookmarkPanelRemoveButtonRect.top,
+ resolve);
+ });
+ EventUtils.synthesizeKey("D", {accelKey: true}, window);
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ document.getElementById("editBookmarkPanelRemoveButton").click();
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+registerCleanupFunction(function() {
+ delete StarUI._closePanelQuickForTesting;
+});
diff --git a/browser/base/content/test/general/browser_bookmark_titles.js b/browser/base/content/test/general/browser_bookmark_titles.js
new file mode 100644
index 000000000..1f7082396
--- /dev/null
+++ b/browser/base/content/test/general/browser_bookmark_titles.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 tests for the default titles that new bookmarks get.
+
+var tests = [
+ // Common page.
+ ['http://example.com/browser/browser/base/content/test/general/dummy_page.html',
+ 'Dummy test page'],
+ // Data URI.
+ ['data:text/html;charset=utf-8,<title>test%20data:%20url</title>',
+ 'test data: url'],
+ // about:neterror
+ ['data:application/vnd.mozilla.xul+xml,',
+ 'data:application/vnd.mozilla.xul+xml,'],
+ // about:certerror
+ ['https://untrusted.example.com/somepage.html',
+ 'https://untrusted.example.com/somepage.html']
+];
+
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab();
+ let browser = gBrowser.selectedBrowser;
+ browser.stop(); // stop the about:blank load.
+
+ // Test that a bookmark of each URI gets the corresponding default title.
+ for (let i = 0; i < tests.length; ++i) {
+ let [uri, title] = tests[i];
+
+ let promiseLoaded = promisePageLoaded(browser);
+ BrowserTestUtils.loadURI(browser, uri);
+ yield promiseLoaded;
+ yield checkBookmark(uri, title);
+ }
+
+ // Network failure test: now that dummy_page.html is in history, bookmarking
+ // it should give the last known page title as the default bookmark title.
+
+ // Simulate a network outage with offline mode. (Localhost is still
+ // accessible in offline mode, so disable the test proxy as well.)
+ BrowserOffline.toggleOfflineStatus();
+ let proxy = Services.prefs.getIntPref('network.proxy.type');
+ Services.prefs.setIntPref('network.proxy.type', 0);
+ registerCleanupFunction(function () {
+ BrowserOffline.toggleOfflineStatus();
+ Services.prefs.setIntPref('network.proxy.type', proxy);
+ });
+
+ // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
+ Services.cache2.clear();
+
+ let [uri, title] = tests[0];
+
+ let promiseLoaded = promisePageLoaded(browser);
+ BrowserTestUtils.loadURI(browser, uri);
+ yield promiseLoaded;
+
+ // The offline mode test is only good if the page failed to load.
+ yield ContentTask.spawn(browser, null, function() {
+ is(content.document.documentURI.substring(0, 14), 'about:neterror',
+ "Offline mode successfully simulated network outage.");
+ });
+ yield checkBookmark(uri, title);
+
+ gBrowser.removeCurrentTab();
+});
+
+// Bookmark the current page and confirm that the new bookmark has the expected
+// title. (Then delete the bookmark.)
+function* checkBookmark(uri, expected_title) {
+ is(gBrowser.selectedBrowser.currentURI.spec, uri,
+ "Trying to bookmark the expected uri");
+
+ let promiseBookmark = promiseOnBookmarkItemAdded(gBrowser.selectedBrowser.currentURI);
+ PlacesCommandHook.bookmarkCurrentPage(false);
+ yield promiseBookmark;
+
+ let id = PlacesUtils.getMostRecentBookmarkForURI(PlacesUtils._uri(uri));
+ ok(id > 0, "Found the expected bookmark");
+ let title = PlacesUtils.bookmarks.getItemTitle(id);
+ is(title, expected_title, "Bookmark got a good default title.");
+
+ PlacesUtils.bookmarks.removeItem(id);
+}
+
+// BrowserTestUtils.browserLoaded doesn't work for the about pages, so use a
+// custom page load listener.
+function promisePageLoaded(browser)
+{
+ return ContentTask.spawn(browser, null, function* () {
+ yield ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", true,
+ (event) => {
+ return event.originalTarget === content.document &&
+ event.target.location.href !== "about:blank"
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_bug1015721.js b/browser/base/content/test/general/browser_bug1015721.js
new file mode 100644
index 000000000..e3e715396
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1015721.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/. */
+"use strict";
+
+const TEST_PAGE = "http://example.org/browser/browser/base/content/test/general/zoom_test.html";
+
+var gTab1, gTab2, gLevel1;
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ gTab1 = gBrowser.addTab();
+ gTab2 = gBrowser.addTab();
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ yield FullZoomHelper.load(gTab1, TEST_PAGE);
+ yield FullZoomHelper.load(gTab2, TEST_PAGE);
+ }).then(zoomTab1, FullZoomHelper.failAndContinue(finish));
+}
+
+function zoomTab1() {
+ Task.spawn(function* () {
+ is(gBrowser.selectedTab, gTab1, "Tab 1 is selected");
+ FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1");
+
+ let browser1 = gBrowser.getBrowserForTab(gTab1);
+ yield BrowserTestUtils.synthesizeMouse(null, 10, 10, {
+ wheel: true, ctrlKey: true, deltaY: -1, deltaMode: WheelEvent.DOM_DELTA_LINE
+ }, browser1);
+
+ info("Waiting for tab 1 to be zoomed");
+ yield promiseWaitForCondition(() => {
+ gLevel1 = ZoomManager.getZoomForBrowser(browser1);
+ return gLevel1 > 1;
+ });
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(gTab2, gLevel1, "Tab 2 should have zoomed along with tab 1");
+ }).then(finishTest, FullZoomHelper.failAndContinue(finish));
+}
+
+function finishTest() {
+ Task.spawn(function* () {
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab1);
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab2);
+ }).then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/general/browser_bug1045809.js b/browser/base/content/test/general/browser_bug1045809.js
new file mode 100644
index 000000000..63b6b06d5
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1045809.js
@@ -0,0 +1,68 @@
+// Test that the Mixed Content Doorhanger Action to re-enable protection works
+
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+
+var origBlockActive;
+
+add_task(function* () {
+ registerCleanupFunction(function() {
+ Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive);
+ gBrowser.removeCurrentTab();
+ });
+
+ // Store original preferences so we can restore settings after testing
+ origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE);
+
+ // Make sure mixed content blocking is on
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+
+ var url =
+ "https://test1.example.com/browser/browser/base/content/test/general/" +
+ "file_bug1045809_1.html";
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ // Test 1: mixed content must be blocked
+ yield promiseTabLoadEvent(tab, url);
+ yield* test1(gBrowser.getBrowserForTab(tab));
+
+ yield promiseTabLoadEvent(tab);
+ // Test 2: mixed content must NOT be blocked
+ yield* test2(gBrowser.getBrowserForTab(tab));
+
+ // Test 3: mixed content must be blocked again
+ yield promiseTabLoadEvent(tab);
+ yield* test3(gBrowser.getBrowserForTab(tab));
+});
+
+function* test1(gTestBrowser) {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ var x = content.document.getElementsByTagName("iframe")[0].contentDocument.getElementById("mixedContentContainer");
+ is(x, null, "Mixed Content is NOT to be found in Test1");
+ });
+
+ // Disable Mixed Content Protection for the page (and reload)
+ gIdentityHandler.disableMixedContentProtection();
+}
+
+function* test2(gTestBrowser) {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: true, activeBlocked: false, passiveLoaded: false});
+
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ var x = content.document.getElementsByTagName("iframe")[0].contentDocument.getElementById("mixedContentContainer");
+ isnot(x, null, "Mixed Content is to be found in Test2");
+ });
+
+ // Re-enable Mixed Content Protection for the page (and reload)
+ gIdentityHandler.enableMixedContentProtection();
+}
+
+function* test3(gTestBrowser) {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ var x = content.document.getElementsByTagName("iframe")[0].contentDocument.getElementById("mixedContentContainer");
+ is(x, null, "Mixed Content is NOT to be found in Test3");
+ });
+}
diff --git a/browser/base/content/test/general/browser_bug1064280_changeUrlInPinnedTab.js b/browser/base/content/test/general/browser_bug1064280_changeUrlInPinnedTab.js
new file mode 100644
index 000000000..98e0e74db
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1064280_changeUrlInPinnedTab.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ // Test that changing the URL in a pinned tab works correctly
+
+ let TEST_LINK_INITIAL = "about:";
+ let TEST_LINK_CHANGED = "about:support";
+
+ let appTab = gBrowser.addTab(TEST_LINK_INITIAL);
+ let browser = appTab.linkedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ gBrowser.pinTab(appTab);
+ is(appTab.pinned, true, "Tab was successfully pinned");
+
+ let initialTabsNo = gBrowser.tabs.length;
+
+ let goButton = document.getElementById("urlbar-go-button");
+ gBrowser.selectedTab = appTab;
+ gURLBar.focus();
+ gURLBar.value = TEST_LINK_CHANGED;
+
+ goButton.click();
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ is(appTab.linkedBrowser.currentURI.spec, TEST_LINK_CHANGED,
+ "New page loaded in the app tab");
+ is(gBrowser.tabs.length, initialTabsNo, "No additional tabs were opened");
+});
+
+registerCleanupFunction(function () {
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/general/browser_bug1261299.js b/browser/base/content/test/general/browser_bug1261299.js
new file mode 100644
index 000000000..673ef2a0a
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1261299.js
@@ -0,0 +1,73 @@
+/* -*- 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/. */
+
+/**
+ * Tests for Bug 1261299
+ * Test that the service menu code path is called properly and the
+ * current selection (transferable) is cached properly on the parent process.
+ */
+
+add_task(function* test_content_and_chrome_selection()
+{
+ let testPage =
+ 'data:text/html,' +
+ '<textarea id="textarea">Write something here</textarea>';
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ let selectedText;
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, testPage);
+ yield BrowserTestUtils.synthesizeMouse("#textarea", 0, 0, {}, gBrowser.selectedBrowser);
+ yield BrowserTestUtils.synthesizeKey("KEY_ArrowRight",
+ {shiftKey: true, ctrlKey: true, code: "ArrowRight"}, gBrowser.selectedBrowser);
+ selectedText = DOMWindowUtils.GetSelectionAsPlaintext();
+ is(selectedText, "Write something here", "The macOS services got the selected content text");
+
+ gURLBar.value = "test.mozilla.org";
+ yield gURLBar.focus();
+ yield BrowserTestUtils.synthesizeKey("KEY_ArrowRight",
+ {shiftKey: true, ctrlKey: true, code: "ArrowRight"}, gBrowser.selectedBrowser);
+ selectedText = DOMWindowUtils.GetSelectionAsPlaintext();
+ is(selectedText, "test.mozilla.org", "The macOS services got the selected chrome text");
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+// Test switching active selection.
+// Each tab has a content selection and when you switch to that tab, its selection becomes
+// active aka the current selection.
+// Expect: The active selection is what is being sent to OSX service menu.
+
+add_task(function* test_active_selection_switches_properly()
+{
+ let testPage1 =
+ 'data:text/html,' +
+ '<textarea id="textarea">Write something here</textarea>';
+ let testPage2 =
+ 'data:text/html,' +
+ '<textarea id="textarea">Nothing available</textarea>';
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ let selectedText;
+
+ let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, testPage1);
+ yield BrowserTestUtils.synthesizeMouse("#textarea", 0, 0, {}, gBrowser.selectedBrowser);
+ yield BrowserTestUtils.synthesizeKey("KEY_ArrowRight",
+ {shiftKey: true, ctrlKey: true, code: "ArrowRight"}, gBrowser.selectedBrowser);
+
+ let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, testPage2);
+ yield BrowserTestUtils.synthesizeMouse("#textarea", 0, 0, {}, gBrowser.selectedBrowser);
+ yield BrowserTestUtils.synthesizeKey("KEY_ArrowRight",
+ {shiftKey: true, ctrlKey: true, code: "ArrowRight"}, gBrowser.selectedBrowser);
+
+ yield BrowserTestUtils.switchTab(gBrowser, tab1);
+ selectedText = DOMWindowUtils.GetSelectionAsPlaintext();
+ is(selectedText, "Write something here", "The macOS services got the selected content text");
+
+ yield BrowserTestUtils.switchTab(gBrowser, tab2);
+ selectedText = DOMWindowUtils.GetSelectionAsPlaintext();
+ is(selectedText, "Nothing available", "The macOS services got the selected content text");
+
+ yield BrowserTestUtils.removeTab(tab1);
+ yield BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/base/content/test/general/browser_bug1297539.js b/browser/base/content/test/general/browser_bug1297539.js
new file mode 100644
index 000000000..d7e675437
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1297539.js
@@ -0,0 +1,114 @@
+/* -*- 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/. */
+
+/**
+ * Test for Bug 1297539
+ * Test that the content event "pasteTransferable"
+ * (mozilla::EventMessage::eContentCommandPasteTransferable)
+ * is handled correctly for plain text and html in the remote case.
+ *
+ * Original test test_bug525389.html for command content event
+ * "pasteTransferable" runs only in the content process.
+ * This doesn't test the remote case.
+ *
+ */
+
+"use strict";
+
+function getLoadContext() {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext);
+}
+
+function getTransferableFromClipboard(asHTML) {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ if (asHTML) {
+ trans.addDataFlavor("text/html");
+ } else {
+ trans.addDataFlavor("text/unicode");
+ }
+ let clip = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
+ clip.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
+ return trans;
+}
+
+function* cutCurrentSelection(elementQueryString, property, browser) {
+ // Cut the current selection.
+ yield BrowserTestUtils.synthesizeKey("x", {accelKey: true}, browser);
+
+ // The editor should be empty after cut.
+ yield ContentTask.spawn(browser, [elementQueryString, property],
+ function* ([contentElementQueryString, contentProperty]) {
+ let element = content.document.querySelector(contentElementQueryString);
+ is(element[contentProperty], "",
+ `${contentElementQueryString} should be empty after cut (superkey + x)`);
+ });
+}
+
+// Test that you are able to pasteTransferable for plain text
+// which is handled by TextEditor::PasteTransferable to paste into the editor.
+add_task(function* test_paste_transferable_plain_text()
+{
+ let testPage =
+ 'data:text/html,' +
+ '<textarea id="textarea">Write something here</textarea>';
+
+ yield BrowserTestUtils.withNewTab(testPage, function* (browser) {
+ // Select all the content in your editor element.
+ yield BrowserTestUtils.synthesizeMouse("#textarea", 0, 0, {}, browser);
+ yield BrowserTestUtils.synthesizeKey("a", {accelKey: true}, browser);
+
+ yield* cutCurrentSelection("#textarea", "value", browser);
+
+ let trans = getTransferableFromClipboard(false);
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ DOMWindowUtils.sendContentCommandEvent("pasteTransferable", trans);
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let textArea = content.document.querySelector('#textarea');
+ is(textArea.value, "Write something here",
+ "Send content command pasteTransferable successful");
+ });
+ });
+});
+
+// Test that you are able to pasteTransferable for html
+// which is handled by HTMLEditor::PasteTransferable to paste into the editor.
+//
+// On Linux,
+// BrowserTestUtils.synthesizeKey("a", {accelKey: true}, browser);
+// doesn't seem to trigger for contenteditable which is why we use
+// Selection to select the contenteditable contents.
+add_task(function* test_paste_transferable_html()
+{
+ let testPage =
+ 'data:text/html,' +
+ '<div contenteditable="true"><b>Bold Text</b><i>italics</i></div>';
+
+ yield BrowserTestUtils.withNewTab(testPage, function* (browser) {
+ // Select all the content in your editor element.
+ yield BrowserTestUtils.synthesizeMouse("div", 0, 0, {}, browser);
+ yield ContentTask.spawn(browser, {}, function* () {
+ let element = content.document.querySelector("div");
+ let selection = content.window.getSelection();
+ selection.selectAllChildren(element);
+ });
+
+ yield* cutCurrentSelection("div", "textContent", browser);
+
+ let trans = getTransferableFromClipboard(true);
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils(window);
+ DOMWindowUtils.sendContentCommandEvent("pasteTransferable", trans);
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let textArea = content.document.querySelector('div');
+ is(textArea.innerHTML, "<b>Bold Text</b><i>italics</i>",
+ "Send content command pasteTransferable successful");
+ });
+ });
+});
diff --git a/browser/base/content/test/general/browser_bug1299667.js b/browser/base/content/test/general/browser_bug1299667.js
new file mode 100644
index 000000000..084c8d49f
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1299667.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { addObserver, removeObserver } = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+
+function receive(topic) {
+ return new Promise((resolve, reject) => {
+ let timeout = setTimeout(() => {
+ reject(new Error("Timeout"));
+ }, 90000);
+
+ const observer = {
+ observe: subject => {
+ removeObserver(observer, topic);
+ clearTimeout(timeout);
+ resolve(subject);
+ }
+ };
+ addObserver(observer, topic, false);
+ });
+}
+
+add_task(function* () {
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.history.pushState({}, "2", "2.html");
+ });
+
+ yield receive("sessionstore-state-write-complete");
+
+ // Wait for the session data to be flushed before continuing the test
+ yield new Promise(resolve => SessionStore.getSessionHistory(gBrowser.selectedTab, resolve));
+
+ let backButton = document.getElementById("back-button");
+ let contextMenu = document.getElementById("backForwardMenu");
+
+ info("waiting for the history menu to open");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(backButton, {type: "contextmenu", button: 2});
+ let event = yield popupShownPromise;
+
+ ok(true, "history menu opened");
+
+ // Wait for the session data to be flushed before continuing the test
+ yield new Promise(resolve => SessionStore.getSessionHistory(gBrowser.selectedTab, resolve));
+
+ is(event.target.children.length, 2, "Two history items");
+
+ let node = event.target.firstChild;
+ is(node.getAttribute("uri"), "http://example.com/2.html", "first item uri");
+ is(node.getAttribute("index"), "1", "first item index");
+ is(node.getAttribute("historyindex"), "0", "first item historyindex");
+
+ node = event.target.lastChild;
+ is(node.getAttribute("uri"), "http://example.com/", "second item uri");
+ is(node.getAttribute("index"), "0", "second item index");
+ is(node.getAttribute("historyindex"), "-1", "second item historyindex");
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ event.target.hidePopup();
+ yield popupHiddenPromise;
+ info("Hidden popup");
+
+ let onClose = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabClose");
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ yield onClose;
+ info("Tab closed");
+});
diff --git a/browser/base/content/test/general/browser_bug321000.js b/browser/base/content/test/general/browser_bug321000.js
new file mode 100644
index 000000000..b30b7101d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug321000.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 kTestString = " hello hello \n world\nworld ";
+
+var gTests = [
+
+ { desc: "Urlbar strips newlines and surrounding whitespace",
+ element: gURLBar,
+ expected: kTestString.replace(/\s*\n\s*/g, '')
+ },
+
+ { desc: "Searchbar replaces newlines with spaces",
+ element: document.getElementById('searchbar'),
+ expected: kTestString.replace(/\n/g, ' ')
+ },
+
+];
+
+// Test for bug 23485 and bug 321000.
+// Urlbar should strip newlines,
+// search bar should replace newlines with spaces.
+function test() {
+ waitForExplicitFinish();
+
+ let cbHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+
+ // Put a multi-line string in the clipboard.
+ // Setting the clipboard value is an async OS operation, so we need to poll
+ // the clipboard for valid data before going on.
+ waitForClipboard(kTestString, function() { cbHelper.copyString(kTestString); },
+ next_test, finish);
+}
+
+function next_test() {
+ if (gTests.length)
+ test_paste(gTests.shift());
+ else
+ finish();
+}
+
+function test_paste(aCurrentTest) {
+ var element = aCurrentTest.element;
+
+ // Register input listener.
+ var inputListener = {
+ test: aCurrentTest,
+ handleEvent: function(event) {
+ element.removeEventListener(event.type, this, false);
+
+ is(element.value, this.test.expected, this.test.desc);
+
+ // Clear the field and go to next test.
+ element.value = "";
+ setTimeout(next_test, 0);
+ }
+ }
+ element.addEventListener("input", inputListener, false);
+
+ // Focus the window.
+ window.focus();
+ gBrowser.selectedBrowser.focus();
+
+ // Focus the element and wait for focus event.
+ info("About to focus " + element.id);
+ element.addEventListener("focus", function() {
+ element.removeEventListener("focus", arguments.callee, false);
+ executeSoon(function() {
+ // Pasting is async because the Accel+V codepath ends up going through
+ // nsDocumentViewer::FireClipboardEvent.
+ info("Pasting into " + element.id);
+ EventUtils.synthesizeKey("v", { accelKey: true });
+ });
+ }, false);
+ element.focus();
+}
diff --git a/browser/base/content/test/general/browser_bug356571.js b/browser/base/content/test/general/browser_bug356571.js
new file mode 100644
index 000000000..ab689d0f8
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug356571.js
@@ -0,0 +1,93 @@
+// Bug 356571 - loadOneOrMoreURIs gives up if one of the URLs has an unknown protocol
+
+var Cr = Components.results;
+var Cm = Components.manager;
+
+// Set to true when docShell alerts for unknown protocol error
+var didFail = false;
+
+// Override Alert to avoid blocking the test due to unknown protocol error
+const kPromptServiceUUID = "{6cc9c9fe-bc0b-432b-a410-253ef8bcc699}";
+const kPromptServiceContractID = "@mozilla.org/embedcomp/prompt-service;1";
+
+// Save original prompt service factory
+const kPromptServiceFactory = Cm.getClassObject(Cc[kPromptServiceContractID],
+ Ci.nsIFactory);
+
+var fakePromptServiceFactory = {
+ createInstance: function(aOuter, aIid) {
+ if (aOuter != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return promptService.QueryInterface(aIid);
+ }
+};
+
+var promptService = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]),
+ alert: function() {
+ didFail = true;
+ }
+};
+
+/* FIXME
+Cm.QueryInterface(Ci.nsIComponentRegistrar)
+ .registerFactory(Components.ID(kPromptServiceUUID), "Prompt Service",
+ kPromptServiceContractID, fakePromptServiceFactory);
+*/
+
+const kCompleteState = Ci.nsIWebProgressListener.STATE_STOP +
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+
+const kDummyPage = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+const kURIs = [
+ "bad://www.mozilla.org/",
+ kDummyPage,
+ kDummyPage,
+];
+
+var gProgressListener = {
+ _runCount: 0,
+ onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if ((aStateFlags & kCompleteState) == kCompleteState) {
+ if (++this._runCount != kURIs.length)
+ return;
+ // Check we failed on unknown protocol (received an alert from docShell)
+ ok(didFail, "Correctly failed on unknown protocol");
+ // Check we opened all tabs
+ ok(gBrowser.tabs.length == kURIs.length, "Correctly opened all expected tabs");
+ finishTest();
+ }
+ }
+}
+
+function test() {
+ todo(false, "temp. disabled");
+ return; /* FIXME */
+ /*
+ waitForExplicitFinish();
+ // Wait for all tabs to finish loading
+ gBrowser.addTabsProgressListener(gProgressListener);
+ loadOneOrMoreURIs(kURIs.join("|"));
+ */
+}
+
+function finishTest() {
+ // Unregister the factory so we do not leak
+ Cm.QueryInterface(Ci.nsIComponentRegistrar)
+ .unregisterFactory(Components.ID(kPromptServiceUUID),
+ fakePromptServiceFactory);
+
+ // Restore the original factory
+ Cm.QueryInterface(Ci.nsIComponentRegistrar)
+ .registerFactory(Components.ID(kPromptServiceUUID), "Prompt Service",
+ kPromptServiceContractID, kPromptServiceFactory);
+
+ // Remove the listener
+ gBrowser.removeTabsProgressListener(gProgressListener);
+
+ // Close opened tabs
+ for (var i = gBrowser.tabs.length-1; i > 0; i--)
+ gBrowser.removeTab(gBrowser.tabs[i]);
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug380960.js b/browser/base/content/test/general/browser_bug380960.js
new file mode 100644
index 000000000..d6b64543b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug380960.js
@@ -0,0 +1,11 @@
+function test() {
+ var tab = gBrowser.addTab("about:blank", { skipAnimation: true });
+ gBrowser.removeTab(tab);
+ is(tab.parentNode, null, "tab removed immediately");
+
+ tab = gBrowser.addTab("about:blank", { skipAnimation: true });
+ gBrowser.removeTab(tab, { animate: true });
+ gBrowser.removeTab(tab);
+ is(tab.parentNode, null, "tab removed immediately when calling removeTab again after the animation was kicked off");
+}
+
diff --git a/browser/base/content/test/general/browser_bug386835.js b/browser/base/content/test/general/browser_bug386835.js
new file mode 100644
index 000000000..1c3ba99c5
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug386835.js
@@ -0,0 +1,89 @@
+var gTestPage = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+var gTestImage = "http://example.org/browser/browser/base/content/test/general/moz.png";
+var gTab1, gTab2, gTab3;
+var gLevel;
+const BACK = 0;
+const FORWARD = 1;
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ gTab1 = gBrowser.addTab(gTestPage);
+ gTab2 = gBrowser.addTab();
+ gTab3 = gBrowser.addTab();
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ yield FullZoomHelper.load(gTab1, gTestPage);
+ yield FullZoomHelper.load(gTab2, gTestPage);
+ }).then(secondPageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function secondPageLoaded() {
+ Task.spawn(function* () {
+ FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1");
+ FullZoomHelper.zoomTest(gTab3, 1, "Initial zoom of tab 3 should be 1");
+
+ // Now have three tabs, two with the test page, one blank. Tab 1 is selected
+ // Zoom tab 1
+ FullZoom.enlarge();
+ gLevel = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1));
+
+ ok(gLevel > 1, "New zoom for tab 1 should be greater than 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2");
+ FullZoomHelper.zoomTest(gTab3, 1, "Zooming tab 1 should not affect tab 3");
+
+ yield FullZoomHelper.load(gTab3, gTestPage);
+ }).then(thirdPageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function thirdPageLoaded() {
+ Task.spawn(function* () {
+ FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed");
+ FullZoomHelper.zoomTest(gTab2, 1, "Tab 2 should still not be affected");
+ FullZoomHelper.zoomTest(gTab3, gLevel, "Tab 3 should have zoomed as it was loading in the background");
+
+ // Switching to tab 2 should update its zoom setting.
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed");
+ FullZoomHelper.zoomTest(gTab2, gLevel, "Tab 2 should be zoomed now");
+ FullZoomHelper.zoomTest(gTab3, gLevel, "Tab 3 should still be zoomed");
+
+ yield FullZoomHelper.load(gTab1, gTestImage);
+ }).then(imageLoaded, FullZoomHelper.failAndContinue(finish));
+}
+
+function imageLoaded() {
+ Task.spawn(function* () {
+ FullZoomHelper.zoomTest(gTab1, 1, "Zoom should be 1 when image was loaded in the background");
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ FullZoomHelper.zoomTest(gTab1, 1, "Zoom should still be 1 when tab with image is selected");
+ }).then(imageZoomSwitch, FullZoomHelper.failAndContinue(finish));
+}
+
+function imageZoomSwitch() {
+ Task.spawn(function* () {
+ yield FullZoomHelper.navigate(BACK);
+ yield FullZoomHelper.navigate(FORWARD);
+ FullZoomHelper.zoomTest(gTab1, 1, "Tab 1 should not be zoomed when an image loads");
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(gTab1, 1, "Tab 1 should still not be zoomed when deselected");
+ }).then(finishTest, FullZoomHelper.failAndContinue(finish));
+}
+
+var finishTestStarted = false;
+function finishTest() {
+ Task.spawn(function* () {
+ ok(!finishTestStarted, "finishTest called more than once");
+ finishTestStarted = true;
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab1);
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab2);
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab3);
+ }).then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/general/browser_bug406216.js b/browser/base/content/test/general/browser_bug406216.js
new file mode 100644
index 000000000..e1bd38395
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug406216.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/. */
+
+/*
+ * "TabClose" event is possibly used for closing related tabs of the current.
+ * "removeTab" method should work correctly even if the number of tabs are
+ * changed while "TabClose" event.
+ */
+
+var count = 0;
+const URIS = ["about:config",
+ "about:plugins",
+ "about:buildconfig",
+ "data:text/html,<title>OK</title>"];
+
+function test() {
+ waitForExplicitFinish();
+ URIS.forEach(addTab);
+}
+
+function addTab(aURI, aIndex) {
+ var tab = gBrowser.addTab(aURI);
+ if (aIndex == 0)
+ gBrowser.removeTab(gBrowser.tabs[0], {skipPermitUnload: true});
+
+ tab.linkedBrowser.addEventListener("load", function (event) {
+ event.currentTarget.removeEventListener("load", arguments.callee, true);
+ if (++count == URIS.length)
+ executeSoon(doTabsTest);
+ }, true);
+}
+
+function doTabsTest() {
+ is(gBrowser.tabs.length, URIS.length, "Correctly opened all expected tabs");
+
+ // sample of "close related tabs" feature
+ gBrowser.tabContainer.addEventListener("TabClose", function (event) {
+ event.currentTarget.removeEventListener("TabClose", arguments.callee, true);
+ var closedTab = event.originalTarget;
+ var scheme = closedTab.linkedBrowser.currentURI.scheme;
+ Array.slice(gBrowser.tabs).forEach(function (aTab) {
+ if (aTab != closedTab && aTab.linkedBrowser.currentURI.scheme == scheme)
+ gBrowser.removeTab(aTab, {skipPermitUnload: true});
+ });
+ }, true);
+
+ gBrowser.removeTab(gBrowser.tabs[0], {skipPermitUnload: true});
+ is(gBrowser.tabs.length, 1, "Related tabs are not closed unexpectedly");
+
+ gBrowser.addTab("about:blank");
+ gBrowser.removeTab(gBrowser.tabs[0], {skipPermitUnload: true});
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug408415.js b/browser/base/content/test/general/browser_bug408415.js
new file mode 100644
index 000000000..d8f80f8be
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug408415.js
@@ -0,0 +1,45 @@
+add_task(function* test() {
+ let testPath = getRootDirectory(gTestPath);
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" },
+ function* (tabBrowser) {
+ const URI = testPath + "file_with_favicon.html";
+ const expectedIcon = testPath + "file_generic_favicon.ico";
+
+ let got_favicon = Promise.defer();
+ let listener = {
+ onLinkIconAvailable(browser, iconURI) {
+ if (got_favicon && iconURI && browser === tabBrowser) {
+ got_favicon.resolve(iconURI);
+ got_favicon = null;
+ }
+ }
+ };
+ gBrowser.addTabsProgressListener(listener);
+
+ BrowserTestUtils.loadURI(tabBrowser, URI);
+
+ let iconURI = yield got_favicon.promise;
+ is(iconURI, expectedIcon, "Correct icon before pushState.");
+
+ got_favicon = Promise.defer();
+ got_favicon.promise.then(() => { ok(false, "shouldn't be called"); }, (e) => e);
+ yield ContentTask.spawn(tabBrowser, null, function() {
+ content.location.href += "#foo";
+ });
+
+ // We've navigated and shouldn't get a call to onLinkIconAvailable.
+ TestUtils.executeSoon(() => {
+ got_favicon.reject(gBrowser.getIcon(gBrowser.getTabForBrowser(tabBrowser)));
+ });
+ try {
+ yield got_favicon.promise;
+ } catch (e) {
+ iconURI = e;
+ }
+ is(iconURI, expectedIcon, "Correct icon after pushState.");
+
+ gBrowser.removeTabsProgressListener(listener);
+ });
+});
+
diff --git a/browser/base/content/test/general/browser_bug409481.js b/browser/base/content/test/general/browser_bug409481.js
new file mode 100644
index 000000000..395ad93d4
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug409481.js
@@ -0,0 +1,83 @@
+function test() {
+ waitForExplicitFinish();
+
+ // XXX This looks a bit odd, but is needed to avoid throwing when removing the
+ // event listeners below. See bug 310955.
+ document.getElementById("sidebar").addEventListener("load", delayedOpenUrl, true);
+ SidebarUI.show("viewWebPanelsSidebar");
+}
+
+function delayedOpenUrl() {
+ ok(true, "Ran delayedOpenUrl");
+ setTimeout(openPanelUrl, 100);
+}
+
+function openPanelUrl(event) {
+ ok(!document.getElementById("sidebar-box").hidden, "Sidebar showing");
+
+ var sidebar = document.getElementById("sidebar");
+ var root = sidebar.contentDocument.documentElement;
+ ok(root.nodeName != "parsererror", "Sidebar is well formed");
+
+ sidebar.removeEventListener("load", delayedOpenUrl, true);
+ // XXX See comment above
+ sidebar.contentDocument.addEventListener("load", delayedRunTest, true);
+ var url = 'data:text/html,<div%20id="test_bug409481">Content!</div><a id="link" href="http://www.example.com/ctest">Link</a><input id="textbox">';
+ sidebar.contentWindow.loadWebPanel(url);
+}
+
+function delayedRunTest() {
+ ok(true, "Ran delayedRunTest");
+ setTimeout(runTest, 100);
+}
+
+function runTest(event) {
+ var sidebar = document.getElementById("sidebar");
+ sidebar.contentDocument.removeEventListener("load", delayedRunTest, true);
+
+ var browser = sidebar.contentDocument.getElementById("web-panels-browser");
+ var div = browser && browser.contentDocument.getElementById("test_bug409481");
+ ok(div && div.textContent == "Content!", "Sidebar content loaded");
+
+ var link = browser && browser.contentDocument.getElementById("link");
+ sidebar.contentDocument.addEventListener("popupshown", contextMenuOpened, false);
+
+ EventUtils.synthesizeMouseAtCenter(link, { type: "contextmenu", button: 2 }, browser.contentWindow);
+}
+
+function contextMenuOpened()
+{
+ var sidebar = document.getElementById("sidebar");
+ sidebar.contentDocument.removeEventListener("popupshown", contextMenuOpened, false);
+
+ var copyLinkCommand = sidebar.contentDocument.getElementById("context-copylink");
+ copyLinkCommand.addEventListener("command", copyLinkCommandExecuted, false);
+ copyLinkCommand.doCommand();
+}
+
+function copyLinkCommandExecuted(event)
+{
+ event.target.removeEventListener("command", copyLinkCommandExecuted, false);
+
+ var sidebar = document.getElementById("sidebar");
+ var browser = sidebar.contentDocument.getElementById("web-panels-browser");
+ var textbox = browser && browser.contentDocument.getElementById("textbox");
+ textbox.focus();
+ document.commandDispatcher.getControllerForCommand("cmd_paste").doCommand("cmd_paste");
+ is(textbox.value, "http://www.example.com/ctest", "copy link command");
+
+ sidebar.contentDocument.addEventListener("popuphidden", contextMenuClosed, false);
+ event.target.parentNode.hidePopup();
+}
+
+function contextMenuClosed()
+{
+ var sidebar = document.getElementById("sidebar");
+ sidebar.contentDocument.removeEventListener("popuphidden", contextMenuClosed, false);
+
+ SidebarUI.hide();
+
+ ok(document.getElementById("sidebar-box").hidden, "Sidebar successfully hidden");
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug409624.js b/browser/base/content/test/general/browser_bug409624.js
new file mode 100644
index 000000000..8e46ec0c2
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug409624.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+
+add_task(function* test() {
+ // This test relies on the form history being empty to start with delete
+ // all the items first.
+ yield new Promise((resolve, reject) => {
+ FormHistory.update({ op: "remove" },
+ { handleError(error) {
+ reject(error);
+ },
+ handleCompletion(reason) {
+ if (!reason) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ });
+ });
+
+ let prefService = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService);
+
+ let tempScope = {};
+ Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+ let Sanitizer = tempScope.Sanitizer;
+ let s = new Sanitizer();
+ s.prefDomain = "privacy.cpd.";
+ let prefBranch = prefService.getBranch(s.prefDomain);
+
+ prefBranch.setBoolPref("cache", false);
+ prefBranch.setBoolPref("cookies", false);
+ prefBranch.setBoolPref("downloads", false);
+ prefBranch.setBoolPref("formdata", true);
+ prefBranch.setBoolPref("history", false);
+ prefBranch.setBoolPref("offlineApps", false);
+ prefBranch.setBoolPref("passwords", false);
+ prefBranch.setBoolPref("sessions", false);
+ prefBranch.setBoolPref("siteSettings", false);
+
+ // Sanitize now so we can test the baseline point.
+ yield s.sanitize();
+ ok(!gFindBar.hasTransactions, "pre-test baseline for sanitizer");
+
+ gFindBar.getElement("findbar-textbox").value = "m";
+ ok(gFindBar.hasTransactions, "formdata can be cleared after input");
+
+ yield s.sanitize();
+ is(gFindBar.getElement("findbar-textbox").value, "", "findBar textbox should be empty after sanitize");
+ ok(!gFindBar.hasTransactions, "No transactions after sanitize");
+});
diff --git a/browser/base/content/test/general/browser_bug413915.js b/browser/base/content/test/general/browser_bug413915.js
new file mode 100644
index 000000000..86c94c427
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug413915.js
@@ -0,0 +1,62 @@
+XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
+ "resource:///modules/Feeds.jsm");
+
+function test() {
+ var exampleUri = makeURI("http://example.com/");
+ var secman = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager);
+ var principal = secman.createCodebasePrincipal(exampleUri, {});
+
+ function testIsFeed(aTitle, aHref, aType, aKnown) {
+ var link = { title: aTitle, href: aHref, type: aType };
+ return Feeds.isValidFeed(link, principal, aKnown);
+ }
+
+ var href = "http://example.com/feed/";
+ var atomType = "application/atom+xml";
+ var funkyAtomType = " aPPLICAtion/Atom+XML ";
+ var rssType = "application/rss+xml";
+ var funkyRssType = " Application/RSS+XML ";
+ var rdfType = "application/rdf+xml";
+ var texmlType = "text/xml";
+ var appxmlType = "application/xml";
+ var noRss = "Foo";
+ var rss = "RSS";
+
+ // things that should be valid
+ ok(testIsFeed(noRss, href, atomType, false) == atomType,
+ "detect Atom feed");
+ ok(testIsFeed(noRss, href, funkyAtomType, false) == atomType,
+ "clean up and detect Atom feed");
+ ok(testIsFeed(noRss, href, rssType, false) == rssType,
+ "detect RSS feed");
+ ok(testIsFeed(noRss, href, funkyRssType, false) == rssType,
+ "clean up and detect RSS feed");
+
+ // things that should not be feeds
+ ok(testIsFeed(noRss, href, rdfType, false) == null,
+ "should not detect RDF non-feed");
+ ok(testIsFeed(rss, href, rdfType, false) == null,
+ "should not detect RDF feed from type and title");
+ ok(testIsFeed(noRss, href, texmlType, false) == null,
+ "should not detect text/xml non-feed");
+ ok(testIsFeed(rss, href, texmlType, false) == null,
+ "should not detect text/xml feed from type and title");
+ ok(testIsFeed(noRss, href, appxmlType, false) == null,
+ "should not detect application/xml non-feed");
+ ok(testIsFeed(rss, href, appxmlType, false) == null,
+ "should not detect application/xml feed from type and title");
+
+ // security check only, returns cleaned up type or "application/rss+xml"
+ ok(testIsFeed(noRss, href, atomType, true) == atomType,
+ "feed security check should return Atom type");
+ ok(testIsFeed(noRss, href, funkyAtomType, true) == atomType,
+ "feed security check should return cleaned up Atom type");
+ ok(testIsFeed(noRss, href, rssType, true) == rssType,
+ "feed security check should return RSS type");
+ ok(testIsFeed(noRss, href, funkyRssType, true) == rssType,
+ "feed security check should return cleaned up RSS type");
+ ok(testIsFeed(noRss, href, "", true) == rssType,
+ "feed security check without type should return RSS type");
+ ok(testIsFeed(noRss, href, "garbage", true) == "garbage",
+ "feed security check with garbage type should return garbage");
+}
diff --git a/browser/base/content/test/general/browser_bug416661.js b/browser/base/content/test/general/browser_bug416661.js
new file mode 100644
index 000000000..a37971a34
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug416661.js
@@ -0,0 +1,43 @@
+var tabElm, zoomLevel;
+function start_test_prefNotSet() {
+ Task.spawn(function* () {
+ is(ZoomManager.zoom, 1, "initial zoom level should be 1");
+ FullZoom.enlarge();
+
+ // capture the zoom level to test later
+ zoomLevel = ZoomManager.zoom;
+ isnot(zoomLevel, 1, "zoom level should have changed");
+
+ yield FullZoomHelper.load(gBrowser.selectedTab, "http://mochi.test:8888/browser/browser/base/content/test/general/moz.png");
+ }).then(continue_test_prefNotSet, FullZoomHelper.failAndContinue(finish));
+}
+
+function continue_test_prefNotSet () {
+ Task.spawn(function* () {
+ is(ZoomManager.zoom, 1, "zoom level pref should not apply to an image");
+ yield FullZoom.reset();
+
+ yield FullZoomHelper.load(gBrowser.selectedTab, "http://mochi.test:8888/browser/browser/base/content/test/general/zoom_test.html");
+ }).then(end_test_prefNotSet, FullZoomHelper.failAndContinue(finish));
+}
+
+function end_test_prefNotSet() {
+ Task.spawn(function* () {
+ is(ZoomManager.zoom, zoomLevel, "the zoom level should have persisted");
+
+ // Reset the zoom so that other tests have a fresh zoom level
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange();
+ finish();
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ tabElm = gBrowser.addTab();
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(tabElm);
+ yield FullZoomHelper.load(tabElm, "http://mochi.test:8888/browser/browser/base/content/test/general/zoom_test.html");
+ }).then(start_test_prefNotSet, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/general/browser_bug417483.js b/browser/base/content/test/general/browser_bug417483.js
new file mode 100644
index 000000000..43ff7b917
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug417483.js
@@ -0,0 +1,30 @@
+add_task(function* () {
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, true);
+ const htmlContent = "data:text/html, <iframe src='data:text/html,text text'></iframe>";
+ gBrowser.loadURI(htmlContent);
+ yield loadedPromise;
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { }, function* (arg) {
+ let frame = content.frames[0];
+ let sel = frame.getSelection();
+ let range = frame.document.createRange();
+ let tn = frame.document.body.childNodes[0];
+ range.setStart(tn, 4);
+ range.setEnd(tn, 5);
+ sel.addRange(range);
+ frame.focus();
+ });
+
+ let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouse("frame", 5, 5,
+ { type: "contextmenu", button: 2}, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ ok(document.getElementById("frame-sep").hidden, "'frame-sep' should be hidden if the selection contains only spaces");
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
+ contentAreaContextMenu.hidePopup();
+ yield popupHiddenPromise;
+});
diff --git a/browser/base/content/test/general/browser_bug419612.js b/browser/base/content/test/general/browser_bug419612.js
new file mode 100644
index 000000000..8c34b2d39
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug419612.js
@@ -0,0 +1,32 @@
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ let testPage = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+ let tab1 = gBrowser.addTab();
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ yield FullZoomHelper.load(tab1, testPage);
+
+ let tab2 = gBrowser.addTab();
+ yield FullZoomHelper.load(tab2, testPage);
+
+ FullZoom.enlarge();
+ let tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser);
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ let tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser);
+ is(tab2Zoom, tab1Zoom, "Zoom should affect background tabs");
+
+ gPrefService.setBoolPref("browser.zoom.updateBackgroundTabs", false);
+ yield FullZoom.reset();
+ gBrowser.selectedTab = tab1;
+ tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser);
+ tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser);
+ isnot(tab1Zoom, tab2Zoom, "Zoom should not affect background tabs");
+
+ if (gPrefService.prefHasUserValue("browser.zoom.updateBackgroundTabs"))
+ gPrefService.clearUserPref("browser.zoom.updateBackgroundTabs");
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(tab1);
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(tab2);
+ }).then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/general/browser_bug422590.js b/browser/base/content/test/general/browser_bug422590.js
new file mode 100644
index 000000000..f26919cc5
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug422590.js
@@ -0,0 +1,50 @@
+function test() {
+ waitForExplicitFinish();
+ // test the main (normal) browser window
+ testCustomize(window, testChromeless);
+}
+
+function testChromeless() {
+ // test a chromeless window
+ var newWin = openDialog(getBrowserURL(), "_blank",
+ "chrome,dialog=no,location=yes,toolbar=no", "about:blank");
+ ok(newWin, "got new window");
+
+ whenDelayedStartupFinished(newWin, function () {
+ // Check that the search bar is hidden
+ var searchBar = newWin.BrowserSearch.searchBar;
+ ok(searchBar, "got search bar");
+
+ var searchBarBO = searchBar.boxObject;
+ is(searchBarBO.width, 0, "search bar hidden");
+ is(searchBarBO.height, 0, "search bar hidden");
+
+ testCustomize(newWin, function () {
+ newWin.close();
+ finish();
+ });
+ });
+}
+
+function testCustomize(aWindow, aCallback) {
+ var fileMenu = aWindow.document.getElementById("file-menu");
+ ok(fileMenu, "got file menu");
+ is(fileMenu.disabled, false, "file menu initially enabled");
+
+ openToolbarCustomizationUI(function () {
+ // Can't use the property, since the binding may have since been removed
+ // if the element is hidden (see bug 422590)
+ is(fileMenu.getAttribute("disabled"), "true",
+ "file menu is disabled during toolbar customization");
+
+ closeToolbarCustomizationUI(onClose, aWindow);
+ }, aWindow);
+
+ function onClose() {
+ is(fileMenu.getAttribute("disabled"), "false",
+ "file menu is enabled after toolbar customization");
+
+ if (aCallback)
+ aCallback();
+ }
+}
diff --git a/browser/base/content/test/general/browser_bug423833.js b/browser/base/content/test/general/browser_bug423833.js
new file mode 100644
index 000000000..d4069338b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug423833.js
@@ -0,0 +1,138 @@
+/* Tests for proper behaviour of "Show this frame" context menu options */
+
+// Two frames, one with text content, the other an error page
+var invalidPage = 'http://127.0.0.1:55555/';
+var validPage = 'http://example.com/';
+var testPage = 'data:text/html,<frameset cols="400,400"><frame src="' + validPage + '"><frame src="' + invalidPage + '"></frameset>';
+
+// Store the tab and window created in tests 2 and 3 respectively
+var test2tab;
+var test3window;
+
+// We use setInterval instead of setTimeout to avoid race conditions on error doc loads
+var intervalID;
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", test1Setup, true);
+ content.location = testPage;
+}
+
+function test1Setup() {
+ if (content.frames.length < 2 ||
+ content.frames[1].location != invalidPage)
+ // The error frame hasn't loaded yet
+ return;
+
+ gBrowser.selectedBrowser.removeEventListener("load", test1Setup, true);
+
+ var badFrame = content.frames[1];
+ document.popupNode = badFrame.document.firstChild;
+
+ var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+ var contextMenu = new nsContextMenu(contentAreaContextMenu);
+
+ // We'd like to use another load listener here, but error pages don't fire load events
+ contextMenu.showOnlyThisFrame();
+ intervalID = setInterval(testShowOnlyThisFrame, 3000);
+}
+
+function testShowOnlyThisFrame() {
+ if (content.location.href == testPage)
+ // This is a stale event from the original page loading
+ return;
+
+ // We should now have loaded the error page frame content directly
+ // in the tab, make sure the URL is right.
+ clearInterval(intervalID);
+
+ is(content.location.href, invalidPage, "Should navigate to page url, not about:neterror");
+
+ // Go back to the frames page
+ gBrowser.addEventListener("load", test2Setup, true);
+ content.location = testPage;
+}
+
+function test2Setup() {
+ if (content.frames.length < 2 ||
+ content.frames[1].location != invalidPage)
+ // The error frame hasn't loaded yet
+ return;
+
+ gBrowser.removeEventListener("load", test2Setup, true);
+
+ // Now let's do the whole thing again, but this time for "Open frame in new tab"
+ var badFrame = content.frames[1];
+
+ document.popupNode = badFrame.document.firstChild;
+
+ var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+ var contextMenu = new nsContextMenu(contentAreaContextMenu);
+
+ gBrowser.tabContainer.addEventListener("TabOpen", function (event) {
+ test2tab = event.target;
+ gBrowser.tabContainer.removeEventListener("TabOpen", arguments.callee, false);
+ }, false);
+ contextMenu.openFrameInTab();
+ ok(test2tab, "openFrameInTab() opened a tab");
+
+ gBrowser.selectedTab = test2tab;
+
+ intervalID = setInterval(testOpenFrameInTab, 3000);
+}
+
+function testOpenFrameInTab() {
+ if (gBrowser.contentDocument.location.href == "about:blank")
+ // Wait another cycle
+ return;
+
+ clearInterval(intervalID);
+
+ // We should now have the error page in a new, active tab.
+ is(gBrowser.contentDocument.location.href, invalidPage, "New tab should have page url, not about:neterror");
+
+ // Clear up the new tab, and punt to test 3
+ gBrowser.removeCurrentTab();
+
+ test3Setup();
+}
+
+function test3Setup() {
+ // One more time, for "Open frame in new window"
+ var badFrame = content.frames[1];
+ document.popupNode = badFrame.document.firstChild;
+
+ var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+ var contextMenu = new nsContextMenu(contentAreaContextMenu);
+
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened")
+ test3window = aSubject;
+ Services.ww.unregisterNotification(arguments.callee);
+ });
+
+ contextMenu.openFrame();
+
+ intervalID = setInterval(testOpenFrame, 3000);
+}
+
+function testOpenFrame() {
+ if (!test3window || test3window.content.location.href == "about:blank") {
+ info("testOpenFrame: Wait another cycle");
+ return;
+ }
+
+ clearInterval(intervalID);
+
+ is(test3window.content.location.href, invalidPage, "New window should have page url, not about:neterror");
+
+ test3window.close();
+ cleanup();
+}
+
+function cleanup() {
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug424101.js b/browser/base/content/test/general/browser_bug424101.js
new file mode 100644
index 000000000..8000d2ae9
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug424101.js
@@ -0,0 +1,52 @@
+/* Make sure that the context menu appears on form elements */
+
+add_task(function *() {
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "data:text/html,test");
+
+ let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+
+ let tests = [
+ { element: "input", type: "text" },
+ { element: "input", type: "password" },
+ { element: "input", type: "image" },
+ { element: "input", type: "button" },
+ { element: "input", type: "submit" },
+ { element: "input", type: "reset" },
+ { element: "input", type: "checkbox" },
+ { element: "input", type: "radio" },
+ { element: "button" },
+ { element: "select" },
+ { element: "option" },
+ { element: "optgroup" }
+ ];
+
+ for (let index = 0; index < tests.length; index++) {
+ let test = tests[index];
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser,
+ { element: test.element, type: test.type, index: index },
+ function* (arg) {
+ let element = content.document.createElement(arg.element);
+ element.id = "element" + arg.index;
+ if (arg.type) {
+ element.setAttribute("type", arg.type);
+ }
+ content.document.body.appendChild(element);
+ });
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#element" + index,
+ { type: "contextmenu", button: 2}, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ let typeAttr = test.type ? "type=" + test.type + " " : "";
+ is(gContextMenu.shouldDisplay, true,
+ "context menu behavior for <" + test.element + " " + typeAttr + "> is wrong");
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
+ contentAreaContextMenu.hidePopup();
+ yield popupHiddenPromise;
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug427559.js b/browser/base/content/test/general/browser_bug427559.js
new file mode 100644
index 000000000..78cecdefa
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug427559.js
@@ -0,0 +1,38 @@
+"use strict";
+
+/*
+ * Test bug 427559 to make sure focused elements that are no longer on the page
+ * will have focus transferred to the window when changing tabs back to that
+ * tab with the now-gone element.
+ */
+
+// Default focus on a button and have it kill itself on blur.
+const URL = 'data:text/html;charset=utf-8,' +
+ '<body><button onblur="this.remove()">' +
+ '<script>document.body.firstChild.focus()</script></body>';
+
+function getFocusedLocalName(browser) {
+ return ContentTask.spawn(browser, null, function* () {
+ return content.document.activeElement.localName;
+ });
+}
+
+add_task(function* () {
+ let testTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+
+ let browser = testTab.linkedBrowser;
+
+ is((yield getFocusedLocalName(browser)), "button", "button is focused");
+
+ let blankTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+ yield BrowserTestUtils.switchTab(gBrowser, testTab);
+
+ // Make sure focus is given to the window because the element is now gone.
+ is((yield getFocusedLocalName(browser)), "body", "body is focused");
+
+ // Cleanup.
+ gBrowser.removeTab(blankTab);
+ gBrowser.removeCurrentTab();
+
+});
diff --git a/browser/base/content/test/general/browser_bug431826.js b/browser/base/content/test/general/browser_bug431826.js
new file mode 100644
index 000000000..592ea9cef
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug431826.js
@@ -0,0 +1,50 @@
+function remote(task) {
+ return ContentTask.spawn(gBrowser.selectedBrowser, null, task);
+}
+
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ let promise = remote(function () {
+ return ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", true, event => {
+ return content.document.documentURI != "about:blank";
+ }).then(() => 0); // don't want to send the event to the chrome process
+ });
+ gBrowser.loadURI("https://nocert.example.com/");
+ yield promise;
+
+ yield remote(() => {
+ // Confirm that we are displaying the contributed error page, not the default
+ let uri = content.document.documentURI;
+ Assert.ok(uri.startsWith("about:certerror"), "Broken page should go to about:certerror, not about:neterror");
+ });
+
+ yield remote(() => {
+ let div = content.document.getElementById("badCertAdvancedPanel");
+ // Confirm that the expert section is collapsed
+ Assert.ok(div, "Advanced content div should exist");
+ Assert.equal(div.ownerGlobal.getComputedStyle(div).display,
+ "none", "Advanced content should not be visible by default");
+ });
+
+ // Tweak the expert mode pref
+ gPrefService.setBoolPref("browser.xul.error_pages.expert_bad_cert", true);
+
+ promise = remote(function () {
+ return ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", true);
+ });
+ gBrowser.reload();
+ yield promise;
+
+ yield remote(() => {
+ let div = content.document.getElementById("badCertAdvancedPanel");
+ Assert.ok(div, "Advanced content div should exist");
+ Assert.equal(div.ownerGlobal.getComputedStyle(div).display,
+ "block", "Advanced content should be visible by default");
+ });
+
+ // Clean up
+ gBrowser.removeCurrentTab();
+ if (gPrefService.prefHasUserValue("browser.xul.error_pages.expert_bad_cert"))
+ gPrefService.clearUserPref("browser.xul.error_pages.expert_bad_cert");
+});
diff --git a/browser/base/content/test/general/browser_bug432599.js b/browser/base/content/test/general/browser_bug432599.js
new file mode 100644
index 000000000..a5f7c0b5e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug432599.js
@@ -0,0 +1,127 @@
+function invokeUsingCtrlD(phase) {
+ switch (phase) {
+ case 1:
+ EventUtils.synthesizeKey("d", { accelKey: true });
+ break;
+ case 2:
+ case 4:
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ break;
+ case 3:
+ EventUtils.synthesizeKey("d", { accelKey: true });
+ EventUtils.synthesizeKey("d", { accelKey: true });
+ break;
+ }
+}
+
+function invokeUsingStarButton(phase) {
+ switch (phase) {
+ case 1:
+ EventUtils.synthesizeMouseAtCenter(BookmarkingUI.star, {});
+ break;
+ case 2:
+ case 4:
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ break;
+ case 3:
+ EventUtils.synthesizeMouseAtCenter(BookmarkingUI.star,
+ { clickCount: 2 });
+ break;
+ }
+}
+
+var testURL = "data:text/plain,Content";
+var bookmarkId;
+
+function add_bookmark(aURI, aTitle) {
+ return PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ aTitle);
+}
+
+// test bug 432599
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ waitForStarChange(false, initTest);
+ }, true);
+
+ content.location = testURL;
+}
+
+function initTest() {
+ // First, bookmark the page.
+ bookmarkId = add_bookmark(makeURI(testURL), "Bug 432599 Test");
+
+ checkBookmarksPanel(invokers[currentInvoker], 1);
+}
+
+function waitForStarChange(aValue, aCallback) {
+ let expectedStatus = aValue ? BookmarkingUI.STATUS_STARRED
+ : BookmarkingUI.STATUS_UNSTARRED;
+ if (BookmarkingUI.status == BookmarkingUI.STATUS_UPDATING ||
+ BookmarkingUI.status != expectedStatus) {
+ info("Waiting for star button change.");
+ setTimeout(waitForStarChange, 50, aValue, aCallback);
+ return;
+ }
+ aCallback();
+}
+
+var invokers = [invokeUsingStarButton, invokeUsingCtrlD];
+var currentInvoker = 0;
+
+var initialValue;
+var initialRemoveHidden;
+
+var popupElement = document.getElementById("editBookmarkPanel");
+var titleElement = document.getElementById("editBookmarkPanelTitle");
+var removeElement = document.getElementById("editBookmarkPanelRemoveButton");
+
+function checkBookmarksPanel(invoker, phase)
+{
+ let onPopupShown = function(aEvent) {
+ if (aEvent.originalTarget == popupElement) {
+ popupElement.removeEventListener("popupshown", arguments.callee, false);
+ checkBookmarksPanel(invoker, phase + 1);
+ }
+ };
+ let onPopupHidden = function(aEvent) {
+ if (aEvent.originalTarget == popupElement) {
+ popupElement.removeEventListener("popuphidden", arguments.callee, false);
+ if (phase < 4) {
+ checkBookmarksPanel(invoker, phase + 1);
+ } else {
+ ++currentInvoker;
+ if (currentInvoker < invokers.length) {
+ checkBookmarksPanel(invokers[currentInvoker], 1);
+ } else {
+ gBrowser.removeTab(gBrowser.selectedTab, {skipPermitUnload: true});
+ PlacesUtils.bookmarks.removeItem(bookmarkId);
+ executeSoon(finish);
+ }
+ }
+ }
+ };
+
+ switch (phase) {
+ case 1:
+ case 3:
+ popupElement.addEventListener("popupshown", onPopupShown, false);
+ break;
+ case 2:
+ popupElement.addEventListener("popuphidden", onPopupHidden, false);
+ initialValue = titleElement.value;
+ initialRemoveHidden = removeElement.hidden;
+ break;
+ case 4:
+ popupElement.addEventListener("popuphidden", onPopupHidden, false);
+ is(titleElement.value, initialValue, "The bookmark panel's title should be the same");
+ is(removeElement.hidden, initialRemoveHidden, "The bookmark panel's visibility should not change");
+ break;
+ }
+ invoker(phase);
+}
diff --git a/browser/base/content/test/general/browser_bug435035.js b/browser/base/content/test/general/browser_bug435035.js
new file mode 100644
index 000000000..7570ef0d7
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug435035.js
@@ -0,0 +1,17 @@
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ is(document.getElementById("identity-box").className,
+ "unknownIdentity mixedDisplayContent",
+ "identity box has class name for mixed content");
+
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+
+ gBrowser.loadURI(
+ "https://example.com/browser/browser/base/content/test/general/test_bug435035.html"
+ );
+}
diff --git a/browser/base/content/test/general/browser_bug435325.js b/browser/base/content/test/general/browser_bug435325.js
new file mode 100644
index 000000000..2ae15deff
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug435325.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Ensure that clicking the button in the Offline mode neterror page makes the browser go online. See bug 435325. */
+
+var proxyPrefValue;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Go offline and disable the proxy and cache, then try to load the test URL.
+ Services.io.offline = true;
+
+ // 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.prefs.setBoolPref("browser.cache.disk.enable", false);
+ Services.prefs.setBoolPref("browser.cache.memory.enable", false);
+
+ gBrowser.selectedTab = gBrowser.addTab("http://example.com/");
+
+ let contentScript = `
+ let listener = function () {
+ removeEventListener("DOMContentLoaded", listener);
+ sendAsyncMessage("Test:DOMContentLoaded", { uri: content.document.documentURI });
+ };
+ addEventListener("DOMContentLoaded", listener);
+ `;
+
+ function pageloaded({ data }) {
+ mm.removeMessageListener("Test:DOMContentLoaded", pageloaded);
+ checkPage(data);
+ }
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.addMessageListener("Test:DOMContentLoaded", pageloaded);
+ mm.loadFrameScript("data:," + contentScript, true);
+}
+
+function checkPage(data) {
+ ok(Services.io.offline, "Setting Services.io.offline to true.");
+
+ is(data.uri.substring(0, 27),
+ "about:neterror?e=netOffline", "Loading the Offline mode neterror page.");
+
+ // Re-enable the proxy so example.com is resolved to localhost, rather than
+ // the actual example.com.
+ Services.prefs.setIntPref("network.proxy.type", proxyPrefValue);
+
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ ok(!Services.io.offline, "After clicking the Try Again button, we're back " +
+ "online.");
+ Services.obs.removeObserver(observer, "network:offline-status-changed", false);
+ finish();
+ }, "network:offline-status-changed", false);
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ content.document.getElementById("errorTryAgain").click();
+ });
+}
+
+registerCleanupFunction(function() {
+ Services.prefs.setBoolPref("browser.cache.disk.enable", true);
+ Services.prefs.setBoolPref("browser.cache.memory.enable", true);
+ Services.io.offline = false;
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug441778.js b/browser/base/content/test/general/browser_bug441778.js
new file mode 100644
index 000000000..fa938541f
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug441778.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/. */
+
+/*
+ * Test the fix for bug 441778 to ensure site-specific page zoom doesn't get
+ * modified by sub-document loads of content from a different domain.
+ */
+
+function test() {
+ waitForExplicitFinish();
+
+ const TEST_PAGE_URL = 'data:text/html,<body><iframe src=""></iframe></body>';
+ const TEST_IFRAME_URL = "http://test2.example.org/";
+
+ Task.spawn(function* () {
+ // Prepare the test tab
+ let tab = gBrowser.addTab();
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+
+ let testBrowser = tab.linkedBrowser;
+
+ yield FullZoomHelper.load(tab, TEST_PAGE_URL);
+
+ // Change the zoom level and then save it so we can compare it to the level
+ // after loading the sub-document.
+ FullZoom.enlarge();
+ var zoomLevel = ZoomManager.zoom;
+
+ // Start the sub-document load.
+ let deferred = Promise.defer();
+ executeSoon(function () {
+ BrowserTestUtils.browserLoaded(testBrowser, true).then(url => {
+ is(url, TEST_IFRAME_URL, "got the load event for the iframe");
+ is(ZoomManager.zoom, zoomLevel, "zoom is retained after sub-document load");
+
+ FullZoomHelper.removeTabAndWaitForLocationChange().
+ then(() => deferred.resolve());
+ });
+ ContentTask.spawn(testBrowser, TEST_IFRAME_URL, url => {
+ content.document.querySelector("iframe").src = url;
+ });
+ });
+ yield deferred.promise;
+ }).then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/general/browser_bug455852.js b/browser/base/content/test/general/browser_bug455852.js
new file mode 100644
index 000000000..ce883b581
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug455852.js
@@ -0,0 +1,20 @@
+add_task(function*() {
+ is(gBrowser.tabs.length, 1, "one tab is open");
+
+ gBrowser.selectedBrowser.focus();
+ isnot(document.activeElement, gURLBar.inputField, "location bar is not focused");
+
+ var tab = gBrowser.selectedTab;
+ gPrefService.setBoolPref("browser.tabs.closeWindowWithLastTab", false);
+
+ let tabClosedPromise = BrowserTestUtils.removeTab(tab, {dontRemove: true});
+ EventUtils.synthesizeKey("w", { accelKey: true });
+ yield tabClosedPromise;
+
+ is(tab.parentNode, null, "ctrl+w removes the tab");
+ is(gBrowser.tabs.length, 1, "a new tab has been opened");
+ is(document.activeElement, gURLBar.inputField, "location bar is focused for the new tab");
+
+ if (gPrefService.prefHasUserValue("browser.tabs.closeWindowWithLastTab"))
+ gPrefService.clearUserPref("browser.tabs.closeWindowWithLastTab");
+});
diff --git a/browser/base/content/test/general/browser_bug460146.js b/browser/base/content/test/general/browser_bug460146.js
new file mode 100644
index 000000000..1fdf0921c
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug460146.js
@@ -0,0 +1,51 @@
+/* Check proper image url retrieval from all kinds of elements/styles */
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ gBrowser.selectedBrowser.addEventListener("load", function () {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+
+ var pageInfo = BrowserPageInfo(gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab");
+
+ pageInfo.addEventListener("load", function () {
+ pageInfo.removeEventListener("load", arguments.callee, true);
+ pageInfo.onFinished.push(function () {
+ executeSoon(function () {
+ var imageTree = pageInfo.document.getElementById("imagetree");
+ var imageRowsNum = imageTree.view.rowCount;
+
+ ok(imageTree, "Image tree is null (media tab is broken)");
+
+ ok(imageRowsNum == 7, "Number of images listed: " +
+ imageRowsNum + ", should be 7");
+
+ pageInfo.close();
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ });
+ }, true);
+ }, true);
+
+ content.location =
+ "data:text/html," +
+ "<html>" +
+ " <head>" +
+ " <title>Test for media tab</title>" +
+ " <link rel='shortcut icon' href='file:///dummy_icon.ico'>" + // Icon
+ " </head>" +
+ " <body style='background-image:url(about:logo?a);'>" + // Background
+ " <img src='file:///dummy_image.gif'>" + // Image
+ " <ul>" +
+ " <li style='list-style:url(about:logo?b);'>List Item 1</li>" + // Bullet
+ " </ul> " +
+ " <div style='-moz-border-image: url(about:logo?c) 20 20 20 20;'>test</div>" + // Border
+ " <a href='' style='cursor: url(about:logo?d),default;'>test link</a>" + // Cursor
+ " <object type='image/svg+xml' width=20 height=20 data='file:///dummy_object.svg'></object>" + // Object
+ " </body>" +
+ "</html>";
+}
diff --git a/browser/base/content/test/general/browser_bug462289.js b/browser/base/content/test/general/browser_bug462289.js
new file mode 100644
index 000000000..1ce79f07e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug462289.js
@@ -0,0 +1,81 @@
+var tab1, tab2;
+
+function focus_in_navbar()
+{
+ var parent = document.activeElement.parentNode;
+ while (parent && parent.id != "nav-bar")
+ parent = parent.parentNode;
+
+ return parent != null;
+}
+
+function test()
+{
+ waitForExplicitFinish();
+
+ tab1 = gBrowser.addTab("about:blank", {skipAnimation: true});
+ tab2 = gBrowser.addTab("about:blank", {skipAnimation: true});
+
+ EventUtils.synthesizeMouseAtCenter(tab1, {});
+ executeSoon(step2);
+}
+
+function step2()
+{
+ is(gBrowser.selectedTab, tab1, "1st click on tab1 selects tab");
+ isnot(document.activeElement, tab1, "1st click on tab1 does not activate tab");
+
+ EventUtils.synthesizeMouseAtCenter(tab1, {});
+ executeSoon(step3);
+}
+
+function step3()
+{
+ is(gBrowser.selectedTab, tab1, "2nd click on selected tab1 keeps tab selected");
+ isnot(document.activeElement, tab1, "2nd click on selected tab1 does not activate tab");
+
+ ok(true, "focusing URLBar then sending 1 Shift+Tab.");
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
+ is(gBrowser.selectedTab, tab1, "tab key to selected tab1 keeps tab selected");
+ is(document.activeElement, tab1, "tab key to selected tab1 activates tab");
+
+ EventUtils.synthesizeMouseAtCenter(tab1, {});
+ executeSoon(step4);
+}
+
+function step4()
+{
+ is(gBrowser.selectedTab, tab1, "3rd click on activated tab1 keeps tab selected");
+ is(document.activeElement, tab1, "3rd click on activated tab1 keeps tab activated");
+
+ gBrowser.addEventListener("TabSwitchDone", step5);
+ EventUtils.synthesizeMouseAtCenter(tab2, {});
+}
+
+function step5()
+{
+ gBrowser.removeEventListener("TabSwitchDone", step5);
+
+ // The tabbox selects a tab within a setTimeout in a bubbling mousedown event
+ // listener, and focuses the current tab if another tab previously had focus.
+ is(gBrowser.selectedTab, tab2, "click on tab2 while tab1 is activated selects tab");
+ is(document.activeElement, tab2, "click on tab2 while tab1 is activated activates tab");
+
+ info("focusing content then sending middle-button mousedown to tab2.");
+ gBrowser.selectedBrowser.focus();
+
+ EventUtils.synthesizeMouseAtCenter(tab2, {button: 1, type: "mousedown"});
+ executeSoon(step6);
+}
+
+function step6()
+{
+ is(gBrowser.selectedTab, tab2, "middle-button mousedown on selected tab2 keeps tab selected");
+ isnot(document.activeElement, tab2, "middle-button mousedown on selected tab2 does not activate tab");
+
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab1);
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug462673.js b/browser/base/content/test/general/browser_bug462673.js
new file mode 100644
index 000000000..f5b090917
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug462673.js
@@ -0,0 +1,36 @@
+add_task(function* () {
+ var win = openDialog(getBrowserURL(), "_blank", "chrome,all,dialog=no");
+ yield SimpleTest.promiseFocus(win);
+
+ let tab = win.gBrowser.tabContainer.firstChild;
+ yield promiseTabLoadEvent(tab, getRootDirectory(gTestPath) + "test_bug462673.html");
+
+ is(win.gBrowser.browsers.length, 2, "test_bug462673.html has opened a second tab");
+ is(win.gBrowser.selectedTab, tab.nextSibling, "dependent tab is selected");
+ win.gBrowser.removeTab(tab);
+
+ // Closing a tab will also close its parent chrome window, but async
+ yield promiseWindowWillBeClosed(win);
+});
+
+add_task(function* () {
+ var win = openDialog(getBrowserURL(), "_blank", "chrome,all,dialog=no");
+ yield SimpleTest.promiseFocus(win);
+
+ let tab = win.gBrowser.tabContainer.firstChild;
+ yield promiseTabLoadEvent(tab, getRootDirectory(gTestPath) + "test_bug462673.html");
+
+ var newTab = win.gBrowser.addTab();
+ var newBrowser = newTab.linkedBrowser;
+ win.gBrowser.removeTab(tab);
+ ok(!win.closed, "Window stays open");
+ if (!win.closed) {
+ is(win.gBrowser.tabContainer.childElementCount, 1, "Window has one tab");
+ is(win.gBrowser.browsers.length, 1, "Window has one browser");
+ is(win.gBrowser.selectedTab, newTab, "Remaining tab is selected");
+ is(win.gBrowser.selectedBrowser, newBrowser, "Browser for remaining tab is selected");
+ is(win.gBrowser.mTabBox.selectedPanel, newBrowser.parentNode.parentNode.parentNode.parentNode, "Panel for remaining tab is selected");
+ }
+
+ yield promiseWindowClosed(win);
+});
diff --git a/browser/base/content/test/general/browser_bug477014.js b/browser/base/content/test/general/browser_bug477014.js
new file mode 100644
index 000000000..8a0fac6d8
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug477014.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/. */
+
+// That's a gecko!
+const iconURLSpec = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+var testPage="data:text/plain,test bug 477014";
+
+add_task(function*() {
+ let tabToDetach = gBrowser.addTab(testPage);
+ yield waitForDocLoadComplete(tabToDetach.linkedBrowser);
+
+ gBrowser.setIcon(tabToDetach, iconURLSpec,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ tabToDetach.setAttribute("busy", "true");
+
+ // detach and set the listener on the new window
+ let newWindow = gBrowser.replaceTabWithWindow(tabToDetach);
+ yield promiseWaitForEvent(tabToDetach.linkedBrowser, "SwapDocShells");
+
+ is(newWindow.gBrowser.selectedTab.hasAttribute("busy"), true, "Busy attribute should be correct");
+ is(newWindow.gBrowser.getIcon(), iconURLSpec, "Icon should be correct");
+
+ newWindow.close();
+});
diff --git a/browser/base/content/test/general/browser_bug479408.js b/browser/base/content/test/general/browser_bug479408.js
new file mode 100644
index 000000000..0dfa96f2e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug479408.js
@@ -0,0 +1,17 @@
+function test() {
+ waitForExplicitFinish();
+ let tab = gBrowser.selectedTab = gBrowser.addTab(
+ "http://mochi.test:8888/browser/browser/base/content/test/general/browser_bug479408_sample.html");
+
+ gBrowser.addEventListener("DOMLinkAdded", function(aEvent) {
+ gBrowser.removeEventListener("DOMLinkAdded", arguments.callee, true);
+
+ executeSoon(function() {
+ ok(!tab.linkedBrowser.engines,
+ "the subframe's search engine wasn't detected");
+
+ gBrowser.removeTab(tab);
+ finish();
+ });
+ }, true);
+}
diff --git a/browser/base/content/test/general/browser_bug479408_sample.html b/browser/base/content/test/general/browser_bug479408_sample.html
new file mode 100644
index 000000000..f83f02bb9
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug479408_sample.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<title>Testcase for bug 479408</title>
+
+<iframe src='data:text/html,<link%20rel="search"%20type="application/opensearchdescription+xml"%20title="Search%20bug%20479408"%20href="http://example.com/search.xml">'>
diff --git a/browser/base/content/test/general/browser_bug481560.js b/browser/base/content/test/general/browser_bug481560.js
new file mode 100644
index 000000000..bb9249e75
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug481560.js
@@ -0,0 +1,21 @@
+function test() {
+ waitForExplicitFinish();
+
+ whenNewWindowLoaded(null, function (win) {
+ waitForFocus(function () {
+ function onTabClose() {
+ ok(false, "shouldn't have gotten the TabClose event for the last tab");
+ }
+ var tab = win.gBrowser.selectedTab;
+ tab.addEventListener("TabClose", onTabClose, false);
+
+ EventUtils.synthesizeKey("w", { accelKey: true }, win);
+
+ ok(win.closed, "accel+w closed the window immediately");
+
+ tab.removeEventListener("TabClose", onTabClose, false);
+
+ finish();
+ }, win);
+ });
+}
diff --git a/browser/base/content/test/general/browser_bug484315.js b/browser/base/content/test/general/browser_bug484315.js
new file mode 100644
index 000000000..fb23ae33a
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug484315.js
@@ -0,0 +1,23 @@
+function test() {
+ var contentWin = window.open("about:blank", "", "width=100,height=100");
+ var enumerator = Services.wm.getEnumerator("navigator:browser");
+
+ while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext();
+ if (win.content == contentWin) {
+ gPrefService.setBoolPref("browser.tabs.closeWindowWithLastTab", false);
+ win.gBrowser.removeCurrentTab();
+ ok(win.closed, "popup is closed");
+
+ // clean up
+ if (!win.closed)
+ win.close();
+ if (gPrefService.prefHasUserValue("browser.tabs.closeWindowWithLastTab"))
+ gPrefService.clearUserPref("browser.tabs.closeWindowWithLastTab");
+
+ return;
+ }
+ }
+
+ throw "couldn't find the content window";
+}
diff --git a/browser/base/content/test/general/browser_bug491431.js b/browser/base/content/test/general/browser_bug491431.js
new file mode 100644
index 000000000..d270e912e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug491431.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var testPage = "data:text/plain,test bug 491431 Page";
+
+function test() {
+ waitForExplicitFinish();
+
+ let newWin, tabA, tabB;
+
+ // test normal close
+ tabA = gBrowser.addTab(testPage);
+ gBrowser.tabContainer.addEventListener("TabClose", function(firstTabCloseEvent) {
+ gBrowser.tabContainer.removeEventListener("TabClose", arguments.callee, true);
+ ok(!firstTabCloseEvent.detail.adoptedBy, "This was a normal tab close");
+
+ // test tab close by moving
+ tabB = gBrowser.addTab(testPage);
+ gBrowser.tabContainer.addEventListener("TabClose", function(secondTabCloseEvent) {
+ gBrowser.tabContainer.removeEventListener("TabClose", arguments.callee, true);
+ executeSoon(function() {
+ ok(secondTabCloseEvent.detail.adoptedBy, "This was a tab closed by moving");
+
+ // cleanup
+ newWin.close();
+ executeSoon(finish);
+ });
+ }, true);
+ newWin = gBrowser.replaceTabWithWindow(tabB);
+ }, true);
+ gBrowser.removeTab(tabA);
+}
+
diff --git a/browser/base/content/test/general/browser_bug495058.js b/browser/base/content/test/general/browser_bug495058.js
new file mode 100644
index 000000000..a82c6c931
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug495058.js
@@ -0,0 +1,38 @@
+/**
+ * Tests that the right elements of a tab are focused when it is
+ * torn out into its own window.
+ */
+
+const URIS = [
+ "about:blank",
+ "about:sessionrestore",
+ "about:privatebrowsing",
+];
+
+add_task(function*() {
+ for (let uri of URIS) {
+ let tab = gBrowser.addTab();
+ yield BrowserTestUtils.loadURI(tab.linkedBrowser, uri);
+
+ let win = gBrowser.replaceTabWithWindow(tab);
+ yield TestUtils.topicObserved("browser-delayed-startup-finished",
+ subject => subject == win);
+ tab = win.gBrowser.selectedTab;
+
+ // BrowserTestUtils doesn't get the add-on shims, which means that
+ // MozAfterPaint won't get shimmed over if we add an event handler
+ // for it in the parent.
+ if (tab.linkedBrowser.isRemoteBrowser) {
+ yield BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "MozAfterPaint");
+ } else {
+ yield BrowserTestUtils.waitForEvent(tab.linkedBrowser, "MozAfterPaint");
+ }
+
+ Assert.equal(win.gBrowser.currentURI.spec, uri, uri + ": uri loaded in detached tab");
+ Assert.equal(win.document.activeElement, win.gBrowser.selectedBrowser, uri + ": browser is focused");
+ Assert.equal(win.gURLBar.value, "", uri + ": urlbar is empty");
+ Assert.ok(win.gURLBar.placeholder, uri + ": placeholder text is present");
+
+ yield BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/browser/base/content/test/general/browser_bug517902.js b/browser/base/content/test/general/browser_bug517902.js
new file mode 100644
index 000000000..bc1d16f4b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug517902.js
@@ -0,0 +1,42 @@
+/* Make sure that "View Image Info" loads the correct image data */
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ gBrowser.selectedBrowser.addEventListener("load", function () {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+
+ var doc = gBrowser.contentDocument;
+ var testImg = doc.getElementById("test-image");
+ var pageInfo = BrowserPageInfo(gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab", testImg);
+
+ pageInfo.addEventListener("load", function () {
+ pageInfo.removeEventListener("load", arguments.callee, true);
+ pageInfo.onFinished.push(function () {
+ executeSoon(function () {
+ var pageInfoImg = pageInfo.document.getElementById("thepreviewimage");
+
+ is(pageInfoImg.src, testImg.src, "selected image has the correct source");
+ is(pageInfoImg.width, testImg.width, "selected image has the correct width");
+ is(pageInfoImg.height, testImg.height, "selected image has the correct height");
+
+ pageInfo.close();
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ });
+ }, true);
+ }, true);
+
+ content.location =
+ "data:text/html," +
+ "<style type='text/css'>%23test-image,%23not-test-image {background-image: url('about:logo?c');}</style>" +
+ "<img src='about:logo?b' height=300 width=350 alt=2 id='not-test-image'>" +
+ "<img src='about:logo?b' height=300 width=350 alt=2>" +
+ "<img src='about:logo?a' height=200 width=250>" +
+ "<img src='about:logo?b' height=200 width=250 alt=1>" +
+ "<img src='about:logo?b' height=100 width=150 alt=2 id='test-image'>";
+}
diff --git a/browser/base/content/test/general/browser_bug519216.js b/browser/base/content/test/general/browser_bug519216.js
new file mode 100644
index 000000000..d3a517086
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug519216.js
@@ -0,0 +1,45 @@
+function test() {
+ waitForExplicitFinish();
+ gBrowser.addProgressListener(progressListener1);
+ gBrowser.addProgressListener(progressListener2);
+ gBrowser.addProgressListener(progressListener3);
+ gBrowser.loadURI("data:text/plain,bug519216");
+}
+
+var calledListener1 = false;
+var progressListener1 = {
+ onLocationChange: function onLocationChange() {
+ calledListener1 = true;
+ gBrowser.removeProgressListener(this);
+ }
+};
+
+var calledListener2 = false;
+var progressListener2 = {
+ onLocationChange: function onLocationChange() {
+ ok(calledListener1, "called progressListener1 before progressListener2");
+ calledListener2 = true;
+ gBrowser.removeProgressListener(this);
+ }
+};
+
+var progressListener3 = {
+ onLocationChange: function onLocationChange() {
+ ok(calledListener2, "called progressListener2 before progressListener3");
+ gBrowser.removeProgressListener(this);
+ gBrowser.addProgressListener(progressListener4);
+ executeSoon(function () {
+ expectListener4 = true;
+ gBrowser.reload();
+ });
+ }
+};
+
+var expectListener4 = false;
+var progressListener4 = {
+ onLocationChange: function onLocationChange() {
+ ok(expectListener4, "didn't call progressListener4 for the first location change");
+ gBrowser.removeProgressListener(this);
+ executeSoon(finish);
+ }
+};
diff --git a/browser/base/content/test/general/browser_bug520538.js b/browser/base/content/test/general/browser_bug520538.js
new file mode 100644
index 000000000..e0b64db9d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug520538.js
@@ -0,0 +1,15 @@
+function test() {
+ var tabCount = gBrowser.tabs.length;
+ gBrowser.selectedBrowser.focus();
+ browserDOMWindow.openURI(makeURI("about:blank"),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+ Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+ is(gBrowser.tabs.length, tabCount + 1,
+ "'--new-tab about:blank' opens a new tab");
+ is(gBrowser.selectedTab, gBrowser.tabs[tabCount],
+ "'--new-tab about:blank' selects the new tab");
+ is(document.activeElement, gURLBar.inputField,
+ "'--new-tab about:blank' focuses the location bar");
+ gBrowser.removeCurrentTab();
+}
diff --git a/browser/base/content/test/general/browser_bug521216.js b/browser/base/content/test/general/browser_bug521216.js
new file mode 100644
index 000000000..735ae92f6
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug521216.js
@@ -0,0 +1,50 @@
+var expected = ["TabOpen", "onStateChange", "onLocationChange", "onLinkIconAvailable"];
+var actual = [];
+var tabIndex = -1;
+this.__defineGetter__("tab", () => gBrowser.tabs[tabIndex]);
+
+function test() {
+ waitForExplicitFinish();
+ tabIndex = gBrowser.tabs.length;
+ gBrowser.addTabsProgressListener(progressListener);
+ gBrowser.tabContainer.addEventListener("TabOpen", TabOpen, false);
+ gBrowser.addTab("data:text/html,<html><head><link href='about:logo' rel='shortcut icon'>");
+}
+
+function record(aName) {
+ info("got " + aName);
+ if (actual.indexOf(aName) == -1)
+ actual.push(aName);
+ if (actual.length == expected.length) {
+ is(actual.toString(), expected.toString(),
+ "got events and progress notifications in expected order");
+
+ executeSoon(function(tab) {
+ gBrowser.removeTab(tab);
+ gBrowser.removeTabsProgressListener(progressListener);
+ gBrowser.tabContainer.removeEventListener("TabOpen", TabOpen, false);
+ finish();
+ }.bind(null, tab));
+ }
+}
+
+function TabOpen(aEvent) {
+ if (aEvent.target == tab)
+ record(arguments.callee.name);
+}
+
+var progressListener = {
+ onLocationChange: function onLocationChange(aBrowser) {
+ if (aBrowser == tab.linkedBrowser)
+ record(arguments.callee.name);
+ },
+ onStateChange: function onStateChange(aBrowser) {
+ if (aBrowser == tab.linkedBrowser)
+ record(arguments.callee.name);
+ },
+ onLinkIconAvailable: function onLinkIconAvailable(aBrowser, aIconURL) {
+ if (aBrowser == tab.linkedBrowser &&
+ aIconURL == "about:logo")
+ record(arguments.callee.name);
+ }
+};
diff --git a/browser/base/content/test/general/browser_bug533232.js b/browser/base/content/test/general/browser_bug533232.js
new file mode 100644
index 000000000..6c7a0e51f
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug533232.js
@@ -0,0 +1,36 @@
+function test() {
+ var tab1 = gBrowser.selectedTab;
+ var tab2 = gBrowser.addTab();
+ var childTab1;
+ var childTab2;
+
+ childTab1 = gBrowser.addTab("about:blank", { relatedToCurrent: true });
+ gBrowser.selectedTab = childTab1;
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ is(idx(gBrowser.selectedTab), idx(tab1),
+ "closing a tab next to its parent selects the parent");
+
+ childTab1 = gBrowser.addTab("about:blank", { relatedToCurrent: true });
+ gBrowser.selectedTab = tab2;
+ gBrowser.selectedTab = childTab1;
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ is(idx(gBrowser.selectedTab), idx(tab2),
+ "closing a tab next to its parent doesn't select the parent if another tab had been selected ad interim");
+
+ gBrowser.selectedTab = tab1;
+ childTab1 = gBrowser.addTab("about:blank", { relatedToCurrent: true });
+ childTab2 = gBrowser.addTab("about:blank", { relatedToCurrent: true });
+ gBrowser.selectedTab = childTab1;
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ is(idx(gBrowser.selectedTab), idx(childTab2),
+ "closing a tab next to its parent selects the next tab with the same parent");
+ gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
+ is(idx(gBrowser.selectedTab), idx(tab2),
+ "closing the last tab in a set of child tabs doesn't go back to the parent");
+
+ gBrowser.removeTab(tab2, { skipPermitUnload: true });
+}
+
+function idx(tab) {
+ return Array.indexOf(gBrowser.tabs, tab);
+}
diff --git a/browser/base/content/test/general/browser_bug537013.js b/browser/base/content/test/general/browser_bug537013.js
new file mode 100644
index 000000000..5ae1586ea
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug537013.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests for bug 537013 to ensure proper tab-sequestration of find bar. */
+
+var tabs = [];
+var texts = [
+ "This side up.",
+ "The world is coming to an end. Please log off.",
+ "Klein bottle for sale. Inquire within.",
+ "To err is human; to forgive is not company policy."
+];
+
+var Clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
+var HasFindClipboard = Clipboard.supportsFindClipboard();
+
+function addTabWithText(aText, aCallback) {
+ let newTab = gBrowser.addTab("data:text/html;charset=utf-8,<h1 id='h1'>" +
+ aText + "</h1>");
+ tabs.push(newTab);
+ gBrowser.selectedTab = newTab;
+}
+
+function setFindString(aString) {
+ gFindBar.open();
+ gFindBar._findField.focus();
+ gFindBar._findField.select();
+ EventUtils.sendString(aString);
+ is(gFindBar._findField.value, aString, "Set the field correctly!");
+}
+
+var newWindow;
+
+function test() {
+ waitForExplicitFinish();
+ registerCleanupFunction(function () {
+ while (tabs.length) {
+ gBrowser.removeTab(tabs.pop());
+ }
+ });
+ texts.forEach(aText => addTabWithText(aText));
+
+ // Set up the first tab
+ gBrowser.selectedTab = tabs[0];
+
+ setFindString(texts[0]);
+ // Turn on highlight for testing bug 891638
+ gFindBar.toggleHighlight(true);
+
+ // Make sure the second tab is correct, then set it up
+ gBrowser.selectedTab = tabs[1];
+ gBrowser.selectedTab.addEventListener("TabFindInitialized", continueTests1);
+ // Initialize the findbar
+ gFindBar;
+}
+function continueTests1() {
+ gBrowser.selectedTab.removeEventListener("TabFindInitialized",
+ continueTests1);
+ ok(true, "'TabFindInitialized' event properly dispatched!");
+ ok(gFindBar.hidden, "Second tab doesn't show find bar!");
+ gFindBar.open();
+ is(gFindBar._findField.value, texts[0],
+ "Second tab kept old find value for new initialization!");
+ setFindString(texts[1]);
+
+ // Confirm the first tab is still correct, ensure re-hiding works as expected
+ gBrowser.selectedTab = tabs[0];
+ ok(!gFindBar.hidden, "First tab shows find bar!");
+ // When the Find Clipboard is supported, this test not relevant.
+ if (!HasFindClipboard)
+ is(gFindBar._findField.value, texts[0], "First tab persists find value!");
+ ok(gFindBar.getElement("highlight").checked,
+ "Highlight button state persists!");
+
+ // While we're here, let's test the backout of bug 253793.
+ gBrowser.reload();
+ gBrowser.addEventListener("DOMContentLoaded", continueTests2, true);
+}
+
+function continueTests2() {
+ gBrowser.removeEventListener("DOMContentLoaded", continueTests2, true);
+ ok(gFindBar.getElement("highlight").checked, "Highlight never reset!");
+ continueTests3();
+}
+
+function continueTests3() {
+ ok(gFindBar.getElement("highlight").checked, "Highlight button reset!");
+ gFindBar.close();
+ ok(gFindBar.hidden, "First tab doesn't show find bar!");
+ gBrowser.selectedTab = tabs[1];
+ ok(!gFindBar.hidden, "Second tab shows find bar!");
+ // Test for bug 892384
+ is(gFindBar._findField.getAttribute("focused"), "true",
+ "Open findbar refocused on tab change!");
+ gURLBar.focus();
+ gBrowser.selectedTab = tabs[0];
+ ok(gFindBar.hidden, "First tab doesn't show find bar!");
+
+ // Set up a third tab, no tests here
+ gBrowser.selectedTab = tabs[2];
+ setFindString(texts[2]);
+
+ // Now we jump to the second, then first, and then fourth
+ gBrowser.selectedTab = tabs[1];
+ // Test for bug 892384
+ ok(!gFindBar._findField.hasAttribute("focused"),
+ "Open findbar not refocused on tab change!");
+ gBrowser.selectedTab = tabs[0];
+ gBrowser.selectedTab = tabs[3];
+ ok(gFindBar.hidden, "Fourth tab doesn't show find bar!");
+ is(gFindBar, gBrowser.getFindBar(), "Find bar is right one!");
+ gFindBar.open();
+ // Disabled the following assertion due to intermittent failure on OSX 10.6 Debug.
+ if (!HasFindClipboard) {
+ is(gFindBar._findField.value, texts[1],
+ "Fourth tab has second tab's find value!");
+ }
+
+ newWindow = gBrowser.replaceTabWithWindow(tabs.pop());
+ whenDelayedStartupFinished(newWindow, checkNewWindow);
+}
+
+// Test that findbar gets restored when a tab is moved to a new window.
+function checkNewWindow() {
+ ok(!newWindow.gFindBar.hidden, "New window shows find bar!");
+ // Disabled the following assertion due to intermittent failure on OSX 10.6 Debug.
+ if (!HasFindClipboard) {
+ is(newWindow.gFindBar._findField.value, texts[1],
+ "New window find bar has correct find value!");
+ }
+ ok(!newWindow.gFindBar.getElement("find-next").disabled,
+ "New window findbar has enabled buttons!");
+ newWindow.close();
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug537474.js b/browser/base/content/test/general/browser_bug537474.js
new file mode 100644
index 000000000..f1139f235
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug537474.js
@@ -0,0 +1,8 @@
+add_task(function *() {
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ browserDOMWindow.openURI(makeURI("about:"), null,
+ Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW, null)
+ yield browserLoadedPromise;
+ is(gBrowser.currentURI.spec, "about:", "page loads in the current content window");
+});
+
diff --git a/browser/base/content/test/general/browser_bug550565.js b/browser/base/content/test/general/browser_bug550565.js
new file mode 100644
index 000000000..b0e094e07
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug550565.js
@@ -0,0 +1,44 @@
+add_task(function* test() {
+ let testPath = getRootDirectory(gTestPath);
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" },
+ function* (tabBrowser) {
+ const URI = testPath + "file_with_favicon.html";
+ const expectedIcon = testPath + "file_generic_favicon.ico";
+
+ let got_favicon = Promise.defer();
+ let listener = {
+ onLinkIconAvailable(browser, iconURI) {
+ if (got_favicon && iconURI && browser === tabBrowser) {
+ got_favicon.resolve(iconURI);
+ got_favicon = null;
+ }
+ }
+ };
+ gBrowser.addTabsProgressListener(listener);
+
+ BrowserTestUtils.loadURI(tabBrowser, URI);
+
+ let iconURI = yield got_favicon.promise;
+ is(iconURI, expectedIcon, "Correct icon before pushState.");
+
+ got_favicon = Promise.defer();
+ got_favicon.promise.then(() => { ok(false, "shouldn't be called"); }, (e) => e);
+ yield ContentTask.spawn(tabBrowser, null, function() {
+ content.history.pushState("page2", "page2", "page2");
+ });
+
+ // We've navigated and shouldn't get a call to onLinkIconAvailable.
+ TestUtils.executeSoon(() => {
+ got_favicon.reject(gBrowser.getIcon(gBrowser.getTabForBrowser(tabBrowser)));
+ });
+ try {
+ yield got_favicon.promise;
+ } catch (e) {
+ iconURI = e;
+ }
+ is(iconURI, expectedIcon, "Correct icon after pushState.");
+
+ gBrowser.removeTabsProgressListener(listener);
+ });
+});
diff --git a/browser/base/content/test/general/browser_bug553455.js b/browser/base/content/test/general/browser_bug553455.js
new file mode 100644
index 000000000..c29a810de
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug553455.js
@@ -0,0 +1,1200 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTROOT = "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/";
+const TESTROOT2 = "http://example.org/browser/toolkit/mozapps/extensions/test/xpinstall/";
+const SECUREROOT = "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/";
+const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
+const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+const PROGRESS_NOTIFICATION = "addon-progress";
+
+const { REQUIRE_SIGNING } = Cu.import("resource://gre/modules/addons/AddonConstants.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm");
+
+var rootDir = getRootDirectory(gTestPath);
+var rootPath = rootDir.split('/');
+var chromeName = rootPath[0] + '//' + rootPath[2];
+var croot = chromeName + "/content/browser/toolkit/mozapps/extensions/test/xpinstall/";
+var jar = getJar(croot);
+if (jar) {
+ var tmpdir = extractJarToTmp(jar);
+ croot = 'file://' + tmpdir.path + '/';
+}
+const CHROMEROOT = croot;
+
+var gApp = document.getElementById("bundle_brand").getString("brandShortName");
+var gVersion = Services.appinfo.version;
+
+function getObserverTopic(aNotificationId) {
+ let topic = aNotificationId;
+ if (topic == "xpinstall-disabled")
+ topic = "addon-install-disabled";
+ else if (topic == "addon-progress")
+ topic = "addon-install-started";
+ else if (topic == "addon-install-restart")
+ topic = "addon-install-complete";
+ return topic;
+}
+
+function waitForProgressNotification(aPanelOpen = false, aExpectedCount = 1) {
+ return Task.spawn(function* () {
+ let notificationId = PROGRESS_NOTIFICATION;
+ info("Waiting for " + notificationId + " notification");
+
+ let topic = getObserverTopic(notificationId);
+
+ let observerPromise = new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ // Ignore the progress notification unless that is the notification we want
+ if (notificationId != PROGRESS_NOTIFICATION &&
+ aTopic == getObserverTopic(PROGRESS_NOTIFICATION)) {
+ return;
+ }
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }, topic, false);
+ });
+
+ let panelEventPromise;
+ if (aPanelOpen) {
+ panelEventPromise = Promise.resolve();
+ } else {
+ panelEventPromise = new Promise(resolve => {
+ PopupNotifications.panel.addEventListener("popupshowing", function eventListener() {
+ PopupNotifications.panel.removeEventListener("popupshowing", eventListener);
+ resolve();
+ });
+ });
+ }
+
+ yield observerPromise;
+ yield panelEventPromise;
+
+ info("Saw a notification");
+ ok(PopupNotifications.isPanelOpen, "Panel should be open");
+ is(PopupNotifications.panel.childNodes.length, aExpectedCount, "Should be the right number of notifications");
+ if (PopupNotifications.panel.childNodes.length) {
+ let nodes = Array.from(PopupNotifications.panel.childNodes);
+ let notification = nodes.find(n => n.id == notificationId + "-notification");
+ ok(notification, `Should have seen the right notification`);
+ }
+
+ return PopupNotifications.panel;
+ });
+}
+
+function waitForNotification(aId, aExpectedCount = 1) {
+ return Task.spawn(function* () {
+ info("Waiting for " + aId + " notification");
+
+ let topic = getObserverTopic(aId);
+
+ let observerPromise = new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ // Ignore the progress notification unless that is the notification we want
+ if (aId != PROGRESS_NOTIFICATION &&
+ aTopic == getObserverTopic(PROGRESS_NOTIFICATION)) {
+ return;
+ }
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }, topic, false);
+ });
+
+ let panelEventPromise = new Promise(resolve => {
+ PopupNotifications.panel.addEventListener("PanelUpdated", function eventListener(e) {
+ // Skip notifications that are not the one that we are supposed to be looking for
+ if (e.detail.indexOf(aId) == -1) {
+ return;
+ }
+ PopupNotifications.panel.removeEventListener("PanelUpdated", eventListener);
+ resolve();
+ });
+ });
+
+ yield observerPromise;
+ yield panelEventPromise;
+
+ info("Saw a notification");
+ ok(PopupNotifications.isPanelOpen, "Panel should be open");
+ is(PopupNotifications.panel.childNodes.length, aExpectedCount, "Should be the right number of notifications");
+ if (PopupNotifications.panel.childNodes.length) {
+ let nodes = Array.from(PopupNotifications.panel.childNodes);
+ let notification = nodes.find(n => n.id == aId + "-notification");
+ ok(notification, `Should have seen the right notification`);
+ }
+
+ return PopupNotifications.panel;
+ });
+}
+
+function waitForNotificationClose() {
+ return new Promise(resolve => {
+ info("Waiting for notification to close");
+ PopupNotifications.panel.addEventListener("popuphidden", function listener() {
+ PopupNotifications.panel.removeEventListener("popuphidden", listener, false);
+ resolve();
+ }, false);
+ });
+}
+
+function waitForInstallDialog() {
+ return Task.spawn(function* () {
+ if (Preferences.get("xpinstall.customConfirmationUI", false)) {
+ yield waitForNotification("addon-install-confirmation");
+ return;
+ }
+
+ info("Waiting for install dialog");
+
+ let window = yield new Promise(resolve => {
+ Services.wm.addListener({
+ onOpenWindow: function(aXULWindow) {
+ Services.wm.removeListener(this);
+ resolve(aXULWindow);
+ },
+ onCloseWindow: function(aXULWindow) {
+ },
+ onWindowTitleChange: function(aXULWindow, aNewTitle) {
+ }
+ });
+ });
+ info("Install dialog opened, waiting for focus");
+
+ let domwindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ yield new Promise(resolve => {
+ waitForFocus(function() {
+ resolve();
+ }, domwindow);
+ });
+ info("Saw install dialog");
+ is(domwindow.document.location.href, XPINSTALL_URL, "Should have seen the right window open");
+
+ // Override the countdown timer on the accept button
+ let button = domwindow.document.documentElement.getButton("accept");
+ button.disabled = false;
+
+ return;
+ });
+}
+
+function removeTab() {
+ return Promise.all([
+ waitForNotificationClose(),
+ BrowserTestUtils.removeTab(gBrowser.selectedTab)
+ ]);
+}
+
+function acceptInstallDialog() {
+ if (Preferences.get("xpinstall.customConfirmationUI", false)) {
+ document.getElementById("addon-install-confirmation-accept").click();
+ } else {
+ let win = Services.wm.getMostRecentWindow("Addons:Install");
+ win.document.documentElement.acceptDialog();
+ }
+}
+
+function cancelInstallDialog() {
+ if (Preferences.get("xpinstall.customConfirmationUI", false)) {
+ document.getElementById("addon-install-confirmation-cancel").click();
+ } else {
+ let win = Services.wm.getMostRecentWindow("Addons:Install");
+ win.document.documentElement.cancelDialog();
+ }
+}
+
+function waitForSingleNotification(aCallback) {
+ return Task.spawn(function* () {
+ while (PopupNotifications.panel.childNodes.length == 2) {
+ yield new Promise(resolve => executeSoon(resolve));
+
+ info("Waiting for single notification");
+ // Notification should never close while we wait
+ ok(PopupNotifications.isPanelOpen, "Notification should still be open");
+ }
+ });
+}
+
+function setupRedirect(aSettings) {
+ var url = "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/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);
+}
+
+function getInstalls() {
+ return new Promise(resolve => {
+ AddonManager.getAllInstalls(installs => resolve(installs));
+ });
+}
+
+var TESTS = [
+function test_disabledInstall() {
+ return Task.spawn(function* () {
+ Services.prefs.setBoolPref("xpinstall.enabled", false);
+
+ let notificationPromise = waitForNotification("xpinstall-disabled");
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "amosigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.button.label, "Enable", "Should have seen the right button");
+ is(notification.getAttribute("label"),
+ "Software installation is currently disabled. Click Enable and try again.");
+
+ let closePromise = waitForNotificationClose();
+ // Click on Enable
+ EventUtils.synthesizeMouseAtCenter(notification.button, {});
+ yield closePromise;
+
+ try {
+ ok(Services.prefs.getBoolPref("xpinstall.enabled"), "Installation should be enabled");
+ }
+ catch (e) {
+ ok(false, "xpinstall.enabled should be set");
+ }
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ let installs = yield getInstalls();
+ is(installs.length, 0, "Shouldn't be any pending installs");
+ });
+},
+
+function test_blockedInstall() {
+ return Task.spawn(function* () {
+ let notificationPromise = waitForNotification("addon-install-blocked");
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "amosigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.button.label, "Allow", "Should have seen the right button");
+ is(notification.getAttribute("origin"), "example.com",
+ "Should have seen the right origin host");
+ is(notification.getAttribute("label"),
+ gApp + " prevented this site from asking you to install software on your computer.",
+ "Should have seen the right message");
+
+ let dialogPromise = waitForInstallDialog();
+ // Click on Allow
+ EventUtils.synthesizeMouse(notification.button, 20, 10, {});
+ // Notification should have changed to progress notification
+ ok(PopupNotifications.isPanelOpen, "Notification should still be open");
+ notification = panel.childNodes[0];
+ is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
+ yield dialogPromise;
+
+ notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ panel = yield notificationPromise;
+
+ notification = panel.childNodes[0];
+ is(notification.button.label, "Restart Now", "Should have seen the right button");
+ is(notification.getAttribute("label"),
+ "XPI Test will be installed after you restart " + gApp + ".",
+ "Should have seen the right message");
+
+ let installs = yield getInstalls();
+ is(installs.length, 1, "Should be one pending install");
+ installs[0].cancel();
+ yield removeTab();
+ });
+},
+
+function test_whitelistedInstall() {
+ return Task.spawn(function* () {
+ let originalTab = gBrowser.selectedTab;
+ let tab;
+ gBrowser.selectedTab = originalTab;
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "amosigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?"
+ + triggers).then(newTab => tab = newTab);
+ yield progressPromise;
+ yield dialogPromise;
+ yield BrowserTestUtils.waitForCondition(() => !!tab, "tab should be present");
+
+ is(gBrowser.selectedTab, tab,
+ "tab selected in response to the addon-install-confirmation notification");
+
+ let notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.button.label, "Restart Now", "Should have seen the right button");
+ is(notification.getAttribute("label"),
+ "XPI Test will be installed after you restart " + gApp + ".",
+ "Should have seen the right message");
+
+ let installs = yield getInstalls();
+ is(installs.length, 1, "Should be one pending install");
+ installs[0].cancel();
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_failedDownload() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let failPromise = waitForNotification("addon-install-failed");
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "missing.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ let panel = yield failPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.getAttribute("label"),
+ "The add-on could not be downloaded because of a connection failure.",
+ "Should have seen the right message");
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_corruptFile() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let failPromise = waitForNotification("addon-install-failed");
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "corrupt.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ let panel = yield failPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.getAttribute("label"),
+ "The add-on downloaded from this site could not be installed " +
+ "because it appears to be corrupt.",
+ "Should have seen the right message");
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_incompatible() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let failPromise = waitForNotification("addon-install-failed");
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "incompatible.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ let panel = yield failPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.getAttribute("label"),
+ "XPI Test could not be installed because it is not compatible with " +
+ gApp + " " + gVersion + ".",
+ "Should have seen the right message");
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_restartless() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "restartless.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ let notificationPromise = waitForNotification("addon-install-complete");
+ acceptInstallDialog();
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.getAttribute("label"),
+ "XPI Test has been installed successfully.",
+ "Should have seen the right message");
+
+ let installs = yield getInstalls();
+ is(installs.length, 0, "Should be no pending installs");
+
+ let addon = yield new Promise(resolve => {
+ AddonManager.getAddonByID("restartless-xpi@tests.mozilla.org", result => {
+ resolve(result);
+ });
+ });
+ addon.uninstall();
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+
+ let closePromise = waitForNotificationClose();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ yield closePromise;
+ });
+},
+
+function test_multiple() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": "amosigned.xpi",
+ "Restartless XPI": "restartless.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ let panel = yield progressPromise;
+ yield dialogPromise;
+
+ let notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.button.label, "Restart Now", "Should have seen the right button");
+ is(notification.getAttribute("label"),
+ "2 add-ons will be installed after you restart " + gApp + ".",
+ "Should have seen the right message");
+
+ let installs = yield getInstalls();
+ is(installs.length, 1, "Should be one pending install");
+ installs[0].cancel();
+
+ let addon = yield new Promise(resolve => {
+ AddonManager.getAddonByID("restartless-xpi@tests.mozilla.org", function (result) {
+ resolve(result);
+ });
+ });
+ addon.uninstall();
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_sequential() {
+ return Task.spawn(function* () {
+ // This test is only relevant if using the new doorhanger UI
+ if (!Preferences.get("xpinstall.customConfirmationUI", false)) {
+ return;
+ }
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "Restartless XPI": "restartless.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ // Should see the right add-on
+ let container = document.getElementById("addon-install-confirmation-content");
+ is(container.childNodes.length, 1, "Should be one item listed");
+ is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
+
+ progressPromise = waitForProgressNotification(true, 2);
+ triggers = encodeURIComponent(JSON.stringify({
+ "Theme XPI": "theme.xpi"
+ }));
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+
+ // Should still have the right add-on in the confirmation notification
+ is(container.childNodes.length, 1, "Should be one item listed");
+ is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
+
+ // Wait for the install to complete, we won't see a new confirmation
+ // notification
+ yield new Promise(resolve => {
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(observer, "addon-install-confirmation");
+ resolve();
+ }, "addon-install-confirmation", false);
+ });
+
+ // Make sure browser-addons.js executes first
+ yield new Promise(resolve => executeSoon(resolve));
+
+ // Should have dropped the progress notification
+ is(PopupNotifications.panel.childNodes.length, 1, "Should be the right number of notifications");
+ is(PopupNotifications.panel.childNodes[0].id, "addon-install-confirmation-notification",
+ "Should only be showing one install confirmation");
+
+ // Should still have the right add-on in the confirmation notification
+ is(container.childNodes.length, 1, "Should be one item listed");
+ is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
+
+ cancelInstallDialog();
+
+ ok(PopupNotifications.isPanelOpen, "Panel should still be open");
+ is(PopupNotifications.panel.childNodes.length, 1, "Should be the right number of notifications");
+ is(PopupNotifications.panel.childNodes[0].id, "addon-install-confirmation-notification",
+ "Should still have an install confirmation open");
+
+ // Should have the next add-on's confirmation dialog
+ is(container.childNodes.length, 1, "Should be one item listed");
+ is(container.childNodes[0].firstChild.getAttribute("value"), "Theme Test", "Should have the right add-on");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+ let closePromise = waitForNotificationClose();
+ cancelInstallDialog();
+ yield closePromise;
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+},
+
+function test_someUnverified() {
+ return Task.spawn(function* () {
+ // This test is only relevant if using the new doorhanger UI and allowing
+ // unsigned add-ons
+ if (!Preferences.get("xpinstall.customConfirmationUI", false) ||
+ Preferences.get("xpinstall.signatures.required", true) ||
+ REQUIRE_SIGNING) {
+ return;
+ }
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "Extension XPI": "restartless-unsigned.xpi",
+ "Theme XPI": "theme.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ let notification = document.getElementById("addon-install-confirmation-notification");
+ let message = notification.getAttribute("label");
+ is(message, "Caution: This site would like to install 2 add-ons in " + gApp +
+ ", some of which are unverified. Proceed at your own risk.",
+ "Should see the right message");
+
+ let container = document.getElementById("addon-install-confirmation-content");
+ is(container.childNodes.length, 2, "Should be two items listed");
+ is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
+ is(container.childNodes[0].lastChild.getAttribute("class"),
+ "addon-install-confirmation-unsigned", "Should have the unverified marker");
+ is(container.childNodes[1].firstChild.getAttribute("value"), "Theme Test", "Should have the right add-on");
+ is(container.childNodes[1].childNodes.length, 1, "Shouldn't have the unverified marker");
+
+ let notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ yield notificationPromise;
+
+ let [addon, theme] = yield new Promise(resolve => {
+ AddonManager.getAddonsByIDs(["restartless-xpi@tests.mozilla.org",
+ "theme-xpi@tests.mozilla.org"],
+ function(addons) {
+ resolve(addons);
+ });
+ });
+ addon.uninstall();
+ // Installing a new theme tries to switch to it, switch back to the
+ // default theme.
+ theme.userDisabled = true;
+ theme.uninstall();
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_allUnverified() {
+ return Task.spawn(function* () {
+ // This test is only relevant if using the new doorhanger UI and allowing
+ // unsigned add-ons
+ if (!Preferences.get("xpinstall.customConfirmationUI", false) ||
+ Preferences.get("xpinstall.signatures.required", true) ||
+ REQUIRE_SIGNING) {
+ return;
+ }
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "Extension XPI": "restartless-unsigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ let notification = document.getElementById("addon-install-confirmation-notification");
+ let message = notification.getAttribute("label");
+ is(message, "Caution: This site would like to install an unverified add-on in " + gApp + ". Proceed at your own risk.");
+
+ let container = document.getElementById("addon-install-confirmation-content");
+ is(container.childNodes.length, 1, "Should be one item listed");
+ is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
+ is(container.childNodes[0].childNodes.length, 1, "Shouldn't have the unverified marker");
+
+ let notificationPromise = waitForNotification("addon-install-complete");
+ acceptInstallDialog();
+ yield notificationPromise;
+
+ let addon = yield new Promise(resolve => {
+ AddonManager.getAddonByID("restartless-xpi@tests.mozilla.org", function(result) {
+ resolve(result);
+ });
+ });
+ addon.uninstall();
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_url() {
+ return Task.spawn(function* () {
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.loadURI(TESTROOT + "amosigned.xpi");
+ yield progressPromise;
+ yield dialogPromise;
+
+ let notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.button.label, "Restart Now", "Should have seen the right button");
+ is(notification.getAttribute("label"),
+ "XPI Test will be installed after you restart " + gApp + ".",
+ "Should have seen the right message");
+
+ let installs = yield getInstalls();
+ is(installs.length, 1, "Should be one pending install");
+ installs[0].cancel();
+
+ yield removeTab();
+ });
+},
+
+function test_localFile() {
+ return Task.spawn(function* () {
+ let cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIChromeRegistry);
+ let path;
+ try {
+ path = cr.convertChromeURL(makeURI(CHROMEROOT + "corrupt.xpi")).spec;
+ } catch (ex) {
+ path = CHROMEROOT + "corrupt.xpi";
+ }
+
+ let failPromise = new Promise(resolve => {
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(observer, "addon-install-failed");
+ resolve();
+ }, "addon-install-failed", false);
+ });
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.loadURI(path);
+ yield failPromise;
+
+ // Wait for the browser code to add the failure notification
+ yield waitForSingleNotification();
+
+ let notification = PopupNotifications.panel.childNodes[0];
+ is(notification.id, "addon-install-failed-notification", "Should have seen the install fail");
+ is(notification.getAttribute("label"),
+ "This add-on could not be installed because it appears to be corrupt.",
+ "Should have seen the right message");
+
+ yield removeTab();
+ });
+},
+
+function test_tabClose() {
+ return Task.spawn(function* () {
+ if (!Preferences.get("xpinstall.customConfirmationUI", false)) {
+ runNextTest();
+ return;
+ }
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.loadURI(TESTROOT + "amosigned.xpi");
+ yield progressPromise;
+ yield dialogPromise;
+
+ let installs = yield getInstalls();
+ is(installs.length, 1, "Should be one pending install");
+
+ let closePromise = waitForNotificationClose();
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ yield closePromise;
+
+ installs = yield getInstalls();
+ is(installs.length, 0, "Should be no pending install since the tab is closed");
+ });
+},
+
+// Add-ons should be cancelled and the install notification destroyed when
+// navigating to a new origin
+function test_tabNavigate() {
+ return Task.spawn(function* () {
+ if (!Preferences.get("xpinstall.customConfirmationUI", false)) {
+ return;
+ }
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "Extension XPI": "amosigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ let closePromise = waitForNotificationClose();
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.loadURI("about:blank");
+ yield closePromise;
+
+ let installs = yield getInstalls();
+ is(installs.length, 0, "Should be no pending install");
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield loadPromise;
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+},
+
+function test_urlBar() {
+ return Task.spawn(function* () {
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gURLBar.value = TESTROOT + "amosigned.xpi";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ yield progressPromise;
+ let installDialog = yield dialogPromise;
+
+ let notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog(installDialog);
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.button.label, "Restart Now", "Should have seen the right button");
+ is(notification.getAttribute("label"),
+ "XPI Test will be installed after you restart " + gApp + ".",
+ "Should have seen the right message");
+
+ let installs = yield getInstalls();
+ is(installs.length, 1, "Should be one pending install");
+ installs[0].cancel();
+
+ yield removeTab();
+ });
+},
+
+function test_wrongHost() {
+ return Task.spawn(function* () {
+ let requestedUrl = TESTROOT2 + "enabled.html";
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, requestedUrl);
+ gBrowser.loadURI(TESTROOT2 + "enabled.html");
+ yield loadedPromise;
+
+ let progressPromise = waitForProgressNotification();
+ let notificationPromise = waitForNotification("addon-install-failed");
+ gBrowser.loadURI(TESTROOT + "corrupt.xpi");
+ yield progressPromise;
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.getAttribute("label"),
+ "The add-on downloaded from this site could not be installed " +
+ "because it appears to be corrupt.",
+ "Should have seen the right message");
+
+ yield removeTab();
+ });
+},
+
+function test_reload() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": "amosigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ let notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.button.label, "Restart Now", "Should have seen the right button");
+ is(notification.getAttribute("label"),
+ "XPI Test will be installed after you restart " + gApp + ".",
+ "Should have seen the right message");
+
+ function testFail() {
+ ok(false, "Reloading should not have hidden the notification");
+ }
+ PopupNotifications.panel.addEventListener("popuphiding", testFail, false);
+ let requestedUrl = TESTROOT2 + "enabled.html";
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, requestedUrl);
+ gBrowser.loadURI(TESTROOT2 + "enabled.html");
+ yield loadedPromise;
+ PopupNotifications.panel.removeEventListener("popuphiding", testFail, false);
+
+ let installs = yield getInstalls();
+ is(installs.length, 1, "Should be one pending install");
+ installs[0].cancel();
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_theme() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "Theme XPI": "theme.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ let notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ is(notification.button.label, "Restart Now", "Should have seen the right button");
+ is(notification.getAttribute("label"),
+ "Theme Test will be installed after you restart " + gApp + ".",
+ "Should have seen the right message");
+
+ let addon = yield new Promise(resolve => {
+ AddonManager.getAddonByID("{972ce4c6-7e08-4474-a285-3208198ce6fd}", function(result) {
+ resolve(result);
+ });
+ });
+ ok(addon.userDisabled, "Should be switching away from the default theme.");
+ // Undo the pending theme switch
+ addon.userDisabled = false;
+
+ addon = yield new Promise(resolve => {
+ AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(result) {
+ resolve(result);
+ });
+ });
+ isnot(addon, null, "Test theme will have been installed");
+ addon.uninstall();
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_renotifyBlocked() {
+ return Task.spawn(function* () {
+ let notificationPromise = waitForNotification("addon-install-blocked");
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "amosigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ let panel = yield notificationPromise;
+
+ let closePromise = waitForNotificationClose();
+ // hide the panel (this simulates the user dismissing it)
+ panel.hidePopup();
+ yield closePromise;
+
+ info("Timeouts after this probably mean bug 589954 regressed");
+
+ yield new Promise(resolve => executeSoon(resolve));
+
+ notificationPromise = waitForNotification("addon-install-blocked");
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+ yield notificationPromise;
+
+ let installs = yield getInstalls();
+ is(installs.length, 2, "Should be two pending installs");
+
+ closePromise = waitForNotificationClose();
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ yield closePromise;
+
+ installs = yield getInstalls();
+ is(installs.length, 0, "Should have cancelled the installs");
+ });
+},
+
+function test_renotifyInstalled() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let progressPromise = waitForProgressNotification();
+ let dialogPromise = waitForInstallDialog();
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "amosigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ // Wait for the complete notification
+ let notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ let panel = yield notificationPromise;
+
+ let closePromise = waitForNotificationClose();
+ // hide the panel (this simulates the user dismissing it)
+ panel.hidePopup();
+ yield closePromise;
+
+ // Install another
+ yield new Promise(resolve => executeSoon(resolve));
+
+ progressPromise = waitForProgressNotification();
+ dialogPromise = waitForInstallDialog();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+ yield progressPromise;
+ yield dialogPromise;
+
+ info("Timeouts after this probably mean bug 589954 regressed");
+
+ // Wait for the complete notification
+ notificationPromise = waitForNotification("addon-install-restart");
+ acceptInstallDialog();
+ yield notificationPromise;
+
+ let installs = yield getInstalls();
+ is(installs.length, 1, "Should be one pending installs");
+ installs[0].cancel();
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield removeTab();
+ });
+},
+
+function test_cancel() {
+ return Task.spawn(function* () {
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ let notificationPromise = waitForNotification(PROGRESS_NOTIFICATION);
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "slowinstall.sjs?file=amosigned.xpi"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ // Close the notification
+ let anchor = document.getElementById("addons-notification-icon");
+ anchor.click();
+ // Reopen the notification
+ anchor.click();
+
+ ok(PopupNotifications.isPanelOpen, "Notification should still be open");
+ is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
+ notification = panel.childNodes[0];
+ is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
+ let button = document.getElementById("addon-progress-cancel");
+
+ // Cancel the download
+ let install = notification.notification.options.installs[0];
+ let cancelledPromise = new Promise(resolve => {
+ install.addListener({
+ onDownloadCancelled: function() {
+ install.removeListener(this);
+ resolve();
+ }
+ });
+ });
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ yield cancelledPromise;
+
+ yield new Promise(resolve => executeSoon(resolve));
+
+ ok(!PopupNotifications.isPanelOpen, "Notification should be closed");
+
+ let installs = yield getInstalls();
+ is(installs.length, 0, "Should be no pending install");
+
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+},
+
+function test_failedSecurity() {
+ return Task.spawn(function* () {
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+ setupRedirect({
+ "Location": TESTROOT + "amosigned.xpi"
+ });
+
+ let notificationPromise = waitForNotification("addon-install-blocked");
+ let triggers = encodeURIComponent(JSON.stringify({
+ "XPI": "redirect.sjs?mode=redirect"
+ }));
+ BrowserTestUtils.openNewForegroundTab(gBrowser, SECUREROOT + "installtrigger.html?" + triggers);
+ let panel = yield notificationPromise;
+
+ let notification = panel.childNodes[0];
+ // Click on Allow
+ EventUtils.synthesizeMouse(notification.button, 20, 10, {});
+
+ // Notification should have changed to progress notification
+ ok(PopupNotifications.isPanelOpen, "Notification should still be open");
+ is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
+ notification = panel.childNodes[0];
+ is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
+
+ // Wait for it to fail
+ yield new Promise(resolve => {
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(observer, "addon-install-failed");
+ resolve();
+ }, "addon-install-failed", false);
+ });
+
+ // Allow the browser code to add the failure notification and then wait
+ // for the progress notification to dismiss itself
+ yield waitForSingleNotification();
+ is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
+ notification = panel.childNodes[0];
+ is(notification.id, "addon-install-failed-notification", "Should have seen the install fail");
+
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true);
+ yield removeTab();
+ });
+}
+];
+
+var gTestStart = null;
+
+var XPInstallObserver = {
+ observe: function (aSubject, aTopic, aData) {
+ var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
+ info("Observed " + aTopic + " for " + installInfo.installs.length + " installs");
+ installInfo.installs.forEach(function(aInstall) {
+ info("Install of " + aInstall.sourceURI.spec + " was in state " + aInstall.state);
+ });
+ }
+};
+
+add_task(function* () {
+ requestLongerTimeout(4);
+
+ Services.prefs.setBoolPref("extensions.logging.enabled", true);
+ Services.prefs.setBoolPref("extensions.strictCompatibility", true);
+ Services.prefs.setBoolPref("extensions.install.requireSecureOrigin", false);
+ Services.prefs.setIntPref("security.dialog_enable_delay", 0);
+
+ Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
+ Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
+ Services.obs.addObserver(XPInstallObserver, "addon-install-failed", false);
+ Services.obs.addObserver(XPInstallObserver, "addon-install-complete", false);
+
+ registerCleanupFunction(function() {
+ // Make sure no more test parts run in case we were timed out
+ TESTS = [];
+
+ AddonManager.getAllInstalls(function(aInstalls) {
+ aInstalls.forEach(function(aInstall) {
+ aInstall.cancel();
+ });
+ });
+
+ Services.prefs.clearUserPref("extensions.logging.enabled");
+ Services.prefs.clearUserPref("extensions.strictCompatibility");
+ Services.prefs.clearUserPref("extensions.install.requireSecureOrigin");
+ Services.prefs.clearUserPref("security.dialog_enable_delay");
+
+ Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
+ Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
+ Services.obs.removeObserver(XPInstallObserver, "addon-install-failed");
+ Services.obs.removeObserver(XPInstallObserver, "addon-install-complete");
+ });
+
+ for (let i = 0; i < TESTS.length; ++i) {
+ if (gTestStart)
+ info("Test part took " + (Date.now() - gTestStart) + "ms");
+
+ ok(!PopupNotifications.isPanelOpen, "Notification should be closed");
+
+ let installs = yield new Promise(resolve => {
+ AddonManager.getAllInstalls(function(aInstalls) {
+ resolve(aInstalls);
+ });
+ });
+
+ is(installs.length, 0, "Should be no active installs");
+ info("Running " + TESTS[i].name);
+ gTestStart = Date.now();
+ yield TESTS[i]();
+ }
+});
diff --git a/browser/base/content/test/general/browser_bug555224.js b/browser/base/content/test/general/browser_bug555224.js
new file mode 100644
index 000000000..d27bf0040
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug555224.js
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 TEST_PAGE = "/browser/browser/base/content/test/general/dummy_page.html";
+var gTestTab, gBgTab, gTestZoom;
+
+function testBackgroundLoad() {
+ Task.spawn(function* () {
+ is(ZoomManager.zoom, gTestZoom, "opening a background tab should not change foreground zoom");
+
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gBgTab);
+
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gTestTab);
+ finish();
+ });
+}
+
+function testInitialZoom() {
+ Task.spawn(function* () {
+ is(ZoomManager.zoom, 1, "initial zoom level should be 1");
+ FullZoom.enlarge();
+
+ gTestZoom = ZoomManager.zoom;
+ isnot(gTestZoom, 1, "zoom level should have changed");
+
+ gBgTab = gBrowser.addTab();
+ yield FullZoomHelper.load(gBgTab, "http://mochi.test:8888" + TEST_PAGE);
+ }).then(testBackgroundLoad, FullZoomHelper.failAndContinue(finish));
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ gTestTab = gBrowser.addTab();
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTestTab);
+ yield FullZoomHelper.load(gTestTab, "http://example.org" + TEST_PAGE);
+ }).then(testInitialZoom, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/general/browser_bug555767.js b/browser/base/content/test/general/browser_bug555767.js
new file mode 100644
index 000000000..bc774f7dc
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug555767.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/. */
+
+ add_task(function* () {
+ let testURL = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+ let tabSelected = false;
+
+ // Open the base tab
+ let baseTab = gBrowser.addTab(testURL);
+
+ // Wait for the tab to be fully loaded so matching happens correctly
+ yield promiseTabLoaded(baseTab);
+ if (baseTab.linkedBrowser.currentURI.spec == "about:blank")
+ return;
+ baseTab.linkedBrowser.removeEventListener("load", arguments.callee, true);
+
+ let testTab = gBrowser.addTab();
+
+ // Select the testTab
+ gBrowser.selectedTab = testTab;
+
+ // Set the urlbar to include the moz-action
+ gURLBar.value = "moz-action:switchtab," + JSON.stringify({url: testURL});
+ // Focus the urlbar so we can press enter
+ gURLBar.focus();
+
+ // Functions for TabClose and TabSelect
+ function onTabClose(aEvent) {
+ gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, false);
+ // Make sure we get the TabClose event for testTab
+ is(aEvent.originalTarget, testTab, "Got the TabClose event for the right tab");
+ // Confirm that we did select the tab
+ ok(tabSelected, "Confirming that the tab was selected");
+ gBrowser.removeTab(baseTab);
+ finish();
+ }
+ function onTabSelect(aEvent) {
+ gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect, false);
+ // Make sure we got the TabSelect event for baseTab
+ is(aEvent.originalTarget, baseTab, "Got the TabSelect event for the right tab");
+ // Confirm that the selected tab is in fact base tab
+ is(gBrowser.selectedTab, baseTab, "We've switched to the correct tab");
+ tabSelected = true;
+ }
+
+ // Add the TabClose, TabSelect event listeners before we press enter
+ gBrowser.tabContainer.addEventListener("TabClose", onTabClose, false);
+ gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect, false);
+
+ // Press enter!
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ });
+
diff --git a/browser/base/content/test/general/browser_bug559991.js b/browser/base/content/test/general/browser_bug559991.js
new file mode 100644
index 000000000..b1516a8b4
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug559991.js
@@ -0,0 +1,42 @@
+var tab;
+
+function test() {
+
+ // ----------
+ // Test setup
+
+ waitForExplicitFinish();
+
+ gPrefService.setBoolPref("browser.zoom.updateBackgroundTabs", true);
+ gPrefService.setBoolPref("browser.zoom.siteSpecific", true);
+
+ let uri = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+
+ Task.spawn(function* () {
+ tab = gBrowser.addTab();
+ yield FullZoomHelper.load(tab, uri);
+
+ // -------------------------------------------------------------------
+ // Test - Trigger a tab switch that should update the zoom level
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(tab);
+ ok(true, "applyPrefToSetting was called");
+ }).then(endTest, FullZoomHelper.failAndContinue(endTest));
+}
+
+// -------------
+// Test clean-up
+function endTest() {
+ Task.spawn(function* () {
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(tab);
+
+ tab = null;
+
+ if (gPrefService.prefHasUserValue("browser.zoom.updateBackgroundTabs"))
+ gPrefService.clearUserPref("browser.zoom.updateBackgroundTabs");
+
+ if (gPrefService.prefHasUserValue("browser.zoom.siteSpecific"))
+ gPrefService.clearUserPref("browser.zoom.siteSpecific");
+
+ finish();
+ });
+}
diff --git a/browser/base/content/test/general/browser_bug561636.js b/browser/base/content/test/general/browser_bug561636.js
new file mode 100644
index 000000000..69bc475c3
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug561636.js
@@ -0,0 +1,370 @@
+var gInvalidFormPopup = document.getElementById('invalid-form-popup');
+ok(gInvalidFormPopup,
+ "The browser should have a popup to show when a form is invalid");
+
+function checkPopupShow()
+{
+ ok(gInvalidFormPopup.state == 'showing' || gInvalidFormPopup.state == 'open',
+ "[Test " + testId + "] The invalid form popup should be shown");
+}
+
+function checkPopupHide()
+{
+ ok(gInvalidFormPopup.state != 'showing' && gInvalidFormPopup.state != 'open',
+ "[Test " + testId + "] The invalid form popup should not be shown");
+}
+
+var gObserver = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
+
+ notifyInvalidSubmit : function (aFormElement, aInvalidElements)
+ {
+ }
+};
+
+var testId = 0;
+
+function incrementTest()
+{
+ testId++;
+ info("Starting next part of test");
+}
+
+function getDocHeader()
+{
+ return "<html><head><meta charset='utf-8'></head><body>" + getEmptyFrame();
+}
+
+function getDocFooter()
+{
+ return "</body></html>";
+}
+
+function getEmptyFrame()
+{
+ return "<iframe style='width:100px; height:30px; margin:3px; border:1px solid lightgray;' " +
+ "name='t' srcdoc=\"<html><head><meta charset='utf-8'></head><body>form target</body></html>\"></iframe>";
+}
+
+function* openNewTab(uri, background)
+{
+ let tab = gBrowser.addTab();
+ let browser = gBrowser.getBrowserForTab(tab);
+ if (!background) {
+ gBrowser.selectedTab = tab;
+ }
+ yield promiseTabLoadEvent(tab, "data:text/html," + escape(uri));
+ return browser;
+}
+
+function* clickChildElement(browser)
+{
+ yield ContentTask.spawn(browser, {}, function* () {
+ content.document.getElementById('s').click();
+ });
+}
+
+function* blurChildElement(browser)
+{
+ yield ContentTask.spawn(browser, {}, function* () {
+ content.document.getElementById('i').blur();
+ });
+}
+
+function* checkChildFocus(browser, message)
+{
+ yield ContentTask.spawn(browser, [message, testId], function* (args) {
+ let [msg, id] = args;
+ var focused = content.document.activeElement == content.document.getElementById('i');
+
+ var validMsg = true;
+ if (msg) {
+ validMsg = (msg == content.document.getElementById('i').validationMessage);
+ }
+
+ Assert.equal(focused, true, "Test " + id + " First invalid element should be focused");
+ Assert.equal(validMsg, true, "Test " + id + " The panel should show the message from validationMessage");
+ });
+}
+
+/**
+ * In this test, we check that no popup appears if the form is valid.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ yield clickChildElement(browser);
+
+ yield new Promise((resolve, reject) => {
+ // XXXndeakin This isn't really going to work when the content is another process
+ executeSoon(function() {
+ checkPopupHide();
+ resolve();
+ });
+ });
+
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * In this test, we check that, when an invalid form is submitted,
+ * the invalid element is focused and a popup appears.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input required id='i'><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * In this test, we check that, when an invalid form is submitted,
+ * the first invalid element is focused and a popup appears.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input><input id='i' required><input required><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * In this test, we check that, we hide the popup by interacting with the
+ * invalid element if the element becomes valid.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+
+ let popupHiddenPromise = promiseWaitForEvent(gInvalidFormPopup, "popuphidden");
+ EventUtils.synthesizeKey("a", {});
+ yield popupHiddenPromise;
+
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * In this test, we check that, we don't hide the popup by interacting with the
+ * invalid element if the element is still invalid.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input type='email' id='i' required><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+
+ yield new Promise((resolve, reject) => {
+ EventUtils.synthesizeKey("a", {});
+ executeSoon(function() {
+ checkPopupShow();
+ resolve();
+ })
+ });
+
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * In this test, we check that we can hide the popup by blurring the invalid
+ * element.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+
+ let popupHiddenPromise = promiseWaitForEvent(gInvalidFormPopup, "popuphidden");
+ yield blurChildElement(browser);
+ yield popupHiddenPromise;
+
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * In this test, we check that we can hide the popup by pressing TAB.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+
+ let popupHiddenPromise = promiseWaitForEvent(gInvalidFormPopup, "popuphidden");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ yield popupHiddenPromise;
+
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * In this test, we check that the popup will hide if we move to another tab.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>" + getDocFooter();
+ let browser1 = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser1);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser1, gInvalidFormPopup.firstChild.textContent);
+
+ let popupHiddenPromise = promiseWaitForEvent(gInvalidFormPopup, "popuphidden");
+
+ let browser2 = yield openNewTab("data:text/html,<html></html>");
+ yield popupHiddenPromise;
+
+ gBrowser.removeTab(gBrowser.getTabForBrowser(browser1));
+ gBrowser.removeTab(gBrowser.getTabForBrowser(browser2));
+});
+
+/**
+ * In this test, we check that nothing happen if the invalid form is
+ * submitted in a background tab.
+ */
+add_task(function* ()
+{
+ // Observers don't propagate currently across processes. We may add support for this in the
+ // future via the addon compat layer.
+ if (gMultiProcessBrowser) {
+ return;
+ }
+
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri, true);
+ isnot(gBrowser.selectedBrowser, browser, "This tab should have been loaded in background");
+
+ let notifierPromise = new Promise((resolve, reject) => {
+ gObserver.notifyInvalidSubmit = function() {
+ executeSoon(function() {
+ checkPopupHide();
+
+ // Clean-up
+ Services.obs.removeObserver(gObserver, "invalidformsubmit");
+ gObserver.notifyInvalidSubmit = function () {};
+ resolve();
+ });
+ };
+
+ Services.obs.addObserver(gObserver, "invalidformsubmit", false);
+
+ executeSoon(function () {
+ browser.contentDocument.getElementById('s').click();
+ });
+ });
+
+ yield notifierPromise;
+
+ gBrowser.removeTab(gBrowser.getTabForBrowser(browser));
+});
+
+/**
+ * In this test, we check that the author defined error message is shown.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input x-moz-errormessage='foo' required id='i'><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+
+ is(gInvalidFormPopup.firstChild.textContent, "foo",
+ "The panel should show the author defined error message");
+
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * In this test, we check that the message is correctly updated when it changes.
+ */
+add_task(function* ()
+{
+ incrementTest();
+ let uri = getDocHeader() + "<form target='t' action='data:text/html,'><input type='email' required id='i'><input id='s' type='submit'></form>" + getDocFooter();
+ let browser = yield openNewTab(uri);
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+ yield clickChildElement(browser);
+ yield popupShownPromise;
+
+ checkPopupShow();
+ yield checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+
+ let inputPromise = promiseWaitForEvent(gBrowser.contentDocument.getElementById('i'), "input");
+ EventUtils.synthesizeKey('f', {});
+ yield inputPromise;
+
+ // Now, the element suffers from another error, the message should have
+ // been updated.
+ yield new Promise((resolve, reject) => {
+ // XXXndeakin This isn't really going to work when the content is another process
+ executeSoon(function() {
+ checkChildFocus(browser, gInvalidFormPopup.firstChild.textContent);
+ resolve();
+ });
+ });
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug563588.js b/browser/base/content/test/general/browser_bug563588.js
new file mode 100644
index 000000000..a1774fb7e
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug563588.js
@@ -0,0 +1,30 @@
+function press(key, expectedPos) {
+ var originalSelectedTab = gBrowser.selectedTab;
+ EventUtils.synthesizeKey("VK_" + key.toUpperCase(), { accelKey: true });
+ is(gBrowser.selectedTab, originalSelectedTab,
+ "accel+" + key + " doesn't change which tab is selected");
+ is(gBrowser.tabContainer.selectedIndex, expectedPos,
+ "accel+" + key + " moves the tab to the expected position");
+ is(document.activeElement, gBrowser.selectedTab,
+ "accel+" + key + " leaves the selected tab focused");
+}
+
+function test() {
+ gBrowser.addTab();
+ gBrowser.addTab();
+ is(gBrowser.tabs.length, 3, "got three tabs");
+ is(gBrowser.tabs[0], gBrowser.selectedTab, "first tab is selected");
+
+ gBrowser.selectedTab.focus();
+ is(document.activeElement, gBrowser.selectedTab, "selected tab is focused");
+
+ press("right", 1);
+ press("down", 2);
+ press("left", 1);
+ press("up", 0);
+ press("end", 2);
+ press("home", 0);
+
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+}
diff --git a/browser/base/content/test/general/browser_bug565575.js b/browser/base/content/test/general/browser_bug565575.js
new file mode 100644
index 000000000..3555a2e7f
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug565575.js
@@ -0,0 +1,14 @@
+add_task(function* () {
+ gBrowser.selectedBrowser.focus();
+
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => BrowserOpenTab(), false);
+ ok(gURLBar.focused, "location bar is focused for a new tab");
+
+ yield BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+ ok(!gURLBar.focused, "location bar isn't focused for the previously selected tab");
+
+ yield BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[1]);
+ ok(gURLBar.focused, "location bar is re-focused when selecting the new tab");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug567306.js b/browser/base/content/test/general/browser_bug567306.js
new file mode 100644
index 000000000..742ff6726
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug567306.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var {Ci: interfaces, Cc: classes} = Components;
+
+var Clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
+var HasFindClipboard = Clipboard.supportsFindClipboard();
+
+add_task(function* () {
+ let newwindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ let selectedBrowser = newwindow.gBrowser.selectedBrowser;
+ yield new Promise((resolve, reject) => {
+ selectedBrowser.addEventListener("pageshow", function pageshowListener() {
+ if (selectedBrowser.currentURI.spec == "about:blank")
+ return;
+
+ selectedBrowser.removeEventListener("pageshow", pageshowListener, true);
+ ok(true, "pageshow listener called: " + newwindow.content.location);
+ resolve();
+ }, true);
+ selectedBrowser.loadURI("data:text/html,<h1 id='h1'>Select Me</h1>");
+ });
+
+ yield SimpleTest.promiseFocus(newwindow);
+
+ ok(!newwindow.gFindBarInitialized, "find bar is not yet initialized");
+ let findBar = newwindow.gFindBar;
+
+ yield ContentTask.spawn(selectedBrowser, { }, function* () {
+ let elt = content.document.getElementById("h1");
+ let selection = content.getSelection();
+ let range = content.document.createRange();
+ range.setStart(elt, 0);
+ range.setEnd(elt, 1);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ });
+
+ yield findBar.onFindCommand();
+
+ // When the OS supports the Find Clipboard (OSX), the find field value is
+ // persisted across Fx sessions, thus not useful to test.
+ if (!HasFindClipboard)
+ is(findBar._findField.value, "Select Me", "Findbar is initialized with selection");
+ findBar.close();
+ yield promiseWindowClosed(newwindow);
+});
+
diff --git a/browser/base/content/test/general/browser_bug575561.js b/browser/base/content/test/general/browser_bug575561.js
new file mode 100644
index 000000000..b6d17a447
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug575561.js
@@ -0,0 +1,97 @@
+requestLongerTimeout(2);
+
+const TEST_URL = "http://example.com/browser/browser/base/content/test/general/app_bug575561.html";
+
+add_task(function*() {
+ SimpleTest.requestCompleteLog();
+
+ // Pinned: Link to the same domain should not open a new tab
+ // Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html
+ yield testLink(0, true, false);
+ // Pinned: Link to a different subdomain should open a new tab
+ // Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html
+ yield testLink(1, true, true);
+
+ // Pinned: Link to a different domain should open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
+ yield testLink(2, true, true);
+
+ // Not Pinned: Link to a different domain should not open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
+ yield testLink(2, false, false);
+
+ // Pinned: Targetted link should open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo"
+ yield testLink(3, true, true);
+
+ // Pinned: Link in a subframe should not open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe
+ yield testLink(0, true, false, true);
+
+ // Pinned: Link to the same domain (with www prefix) should not open a new tab
+ // Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html
+ yield testLink(4, true, false);
+
+ // Pinned: Link to a data: URI should not open a new tab
+ // Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>
+ yield testLink(5, true, false);
+
+ // Pinned: Link to an about: URI should not open a new tab
+ // Tests link to about:logo
+ yield testLink(function(doc) {
+ let link = doc.createElement("a");
+ link.textContent = "Link to Mozilla";
+ link.href = "about:logo";
+ doc.body.appendChild(link);
+ return link;
+ }, true, false, false, "about:robots");
+});
+
+var waitForPageLoad = Task.async(function*(browser, linkLocation) {
+ yield waitForDocLoadComplete();
+
+ is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab");
+});
+
+var waitForTabOpen = Task.async(function*() {
+ let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true);
+ ok(true, "Link should open a new tab");
+
+ yield waitForDocLoadComplete(event.target.linkedBrowser);
+ yield Promise.resolve();
+
+ gBrowser.removeCurrentTab();
+});
+
+var testLink = Task.async(function*(aLinkIndexOrFunction, pinTab, expectNewTab, testSubFrame, aURL = TEST_URL) {
+ let appTab = gBrowser.addTab(aURL, {skipAnimation: true});
+ if (pinTab)
+ gBrowser.pinTab(appTab);
+ gBrowser.selectedTab = appTab;
+
+ yield waitForDocLoadComplete();
+
+ let browser = appTab.linkedBrowser;
+ if (testSubFrame)
+ browser = browser.contentDocument.querySelector("iframe");
+
+ let link;
+ if (typeof aLinkIndexOrFunction == "function") {
+ link = aLinkIndexOrFunction(browser.contentDocument);
+ } else {
+ link = browser.contentDocument.querySelectorAll("a")[aLinkIndexOrFunction];
+ }
+
+ let promise;
+ if (expectNewTab)
+ promise = waitForTabOpen();
+ else
+ promise = waitForPageLoad(browser, link.href);
+
+ info("Clicking " + link.textContent);
+ link.click();
+
+ yield promise;
+
+ gBrowser.removeTab(appTab);
+});
diff --git a/browser/base/content/test/general/browser_bug575830.js b/browser/base/content/test/general/browser_bug575830.js
new file mode 100644
index 000000000..5393c08d7
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug575830.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+function test() {
+ let tab1, tab2;
+ const TEST_IMAGE = "http://example.org/browser/browser/base/content/test/general/moz.png";
+
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ tab1 = gBrowser.addTab();
+ tab2 = gBrowser.addTab();
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ yield FullZoomHelper.load(tab1, TEST_IMAGE);
+
+ is(ZoomManager.zoom, 1, "initial zoom level for first should be 1");
+
+ FullZoom.enlarge();
+ let zoom = ZoomManager.zoom;
+ isnot(zoom, 1, "zoom level should have changed");
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(tab2);
+ is(ZoomManager.zoom, 1, "initial zoom level for second tab should be 1");
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(tab1);
+ is(ZoomManager.zoom, zoom, "zoom level for first tab should not have changed");
+
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(tab1);
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(tab2);
+ }).then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/general/browser_bug577121.js b/browser/base/content/test/general/browser_bug577121.js
new file mode 100644
index 000000000..5ebfdc115
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug577121.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 test() {
+ Services.prefs.setBoolPref("browser.tabs.animate", false);
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("browser.tabs.animate");
+ });
+
+ // Open 2 other tabs, and pin the second one. Like that, the initial tab
+ // should get closed.
+ let testTab1 = gBrowser.addTab();
+ let testTab2 = gBrowser.addTab();
+ gBrowser.pinTab(testTab2);
+
+ // Now execute "Close other Tabs" on the first manually opened tab (tab1).
+ // -> tab2 ist pinned, tab1 should remain open and the initial tab should
+ // get closed.
+ gBrowser.removeAllTabsBut(testTab1);
+
+ is(gBrowser.tabs.length, 2, "there are two remaining tabs open");
+ is(gBrowser.tabs[0], testTab2, "pinned tab2 stayed open");
+ is(gBrowser.tabs[1], testTab1, "tab1 stayed open");
+
+ // Cleanup. Close only one tab because we need an opened tab at the end of
+ // the test.
+ gBrowser.removeTab(testTab2);
+}
diff --git a/browser/base/content/test/general/browser_bug578534.js b/browser/base/content/test/general/browser_bug578534.js
new file mode 100644
index 000000000..0d61cca76
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug578534.js
@@ -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/. */
+
+add_task(function* test() {
+ let uriString = "http://example.com/";
+ let cookieBehavior = "network.cookie.cookieBehavior";
+ let uriObj = Services.io.newURI(uriString, null, null)
+ let cp = Components.classes["@mozilla.org/cookie/permission;1"]
+ .getService(Components.interfaces.nsICookiePermission);
+
+ yield SpecialPowers.pushPrefEnv({ set: [[ cookieBehavior, 2 ]] });
+ cp.setAccess(uriObj, cp.ACCESS_ALLOW);
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: uriString }, function* (browser) {
+ yield ContentTask.spawn(browser, null, function() {
+ is(content.navigator.cookieEnabled, true,
+ "navigator.cookieEnabled should be true");
+ });
+ });
+
+ cp.setAccess(uriObj, cp.ACCESS_DEFAULT);
+});
diff --git a/browser/base/content/test/general/browser_bug579872.js b/browser/base/content/test/general/browser_bug579872.js
new file mode 100644
index 000000000..bc10ca0c8
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug579872.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/. */
+
+function test() {
+ let newTab = gBrowser.addTab();
+ waitForExplicitFinish();
+ BrowserTestUtils.browserLoaded(newTab.linkedBrowser).then(mainPart);
+
+ function mainPart() {
+ gBrowser.pinTab(newTab);
+ gBrowser.selectedTab = newTab;
+
+ openUILinkIn("javascript:var x=0;", "current");
+ is(gBrowser.tabs.length, 2, "Should open in current tab");
+
+ openUILinkIn("http://example.com/1", "current");
+ is(gBrowser.tabs.length, 2, "Should open in current tab");
+
+ openUILinkIn("http://example.org/", "current");
+ is(gBrowser.tabs.length, 3, "Should open in new tab");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(gBrowser.tabs[1]); // example.org tab
+ finish();
+ }
+ newTab.linkedBrowser.loadURI("http://example.com");
+}
diff --git a/browser/base/content/test/general/browser_bug580638.js b/browser/base/content/test/general/browser_bug580638.js
new file mode 100644
index 000000000..66defafe3
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug580638.js
@@ -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/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ function testState(aPinned) {
+ function elemAttr(id, attr) {
+ return document.getElementById(id).getAttribute(attr);
+ }
+
+ if (aPinned) {
+ is(elemAttr("key_close", "disabled"), "true",
+ "key_close should be disabled when a pinned-tab is selected");
+ is(elemAttr("menu_close", "key"), "",
+ "menu_close shouldn't have a key set when a pinned is selected");
+ }
+ else {
+ is(elemAttr("key_close", "disabled"), "",
+ "key_closed shouldn't have disabled state set when a non-pinned tab is selected");
+ is(elemAttr("menu_close", "key"), "key_close",
+ "menu_close should have key_close set as its key when a non-pinned tab is selected");
+ }
+ }
+
+ let lastSelectedTab = gBrowser.selectedTab;
+ ok(!lastSelectedTab.pinned, "We should have started with a regular tab selected");
+
+ testState(false);
+
+ let pinnedTab = gBrowser.addTab("about:blank");
+ gBrowser.pinTab(pinnedTab);
+
+ // Just pinning the tab shouldn't change the key state.
+ testState(false);
+
+ // Test updating key state after selecting a tab.
+ gBrowser.selectedTab = pinnedTab;
+ testState(true);
+
+ gBrowser.selectedTab = lastSelectedTab;
+ testState(false);
+
+ gBrowser.selectedTab = pinnedTab;
+ testState(true);
+
+ // Test updating the key state after un/pinning the tab.
+ gBrowser.unpinTab(pinnedTab);
+ testState(false);
+
+ gBrowser.pinTab(pinnedTab);
+ testState(true);
+
+ // Test updating the key state after removing the tab.
+ gBrowser.removeTab(pinnedTab);
+ testState(false);
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug580956.js b/browser/base/content/test/general/browser_bug580956.js
new file mode 100644
index 000000000..b8e7bc20b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug580956.js
@@ -0,0 +1,26 @@
+function numClosedTabs() {
+ return SessionStore.getClosedTabCount(window);
+}
+
+function isUndoCloseEnabled() {
+ updateTabContextMenu();
+ return !document.getElementById("context_undoCloseTab").disabled;
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", 0);
+ gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo");
+ is(numClosedTabs(), 0, "There should be 0 closed tabs.");
+ ok(!isUndoCloseEnabled(), "Undo Close Tab should be disabled.");
+
+ var tab = gBrowser.addTab("http://mochi.test:8888/");
+ var browser = gBrowser.getBrowserForTab(tab);
+ BrowserTestUtils.browserLoaded(browser).then(() => {
+ BrowserTestUtils.removeTab(tab).then(() => {
+ ok(isUndoCloseEnabled(), "Undo Close Tab should be enabled.");
+ finish();
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_bug581242.js b/browser/base/content/test/general/browser_bug581242.js
new file mode 100644
index 000000000..668c0cd41
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug581242.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test() {
+ // Create a new tab and load about:addons
+ let blanktab = gBrowser.addTab();
+ gBrowser.selectedTab = blanktab;
+ BrowserOpenAddonsMgr();
+
+ is(blanktab, gBrowser.selectedTab, "Current tab should be blank tab");
+ // Verify that about:addons loads
+ waitForExplicitFinish();
+ gBrowser.selectedBrowser.addEventListener("load", function() {
+ gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+ let browser = blanktab.linkedBrowser;
+ is(browser.currentURI.spec, "about:addons", "about:addons should load into blank tab.");
+ gBrowser.removeTab(blanktab);
+ finish();
+ }, true);
+}
diff --git a/browser/base/content/test/general/browser_bug581253.js b/browser/base/content/test/general/browser_bug581253.js
new file mode 100644
index 000000000..0c537c3d3
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug581253.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var testURL = "data:text/plain,nothing but plain text";
+var testTag = "581253_tag";
+var timerID = -1;
+
+function test() {
+ registerCleanupFunction(function() {
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ if (timerID > 0) {
+ clearTimeout(timerID);
+ }
+ });
+ waitForExplicitFinish();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ tab.linkedBrowser.addEventListener("load", (function(event) {
+ tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
+
+ let uri = makeURI(testURL);
+ let bmTxn =
+ new PlacesCreateBookmarkTransaction(uri,
+ PlacesUtils.unfiledBookmarksFolderId,
+ -1, "", null, []);
+ PlacesUtils.transactionManager.doTransaction(bmTxn);
+
+ ok(PlacesUtils.bookmarks.isBookmarked(uri), "the test url is bookmarked");
+ waitForStarChange(true, onStarred);
+ }), true);
+
+ content.location = testURL;
+}
+
+function waitForStarChange(aValue, aCallback) {
+ let expectedStatus = aValue ? BookmarkingUI.STATUS_STARRED
+ : BookmarkingUI.STATUS_UNSTARRED;
+ if (BookmarkingUI.status == BookmarkingUI.STATUS_UPDATING ||
+ BookmarkingUI.status != expectedStatus) {
+ info("Waiting for star button change.");
+ setTimeout(waitForStarChange, 50, aValue, aCallback);
+ return;
+ }
+ aCallback();
+}
+
+function onStarred() {
+ is(BookmarkingUI.status, BookmarkingUI.STATUS_STARRED,
+ "star button indicates that the page is bookmarked");
+
+ let uri = makeURI(testURL);
+ let tagTxn = new PlacesTagURITransaction(uri, [testTag]);
+ PlacesUtils.transactionManager.doTransaction(tagTxn);
+
+ StarUI.panel.addEventListener("popupshown", onPanelShown, false);
+ BookmarkingUI.star.click();
+}
+
+function onPanelShown(aEvent) {
+ if (aEvent.target == StarUI.panel) {
+ StarUI.panel.removeEventListener("popupshown", arguments.callee, false);
+ let tagsField = document.getElementById("editBMPanel_tagsField");
+ ok(tagsField.value == testTag, "tags field value was set");
+ tagsField.focus();
+
+ StarUI.panel.addEventListener("popuphidden", onPanelHidden, false);
+ let removeButton = document.getElementById("editBookmarkPanelRemoveButton");
+ removeButton.click();
+ }
+}
+
+function onPanelHidden(aEvent) {
+ if (aEvent.target == StarUI.panel) {
+ StarUI.panel.removeEventListener("popuphidden", arguments.callee, false);
+
+ executeSoon(function() {
+ ok(!PlacesUtils.bookmarks.isBookmarked(makeURI(testURL)),
+ "the bookmark for the test url has been removed");
+ is(BookmarkingUI.status, BookmarkingUI.STATUS_UNSTARRED,
+ "star button indicates that the bookmark has been removed");
+ gBrowser.removeCurrentTab();
+ PlacesTestUtils.clearHistory().then(finish);
+ });
+ }
+}
diff --git a/browser/base/content/test/general/browser_bug585558.js b/browser/base/content/test/general/browser_bug585558.js
new file mode 100644
index 000000000..bae832b4d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug585558.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var tabs = [];
+
+function addTab(aURL) {
+ tabs.push(gBrowser.addTab(aURL, {skipAnimation: true}));
+}
+
+function testAttrib(elem, attrib, attribValue, msg) {
+ is(elem.hasAttribute(attrib), attribValue, msg);
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ is(gBrowser.tabs.length, 1, "one tab is open initially");
+
+ // Add several new tabs in sequence, hiding some, to ensure that the
+ // correct attributes get set
+
+ addTab("http://mochi.test:8888/#0");
+ addTab("http://mochi.test:8888/#1");
+ addTab("http://mochi.test:8888/#2");
+ addTab("http://mochi.test:8888/#3");
+
+ gBrowser.selectedTab = gBrowser.tabs[0];
+ testAttrib(gBrowser.tabs[0], "first-visible-tab", true,
+ "First tab marked first-visible-tab!");
+ testAttrib(gBrowser.tabs[4], "last-visible-tab", true,
+ "Fifth tab marked last-visible-tab!");
+ testAttrib(gBrowser.tabs[0], "selected", true, "First tab marked selected!");
+ testAttrib(gBrowser.tabs[0], "afterselected-visible", false,
+ "First tab not marked afterselected-visible!");
+ testAttrib(gBrowser.tabs[1], "afterselected-visible", true,
+ "Second tab marked afterselected-visible!");
+ gBrowser.hideTab(gBrowser.tabs[1]);
+ executeSoon(test_hideSecond);
+}
+
+function test_hideSecond() {
+ testAttrib(gBrowser.tabs[2], "afterselected-visible", true,
+ "Third tab marked afterselected-visible!");
+ gBrowser.showTab(gBrowser.tabs[1])
+ executeSoon(test_showSecond);
+}
+
+function test_showSecond() {
+ testAttrib(gBrowser.tabs[1], "afterselected-visible", true,
+ "Second tab marked afterselected-visible!");
+ testAttrib(gBrowser.tabs[2], "afterselected-visible", false,
+ "Third tab not marked as afterselected-visible!");
+ gBrowser.selectedTab = gBrowser.tabs[1];
+ gBrowser.hideTab(gBrowser.tabs[0]);
+ executeSoon(test_hideFirst);
+}
+
+function test_hideFirst() {
+ testAttrib(gBrowser.tabs[0], "first-visible-tab", false,
+ "Hidden first tab not marked first-visible-tab!");
+ testAttrib(gBrowser.tabs[1], "first-visible-tab", true,
+ "Second tab marked first-visible-tab!");
+ gBrowser.showTab(gBrowser.tabs[0]);
+ executeSoon(test_showFirst);
+}
+
+function test_showFirst() {
+ testAttrib(gBrowser.tabs[0], "first-visible-tab", true,
+ "First tab marked first-visible-tab!");
+ gBrowser.selectedTab = gBrowser.tabs[2];
+ testAttrib(gBrowser.tabs[3], "afterselected-visible", true,
+ "Fourth tab marked afterselected-visible!");
+
+ gBrowser.moveTabTo(gBrowser.selectedTab, 1);
+ executeSoon(test_movedLower);
+}
+
+function test_movedLower() {
+ testAttrib(gBrowser.tabs[2], "afterselected-visible", true,
+ "Third tab marked afterselected-visible!");
+ test_hoverOne();
+}
+
+function test_hoverOne() {
+ EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[4], { type: "mousemove" });
+ testAttrib(gBrowser.tabs[3], "beforehovered", true, "Fourth tab marked beforehovered");
+ EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[3], { type: "mousemove" });
+ testAttrib(gBrowser.tabs[2], "beforehovered", true, "Third tab marked beforehovered!");
+ testAttrib(gBrowser.tabs[2], "afterhovered", false, "Third tab not marked afterhovered!");
+ testAttrib(gBrowser.tabs[4], "afterhovered", true, "Fifth tab marked afterhovered!");
+ testAttrib(gBrowser.tabs[4], "beforehovered", false, "Fifth tab not marked beforehovered!");
+ testAttrib(gBrowser.tabs[0], "beforehovered", false, "First tab not marked beforehovered!");
+ testAttrib(gBrowser.tabs[0], "afterhovered", false, "First tab not marked afterhovered!");
+ testAttrib(gBrowser.tabs[1], "beforehovered", false, "Second tab not marked beforehovered!");
+ testAttrib(gBrowser.tabs[1], "afterhovered", false, "Second tab not marked afterhovered!");
+ testAttrib(gBrowser.tabs[3], "beforehovered", false, "Fourth tab not marked beforehovered!");
+ testAttrib(gBrowser.tabs[3], "afterhovered", false, "Fourth tab not marked afterhovered!");
+ gBrowser.removeTab(tabs.pop());
+ executeSoon(test_hoverStatePersistence);
+}
+
+function test_hoverStatePersistence() {
+ // Test that the afterhovered and beforehovered attributes are still there when
+ // a tab is selected and then unselected again. See bug 856107.
+
+ function assertState() {
+ testAttrib(gBrowser.tabs[0], "beforehovered", true, "First tab still marked beforehovered!");
+ testAttrib(gBrowser.tabs[0], "afterhovered", false, "First tab not marked afterhovered!");
+ testAttrib(gBrowser.tabs[2], "afterhovered", true, "Third tab still marked afterhovered!");
+ testAttrib(gBrowser.tabs[2], "beforehovered", false, "Third tab not marked afterhovered!");
+ testAttrib(gBrowser.tabs[1], "beforehovered", false, "Second tab not marked beforehovered!");
+ testAttrib(gBrowser.tabs[1], "afterhovered", false, "Second tab not marked afterhovered!");
+ testAttrib(gBrowser.tabs[3], "beforehovered", false, "Fourth tab not marked beforehovered!");
+ testAttrib(gBrowser.tabs[3], "afterhovered", false, "Fourth tab not marked afterhovered!");
+ }
+
+ gBrowser.selectedTab = gBrowser.tabs[3];
+ EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[1], { type: "mousemove" });
+ assertState();
+ gBrowser.selectedTab = gBrowser.tabs[1];
+ assertState();
+ gBrowser.selectedTab = gBrowser.tabs[3];
+ assertState();
+ executeSoon(test_pinning);
+}
+
+function test_pinning() {
+ gBrowser.selectedTab = gBrowser.tabs[3];
+ testAttrib(gBrowser.tabs[3], "last-visible-tab", true,
+ "Fourth tab marked last-visible-tab!");
+ testAttrib(gBrowser.tabs[3], "selected", true, "Fourth tab marked selected!");
+ testAttrib(gBrowser.tabs[3], "afterselected-visible", false,
+ "Fourth tab not marked afterselected-visible!");
+ // Causes gBrowser.tabs to change indices
+ gBrowser.pinTab(gBrowser.tabs[3]);
+ testAttrib(gBrowser.tabs[3], "last-visible-tab", true,
+ "Fourth tab marked last-visible-tab!");
+ testAttrib(gBrowser.tabs[1], "afterselected-visible", true,
+ "Second tab marked afterselected-visible!");
+ testAttrib(gBrowser.tabs[0], "first-visible-tab", true,
+ "First tab marked first-visible-tab!");
+ testAttrib(gBrowser.tabs[0], "selected", true, "First tab marked selected!");
+ gBrowser.selectedTab = gBrowser.tabs[1];
+ testAttrib(gBrowser.tabs[2], "afterselected-visible", true,
+ "Third tab marked afterselected-visible!");
+ test_cleanUp();
+}
+
+function test_cleanUp() {
+ tabs.forEach(gBrowser.removeTab, gBrowser);
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug585785.js b/browser/base/content/test/general/browser_bug585785.js
new file mode 100644
index 000000000..4f9045231
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug585785.js
@@ -0,0 +1,35 @@
+var tab;
+
+function test() {
+ waitForExplicitFinish();
+
+ tab = gBrowser.addTab();
+ isnot(tab.getAttribute("fadein"), "true", "newly opened tab is yet to fade in");
+
+ // Try to remove the tab right before the opening animation's first frame
+ window.requestAnimationFrame(checkAnimationState);
+}
+
+function checkAnimationState() {
+ is(tab.getAttribute("fadein"), "true", "tab opening animation initiated");
+
+ info(window.getComputedStyle(tab).maxWidth);
+ gBrowser.removeTab(tab, { animate: true });
+ if (!tab.parentNode) {
+ ok(true, "tab removed synchronously since the opening animation hasn't moved yet");
+ finish();
+ return;
+ }
+
+ info("tab didn't close immediately, so the tab opening animation must have started moving");
+ info("waiting for the tab to close asynchronously");
+ tab.addEventListener("transitionend", function (event) {
+ if (event.propertyName == "max-width") {
+ tab.removeEventListener("transitionend", arguments.callee, false);
+ executeSoon(function () {
+ ok(!tab.parentNode, "tab removed asynchronously");
+ finish();
+ });
+ }
+ }, false);
+}
diff --git a/browser/base/content/test/general/browser_bug585830.js b/browser/base/content/test/general/browser_bug585830.js
new file mode 100644
index 000000000..6d3adf198
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug585830.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/. */
+
+function test() {
+ let tab1 = gBrowser.selectedTab;
+ let tab2 = gBrowser.addTab("about:blank", {skipAnimation: true});
+ gBrowser.addTab();
+ gBrowser.selectedTab = tab2;
+
+ gBrowser.removeCurrentTab({animate: true});
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, tab1, "First tab should be selected");
+ gBrowser.removeTab(tab2);
+
+ // test for "null has no properties" fix. See Bug 585830 Comment 13
+ gBrowser.removeCurrentTab({animate: true});
+ try {
+ gBrowser.tabContainer.advanceSelectedTab(-1, false);
+ } catch (err) {
+ ok(false, "Shouldn't throw");
+ }
+
+ gBrowser.removeTab(tab1);
+}
diff --git a/browser/base/content/test/general/browser_bug590206.js b/browser/base/content/test/general/browser_bug590206.js
new file mode 100644
index 000000000..f73d144e9
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug590206.js
@@ -0,0 +1,163 @@
+/*
+ * Test the identity mode UI for a variety of page types
+ */
+
+"use strict";
+
+const DUMMY = "browser/browser/base/content/test/general/dummy_page.html";
+
+function loadNewTab(url) {
+ return BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+}
+
+function getIdentityMode() {
+ return document.getElementById("identity-box").className;
+}
+
+function getConnectionState() {
+ gIdentityHandler.refreshIdentityPopup();
+ return document.getElementById("identity-popup").getAttribute("connection");
+}
+
+// This test is slow on Linux debug e10s
+requestLongerTimeout(2);
+
+add_task(function* test_webpage() {
+ let oldTab = gBrowser.selectedTab;
+
+ let newTab = yield loadNewTab("http://example.com/" + DUMMY);
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_blank() {
+ let oldTab = gBrowser.selectedTab;
+
+ let newTab = yield loadNewTab("about:blank");
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_chrome() {
+ let oldTab = gBrowser.selectedTab;
+
+ let newTab = yield loadNewTab("chrome://mozapps/content/extensions/extensions.xul");
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_https() {
+ let oldTab = gBrowser.selectedTab;
+
+ let newTab = yield loadNewTab("https://example.com/" + DUMMY);
+ is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "verifiedDomain", "Identity should be verified");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_addons() {
+ let oldTab = gBrowser.selectedTab;
+
+ let newTab = yield loadNewTab("about:addons");
+ is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "chromeUI", "Identity should be chrome");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_file() {
+ let oldTab = gBrowser.selectedTab;
+ let fileURI = getTestFilePath("");
+
+ let newTab = yield loadNewTab(fileURI);
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_resource_uri() {
+ let oldTab = gBrowser.selectedTab;
+ let dataURI = "resource://gre/modules/Services.jsm";
+
+ let newTab = yield loadNewTab(dataURI);
+
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_data_uri() {
+ let oldTab = gBrowser.selectedTab;
+ let dataURI = "data:text/html,hi"
+
+ let newTab = yield loadNewTab(dataURI);
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* test_about_uri() {
+ let oldTab = gBrowser.selectedTab;
+ let aboutURI = "about:robots"
+
+ let newTab = yield loadNewTab(aboutURI);
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.selectedTab = oldTab;
+ is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+ gBrowser.selectedTab = newTab;
+ is(getConnectionState(), "file", "Connection should be file");
+
+ gBrowser.removeTab(newTab);
+});
diff --git a/browser/base/content/test/general/browser_bug592338.js b/browser/base/content/test/general/browser_bug592338.js
new file mode 100644
index 000000000..ca9cc361a
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug592338.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTROOT = "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/";
+
+var tempScope = {};
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", tempScope);
+var LightweightThemeManager = tempScope.LightweightThemeManager;
+
+function wait_for_notification(aCallback) {
+ PopupNotifications.panel.addEventListener("popupshown", function() {
+ PopupNotifications.panel.removeEventListener("popupshown", arguments.callee, false);
+ aCallback(PopupNotifications.panel);
+ }, false);
+}
+
+var TESTS = [
+function test_install_http() {
+ is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected");
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.org/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab("http://example.org/browser/browser/base/content/test/general/bug592338.html");
+ gBrowser.selectedBrowser.addEventListener("pageshow", function() {
+ if (gBrowser.contentDocument.location.href == "about:blank")
+ return;
+
+ gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+ executeSoon(function() {
+ BrowserTestUtils.synthesizeMouse("#theme-install", 2, 2, {}, gBrowser.selectedBrowser);
+
+ is(LightweightThemeManager.currentTheme, null, "Should not have installed the test theme");
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ pm.remove(makeURI("http://example.org/"), "install");
+
+ runNextTest();
+ });
+ }, false);
+},
+
+function test_install_lwtheme() {
+ is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected");
+
+ var pm = Services.perms;
+ pm.add(makeURI("https://example.com/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab("https://example.com/browser/browser/base/content/test/general/bug592338.html");
+ gBrowser.selectedBrowser.addEventListener("pageshow", function() {
+ if (gBrowser.contentDocument.location.href == "about:blank")
+ return;
+
+ gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+ BrowserTestUtils.synthesizeMouse("#theme-install", 2, 2, {}, gBrowser.selectedBrowser);
+ let notificationBox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
+ waitForCondition(
+ () => notificationBox.getNotificationWithValue("lwtheme-install-notification"),
+ () => {
+ is(LightweightThemeManager.currentTheme.id, "test", "Should have installed the test theme");
+
+ LightweightThemeManager.currentTheme = null;
+ gBrowser.removeTab(gBrowser.selectedTab);
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+
+ runNextTest();
+ }
+ );
+ }, false);
+},
+
+function test_lwtheme_switch_theme() {
+ is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected");
+
+ AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(aAddon) {
+ aAddon.userDisabled = false;
+ ok(aAddon.isActive, "Theme should have immediately enabled");
+ Services.prefs.setBoolPref("extensions.dss.enabled", false);
+
+ var pm = Services.perms;
+ pm.add(makeURI("https://example.com/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab("https://example.com/browser/browser/base/content/test/general/bug592338.html");
+ gBrowser.selectedBrowser.addEventListener("pageshow", function() {
+ if (gBrowser.contentDocument.location.href == "about:blank")
+ return;
+
+ gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+ executeSoon(function() {
+ wait_for_notification(function(aPanel) {
+ is(LightweightThemeManager.currentTheme, null, "Should not have installed the test lwtheme");
+ ok(aAddon.isActive, "Test theme should still be active");
+
+ let notification = aPanel.childNodes[0];
+ is(notification.button.label, "Restart Now", "Should have seen the right button");
+
+ ok(aAddon.userDisabled, "Should be waiting to disable the test theme");
+ aAddon.userDisabled = false;
+ Services.prefs.setBoolPref("extensions.dss.enabled", true);
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ runNextTest();
+ });
+ BrowserTestUtils.synthesizeMouse("#theme-install", 2, 2, {}, gBrowser.selectedBrowser);
+ });
+ }, false);
+ });
+}
+];
+
+function runNextTest() {
+ AddonManager.getAllInstalls(function(aInstalls) {
+ is(aInstalls.length, 0, "Should be no active installs");
+
+ if (TESTS.length == 0) {
+ AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(aAddon) {
+ aAddon.uninstall();
+
+ Services.prefs.setBoolPref("extensions.logging.enabled", false);
+ Services.prefs.setBoolPref("extensions.dss.enabled", false);
+
+ finish();
+ });
+ return;
+ }
+
+ info("Running " + TESTS[0].name);
+ TESTS.shift()();
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("extensions.logging.enabled", true);
+
+ AddonManager.getInstallForURL(TESTROOT + "theme.xpi", function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function() {
+ AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(aAddon) {
+ isnot(aAddon, null, "Should have installed the test theme.");
+
+ // In order to switch themes while the test is running we turn on dynamic
+ // theme switching. This means the test isn't exactly correct but should
+ // do some good
+ Services.prefs.setBoolPref("extensions.dss.enabled", true);
+
+ runNextTest();
+ });
+ }
+ });
+
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
diff --git a/browser/base/content/test/general/browser_bug594131.js b/browser/base/content/test/general/browser_bug594131.js
new file mode 100644
index 000000000..ce09026ac
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug594131.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test() {
+ let newTab = gBrowser.addTab("http://example.com");
+ waitForExplicitFinish();
+ BrowserTestUtils.browserLoaded(newTab.linkedBrowser).then(mainPart);
+
+ function mainPart() {
+ gBrowser.pinTab(newTab);
+ gBrowser.selectedTab = newTab;
+
+ openUILinkIn("http://example.org/", "current", { inBackground: true });
+ isnot(gBrowser.selectedTab, newTab, "shouldn't load in background");
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(gBrowser.tabs[1]); // example.org tab
+ finish();
+ }
+}
diff --git a/browser/base/content/test/general/browser_bug595507.js b/browser/base/content/test/general/browser_bug595507.js
new file mode 100644
index 000000000..54ae42346
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug595507.js
@@ -0,0 +1,36 @@
+/**
+ * Make sure that the form validation error message shows even if the form is in an iframe.
+ */
+add_task(function* () {
+ let uri = "<iframe src=\"data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input required id='i'><input id='s' type='submit'></form>\"</iframe>";
+
+ var gInvalidFormPopup = document.getElementById('invalid-form-popup');
+ ok(gInvalidFormPopup,
+ "The browser should have a popup to show when a form is invalid");
+
+ let tab = gBrowser.addTab();
+ let browser = gBrowser.getBrowserForTab(tab);
+ gBrowser.selectedTab = tab;
+
+ yield promiseTabLoadEvent(tab, "data:text/html," + escape(uri));
+
+ let popupShownPromise = promiseWaitForEvent(gInvalidFormPopup, "popupshown");
+
+ yield ContentTask.spawn(browser, {}, function* () {
+ content.document.getElementsByTagName('iframe')[0]
+ .contentDocument.getElementById('s').click();
+ });
+ yield popupShownPromise;
+
+ yield ContentTask.spawn(browser, {}, function* () {
+ let childdoc = content.document.getElementsByTagName('iframe')[0].contentDocument;
+ Assert.equal(childdoc.activeElement, childdoc.getElementById("i"),
+ "First invalid element should be focused");
+ });
+
+ ok(gInvalidFormPopup.state == 'showing' || gInvalidFormPopup.state == 'open',
+ "The invalid form popup should be shown");
+
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/browser/base/content/test/general/browser_bug596687.js b/browser/base/content/test/general/browser_bug596687.js
new file mode 100644
index 000000000..5c2b4fbfe
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug596687.js
@@ -0,0 +1,25 @@
+add_task(function* test() {
+ var tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ var gotTabAttrModified = false;
+ var gotTabClose = false;
+
+ function onTabClose() {
+ gotTabClose = true;
+ tab.addEventListener("TabAttrModified", onTabAttrModified, false);
+ }
+
+ function onTabAttrModified() {
+ gotTabAttrModified = true;
+ }
+
+ tab.addEventListener("TabClose", onTabClose, false);
+
+ yield BrowserTestUtils.removeTab(tab);
+
+ ok(gotTabClose, "should have got the TabClose event");
+ ok(!gotTabAttrModified, "shouldn't have got the TabAttrModified event after TabClose");
+
+ tab.removeEventListener("TabClose", onTabClose, false);
+ tab.removeEventListener("TabAttrModified", onTabAttrModified, false);
+});
diff --git a/browser/base/content/test/general/browser_bug597218.js b/browser/base/content/test/general/browser_bug597218.js
new file mode 100644
index 000000000..5f4ededc3
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug597218.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/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ // establish initial state
+ is(gBrowser.tabs.length, 1, "we start with one tab");
+
+ // create a tab
+ let tab = gBrowser.loadOneTab("about:blank");
+ ok(!tab.hidden, "tab starts out not hidden");
+ is(gBrowser.tabs.length, 2, "we now have two tabs");
+
+ // make sure .hidden is read-only
+ tab.hidden = true;
+ ok(!tab.hidden, "can't set .hidden directly");
+
+ // hide the tab
+ gBrowser.hideTab(tab);
+ ok(tab.hidden, "tab is hidden");
+
+ // now pin it and make sure it gets unhidden
+ gBrowser.pinTab(tab);
+ ok(tab.pinned, "tab was pinned");
+ ok(!tab.hidden, "tab was unhidden");
+
+ // try hiding it now that it's pinned; shouldn't be able to
+ gBrowser.hideTab(tab);
+ ok(!tab.hidden, "tab did not hide");
+
+ // clean up
+ gBrowser.removeTab(tab);
+ is(gBrowser.tabs.length, 1, "we finish with one tab");
+
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug609700.js b/browser/base/content/test/general/browser_bug609700.js
new file mode 100644
index 000000000..8b4f1ea91
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug609700.js
@@ -0,0 +1,20 @@
+function test() {
+ waitForExplicitFinish();
+
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ Services.ww.unregisterNotification(arguments.callee);
+
+ ok(true, "duplicateTabIn opened a new window");
+
+ whenDelayedStartupFinished(aSubject, function () {
+ executeSoon(function () {
+ aSubject.close();
+ finish();
+ });
+ }, false);
+ }
+ });
+
+ duplicateTabIn(gBrowser.selectedTab, "window");
+}
diff --git a/browser/base/content/test/general/browser_bug623893.js b/browser/base/content/test/general/browser_bug623893.js
new file mode 100644
index 000000000..fa6da1b22
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug623893.js
@@ -0,0 +1,37 @@
+add_task(function* test() {
+ yield BrowserTestUtils.withNewTab("data:text/plain;charset=utf-8,1", function* (browser) {
+ BrowserTestUtils.loadURI(browser, "data:text/plain;charset=utf-8,2");
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ BrowserTestUtils.loadURI(browser, "data:text/plain;charset=utf-8,3");
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ yield duplicate(0, "maintained the original index");
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ yield duplicate(-1, "went back");
+ yield duplicate(1, "went forward");
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+function promiseGetIndex(browser) {
+ return ContentTask.spawn(browser, null, function() {
+ let shistory = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+ return shistory.index;
+ });
+}
+
+let duplicate = Task.async(function* (delta, msg, cb) {
+ var startIndex = yield promiseGetIndex(gBrowser.selectedBrowser);
+
+ duplicateTabIn(gBrowser.selectedTab, "tab", delta);
+
+ let tab = gBrowser.selectedTab;
+ yield BrowserTestUtils.waitForEvent(tab, "SSTabRestored");
+
+ let endIndex = yield promiseGetIndex(gBrowser.selectedBrowser);
+ is(endIndex, startIndex + delta, msg);
+});
diff --git a/browser/base/content/test/general/browser_bug624734.js b/browser/base/content/test/general/browser_bug624734.js
new file mode 100644
index 000000000..d6fc7acbc
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug624734.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 624734 - Star UI has no tooltip until bookmarked page is visited
+
+function finishTest() {
+ is(BookmarkingUI.button.getAttribute("buttontooltiptext"),
+ BookmarkingUI._unstarredTooltip,
+ "Star icon should have the unstarred tooltip text");
+
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+ if (BookmarkingUI.status == BookmarkingUI.STATUS_UPDATING) {
+ waitForCondition(() => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING, finishTest, "BookmarkingUI was updating for too long");
+ } else {
+ finishTest();
+ }
+ });
+
+ tab.linkedBrowser.loadURI("http://example.com/browser/browser/base/content/test/general/dummy_page.html");
+}
diff --git a/browser/base/content/test/general/browser_bug633691.js b/browser/base/content/test/general/browser_bug633691.js
new file mode 100644
index 000000000..28a8440ff
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug633691.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(function* test() {
+ const URL = "data:text/html,<iframe width='700' height='700'></iframe>";
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: URL }, function* (browser) {
+ yield ContentTask.spawn(browser,
+ { is_element_hidden_: is_element_hidden.toSource(),
+ is_hidden_: is_hidden.toSource() },
+ function* ({ is_element_hidden_, is_hidden_ }) {
+ let loadError =
+ ContentTaskUtils.waitForEvent(this, "AboutNetErrorLoad", false, null, true);
+ let iframe = content.document.querySelector("iframe");
+ iframe.src = "https://expired.example.com/";
+
+ yield loadError;
+
+ let is_hidden = eval(`(() => ${is_hidden_})()`);
+ let is_element_hidden = eval(`(() => ${is_element_hidden_})()`);
+ let doc = content.document.getElementsByTagName("iframe")[0].contentDocument;
+ let aP = doc.getElementById("badCertAdvancedPanel");
+ ok(aP, "Advanced content should exist");
+ void is_hidden; // Quiet eslint warnings (actual use under is_element_hidden)
+ is_element_hidden(aP, "Advanced content should not be visible by default")
+ });
+ });
+});
diff --git a/browser/base/content/test/general/browser_bug647886.js b/browser/base/content/test/general/browser_bug647886.js
new file mode 100644
index 000000000..6c28c465c
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug647886.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.history.pushState({}, "2", "2.html");
+ });
+
+ var backButton = document.getElementById("back-button");
+ var rect = backButton.getBoundingClientRect();
+
+ info("waiting for the history menu to open");
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(backButton, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(backButton, {type: "mousedown"});
+ EventUtils.synthesizeMouse(backButton, rect.width / 2, rect.height, {type: "mouseup"});
+ let event = yield popupShownPromise;
+
+ ok(true, "history menu opened");
+
+ // Wait for the session data to be flushed before continuing the test
+ yield new Promise(resolve => SessionStore.getSessionHistory(gBrowser.selectedTab, resolve));
+
+ is(event.target.children.length, 2, "Two history items");
+
+ let node = event.target.firstChild;
+ is(node.getAttribute("uri"), "http://example.com/2.html", "first item uri");
+ is(node.getAttribute("index"), "1", "first item index");
+ is(node.getAttribute("historyindex"), "0", "first item historyindex");
+
+ node = event.target.lastChild;
+ is(node.getAttribute("uri"), "http://example.com/", "second item uri");
+ is(node.getAttribute("index"), "0", "second item index");
+ is(node.getAttribute("historyindex"), "-1", "second item historyindex");
+
+ event.target.hidePopup();
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/general/browser_bug655584.js b/browser/base/content/test/general/browser_bug655584.js
new file mode 100644
index 000000000..b836e3173
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug655584.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 655584 - awesomebar suggestions don't update after tab is closed
+
+add_task(function* () {
+ var tab1 = gBrowser.addTab();
+ var tab2 = gBrowser.addTab();
+
+ // When urlbar in a new tab is focused, and a tab switch occurs,
+ // the urlbar popup should be closed
+ yield BrowserTestUtils.switchTab(gBrowser, tab2);
+ gURLBar.focus(); // focus the urlbar in the tab we will switch to
+ yield BrowserTestUtils.switchTab(gBrowser, tab1);
+ gURLBar.openPopup();
+ yield BrowserTestUtils.switchTab(gBrowser, tab2);
+ ok(!gURLBar.popupOpen, "urlbar focused in tab to switch to, close popup");
+
+ // cleanup
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug664672.js b/browser/base/content/test/general/browser_bug664672.js
new file mode 100644
index 000000000..2064f77d0
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug664672.js
@@ -0,0 +1,19 @@
+function test() {
+ waitForExplicitFinish();
+
+ var tab = gBrowser.addTab();
+
+ tab.addEventListener("TabClose", function () {
+ tab.removeEventListener("TabClose", arguments.callee, false);
+
+ ok(tab.linkedBrowser, "linkedBrowser should still exist during the TabClose event");
+
+ executeSoon(function () {
+ ok(!tab.linkedBrowser, "linkedBrowser should be gone after the TabClose event");
+
+ finish();
+ });
+ }, false);
+
+ gBrowser.removeTab(tab);
+}
diff --git a/browser/base/content/test/general/browser_bug676619.js b/browser/base/content/test/general/browser_bug676619.js
new file mode 100644
index 000000000..6b596481d
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug676619.js
@@ -0,0 +1,124 @@
+function test () {
+ requestLongerTimeout(3);
+ waitForExplicitFinish();
+
+ var isHTTPS = false;
+
+ function loadListener() {
+ function testLocation(link, url, next) {
+ new TabOpenListener(url, function () {
+ gBrowser.removeTab(this.tab);
+ }, function () {
+ next();
+ });
+
+ ContentTask.spawn(gBrowser.selectedBrowser, link, contentLink => {
+ content.document.getElementById(contentLink).click();
+ });
+ }
+
+ function testLink(link, name, next) {
+ addWindowListener("chrome://mozapps/content/downloads/unknownContentType.xul", function (win) {
+ ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ Assert.equal(content.document.getElementById("unload-flag").textContent,
+ "Okay", "beforeunload shouldn't have fired");
+ }).then(() => {
+ is(win.document.getElementById("location").value, name, "file name should match");
+ win.close();
+ next();
+ });
+ });
+
+ ContentTask.spawn(gBrowser.selectedBrowser, link, contentLink => {
+ content.document.getElementById(contentLink).click();
+ });
+ }
+
+ testLink("link1", "test.txt",
+ testLink.bind(null, "link2", "video.ogg",
+ testLink.bind(null, "link3", "just some video",
+ testLink.bind(null, "link4", "with-target.txt",
+ testLink.bind(null, "link5", "javascript.txt",
+ testLink.bind(null, "link6", "test.blob",
+ testLocation.bind(null, "link7", "http://example.com/",
+ function () {
+ if (isHTTPS) {
+ finish();
+ } else {
+ // same test again with https:
+ isHTTPS = true;
+ gBrowser.loadURI("https://example.com:443/browser/browser/base/content/test/general/download_page.html");
+ }
+ })))))));
+
+ }
+
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ loadListener();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(loadListener);
+ });
+
+ gBrowser.loadURI("http://mochi.test:8888/browser/browser/base/content/test/general/download_page.html");
+}
+
+
+function addWindowListener(aURL, aCallback) {
+ Services.wm.addListener({
+ onOpenWindow: function(aXULWindow) {
+ info("window opened, waiting for focus");
+ Services.wm.removeListener(this);
+
+ var domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ waitForFocus(function() {
+ is(domwindow.document.location.href, aURL, "should have seen the right window open");
+ aCallback(domwindow);
+ }, domwindow);
+ },
+ onCloseWindow: function(aXULWindow) { },
+ onWindowTitleChange: function(aXULWindow, aNewTitle) { }
+ });
+}
+
+// This listens for the next opened tab and checks it is of the right url.
+// opencallback is called when the new tab is fully loaded
+// closecallback is called when the tab is closed
+function TabOpenListener(url, opencallback, closecallback) {
+ this.url = url;
+ this.opencallback = opencallback;
+ this.closecallback = closecallback;
+
+ gBrowser.tabContainer.addEventListener("TabOpen", this, false);
+}
+
+TabOpenListener.prototype = {
+ url: null,
+ opencallback: null,
+ closecallback: null,
+ tab: null,
+ browser: null,
+
+ handleEvent: function(event) {
+ if (event.type == "TabOpen") {
+ gBrowser.tabContainer.removeEventListener("TabOpen", this, false);
+ this.tab = event.originalTarget;
+ this.browser = this.tab.linkedBrowser;
+ BrowserTestUtils.browserLoaded(this.browser, false, this.url).then(() => {
+ this.tab.addEventListener("TabClose", this, false);
+ var url = this.browser.currentURI.spec;
+ is(url, this.url, "Should have opened the correct tab");
+ this.opencallback();
+ });
+ } else if (event.type == "TabClose") {
+ if (event.originalTarget != this.tab)
+ return;
+ this.tab.removeEventListener("TabClose", this, false);
+ this.opencallback = null;
+ this.tab = null;
+ this.browser = null;
+ // Let the window close complete
+ executeSoon(this.closecallback);
+ this.closecallback = null;
+ }
+ }
+};
diff --git a/browser/base/content/test/general/browser_bug678392-1.html b/browser/base/content/test/general/browser_bug678392-1.html
new file mode 100644
index 000000000..c3b235dd0
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug678392-1.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+ <title>bug678392 - 1</title>
+ </head>
+ <body>
+bug 678392 test page 1
+ </body>
+</html> \ No newline at end of file
diff --git a/browser/base/content/test/general/browser_bug678392-2.html b/browser/base/content/test/general/browser_bug678392-2.html
new file mode 100644
index 000000000..9b18efcf7
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug678392-2.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+ <title>bug678392 - 2</title>
+ </head>
+ <body>
+bug 678392 test page 2
+ </body>
+</html> \ No newline at end of file
diff --git a/browser/base/content/test/general/browser_bug678392.js b/browser/base/content/test/general/browser_bug678392.js
new file mode 100644
index 000000000..6aedeefdf
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug678392.js
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 HTTPROOT = "http://example.com/browser/browser/base/content/test/general/";
+
+function maxSnapshotOverride() {
+ return 5;
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ BrowserOpenTab();
+ let tab = gBrowser.selectedTab;
+ registerCleanupFunction(function () { gBrowser.removeTab(tab); });
+
+ ok(gHistorySwipeAnimation, "gHistorySwipeAnimation exists.");
+
+ if (!gHistorySwipeAnimation._isSupported()) {
+ is(gHistorySwipeAnimation.active, false, "History swipe animation is not " +
+ "active when not supported by the platform.");
+ finish();
+ return;
+ }
+
+ gHistorySwipeAnimation._getMaxSnapshots = maxSnapshotOverride;
+ gHistorySwipeAnimation.init();
+
+ is(gHistorySwipeAnimation.active, true, "History swipe animation support " +
+ "was successfully initialized when supported.");
+
+ cleanupArray();
+ load(gBrowser.selectedTab, HTTPROOT + "browser_bug678392-2.html", test0);
+}
+
+function load(aTab, aUrl, aCallback) {
+ aTab.linkedBrowser.addEventListener("load", function onload(aEvent) {
+ aEvent.currentTarget.removeEventListener("load", onload, true);
+ waitForFocus(aCallback, content);
+ }, true);
+ aTab.linkedBrowser.loadURI(aUrl);
+}
+
+function cleanupArray() {
+ let arr = gHistorySwipeAnimation._trackedSnapshots;
+ while (arr.length > 0) {
+ delete arr[0].browser.snapshots[arr[0].index]; // delete actual snapshot
+ arr.splice(0, 1);
+ }
+}
+
+function testArrayCleanup() {
+ // Test cleanup of array of tracked snapshots.
+ let arr = gHistorySwipeAnimation._trackedSnapshots;
+ is(arr.length, 0, "Snapshots were removed correctly from the array of " +
+ "tracked snapshots.");
+}
+
+function test0() {
+ // Test growing of array of tracked snapshots.
+ let tab = gBrowser.selectedTab;
+
+ load(tab, HTTPROOT + "browser_bug678392-1.html", function() {
+ ok(gHistorySwipeAnimation._trackedSnapshots, "Array for snapshot " +
+ "tracking is initialized.");
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 1, "Snapshot array " +
+ "has correct length of 1 after loading one page.");
+ load(tab, HTTPROOT + "browser_bug678392-2.html", function() {
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Snapshot array " +
+ " has correct length of 2 after loading two pages.");
+ load(tab, HTTPROOT + "browser_bug678392-1.html", function() {
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 3, "Snapshot " +
+ "array has correct length of 3 after loading three pages.");
+ load(tab, HTTPROOT + "browser_bug678392-2.html", function() {
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Snapshot " +
+ "array has correct length of 4 after loading four pages.");
+ cleanupArray();
+ testArrayCleanup();
+ test1();
+ });
+ });
+ });
+ });
+}
+
+function verifyRefRemoved(aIndex, aBrowser) {
+ let wasFound = false;
+ let arr = gHistorySwipeAnimation._trackedSnapshots;
+ for (let i = 0; i < arr.length; i++) {
+ if (arr[i].index == aIndex && arr[i].browser == aBrowser)
+ wasFound = true;
+ }
+ is(wasFound, false, "The reference that was previously removed was " +
+ "still found in the array of tracked snapshots.");
+}
+
+function test1() {
+ // Test presence of snpashots in per-tab array of snapshots and removal of
+ // individual snapshots (and corresponding references in the array of
+ // tracked snapshots).
+ let tab = gBrowser.selectedTab;
+
+ load(tab, HTTPROOT + "browser_bug678392-1.html", function() {
+ var historyIndex = gBrowser.webNavigation.sessionHistory.index - 1;
+ load(tab, HTTPROOT + "browser_bug678392-2.html", function() {
+ load(tab, HTTPROOT + "browser_bug678392-1.html", function() {
+ load(tab, HTTPROOT + "browser_bug678392-2.html", function() {
+ let browser = gBrowser.selectedBrowser;
+ ok(browser.snapshots, "Array of snapshots exists in browser.");
+ ok(browser.snapshots[historyIndex], "First page exists in snapshot " +
+ "array.");
+ ok(browser.snapshots[historyIndex + 1], "Second page exists in " +
+ "snapshot array.");
+ ok(browser.snapshots[historyIndex + 2], "Third page exists in " +
+ "snapshot array.");
+ ok(browser.snapshots[historyIndex + 3], "Fourth page exists in " +
+ "snapshot array.");
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Length of " +
+ "array of tracked snapshots is equal to 4 after loading four " +
+ "pages.");
+
+ // Test removal of reference in the middle of the array.
+ gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex + 1,
+ browser);
+ verifyRefRemoved(historyIndex + 1, browser);
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 3, "Length of " +
+ "array of tracked snapshots is equal to 3 after removing one" +
+ "reference from the array with length 4.");
+
+ // Test removal of reference at end of array.
+ gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex + 3,
+ browser);
+ verifyRefRemoved(historyIndex + 3, browser);
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Length of " +
+ "array of tracked snapshots is equal to 2 after removing two" +
+ "references from the array with length 4.");
+
+ // Test removal of reference at head of array.
+ gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex,
+ browser);
+ verifyRefRemoved(historyIndex, browser);
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 1, "Length of " +
+ "array of tracked snapshots is equal to 1 after removing three" +
+ "references from the array with length 4.");
+
+ cleanupArray();
+ test2();
+ });
+ });
+ });
+ });
+}
+
+function test2() {
+ // Test growing of snapshot array across tabs.
+ let tab = gBrowser.selectedTab;
+
+ load(tab, HTTPROOT + "browser_bug678392-1.html", function() {
+ load(tab, HTTPROOT + "browser_bug678392-2.html", function() {
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Length of " +
+ "snapshot array is equal to 2 after loading two pages");
+ let prevTab = tab;
+ tab = gBrowser.addTab("about:newtab");
+ gBrowser.selectedTab = tab;
+ load(tab, HTTPROOT + "browser_bug678392-2.html" /* initial page */,
+ function() {
+ load(tab, HTTPROOT + "browser_bug678392-1.html", function() {
+ load(tab, HTTPROOT + "browser_bug678392-2.html", function() {
+ is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Length " +
+ "of snapshot array is equal to 4 after loading two pages in " +
+ "two tabs each.");
+ gBrowser.removeCurrentTab();
+ gBrowser.selectedTab = prevTab;
+ cleanupArray();
+ test3();
+ });
+ });
+ });
+ });
+ });
+}
+
+function test3() {
+ // Test uninit of gHistorySwipeAnimation.
+ // This test MUST be the last one to execute.
+ gHistorySwipeAnimation.uninit();
+ is(gHistorySwipeAnimation.active, false, "History swipe animation support " +
+ "was successfully uninitialized");
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_bug710878.js b/browser/base/content/test/general/browser_bug710878.js
new file mode 100644
index 000000000..dd99d67cf
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug710878.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PAGE = "data:text/html;charset=utf-8,<a href='%23xxx'><span>word1 <span> word2 </span></span><span> word3</span></a>";
+
+/**
+ * Tests that we correctly compute the text for context menu
+ * selection of some content.
+ */
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu,
+ "popupshown");
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(contextMenu,
+ "popuphidden");
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("a", {
+ type: "contextmenu",
+ button: 2,
+ }, browser);
+
+ yield awaitPopupShown;
+
+ is(gContextMenu.linkTextStr, "word1 word2 word3",
+ "Text under link is correctly computed.");
+
+ contextMenu.hidePopup();
+ yield awaitPopupHidden;
+ });
+});
diff --git a/browser/base/content/test/general/browser_bug719271.js b/browser/base/content/test/general/browser_bug719271.js
new file mode 100644
index 000000000..c3bb9cd26
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug719271.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/. */
+"use strict";
+
+const TEST_PAGE = "http://example.org/browser/browser/base/content/test/general/zoom_test.html";
+const TEST_VIDEO = "http://example.org/browser/browser/base/content/test/general/video.ogg";
+
+var gTab1, gTab2, gLevel1;
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ gTab1 = gBrowser.addTab();
+ gTab2 = gBrowser.addTab();
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ yield FullZoomHelper.load(gTab1, TEST_PAGE);
+ yield FullZoomHelper.load(gTab2, TEST_VIDEO);
+ }).then(zoomTab1, FullZoomHelper.failAndContinue(finish));
+}
+
+function zoomTab1() {
+ Task.spawn(function* () {
+ is(gBrowser.selectedTab, gTab1, "Tab 1 is selected");
+
+ // Reset zoom level if we run this test > 1 time in same browser session.
+ var level1 = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1));
+ if (level1 > 1)
+ FullZoom.reduce();
+
+ FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1");
+
+ FullZoom.enlarge();
+ gLevel1 = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1));
+
+ ok(gLevel1 > 1, "New zoom for tab 1 should be greater than 1");
+ FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2");
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ FullZoomHelper.zoomTest(gTab2, 1, "Tab 2 is still unzoomed after it is selected");
+ FullZoomHelper.zoomTest(gTab1, gLevel1, "Tab 1 is still zoomed");
+ }).then(zoomTab2, FullZoomHelper.failAndContinue(finish));
+}
+
+function zoomTab2() {
+ Task.spawn(function* () {
+ is(gBrowser.selectedTab, gTab2, "Tab 2 is selected");
+
+ FullZoom.reduce();
+ let level2 = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab2));
+
+ ok(level2 < 1, "New zoom for tab 2 should be less than 1");
+ FullZoomHelper.zoomTest(gTab1, gLevel1, "Zooming tab 2 should not affect tab 1");
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ FullZoomHelper.zoomTest(gTab1, gLevel1, "Tab 1 should have the same zoom after it's selected");
+ }).then(testNavigation, FullZoomHelper.failAndContinue(finish));
+}
+
+function testNavigation() {
+ Task.spawn(function* () {
+ yield FullZoomHelper.load(gTab1, TEST_VIDEO);
+ FullZoomHelper.zoomTest(gTab1, 1, "Zoom should be 1 when a video was loaded");
+ yield waitForNextTurn(); // trying to fix orange bug 806046
+ yield FullZoomHelper.navigate(FullZoomHelper.BACK);
+ FullZoomHelper.zoomTest(gTab1, gLevel1, "Zoom should be restored when a page is loaded");
+ yield waitForNextTurn(); // trying to fix orange bug 806046
+ yield FullZoomHelper.navigate(FullZoomHelper.FORWARD);
+ FullZoomHelper.zoomTest(gTab1, 1, "Zoom should be 1 again when navigating back to a video");
+ }).then(finishTest, FullZoomHelper.failAndContinue(finish));
+}
+
+function waitForNextTurn() {
+ let deferred = Promise.defer();
+ setTimeout(() => deferred.resolve(), 0);
+ return deferred.promise;
+}
+
+var finishTestStarted = false;
+function finishTest() {
+ Task.spawn(function* () {
+ ok(!finishTestStarted, "finishTest called more than once");
+ finishTestStarted = true;
+
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1);
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab1);
+ yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2);
+ yield FullZoom.reset();
+ yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab2);
+ }).then(finish, FullZoomHelper.failAndContinue(finish));
+}
diff --git a/browser/base/content/test/general/browser_bug724239.js b/browser/base/content/test/general/browser_bug724239.js
new file mode 100644
index 000000000..430751b91
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug724239.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* test() {
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" },
+ function* (browser) {
+ BrowserTestUtils.loadURI(browser, "http://example.com");
+ yield BrowserTestUtils.browserLoaded(browser);
+ ok(!gBrowser.canGoBack, "about:newtab wasn't added to the session history");
+ });
+});
diff --git a/browser/base/content/test/general/browser_bug734076.js b/browser/base/content/test/general/browser_bug734076.js
new file mode 100644
index 000000000..9de7d913f
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug734076.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* ()
+{
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, null, false);
+
+ let browser = tab.linkedBrowser;
+ browser.stop(); // stop the about:blank load
+
+ let writeDomainURL = encodeURI("data:text/html,<script>document.write(document.domain);</script>");
+
+ let tests = [
+ {
+ name: "view background image",
+ url: "http://mochi.test:8888/",
+ element: "body",
+ go: function () {
+ return ContentTask.spawn(gBrowser.selectedBrowser, { writeDomainURL: writeDomainURL }, function* (arg) {
+ let contentBody = content.document.body;
+ contentBody.style.backgroundImage = "url('" + arg.writeDomainURL + "')";
+
+ return "context-viewbgimage";
+ });
+ },
+ verify: function () {
+ return ContentTask.spawn(gBrowser.selectedBrowser, null, function* (arg) {
+ Assert.ok(!content.document.body.textContent,
+ "no domain was inherited for view background image");
+ });
+ }
+ },
+ {
+ name: "view image",
+ url: "http://mochi.test:8888/",
+ element: "img",
+ go: function () {
+ return ContentTask.spawn(gBrowser.selectedBrowser, { writeDomainURL: writeDomainURL }, function* (arg) {
+ let doc = content.document;
+ let img = doc.createElement("img");
+ img.height = 100;
+ img.width = 100;
+ img.setAttribute("src", arg.writeDomainURL);
+ doc.body.insertBefore(img, doc.body.firstChild);
+
+ return "context-viewimage";
+ });
+ },
+ verify: function () {
+ return ContentTask.spawn(gBrowser.selectedBrowser, null, function* (arg) {
+ Assert.ok(!content.document.body.textContent,
+ "no domain was inherited for view image");
+ });
+ }
+ },
+ {
+ name: "show only this frame",
+ url: "http://mochi.test:8888/",
+ element: "iframe",
+ go: function () {
+ return ContentTask.spawn(gBrowser.selectedBrowser, { writeDomainURL: writeDomainURL }, function* (arg) {
+ let doc = content.document;
+ let iframe = doc.createElement("iframe");
+ iframe.setAttribute("src", arg.writeDomainURL);
+ doc.body.insertBefore(iframe, doc.body.firstChild);
+
+ // Wait for the iframe to load.
+ return new Promise(resolve => {
+ iframe.addEventListener("load", function onload() {
+ iframe.removeEventListener("load", onload, true);
+ resolve("context-showonlythisframe");
+ }, true);
+ });
+ });
+ },
+ verify: function () {
+ return ContentTask.spawn(gBrowser.selectedBrowser, null, function* (arg) {
+ Assert.ok(!content.document.body.textContent,
+ "no domain was inherited for 'show only this frame'");
+ });
+ }
+ }
+ ];
+
+ let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+
+ for (let test of tests) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.loadURI(test.url);
+ yield loadedPromise;
+
+ info("Run subtest " + test.name);
+ let commandToRun = yield test.go();
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouse(test.element, 3, 3,
+ { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+ info("onImage: " + gContextMenu.onImage);
+ info("target: " + gContextMenu.target.tagName);
+
+ let loadedAfterCommandPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ document.getElementById(commandToRun).click();
+ yield loadedAfterCommandPromise;
+
+ yield test.verify();
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
+ contentAreaContextMenu.hidePopup();
+ yield popupHiddenPromise;
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug735471.js b/browser/base/content/test/general/browser_bug735471.js
new file mode 100644
index 000000000..9afb52c4b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug735471.js
@@ -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/.
+ */
+
+
+function test() {
+ waitForExplicitFinish();
+ // Open a new tab.
+ whenNewTabLoaded(window, testPreferences);
+}
+
+function testPreferences() {
+ whenTabLoaded(gBrowser.selectedTab, function () {
+ is(content.location.href, "about:preferences", "Checking if the preferences tab was opened");
+
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+
+ openPreferences();
+}
diff --git a/browser/base/content/test/general/browser_bug749738.js b/browser/base/content/test/general/browser_bug749738.js
new file mode 100644
index 000000000..7e805b799
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug749738.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/. */
+
+"use strict";
+
+const DUMMY_PAGE = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = gBrowser.addTab();
+ gBrowser.selectedTab = tab;
+
+ BrowserTestUtils.loadURI(tab.linkedBrowser, DUMMY_PAGE);
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+ gFindBar.onFindCommand();
+ EventUtils.sendString("Dummy");
+ gBrowser.removeTab(tab);
+
+ try {
+ gFindBar.close();
+ ok(true, "findbar.close should not throw an exception");
+ } catch (e) {
+ ok(false, "findbar.close threw exception: " + e);
+ }
+ finish();
+ });
+}
diff --git a/browser/base/content/test/general/browser_bug763468_perwindowpb.js b/browser/base/content/test/general/browser_bug763468_perwindowpb.js
new file mode 100644
index 000000000..23cb14b8c
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug763468_perwindowpb.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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
+ waitForExplicitFinish, whenNewWindowLoaded, whenNewTabLoaded,
+ executeSoon, registerCleanupFunction, finish, is
+*/
+/* exported test */
+
+// This test makes sure that opening a new tab in private browsing mode opens about:privatebrowsing
+function test() {
+ // initialization
+ waitForExplicitFinish();
+
+ let windowsToClose = [];
+ let newTabURL;
+ let mode;
+
+ function doTest(aIsPrivateMode, aWindow, aCallback) {
+ whenNewTabLoaded(aWindow, function() {
+ if (aIsPrivateMode) {
+ mode = "per window private browsing";
+ newTabURL = "about:privatebrowsing";
+ } else {
+ mode = "normal";
+ newTabURL = "about:newtab";
+ }
+
+ is(aWindow.gBrowser.currentURI.spec, newTabURL,
+ "URL of NewTab should be " + newTabURL + " in " + mode + " mode");
+
+ aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
+ aCallback();
+ });
+ }
+
+ function testOnWindow(aOptions, aCallback) {
+ whenNewWindowLoaded(aOptions, function(aWin) {
+ windowsToClose.push(aWin);
+ // execute should only be called when need, like when you are opening
+ // web pages on the test. If calling executeSoon() is not necesary, then
+ // call whenNewWindowLoaded() instead of testOnWindow() on your test.
+ executeSoon(() => aCallback(aWin));
+ });
+ }
+
+ // this function is called after calling finish() on the test.
+ registerCleanupFunction(function() {
+ windowsToClose.forEach(function(aWin) {
+ aWin.close();
+ });
+ });
+
+ // test first when not on private mode
+ testOnWindow({}, function(aWin) {
+ doTest(false, aWin, function() {
+ // then test when on private mode
+ testOnWindow({private: true}, function(aWin2) {
+ doTest(true, aWin2, function() {
+ // then test again when not on private mode
+ testOnWindow({}, function(aWin3) {
+ doTest(false, aWin3, finish);
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_bug767836_perwindowpb.js b/browser/base/content/test/general/browser_bug767836_perwindowpb.js
new file mode 100644
index 000000000..7f5d15e76
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug767836_perwindowpb.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/. */
+"use strict";
+/* globals waitForExplicitFinish, executeSoon, finish, whenNewWindowLoaded, ok */
+/* globals is */
+/* exported test */
+
+function test() {
+ // initialization
+ waitForExplicitFinish();
+
+ let aboutNewTabService = Components.classes["@mozilla.org/browser/aboutnewtab-service;1"]
+ .getService(Components.interfaces.nsIAboutNewTabService);
+ let newTabURL;
+ let testURL = "http://example.com/";
+ let defaultURL = aboutNewTabService.newTabURL;
+ let mode;
+
+ function doTest(aIsPrivateMode, aWindow, aCallback) {
+ openNewTab(aWindow, function() {
+ if (aIsPrivateMode) {
+ mode = "per window private browsing";
+ newTabURL = "about:privatebrowsing";
+ } else {
+ mode = "normal";
+ newTabURL = "about:newtab";
+ }
+
+ // Check the new tab opened while in normal/private mode
+ is(aWindow.gBrowser.selectedBrowser.currentURI.spec, newTabURL,
+ "URL of NewTab should be " + newTabURL + " in " + mode + " mode");
+ // Set the custom newtab url
+ aboutNewTabService.newTabURL = testURL;
+ is(aboutNewTabService.newTabURL, testURL, "Custom newtab url is set");
+
+ // Open a newtab after setting the custom newtab url
+ openNewTab(aWindow, function() {
+ is(aWindow.gBrowser.selectedBrowser.currentURI.spec, testURL,
+ "URL of NewTab should be the custom url");
+
+ // Clear the custom url.
+ aboutNewTabService.resetNewTabURL();
+ is(aboutNewTabService.newTabURL, defaultURL, "No custom newtab url is set");
+
+ aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
+ aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
+ aWindow.close();
+ aCallback();
+ });
+ });
+ }
+
+ function testOnWindow(aIsPrivate, aCallback) {
+ whenNewWindowLoaded({private: aIsPrivate}, function(win) {
+ executeSoon(() => aCallback(win));
+ });
+ }
+
+ // check whether any custom new tab url has been configured
+ ok(!aboutNewTabService.overridden, "No custom newtab url is set");
+
+ // test normal mode
+ testOnWindow(false, function(aWindow) {
+ doTest(false, aWindow, function() {
+ // test private mode
+ testOnWindow(true, function(aWindow2) {
+ doTest(true, aWindow2, function() {
+ finish();
+ });
+ });
+ });
+ });
+}
+
+function openNewTab(aWindow, aCallback) {
+ // Open a new tab
+ aWindow.BrowserOpenTab();
+
+ let browser = aWindow.gBrowser.selectedBrowser;
+ if (browser.contentDocument.readyState === "complete") {
+ executeSoon(aCallback);
+ return;
+ }
+
+ browser.addEventListener("load", function onLoad() {
+ browser.removeEventListener("load", onLoad, true);
+ executeSoon(aCallback);
+ }, true);
+}
diff --git a/browser/base/content/test/general/browser_bug817947.js b/browser/base/content/test/general/browser_bug817947.js
new file mode 100644
index 000000000..3a76e36d3
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug817947.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+const URL = "http://mochi.test:8888/browser/";
+const PREF = "browser.sessionstore.restore_on_demand";
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF);
+ });
+
+ preparePendingTab(function (aTab) {
+ let win = gBrowser.replaceTabWithWindow(aTab);
+
+ whenDelayedStartupFinished(win, function () {
+ let [tab] = win.gBrowser.tabs;
+
+ whenLoaded(tab.linkedBrowser, function () {
+ is(tab.linkedBrowser.currentURI.spec, URL, "correct url should be loaded");
+ ok(!tab.hasAttribute("pending"), "tab should not be pending");
+
+ win.close();
+ finish();
+ });
+ });
+ });
+}
+
+function preparePendingTab(aCallback) {
+ let tab = gBrowser.addTab(URL);
+
+ whenLoaded(tab.linkedBrowser, function () {
+ BrowserTestUtils.removeTab(tab).then(() => {
+ let [{state}] = JSON.parse(SessionStore.getClosedTabData(window));
+
+ tab = gBrowser.addTab("about:blank");
+ whenLoaded(tab.linkedBrowser, function () {
+ SessionStore.setTabState(tab, JSON.stringify(state));
+ ok(tab.hasAttribute("pending"), "tab should be pending");
+ aCallback(tab);
+ });
+ });
+ });
+}
+
+function whenLoaded(aElement, aCallback) {
+ aElement.addEventListener("load", function onLoad() {
+ aElement.removeEventListener("load", onLoad, true);
+ executeSoon(aCallback);
+ }, true);
+}
diff --git a/browser/base/content/test/general/browser_bug822367.js b/browser/base/content/test/general/browser_bug822367.js
new file mode 100644
index 000000000..0d60c05cd
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug822367.js
@@ -0,0 +1,187 @@
+/*
+ * User Override Mixed Content Block - Tests for Bug 822367
+ */
+
+
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+
+// We alternate for even and odd test cases to simulate different hosts
+const gHttpTestRoot = "https://example.com/browser/browser/base/content/test/general/";
+const gHttpTestRoot2 = "https://test1.example.com/browser/browser/base/content/test/general/";
+
+var gTestBrowser = null;
+
+add_task(function* test() {
+ yield SpecialPowers.pushPrefEnv({ set: [[ PREF_DISPLAY, true ],
+ [ PREF_ACTIVE, true ]] });
+
+ var newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ newTab.linkedBrowser.stop()
+
+ // Mixed Script Test
+ var url = gHttpTestRoot + "file_bug822367_1.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+// Mixed Script Test
+add_task(function* MixedTest1A() {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest1B() {
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ yield ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p1").innerHTML == "hello",
+ "Waited too long for mixed script to run in Test 1");
+ });
+});
+
+// Mixed Display Test - Doorhanger should not appear
+add_task(function* MixedTest2() {
+ var url = gHttpTestRoot2 + "file_bug822367_2.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: false, passiveLoaded: false});
+});
+
+// Mixed Script and Display Test - User Override should cause both the script and the image to load.
+add_task(function* MixedTest3() {
+ var url = gHttpTestRoot + "file_bug822367_3.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest3A() {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest3B() {
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let p1 = ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p1").innerHTML == "hello",
+ "Waited too long for mixed script to run in Test 3");
+ let p2 = ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p2").innerHTML == "bye",
+ "Waited too long for mixed image to load in Test 3");
+ yield Promise.all([ p1, p2 ]);
+ });
+
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: true, activeBlocked: false, passiveLoaded: true});
+});
+
+// Location change - User override on one page doesn't propogate to another page after location change.
+add_task(function* MixedTest4() {
+ var url = gHttpTestRoot2 + "file_bug822367_4.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest4A() {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest4B() {
+ let url = gHttpTestRoot + "file_bug822367_4B.html";
+ yield ContentTask.spawn(gTestBrowser, url, function* (wantedUrl) {
+ yield ContentTaskUtils.waitForCondition(
+ () => content.document.location == wantedUrl,
+ "Waited too long for mixed script to run in Test 4");
+ });
+});
+
+add_task(function* MixedTest4C() {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ yield ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p1").innerHTML == "",
+ "Mixed script loaded in test 4 after location change!");
+ });
+});
+
+// Mixed script attempts to load in a document.open()
+add_task(function* MixedTest5() {
+ var url = gHttpTestRoot + "file_bug822367_5.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest5A() {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest5B() {
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ yield ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("p1").innerHTML == "hello",
+ "Waited too long for mixed script to run in Test 5");
+ });
+});
+
+// Mixed script attempts to load in a document.open() that is within an iframe.
+add_task(function* MixedTest6() {
+ var url = gHttpTestRoot2 + "file_bug822367_6.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest6A() {
+ gTestBrowser.removeEventListener("load", MixedTest6A, true);
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+
+ yield BrowserTestUtils.waitForCondition(
+ () => gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"),
+ "Waited too long for control center to get mixed active blocked state");
+});
+
+add_task(function* MixedTest6B() {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* MixedTest6C() {
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ function test() {
+ try {
+ return content.document.getElementById("f1").contentDocument.getElementById("p1").innerHTML == "hello";
+ } catch (e) {
+ return false;
+ }
+ }
+
+ yield ContentTaskUtils.waitForCondition(test, "Waited too long for mixed script to run in Test 6");
+ });
+});
+
+add_task(function* MixedTest6D() {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: true, activeBlocked: false, passiveLoaded: false});
+});
+
+add_task(function* cleanup() {
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug832435.js b/browser/base/content/test/general/browser_bug832435.js
new file mode 100644
index 000000000..6be2604cd
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug832435.js
@@ -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/. */
+
+function test() {
+ waitForExplicitFinish();
+ ok(true, "Starting up");
+
+ gBrowser.selectedBrowser.focus();
+ gURLBar.addEventListener("focus", function onFocus() {
+ gURLBar.removeEventListener("focus", onFocus);
+ ok(true, "Invoked onfocus handler");
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
+
+ // javscript: URIs are evaluated async.
+ SimpleTest.executeSoon(function() {
+ ok(true, "Evaluated without crashing");
+ finish();
+ });
+ });
+ gURLBar.inputField.value = "javascript: var foo = '11111111'; ";
+ gURLBar.focus();
+}
diff --git a/browser/base/content/test/general/browser_bug839103.js b/browser/base/content/test/general/browser_bug839103.js
new file mode 100644
index 000000000..5240c92ed
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug839103.js
@@ -0,0 +1,120 @@
+const gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+
+add_task(function* test() {
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" },
+ function* (browser) {
+ yield ContentTask.spawn(browser, gTestRoot, testBody);
+ });
+});
+
+// This function runs entirely in the content process. It doesn't have access
+// any free variables in this file.
+function* testBody(testRoot) {
+ const gStyleSheet = "bug839103.css";
+
+ let loaded = ContentTaskUtils.waitForEvent(this, "load", true);
+ content.location = testRoot + "test_bug839103.html";
+
+ yield loaded;
+ function unexpectedContentEvent(event) {
+ ok(false, "Received a " + event.type + " event on content");
+ }
+
+ // We've seen the original stylesheet in the document.
+ // Now add a stylesheet on the fly and make sure we see it.
+ let doc = content.document;
+ doc.styleSheetChangeEventsEnabled = true;
+ doc.addEventListener("StyleSheetAdded", unexpectedContentEvent);
+ doc.addEventListener("StyleSheetRemoved", unexpectedContentEvent);
+ doc.addEventListener("StyleSheetApplicableStateChanged", unexpectedContentEvent);
+ doc.defaultView.addEventListener("StyleSheetAdded", unexpectedContentEvent);
+ doc.defaultView.addEventListener("StyleSheetRemoved", unexpectedContentEvent);
+ doc.defaultView.addEventListener("StyleSheetApplicableStateChanged", unexpectedContentEvent);
+
+ let link = doc.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("type", "text/css");
+ link.setAttribute("href", testRoot + gStyleSheet);
+
+ let sheetAdded =
+ ContentTaskUtils.waitForEvent(this, "StyleSheetAdded", true);
+ let stateChanged =
+ ContentTaskUtils.waitForEvent(this, "StyleSheetApplicableStateChanged", true);
+ doc.body.appendChild(link);
+
+ let evt = yield sheetAdded;
+ info("received dynamic style sheet event");
+ is(evt.type, "StyleSheetAdded", "evt.type has expected value");
+ is(evt.target, doc, "event targets correct document");
+ ok(evt.stylesheet, "evt.stylesheet is defined");
+ ok(evt.stylesheet.toString().includes("CSSStyleSheet"), "evt.stylesheet is a stylesheet");
+ ok(evt.documentSheet, "style sheet is a document sheet");
+
+ evt = yield stateChanged;
+ info("received dynamic style sheet applicable state change event");
+ is(evt.type, "StyleSheetApplicableStateChanged", "evt.type has expected value");
+ is(evt.target, doc, "event targets correct document");
+ is(evt.stylesheet, link.sheet, "evt.stylesheet has the right value");
+ is(evt.applicable, true, "evt.applicable has the right value");
+
+ stateChanged =
+ ContentTaskUtils.waitForEvent(this, "StyleSheetApplicableStateChanged", true);
+ link.disabled = true;
+
+ evt = yield stateChanged;
+ is(evt.type, "StyleSheetApplicableStateChanged", "evt.type has expected value");
+ info("received dynamic style sheet applicable state change event after media=\"\" changed");
+ is(evt.target, doc, "event targets correct document");
+ is(evt.stylesheet, link.sheet, "evt.stylesheet has the right value");
+ is(evt.applicable, false, "evt.applicable has the right value");
+
+ let sheetRemoved =
+ ContentTaskUtils.waitForEvent(this, "StyleSheetRemoved", true);
+ doc.body.removeChild(link);
+
+ evt = yield sheetRemoved;
+ info("received dynamic style sheet removal");
+ is(evt.type, "StyleSheetRemoved", "evt.type has expected value");
+ is(evt.target, doc, "event targets correct document");
+ ok(evt.stylesheet, "evt.stylesheet is defined");
+ ok(evt.stylesheet.toString().includes("CSSStyleSheet"), "evt.stylesheet is a stylesheet");
+ ok(evt.stylesheet.href.includes(gStyleSheet), "evt.stylesheet is the removed stylesheet");
+
+ let ruleAdded =
+ ContentTaskUtils.waitForEvent(this, "StyleRuleAdded", true);
+ doc.querySelector("style").sheet.insertRule("*{color:black}", 0);
+
+ evt = yield ruleAdded;
+ info("received style rule added event");
+ is(evt.type, "StyleRuleAdded", "evt.type has expected value");
+ is(evt.target, doc, "event targets correct document");
+ ok(evt.stylesheet, "evt.stylesheet is defined");
+ ok(evt.stylesheet.toString().includes("CSSStyleSheet"), "evt.stylesheet is a stylesheet");
+ ok(evt.rule, "evt.rule is defined");
+ is(evt.rule.cssText, "* { color: black; }", "evt.rule.cssText has expected value");
+
+ let ruleChanged =
+ ContentTaskUtils.waitForEvent(this, "StyleRuleChanged", true);
+ evt.rule.style.cssText = "color:green";
+
+ evt = yield ruleChanged;
+ ok(true, "received style rule changed event");
+ is(evt.type, "StyleRuleChanged", "evt.type has expected value");
+ is(evt.target, doc, "event targets correct document");
+ ok(evt.stylesheet, "evt.stylesheet is defined");
+ ok(evt.stylesheet.toString().includes("CSSStyleSheet"), "evt.stylesheet is a stylesheet");
+ ok(evt.rule, "evt.rule is defined");
+ is(evt.rule.cssText, "* { color: green; }", "evt.rule.cssText has expected value");
+
+ let ruleRemoved =
+ ContentTaskUtils.waitForEvent(this, "StyleRuleRemoved", true);
+ evt.stylesheet.deleteRule(0);
+
+ evt = yield ruleRemoved;
+ info("received style rule removed event");
+ is(evt.type, "StyleRuleRemoved", "evt.type has expected value");
+ is(evt.target, doc, "event targets correct document");
+ ok(evt.stylesheet, "evt.stylesheet is defined");
+ ok(evt.stylesheet.toString().includes("CSSStyleSheet"), "evt.stylesheet is a stylesheet");
+ ok(evt.rule, "evt.rule is defined");
+}
diff --git a/browser/base/content/test/general/browser_bug882977.js b/browser/base/content/test/general/browser_bug882977.js
new file mode 100644
index 000000000..ed958e06b
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug882977.js
@@ -0,0 +1,29 @@
+"use strict";
+
+/**
+ * Tests that the identity-box shows the chromeUI styling
+ * when viewing about:home in a new window.
+ */
+add_task(function*() {
+ let homepage = "about:home";
+ yield SpecialPowers.pushPrefEnv({
+ "set": [
+ ["browser.startup.homepage", homepage],
+ ["browser.startup.page", 1],
+ ]
+ });
+
+ let win = OpenBrowserWindow();
+ yield BrowserTestUtils.firstBrowserLoaded(win, false);
+
+ let browser = win.gBrowser.selectedBrowser;
+ is(browser.currentURI.spec, homepage, "Loaded the correct homepage");
+ checkIdentityMode(win);
+
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+function checkIdentityMode(win) {
+ let identityMode = win.document.getElementById("identity-box").className;
+ is(identityMode, "chromeUI", "Identity state should be chromeUI for about:home in a new window");
+}
diff --git a/browser/base/content/test/general/browser_bug902156.js b/browser/base/content/test/general/browser_bug902156.js
new file mode 100644
index 000000000..74969ead4
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug902156.js
@@ -0,0 +1,174 @@
+/*
+ * Description of the Tests for
+ * - Bug 902156: Persist "disable protection" option for Mixed Content Blocker
+ *
+ * 1. Navigate to the same domain via document.location
+ * - Load a html page which has mixed content
+ * - Control Center button to disable protection appears - we disable it
+ * - Load a new page from the same origin using document.location
+ * - Control Center button should not appear anymore!
+ *
+ * 2. Navigate to the same domain via simulateclick for a link on the page
+ * - Load a html page which has mixed content
+ * - Control Center button to disable protection appears - we disable it
+ * - Load a new page from the same origin simulating a click
+ * - Control Center button should not appear anymore!
+ *
+ * 3. Navigate to a differnet domain and show the content is still blocked
+ * - Load a different html page which has mixed content
+ * - Control Center button to disable protection should appear again because
+ * we navigated away from html page where we disabled the protection.
+ *
+ * Note, for all tests we set gHttpTestRoot to use 'https'.
+ */
+
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+
+// We alternate for even and odd test cases to simulate different hosts
+const gHttpTestRoot1 = "https://test1.example.com/browser/browser/base/content/test/general/";
+const gHttpTestRoot2 = "https://test2.example.com/browser/browser/base/content/test/general/";
+
+var origBlockActive;
+var gTestBrowser = null;
+
+registerCleanupFunction(function() {
+ // Set preferences back to their original values
+ Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive);
+});
+
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+ finish();
+}
+
+// ------------------------ Test 1 ------------------------------
+
+function test1A() {
+ BrowserTestUtils.browserLoaded(gTestBrowser).then(test1B);
+
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ // Disable Mixed Content Protection for the page (and reload)
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+}
+
+function test1B() {
+ var expected = "Mixed Content Blocker disabled";
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test1C, "Error: Waited too long for mixed script to run in Test 1B");
+}
+
+function test1C() {
+ var actual = content.document.getElementById('mctestdiv').innerHTML;
+ is(actual, "Mixed Content Blocker disabled", "OK: Executed mixed script in Test 1C");
+
+ // The Script loaded after we disabled the page, now we are going to reload the
+ // page and see if our decision is persistent
+ BrowserTestUtils.browserLoaded(gTestBrowser).then(test1D);
+
+ var url = gHttpTestRoot1 + "file_bug902156_2.html";
+ gTestBrowser.loadURI(url);
+}
+
+function test1D() {
+ // The Control Center button should appear but isMixedContentBlocked should be NOT true,
+ // because our decision of disabling the mixed content blocker is persistent.
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: true, activeBlocked: false, passiveLoaded: false});
+
+ var actual = content.document.getElementById('mctestdiv').innerHTML;
+ is(actual, "Mixed Content Blocker disabled", "OK: Executed mixed script in Test 1D");
+
+ // move on to Test 2
+ test2();
+}
+
+// ------------------------ Test 2 ------------------------------
+
+function test2() {
+ BrowserTestUtils.browserLoaded(gTestBrowser).then(test2A);
+ var url = gHttpTestRoot2 + "file_bug902156_2.html";
+ gTestBrowser.loadURI(url);
+}
+
+function test2A() {
+ BrowserTestUtils.browserLoaded(gTestBrowser).then(test2B);
+
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ // Disable Mixed Content Protection for the page (and reload)
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+}
+
+function test2B() {
+ var expected = "Mixed Content Blocker disabled";
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test2C, "Error: Waited too long for mixed script to run in Test 2B");
+}
+
+function test2C() {
+ var actual = content.document.getElementById('mctestdiv').innerHTML;
+ is(actual, "Mixed Content Blocker disabled", "OK: Executed mixed script in Test 2C");
+
+ // The Script loaded after we disabled the page, now we are going to reload the
+ // page and see if our decision is persistent
+ BrowserTestUtils.browserLoaded(gTestBrowser).then(test2D);
+
+ // reload the page using the provided link in the html file
+ var mctestlink = content.document.getElementById("mctestlink");
+ mctestlink.click();
+}
+
+function test2D() {
+ // The Control Center button should appear but isMixedContentBlocked should be NOT true,
+ // because our decision of disabling the mixed content blocker is persistent.
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: true, activeBlocked: false, passiveLoaded: false});
+
+ var actual = content.document.getElementById('mctestdiv').innerHTML;
+ is(actual, "Mixed Content Blocker disabled", "OK: Executed mixed script in Test 2D");
+
+ // move on to Test 3
+ test3();
+}
+
+// ------------------------ Test 3 ------------------------------
+
+function test3() {
+ BrowserTestUtils.browserLoaded(gTestBrowser).then(test3A);
+ var url = gHttpTestRoot1 + "file_bug902156_3.html";
+ gTestBrowser.loadURI(url);
+}
+
+function test3A() {
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ // We are done with tests, clean up
+ cleanUpAfterTests();
+}
+
+// ------------------------------------------------------
+
+function test() {
+ // Performing async calls, e.g. 'onload', we have to wait till all of them finished
+ waitForExplicitFinish();
+
+ // Store original preferences so we can restore settings after testing
+ origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE);
+
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+
+ // Not really sure what this is doing
+ var newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ newTab.linkedBrowser.stop()
+
+ // Starting Test Number 1:
+ BrowserTestUtils.browserLoaded(gTestBrowser).then(test1A);
+ var url = gHttpTestRoot1 + "file_bug902156_1.html";
+ gTestBrowser.loadURI(url);
+}
diff --git a/browser/base/content/test/general/browser_bug906190.js b/browser/base/content/test/general/browser_bug906190.js
new file mode 100644
index 000000000..613f50efd
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug906190.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the persistence of the "disable protection" option for Mixed Content
+ * Blocker in child tabs (bug 906190).
+ */
+
+requestLongerTimeout(2);
+
+// We use the different urls for testing same origin checks before allowing
+// mixed content on child tabs.
+const gHttpTestRoot1 = "https://test1.example.com/browser/browser/base/content/test/general/";
+const gHttpTestRoot2 = "https://test2.example.com/browser/browser/base/content/test/general/";
+
+/**
+ * For all tests, we load the pages over HTTPS and test both:
+ * - |CTRL+CLICK|
+ * - |RIGHT CLICK -> OPEN LINK IN TAB|
+ */
+function* doTest(parentTabSpec, childTabSpec, testTaskFn, waitForMetaRefresh) {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: parentTabSpec,
+ }, function* (browser) {
+ // As a sanity check, test that active content has been blocked as expected.
+ yield assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: false, activeBlocked: true, passiveLoaded: false,
+ });
+
+ // Disable the Mixed Content Blocker for the page, which reloads it.
+ let promiseReloaded = BrowserTestUtils.browserLoaded(browser);
+ gIdentityHandler.disableMixedContentProtection();
+ yield promiseReloaded;
+
+ // Wait for the script in the page to update the contents of the test div.
+ let testDiv = content.document.getElementById('mctestdiv');
+ yield promiseWaitForCondition(
+ () => testDiv.innerHTML == "Mixed Content Blocker disabled");
+
+ // Add the link for the child tab to the page.
+ let mainDiv = content.document.createElement("div");
+ mainDiv.innerHTML =
+ '<p><a id="linkToOpenInNewTab" href="' + childTabSpec + '">Link</a></p>';
+ content.document.body.appendChild(mainDiv);
+
+ // Execute the test in the child tabs with the two methods to open it.
+ for (let openFn of [simulateCtrlClick, simulateContextMenuOpenInTab]) {
+ let promiseTabLoaded = waitForSomeTabToLoad();
+ openFn(browser);
+ yield promiseTabLoaded;
+ gBrowser.selectTabAtIndex(2);
+
+ if (waitForMetaRefresh) {
+ yield waitForSomeTabToLoad();
+ }
+
+ yield testTaskFn();
+
+ gBrowser.removeCurrentTab();
+ }
+ });
+}
+
+function simulateCtrlClick(browser) {
+ BrowserTestUtils.synthesizeMouseAtCenter("#linkToOpenInNewTab",
+ { ctrlKey: true, metaKey: true },
+ browser);
+}
+
+function simulateContextMenuOpenInTab(browser) {
+ BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+ // These are operations that must be executed synchronously with the event.
+ document.getElementById("context-openlinkintab").doCommand();
+ event.target.hidePopup();
+ return true;
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter("#linkToOpenInNewTab",
+ { type: "contextmenu", button: 2 },
+ browser);
+}
+
+// Waits for a load event somewhere in the browser but ignore events coming
+// from <xul:browser>s without a tab assigned. That are most likely browsers
+// that preload the new tab page.
+function waitForSomeTabToLoad() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener("load", function onLoad(event) {
+ let tab = gBrowser._getTabForContentWindow(event.target.defaultView.top);
+ if (tab) {
+ gBrowser.removeEventListener("load", onLoad, true);
+ resolve();
+ }
+ }, true);
+ });
+}
+
+/**
+ * Ensure the Mixed Content Blocker is enabled.
+ */
+add_task(function* test_initialize() {
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({
+ "set": [["security.mixed_content.block_active_content", true]],
+ }, resolve));
+});
+
+/**
+ * 1. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a subpage from the same origin in a new tab simulating a click
+ * - Doorhanger should >> NOT << appear anymore!
+ */
+add_task(function* test_same_origin() {
+ yield doTest(gHttpTestRoot1 + "file_bug906190_1.html",
+ gHttpTestRoot1 + "file_bug906190_2.html", function* () {
+ // The doorhanger should appear but activeBlocked should be >> NOT << true,
+ // because our decision of disabling the mixed content blocker is persistent
+ // across tabs.
+ yield assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: true, activeBlocked: false, passiveLoaded: false,
+ });
+
+ is(content.document.getElementById('mctestdiv').innerHTML,
+ "Mixed Content Blocker disabled", "OK: Executed mixed script");
+ });
+});
+
+/**
+ * 2. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from a different origin in a new tab simulating a click
+ * - Doorhanger >> SHOULD << appear again!
+ */
+add_task(function* test_different_origin() {
+ yield doTest(gHttpTestRoot1 + "file_bug906190_2.html",
+ gHttpTestRoot2 + "file_bug906190_2.html", function* () {
+ // The doorhanger should appear and activeBlocked should be >> TRUE <<,
+ // because our decision of disabling the mixed content blocker should only
+ // persist if pages are from the same domain.
+ yield assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: false, activeBlocked: true, passiveLoaded: false,
+ });
+
+ is(content.document.getElementById('mctestdiv').innerHTML,
+ "Mixed Content Blocker enabled", "OK: Blocked mixed script");
+ });
+});
+
+/**
+ * 3. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from the same origin in a new tab simulating a click
+ * - Redirect to another page from the same origin using meta-refresh
+ * - Doorhanger should >> NOT << appear again!
+ */
+add_task(function* test_same_origin_metarefresh_same_origin() {
+ // file_bug906190_3_4.html redirects to page test1.example.com/* using meta-refresh
+ yield doTest(gHttpTestRoot1 + "file_bug906190_1.html",
+ gHttpTestRoot1 + "file_bug906190_3_4.html", function* () {
+ // The doorhanger should appear but activeBlocked should be >> NOT << true!
+ yield assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: true, activeBlocked: false, passiveLoaded: false,
+ });
+
+ is(content.document.getElementById('mctestdiv').innerHTML,
+ "Mixed Content Blocker disabled", "OK: Executed mixed script");
+ }, true);
+});
+
+/**
+ * 4. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from the same origin in a new tab simulating a click
+ * - Redirect to another page from a different origin using meta-refresh
+ * - Doorhanger >> SHOULD << appear again!
+ */
+add_task(function* test_same_origin_metarefresh_different_origin() {
+ yield doTest(gHttpTestRoot2 + "file_bug906190_1.html",
+ gHttpTestRoot2 + "file_bug906190_3_4.html", function* () {
+ // The doorhanger should appear and activeBlocked should be >> TRUE <<.
+ yield assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: false, activeBlocked: true, passiveLoaded: false,
+ });
+
+ is(content.document.getElementById('mctestdiv').innerHTML,
+ "Mixed Content Blocker enabled", "OK: Blocked mixed script");
+ }, true);
+});
+
+/**
+ * 5. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from the same origin in a new tab simulating a click
+ * - Redirect to another page from the same origin using 302 redirect
+ */
+add_task(function* test_same_origin_302redirect_same_origin() {
+ // the sjs files returns a 302 redirect- note, same origins
+ yield doTest(gHttpTestRoot1 + "file_bug906190_1.html",
+ gHttpTestRoot1 + "file_bug906190.sjs", function* () {
+ // The doorhanger should appear but activeBlocked should be >> NOT << true.
+ // Currently it is >> TRUE << - see follow up bug 914860
+ ok(!gIdentityHandler._identityBox.classList.contains("mixedActiveBlocked"),
+ "OK: Mixed Content is NOT being blocked");
+
+ is(content.document.getElementById('mctestdiv').innerHTML,
+ "Mixed Content Blocker disabled", "OK: Executed mixed script");
+ });
+});
+
+/**
+ * 6. - Load a html page which has mixed content
+ * - Doorhanger to disable protection appears - we disable it
+ * - Load a new page from the same origin in a new tab simulating a click
+ * - Redirect to another page from a different origin using 302 redirect
+ */
+add_task(function* test_same_origin_302redirect_different_origin() {
+ // the sjs files returns a 302 redirect - note, different origins
+ yield doTest(gHttpTestRoot2 + "file_bug906190_1.html",
+ gHttpTestRoot2 + "file_bug906190.sjs", function* () {
+ // The doorhanger should appear and activeBlocked should be >> TRUE <<.
+ yield assertMixedContentBlockingState(gBrowser, {
+ activeLoaded: false, activeBlocked: true, passiveLoaded: false,
+ });
+
+ is(content.document.getElementById('mctestdiv').innerHTML,
+ "Mixed Content Blocker enabled", "OK: Blocked mixed script");
+ });
+});
+
+/**
+ * 7. - Test memory leak issue on redirection error. See Bug 1269426.
+ */
+add_task(function* test_bad_redirection() {
+ // the sjs files returns a 302 redirect - note, different origins
+ yield doTest(gHttpTestRoot2 + "file_bug906190_1.html",
+ gHttpTestRoot2 + "file_bug906190.sjs?bad-redirection=1", function* () {
+ // Nothing to do. Just see if memory leak is reported in the end.
+ ok(true, "Nothing to do");
+ });
+});
diff --git a/browser/base/content/test/general/browser_bug963945.js b/browser/base/content/test/general/browser_bug963945.js
new file mode 100644
index 000000000..4531964b0
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug963945.js
@@ -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/. */
+
+/*
+ * This test ensures the about:addons tab is only
+ * opened one time when in private browsing.
+ */
+
+add_task(function* test() {
+ let win = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+
+ let tab = win.gBrowser.selectedTab = win.gBrowser.addTab("about:addons");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield promiseWaitForFocus(win);
+
+ EventUtils.synthesizeKey("a", { ctrlKey: true, shiftKey: true }, win);
+
+ is(win.gBrowser.tabs.length, 2, "about:addons tab was re-focused.");
+ is(win.gBrowser.currentURI.spec, "about:addons", "Addons tab was opened.");
+
+ yield BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/general/browser_bug970746.js b/browser/base/content/test/general/browser_bug970746.js
new file mode 100644
index 000000000..623623e55
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug970746.js
@@ -0,0 +1,121 @@
+/* Make sure context menu includes option to search hyperlink text on search engine */
+
+add_task(function *() {
+ const url = "http://mochi.test:8888/browser/browser/base/content/test/general/browser_bug970746.xhtml";
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ const ellipsis = "\u2026";
+
+ let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+
+ // Tests if the "Search <engine> for '<some terms>'" context menu item is shown for the
+ // given query string of an element. Tests to make sure label includes the proper search terms.
+ //
+ // Each test:
+ //
+ // id: The id of the element to test.
+ // isSelected: Flag to enable selecting (text highlight) the contents of the element
+ // shouldBeShown: The display state of the menu item
+ // expectedLabelContents: The menu item label should contain a portion of this string.
+ // Will only be tested if shouldBeShown is true.
+ let tests = [
+ {
+ id: "link",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm a link!",
+ },
+ {
+ id: "link",
+ isSelected: false,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm a link!",
+ },
+ {
+ id: "longLink",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm a really lo" + ellipsis,
+ },
+ {
+ id: "longLink",
+ isSelected: false,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm a really lo" + ellipsis,
+ },
+ {
+ id: "plainText",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "Right clicking " + ellipsis,
+ },
+ {
+ id: "plainText",
+ isSelected: false,
+ shouldBeShown: false,
+ },
+ {
+ id: "mixedContent",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm some text, " + ellipsis,
+ },
+ {
+ id: "mixedContent",
+ isSelected: false,
+ shouldBeShown: false,
+ },
+ {
+ id: "partialLink",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "link selection",
+ },
+ {
+ id: "partialLink",
+ isSelected: false,
+ shouldBeShown: true,
+ expectedLabelContents: "A partial link " + ellipsis,
+ },
+ {
+ id: "surrogatePair",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "This character\uD83D\uDD25" + ellipsis,
+ }
+ ];
+
+ for (let test of tests) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser,
+ { selectElement: test.isSelected ? test.id : null },
+ function* (arg) {
+ let selection = content.getSelection();
+ selection.removeAllRanges();
+
+ if (arg.selectElement) {
+ selection.selectAllChildren(content.document.getElementById(arg.selectElement));
+ }
+ });
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#" + test.id,
+ { type: "contextmenu", button: 2}, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ let menuItem = document.getElementById("context-searchselect");
+ is(menuItem.hidden, !test.shouldBeShown,
+ "search context menu item is shown for '#" + test.id + "' and selected is '" + test.isSelected + "'");
+
+ if (test.shouldBeShown) {
+ ok(menuItem.label.includes(test.expectedLabelContents),
+ "Menu item text '" + menuItem.label + "' contains the correct search terms '" + test.expectedLabelContents + "'");
+ }
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
+ contentAreaContextMenu.hidePopup();
+ yield popupHiddenPromise;
+ }
+
+ // cleanup
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_bug970746.xhtml b/browser/base/content/test/general/browser_bug970746.xhtml
new file mode 100644
index 000000000..9d78d7147
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug970746.xhtml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <body>
+ <a href="http://mozilla.org" id="link">I'm a link!</a>
+ <a href="http://mozilla.org" id="longLink">I'm a really long link and I should be truncated.</a>
+
+ <span id="plainText">
+ Right clicking me when I'm selected should show the menu item.
+ </span>
+ <span id="mixedContent">
+ I'm some text, and <a href="http://mozilla.org">I'm a link!</a>
+ </span>
+
+ <a href="http://mozilla.org">A partial <span id="partialLink">link selection</span></a>
+
+ <span id="surrogatePair">
+ This character🔥 shouldn't be truncated.
+ </span>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/browser_clipboard.js b/browser/base/content/test/general/browser_clipboard.js
new file mode 100644
index 000000000..33c6de52d
--- /dev/null
+++ b/browser/base/content/test/general/browser_clipboard.js
@@ -0,0 +1,174 @@
+// This test is used to check copy and paste in editable areas to ensure that non-text
+// types (html and images) are copied to and pasted from the clipboard properly.
+
+var testPage = "<body style='margin: 0'>" +
+ " <img id='img' tabindex='1' src='http://example.org/browser/browser/base/content/test/general/moz.png'>" +
+ " <div id='main' contenteditable='true'>Test <b>Bold</b> After Text</div>" +
+ "</body>";
+
+add_task(function*() {
+ let tab = gBrowser.addTab();
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ gBrowser.selectedTab = tab;
+
+ yield promiseTabLoadEvent(tab, "data:text/html," + escape(testPage));
+ yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW);
+
+ const modifier = (navigator.platform.indexOf("Mac") >= 0) ?
+ Components.interfaces.nsIDOMWindowUtils.MODIFIER_META :
+ Components.interfaces.nsIDOMWindowUtils.MODIFIER_CONTROL;
+
+ // On windows, HTML clipboard includes extra data.
+ // The values are from widget/windows/nsDataObj.cpp.
+ const htmlPrefix = (navigator.platform.indexOf("Win") >= 0) ? "<html><body>\n<!--StartFragment-->" : "";
+ const htmlPostfix = (navigator.platform.indexOf("Win") >= 0) ? "<!--EndFragment-->\n</body>\n</html>" : "";
+
+ yield ContentTask.spawn(browser, { modifier, htmlPrefix, htmlPostfix }, function* (arg) {
+ var doc = content.document;
+ var main = doc.getElementById("main");
+ main.focus();
+
+ const utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+ function sendKey(key) {
+ if (utils.sendKeyEvent("keydown", key, 0, arg.modifier)) {
+ utils.sendKeyEvent("keypress", key, key.charCodeAt(0), arg.modifier);
+ }
+ utils.sendKeyEvent("keyup", key, 0, arg.modifier);
+ }
+
+ // Select an area of the text.
+ let selection = doc.getSelection();
+ selection.modify("move", "left", "line");
+ selection.modify("move", "right", "character");
+ selection.modify("move", "right", "character");
+ selection.modify("move", "right", "character");
+ selection.modify("extend", "right", "word");
+ selection.modify("extend", "right", "word");
+
+ yield new Promise((resolve, reject) => {
+ addEventListener("copy", function copyEvent(event) {
+ removeEventListener("copy", copyEvent, true);
+ // The data is empty as the selection is copied during the event default phase.
+ Assert.equal(event.clipboardData.mozItemCount, 0, "Zero items on clipboard");
+ resolve();
+ }, true)
+
+ sendKey("c");
+ });
+
+ selection.modify("move", "right", "line");
+
+ yield new Promise((resolve, reject) => {
+ addEventListener("paste", function copyEvent(event) {
+ removeEventListener("paste", copyEvent, true);
+ let clipboardData = event.clipboardData;
+ Assert.equal(clipboardData.mozItemCount, 1, "One item on clipboard");
+ Assert.equal(clipboardData.types.length, 2, "Two types on clipboard");
+ Assert.equal(clipboardData.types[0], "text/html", "text/html on clipboard");
+ Assert.equal(clipboardData.types[1], "text/plain", "text/plain on clipboard");
+ Assert.equal(clipboardData.getData("text/html"), arg.htmlPrefix +
+ "t <b>Bold</b>" + arg.htmlPostfix, "text/html value");
+ Assert.equal(clipboardData.getData("text/plain"), "t Bold", "text/plain value");
+ resolve();
+ }, true)
+ sendKey("v");
+ });
+
+ Assert.equal(main.innerHTML, "Test <b>Bold</b> After Textt <b>Bold</b>", "Copy and paste html");
+
+ selection.modify("extend", "left", "word");
+ selection.modify("extend", "left", "word");
+ selection.modify("extend", "left", "character");
+
+ yield new Promise((resolve, reject) => {
+ addEventListener("cut", function copyEvent(event) {
+ removeEventListener("cut", copyEvent, true);
+ event.clipboardData.setData("text/plain", "Some text");
+ event.clipboardData.setData("text/html", "<i>Italic</i> ");
+ selection.deleteFromDocument();
+ event.preventDefault();
+ resolve();
+ }, true)
+ sendKey("x");
+ });
+
+ selection.modify("move", "left", "line");
+
+ yield new Promise((resolve, reject) => {
+ addEventListener("paste", function copyEvent(event) {
+ removeEventListener("paste", copyEvent, true);
+ let clipboardData = event.clipboardData;
+ Assert.equal(clipboardData.mozItemCount, 1, "One item on clipboard 2");
+ Assert.equal(clipboardData.types.length, 2, "Two types on clipboard 2");
+ Assert.equal(clipboardData.types[0], "text/html", "text/html on clipboard 2");
+ Assert.equal(clipboardData.types[1], "text/plain", "text/plain on clipboard 2");
+ Assert.equal(clipboardData.getData("text/html"), arg.htmlPrefix +
+ "<i>Italic</i> " + arg.htmlPostfix, "text/html value 2");
+ Assert.equal(clipboardData.getData("text/plain"), "Some text", "text/plain value 2");
+ resolve();
+ }, true)
+ sendKey("v");
+ });
+
+ Assert.equal(main.innerHTML, "<i>Italic</i> Test <b>Bold</b> After<b></b>",
+ "Copy and paste html 2");
+ });
+
+ // Next, check that the Copy Image command works.
+
+ // The context menu needs to be opened to properly initialize for the copy
+ // image command to run.
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let contextMenuShown = promisePopupShown(contextMenu);
+ BrowserTestUtils.synthesizeMouseAtCenter("#img", { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
+ yield contextMenuShown;
+
+ document.getElementById("context-copyimage-contents").doCommand();
+
+ contextMenu.hidePopup();
+ yield promisePopupHidden(contextMenu);
+
+ // Focus the content again
+ yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW);
+
+ yield ContentTask.spawn(browser, { modifier, htmlPrefix, htmlPostfix }, function* (arg) {
+ var doc = content.document;
+ var main = doc.getElementById("main");
+ main.focus();
+
+ yield new Promise((resolve, reject) => {
+ addEventListener("paste", function copyEvent(event) {
+ removeEventListener("paste", copyEvent, true);
+ let clipboardData = event.clipboardData;
+
+ // DataTransfer doesn't support the image types yet, so only text/html
+ // will be present.
+ if (clipboardData.getData("text/html") !== arg.htmlPrefix +
+ '<img id="img" tabindex="1" src="http://example.org/browser/browser/base/content/test/general/moz.png">' +
+ arg.htmlPostfix) {
+ reject('Clipboard Data did not contain an image, was ' + clipboardData.getData("text/html"));
+ }
+ resolve();
+ }, true)
+
+ const utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+ if (utils.sendKeyEvent("keydown", "v", 0, arg.modifier)) {
+ utils.sendKeyEvent("keypress", "v", "v".charCodeAt(0), arg.modifier);
+ }
+ utils.sendKeyEvent("keyup", "v", 0, arg.modifier);
+ });
+
+ // The new content should now include an image.
+ Assert.equal(main.innerHTML, '<i>Italic</i> <img id="img" tabindex="1" ' +
+ 'src="http://example.org/browser/browser/base/content/test/general/moz.png">' +
+ 'Test <b>Bold</b> After<b></b>', "Paste after copy image");
+ });
+
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/browser/base/content/test/general/browser_clipboard_pastefile.js b/browser/base/content/test/general/browser_clipboard_pastefile.js
new file mode 100644
index 000000000..fe87284f3
--- /dev/null
+++ b/browser/base/content/test/general/browser_clipboard_pastefile.js
@@ -0,0 +1,62 @@
+// This test is used to check that pasting files removes all non-file data from
+// event.clipboardData.
+
+add_task(function*() {
+ var textbox = document.createElement("textbox");
+ document.documentElement.appendChild(textbox);
+
+ textbox.focus();
+ textbox.value = "Text";
+ textbox.select();
+
+ yield new Promise((resolve, reject) => {
+ textbox.addEventListener("copy", function copyEvent(event) {
+ textbox.removeEventListener("copy", copyEvent, true);
+ event.clipboardData.setData("text/plain", "Alternate");
+ // For this test, it doesn't matter that the file isn't actually a file.
+ event.clipboardData.setData("application/x-moz-file", "Sample");
+ event.preventDefault();
+ resolve();
+ }, true)
+
+ EventUtils.synthesizeKey("c", { accelKey: true });
+ });
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser,
+ "https://example.com/browser/browser/base/content/test/general/clipboard_pastefile.html");
+ let browser = tab.linkedBrowser;
+
+ yield ContentTask.spawn(browser, { }, function* (arg) {
+ content.document.getElementById("input").focus();
+ });
+
+ yield BrowserTestUtils.synthesizeKey("v", { accelKey: true }, browser);
+
+ let output = yield ContentTask.spawn(browser, { }, function* (arg) {
+ return content.document.getElementById("output").textContent;
+ });
+ is (output, "Passed", "Paste file");
+
+ textbox.focus();
+
+ yield new Promise((resolve, reject) => {
+ textbox.addEventListener("paste", function copyEvent(event) {
+ textbox.removeEventListener("paste", copyEvent, true);
+
+ let dt = event.clipboardData;
+ is(dt.types.length, 3, "number of types");
+ ok(dt.types.includes("text/plain"), "text/plain exists in types");
+ ok(dt.mozTypesAt(0).contains("text/plain"), "text/plain exists in mozTypesAt");
+ is(dt.getData("text/plain"), "Alternate", "text/plain returned in getData");
+ is(dt.mozGetDataAt("text/plain", 0), "Alternate", "text/plain returned in mozGetDataAt");
+
+ resolve();
+ }, true);
+
+ EventUtils.synthesizeKey("v", { accelKey: true });
+ });
+
+ document.documentElement.removeChild(textbox);
+
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_contentAltClick.js b/browser/base/content/test/general/browser_contentAltClick.js
new file mode 100644
index 000000000..1a3b0fccc
--- /dev/null
+++ b/browser/base/content/test/general/browser_contentAltClick.js
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 for Bug 1109146.
+ * The tests opens a new tab and alt + clicks to download files
+ * and confirms those files are on the download list.
+ *
+ * The difference between this and the test "browser_contentAreaClick.js" is that
+ * the code path in e10s uses ContentClick.jsm instead of browser.js::contentAreaClick() util.
+ */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+
+function setup() {
+ gPrefService.setBoolPref("browser.altClickSave", true);
+
+ let testPage =
+ 'data:text/html,' +
+ '<p><a id="commonlink" href="http://mochi.test/moz/">Common link</a></p>' +
+ '<p><math id="mathxlink" xmlns="http://www.w3.org/1998/Math/MathML" xlink:type="simple" xlink:href="http://mochi.test/moz/"><mtext>MathML XLink</mtext></math></p>' +
+ '<p><svg id="svgxlink" xmlns="http://www.w3.org/2000/svg" width="100px" height="50px" version="1.1"><a xlink:type="simple" xlink:href="http://mochi.test/moz/"><text transform="translate(10, 25)">SVG XLink</text></a></svg></p>';
+
+ return BrowserTestUtils.openNewForegroundTab(gBrowser, testPage);
+}
+
+function* clean_up() {
+ // Remove downloads.
+ let downloadList = yield Downloads.getList(Downloads.ALL);
+ let downloads = yield downloadList.getAll();
+ for (let download of downloads) {
+ yield downloadList.remove(download);
+ yield download.finalize(true);
+ }
+ // Remove download history.
+ yield PlacesTestUtils.clearHistory();
+
+ gPrefService.clearUserPref("browser.altClickSave");
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
+add_task(function* test_alt_click()
+{
+ yield setup();
+
+ let downloadList = yield Downloads.getList(Downloads.ALL);
+ let downloads = [];
+ let downloadView;
+ // When 1 download has been attempted then resolve the promise.
+ let finishedAllDownloads = new Promise( (resolve) => {
+ downloadView = {
+ onDownloadAdded: function (aDownload) {
+ downloads.push(aDownload);
+ resolve();
+ },
+ };
+ });
+ yield downloadList.addView(downloadView);
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#commonlink", {altKey: true}, gBrowser.selectedBrowser);
+
+ // Wait for all downloads to be added to the download list.
+ yield finishedAllDownloads;
+ yield downloadList.removeView(downloadView);
+
+ is(downloads.length, 1, "1 downloads");
+ is(downloads[0].source.url, "http://mochi.test/moz/", "Downloaded #commonlink element");
+
+ yield* clean_up();
+});
+
+add_task(function* test_alt_click_on_xlinks()
+{
+ yield setup();
+
+ let downloadList = yield Downloads.getList(Downloads.ALL);
+ let downloads = [];
+ let downloadView;
+ // When all 2 downloads have been attempted then resolve the promise.
+ let finishedAllDownloads = new Promise( (resolve) => {
+ downloadView = {
+ onDownloadAdded: function (aDownload) {
+ downloads.push(aDownload);
+ if (downloads.length == 2) {
+ resolve();
+ }
+ },
+ };
+ });
+ yield downloadList.addView(downloadView);
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#mathxlink", {altKey: true}, gBrowser.selectedBrowser);
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#svgxlink", {altKey: true}, gBrowser.selectedBrowser);
+
+ // Wait for all downloads to be added to the download list.
+ yield finishedAllDownloads;
+ yield downloadList.removeView(downloadView);
+
+ is(downloads.length, 2, "2 downloads");
+ is(downloads[0].source.url, "http://mochi.test/moz/", "Downloaded #mathxlink element");
+ is(downloads[1].source.url, "http://mochi.test/moz/", "Downloaded #svgxlink element");
+
+ yield* clean_up();
+});
diff --git a/browser/base/content/test/general/browser_contentAreaClick.js b/browser/base/content/test/general/browser_contentAreaClick.js
new file mode 100644
index 000000000..facdfb498
--- /dev/null
+++ b/browser/base/content/test/general/browser_contentAreaClick.js
@@ -0,0 +1,307 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 for bug 549340.
+ * Test for browser.js::contentAreaClick() util.
+ *
+ * The test opens a new browser window, then replaces browser.js methods invoked
+ * by contentAreaClick with a mock function that tracks which methods have been
+ * called.
+ * Each sub-test synthesizes a mouse click event on links injected in content,
+ * the event is collected by a click handler that ensures that contentAreaClick
+ * correctly prevent default events, and follows the correct code path.
+ */
+
+var gTests = [
+
+ {
+ desc: "Simple left click",
+ setup: function() {},
+ clean: function() {},
+ event: {},
+ targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ],
+ expectedInvokedMethods: [],
+ preventDefault: false,
+ },
+
+ {
+ desc: "Ctrl/Cmd left click",
+ setup: function() {},
+ clean: function() {},
+ event: { ctrlKey: true,
+ metaKey: true },
+ targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ],
+ expectedInvokedMethods: [ "urlSecurityCheck", "openLinkIn" ],
+ preventDefault: true,
+ },
+
+ // The next test was once handling feedService.forcePreview(). Now it should
+ // just be like Alt click.
+ {
+ desc: "Shift+Alt left click",
+ setup: function() {
+ gPrefService.setBoolPref("browser.altClickSave", true);
+ },
+ clean: function() {
+ gPrefService.clearUserPref("browser.altClickSave");
+ },
+ event: { shiftKey: true,
+ altKey: true },
+ targets: [ "commonlink", "maplink" ],
+ expectedInvokedMethods: [ "gatherTextUnder", "saveURL" ],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Shift+Alt left click on XLinks",
+ setup: function() {
+ gPrefService.setBoolPref("browser.altClickSave", true);
+ },
+ clean: function() {
+ gPrefService.clearUserPref("browser.altClickSave");
+ },
+ event: { shiftKey: true,
+ altKey: true },
+ targets: [ "mathxlink", "svgxlink"],
+ expectedInvokedMethods: [ "saveURL" ],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Shift click",
+ setup: function() {},
+ clean: function() {},
+ event: { shiftKey: true },
+ targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ],
+ expectedInvokedMethods: [ "urlSecurityCheck", "openLinkIn" ],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Alt click",
+ setup: function() {
+ gPrefService.setBoolPref("browser.altClickSave", true);
+ },
+ clean: function() {
+ gPrefService.clearUserPref("browser.altClickSave");
+ },
+ event: { altKey: true },
+ targets: [ "commonlink", "maplink" ],
+ expectedInvokedMethods: [ "gatherTextUnder", "saveURL" ],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Alt click on XLinks",
+ setup: function() {
+ gPrefService.setBoolPref("browser.altClickSave", true);
+ },
+ clean: function() {
+ gPrefService.clearUserPref("browser.altClickSave");
+ },
+ event: { altKey: true },
+ targets: [ "mathxlink", "svgxlink" ],
+ expectedInvokedMethods: [ "saveURL" ],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Panel click",
+ setup: function() {},
+ clean: function() {},
+ event: {},
+ targets: [ "panellink" ],
+ expectedInvokedMethods: [ "urlSecurityCheck", "loadURI" ],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Simple middle click opentab",
+ setup: function() {},
+ clean: function() {},
+ event: { button: 1 },
+ targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ],
+ expectedInvokedMethods: [ "urlSecurityCheck", "openLinkIn" ],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Simple middle click openwin",
+ setup: function() {
+ gPrefService.setBoolPref("browser.tabs.opentabfor.middleclick", false);
+ },
+ clean: function() {
+ gPrefService.clearUserPref("browser.tabs.opentabfor.middleclick");
+ },
+ event: { button: 1 },
+ targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ],
+ expectedInvokedMethods: [ "urlSecurityCheck", "openLinkIn" ],
+ preventDefault: true,
+ },
+
+ {
+ desc: "Middle mouse paste",
+ setup: function() {
+ gPrefService.setBoolPref("middlemouse.contentLoadURL", true);
+ gPrefService.setBoolPref("general.autoScroll", false);
+ },
+ clean: function() {
+ gPrefService.clearUserPref("middlemouse.contentLoadURL");
+ gPrefService.clearUserPref("general.autoScroll");
+ },
+ event: { button: 1 },
+ targets: [ "emptylink" ],
+ expectedInvokedMethods: [ "middleMousePaste" ],
+ preventDefault: true,
+ },
+
+];
+
+// Array of method names that will be replaced in the new window.
+var gReplacedMethods = [
+ "middleMousePaste",
+ "urlSecurityCheck",
+ "loadURI",
+ "gatherTextUnder",
+ "saveURL",
+ "openLinkIn",
+ "getShortcutOrURIAndPostData",
+];
+
+// Reference to the new window.
+var gTestWin = null;
+
+// List of methods invoked by a specific call to contentAreaClick.
+var gInvokedMethods = [];
+
+// The test currently running.
+var gCurrentTest = null;
+
+function test() {
+ waitForExplicitFinish();
+
+ gTestWin = openDialog(location, "", "chrome,all,dialog=no", "about:blank");
+ whenDelayedStartupFinished(gTestWin, function () {
+ info("Browser window opened");
+ waitForFocus(function() {
+ info("Browser window focused");
+ waitForFocus(function() {
+ info("Setting up browser...");
+ setupTestBrowserWindow();
+ info("Running tests...");
+ executeSoon(runNextTest);
+ }, gTestWin.content, true);
+ }, gTestWin);
+ });
+}
+
+// Click handler used to steal click events.
+var gClickHandler = {
+ handleEvent: function (event) {
+ let linkId = event.target.id || event.target.localName;
+ is(event.type, "click",
+ gCurrentTest.desc + ":Handler received a click event on " + linkId);
+
+ let isPanelClick = linkId == "panellink";
+ gTestWin.contentAreaClick(event, isPanelClick);
+ let prevent = event.defaultPrevented;
+ is(prevent, gCurrentTest.preventDefault,
+ gCurrentTest.desc + ": event.defaultPrevented is correct (" + prevent + ")")
+
+ // Check that all required methods have been called.
+ gCurrentTest.expectedInvokedMethods.forEach(function(aExpectedMethodName) {
+ isnot(gInvokedMethods.indexOf(aExpectedMethodName), -1,
+ gCurrentTest.desc + ":" + aExpectedMethodName + " was invoked");
+ });
+
+ if (gInvokedMethods.length != gCurrentTest.expectedInvokedMethods.length) {
+ ok(false, "Wrong number of invoked methods");
+ gInvokedMethods.forEach(method => info(method + " was invoked"));
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ executeSoon(runNextTest);
+ }
+}
+
+// Wraps around the methods' replacement mock function.
+function wrapperMethod(aInvokedMethods, aMethodName) {
+ return function () {
+ aInvokedMethods.push(aMethodName);
+ // At least getShortcutOrURIAndPostData requires to return url
+ return (aMethodName == "getShortcutOrURIAndPostData") ? arguments.url : arguments[0];
+ }
+}
+
+function setupTestBrowserWindow() {
+ // Steal click events and don't propagate them.
+ gTestWin.addEventListener("click", gClickHandler, true);
+
+ // Replace methods.
+ gReplacedMethods.forEach(function (aMethodName) {
+ gTestWin["old_" + aMethodName] = gTestWin[aMethodName];
+ gTestWin[aMethodName] = wrapperMethod(gInvokedMethods, aMethodName);
+ });
+
+ // Inject links in content.
+ let doc = gTestWin.content.document;
+ let mainDiv = doc.createElement("div");
+ mainDiv.innerHTML =
+ '<p><a id="commonlink" href="http://mochi.test/moz/">Common link</a></p>' +
+ '<p><a id="panellink" href="http://mochi.test/moz/">Panel link</a></p>' +
+ '<p><a id="emptylink">Empty link</a></p>' +
+ '<p><math id="mathxlink" xmlns="http://www.w3.org/1998/Math/MathML" xlink:type="simple" xlink:href="http://mochi.test/moz/"><mtext>MathML XLink</mtext></math></p>' +
+ '<p><svg id="svgxlink" xmlns="http://www.w3.org/2000/svg" width="100px" height="50px" version="1.1"><a xlink:type="simple" xlink:href="http://mochi.test/moz/"><text transform="translate(10, 25)">SVG XLink</text></a></svg></p>' +
+ '<p><map name="map" id="map"><area href="http://mochi.test/moz/" shape="rect" coords="0,0,128,128" /></map><img id="maplink" usemap="#map" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAABGdBTUEAALGPC%2FxhBQAAAOtJREFUeF7t0IEAAAAAgKD9qRcphAoDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGBgwIAAAT0N51AAAAAASUVORK5CYII%3D"/></p>'
+ doc.body.appendChild(mainDiv);
+}
+
+function runNextTest() {
+ if (!gCurrentTest) {
+ gCurrentTest = gTests.shift();
+ gCurrentTest.setup();
+ }
+
+ if (gCurrentTest.targets.length == 0) {
+ info(gCurrentTest.desc + ": cleaning up...")
+ gCurrentTest.clean();
+
+ if (gTests.length > 0) {
+ gCurrentTest = gTests.shift();
+ gCurrentTest.setup();
+ }
+ else {
+ finishTest();
+ return;
+ }
+ }
+
+ // Move to next target.
+ gInvokedMethods.length = 0;
+ let target = gCurrentTest.targets.shift();
+
+ info(gCurrentTest.desc + ": testing " + target);
+
+ // Fire click event.
+ let targetElt = gTestWin.content.document.getElementById(target);
+ ok(targetElt, gCurrentTest.desc + ": target is valid (" + targetElt.id + ")");
+ EventUtils.synthesizeMouseAtCenter(targetElt, gCurrentTest.event, gTestWin.content);
+}
+
+function finishTest() {
+ info("Restoring browser...");
+ gTestWin.removeEventListener("click", gClickHandler, true);
+
+ // Restore original methods.
+ gReplacedMethods.forEach(function (aMethodName) {
+ gTestWin[aMethodName] = gTestWin["old_" + aMethodName];
+ delete gTestWin["old_" + aMethodName];
+ });
+
+ gTestWin.close();
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_contentSearchUI.js b/browser/base/content/test/general/browser_contentSearchUI.js
new file mode 100644
index 000000000..003f80aff
--- /dev/null
+++ b/browser/base/content/test/general/browser_contentSearchUI.js
@@ -0,0 +1,771 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_PAGE_BASENAME = "contentSearchUI.html";
+const TEST_CONTENT_SCRIPT_BASENAME = "contentSearchUI.js";
+const TEST_ENGINE_PREFIX = "browser_searchSuggestionEngine";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml";
+
+const TEST_MSG = "ContentSearchUIControllerTest";
+
+requestLongerTimeout(2);
+
+add_task(function* emptyInput() {
+ yield setUp();
+
+ let state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("key", "VK_BACK_SPACE");
+ checkState(state, "", [], -1);
+
+ yield msg("reset");
+});
+
+add_task(function* blur() {
+ yield setUp();
+
+ let state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("blur");
+ checkState(state, "x", [], -1);
+
+ yield msg("reset");
+});
+
+add_task(function* upDownKeys() {
+ yield setUp();
+
+ let state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ // Cycle down the suggestions starting from no selection.
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "x", ["xfoo", "xbar"], 3);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ // Cycle up starting from no selection.
+ state = yield msg("key", "VK_UP");
+ checkState(state, "x", ["xfoo", "xbar"], 3);
+
+ state = yield msg("key", "VK_UP");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = yield msg("key", "VK_UP");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ state = yield msg("key", "VK_UP");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+ state = yield msg("key", "VK_UP");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ yield msg("reset");
+});
+
+add_task(function* rightLeftKeys() {
+ yield setUp();
+
+ let state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("key", "VK_LEFT");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("key", "VK_LEFT");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("key", "VK_RIGHT");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("key", "VK_RIGHT");
+ checkState(state, "x", [], -1);
+
+ state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+ // This should make the xfoo suggestion sticky. To make sure it sticks,
+ // trigger suggestions again and cycle through them by pressing Down until
+ // nothing is selected again.
+ state = yield msg("key", "VK_RIGHT");
+ checkState(state, "xfoo", [], -1);
+
+ state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+ checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xfoobar", ["xfoofoo", "xfoobar"], 1);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 2);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 3);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
+
+ yield msg("reset");
+});
+
+add_task(function* tabKey() {
+ yield setUp();
+ yield msg("key", { key: "x", waitForSuggestions: true });
+
+ let state = yield msg("key", "VK_TAB");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = yield msg("key", "VK_TAB");
+ checkState(state, "x", ["xfoo", "xbar"], 3);
+
+ state = yield msg("key", { key: "VK_TAB", modifiers: { shiftKey: true }});
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = yield msg("key", { key: "VK_TAB", modifiers: { shiftKey: true }});
+ checkState(state, "x", [], -1);
+
+ yield setUp();
+
+ yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+
+ for (let i = 0; i < 3; ++i) {
+ state = yield msg("key", "VK_TAB");
+ }
+ checkState(state, "x", [], -1);
+
+ yield setUp();
+
+ yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+ state = yield msg("key", "VK_TAB");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, 0);
+
+ state = yield msg("key", "VK_TAB");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, 1);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = yield msg("key", "VK_UP");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ state = yield msg("key", "VK_TAB");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+ state = yield msg("key", "VK_TAB");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = yield msg("key", "VK_TAB");
+ checkState(state, "xbar", [], -1);
+
+ yield msg("reset");
+});
+
+add_task(function* cycleSuggestions() {
+ yield setUp();
+ yield msg("key", { key: "x", waitForSuggestions: true });
+
+ let cycle = Task.async(function* (aSelectedButtonIndex) {
+ let modifiers = {
+ shiftKey: true,
+ accelKey: true,
+ };
+
+ let state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+
+ state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
+
+ state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+ checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+
+ state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+
+ state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+ checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+
+ state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
+
+ state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+
+ state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+ checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+ });
+
+ yield cycle();
+
+ // Repeat with a one-off selected.
+ let state = yield msg("key", "VK_TAB");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+ yield cycle(0);
+
+ // Repeat with the settings button selected.
+ state = yield msg("key", "VK_TAB");
+ checkState(state, "x", ["xfoo", "xbar"], 3);
+ yield cycle(1);
+
+ yield msg("reset");
+});
+
+add_task(function* cycleOneOffs() {
+ yield setUp();
+ yield msg("key", { key: "x", waitForSuggestions: true });
+
+ yield msg("addDuplicateOneOff");
+
+ let state = yield msg("key", "VK_DOWN");
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ let modifiers = {
+ altKey: true,
+ };
+
+ state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+ state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+ state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ // If the settings button is selected, pressing alt+up/down should select the
+ // last/first one-off respectively (and deselect the settings button).
+ yield msg("key", "VK_TAB");
+ yield msg("key", "VK_TAB");
+ state = yield msg("key", "VK_TAB"); // Settings button selected.
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
+
+ state = yield msg("key", { key: "VK_UP", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = yield msg("key", "VK_TAB");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
+
+ state = yield msg("key", { key: "VK_DOWN", modifiers: modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+ yield msg("removeLastOneOff");
+ yield msg("reset");
+});
+
+add_task(function* mouse() {
+ yield setUp();
+
+ let state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("mousemove", 0);
+ checkState(state, "x", ["xfoo", "xbar"], 0);
+
+ state = yield msg("mousemove", 1);
+ checkState(state, "x", ["xfoo", "xbar"], 1);
+
+ state = yield msg("mousemove", 2);
+ checkState(state, "x", ["xfoo", "xbar"], 1, 0);
+
+ state = yield msg("mousemove", 3);
+ checkState(state, "x", ["xfoo", "xbar"], 1, 1);
+
+ state = yield msg("mousemove", -1);
+ checkState(state, "x", ["xfoo", "xbar"], 1);
+
+ yield msg("reset");
+ yield setUp();
+
+ state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = yield msg("mousemove", 0);
+ checkState(state, "x", ["xfoo", "xbar"], 0);
+
+ state = yield msg("mousemove", 2);
+ checkState(state, "x", ["xfoo", "xbar"], 0, 0);
+
+ state = yield msg("mousemove", -1);
+ checkState(state, "x", ["xfoo", "xbar"], 0);
+
+ yield msg("reset");
+});
+
+add_task(function* formHistory() {
+ yield setUp();
+
+ // Type an X and add it to form history.
+ let state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+ // Wait for Satchel to say it's been added to form history.
+ let deferred = Promise.defer();
+ Services.obs.addObserver(function onAdd(subj, topic, data) {
+ if (data == "formhistory-add") {
+ Services.obs.removeObserver(onAdd, "satchel-storage-changed");
+ executeSoon(() => deferred.resolve());
+ }
+ }, "satchel-storage-changed", false);
+ yield Promise.all([msg("addInputValueToFormHistory"), deferred.promise]);
+
+ // Reset the input.
+ state = yield msg("reset");
+ checkState(state, "", [], -1);
+
+ // Type an X again. The form history entry should appear.
+ state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
+ -1);
+
+ // Select the form history entry and delete it.
+ state = yield msg("key", "VK_DOWN");
+ checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
+ 0);
+
+ // Wait for Satchel.
+ deferred = Promise.defer();
+ Services.obs.addObserver(function onRemove(subj, topic, data) {
+ if (data == "formhistory-remove") {
+ Services.obs.removeObserver(onRemove, "satchel-storage-changed");
+ executeSoon(() => deferred.resolve());
+ }
+ }, "satchel-storage-changed", false);
+
+ state = yield msg("key", "VK_DELETE");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ yield deferred.promise;
+
+ // Reset the input.
+ state = yield msg("reset");
+ checkState(state, "", [], -1);
+
+ // Type an X again. The form history entry should still be gone.
+ state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ yield msg("reset");
+});
+
+add_task(function* cycleEngines() {
+ yield setUp();
+ yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+
+ let promiseEngineChange = function(newEngineName) {
+ let deferred = Promise.defer();
+ Services.obs.addObserver(function resolver(subj, topic, data) {
+ if (data != "engine-current") {
+ return;
+ }
+ SimpleTest.is(subj.name, newEngineName, "Engine cycled correctly");
+ Services.obs.removeObserver(resolver, "browser-search-engine-modified");
+ deferred.resolve();
+ }, "browser-search-engine-modified", false);
+ return deferred.promise;
+ }
+
+ let p = promiseEngineChange(TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME);
+ yield msg("key", { key: "VK_DOWN", modifiers: { accelKey: true }});
+ yield p;
+
+ p = promiseEngineChange(TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME);
+ yield msg("key", { key: "VK_UP", modifiers: { accelKey: true }});
+ yield p;
+
+ yield msg("reset");
+});
+
+add_task(function* search() {
+ yield setUp();
+
+ let modifiers = {};
+ ["altKey", "ctrlKey", "metaKey", "shiftKey"].forEach(k => modifiers[k] = true);
+
+ // Test typing a query and pressing enter.
+ let p = msg("waitForSearch");
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+ let mesg = yield p;
+ let eventData = {
+ engineName: TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME,
+ searchString: "x",
+ healthReportKey: "test",
+ searchPurpose: "test",
+ originalEvent: modifiers,
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Test typing a query, then selecting a suggestion and pressing enter.
+ p = msg("waitForSearch");
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ yield msg("key", "VK_DOWN");
+ yield msg("key", "VK_DOWN");
+ yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+ mesg = yield p;
+ eventData.searchString = "xfoo";
+ eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
+ eventData.selection = {
+ index: 1,
+ kind: "key",
+ }
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Test typing a query, then selecting a one-off button and pressing enter.
+ p = msg("waitForSearch");
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ yield msg("key", "VK_UP");
+ yield msg("key", "VK_UP");
+ yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+ mesg = yield p;
+ delete eventData.selection;
+ eventData.searchString = "x";
+ eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME;
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Test typing a query and clicking the search engine header.
+ p = msg("waitForSearch");
+ modifiers.button = 0;
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ yield msg("mousemove", -1);
+ yield msg("click", { eltIdx: -1, modifiers: modifiers });
+ mesg = yield p;
+ eventData.originalEvent = modifiers;
+ eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Test typing a query and then clicking a suggestion.
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ p = msg("waitForSearch");
+ yield msg("mousemove", 1);
+ yield msg("click", { eltIdx: 1, modifiers: modifiers });
+ mesg = yield p;
+ eventData.searchString = "xfoo";
+ eventData.selection = {
+ index: 1,
+ kind: "mouse",
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Test typing a query and then clicking a one-off button.
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ p = msg("waitForSearch");
+ yield msg("mousemove", 3);
+ yield msg("click", { eltIdx: 3, modifiers: modifiers });
+ mesg = yield p;
+ eventData.searchString = "x";
+ eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME;
+ delete eventData.selection;
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Test selecting a suggestion, then clicking a one-off without deselecting the
+ // suggestion.
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ p = msg("waitForSearch");
+ yield msg("mousemove", 1);
+ yield msg("mousemove", 3);
+ yield msg("click", { eltIdx: 3, modifiers: modifiers });
+ mesg = yield p;
+ eventData.searchString = "xfoo"
+ eventData.selection = {
+ index: 1,
+ kind: "mouse",
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Same as above, but with the keyboard.
+ delete modifiers.button;
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ p = msg("waitForSearch");
+ yield msg("key", "VK_DOWN");
+ yield msg("key", "VK_DOWN");
+ yield msg("key", "VK_TAB");
+ yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+ mesg = yield p;
+ eventData.selection = {
+ index: 1,
+ kind: "key",
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Test searching when using IME composition.
+ let state = yield msg("startComposition", { data: "" });
+ checkState(state, "", [], -1);
+ state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
+ checkState(state, "x", [{ str: "x", type: "formHistory" },
+ { str: "xfoo", type: "formHistory" }, "xbar"], -1);
+ yield msg("commitComposition");
+ delete modifiers.button;
+ p = msg("waitForSearch");
+ yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+ mesg = yield p;
+ eventData.searchString = "x"
+ eventData.originalEvent = modifiers;
+ eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
+ delete eventData.selection;
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ state = yield msg("startComposition", { data: "" });
+ checkState(state, "", [], -1);
+ state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
+ checkState(state, "x", [{ str: "x", type: "formHistory" },
+ { str: "xfoo", type: "formHistory" }, "xbar"], -1);
+
+ // Mouse over the first suggestion.
+ state = yield msg("mousemove", 0);
+ checkState(state, "x", [{ str: "x", type: "formHistory" },
+ { str: "xfoo", type: "formHistory" }, "xbar"], 0);
+
+ // Mouse over the second suggestion.
+ state = yield msg("mousemove", 1);
+ checkState(state, "x", [{ str: "x", type: "formHistory" },
+ { str: "xfoo", type: "formHistory" }, "xbar"], 1);
+
+ modifiers.button = 0;
+ p = msg("waitForSearch");
+ yield msg("click", { eltIdx: 1, modifiers: modifiers });
+ mesg = yield p;
+ eventData.searchString = "xfoo";
+ eventData.originalEvent = modifiers;
+ eventData.selection = {
+ index: 1,
+ kind: "mouse",
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ yield promiseTab();
+ yield setUp();
+
+ // Remove form history entries.
+ // Wait for Satchel.
+ let deferred = Promise.defer();
+ let historyCount = 2;
+ Services.obs.addObserver(function onRemove(subj, topic, data) {
+ if (data == "formhistory-remove") {
+ if (--historyCount) {
+ return;
+ }
+ Services.obs.removeObserver(onRemove, "satchel-storage-changed");
+ executeSoon(() => deferred.resolve());
+ }
+ }, "satchel-storage-changed", false);
+
+ yield msg("key", { key: "x", waitForSuggestions: true });
+ yield msg("key", "VK_DOWN");
+ yield msg("key", "VK_DOWN");
+ yield msg("key", "VK_DELETE");
+ yield msg("key", "VK_DOWN");
+ yield msg("key", "VK_DELETE");
+ yield deferred.promise;
+
+ yield msg("reset");
+ state = yield msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ yield promiseTab();
+ yield setUp();
+ yield msg("reset");
+});
+
+add_task(function* settings() {
+ yield setUp();
+ yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+ yield msg("key", "VK_UP");
+ let p = msg("waitForSearchSettings");
+ yield msg("key", "VK_RETURN");
+ yield p;
+
+ yield msg("reset");
+});
+
+var gDidInitialSetUp = false;
+
+function setUp(aNoEngine) {
+ return Task.spawn(function* () {
+ if (!gDidInitialSetUp) {
+ Cu.import("resource:///modules/ContentSearch.jsm");
+ let originalOnMessageSearch = ContentSearch._onMessageSearch;
+ let originalOnMessageManageEngines = ContentSearch._onMessageManageEngines;
+ ContentSearch._onMessageSearch = () => {};
+ ContentSearch._onMessageManageEngines = () => {};
+ registerCleanupFunction(() => {
+ ContentSearch._onMessageSearch = originalOnMessageSearch;
+ ContentSearch._onMessageManageEngines = originalOnMessageManageEngines;
+ });
+ yield setUpEngines();
+ yield promiseTab();
+ gDidInitialSetUp = true;
+ }
+ yield msg("focus");
+ });
+}
+
+function msg(type, data=null) {
+ gMsgMan.sendAsyncMessage(TEST_MSG, {
+ type: type,
+ data: data,
+ });
+ let deferred = Promise.defer();
+ gMsgMan.addMessageListener(TEST_MSG, function onMsg(msgObj) {
+ if (msgObj.data.type != type) {
+ return;
+ }
+ gMsgMan.removeMessageListener(TEST_MSG, onMsg);
+ deferred.resolve(msgObj.data.data);
+ });
+ return deferred.promise;
+}
+
+function checkState(actualState, expectedInputVal, expectedSuggestions,
+ expectedSelectedIdx, expectedSelectedButtonIdx) {
+ expectedSuggestions = expectedSuggestions.map(sugg => {
+ return typeof(sugg) == "object" ? sugg : {
+ str: sugg,
+ type: "remote",
+ };
+ });
+
+ if (expectedSelectedIdx == -1 && expectedSelectedButtonIdx != undefined) {
+ expectedSelectedIdx = expectedSuggestions.length + expectedSelectedButtonIdx;
+ }
+
+ let expectedState = {
+ selectedIndex: expectedSelectedIdx,
+ numSuggestions: expectedSuggestions.length,
+ suggestionAtIndex: expectedSuggestions.map(s => s.str),
+ isFormHistorySuggestionAtIndex:
+ expectedSuggestions.map(s => s.type == "formHistory"),
+
+ tableHidden: expectedSuggestions.length == 0,
+
+ inputValue: expectedInputVal,
+ ariaExpanded: expectedSuggestions.length == 0 ? "false" : "true",
+ };
+ if (expectedSelectedButtonIdx != undefined) {
+ expectedState.selectedButtonIndex = expectedSelectedButtonIdx;
+ }
+ else if (expectedSelectedIdx < expectedSuggestions.length) {
+ expectedState.selectedButtonIndex = -1;
+ }
+ else {
+ expectedState.selectedButtonIndex = expectedSelectedIdx - expectedSuggestions.length;
+ }
+
+ SimpleTest.isDeeply(actualState, expectedState, "State");
+}
+
+var gMsgMan;
+
+function* promiseTab() {
+ let deferred = Promise.defer();
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(() => BrowserTestUtils.removeTab(tab));
+ let pageURL = getRootDirectory(gTestPath) + TEST_PAGE_BASENAME;
+ tab.linkedBrowser.addEventListener("load", function onLoad(event) {
+ tab.linkedBrowser.removeEventListener("load", onLoad, true);
+ gMsgMan = tab.linkedBrowser.messageManager;
+ gMsgMan.sendAsyncMessage("ContentSearch", {
+ type: "AddToWhitelist",
+ data: [pageURL],
+ });
+ promiseMsg("ContentSearch", "AddToWhitelistAck", gMsgMan).then(() => {
+ let jsURL = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME;
+ gMsgMan.loadFrameScript(jsURL, false);
+ deferred.resolve(msg("init"));
+ });
+ }, true, true);
+ openUILinkIn(pageURL, "current");
+ return deferred.promise;
+}
+
+function promiseMsg(name, type, msgMan) {
+ let deferred = Promise.defer();
+ info("Waiting for " + name + " message " + type + "...");
+ msgMan.addMessageListener(name, function onMsg(msgObj) {
+ info("Received " + name + " message " + msgObj.data.type + "\n");
+ if (msgObj.data.type == type) {
+ msgMan.removeMessageListener(name, onMsg);
+ deferred.resolve(msgObj);
+ }
+ });
+ return deferred.promise;
+}
+
+function setUpEngines() {
+ return Task.spawn(function* () {
+ info("Removing default search engines");
+ let currentEngineName = Services.search.currentEngine.name;
+ let currentEngines = Services.search.getVisibleEngines();
+ info("Adding test search engines");
+ let engine1 = yield promiseNewSearchEngine(TEST_ENGINE_BASENAME);
+ yield promiseNewSearchEngine(TEST_ENGINE_2_BASENAME);
+ Services.search.currentEngine = engine1;
+ for (let engine of currentEngines) {
+ Services.search.removeEngine(engine);
+ }
+ registerCleanupFunction(() => {
+ Services.search.restoreDefaultEngines();
+ Services.search.currentEngine = Services.search.getEngineByName(currentEngineName);
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_contextmenu.js b/browser/base/content/test/general/browser_contextmenu.js
new file mode 100644
index 000000000..3e0135848
--- /dev/null
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -0,0 +1,996 @@
+"use strict";
+
+let contextMenu;
+let LOGIN_FILL_ITEMS = [
+ "---", null,
+ "fill-login", null,
+ [
+ "fill-login-no-logins", false,
+ "---", null,
+ "fill-login-saved-passwords", true
+ ], null,
+];
+let hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
+let hasContainers = Services.prefs.getBoolPref("privacy.userContext.enabled");
+
+const example_base = "http://example.com/browser/browser/base/content/test/general/";
+const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/general/";
+
+Services.scriptloader.loadSubScript(chrome_base + "contextmenu_common.js", this);
+
+// Below are test cases for XUL element
+add_task(function* test_xul_text_link_label() {
+ let url = chrome_base + "subtst_contextmenu_xul.xul";
+
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ yield test_contextmenu("#test-xul-text-link-label",
+ ["context-openlinkintab", true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink", true,
+ "context-openlinkprivate", true,
+ "---", null,
+ "context-bookmarklink", true,
+ "context-savelink", true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink", true,
+ "context-searchselect", true
+ ]
+ );
+
+ // Clean up so won't affect HTML element test cases
+ lastElementSelector = null;
+ gBrowser.removeCurrentTab();
+});
+
+// Below are test cases for HTML element
+
+add_task(function* test_setup_html() {
+ let url = example_base + "subtst_contextmenu.html";
+
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let videoIframe = doc.querySelector("#test-video-in-iframe");
+ let video = videoIframe.contentDocument.querySelector("video");
+ let awaitPause = ContentTaskUtils.waitForEvent(video, "pause");
+ video.pause();
+ yield awaitPause;
+
+ let audioIframe = doc.querySelector("#test-audio-in-iframe");
+ // media documents always use a <video> tag.
+ let audio = audioIframe.contentDocument.querySelector("video");
+ awaitPause = ContentTaskUtils.waitForEvent(audio, "pause");
+ audio.pause();
+ yield awaitPause;
+ });
+});
+
+let plainTextItems;
+add_task(function* test_plaintext() {
+ plainTextItems = ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", true,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true
+ ];
+ yield test_contextmenu("#test-text", plainTextItems);
+});
+
+add_task(function* test_link() {
+ yield test_contextmenu("#test-link",
+ ["context-openlinkintab", true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink", true,
+ "context-openlinkprivate", true,
+ "---", null,
+ "context-bookmarklink", true,
+ "context-savelink", true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink", true,
+ "context-searchselect", true
+ ]
+ );
+});
+
+add_task(function* test_mailto() {
+ yield test_contextmenu("#test-mailto",
+ ["context-copyemail", true,
+ "context-searchselect", true
+ ]
+ );
+});
+
+add_task(function* test_image() {
+ yield test_contextmenu("#test-image",
+ ["context-viewimage", true,
+ "context-copyimage-contents", true,
+ "context-copyimage", true,
+ "---", null,
+ "context-saveimage", true,
+ "context-sendimage", true,
+ "context-setDesktopBackground", true,
+ "context-viewimageinfo", true
+ ]
+ );
+});
+
+add_task(function* test_canvas() {
+ yield test_contextmenu("#test-canvas",
+ ["context-viewimage", true,
+ "context-saveimage", true,
+ "context-selectall", true
+ ]
+ );
+});
+
+add_task(function* test_video_ok() {
+ yield test_contextmenu("#test-video-ok",
+ ["context-media-play", true,
+ "context-media-mute", true,
+ "context-media-playbackrate", null,
+ ["context-media-playbackrate-050x", true,
+ "context-media-playbackrate-100x", true,
+ "context-media-playbackrate-125x", true,
+ "context-media-playbackrate-150x", true,
+ "context-media-playbackrate-200x", true], null,
+ "context-media-loop", true,
+ "context-media-hidecontrols", true,
+ "context-video-fullscreen", true,
+ "---", null,
+ "context-viewvideo", true,
+ "context-copyvideourl", true,
+ "---", null,
+ "context-savevideo", true,
+ "context-video-saveimage", true,
+ "context-sendvideo", true,
+ "context-castvideo", null,
+ [], null
+ ]
+ );
+});
+
+add_task(function* test_audio_in_video() {
+ yield test_contextmenu("#test-audio-in-video",
+ ["context-media-play", true,
+ "context-media-mute", true,
+ "context-media-playbackrate", null,
+ ["context-media-playbackrate-050x", true,
+ "context-media-playbackrate-100x", true,
+ "context-media-playbackrate-125x", true,
+ "context-media-playbackrate-150x", true,
+ "context-media-playbackrate-200x", true], null,
+ "context-media-loop", true,
+ "context-media-showcontrols", true,
+ "---", null,
+ "context-copyaudiourl", true,
+ "---", null,
+ "context-saveaudio", true,
+ "context-sendaudio", true
+ ]
+ );
+});
+
+add_task(function* test_video_bad() {
+ yield test_contextmenu("#test-video-bad",
+ ["context-media-play", false,
+ "context-media-mute", false,
+ "context-media-playbackrate", null,
+ ["context-media-playbackrate-050x", false,
+ "context-media-playbackrate-100x", false,
+ "context-media-playbackrate-125x", false,
+ "context-media-playbackrate-150x", false,
+ "context-media-playbackrate-200x", false], null,
+ "context-media-loop", true,
+ "context-media-hidecontrols", false,
+ "context-video-fullscreen", false,
+ "---", null,
+ "context-viewvideo", true,
+ "context-copyvideourl", true,
+ "---", null,
+ "context-savevideo", true,
+ "context-video-saveimage", false,
+ "context-sendvideo", true,
+ "context-castvideo", null,
+ [], null
+ ]
+ );
+});
+
+add_task(function* test_video_bad2() {
+ yield test_contextmenu("#test-video-bad2",
+ ["context-media-play", false,
+ "context-media-mute", false,
+ "context-media-playbackrate", null,
+ ["context-media-playbackrate-050x", false,
+ "context-media-playbackrate-100x", false,
+ "context-media-playbackrate-125x", false,
+ "context-media-playbackrate-150x", false,
+ "context-media-playbackrate-200x", false], null,
+ "context-media-loop", true,
+ "context-media-hidecontrols", false,
+ "context-video-fullscreen", false,
+ "---", null,
+ "context-viewvideo", false,
+ "context-copyvideourl", false,
+ "---", null,
+ "context-savevideo", false,
+ "context-video-saveimage", false,
+ "context-sendvideo", false,
+ "context-castvideo", null,
+ [], null
+ ]
+ );
+});
+
+add_task(function* test_iframe() {
+ yield test_contextmenu("#test-iframe",
+ ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", true,
+ "frame", null,
+ ["context-showonlythisframe", true,
+ "context-openframeintab", true,
+ "context-openframe", true,
+ "---", null,
+ "context-reloadframe", true,
+ "---", null,
+ "context-bookmarkframe", true,
+ "context-saveframe", true,
+ "---", null,
+ "context-printframe", true,
+ "---", null,
+ "context-viewframesource", true,
+ "context-viewframeinfo", true], null,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true
+ ]
+ );
+});
+
+add_task(function* test_video_in_iframe() {
+ yield test_contextmenu("#test-video-in-iframe",
+ ["context-media-play", true,
+ "context-media-mute", true,
+ "context-media-playbackrate", null,
+ ["context-media-playbackrate-050x", true,
+ "context-media-playbackrate-100x", true,
+ "context-media-playbackrate-125x", true,
+ "context-media-playbackrate-150x", true,
+ "context-media-playbackrate-200x", true], null,
+ "context-media-loop", true,
+ "context-media-hidecontrols", true,
+ "context-video-fullscreen", true,
+ "---", null,
+ "context-viewvideo", true,
+ "context-copyvideourl", true,
+ "---", null,
+ "context-savevideo", true,
+ "context-video-saveimage", true,
+ "context-sendvideo", true,
+ "context-castvideo", null,
+ [], null,
+ "frame", null,
+ ["context-showonlythisframe", true,
+ "context-openframeintab", true,
+ "context-openframe", true,
+ "---", null,
+ "context-reloadframe", true,
+ "---", null,
+ "context-bookmarkframe", true,
+ "context-saveframe", true,
+ "---", null,
+ "context-printframe", true,
+ "---", null,
+ "context-viewframeinfo", true], null]
+ );
+});
+
+add_task(function* test_audio_in_iframe() {
+ yield test_contextmenu("#test-audio-in-iframe",
+ ["context-media-play", true,
+ "context-media-mute", true,
+ "context-media-playbackrate", null,
+ ["context-media-playbackrate-050x", true,
+ "context-media-playbackrate-100x", true,
+ "context-media-playbackrate-125x", true,
+ "context-media-playbackrate-150x", true,
+ "context-media-playbackrate-200x", true], null,
+ "context-media-loop", true,
+ "---", null,
+ "context-copyaudiourl", true,
+ "---", null,
+ "context-saveaudio", true,
+ "context-sendaudio", true,
+ "frame", null,
+ ["context-showonlythisframe", true,
+ "context-openframeintab", true,
+ "context-openframe", true,
+ "---", null,
+ "context-reloadframe", true,
+ "---", null,
+ "context-bookmarkframe", true,
+ "context-saveframe", true,
+ "---", null,
+ "context-printframe", true,
+ "---", null,
+ "context-viewframeinfo", true], null]
+ );
+});
+
+add_task(function* test_image_in_iframe() {
+ yield test_contextmenu("#test-image-in-iframe",
+ ["context-viewimage", true,
+ "context-copyimage-contents", true,
+ "context-copyimage", true,
+ "---", null,
+ "context-saveimage", true,
+ "context-sendimage", true,
+ "context-setDesktopBackground", true,
+ "context-viewimageinfo", true,
+ "frame", null,
+ ["context-showonlythisframe", true,
+ "context-openframeintab", true,
+ "context-openframe", true,
+ "---", null,
+ "context-reloadframe", true,
+ "---", null,
+ "context-bookmarkframe", true,
+ "context-saveframe", true,
+ "---", null,
+ "context-printframe", true,
+ "---", null,
+ "context-viewframeinfo", true], null]
+ );
+});
+
+add_task(function* test_textarea() {
+ // Disabled since this is seeing spell-check-enabled
+ // instead of spell-add-dictionaries-main
+ todo(false, "spell checker tests are failing, bug 1246296");
+ return;
+
+ /*
+ yield test_contextmenu("#test-textarea",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null,
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-add-dictionaries-main", true,
+ ],
+ {
+ skipFocusChange: true,
+ }
+ );
+ */
+});
+
+add_task(function* test_textarea_spellcheck() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+ return;
+
+ /*
+ yield test_contextmenu("#test-textarea",
+ ["*chubbiness", true, // spelling suggestion
+ "spell-add-to-dictionary", true,
+ "---", null,
+ "context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null
+ ],
+ {
+ waitForSpellCheck: true,
+ offsetX: 6,
+ offsetY: 6,
+ postCheckContextMenuFn() {
+ document.getElementById("spell-add-to-dictionary").doCommand();
+ }
+ }
+ );
+ */
+});
+
+add_task(function* test_plaintext2() {
+ yield test_contextmenu("#test-text", plainTextItems);
+});
+
+add_task(function* test_undo_add_to_dictionary() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+ return;
+
+ /*
+ yield test_contextmenu("#test-textarea",
+ ["spell-undo-add-to-dictionary", true,
+ "---", null,
+ "context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null
+ ],
+ {
+ waitForSpellCheck: true,
+ postCheckContextMenuFn() {
+ document.getElementById("spell-undo-add-to-dictionary")
+ .doCommand();
+ }
+ }
+ );
+ */
+});
+
+add_task(function* test_contenteditable() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+ return;
+
+ /*
+ yield test_contextmenu("#test-contenteditable",
+ ["spell-no-suggestions", false,
+ "spell-add-to-dictionary", true,
+ "---", null,
+ "context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null
+ ],
+ {waitForSpellCheck: true}
+ );
+ */
+});
+
+add_task(function* test_copylinkcommand() {
+ yield test_contextmenu("#test-link", null, {
+ postCheckContextMenuFn: function*() {
+ document.commandDispatcher
+ .getControllerForCommand("cmd_copyLink")
+ .doCommand("cmd_copyLink");
+
+ // The easiest way to check the clipboard is to paste the contents
+ // into a textbox.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let input = doc.getElementById("test-input");
+ input.focus();
+ input.value = "";
+ });
+ document.commandDispatcher
+ .getControllerForCommand("cmd_paste")
+ .doCommand("cmd_paste");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let input = doc.getElementById("test-input");
+ Assert.equal(input.value, "http://mozilla.com/", "paste for command cmd_paste");
+ });
+ }
+ });
+});
+
+add_task(function* test_pagemenu() {
+ yield test_contextmenu("#test-pagemenu",
+ ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "+Plain item", {type: "", icon: "", checked: false, disabled: false},
+ "+Disabled item", {type: "", icon: "", checked: false, disabled: true},
+ "+Item w/ textContent", {type: "", icon: "", checked: false, disabled: false},
+ "---", null,
+ "+Checkbox", {type: "checkbox", icon: "", checked: true, disabled: false},
+ "---", null,
+ "+Radio1", {type: "checkbox", icon: "", checked: true, disabled: false},
+ "+Radio2", {type: "checkbox", icon: "", checked: false, disabled: false},
+ "+Radio3", {type: "checkbox", icon: "", checked: false, disabled: false},
+ "---", null,
+ "+Item w/ icon", {type: "", icon: "favicon.ico", checked: false, disabled: false},
+ "+Item w/ bad icon", {type: "", icon: "", checked: false, disabled: false},
+ "---", null,
+ "generated-submenu-1", true,
+ ["+Radio1", {type: "checkbox", icon: "", checked: false, disabled: false},
+ "+Radio2", {type: "checkbox", icon: "", checked: true, disabled: false},
+ "+Radio3", {type: "checkbox", icon: "", checked: false, disabled: false},
+ "---", null,
+ "+Checkbox", {type: "checkbox", icon: "", checked: false, disabled: false}], null,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", true,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true
+ ],
+ {postCheckContextMenuFn: function*() {
+ let item = contextMenu.getElementsByAttribute("generateditemid", "1")[0];
+ ok(item, "Got generated XUL menu item");
+ item.doCommand();
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let pagemenu = content.document.getElementById("test-pagemenu");
+ Assert.ok(!pagemenu.hasAttribute("hopeless"), "attribute got removed");
+ });
+ }
+ });
+});
+
+add_task(function* test_dom_full_screen() {
+ yield test_contextmenu("#test-dom-full-screen",
+ ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "context-leave-dom-fullscreen", true,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", true,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true
+ ],
+ {
+ shiftkey: true,
+ *preCheckContextMenuFn() {
+ yield pushPrefs(["full-screen-api.allow-trusted-requests-only", false],
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"])
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let win = doc.defaultView;
+ let full_screen_element = doc.getElementById("test-dom-full-screen");
+ let awaitFullScreenChange =
+ ContentTaskUtils.waitForEvent(win, "fullscreenchange");
+ full_screen_element.requestFullscreen();
+ yield awaitFullScreenChange;
+ });
+ },
+ *postCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let win = content.document.defaultView;
+ let awaitFullScreenChange =
+ ContentTaskUtils.waitForEvent(win, "fullscreenchange");
+ content.document.exitFullscreen();
+ yield awaitFullScreenChange;
+ });
+ }
+ }
+ );
+});
+
+add_task(function* test_pagemenu2() {
+ yield test_contextmenu("#test-text",
+ ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", true,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true
+ ],
+ {shiftkey: true}
+ );
+});
+
+add_task(function* test_select_text() {
+ yield test_contextmenu("#test-select-text",
+ ["context-copy", true,
+ "context-selectall", true,
+ "---", null,
+ "context-searchselect", true,
+ "context-viewpartialsource-selection", true
+ ],
+ {
+ offsetX: 6,
+ offsetY: 6,
+ *preCheckContextMenuFn() {
+ yield selectText("#test-select-text");
+ }
+ }
+ );
+});
+
+add_task(function* test_select_text_link() {
+ yield test_contextmenu("#test-select-text-link",
+ ["context-openlinkincurrent", true,
+ "context-openlinkintab", true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink", true,
+ "context-openlinkprivate", true,
+ "---", null,
+ "context-bookmarklink", true,
+ "context-savelink", true,
+ "context-copy", true,
+ "context-selectall", true,
+ "---", null,
+ "context-searchselect", true,
+ "context-viewpartialsource-selection", true
+ ],
+ {
+ offsetX: 6,
+ offsetY: 6,
+ *preCheckContextMenuFn() {
+ yield selectText("#test-select-text-link");
+ },
+ *postCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let win = content.document.defaultView;
+ win.getSelection().removeAllRanges();
+ });
+ }
+ }
+ );
+});
+
+add_task(function* test_imagelink() {
+ yield test_contextmenu("#test-image-link",
+ ["context-openlinkintab", true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink", true,
+ "context-openlinkprivate", true,
+ "---", null,
+ "context-bookmarklink", true,
+ "context-savelink", true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink", true,
+ "---", null,
+ "context-viewimage", true,
+ "context-copyimage-contents", true,
+ "context-copyimage", true,
+ "---", null,
+ "context-saveimage", true,
+ "context-sendimage", true,
+ "context-setDesktopBackground", true,
+ "context-viewimageinfo", true
+ ]
+ );
+});
+
+add_task(function* test_select_input_text() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+ return;
+
+ /*
+ yield test_contextmenu("#test-select-input-text",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", true,
+ "---", null,
+ "context-selectall", true,
+ "context-searchselect", true,
+ "---", null,
+ "spell-check-enabled", true
+ ].concat(LOGIN_FILL_ITEMS),
+ {
+ *preCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let win = doc.defaultView;
+ win.getSelection().removeAllRanges();
+ let element = doc.querySelector("#test-select-input-text");
+ element.select();
+ });
+ }
+ }
+ );
+ */
+});
+
+add_task(function* test_select_input_text_password() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+ return;
+
+ /*
+ yield test_contextmenu("#test-select-input-text-type-password",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", true,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ // spell checker is shown on input[type="password"] on this testcase
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null
+ ].concat(LOGIN_FILL_ITEMS),
+ {
+ *preCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let win = doc.defaultView;
+ win.getSelection().removeAllRanges();
+ let element = doc.querySelector("#test-select-input-text-type-password");
+ element.select();
+ });
+ },
+ *postCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let win = content.document.defaultView;
+ win.getSelection().removeAllRanges();
+ });
+ }
+ }
+ );
+ */
+});
+
+add_task(function* test_click_to_play_blocked_plugin() {
+ yield test_contextmenu("#test-plugin",
+ ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "context-ctp-play", true,
+ "context-ctp-hide", true,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", true,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true
+ ],
+ {
+ preCheckContextMenuFn: function*() {
+ pushPrefs(["plugins.click_to_play", true]);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+ },
+ postCheckContextMenuFn: function*() {
+ getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ }
+ }
+ );
+});
+
+add_task(function* test_longdesc() {
+ yield test_contextmenu("#test-longdesc",
+ ["context-viewimage", true,
+ "context-copyimage-contents", true,
+ "context-copyimage", true,
+ "---", null,
+ "context-saveimage", true,
+ "context-sendimage", true,
+ "context-setDesktopBackground", true,
+ "context-viewimageinfo", true,
+ "context-viewimagedesc", true
+ ]
+ );
+});
+
+add_task(function* test_srcdoc() {
+ yield test_contextmenu("#test-srcdoc",
+ ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", true,
+ "frame", null,
+ ["context-reloadframe", true,
+ "---", null,
+ "context-saveframe", true,
+ "---", null,
+ "context-printframe", true,
+ "---", null,
+ "context-viewframesource", true,
+ "context-viewframeinfo", true], null,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true
+ ]
+ );
+});
+
+add_task(function* test_input_spell_false() {
+ todo(false, "spell checker tests are failing, bug 1246296");
+ return;
+
+ /*
+ yield test_contextmenu("#test-contenteditable-spellcheck-false",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ ]
+ );
+ */
+});
+
+const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
+add_task(function* test_plaintext_sendpagetodevice() {
+ if (!gFxAccounts.sendTabToDeviceEnabled) {
+ return;
+ }
+ const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+
+ let plainTextItemsWithSendPage =
+ ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-sendpagetodevice", true,
+ ["*Foo", true,
+ "*Bar", true,
+ "---", null,
+ "*All Devices", true], null,
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", true,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true
+ ];
+ yield test_contextmenu("#test-text", plainTextItemsWithSendPage, {
+ *onContextMenuShown() {
+ yield openMenuItemSubmenu("context-sendpagetodevice");
+ }
+ });
+
+ restoreRemoteClients(oldGetter);
+});
+
+add_task(function* test_link_sendlinktodevice() {
+ if (!gFxAccounts.sendTabToDeviceEnabled) {
+ return;
+ }
+ const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+
+ yield test_contextmenu("#test-link",
+ ["context-openlinkintab", true,
+ ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
+ // We need a blank entry here because the containers submenu is
+ // dynamically generated with no ids.
+ ...(hasContainers ? ["", null] : []),
+ "context-openlink", true,
+ "context-openlinkprivate", true,
+ "---", null,
+ "context-bookmarklink", true,
+ "context-savelink", true,
+ ...(hasPocket ? ["context-savelinktopocket", true] : []),
+ "context-copylink", true,
+ "context-searchselect", true,
+ "---", null,
+ "context-sendlinktodevice", true,
+ ["*Foo", true,
+ "*Bar", true,
+ "---", null,
+ "*All Devices", true], null,
+ ],
+ {
+ *onContextMenuShown() {
+ yield openMenuItemSubmenu("context-sendlinktodevice");
+ }
+ });
+
+ restoreRemoteClients(oldGetter);
+});
+
+add_task(function* test_cleanup_html() {
+ gBrowser.removeCurrentTab();
+});
+
+/**
+ * Selects the text of the element that matches the provided `selector`
+ *
+ * @param {String} selector
+ * A selector passed to querySelector to find
+ * the element that will be referenced.
+ */
+function* selectText(selector) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, selector, function*(contentSelector) {
+ info(`Selecting text of ${contentSelector}`);
+ let doc = content.document;
+ let win = doc.defaultView;
+ win.getSelection().removeAllRanges();
+ let div = doc.createRange();
+ let element = doc.querySelector(contentSelector);
+ Assert.ok(element, "Found element to select text from");
+ div.setStartBefore(element);
+ div.setEndAfter(element);
+ win.getSelection().addRange(div);
+ });
+}
diff --git a/browser/base/content/test/general/browser_contextmenu_childprocess.js b/browser/base/content/test/general/browser_contextmenu_childprocess.js
new file mode 100644
index 000000000..3d52be9ab
--- /dev/null
+++ b/browser/base/content/test/general/browser_contextmenu_childprocess.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const gBaseURL = "https://example.com/browser/browser/base/content/test/general/";
+
+add_task(function *() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, gBaseURL + "subtst_contextmenu.html");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+
+ // Get the point of the element with the page menu (test-pagemenu) and
+ // synthesize a right mouse click there.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouse("#test-pagemenu", 5, 5, { type : "contextmenu", button : 2 }, tab.linkedBrowser);
+ yield popupShownPromise;
+
+ checkMenu(contextMenu);
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.hidePopup();
+ yield popupHiddenPromise;
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+function checkItems(menuitem, arr)
+{
+ for (let i = 0; i < arr.length; i += 2) {
+ let str = arr[i];
+ let details = arr[i + 1];
+ if (str == "---") {
+ is(menuitem.localName, "menuseparator", "menuseparator");
+ }
+ else if ("children" in details) {
+ is(menuitem.localName, "menu", "submenu");
+ is(menuitem.getAttribute("label"), str, str + " label");
+ checkItems(menuitem.firstChild.firstChild, details.children);
+ }
+ else {
+ is(menuitem.localName, "menuitem", str + " menuitem");
+
+ is(menuitem.getAttribute("label"), str, str + " label");
+ is(menuitem.getAttribute("type"), details.type, str + " type");
+ is(menuitem.getAttribute("image"), details.icon ? gBaseURL + details.icon : "", str + " icon");
+
+ if (details.checked)
+ is(menuitem.getAttribute("checked"), "true", str + " checked");
+ else
+ ok(!menuitem.hasAttribute("checked"), str + " checked");
+
+ if (details.disabled)
+ is(menuitem.getAttribute("disabled"), "true", str + " disabled");
+ else
+ ok(!menuitem.hasAttribute("disabled"), str + " disabled");
+ }
+
+ menuitem = menuitem.nextSibling;
+ }
+}
+
+function checkMenu(contextMenu)
+{
+ let items = [ "Plain item", {type: "", icon: "", checked: false, disabled: false},
+ "Disabled item", {type: "", icon: "", checked: false, disabled: true},
+ "Item w/ textContent", {type: "", icon: "", checked: false, disabled: false},
+ "---", null,
+ "Checkbox", {type: "checkbox", icon: "", checked: true, disabled: false},
+ "---", null,
+ "Radio1", {type: "checkbox", icon: "", checked: true, disabled: false},
+ "Radio2", {type: "checkbox", icon: "", checked: false, disabled: false},
+ "Radio3", {type: "checkbox", icon: "", checked: false, disabled: false},
+ "---", null,
+ "Item w/ icon", {type: "", icon: "favicon.ico", checked: false, disabled: false},
+ "Item w/ bad icon", {type: "", icon: "", checked: false, disabled: false},
+ "---", null,
+ "Submenu", { children:
+ ["Radio1", {type: "checkbox", icon: "", checked: false, disabled: false},
+ "Radio2", {type: "checkbox", icon: "", checked: true, disabled: false},
+ "Radio3", {type: "checkbox", icon: "", checked: false, disabled: false},
+ "---", null,
+ "Checkbox", {type: "checkbox", icon: "", checked: false, disabled: false}] }
+ ];
+ checkItems(contextMenu.childNodes[2], items);
+}
diff --git a/browser/base/content/test/general/browser_contextmenu_input.js b/browser/base/content/test/general/browser_contextmenu_input.js
new file mode 100644
index 000000000..cfc7b7529
--- /dev/null
+++ b/browser/base/content/test/general/browser_contextmenu_input.js
@@ -0,0 +1,243 @@
+"use strict";
+
+let contextMenu;
+let hasPocket = Services.prefs.getBoolPref("extensions.pocket.enabled");
+
+add_task(function* test_setup() {
+ const example_base = "http://example.com/browser/browser/base/content/test/general/";
+ const url = example_base + "subtst_contextmenu_input.html";
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/general/";
+ const contextmenu_common = chrome_base + "contextmenu_common.js";
+ Services.scriptloader.loadSubScript(contextmenu_common, this);
+});
+
+add_task(function* test_text_input() {
+ yield test_contextmenu("#input_text",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", false,
+ "---", null,
+ "spell-check-enabled", true]);
+});
+
+add_task(function* test_text_input_spellcheck() {
+ yield test_contextmenu("#input_spellcheck_no_value",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", false,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null],
+ {
+ waitForSpellCheck: true,
+ // Need to dynamically add/remove the "password" type or LoginManager
+ // will think that the form inputs on the page are part of a login
+ // and will add fill-login context menu items.
+ *preCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let input = doc.getElementById("input_spellcheck_no_value");
+ input.setAttribute("spellcheck", "true");
+ input.clientTop; // force layout flush
+ });
+ },
+ }
+ );
+});
+
+add_task(function* test_text_input_spellcheckwrong() {
+ yield test_contextmenu("#input_spellcheck_incorrect",
+ ["*prodigality", true, // spelling suggestion
+ "spell-add-to-dictionary", true,
+ "---", null,
+ "context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null],
+ {waitForSpellCheck: true}
+ );
+});
+
+add_task(function* test_text_input_spellcheckcorrect() {
+ yield test_contextmenu("#input_spellcheck_correct",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true,
+ "spell-dictionaries", true,
+ ["spell-check-dictionary-en-US", true,
+ "---", null,
+ "spell-add-dictionaries", true], null],
+ {waitForSpellCheck: true}
+ );
+});
+
+add_task(function* test_text_input_disabled() {
+ yield test_contextmenu("#input_disabled",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", true,
+ "---", null,
+ "spell-check-enabled", true],
+ {skipFocusChange: true}
+ );
+});
+
+add_task(function* test_password_input() {
+ todo(false, "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled");
+ yield test_contextmenu("#input_password",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", null,
+ "---", null,
+ "fill-login", null,
+ ["fill-login-no-logins", false,
+ "---", null,
+ "fill-login-saved-passwords", true], null],
+ {
+ skipFocusChange: true,
+ // Need to dynamically add/remove the "password" type or LoginManager
+ // will think that the form inputs on the page are part of a login
+ // and will add fill-login context menu items.
+ *preCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let input = doc.getElementById("input_password");
+ input.type = "password";
+ input.clientTop; // force layout flush
+ });
+ },
+ *postCheckContextMenuFn() {
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ let doc = content.document;
+ let input = doc.getElementById("input_password");
+ input.type = "text";
+ input.clientTop; // force layout flush
+ });
+ },
+ }
+ );
+});
+
+add_task(function* test_tel_email_url_number_input() {
+ todo(false, "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled");
+ for (let selector of ["#input_email", "#input_url", "#input_tel", "#input_number"]) {
+ yield test_contextmenu(selector,
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", null],
+ {skipFocusChange: true}
+ );
+ }
+});
+
+add_task(function* test_date_time_color_range_month_week_datetimelocal_input() {
+ for (let selector of ["#input_date", "#input_time", "#input_color",
+ "#input_range", "#input_month", "#input_week",
+ "#input_datetime-local"]) {
+ yield test_contextmenu(selector,
+ ["context-navigation", null,
+ ["context-back", false,
+ "context-forward", false,
+ "context-reload", true,
+ "context-bookmarkpage", true], null,
+ "---", null,
+ "context-savepage", true,
+ ...(hasPocket ? ["context-pocket", true] : []),
+ "---", null,
+ "context-viewbgimage", false,
+ "context-selectall", null,
+ "---", null,
+ "context-viewsource", true,
+ "context-viewinfo", true],
+ {skipFocusChange: true}
+ );
+ }
+});
+
+add_task(function* test_search_input() {
+ todo(false, "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled");
+ yield test_contextmenu("#input_search",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", null,
+ "---", null,
+ "spell-check-enabled", true],
+ {skipFocusChange: true}
+ );
+});
+
+add_task(function* test_text_input_readonly() {
+ todo(false, "context-selectall is enabled on osx-e10s, and windows when" +
+ " it should be disabled");
+ todo(false, "spell-check should not be enabled for input[readonly]. see bug 1246296");
+ yield test_contextmenu("#input_readonly",
+ ["context-undo", false,
+ "---", null,
+ "context-cut", true,
+ "context-copy", true,
+ "context-paste", null, // ignore clipboard state
+ "context-delete", false,
+ "---", null,
+ "context-selectall", null],
+ {skipFocusChange: true}
+ );
+});
+
+add_task(function* test_cleanup() {
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/general/browser_csp_block_all_mixedcontent.js b/browser/base/content/test/general/browser_csp_block_all_mixedcontent.js
new file mode 100644
index 000000000..00a06f53e
--- /dev/null
+++ b/browser/base/content/test/general/browser_csp_block_all_mixedcontent.js
@@ -0,0 +1,55 @@
+/*
+ * Description of the Test:
+ * We load an https page which uses a CSP including block-all-mixed-content.
+ * The page tries to load a script over http. We make sure the UI is not
+ * influenced when blocking the mixed content. In particular the page
+ * should still appear fully encrypted with a green lock.
+ */
+
+const PRE_PATH = "https://example.com/browser/browser/base/content/test/general/";
+var gTestBrowser = null;
+
+// ------------------------------------------------------
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+ finish();
+}
+
+// ------------------------------------------------------
+function verifyUInotDegraded() {
+ // make sure that not mixed content is loaded and also not blocked
+ assertMixedContentBlockingState(
+ gTestBrowser,
+ { activeLoaded: false,
+ activeBlocked: false,
+ passiveLoaded: false
+ }
+ );
+ // clean up and finish test
+ cleanUpAfterTests();
+}
+
+// ------------------------------------------------------
+function runTests() {
+ var newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ newTab.linkedBrowser.stop();
+
+ // Starting the test
+ BrowserTestUtils.browserLoaded(gTestBrowser).then(verifyUInotDegraded);
+ var url = PRE_PATH + "file_csp_block_all_mixedcontent.html";
+ gTestBrowser.loadURI(url);
+}
+
+// ------------------------------------------------------
+function test() {
+ // Performing async calls, e.g. 'onload', we have to wait till all of them finished
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ { 'set': [["security.mixed_content.block_active_content", true]] },
+ function() { runTests(); }
+ );
+}
diff --git a/browser/base/content/test/general/browser_ctrlTab.js b/browser/base/content/test/general/browser_ctrlTab.js
new file mode 100644
index 000000000..d16aaeca4
--- /dev/null
+++ b/browser/base/content/test/general/browser_ctrlTab.js
@@ -0,0 +1,185 @@
+add_task(function* () {
+ gPrefService.setBoolPref("browser.ctrlTab.previews", true);
+
+ gBrowser.addTab();
+ gBrowser.addTab();
+ gBrowser.addTab();
+
+ checkTabs(4);
+
+ yield ctrlTabTest([2], 1, 0);
+ yield ctrlTabTest([2, 3, 1], 2, 2);
+ yield ctrlTabTest([], 4, 2);
+
+ {
+ let selectedIndex = gBrowser.tabContainer.selectedIndex;
+ yield pressCtrlTab();
+ yield pressCtrlTab(true);
+ yield releaseCtrl();
+ is(gBrowser.tabContainer.selectedIndex, selectedIndex,
+ "Ctrl+Tab -> Ctrl+Shift+Tab keeps the selected tab");
+ }
+
+ { // test for bug 445369
+ let tabs = gBrowser.tabs.length;
+ yield pressCtrlTab();
+ yield synthesizeCtrlW();
+ is(gBrowser.tabs.length, tabs - 1, "Ctrl+Tab -> Ctrl+W removes one tab");
+ yield releaseCtrl();
+ }
+
+ { // test for bug 667314
+ let tabs = gBrowser.tabs.length;
+ yield pressCtrlTab();
+ yield pressCtrlTab(true);
+ yield synthesizeCtrlW();
+ is(gBrowser.tabs.length, tabs - 1, "Ctrl+Tab -> Ctrl+W removes the selected tab");
+ yield releaseCtrl();
+ }
+
+ gBrowser.addTab();
+ checkTabs(3);
+ yield ctrlTabTest([2, 1, 0], 7, 1);
+
+ { // test for bug 1292049
+ let tabToClose = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:buildconfig");
+ checkTabs(4);
+ selectTabs([0, 1, 2, 3]);
+
+ yield BrowserTestUtils.removeTab(tabToClose);
+ checkTabs(3);
+ undoCloseTab();
+ checkTabs(4);
+ is(gBrowser.tabContainer.selectedIndex, 3, "tab is selected after closing and restoring it");
+
+ yield ctrlTabTest([], 1, 2);
+ }
+
+ { // test for bug 445369
+ checkTabs(4);
+ selectTabs([1, 2, 0]);
+
+ let selectedTab = gBrowser.selectedTab;
+ let tabToRemove = gBrowser.tabs[1];
+
+ yield pressCtrlTab();
+ yield pressCtrlTab();
+ yield synthesizeCtrlW();
+ ok(!tabToRemove.parentNode,
+ "Ctrl+Tab*2 -> Ctrl+W removes the second most recently selected tab");
+
+ yield pressCtrlTab(true);
+ yield pressCtrlTab(true);
+ yield releaseCtrl();
+ ok(selectedTab.selected,
+ "Ctrl+Tab*2 -> Ctrl+W -> Ctrl+Shift+Tab*2 keeps the selected tab");
+ }
+ gBrowser.removeTab(gBrowser.tabContainer.lastChild);
+ checkTabs(2);
+
+ yield ctrlTabTest([1], 1, 0);
+
+ gBrowser.removeTab(gBrowser.tabContainer.lastChild);
+ checkTabs(1);
+
+ { // test for bug 445768
+ let focusedWindow = document.commandDispatcher.focusedWindow;
+ let eventConsumed = true;
+ let detectKeyEvent = function (event) {
+ eventConsumed = event.defaultPrevented;
+ };
+ document.addEventListener("keypress", detectKeyEvent, false);
+ yield pressCtrlTab();
+ document.removeEventListener("keypress", detectKeyEvent, false);
+ ok(eventConsumed, "Ctrl+Tab consumed by the tabbed browser if one tab is open");
+ is(focusedWindow, document.commandDispatcher.focusedWindow,
+ "Ctrl+Tab doesn't change focus if one tab is open");
+ }
+
+ // cleanup
+ if (gPrefService.prefHasUserValue("browser.ctrlTab.previews"))
+ gPrefService.clearUserPref("browser.ctrlTab.previews");
+
+ /* private utility functions */
+
+ function* pressCtrlTab(aShiftKey) {
+ let promise;
+ if (!isOpen() && canOpen()) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popupshown");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: !!aShiftKey });
+ return promise;
+ }
+
+ function* releaseCtrl() {
+ let promise;
+ if (isOpen()) {
+ promise = BrowserTestUtils.waitForEvent(ctrlTab.panel, "popuphidden");
+ } else {
+ promise = BrowserTestUtils.waitForEvent(document, "keyup");
+ }
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+ return promise;
+ }
+
+ function* synthesizeCtrlW() {
+ let promise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabClose");
+ EventUtils.synthesizeKey("w", { ctrlKey: true });
+ return promise;
+ }
+
+ function isOpen() {
+ return ctrlTab.isOpen;
+ }
+
+ function canOpen() {
+ return gPrefService.getBoolPref("browser.ctrlTab.previews") && gBrowser.tabs.length > 2;
+ }
+
+ function checkTabs(aTabs) {
+ is(gBrowser.tabs.length, aTabs, "number of open tabs should be " + aTabs);
+ }
+
+ function selectTabs(tabs) {
+ tabs.forEach(function (index) {
+ gBrowser.selectedTab = gBrowser.tabs[index];
+ });
+ }
+
+ function* ctrlTabTest(tabsToSelect, tabTimes, expectedIndex) {
+ selectTabs(tabsToSelect);
+
+ var indexStart = gBrowser.tabContainer.selectedIndex;
+ var tabCount = gBrowser.tabs.length;
+ var normalized = tabTimes % tabCount;
+ var where = normalized == 1 ? "back to the previously selected tab" :
+ normalized + " tabs back in most-recently-selected order";
+
+ for (let i = 0; i < tabTimes; i++) {
+ yield pressCtrlTab();
+
+ if (tabCount > 2)
+ is(gBrowser.tabContainer.selectedIndex, indexStart,
+ "Selected tab doesn't change while tabbing");
+ }
+
+ if (tabCount > 2) {
+ ok(isOpen(),
+ "With " + tabCount + " tabs open, Ctrl+Tab opens the preview panel");
+
+ yield releaseCtrl();
+
+ ok(!isOpen(),
+ "Releasing Ctrl closes the preview panel");
+ } else {
+ ok(!isOpen(),
+ "With " + tabCount + " tabs open, Ctrl+Tab doesn't open the preview panel");
+ }
+
+ is(gBrowser.tabContainer.selectedIndex, expectedIndex,
+ "With "+ tabCount +" tabs open and tab " + indexStart
+ + " selected, Ctrl+Tab*" + tabTimes + " goes " + where);
+ }
+});
diff --git a/browser/base/content/test/general/browser_datachoices_notification.js b/browser/base/content/test/general/browser_datachoices_notification.js
new file mode 100644
index 000000000..360728b4c
--- /dev/null
+++ b/browser/base/content/test/general/browser_datachoices_notification.js
@@ -0,0 +1,221 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+// Pass an empty scope object to the import to prevent "leaked window property"
+// errors in tests.
+var Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+var TelemetryReportingPolicy =
+ Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", {}).TelemetryReportingPolicy;
+
+const PREF_BRANCH = "datareporting.policy.";
+const PREF_BYPASS_NOTIFICATION = PREF_BRANCH + "dataSubmissionPolicyBypassNotification";
+const PREF_CURRENT_POLICY_VERSION = PREF_BRANCH + "currentPolicyVersion";
+const PREF_ACCEPTED_POLICY_VERSION = PREF_BRANCH + "dataSubmissionPolicyAcceptedVersion";
+const PREF_ACCEPTED_POLICY_DATE = PREF_BRANCH + "dataSubmissionPolicyNotifiedTime";
+
+const TEST_POLICY_VERSION = 37;
+
+function fakeShowPolicyTimeout(set, clear) {
+ let reportingPolicy =
+ Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", {}).Policy;
+ reportingPolicy.setShowInfobarTimeout = set;
+ reportingPolicy.clearShowInfobarTimeout = clear;
+}
+
+function sendSessionRestoredNotification() {
+ let reportingPolicyImpl =
+ Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", {}).TelemetryReportingPolicyImpl;
+ reportingPolicyImpl.observe(null, "sessionstore-windows-restored", null);
+}
+
+/**
+ * Wait for a tick.
+ */
+function promiseNextTick() {
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+/**
+ * Wait for a notification to be shown in a notification box.
+ * @param {Object} aNotificationBox The notification box.
+ * @return {Promise} Resolved when the notification is displayed.
+ */
+function promiseWaitForAlertActive(aNotificationBox) {
+ let deferred = PromiseUtils.defer();
+ aNotificationBox.addEventListener("AlertActive", function onActive() {
+ aNotificationBox.removeEventListener("AlertActive", onActive, true);
+ deferred.resolve();
+ });
+ return deferred.promise;
+}
+
+/**
+ * Wait for a notification to be closed.
+ * @param {Object} aNotification The notification.
+ * @return {Promise} Resolved when the notification is closed.
+ */
+function promiseWaitForNotificationClose(aNotification) {
+ let deferred = PromiseUtils.defer();
+ waitForNotificationClose(aNotification, deferred.resolve);
+ return deferred.promise;
+}
+
+function triggerInfoBar(expectedTimeoutMs) {
+ let showInfobarCallback = null;
+ let timeoutMs = null;
+ fakeShowPolicyTimeout((callback, timeout) => {
+ showInfobarCallback = callback;
+ timeoutMs = timeout;
+ }, () => {});
+ sendSessionRestoredNotification();
+ Assert.ok(!!showInfobarCallback, "Must have a timer callback.");
+ if (expectedTimeoutMs !== undefined) {
+ Assert.equal(timeoutMs, expectedTimeoutMs, "Timeout should match");
+ }
+ showInfobarCallback();
+}
+
+var checkInfobarButton = Task.async(function* (aNotification) {
+ // Check that the button on the data choices infobar does the right thing.
+ let buttons = aNotification.getElementsByTagName("button");
+ Assert.equal(buttons.length, 1, "There is 1 button in the data reporting notification.");
+ let button = buttons[0];
+
+ // Add an observer to ensure the "advanced" pane opened (but don't bother
+ // closing it - we close the entire window when done.)
+ let paneLoadedPromise = promiseTopicObserved("advanced-pane-loaded");
+
+ // Click on the button.
+ button.click();
+
+ // Wait for the preferences panel to open.
+ yield paneLoadedPromise;
+ yield promiseNextTick();
+});
+
+add_task(function* setup() {
+ const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, true);
+ const currentPolicyVersion = Preferences.get(PREF_CURRENT_POLICY_VERSION, 1);
+
+ // Register a cleanup function to reset our preferences.
+ registerCleanupFunction(() => {
+ Preferences.set(PREF_BYPASS_NOTIFICATION, bypassNotification);
+ Preferences.set(PREF_CURRENT_POLICY_VERSION, currentPolicyVersion);
+
+ return closeAllNotifications();
+ });
+
+ // Don't skip the infobar visualisation.
+ Preferences.set(PREF_BYPASS_NOTIFICATION, false);
+ // Set the current policy version.
+ Preferences.set(PREF_CURRENT_POLICY_VERSION, TEST_POLICY_VERSION);
+});
+
+function clearAcceptedPolicy() {
+ // Reset the accepted policy.
+ Preferences.reset(PREF_ACCEPTED_POLICY_VERSION);
+ Preferences.reset(PREF_ACCEPTED_POLICY_DATE);
+}
+
+add_task(function* test_single_window() {
+ clearAcceptedPolicy();
+
+ // Close all the notifications, then try to trigger the data choices infobar.
+ yield closeAllNotifications();
+
+ let notificationBox = document.getElementById("global-notificationbox");
+
+ // Make sure that we have a coherent initial state.
+ Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0), 0,
+ "No version should be set on init.");
+ Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_DATE, 0), 0,
+ "No date should be set on init.");
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(),
+ "User not notified about datareporting policy.");
+
+ let alertShownPromise = promiseWaitForAlertActive(notificationBox);
+ Assert.ok(!TelemetryReportingPolicy.canUpload(),
+ "User should not be allowed to upload.");
+
+ // Wait for the infobar to be displayed.
+ triggerInfoBar(10 * 1000);
+ yield alertShownPromise;
+
+ Assert.equal(notificationBox.allNotifications.length, 1, "Notification Displayed.");
+ Assert.ok(TelemetryReportingPolicy.canUpload(), "User should be allowed to upload now.");
+
+ yield promiseNextTick();
+ let promiseClosed = promiseWaitForNotificationClose(notificationBox.currentNotification);
+ yield checkInfobarButton(notificationBox.currentNotification);
+ yield promiseClosed;
+
+ Assert.equal(notificationBox.allNotifications.length, 0, "No notifications remain.");
+
+ // Check that we are still clear to upload and that the policy data is saved.
+ Assert.ok(TelemetryReportingPolicy.canUpload());
+ Assert.equal(TelemetryReportingPolicy.testIsUserNotified(), true,
+ "User notified about datareporting policy.");
+ Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0), TEST_POLICY_VERSION,
+ "Version pref set.");
+ Assert.greater(parseInt(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), 10), -1,
+ "Date pref set.");
+});
+
+add_task(function* test_multiple_windows() {
+ clearAcceptedPolicy();
+
+ // Close all the notifications, then try to trigger the data choices infobar.
+ yield closeAllNotifications();
+
+ // Ensure we see the notification on all windows and that action on one window
+ // results in dismiss on every window.
+ let otherWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ // Get the notification box for both windows.
+ let notificationBoxes = [
+ document.getElementById("global-notificationbox"),
+ otherWindow.document.getElementById("global-notificationbox")
+ ];
+
+ Assert.ok(notificationBoxes[1], "2nd window has a global notification box.");
+
+ // Make sure that we have a coherent initial state.
+ Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0), 0, "No version should be set on init.");
+ Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_DATE, 0), 0, "No date should be set on init.");
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(), "User not notified about datareporting policy.");
+
+ let showAlertPromises = [
+ promiseWaitForAlertActive(notificationBoxes[0]),
+ promiseWaitForAlertActive(notificationBoxes[1])
+ ];
+
+ Assert.ok(!TelemetryReportingPolicy.canUpload(),
+ "User should not be allowed to upload.");
+
+ // Wait for the infobars.
+ triggerInfoBar(10 * 1000);
+ yield Promise.all(showAlertPromises);
+
+ // Both notification were displayed. Close one and check that both gets closed.
+ let closeAlertPromises = [
+ promiseWaitForNotificationClose(notificationBoxes[0].currentNotification),
+ promiseWaitForNotificationClose(notificationBoxes[1].currentNotification)
+ ];
+ notificationBoxes[0].currentNotification.close();
+ yield Promise.all(closeAlertPromises);
+
+ // Close the second window we opened.
+ yield BrowserTestUtils.closeWindow(otherWindow);
+
+ // Check that we are clear to upload and that the policy data us saved.
+ Assert.ok(TelemetryReportingPolicy.canUpload(), "User should be allowed to upload now.");
+ Assert.equal(TelemetryReportingPolicy.testIsUserNotified(), true,
+ "User notified about datareporting policy.");
+ Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0), TEST_POLICY_VERSION,
+ "Version pref set.");
+ Assert.greater(parseInt(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), 10), -1,
+ "Date pref set.");
+});
diff --git a/browser/base/content/test/general/browser_decoderDoctor.js b/browser/base/content/test/general/browser_decoderDoctor.js
new file mode 100644
index 000000000..a37972160
--- /dev/null
+++ b/browser/base/content/test/general/browser_decoderDoctor.js
@@ -0,0 +1,122 @@
+"use strict";
+
+function* test_decoder_doctor_notification(type, notificationMessage, options) {
+ yield BrowserTestUtils.withNewTab({ gBrowser }, function*(browser) {
+ let awaitNotificationBar =
+ BrowserTestUtils.waitForNotificationBar(gBrowser, browser, "decoder-doctor-notification");
+
+ yield ContentTask.spawn(browser, type, function*(aType) {
+ Services.obs.notifyObservers(content.window,
+ "decoder-doctor-notification",
+ JSON.stringify({type: aType,
+ isSolved: false,
+ decoderDoctorReportId: "test",
+ formats: "test"}));
+ });
+
+ let notification;
+ try {
+ notification = yield awaitNotificationBar;
+ } catch (ex) {
+ ok(false, ex);
+ return;
+ }
+ ok(notification, "Got decoder-doctor-notification notification");
+
+ is(notification.getAttribute("label"), notificationMessage,
+ "notification message should match expectation");
+ let button = notification.childNodes[0];
+ if (options && options.noLearnMoreButton) {
+ ok(!button, "There should not be a Learn More button");
+ return;
+ }
+
+ is(button.getAttribute("label"), gNavigatorBundle.getString("decoder.noCodecs.button"),
+ "notification button should be 'Learn more'");
+ is(button.getAttribute("accesskey"), gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ "notification button should have accesskey");
+
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ let url = baseURL + ((options && options.sumo) ||
+ "fix-video-audio-problems-firefox-windows");
+ let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, url);
+ button.click();
+ let sumoTab = yield awaitNewTab;
+ yield BrowserTestUtils.removeTab(sumoTab);
+ });
+}
+
+add_task(function* test_adobe_cdm_not_found() {
+ // This is only sent on Windows.
+ if (AppConstants.platform != "win") {
+ return;
+ }
+
+ let message;
+ if (AppConstants.isPlatformAndVersionAtMost("win", "5.9")) {
+ message = gNavigatorBundle.getFormattedString("emeNotifications.drmContentDisabled.message", [""]);
+ } else {
+ message = gNavigatorBundle.getString("decoder.noCodecs.message");
+ }
+
+ yield test_decoder_doctor_notification("adobe-cdm-not-found", message);
+});
+
+add_task(function* test_adobe_cdm_not_activated() {
+ // This is only sent on Windows.
+ if (AppConstants.platform != "win") {
+ return;
+ }
+
+ let message;
+ if (AppConstants.isPlatformAndVersionAtMost("win", "5.9")) {
+ message = gNavigatorBundle.getString("decoder.noCodecsXP.message");
+ } else {
+ message = gNavigatorBundle.getString("decoder.noCodecs.message");
+ }
+
+ yield test_decoder_doctor_notification("adobe-cdm-not-activated", message);
+});
+
+add_task(function* test_platform_decoder_not_found() {
+ // Not sent on Windows XP.
+ if (AppConstants.isPlatformAndVersionAtMost("win", "5.9")) {
+ return;
+ }
+
+ let message;
+ let isLinux = AppConstants.platform == "linux";
+ if (isLinux) {
+ message = gNavigatorBundle.getString("decoder.noCodecsLinux.message");
+ } else {
+ message = gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+ }
+
+ yield test_decoder_doctor_notification("platform-decoder-not-found",
+ message,
+ {noLearnMoreButton: isLinux});
+});
+
+add_task(function* test_cannot_initialize_pulseaudio() {
+ // This is only sent on Linux.
+ if (AppConstants.platform != "linux") {
+ return;
+ }
+
+ let message = gNavigatorBundle.getString("decoder.noPulseAudio.message");
+ yield test_decoder_doctor_notification("cannot-initialize-pulseaudio",
+ message,
+ {sumo: "fix-common-audio-and-video-issues"});
+});
+
+add_task(function* test_unsupported_libavcodec() {
+ // This is only sent on Linux.
+ if (AppConstants.platform != "linux") {
+ return;
+ }
+
+ let message = gNavigatorBundle.getString("decoder.unsupportedLibavcodec.message");
+ yield test_decoder_doctor_notification("unsupported-libavcodec",
+ message,
+ {noLearnMoreButton: true});
+});
diff --git a/browser/base/content/test/general/browser_devedition.js b/browser/base/content/test/general/browser_devedition.js
new file mode 100644
index 000000000..06ee42e7e
--- /dev/null
+++ b/browser/base/content/test/general/browser_devedition.js
@@ -0,0 +1,129 @@
+/*
+ * Testing changes for Developer Edition theme.
+ * A special stylesheet should be added to the browser.xul document
+ * when the firefox-devedition@mozilla.org lightweight theme
+ * is applied.
+ */
+
+const PREF_LWTHEME_USED_THEMES = "lightweightThemes.usedThemes";
+const PREF_DEVTOOLS_THEME = "devtools.theme";
+const {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
+
+LightweightThemeManager.clearBuiltInThemes();
+LightweightThemeManager.addBuiltInTheme(dummyLightweightTheme("firefox-devedition@mozilla.org"));
+
+registerCleanupFunction(() => {
+ // Set preferences back to their original values
+ LightweightThemeManager.currentTheme = null;
+ Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+ Services.prefs.clearUserPref(PREF_LWTHEME_USED_THEMES);
+
+ LightweightThemeManager.currentTheme = null;
+ LightweightThemeManager.clearBuiltInThemes();
+});
+
+add_task(function* startTests() {
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
+
+ info ("Setting the current theme to null");
+ LightweightThemeManager.currentTheme = null;
+ ok (!DevEdition.isStyleSheetEnabled, "There is no devedition style sheet when no lw theme is applied.");
+
+ info ("Adding a lightweight theme.");
+ LightweightThemeManager.currentTheme = dummyLightweightTheme("preview0");
+ ok (!DevEdition.isStyleSheetEnabled, "The devedition stylesheet has been removed when a lightweight theme is applied.");
+
+ info ("Applying the devedition lightweight theme.");
+ let onAttributeAdded = waitForBrightTitlebarAttribute();
+ LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme("firefox-devedition@mozilla.org");
+ ok (DevEdition.isStyleSheetEnabled, "The devedition stylesheet has been added when the devedition lightweight theme is applied");
+ yield onAttributeAdded;
+ is (document.documentElement.getAttribute("brighttitlebarforeground"), "true",
+ "The brighttitlebarforeground attribute is set on the window.");
+
+ info ("Unapplying all themes.");
+ LightweightThemeManager.currentTheme = null;
+ ok (!DevEdition.isStyleSheetEnabled, "There is no devedition style sheet when no lw theme is applied.");
+
+ info ("Applying the devedition lightweight theme.");
+ onAttributeAdded = waitForBrightTitlebarAttribute();
+ LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme("firefox-devedition@mozilla.org");
+ ok (DevEdition.isStyleSheetEnabled, "The devedition stylesheet has been added when the devedition lightweight theme is applied");
+ yield onAttributeAdded;
+ ok (document.documentElement.hasAttribute("brighttitlebarforeground"),
+ "The brighttitlebarforeground attribute is set on the window with dark devtools theme.");
+});
+
+add_task(function* testDevtoolsTheme() {
+ info ("Checking stylesheet and :root attributes based on devtools theme.");
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
+ is (document.documentElement.getAttribute("devtoolstheme"), "light",
+ "The documentElement has an attribute based on devtools theme.");
+ ok (DevEdition.isStyleSheetEnabled, "The devedition stylesheet is still there with the light devtools theme.");
+ ok (!document.documentElement.hasAttribute("brighttitlebarforeground"),
+ "The brighttitlebarforeground attribute is not set on the window with light devtools theme.");
+
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
+ is (document.documentElement.getAttribute("devtoolstheme"), "dark",
+ "The documentElement has an attribute based on devtools theme.");
+ ok (DevEdition.isStyleSheetEnabled, "The devedition stylesheet is still there with the dark devtools theme.");
+ is (document.documentElement.getAttribute("brighttitlebarforeground"), "true",
+ "The brighttitlebarforeground attribute is set on the window with dark devtools theme.");
+
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "foobar");
+ is (document.documentElement.getAttribute("devtoolstheme"), "light",
+ "The documentElement has 'light' as a default for the devtoolstheme attribute");
+ ok (DevEdition.isStyleSheetEnabled, "The devedition stylesheet is still there with the foobar devtools theme.");
+ ok (!document.documentElement.hasAttribute("brighttitlebarforeground"),
+ "The brighttitlebarforeground attribute is not set on the window with light devtools theme.");
+});
+
+function dummyLightweightTheme(id) {
+ return {
+ id: id,
+ name: id,
+ headerURL: "resource:///chrome/browser/content/browser/defaultthemes/devedition.header.png",
+ iconURL: "resource:///chrome/browser/content/browser/defaultthemes/devedition.icon.png",
+ textcolor: "red",
+ accentcolor: "blue"
+ };
+}
+
+add_task(function* testLightweightThemePreview() {
+ info ("Setting devedition to current and the previewing others");
+ LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme("firefox-devedition@mozilla.org");
+ ok (DevEdition.isStyleSheetEnabled, "The devedition stylesheet is enabled.");
+ LightweightThemeManager.previewTheme(dummyLightweightTheme("preview0"));
+ ok (!DevEdition.isStyleSheetEnabled, "The devedition stylesheet is not enabled after a lightweight theme preview.");
+ LightweightThemeManager.resetPreview();
+ LightweightThemeManager.previewTheme(dummyLightweightTheme("preview1"));
+ ok (!DevEdition.isStyleSheetEnabled, "The devedition stylesheet is not enabled after a second lightweight theme preview.");
+ LightweightThemeManager.resetPreview();
+ ok (DevEdition.isStyleSheetEnabled, "The devedition stylesheet is enabled again after resetting the preview.");
+ LightweightThemeManager.currentTheme = null;
+ ok (!DevEdition.isStyleSheetEnabled, "The devedition stylesheet is gone after removing the current theme.");
+
+ info ("Previewing the devedition theme");
+ LightweightThemeManager.previewTheme(LightweightThemeManager.getUsedTheme("firefox-devedition@mozilla.org"));
+ ok (DevEdition.isStyleSheetEnabled, "The devedition stylesheet is enabled.");
+ LightweightThemeManager.previewTheme(dummyLightweightTheme("preview2"));
+ LightweightThemeManager.resetPreview();
+ ok (!DevEdition.isStyleSheetEnabled, "The devedition stylesheet is now disabled after resetting the preview.");
+});
+
+// Use a mutation observer to wait for the brighttitlebarforeground
+// attribute to change. Using this instead of waiting for the load
+// event on the DevEdition styleSheet.
+function waitForBrightTitlebarAttribute() {
+ return new Promise((resolve, reject) => {
+ let mutationObserver = new MutationObserver(function (mutations) {
+ for (let mutation of mutations) {
+ if (mutation.attributeName == "brighttitlebarforeground") {
+ mutationObserver.disconnect();
+ resolve();
+ }
+ }
+ });
+ mutationObserver.observe(document.documentElement, { attributes: true });
+ });
+}
diff --git a/browser/base/content/test/general/browser_discovery.js b/browser/base/content/test/general/browser_discovery.js
new file mode 100644
index 000000000..23d44c6a9
--- /dev/null
+++ b/browser/base/content/test/general/browser_discovery.js
@@ -0,0 +1,162 @@
+var browser;
+
+function doc() {
+ return browser.contentDocument;
+}
+
+function setHandlerFunc(aResultFunc) {
+ gBrowser.addEventListener("DOMLinkAdded", function (event) {
+ gBrowser.removeEventListener("DOMLinkAdded", arguments.callee, false);
+ executeSoon(aResultFunc);
+ }, false);
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ browser = gBrowser.selectedBrowser;
+ browser.addEventListener("load", function (event) {
+ event.currentTarget.removeEventListener("load", arguments.callee, true);
+ iconDiscovery();
+ }, true);
+ var rootDir = getRootDirectory(gTestPath);
+ content.location = rootDir + "discovery.html";
+}
+
+var iconDiscoveryTests = [
+ { text: "rel icon discovered" },
+ { rel: "abcdefg icon qwerty", text: "rel may contain additional rels separated by spaces" },
+ { rel: "ICON", text: "rel is case insensitive" },
+ { rel: "shortcut-icon", pass: false, text: "rel shortcut-icon not discovered" },
+ { href: "moz.png", text: "relative href works" },
+ { href: "notthere.png", text: "404'd icon is removed properly" },
+ { href: "data:image/x-icon,%00", type: "image/x-icon", text: "data: URIs work" },
+ { type: "image/png; charset=utf-8", text: "type may have optional parameters (RFC2046)" }
+];
+
+function runIconDiscoveryTest() {
+ var testCase = iconDiscoveryTests[0];
+ var head = doc().getElementById("linkparent");
+ var hasSrc = gBrowser.getIcon() != null;
+ if (testCase.pass)
+ ok(hasSrc, testCase.text);
+ else
+ ok(!hasSrc, testCase.text);
+
+ head.removeChild(head.getElementsByTagName('link')[0]);
+ iconDiscoveryTests.shift();
+ iconDiscovery(); // Run the next test.
+}
+
+function iconDiscovery() {
+ if (iconDiscoveryTests.length) {
+ setHandlerFunc(runIconDiscoveryTest);
+ gBrowser.setIcon(gBrowser.selectedTab, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ var testCase = iconDiscoveryTests[0];
+ var head = doc().getElementById("linkparent");
+ var link = doc().createElement("link");
+
+ var rootDir = getRootDirectory(gTestPath);
+ var rel = testCase.rel || "icon";
+ var href = testCase.href || rootDir + "moz.png";
+ var type = testCase.type || "image/png";
+ if (testCase.pass == undefined)
+ testCase.pass = true;
+
+ link.rel = rel;
+ link.href = href;
+ link.type = type;
+ head.appendChild(link);
+ } else {
+ searchDiscovery();
+ }
+}
+
+var searchDiscoveryTests = [
+ { text: "rel search discovered" },
+ { rel: "SEARCH", text: "rel is case insensitive" },
+ { rel: "-search-", pass: false, text: "rel -search- not discovered" },
+ { rel: "foo bar baz search quux", text: "rel may contain additional rels separated by spaces" },
+ { href: "https://not.mozilla.com", text: "HTTPS ok" },
+ { href: "ftp://not.mozilla.com", text: "FTP ok" },
+ { href: "data:text/foo,foo", pass: false, text: "data URI not permitted" },
+ { href: "javascript:alert(0)", pass: false, text: "JS URI not permitted" },
+ { type: "APPLICATION/OPENSEARCHDESCRIPTION+XML", text: "type is case insensitve" },
+ { type: " application/opensearchdescription+xml ", text: "type may contain extra whitespace" },
+ { type: "application/opensearchdescription+xml; charset=utf-8", text: "type may have optional parameters (RFC2046)" },
+ { type: "aapplication/opensearchdescription+xml", pass: false, text: "type should not be loosely matched" },
+ { rel: "search search search", count: 1, text: "only one engine should be added" }
+];
+
+function runSearchDiscoveryTest() {
+ var testCase = searchDiscoveryTests[0];
+ var title = testCase.title || searchDiscoveryTests.length;
+ if (browser.engines) {
+ var hasEngine = (testCase.count) ? (browser.engines[0].title == title &&
+ browser.engines.length == testCase.count) :
+ (browser.engines[0].title == title);
+ ok(hasEngine, testCase.text);
+ browser.engines = null;
+ }
+ else
+ ok(!testCase.pass, testCase.text);
+
+ searchDiscoveryTests.shift();
+ searchDiscovery(); // Run the next test.
+}
+
+// This handler is called twice, once for each added link element.
+// Only want to check once the second link element has been added.
+var ranOnce = false;
+function runMultipleEnginesTestAndFinalize() {
+ if (!ranOnce) {
+ ranOnce = true;
+ return;
+ }
+ ok(browser.engines, "has engines");
+ is(browser.engines.length, 1, "only one engine");
+ is(browser.engines[0].uri, "http://first.mozilla.com/search.xml", "first engine wins");
+
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function searchDiscovery() {
+ let head = doc().getElementById("linkparent");
+
+ if (searchDiscoveryTests.length) {
+ setHandlerFunc(runSearchDiscoveryTest);
+ let testCase = searchDiscoveryTests[0];
+ let link = doc().createElement("link");
+
+ let rel = testCase.rel || "search";
+ let href = testCase.href || "http://so.not.here.mozilla.com/search.xml";
+ let type = testCase.type || "application/opensearchdescription+xml";
+ let title = testCase.title || searchDiscoveryTests.length;
+ if (testCase.pass == undefined)
+ testCase.pass = true;
+
+ link.rel = rel;
+ link.href = href;
+ link.type = type;
+ link.title = title;
+ head.appendChild(link);
+ } else {
+ setHandlerFunc(runMultipleEnginesTestAndFinalize);
+ setHandlerFunc(runMultipleEnginesTestAndFinalize);
+ // Test multiple engines with the same title
+ let link = doc().createElement("link");
+ link.rel = "search";
+ link.href = "http://first.mozilla.com/search.xml";
+ link.type = "application/opensearchdescription+xml";
+ link.title = "Test Engine";
+ let link2 = link.cloneNode(false);
+ link2.href = "http://second.mozilla.com/search.xml";
+
+ head.appendChild(link);
+ head.appendChild(link2);
+ }
+}
diff --git a/browser/base/content/test/general/browser_documentnavigation.js b/browser/base/content/test/general/browser_documentnavigation.js
new file mode 100644
index 000000000..eb789d076
--- /dev/null
+++ b/browser/base/content/test/general/browser_documentnavigation.js
@@ -0,0 +1,266 @@
+/*
+ * This test checks that focus is adjusted properly in a browser when pressing F6 and Shift+F6.
+ * There are additional tests in dom/tests/mochitest/chrome/test_focus_docnav.xul which test
+ * non-browser cases.
+ */
+
+var testPage1 = "data:text/html,<html id='html1'><body id='body1'><button id='button1'>Tab 1</button></body></html>";
+var testPage2 = "data:text/html,<html id='html2'><body id='body2'><button id='button2'>Tab 2</button></body></html>";
+var testPage3 = "data:text/html,<html id='html3'><body id='body3' contenteditable='true'><button id='button3'>Tab 3</button></body></html>";
+
+var fm = Services.focus;
+
+function* expectFocusOnF6(backward, expectedDocument, expectedElement, onContent, desc)
+{
+ let focusChangedInChildResolver = null;
+ let focusPromise = onContent ? new Promise(resolve => focusChangedInChildResolver = resolve) :
+ BrowserTestUtils.waitForEvent(window, "focus", true);
+
+ function focusChangedListener(msg) {
+ let expected = expectedDocument;
+ if (!expectedElement.startsWith("html")) {
+ expected += "," + expectedElement;
+ }
+
+ is(msg.data.details, expected, desc + " child focus matches");
+ focusChangedInChildResolver();
+ }
+
+ if (onContent) {
+ messageManager.addMessageListener("BrowserTest:FocusChanged", focusChangedListener);
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { expectedElementId: expectedElement }, function* (arg) {
+ let contentExpectedElement = content.document.getElementById(arg.expectedElementId);
+ if (!contentExpectedElement) {
+ // Element not found, so look in the child frames.
+ for (let f = 0; f < content.frames.length; f++) {
+ if (content.frames[f].document.getElementById(arg.expectedElementId)) {
+ contentExpectedElement = content.frames[f].document;
+ break;
+ }
+ }
+ }
+ else if (contentExpectedElement.localName == "html") {
+ contentExpectedElement = contentExpectedElement.ownerDocument;
+ }
+
+ if (!contentExpectedElement) {
+ sendSyncMessage("BrowserTest:FocusChanged",
+ { details : "expected element " + arg.expectedElementId + " not found" });
+ return;
+ }
+
+ contentExpectedElement.addEventListener("focus", function focusReceived() {
+ contentExpectedElement.removeEventListener("focus", focusReceived, true);
+
+ const contentFM = Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager);
+ let details = contentFM.focusedWindow.document.documentElement.id;
+ if (contentFM.focusedElement) {
+ details += "," + contentFM.focusedElement.id;
+ }
+
+ sendSyncMessage("BrowserTest:FocusChanged", { details : details });
+ }, true);
+ });
+ }
+
+ EventUtils.synthesizeKey("VK_F6", { shiftKey: backward });
+ yield focusPromise;
+
+ if (typeof expectedElement == "string") {
+ expectedElement = fm.focusedWindow.document.getElementById(expectedElement);
+ }
+
+ if (gMultiProcessBrowser && onContent) {
+ expectedDocument = "main-window";
+ expectedElement = gBrowser.selectedBrowser;
+ }
+
+ is(fm.focusedWindow.document.documentElement.id, expectedDocument, desc + " document matches");
+ is(fm.focusedElement, expectedElement, desc + " element matches");
+
+ if (onContent) {
+ messageManager.removeMessageListener("BrowserTest:FocusChanged", focusChangedListener);
+ }
+}
+
+// Load a page and navigate between it and the chrome window.
+add_task(function* ()
+{
+ let page1Promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.selectedBrowser.loadURI(testPage1);
+ yield page1Promise;
+
+ // When the urlbar is focused, pressing F6 should focus the root of the content page.
+ gURLBar.focus();
+ yield* expectFocusOnF6(false, "html1", "html1",
+ true, "basic focus content page");
+
+ // When the content is focused, pressing F6 should focus the urlbar.
+ yield* expectFocusOnF6(false, "main-window", gURLBar.inputField,
+ false, "basic focus content page urlbar");
+
+ // When a button in content is focused, pressing F6 should focus the urlbar.
+ yield* expectFocusOnF6(false, "html1", "html1",
+ true, "basic focus content page with button focused");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { }, function* () {
+ return content.document.getElementById("button1").focus();
+ });
+
+ yield* expectFocusOnF6(false, "main-window", gURLBar.inputField,
+ false, "basic focus content page with button focused urlbar");
+
+ // The document root should be focused, not the button
+ yield* expectFocusOnF6(false, "html1", "html1",
+ true, "basic focus again content page with button focused");
+
+ // Check to ensure that the root element is focused
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { }, function* () {
+ Assert.ok(content.document.activeElement == content.document.documentElement,
+ "basic focus again content page with button focused child root is focused");
+ });
+});
+
+// Open a second tab. Document focus should skip the background tab.
+add_task(function* ()
+{
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, testPage2);
+
+ yield* expectFocusOnF6(false, "main-window", gURLBar.inputField,
+ false, "basic focus content page and second tab urlbar");
+ yield* expectFocusOnF6(false, "html2", "html2",
+ true, "basic focus content page with second tab");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Shift+F6 should navigate backwards. There's only one document here so the effect
+// is the same.
+add_task(function* ()
+{
+ gURLBar.focus();
+ yield* expectFocusOnF6(true, "html1", "html1",
+ true, "back focus content page");
+ yield* expectFocusOnF6(true, "main-window", gURLBar.inputField,
+ false, "back focus content page urlbar");
+});
+
+// Open the sidebar and navigate between the sidebar, content and top-level window
+add_task(function* ()
+{
+ let sidebar = document.getElementById("sidebar");
+
+ let loadPromise = BrowserTestUtils.waitForEvent(sidebar, "load", true);
+ SidebarUI.toggle('viewBookmarksSidebar');
+ yield loadPromise;
+
+
+ gURLBar.focus();
+ yield* expectFocusOnF6(false, "bookmarksPanel",
+ sidebar.contentDocument.getElementById("search-box").inputField,
+ false, "focus with sidebar open sidebar");
+ yield* expectFocusOnF6(false, "html1", "html1",
+ true, "focus with sidebar open content");
+ yield* expectFocusOnF6(false, "main-window", gURLBar.inputField,
+ false, "focus with sidebar urlbar");
+
+ // Now go backwards
+ yield* expectFocusOnF6(true, "html1", "html1",
+ true, "back focus with sidebar open content");
+ yield* expectFocusOnF6(true, "bookmarksPanel",
+ sidebar.contentDocument.getElementById("search-box").inputField,
+ false, "back focus with sidebar open sidebar");
+ yield* expectFocusOnF6(true, "main-window", gURLBar.inputField,
+ false, "back focus with sidebar urlbar");
+
+ SidebarUI.toggle('viewBookmarksSidebar');
+});
+
+// Navigate when the downloads panel is open
+add_task(function* ()
+{
+ yield pushPrefs(["accessibility.tabfocus", 7]);
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown", true);
+ EventUtils.synthesizeMouseAtCenter(document.getElementById("downloads-button"), { });
+ yield popupShownPromise;
+
+ gURLBar.focus();
+ yield* expectFocusOnF6(false, "main-window", document.getElementById("downloadsHistory"),
+ false, "focus with downloads panel open panel");
+ yield* expectFocusOnF6(false, "html1", "html1",
+ true, "focus with downloads panel open");
+ yield* expectFocusOnF6(false, "main-window", gURLBar.inputField,
+ false, "focus downloads panel open urlbar");
+
+ // Now go backwards
+ yield* expectFocusOnF6(true, "html1", "html1",
+ true, "back focus with downloads panel open");
+ yield* expectFocusOnF6(true, "main-window", document.getElementById("downloadsHistory"),
+ false, "back focus with downloads panel open");
+ yield* expectFocusOnF6(true, "main-window", gURLBar.inputField,
+ false, "back focus downloads panel open urlbar");
+
+ let downloadsPopup = document.getElementById("downloadsPanel");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(downloadsPopup, "popuphidden", true);
+ downloadsPopup.hidePopup();
+ yield popupHiddenPromise;
+});
+
+// Navigation with a contenteditable body
+add_task(function* ()
+{
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, testPage3);
+
+ // The body should be focused when it is editable, not the root.
+ gURLBar.focus();
+ yield* expectFocusOnF6(false, "html3", "body3",
+ true, "focus with contenteditable body");
+ yield* expectFocusOnF6(false, "main-window", gURLBar.inputField,
+ false, "focus with contenteditable body urlbar");
+
+ // Now go backwards
+
+ yield* expectFocusOnF6(false, "html3", "body3",
+ true, "back focus with contenteditable body");
+ yield* expectFocusOnF6(false, "main-window", gURLBar.inputField,
+ false, "back focus with contenteditable body urlbar");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// Navigation with a frameset loaded
+add_task(function* ()
+{
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser,
+ "http://mochi.test:8888/browser/browser/base/content/test/general/file_documentnavigation_frameset.html");
+
+ gURLBar.focus();
+ yield* expectFocusOnF6(false, "htmlframe1", "htmlframe1",
+ true, "focus on frameset frame 0");
+ yield* expectFocusOnF6(false, "htmlframe2", "htmlframe2",
+ true, "focus on frameset frame 1");
+ yield* expectFocusOnF6(false, "htmlframe3", "htmlframe3",
+ true, "focus on frameset frame 2");
+ yield* expectFocusOnF6(false, "htmlframe4", "htmlframe4",
+ true, "focus on frameset frame 3");
+ yield* expectFocusOnF6(false, "main-window", gURLBar.inputField,
+ false, "focus on frameset frame urlbar");
+
+ yield* expectFocusOnF6(true, "htmlframe4", "htmlframe4",
+ true, "back focus on frameset frame 3");
+ yield* expectFocusOnF6(true, "htmlframe3", "htmlframe3",
+ true, "back focus on frameset frame 2");
+ yield* expectFocusOnF6(true, "htmlframe2", "htmlframe2",
+ true, "back focus on frameset frame 1");
+ yield* expectFocusOnF6(true, "htmlframe1", "htmlframe1",
+ true, "back focus on frameset frame 0");
+ yield* expectFocusOnF6(true, "main-window", gURLBar.inputField,
+ false, "back focus on frameset frame urlbar");
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+// XXXndeakin add tests for browsers inside of panels
diff --git a/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js b/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js
new file mode 100644
index 000000000..054fb3cc0
--- /dev/null
+++ b/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js
@@ -0,0 +1,221 @@
+"use strict";
+
+var gMessageManager;
+
+function frameScript() {
+ addMessageListener("Test:RequestFullscreen", () => {
+ content.document.body.requestFullscreen();
+ });
+ addMessageListener("Test:ExitFullscreen", () => {
+ content.document.exitFullscreen();
+ });
+ addMessageListener("Test:QueryFullscreenState", () => {
+ sendAsyncMessage("Test:FullscreenState", {
+ inDOMFullscreen: !!content.document.fullscreenElement,
+ inFullscreen: content.fullScreen
+ });
+ });
+ content.document.addEventListener("fullscreenchange", () => {
+ sendAsyncMessage("Test:FullscreenChanged", {
+ inDOMFullscreen: !!content.document.fullscreenElement,
+ inFullscreen: content.fullScreen
+ });
+ });
+ function waitUntilActive() {
+ let doc = content.document;
+ if (doc.docShell.isActive && doc.hasFocus()) {
+ sendAsyncMessage("Test:Activated");
+ } else {
+ setTimeout(waitUntilActive, 10);
+ }
+ }
+ waitUntilActive();
+}
+
+function listenOneMessage(aMsg, aListener) {
+ function listener({ data }) {
+ gMessageManager.removeMessageListener(aMsg, listener);
+ aListener(data);
+ }
+ gMessageManager.addMessageListener(aMsg, listener);
+}
+
+function listenOneEvent(aEvent, aListener) {
+ function listener(evt) {
+ removeEventListener(aEvent, listener);
+ aListener(evt);
+ }
+ addEventListener(aEvent, listener);
+}
+
+function queryFullscreenState() {
+ return new Promise(resolve => {
+ listenOneMessage("Test:FullscreenState", resolve);
+ gMessageManager.sendAsyncMessage("Test:QueryFullscreenState");
+ });
+}
+
+function captureUnexpectedFullscreenChange() {
+ ok(false, "catched an unexpected fullscreen change");
+}
+
+const FS_CHANGE_DOM = 1 << 0;
+const FS_CHANGE_SIZE = 1 << 1;
+const FS_CHANGE_BOTH = FS_CHANGE_DOM | FS_CHANGE_SIZE;
+
+function waitForFullscreenChanges(aFlags) {
+ return new Promise(resolve => {
+ let fullscreenData = null;
+ let sizemodeChanged = false;
+ function tryResolve() {
+ if ((!(aFlags & FS_CHANGE_DOM) || fullscreenData) &&
+ (!(aFlags & FS_CHANGE_SIZE) || sizemodeChanged)) {
+ if (!fullscreenData) {
+ queryFullscreenState().then(resolve);
+ } else {
+ resolve(fullscreenData);
+ }
+ }
+ }
+ if (aFlags & FS_CHANGE_SIZE) {
+ listenOneEvent("sizemodechange", () => {
+ sizemodeChanged = true;
+ tryResolve();
+ });
+ }
+ if (aFlags & FS_CHANGE_DOM) {
+ gMessageManager.removeMessageListener(
+ "Test:FullscreenChanged", captureUnexpectedFullscreenChange);
+ listenOneMessage("Test:FullscreenChanged", data => {
+ gMessageManager.addMessageListener(
+ "Test:FullscreenChanged", captureUnexpectedFullscreenChange);
+ fullscreenData = data;
+ tryResolve();
+ });
+ }
+ });
+}
+
+var gTests = [
+ {
+ desc: "document method",
+ affectsFullscreenMode: false,
+ exitFunc: () => {
+ gMessageManager.sendAsyncMessage("Test:ExitFullscreen");
+ }
+ },
+ {
+ desc: "escape key",
+ affectsFullscreenMode: false,
+ exitFunc: () => {
+ executeSoon(() => EventUtils.synthesizeKey("VK_ESCAPE", {}));
+ }
+ },
+ {
+ desc: "F11 key",
+ affectsFullscreenMode: true,
+ exitFunc: function () {
+ executeSoon(() => EventUtils.synthesizeKey("VK_F11", {}));
+ }
+ }
+];
+
+function checkState(expectedStates, contentStates) {
+ is(contentStates.inDOMFullscreen, expectedStates.inDOMFullscreen,
+ "The DOM fullscreen state of the content should match");
+ // TODO window.fullScreen is not updated as soon as the fullscreen
+ // state flips in child process, hence checking it could cause
+ // anonying intermittent failure. As we just want to confirm the
+ // fullscreen state of the browser window, we can just check the
+ // that on the chrome window below.
+ // is(contentStates.inFullscreen, expectedStates.inFullscreen,
+ // "The fullscreen state of the content should match");
+ is(!!document.fullscreenElement, expectedStates.inDOMFullscreen,
+ "The DOM fullscreen state of the chrome should match");
+ is(window.fullScreen, expectedStates.inFullscreen,
+ "The fullscreen state of the chrome should match");
+}
+
+const kPage = "http://example.org/browser/browser/" +
+ "base/content/test/general/dummy_page.html";
+
+add_task(function* () {
+ yield pushPrefs(
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"]);
+
+ let tab = gBrowser.addTab(kPage);
+ let browser = tab.linkedBrowser;
+ gBrowser.selectedTab = tab;
+ yield waitForDocLoadComplete();
+
+ registerCleanupFunction(() => {
+ if (browser.contentWindow.fullScreen) {
+ BrowserFullScreen();
+ }
+ gBrowser.removeTab(tab);
+ });
+
+ gMessageManager = browser.messageManager;
+ gMessageManager.loadFrameScript(
+ "data:,(" + frameScript.toString() + ")();", false);
+ gMessageManager.addMessageListener(
+ "Test:FullscreenChanged", captureUnexpectedFullscreenChange);
+
+ // Wait for the document being activated, so that
+ // fullscreen request won't be denied.
+ yield new Promise(resolve => listenOneMessage("Test:Activated", resolve));
+
+ for (let test of gTests) {
+ let contentStates;
+ info("Testing exit DOM fullscreen via " + test.desc);
+
+ contentStates = yield queryFullscreenState();
+ checkState({inDOMFullscreen: false, inFullscreen: false}, contentStates);
+
+ /* DOM fullscreen without fullscreen mode */
+
+ info("> Enter DOM fullscreen");
+ gMessageManager.sendAsyncMessage("Test:RequestFullscreen");
+ contentStates = yield waitForFullscreenChanges(FS_CHANGE_BOTH);
+ checkState({inDOMFullscreen: true, inFullscreen: true}, contentStates);
+
+ info("> Exit DOM fullscreen");
+ test.exitFunc();
+ contentStates = yield waitForFullscreenChanges(FS_CHANGE_BOTH);
+ checkState({inDOMFullscreen: false, inFullscreen: false}, contentStates);
+
+ /* DOM fullscreen with fullscreen mode */
+
+ info("> Enter fullscreen mode");
+ // Need to be asynchronous because sizemodechange event could be
+ // dispatched synchronously, which would cause the event listener
+ // miss that event and wait infinitely.
+ executeSoon(() => BrowserFullScreen());
+ contentStates = yield waitForFullscreenChanges(FS_CHANGE_SIZE);
+ checkState({inDOMFullscreen: false, inFullscreen: true}, contentStates);
+
+ info("> Enter DOM fullscreen in fullscreen mode");
+ gMessageManager.sendAsyncMessage("Test:RequestFullscreen");
+ contentStates = yield waitForFullscreenChanges(FS_CHANGE_DOM);
+ checkState({inDOMFullscreen: true, inFullscreen: true}, contentStates);
+
+ info("> Exit DOM fullscreen in fullscreen mode");
+ test.exitFunc();
+ contentStates = yield waitForFullscreenChanges(
+ test.affectsFullscreenMode ? FS_CHANGE_BOTH : FS_CHANGE_DOM);
+ checkState({
+ inDOMFullscreen: false,
+ inFullscreen: !test.affectsFullscreenMode
+ }, contentStates);
+
+ /* Cleanup */
+
+ // Exit fullscreen mode if we are still in
+ if (window.fullScreen) {
+ info("> Cleanup");
+ executeSoon(() => BrowserFullScreen());
+ yield waitForFullscreenChanges(FS_CHANGE_SIZE);
+ }
+ }
+});
diff --git a/browser/base/content/test/general/browser_double_close_tab.js b/browser/base/content/test/general/browser_double_close_tab.js
new file mode 100644
index 000000000..29242c3f9
--- /dev/null
+++ b/browser/base/content/test/general/browser_double_close_tab.js
@@ -0,0 +1,80 @@
+"use strict";
+const TEST_PAGE = "http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
+var testTab;
+
+SpecialPowers.pushPrefEnv({"set": [["dom.require_user_interaction_for_beforeunload", false]]});
+
+function waitForDialog(callback) {
+ function onTabModalDialogLoaded(node) {
+ Services.obs.removeObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
+ callback(node);
+ }
+
+ // Listen for the dialog being created
+ Services.obs.addObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded", false);
+}
+
+function waitForDialogDestroyed(node, callback) {
+ // Now listen for the dialog going away again...
+ let observer = new MutationObserver(function(muts) {
+ if (!node.parentNode) {
+ ok(true, "Dialog is gone");
+ done();
+ }
+ });
+ observer.observe(node.parentNode, {childList: true});
+ let failureTimeout = setTimeout(function() {
+ ok(false, "Dialog should have been destroyed");
+ done();
+ }, 10000);
+
+ function done() {
+ clearTimeout(failureTimeout);
+ observer.disconnect();
+ observer = null;
+ callback();
+ }
+}
+
+add_task(function*() {
+ testTab = gBrowser.selectedTab = gBrowser.addTab();
+ yield promiseTabLoadEvent(testTab, TEST_PAGE);
+ // XXXgijs the reason this has nesting and callbacks rather than promises is
+ // that DOM promises resolve on the next tick. So they're scheduled
+ // in an event queue. So when we spin a new event queue for a modal dialog...
+ // everything gets messed up and the promise's .then callbacks never get
+ // called, despite resolve() being called just fine.
+ yield new Promise(resolveOuter => {
+ waitForDialog(dialogNode => {
+ waitForDialogDestroyed(dialogNode, () => {
+ let doCompletion = () => setTimeout(resolveOuter, 0);
+ info("Now checking if dialog is destroyed");
+ ok(!dialogNode.parentNode, "onbeforeunload dialog should be gone.");
+ if (dialogNode.parentNode) {
+ // Failed to remove onbeforeunload dialog, so do it ourselves:
+ let leaveBtn = dialogNode.ui.button0;
+ waitForDialogDestroyed(dialogNode, doCompletion);
+ EventUtils.synthesizeMouseAtCenter(leaveBtn, {});
+ return;
+ }
+ doCompletion();
+ });
+ // Click again:
+ document.getAnonymousElementByAttribute(testTab, "anonid", "close-button").click();
+ });
+ // Click once:
+ document.getAnonymousElementByAttribute(testTab, "anonid", "close-button").click();
+ });
+ yield promiseWaitForCondition(() => !testTab.parentNode);
+ ok(!testTab.parentNode, "Tab should be closed completely");
+});
+
+registerCleanupFunction(function() {
+ if (testTab.parentNode) {
+ // Remove the handler, or closing this tab will prove tricky:
+ try {
+ testTab.linkedBrowser.contentWindow.onbeforeunload = null;
+ } catch (ex) {}
+ gBrowser.removeTab(testTab);
+ }
+});
diff --git a/browser/base/content/test/general/browser_drag.js b/browser/base/content/test/general/browser_drag.js
new file mode 100644
index 000000000..64ad19bde
--- /dev/null
+++ b/browser/base/content/test/general/browser_drag.js
@@ -0,0 +1,45 @@
+function test()
+{
+ waitForExplicitFinish();
+
+ let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+ let EventUtils = {};
+ scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+ // ---- Test dragging the proxy icon ---
+ var value = content.location.href;
+ var urlString = value + "\n" + content.document.title;
+ var htmlString = "<a href=\"" + value + "\">" + value + "</a>";
+ var expected = [ [
+ { type : "text/x-moz-url",
+ data : urlString },
+ { type : "text/uri-list",
+ data : value },
+ { type : "text/plain",
+ data : value },
+ { type : "text/html",
+ data : htmlString }
+ ] ];
+ // set the valid attribute so dropping is allowed
+ var oldstate = gURLBar.getAttribute("pageproxystate");
+ gURLBar.setAttribute("pageproxystate", "valid");
+ var dt = EventUtils.synthesizeDragStart(document.getElementById("identity-box"), expected);
+ is(dt, null, "drag on proxy icon");
+ gURLBar.setAttribute("pageproxystate", oldstate);
+ // Now, the identity information panel is opened by the proxy icon click.
+ // We need to close it for next tests.
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, window);
+
+ // now test dragging onto a tab
+ var tab = gBrowser.addTab("about:blank", {skipAnimation: true});
+ var browser = gBrowser.getBrowserForTab(tab);
+
+ browser.addEventListener("load", function () {
+ is(browser.contentWindow.location, "http://mochi.test:8888/", "drop on tab");
+ gBrowser.removeTab(tab);
+ finish();
+ }, true);
+
+ EventUtils.synthesizeDrop(tab, tab, [[{type: "text/uri-list", data: "http://mochi.test:8888/"}]], "copy", window);
+}
diff --git a/browser/base/content/test/general/browser_duplicateIDs.js b/browser/base/content/test/general/browser_duplicateIDs.js
new file mode 100644
index 000000000..38fc17820
--- /dev/null
+++ b/browser/base/content/test/general/browser_duplicateIDs.js
@@ -0,0 +1,8 @@
+function test() {
+ var ids = {};
+ Array.forEach(document.querySelectorAll("[id]"), function (node) {
+ var id = node.id;
+ ok(!(id in ids), id + " should be unique");
+ ids[id] = null;
+ });
+}
diff --git a/browser/base/content/test/general/browser_e10s_about_process.js b/browser/base/content/test/general/browser_e10s_about_process.js
new file mode 100644
index 000000000..2b4816754
--- /dev/null
+++ b/browser/base/content/test/general/browser_e10s_about_process.js
@@ -0,0 +1,114 @@
+const CHROME_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+const CONTENT_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+
+const CHROME = {
+ id: "cb34538a-d9da-40f3-b61a-069f0b2cb9fb",
+ path: "test-chrome",
+ flags: 0,
+}
+const CANREMOTE = {
+ id: "2480d3e1-9ce4-4b84-8ae3-910b9a95cbb3",
+ path: "test-allowremote",
+ flags: Ci.nsIAboutModule.URI_CAN_LOAD_IN_CHILD,
+}
+const MUSTREMOTE = {
+ id: "f849cee5-e13e-44d2-981d-0fb3884aaead",
+ path: "test-mustremote",
+ flags: Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD,
+}
+
+const TEST_MODULES = [
+ CHROME,
+ CANREMOTE,
+ MUSTREMOTE
+]
+
+function AboutModule() {
+}
+
+AboutModule.prototype = {
+ newChannel: function(aURI, aLoadInfo) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ getURIFlags: function(aURI) {
+ for (let module of TEST_MODULES) {
+ if (aURI.path.startsWith(module.path)) {
+ return module.flags;
+ }
+ }
+
+ ok(false, "Called getURIFlags for an unknown page " + aURI.spec);
+ return 0;
+ },
+
+ getIndexedDBOriginPostfix: function(aURI) {
+ return null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule])
+};
+
+var AboutModuleFactory = {
+ createInstance: function(aOuter, aIID) {
+ if (aOuter)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ return new AboutModule().QueryInterface(aIID);
+ },
+
+ lockFactory: function(aLock) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
+};
+
+add_task(function* init() {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ for (let module of TEST_MODULES) {
+ registrar.registerFactory(Components.ID(module.id), "",
+ "@mozilla.org/network/protocol/about;1?what=" + module.path,
+ AboutModuleFactory);
+ }
+});
+
+registerCleanupFunction(() => {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ for (let module of TEST_MODULES) {
+ registrar.unregisterFactory(Components.ID(module.id), AboutModuleFactory);
+ }
+});
+
+function test_url(url, chromeResult, contentResult) {
+ is(E10SUtils.canLoadURIInProcess(url, CHROME_PROCESS),
+ chromeResult, "Check URL in chrome process.");
+ is(E10SUtils.canLoadURIInProcess(url, CONTENT_PROCESS),
+ contentResult, "Check URL in content process.");
+
+ is(E10SUtils.canLoadURIInProcess(url + "#foo", CHROME_PROCESS),
+ chromeResult, "Check URL with ref in chrome process.");
+ is(E10SUtils.canLoadURIInProcess(url + "#foo", CONTENT_PROCESS),
+ contentResult, "Check URL with ref in content process.");
+
+ is(E10SUtils.canLoadURIInProcess(url + "?foo", CHROME_PROCESS),
+ chromeResult, "Check URL with query in chrome process.");
+ is(E10SUtils.canLoadURIInProcess(url + "?foo", CONTENT_PROCESS),
+ contentResult, "Check URL with query in content process.");
+
+ is(E10SUtils.canLoadURIInProcess(url + "?foo#bar", CHROME_PROCESS),
+ chromeResult, "Check URL with query and ref in chrome process.");
+ is(E10SUtils.canLoadURIInProcess(url + "?foo#bar", CONTENT_PROCESS),
+ contentResult, "Check URL with query and ref in content process.");
+}
+
+add_task(function* test_chrome() {
+ test_url("about:" + CHROME.path, true, false);
+});
+
+add_task(function* test_any() {
+ test_url("about:" + CANREMOTE.path, true, true);
+});
+
+add_task(function* test_remote() {
+ test_url("about:" + MUSTREMOTE.path, false, true);
+});
diff --git a/browser/base/content/test/general/browser_e10s_chrome_process.js b/browser/base/content/test/general/browser_e10s_chrome_process.js
new file mode 100644
index 000000000..0726447ce
--- /dev/null
+++ b/browser/base/content/test/general/browser_e10s_chrome_process.js
@@ -0,0 +1,150 @@
+// Returns a function suitable for add_task which loads startURL, runs
+// transitionTask and waits for endURL to load, checking that the URLs were
+// loaded in the correct process.
+function makeTest(name, startURL, startProcessIsRemote, endURL, endProcessIsRemote, transitionTask) {
+ return function*() {
+ info("Running test " + name + ", " + transitionTask.name);
+ let browser = gBrowser.selectedBrowser;
+
+ // In non-e10s nothing should be remote
+ if (!gMultiProcessBrowser) {
+ startProcessIsRemote = false;
+ endProcessIsRemote = false;
+ }
+
+ // Load the initial URL and make sure we are in the right initial process
+ info("Loading initial URL");
+ browser.loadURI(startURL);
+ yield waitForDocLoadComplete();
+
+ is(browser.currentURI.spec, startURL, "Shouldn't have been redirected");
+ is(browser.isRemoteBrowser, startProcessIsRemote, "Should be displayed in the right process");
+
+ let docLoadedPromise = waitForDocLoadComplete();
+ let asyncTask = Task.async(transitionTask);
+ let expectSyncChange = yield asyncTask(browser, endURL);
+ if (expectSyncChange) {
+ is(browser.isRemoteBrowser, endProcessIsRemote, "Should have switched to the right process synchronously");
+ }
+ yield docLoadedPromise;
+
+ is(browser.currentURI.spec, endURL, "Should have made it to the final URL");
+ is(browser.isRemoteBrowser, endProcessIsRemote, "Should be displayed in the right process");
+ }
+}
+
+const CHROME_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+const CONTENT_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+const PATH = (getRootDirectory(gTestPath) + "test_process_flags_chrome.html").replace("chrome://mochitests", "");
+
+const CHROME = "chrome://mochitests" + PATH;
+const CANREMOTE = "chrome://mochitests-any" + PATH;
+const MUSTREMOTE = "chrome://mochitests-content" + PATH;
+
+add_task(function* init() {
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+});
+
+registerCleanupFunction(() => {
+ gBrowser.removeCurrentTab();
+});
+
+function test_url(url, chromeResult, contentResult) {
+ is(E10SUtils.canLoadURIInProcess(url, CHROME_PROCESS),
+ chromeResult, "Check URL in chrome process.");
+ is(E10SUtils.canLoadURIInProcess(url, CONTENT_PROCESS),
+ contentResult, "Check URL in content process.");
+
+ is(E10SUtils.canLoadURIInProcess(url + "#foo", CHROME_PROCESS),
+ chromeResult, "Check URL with ref in chrome process.");
+ is(E10SUtils.canLoadURIInProcess(url + "#foo", CONTENT_PROCESS),
+ contentResult, "Check URL with ref in content process.");
+
+ is(E10SUtils.canLoadURIInProcess(url + "?foo", CHROME_PROCESS),
+ chromeResult, "Check URL with query in chrome process.");
+ is(E10SUtils.canLoadURIInProcess(url + "?foo", CONTENT_PROCESS),
+ contentResult, "Check URL with query in content process.");
+
+ is(E10SUtils.canLoadURIInProcess(url + "?foo#bar", CHROME_PROCESS),
+ chromeResult, "Check URL with query and ref in chrome process.");
+ is(E10SUtils.canLoadURIInProcess(url + "?foo#bar", CONTENT_PROCESS),
+ contentResult, "Check URL with query and ref in content process.");
+}
+
+add_task(function* test_chrome() {
+ test_url(CHROME, true, false);
+});
+
+add_task(function* test_any() {
+ test_url(CANREMOTE, true, true);
+});
+
+add_task(function* test_remote() {
+ test_url(MUSTREMOTE, false, true);
+});
+
+// The set of page transitions
+var TESTS = [
+ [
+ "chrome -> chrome",
+ CHROME, false,
+ CHROME, false,
+ ],
+ [
+ "chrome -> canremote",
+ CHROME, false,
+ CANREMOTE, false,
+ ],
+ [
+ "chrome -> mustremote",
+ CHROME, false,
+ MUSTREMOTE, true,
+ ],
+ [
+ "remote -> chrome",
+ MUSTREMOTE, true,
+ CHROME, false,
+ ],
+ [
+ "remote -> canremote",
+ MUSTREMOTE, true,
+ CANREMOTE, true,
+ ],
+ [
+ "remote -> mustremote",
+ MUSTREMOTE, true,
+ MUSTREMOTE, true,
+ ],
+];
+
+// The different ways to transition from one page to another
+var TRANSITIONS = [
+// Loads the new page by calling browser.loadURI directly
+function* loadURI(browser, uri) {
+ info("Calling browser.loadURI");
+ yield BrowserTestUtils.loadURI(browser, uri);
+ return true;
+},
+
+// Loads the new page by finding a link with the right href in the document and
+// clicking it
+function* clickLink(browser, uri) {
+ info("Clicking link");
+
+ function frame_script(frameUri) {
+ let link = content.document.querySelector("a[href='" + frameUri + "']");
+ link.click();
+ }
+
+ browser.messageManager.loadFrameScript("data:,(" + frame_script.toString() + ")(" + JSON.stringify(uri) + ");", false);
+
+ return false;
+},
+];
+
+// Creates a set of test tasks, one for each combination of TESTS and TRANSITIONS.
+for (let test of TESTS) {
+ for (let transition of TRANSITIONS) {
+ add_task(makeTest(...test, transition));
+ }
+}
diff --git a/browser/base/content/test/general/browser_e10s_javascript.js b/browser/base/content/test/general/browser_e10s_javascript.js
new file mode 100644
index 000000000..90e847b09
--- /dev/null
+++ b/browser/base/content/test/general/browser_e10s_javascript.js
@@ -0,0 +1,11 @@
+const CHROME_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+const CONTENT_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+
+add_task(function*() {
+ let url = "javascript:dosomething()";
+
+ ok(E10SUtils.canLoadURIInProcess(url, CHROME_PROCESS),
+ "Check URL in chrome process.");
+ ok(E10SUtils.canLoadURIInProcess(url, CONTENT_PROCESS),
+ "Check URL in content process.");
+});
diff --git a/browser/base/content/test/general/browser_e10s_switchbrowser.js b/browser/base/content/test/general/browser_e10s_switchbrowser.js
new file mode 100644
index 000000000..e6134f749
--- /dev/null
+++ b/browser/base/content/test/general/browser_e10s_switchbrowser.js
@@ -0,0 +1,261 @@
+requestLongerTimeout(2);
+
+const DUMMY_PATH = "browser/browser/base/content/test/general/dummy_page.html";
+
+const gExpectedHistory = {
+ index: -1,
+ entries: []
+};
+
+function get_remote_history(browser) {
+ function frame_script() {
+ let webNav = docShell.QueryInterface(Components.interfaces.nsIWebNavigation);
+ let sessionHistory = webNav.sessionHistory;
+ let result = {
+ index: sessionHistory.index,
+ entries: []
+ };
+
+ for (let i = 0; i < sessionHistory.count; i++) {
+ let entry = sessionHistory.getEntryAtIndex(i, false);
+ result.entries.push({
+ uri: entry.URI.spec,
+ title: entry.title
+ });
+ }
+
+ sendAsyncMessage("Test:History", result);
+ }
+
+ return new Promise(resolve => {
+ browser.messageManager.addMessageListener("Test:History", function listener({data}) {
+ browser.messageManager.removeMessageListener("Test:History", listener);
+ resolve(data);
+ });
+
+ browser.messageManager.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
+ });
+}
+
+var check_history = Task.async(function*() {
+ let sessionHistory = yield get_remote_history(gBrowser.selectedBrowser);
+
+ let count = sessionHistory.entries.length;
+ is(count, gExpectedHistory.entries.length, "Should have the right number of history entries");
+ is(sessionHistory.index, gExpectedHistory.index, "Should have the right history index");
+
+ for (let i = 0; i < count; i++) {
+ let entry = sessionHistory.entries[i];
+ is(entry.uri, gExpectedHistory.entries[i].uri, "Should have the right URI");
+ is(entry.title, gExpectedHistory.entries[i].title, "Should have the right title");
+ }
+});
+
+function clear_history() {
+ gExpectedHistory.index = -1;
+ gExpectedHistory.entries = [];
+}
+
+// Waits for a load and updates the known history
+var waitForLoad = Task.async(function*(uri) {
+ info("Loading " + uri);
+ // Longwinded but this ensures we don't just shortcut to LoadInNewProcess
+ gBrowser.selectedBrowser.webNavigation.loadURI(uri, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
+
+ yield waitForDocLoadComplete();
+ gExpectedHistory.index++;
+ gExpectedHistory.entries.push({
+ uri: gBrowser.currentURI.spec,
+ title: gBrowser.contentTitle
+ });
+});
+
+// Waits for a load and updates the known history
+var waitForLoadWithFlags = Task.async(function*(uri, flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE) {
+ info("Loading " + uri + " flags = " + flags);
+ gBrowser.selectedBrowser.loadURIWithFlags(uri, flags, null, null, null);
+
+ yield waitForDocLoadComplete();
+ if (!(flags & Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY)) {
+
+ if (flags & Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY) {
+ gExpectedHistory.entries.pop();
+ }
+ else {
+ gExpectedHistory.index++;
+ }
+
+ gExpectedHistory.entries.push({
+ uri: gBrowser.currentURI.spec,
+ title: gBrowser.contentTitle
+ });
+ }
+});
+
+var back = Task.async(function*() {
+ info("Going back");
+ gBrowser.goBack();
+ yield waitForDocLoadComplete();
+ gExpectedHistory.index--;
+});
+
+var forward = Task.async(function*() {
+ info("Going forward");
+ gBrowser.goForward();
+ yield waitForDocLoadComplete();
+ gExpectedHistory.index++;
+});
+
+// Tests that navigating from a page that should be in the remote process and
+// a page that should be in the main process works and retains history
+add_task(function* test_navigation() {
+ let expectedRemote = gMultiProcessBrowser;
+
+ info("1");
+ // Create a tab and load a remote page in it
+ gBrowser.selectedTab = gBrowser.addTab("about:blank", {skipAnimation: true});
+ let {permanentKey} = gBrowser.selectedBrowser;
+ yield waitForLoad("http://example.org/" + DUMMY_PATH);
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+
+ info("2");
+ // Load another page
+ yield waitForLoad("http://example.com/" + DUMMY_PATH);
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("3");
+ // Load a non-remote page
+ yield waitForLoad("about:robots");
+ is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("4");
+ // Load a remote page
+ yield waitForLoad("http://example.org/" + DUMMY_PATH);
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("5");
+ yield back();
+ is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("6");
+ yield back();
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("7");
+ yield forward();
+ is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("8");
+ yield forward();
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("9");
+ yield back();
+ is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("10");
+ // Load a new remote page, this should replace the last history entry
+ gExpectedHistory.entries.splice(gExpectedHistory.entries.length - 1, 1);
+ yield waitForLoad("http://example.com/" + DUMMY_PATH);
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+ yield check_history();
+
+ info("11");
+ gBrowser.removeCurrentTab();
+ clear_history();
+});
+
+// Tests that calling gBrowser.loadURI or browser.loadURI to load a page in a
+// different process updates the browser synchronously
+add_task(function* test_synchronous() {
+ let expectedRemote = gMultiProcessBrowser;
+
+ info("1");
+ // Create a tab and load a remote page in it
+ gBrowser.selectedTab = gBrowser.addTab("about:blank", {skipAnimation: true});
+ let {permanentKey} = gBrowser.selectedBrowser;
+ yield waitForLoad("http://example.org/" + DUMMY_PATH);
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+
+ info("2");
+ // Load another page
+ info("Loading about:robots");
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:robots");
+ is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+
+ yield waitForDocLoadComplete();
+ is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+
+ info("3");
+ // Load the remote page again
+ info("Loading http://example.org/" + DUMMY_PATH);
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "http://example.org/" + DUMMY_PATH);
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+
+ yield waitForDocLoadComplete();
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
+
+ info("4");
+ gBrowser.removeCurrentTab();
+ clear_history();
+});
+
+// Tests that load flags are correctly passed through to the child process with
+// normal loads
+add_task(function* test_loadflags() {
+ let expectedRemote = gMultiProcessBrowser;
+
+ info("1");
+ // Create a tab and load a remote page in it
+ gBrowser.selectedTab = gBrowser.addTab("about:blank", {skipAnimation: true});
+ yield waitForLoadWithFlags("about:robots");
+ is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
+ yield check_history();
+
+ info("2");
+ // Load a page in the remote process with some custom flags
+ yield waitForLoadWithFlags("http://example.com/" + DUMMY_PATH, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY);
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ yield check_history();
+
+ info("3");
+ // Load a non-remote page
+ yield waitForLoadWithFlags("about:robots");
+ is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
+ yield check_history();
+
+ info("4");
+ // Load another remote page
+ yield waitForLoadWithFlags("http://example.org/" + DUMMY_PATH, Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY);
+ is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
+ yield check_history();
+
+ is(gExpectedHistory.entries.length, 2, "Should end with the right number of history entries");
+
+ info("5");
+ gBrowser.removeCurrentTab();
+ clear_history();
+});
diff --git a/browser/base/content/test/general/browser_favicon_change.js b/browser/base/content/test/general/browser_favicon_change.js
new file mode 100644
index 000000000..f6b0a2a42
--- /dev/null
+++ b/browser/base/content/test/general/browser_favicon_change.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/base/content/test/general/file_favicon_change.html"
+
+add_task(function*() {
+ let extraTab = gBrowser.selectedTab = gBrowser.addTab();
+ extraTab.linkedBrowser.loadURI(TEST_URL);
+ let tabLoaded = BrowserTestUtils.browserLoaded(extraTab.linkedBrowser);
+ let expectedFavicon = "http://example.org/one-icon";
+ let haveChanged = new Promise.defer();
+ let observer = new MutationObserver(function(mutations) {
+ for (let mut of mutations) {
+ if (mut.attributeName != "image") {
+ continue;
+ }
+ let imageVal = extraTab.getAttribute("image").replace(/#.*$/, "");
+ if (!imageVal) {
+ // The value gets removed because it doesn't load.
+ continue;
+ }
+ is(imageVal, expectedFavicon, "Favicon image should correspond to expected image.");
+ haveChanged.resolve();
+ }
+ });
+ observer.observe(extraTab, {attributes: true});
+ yield tabLoaded;
+ yield haveChanged.promise;
+ haveChanged = new Promise.defer();
+ expectedFavicon = "http://example.org/other-icon";
+ ContentTask.spawn(extraTab.linkedBrowser, null, function() {
+ let ev = new content.CustomEvent("PleaseChangeFavicon", {});
+ content.dispatchEvent(ev);
+ });
+ yield haveChanged.promise;
+ observer.disconnect();
+ gBrowser.removeTab(extraTab);
+});
+
diff --git a/browser/base/content/test/general/browser_favicon_change_not_in_document.js b/browser/base/content/test/general/browser_favicon_change_not_in_document.js
new file mode 100644
index 000000000..d14a1da32
--- /dev/null
+++ b/browser/base/content/test/general/browser_favicon_change_not_in_document.js
@@ -0,0 +1,34 @@
+"use strict";
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/base/content/test/general/file_favicon_change_not_in_document.html"
+
+add_task(function*() {
+ let extraTab = gBrowser.selectedTab = gBrowser.addTab();
+ let tabLoaded = promiseTabLoaded(extraTab);
+ extraTab.linkedBrowser.loadURI(TEST_URL);
+ let expectedFavicon = "http://example.org/one-icon";
+ let haveChanged = new Promise.defer();
+ let observer = new MutationObserver(function(mutations) {
+ for (let mut of mutations) {
+ if (mut.attributeName != "image") {
+ continue;
+ }
+ let imageVal = extraTab.getAttribute("image").replace(/#.*$/, "");
+ if (!imageVal) {
+ // The value gets removed because it doesn't load.
+ continue;
+ }
+ is(imageVal, expectedFavicon, "Favicon image should correspond to expected image.");
+ haveChanged.resolve();
+ }
+ });
+ observer.observe(extraTab, {attributes: true});
+ yield tabLoaded;
+ expectedFavicon = "http://example.org/yet-another-icon";
+ haveChanged = new Promise.defer();
+ yield haveChanged.promise;
+ observer.disconnect();
+ gBrowser.removeTab(extraTab);
+});
+
+
diff --git a/browser/base/content/test/general/browser_feed_discovery.js b/browser/base/content/test/general/browser_feed_discovery.js
new file mode 100644
index 000000000..73dcef755
--- /dev/null
+++ b/browser/base/content/test/general/browser_feed_discovery.js
@@ -0,0 +1,33 @@
+const URL = "http://mochi.test:8888/browser/browser/base/content/test/general/feed_discovery.html"
+
+/** Test for Bug 377611 **/
+
+add_task(function* () {
+ // Open a new tab.
+ gBrowser.selectedTab = gBrowser.addTab(URL);
+ registerCleanupFunction(() => gBrowser.removeCurrentTab());
+
+ let browser = gBrowser.selectedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ let discovered = browser.feeds;
+ ok(discovered.length > 0, "some feeds should be discovered");
+
+ let feeds = {};
+ for (let aFeed of discovered) {
+ feeds[aFeed.href] = true;
+ }
+
+ yield ContentTask.spawn(browser, feeds, function* (contentFeeds) {
+ for (let aLink of content.document.getElementsByTagName("link")) {
+ // ignore real stylesheets, and anything without an href property
+ if (aLink.type != "text/css" && aLink.href) {
+ if (/bogus/i.test(aLink.title)) {
+ ok(!contentFeeds[aLink.href], "don't discover " + aLink.href);
+ } else {
+ ok(contentFeeds[aLink.href], "should discover " + aLink.href);
+ }
+ }
+ }
+ });
+})
diff --git a/browser/base/content/test/general/browser_findbarClose.js b/browser/base/content/test/general/browser_findbarClose.js
new file mode 100644
index 000000000..53503073c
--- /dev/null
+++ b/browser/base/content/test/general/browser_findbarClose.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests find bar auto-close behavior
+
+var newTab;
+
+add_task(function* findbar_test() {
+ waitForExplicitFinish();
+ newTab = gBrowser.addTab("about:blank");
+
+ let promise = ContentTask.spawn(newTab.linkedBrowser, null, function* () {
+ yield ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", false);
+ });
+ newTab.linkedBrowser.loadURI("http://example.com/browser/" +
+ "browser/base/content/test/general/test_bug628179.html");
+ yield promise;
+
+ gFindBar.open();
+
+ yield new ContentTask.spawn(newTab.linkedBrowser, null, function* () {
+ let iframe = content.document.getElementById("iframe");
+ let awaitLoad = ContentTaskUtils.waitForEvent(iframe, "load", false);
+ iframe.src = "http://example.org/";
+ yield awaitLoad;
+ });
+
+ ok(!gFindBar.hidden, "the Find bar isn't hidden after the location of a " +
+ "subdocument changes");
+
+ gFindBar.close();
+ gBrowser.removeTab(newTab);
+ finish();
+});
+
diff --git a/browser/base/content/test/general/browser_focusonkeydown.js b/browser/base/content/test/general/browser_focusonkeydown.js
new file mode 100644
index 000000000..5b3337203
--- /dev/null
+++ b/browser/base/content/test/general/browser_focusonkeydown.js
@@ -0,0 +1,26 @@
+add_task(function *()
+{
+ let keyUps = 0;
+
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "data:text/html,<body>");
+
+ gURLBar.focus();
+
+ window.addEventListener("keyup", function countKeyUps(event) {
+ window.removeEventListener("keyup", countKeyUps, true);
+ if (event.originalTarget == gURLBar.inputField) {
+ keyUps++;
+ }
+ }, true);
+
+ gURLBar.addEventListener("keydown", function redirectFocus(event) {
+ gURLBar.removeEventListener("keydown", redirectFocus, true);
+ gBrowser.selectedBrowser.focus();
+ }, true);
+
+ EventUtils.synthesizeKey("v", { });
+
+ is(keyUps, 1, "Key up fired at url bar");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_fullscreen-window-open.js b/browser/base/content/test/general/browser_fullscreen-window-open.js
new file mode 100644
index 000000000..2624b754a
--- /dev/null
+++ b/browser/base/content/test/general/browser_fullscreen-window-open.js
@@ -0,0 +1,347 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+const PREF_DISABLE_OPEN_NEW_WINDOW = "browser.link.open_newwindow.disabled_in_fullscreen";
+const isOSX = (Services.appinfo.OS === "Darwin");
+
+const TEST_FILE = "file_fullscreen-window-open.html";
+const gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/",
+ "http://127.0.0.1:8888/");
+
+function test () {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, true);
+
+ let newTab = gBrowser.addTab(gHttpTestRoot + TEST_FILE);
+ gBrowser.selectedTab = newTab;
+
+ whenTabLoaded(newTab, function () {
+ // Enter browser fullscreen mode.
+ BrowserFullScreen();
+
+ runNextTest();
+ });
+}
+
+registerCleanupFunction(function() {
+ // Exit browser fullscreen mode.
+ BrowserFullScreen();
+
+ gBrowser.removeCurrentTab();
+
+ Services.prefs.clearUserPref(PREF_DISABLE_OPEN_NEW_WINDOW);
+});
+
+var gTests = [
+ test_open,
+ test_open_with_size,
+ test_open_with_pos,
+ test_open_with_outerSize,
+ test_open_with_innerSize,
+ test_open_with_dialog,
+ test_open_when_open_new_window_by_pref,
+ test_open_with_pref_to_disable_in_fullscreen,
+ test_open_from_chrome,
+];
+
+function runNextTest () {
+ let testCase = gTests.shift();
+ if (testCase) {
+ executeSoon(testCase);
+ }
+ else {
+ finish();
+ }
+}
+
+
+// Test for window.open() with no feature.
+function test_open() {
+ waitForTabOpen({
+ message: {
+ title: "test_open",
+ param: "",
+ },
+ finalizeFn: function () {},
+ });
+}
+
+// Test for window.open() with width/height.
+function test_open_with_size() {
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_size",
+ param: "width=400,height=400",
+ },
+ finalizeFn: function () {},
+ });
+}
+
+// Test for window.open() with top/left.
+function test_open_with_pos() {
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_pos",
+ param: "top=200,left=200",
+ },
+ finalizeFn: function () {},
+ });
+}
+
+// Test for window.open() with outerWidth/Height.
+function test_open_with_outerSize() {
+ let [outerWidth, outerHeight] = [window.outerWidth, window.outerHeight];
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_outerSize",
+ param: "outerWidth=200,outerHeight=200",
+ },
+ successFn: function () {
+ is(window.outerWidth, outerWidth, "Don't change window.outerWidth.");
+ is(window.outerHeight, outerHeight, "Don't change window.outerHeight.");
+ },
+ finalizeFn: function () {},
+ });
+}
+
+// Test for window.open() with innerWidth/Height.
+function test_open_with_innerSize() {
+ let [innerWidth, innerHeight] = [window.innerWidth, window.innerHeight];
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_innerSize",
+ param: "innerWidth=200,innerHeight=200",
+ },
+ successFn: function () {
+ is(window.innerWidth, innerWidth, "Don't change window.innerWidth.");
+ is(window.innerHeight, innerHeight, "Don't change window.innerHeight.");
+ },
+ finalizeFn: function () {},
+ });
+}
+
+// Test for window.open() with dialog.
+function test_open_with_dialog() {
+ waitForTabOpen({
+ message: {
+ title: "test_open_with_dialog",
+ param: "dialog=yes",
+ },
+ finalizeFn: function () {},
+ });
+}
+
+// Test for window.open()
+// when "browser.link.open_newwindow" is nsIBrowserDOMWindow.OPEN_NEWWINDOW
+function test_open_when_open_new_window_by_pref() {
+ const PREF_NAME = "browser.link.open_newwindow";
+ Services.prefs.setIntPref(PREF_NAME, Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW);
+ is(Services.prefs.getIntPref(PREF_NAME), Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW,
+ PREF_NAME + " is nsIBrowserDOMWindow.OPEN_NEWWINDOW at this time");
+
+ waitForTabOpen({
+ message: {
+ title: "test_open_when_open_new_window_by_pref",
+ param: "width=400,height=400",
+ },
+ finalizeFn: function () {
+ Services.prefs.clearUserPref(PREF_NAME);
+ },
+ });
+}
+
+// Test for the pref, "browser.link.open_newwindow.disabled_in_fullscreen"
+function test_open_with_pref_to_disable_in_fullscreen() {
+ Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, false);
+
+ waitForWindowOpen({
+ message: {
+ title: "test_open_with_pref_disabled_in_fullscreen",
+ param: "width=400,height=400",
+ },
+ finalizeFn: function () {
+ Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, true);
+ },
+ });
+}
+
+
+// Test for window.open() called from chrome context.
+function test_open_from_chrome() {
+ waitForWindowOpenFromChrome({
+ message: {
+ title: "test_open_from_chrome",
+ param: "",
+ },
+ finalizeFn: function () {}
+ });
+}
+
+function waitForTabOpen(aOptions) {
+ let message = aOptions.message;
+
+ if (!message.title) {
+ ok(false, "Can't get message.title.");
+ aOptions.finalizeFn();
+ runNextTest();
+ return;
+ }
+
+ info("Running test: " + message.title);
+
+ let onTabOpen = function onTabOpen(aEvent) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true);
+
+ let tab = aEvent.target;
+ whenTabLoaded(tab, function () {
+ is(tab.linkedBrowser.contentTitle, message.title,
+ "Opened Tab is expected: " + message.title);
+
+ if (aOptions.successFn) {
+ aOptions.successFn();
+ }
+
+ gBrowser.removeTab(tab);
+ finalize();
+ });
+ }
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true);
+
+ let finalize = function () {
+ aOptions.finalizeFn();
+ info("Finished: " + message.title);
+ runNextTest();
+ };
+
+ const URI = "data:text/html;charset=utf-8,<!DOCTYPE html><html><head><title>"+
+ message.title +
+ "<%2Ftitle><%2Fhead><body><%2Fbody><%2Fhtml>";
+
+ executeWindowOpenInContent({
+ uri: URI,
+ title: message.title,
+ option: message.param,
+ });
+}
+
+
+function waitForWindowOpen(aOptions) {
+ let message = aOptions.message;
+ let url = aOptions.url || "about:blank";
+
+ if (!message.title) {
+ ok(false, "Can't get message.title");
+ aOptions.finalizeFn();
+ runNextTest();
+ return;
+ }
+
+ info("Running test: " + message.title);
+
+ let onFinalize = function () {
+ aOptions.finalizeFn();
+
+ info("Finished: " + message.title);
+ runNextTest();
+ };
+
+ let listener = new WindowListener(message.title, getBrowserURL(), {
+ onSuccess: aOptions.successFn,
+ onFinalize: onFinalize,
+ });
+ Services.wm.addListener(listener);
+
+ executeWindowOpenInContent({
+ uri: url,
+ title: message.title,
+ option: message.param,
+ });
+}
+
+function executeWindowOpenInContent(aParam) {
+ ContentTask.spawn(gBrowser.selectedBrowser, JSON.stringify(aParam), function* (dataTestParam) {
+ let testElm = content.document.getElementById("test");
+ testElm.setAttribute("data-test-param", dataTestParam);
+ testElm.click();
+ });
+}
+
+function waitForWindowOpenFromChrome(aOptions) {
+ let message = aOptions.message;
+ let url = aOptions.url || "about:blank";
+
+ if (!message.title) {
+ ok(false, "Can't get message.title");
+ aOptions.finalizeFn();
+ runNextTest();
+ return;
+ }
+
+ info("Running test: " + message.title);
+
+ let onFinalize = function () {
+ aOptions.finalizeFn();
+
+ info("Finished: " + message.title);
+ runNextTest();
+ };
+
+ let listener = new WindowListener(message.title, getBrowserURL(), {
+ onSuccess: aOptions.successFn,
+ onFinalize: onFinalize,
+ });
+ Services.wm.addListener(listener);
+
+ window.open(url, message.title, message.option);
+}
+
+function WindowListener(aTitle, aUrl, aCallBackObj) {
+ this.test_title = aTitle;
+ this.test_url = aUrl;
+ this.callback_onSuccess = aCallBackObj.onSuccess;
+ this.callBack_onFinalize = aCallBackObj.onFinalize;
+}
+WindowListener.prototype = {
+
+ test_title: null,
+ test_url: null,
+ callback_onSuccess: null,
+ callBack_onFinalize: null,
+
+ onOpenWindow: function(aXULWindow) {
+ Services.wm.removeListener(this);
+
+ let domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ let onLoad = aEvent => {
+ is(domwindow.document.location.href, this.test_url,
+ "Opened Window is expected: "+ this.test_title);
+ if (this.callback_onSuccess) {
+ this.callback_onSuccess();
+ }
+
+ domwindow.removeEventListener("load", onLoad, true);
+
+ // wait for trasition to fullscreen on OSX Lion later
+ if (isOSX) {
+ setTimeout(function() {
+ domwindow.close();
+ executeSoon(this.callBack_onFinalize);
+ }.bind(this), 3000);
+ }
+ else {
+ domwindow.close();
+ executeSoon(this.callBack_onFinalize);
+ }
+ };
+ domwindow.addEventListener("load", onLoad, true);
+ },
+ onCloseWindow: function(aXULWindow) {},
+ onWindowTitleChange: function(aXULWindow, aNewTitle) {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowMediatorListener,
+ Ci.nsISupports]),
+};
diff --git a/browser/base/content/test/general/browser_fxa_migrate.js b/browser/base/content/test/general/browser_fxa_migrate.js
new file mode 100644
index 000000000..2faf9fb10
--- /dev/null
+++ b/browser/base/content/test/general/browser_fxa_migrate.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const STATE_CHANGED_TOPIC = "fxa-migration:state-changed";
+const NOTIFICATION_TITLE = "fxa-migration";
+
+var imports = {};
+Cu.import("resource://services-sync/FxaMigrator.jsm", imports);
+
+add_task(function* test() {
+ // Fake the state where we saw an EOL notification.
+ Services.obs.notifyObservers(null, STATE_CHANGED_TOPIC, null);
+
+ let notificationBox = document.getElementById("global-notificationbox");
+ Assert.ok(notificationBox.allNotifications.some(n => {
+ return n.getAttribute("value") == NOTIFICATION_TITLE;
+ }), "Disconnect notification should be present");
+});
diff --git a/browser/base/content/test/general/browser_fxa_oauth.html b/browser/base/content/test/general/browser_fxa_oauth.html
new file mode 100644
index 000000000..b31e7ceb4
--- /dev/null
+++ b/browser/base/content/test/general/browser_fxa_oauth.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>fxa_oauth_test</title>
+</head>
+<body>
+<script>
+ window.onload = function() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ // Note: This intentionally sends an object instead of a string, to ensure both work
+ // (see browser_fxa_oauth_with_keys.html for the other test)
+ detail: {
+ id: "oauth_client_id",
+ message: {
+ command: "oauth_complete",
+ data: {
+ state: "state",
+ code: "code1",
+ closeWindow: "signin",
+ },
+ },
+ },
+ });
+
+ window.dispatchEvent(event);
+ };
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/browser_fxa_oauth.js b/browser/base/content/test/general/browser_fxa_oauth.js
new file mode 100644
index 000000000..1f688bfa8
--- /dev/null
+++ b/browser/base/content/test/general/browser_fxa_oauth.js
@@ -0,0 +1,327 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthClient",
+ "resource://gre/modules/FxAccountsOAuthClient.jsm");
+
+const HTTP_PATH = "http://example.com";
+const HTTP_ENDPOINT = "/browser/browser/base/content/test/general/browser_fxa_oauth.html";
+const HTTP_ENDPOINT_WITH_KEYS = "/browser/browser/base/content/test/general/browser_fxa_oauth_with_keys.html";
+
+var gTests = [
+ {
+ desc: "FxA OAuth - should open a new tab, complete OAuth flow",
+ run: function () {
+ return new Promise(function(resolve, reject) {
+ let tabOpened = false;
+ let properURL = "http://example.com/browser/browser/base/content/test/general/browser_fxa_oauth.html";
+ let queryStrings = [
+ "action=signin",
+ "client_id=client_id",
+ "scope=",
+ "state=state",
+ "webChannelId=oauth_client_id",
+ ];
+ queryStrings.sort();
+
+ waitForTab(function (tab) {
+ Assert.ok("Tab successfully opened");
+ Assert.ok(gBrowser.currentURI.spec.split("?")[0], properURL, "Check URL without params");
+ let actualURL = new URL(gBrowser.currentURI.spec);
+ let actualQueryStrings = actualURL.search.substring(1).split("&");
+ actualQueryStrings.sort();
+ Assert.equal(actualQueryStrings.length, queryStrings.length, "Check number of params");
+
+ for (let i = 0; i < queryStrings.length; i++) {
+ Assert.equal(actualQueryStrings[i], queryStrings[i], "Check parameter " + i);
+ }
+
+ tabOpened = true;
+ });
+
+ let client = new FxAccountsOAuthClient({
+ parameters: {
+ state: "state",
+ client_id: "client_id",
+ oauth_uri: HTTP_PATH,
+ content_uri: HTTP_PATH,
+ },
+ authorizationEndpoint: HTTP_ENDPOINT
+ });
+
+ client.onComplete = function(tokenData) {
+ Assert.ok(tabOpened);
+ Assert.equal(tokenData.code, "code1");
+ Assert.equal(tokenData.state, "state");
+ resolve();
+ };
+
+ client.onError = reject;
+
+ client.launchWebFlow();
+ });
+ }
+ },
+ {
+ desc: "FxA OAuth - should open a new tab, complete OAuth flow when forcing auth",
+ run: function () {
+ return new Promise(function(resolve, reject) {
+ let tabOpened = false;
+ let properURL = "http://example.com/browser/browser/base/content/test/general/browser_fxa_oauth.html";
+ let queryStrings = [
+ "action=force_auth",
+ "client_id=client_id",
+ "scope=",
+ "state=state",
+ "webChannelId=oauth_client_id",
+ "email=test%40invalid.com",
+ ];
+ queryStrings.sort();
+
+ waitForTab(function (tab) {
+ Assert.ok("Tab successfully opened");
+ Assert.ok(gBrowser.currentURI.spec.split("?")[0], properURL, "Check URL without params");
+
+ let actualURL = new URL(gBrowser.currentURI.spec);
+ let actualQueryStrings = actualURL.search.substring(1).split("&");
+ actualQueryStrings.sort();
+ Assert.equal(actualQueryStrings.length, queryStrings.length, "Check number of params");
+
+ for (let i = 0; i < queryStrings.length; i++) {
+ Assert.equal(actualQueryStrings[i], queryStrings[i], "Check parameter " + i);
+ }
+
+ tabOpened = true;
+ });
+
+ let client = new FxAccountsOAuthClient({
+ parameters: {
+ state: "state",
+ client_id: "client_id",
+ oauth_uri: HTTP_PATH,
+ content_uri: HTTP_PATH,
+ action: "force_auth",
+ email: "test@invalid.com"
+ },
+ authorizationEndpoint: HTTP_ENDPOINT
+ });
+
+ client.onComplete = function(tokenData) {
+ Assert.ok(tabOpened);
+ Assert.equal(tokenData.code, "code1");
+ Assert.equal(tokenData.state, "state");
+ resolve();
+ };
+
+ client.onError = reject;
+
+ client.launchWebFlow();
+ });
+ }
+ },
+ {
+ desc: "FxA OAuth - should receive an error when there's a state mismatch",
+ run: function () {
+ return new Promise(function(resolve, reject) {
+ let tabOpened = false;
+
+ waitForTab(function (tab) {
+ Assert.ok("Tab successfully opened");
+
+ // It should have passed in the expected non-matching state value.
+ let queryString = gBrowser.currentURI.spec.split("?")[1];
+ Assert.ok(queryString.indexOf('state=different-state') >= 0);
+
+ tabOpened = true;
+ });
+
+ let client = new FxAccountsOAuthClient({
+ parameters: {
+ state: "different-state",
+ client_id: "client_id",
+ oauth_uri: HTTP_PATH,
+ content_uri: HTTP_PATH,
+ },
+ authorizationEndpoint: HTTP_ENDPOINT
+ });
+
+ client.onComplete = reject;
+
+ client.onError = function(err) {
+ Assert.ok(tabOpened);
+ Assert.equal(err.message, "OAuth flow failed. State doesn't match");
+ resolve();
+ };
+
+ client.launchWebFlow();
+ });
+ }
+ },
+ {
+ desc: "FxA OAuth - should be able to request keys during OAuth flow",
+ run: function () {
+ return new Promise(function(resolve, reject) {
+ let tabOpened = false;
+
+ waitForTab(function (tab) {
+ Assert.ok("Tab successfully opened");
+
+ // It should have asked for keys.
+ let queryString = gBrowser.currentURI.spec.split('?')[1];
+ Assert.ok(queryString.indexOf('keys=true') >= 0);
+
+ tabOpened = true;
+ });
+
+ let client = new FxAccountsOAuthClient({
+ parameters: {
+ state: "state",
+ client_id: "client_id",
+ oauth_uri: HTTP_PATH,
+ content_uri: HTTP_PATH,
+ keys: true,
+ },
+ authorizationEndpoint: HTTP_ENDPOINT_WITH_KEYS
+ });
+
+ client.onComplete = function(tokenData, keys) {
+ Assert.ok(tabOpened);
+ Assert.equal(tokenData.code, "code1");
+ Assert.equal(tokenData.state, "state");
+ Assert.deepEqual(keys.kAr, {k: "kAr"});
+ Assert.deepEqual(keys.kBr, {k: "kBr"});
+ resolve();
+ };
+
+ client.onError = reject;
+
+ client.launchWebFlow();
+ });
+ }
+ },
+ {
+ desc: "FxA OAuth - should not receive keys if not explicitly requested",
+ run: function () {
+ return new Promise(function(resolve, reject) {
+ let tabOpened = false;
+
+ waitForTab(function (tab) {
+ Assert.ok("Tab successfully opened");
+
+ // It should not have asked for keys.
+ let queryString = gBrowser.currentURI.spec.split('?')[1];
+ Assert.ok(queryString.indexOf('keys=true') == -1);
+
+ tabOpened = true;
+ });
+
+ let client = new FxAccountsOAuthClient({
+ parameters: {
+ state: "state",
+ client_id: "client_id",
+ oauth_uri: HTTP_PATH,
+ content_uri: HTTP_PATH
+ },
+ // This endpoint will cause the completion message to contain keys.
+ authorizationEndpoint: HTTP_ENDPOINT_WITH_KEYS
+ });
+
+ client.onComplete = function(tokenData, keys) {
+ Assert.ok(tabOpened);
+ Assert.equal(tokenData.code, "code1");
+ Assert.equal(tokenData.state, "state");
+ Assert.strictEqual(keys, undefined);
+ resolve();
+ };
+
+ client.onError = reject;
+
+ client.launchWebFlow();
+ });
+ }
+ },
+ {
+ desc: "FxA OAuth - should receive an error if keys could not be obtained",
+ run: function () {
+ return new Promise(function(resolve, reject) {
+ let tabOpened = false;
+
+ waitForTab(function (tab) {
+ Assert.ok("Tab successfully opened");
+
+ // It should have asked for keys.
+ let queryString = gBrowser.currentURI.spec.split('?')[1];
+ Assert.ok(queryString.indexOf('keys=true') >= 0);
+
+ tabOpened = true;
+ });
+
+ let client = new FxAccountsOAuthClient({
+ parameters: {
+ state: "state",
+ client_id: "client_id",
+ oauth_uri: HTTP_PATH,
+ content_uri: HTTP_PATH,
+ keys: true,
+ },
+ // This endpoint will cause the completion message not to contain keys.
+ authorizationEndpoint: HTTP_ENDPOINT
+ });
+
+ client.onComplete = reject;
+
+ client.onError = function(err) {
+ Assert.ok(tabOpened);
+ Assert.equal(err.message, "OAuth flow failed. Keys were not returned");
+ resolve();
+ };
+
+ client.launchWebFlow();
+ });
+ }
+ }
+]; // gTests
+
+function waitForTab(aCallback) {
+ let container = gBrowser.tabContainer;
+ container.addEventListener("TabOpen", function tabOpener(event) {
+ container.removeEventListener("TabOpen", tabOpener, false);
+ gBrowser.addEventListener("load", function listener() {
+ gBrowser.removeEventListener("load", listener, true);
+ let tab = event.target;
+ aCallback(tab);
+ }, true);
+ }, false);
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+ let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+ let newWhitelist = origWhitelist + " http://example.com";
+ Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+ try {
+ for (let testCase of gTests) {
+ info("Running: " + testCase.desc);
+ yield testCase.run();
+ }
+ } finally {
+ Services.prefs.clearUserPref(webchannelWhitelistPref);
+ }
+ }).then(finish, ex => {
+ Assert.ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+}
diff --git a/browser/base/content/test/general/browser_fxa_oauth_with_keys.html b/browser/base/content/test/general/browser_fxa_oauth_with_keys.html
new file mode 100644
index 000000000..2c28f7088
--- /dev/null
+++ b/browser/base/content/test/general/browser_fxa_oauth_with_keys.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>fxa_oauth_test</title>
+</head>
+<body>
+<script>
+ window.onload = function() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ // Note: This intentionally sends a string instead of an object, to ensure both work
+ // (see browser_fxa_oauth.html for the other test)
+ detail: JSON.stringify({
+ id: "oauth_client_id",
+ message: {
+ command: "oauth_complete",
+ data: {
+ state: "state",
+ code: "code1",
+ closeWindow: "signin",
+ // Keys normally contain more information, but this is enough
+ // to keep Loop's tests happy.
+ keys: { kAr: { k: 'kAr' }, kBr: { k: 'kBr' }},
+ },
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ };
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/browser_fxa_web_channel.html b/browser/base/content/test/general/browser_fxa_web_channel.html
new file mode 100644
index 000000000..be5631ff1
--- /dev/null
+++ b/browser/base/content/test/general/browser_fxa_web_channel.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>fxa_web_channel_test</title>
+</head>
+<body>
+<script>
+ var webChannelId = "account_updates_test";
+
+ window.onload = function() {
+ var testName = window.location.search.replace(/^\?/, "");
+
+ switch (testName) {
+ case "profile_change":
+ test_profile_change();
+ break;
+ case "login":
+ test_login();
+ break;
+ case "can_link_account":
+ test_can_link_account();
+ break;
+ case "logout":
+ test_logout();
+ break;
+ case "delete":
+ test_delete();
+ break;
+ }
+ };
+
+ function test_profile_change() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "profile:change",
+ data: {
+ uid: "abc123",
+ },
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_login() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:login",
+ data: {
+ authAt: Date.now(),
+ email: "testuser@testuser.com",
+ keyFetchToken: 'key_fetch_token',
+ sessionToken: 'session_token',
+ uid: 'uid',
+ unwrapBKey: 'unwrap_b_key',
+ verified: true,
+ },
+ messageId: 1,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_can_link_account() {
+ window.addEventListener("WebChannelMessageToContent", function (e) {
+ // echo any responses from the browser back to the tests on the
+ // fxaccounts_webchannel_response_echo WebChannel. The tests are
+ // listening for events and do the appropriate checks.
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: 'fxaccounts_webchannel_response_echo',
+ message: e.detail.message,
+ })
+ });
+
+ window.dispatchEvent(event);
+ }, true);
+
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:can_link_account",
+ data: {
+ email: "testuser@testuser.com",
+ },
+ messageId: 2,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_logout() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:logout",
+ data: {
+ uid: 'uid'
+ },
+ messageId: 3,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_delete() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: webChannelId,
+ message: {
+ command: "fxaccounts:delete",
+ data: {
+ uid: 'uid'
+ },
+ messageId: 4,
+ },
+ }),
+ });
+
+ window.dispatchEvent(event);
+ }
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/browser_fxa_web_channel.js b/browser/base/content/test/general/browser_fxa_web_channel.js
new file mode 100644
index 000000000..eb0167ffb
--- /dev/null
+++ b/browser/base/content/test/general/browser_fxa_web_channel.js
@@ -0,0 +1,210 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
+ return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
+ "resource://gre/modules/WebChannel.jsm");
+
+// FxAccountsWebChannel isn't explicitly exported by FxAccountsWebChannel.jsm
+// but we can get it here via a backstage pass.
+var {FxAccountsWebChannel} = Components.utils.import("resource://gre/modules/FxAccountsWebChannel.jsm", {});
+
+const TEST_HTTP_PATH = "http://example.com";
+const TEST_BASE_URL = TEST_HTTP_PATH + "/browser/browser/base/content/test/general/browser_fxa_web_channel.html";
+const TEST_CHANNEL_ID = "account_updates_test";
+
+var gTests = [
+ {
+ desc: "FxA Web Channel - should receive message about profile changes",
+ run: function* () {
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ });
+ let promiseObserver = new Promise((resolve, reject) => {
+ makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
+ Assert.equal(data, "abc123");
+ client.tearDown();
+ resolve();
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: TEST_BASE_URL + "?profile_change"
+ }, function* () {
+ yield promiseObserver;
+ });
+ }
+ },
+ {
+ desc: "fxa web channel - login messages should notify the fxAccounts object",
+ run: function* () {
+
+ let promiseLogin = new Promise((resolve, reject) => {
+ let login = (accountData) => {
+ Assert.equal(typeof accountData.authAt, 'number');
+ Assert.equal(accountData.email, 'testuser@testuser.com');
+ Assert.equal(accountData.keyFetchToken, 'key_fetch_token');
+ Assert.equal(accountData.sessionToken, 'session_token');
+ Assert.equal(accountData.uid, 'uid');
+ Assert.equal(accountData.unwrapBKey, 'unwrap_b_key');
+ Assert.equal(accountData.verified, true);
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ login: login
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: TEST_BASE_URL + "?login"
+ }, function* () {
+ yield promiseLogin;
+ });
+ }
+ },
+ {
+ desc: "fxa web channel - can_link_account messages should respond",
+ run: function* () {
+ let properUrl = TEST_BASE_URL + "?can_link_account";
+
+ let promiseEcho = new Promise((resolve, reject) => {
+
+ let webChannelOrigin = Services.io.newURI(properUrl, null, null);
+ // responses sent to content are echoed back over the
+ // `fxaccounts_webchannel_response_echo` channel. Ensure the
+ // fxaccounts:can_link_account message is responded to.
+ let echoWebChannel = new WebChannel('fxaccounts_webchannel_response_echo', webChannelOrigin);
+ echoWebChannel.listen((webChannelId, message, target) => {
+ Assert.equal(message.command, 'fxaccounts:can_link_account');
+ Assert.equal(message.messageId, 2);
+ Assert.equal(message.data.ok, true);
+
+ client.tearDown();
+ echoWebChannel.stopListening();
+
+ resolve();
+ });
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ shouldAllowRelink(acctName) {
+ return acctName === 'testuser@testuser.com';
+ }
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: properUrl
+ }, function* () {
+ yield promiseEcho;
+ });
+ }
+ },
+ {
+ desc: "fxa web channel - logout messages should notify the fxAccounts object",
+ run: function* () {
+ let promiseLogout = new Promise((resolve, reject) => {
+ let logout = (uid) => {
+ Assert.equal(uid, 'uid');
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ logout: logout
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: TEST_BASE_URL + "?logout"
+ }, function* () {
+ yield promiseLogout;
+ });
+ }
+ },
+ {
+ desc: "fxa web channel - delete messages should notify the fxAccounts object",
+ run: function* () {
+ let promiseDelete = new Promise((resolve, reject) => {
+ let logout = (uid) => {
+ Assert.equal(uid, 'uid');
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ logout: logout
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: TEST_BASE_URL + "?delete"
+ }, function* () {
+ yield promiseDelete;
+ });
+ }
+ }
+]; // gTests
+
+function makeObserver(aObserveTopic, aObserveFunc) {
+ let callback = function (aSubject, aTopic, aData) {
+ if (aTopic == aObserveTopic) {
+ removeMe();
+ aObserveFunc(aSubject, aTopic, aData);
+ }
+ };
+
+ function removeMe() {
+ Services.obs.removeObserver(callback, aObserveTopic);
+ }
+
+ Services.obs.addObserver(callback, aObserveTopic, false);
+ return removeMe;
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ for (let testCase of gTests) {
+ info("Running: " + testCase.desc);
+ yield testCase.run();
+ }
+ }).then(finish, ex => {
+ Assert.ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+}
diff --git a/browser/base/content/test/general/browser_fxaccounts.js b/browser/base/content/test/general/browser_fxaccounts.js
new file mode 100644
index 000000000..0f68286dc
--- /dev/null
+++ b/browser/base/content/test/general/browser_fxaccounts.js
@@ -0,0 +1,261 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
+var {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+var {fxAccounts} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
+var FxAccountsCommon = {};
+Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
+
+const TEST_ROOT = "http://example.com/browser/browser/base/content/test/general/";
+
+// instrument gFxAccounts to send observer notifications when it's done
+// what it does.
+(function() {
+ let unstubs = {}; // The original functions we stub out.
+
+ // The stub functions.
+ let stubs = {
+ updateAppMenuItem: function() {
+ return unstubs['updateAppMenuItem'].call(gFxAccounts).then(() => {
+ Services.obs.notifyObservers(null, "test:browser_fxaccounts:updateAppMenuItem", null);
+ });
+ },
+ // Opening preferences is trickier than it should be as leaks are reported
+ // due to the promises it fires off at load time and there's no clear way to
+ // know when they are done.
+ // So just ensure openPreferences is called rather than whether it opens.
+ openPreferences: function() {
+ Services.obs.notifyObservers(null, "test:browser_fxaccounts:openPreferences", null);
+ }
+ };
+
+ for (let name in stubs) {
+ unstubs[name] = gFxAccounts[name];
+ gFxAccounts[name] = stubs[name];
+ }
+ // and undo our damage at the end.
+ registerCleanupFunction(() => {
+ for (let name in unstubs) {
+ gFxAccounts[name] = unstubs[name];
+ }
+ stubs = unstubs = null;
+ });
+})();
+
+// Other setup/cleanup
+var newTab;
+
+Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri",
+ TEST_ROOT + "accounts_testRemoteCommands.html");
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
+ Services.prefs.clearUserPref("identity.fxaccounts.remote.profile.uri");
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function* initialize() {
+ // Set a new tab with something other than about:blank, so it doesn't get reused.
+ // We must wait for it to load or the promiseTabOpen() call in the next test
+ // gets confused.
+ newTab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
+ yield promiseTabLoaded(newTab);
+});
+
+// The elements we care about.
+var panelUILabel = document.getElementById("PanelUI-fxa-label");
+var panelUIStatus = document.getElementById("PanelUI-fxa-status");
+var panelUIFooter = document.getElementById("PanelUI-footer-fxa");
+
+// The tests
+add_task(function* test_nouser() {
+ let user = yield fxAccounts.getSignedInUser();
+ Assert.strictEqual(user, null, "start with no user signed in");
+ let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateAppMenuItem");
+ Services.obs.notifyObservers(null, this.FxAccountsCommon.ONLOGOUT_NOTIFICATION, null);
+ yield promiseUpdateDone;
+
+ // Check the world - the FxA footer area is visible as it is offering a signin.
+ Assert.ok(isFooterVisible())
+
+ Assert.equal(panelUILabel.getAttribute("label"), panelUIStatus.getAttribute("defaultlabel"));
+ Assert.equal(panelUIStatus.getAttribute("tooltiptext"), panelUIStatus.getAttribute("signedinTooltiptext"));
+ Assert.ok(!panelUIFooter.hasAttribute("fxastatus"), "no fxsstatus when signed out");
+ Assert.ok(!panelUIFooter.hasAttribute("fxaprofileimage"), "no fxaprofileimage when signed out");
+
+ let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
+ panelUIStatus.click();
+ yield promisePreferencesOpened;
+});
+
+/*
+XXX - Bug 1191162 - need a better hawk mock story or this will leak in debug builds.
+
+add_task(function* test_unverifiedUser() {
+ let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateAppMenuItem");
+ yield setSignedInUser(false); // this will fire the observer that does the update.
+ yield promiseUpdateDone;
+
+ // Check the world.
+ Assert.ok(isFooterVisible())
+
+ Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
+ Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
+ panelUIStatus.getAttribute("signedinTooltiptext"));
+ Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
+ let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
+ panelUIStatus.click();
+ yield promisePreferencesOpened
+ yield signOut();
+});
+*/
+
+add_task(function* test_verifiedUserEmptyProfile() {
+ // We see 2 updateAppMenuItem() calls - one for the signedInUser and one after
+ // we first fetch the profile. We want them both to fire or we aren't testing
+ // the state we think we are testing.
+ let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateAppMenuItem", 2);
+ gFxAccounts._cachedProfile = null;
+ configureProfileURL({}); // successful but empty profile.
+ yield setSignedInUser(true); // this will fire the observer that does the update.
+ yield promiseUpdateDone;
+
+ // Check the world.
+ Assert.ok(isFooterVisible())
+ Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
+ Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
+ panelUIStatus.getAttribute("signedinTooltiptext"));
+ Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
+
+ let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
+ panelUIStatus.click();
+ yield promisePreferencesOpened;
+ yield signOut();
+});
+
+add_task(function* test_verifiedUserDisplayName() {
+ let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateAppMenuItem", 2);
+ gFxAccounts._cachedProfile = null;
+ configureProfileURL({ displayName: "Test User Display Name" });
+ yield setSignedInUser(true); // this will fire the observer that does the update.
+ yield promiseUpdateDone;
+
+ Assert.ok(isFooterVisible())
+ Assert.equal(panelUILabel.getAttribute("label"), "Test User Display Name");
+ Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
+ panelUIStatus.getAttribute("signedinTooltiptext"));
+ Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
+ yield signOut();
+});
+
+add_task(function* test_verifiedUserProfileFailure() {
+ // profile failure means only one observer fires.
+ let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateAppMenuItem", 1);
+ gFxAccounts._cachedProfile = null;
+ configureProfileURL(null, 500);
+ yield setSignedInUser(true); // this will fire the observer that does the update.
+ yield promiseUpdateDone;
+
+ Assert.ok(isFooterVisible())
+ Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
+ Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
+ panelUIStatus.getAttribute("signedinTooltiptext"));
+ Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
+ yield signOut();
+});
+
+// Helpers.
+function isFooterVisible() {
+ let style = window.getComputedStyle(panelUIFooter);
+ return style.getPropertyValue("display") == "flex";
+}
+
+function configureProfileURL(profile, responseStatus = 200) {
+ let responseBody = profile ? JSON.stringify(profile) : "";
+ let url = TEST_ROOT + "fxa_profile_handler.sjs?" +
+ "responseStatus=" + responseStatus +
+ "responseBody=" + responseBody +
+ // This is a bit cheeky - the FxA code will just append "/profile"
+ // to the preference value. We arrange for this to be seen by our
+ // .sjs as part of the query string.
+ "&path=";
+
+ Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", url);
+}
+
+function promiseObserver(topic, count = 1) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ if (--count == 0) {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ }
+ }
+ Services.obs.addObserver(obs, topic, false);
+ });
+}
+
+// Stolen from browser_aboutHome.js
+function promiseWaitForEvent(node, type, capturing) {
+ return new Promise((resolve) => {
+ node.addEventListener(type, function listener(event) {
+ node.removeEventListener(type, listener, capturing);
+ resolve(event);
+ }, capturing);
+ });
+}
+
+var promiseTabOpen = Task.async(function*(urlBase) {
+ info("Waiting for tab to open...");
+ let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true);
+ let tab = event.target;
+ yield promiseTabLoadEvent(tab);
+ ok(tab.linkedBrowser.currentURI.spec.startsWith(urlBase),
+ "Got " + tab.linkedBrowser.currentURI.spec + ", expecting " + urlBase);
+ let whenUnloaded = promiseTabUnloaded(tab);
+ gBrowser.removeTab(tab);
+ yield whenUnloaded;
+});
+
+function promiseTabUnloaded(tab)
+{
+ return new Promise(resolve => {
+ info("Wait for tab to unload");
+ function handle(event) {
+ tab.linkedBrowser.removeEventListener("unload", handle, true);
+ info("Got unload event");
+ resolve(event);
+ }
+ tab.linkedBrowser.addEventListener("unload", handle, true, true);
+ });
+}
+
+// FxAccounts helpers.
+function setSignedInUser(verified) {
+ let data = {
+ email: "foo@example.com",
+ uid: "1234@lcip.org",
+ assertion: "foobar",
+ sessionToken: "dead",
+ kA: "beef",
+ kB: "cafe",
+ verified: verified,
+
+ oauthTokens: {
+ // a token for the profile server.
+ profile: "key value",
+ }
+ }
+ return fxAccounts.setSignedInUser(data);
+}
+
+var signOut = Task.async(function* () {
+ // This test needs to make sure that any updates for the logout have
+ // completed before starting the next test, or we see the observer
+ // notifications get out of sync.
+ let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateAppMenuItem");
+ // we always want a "localOnly" signout here...
+ yield fxAccounts.signOut(true);
+ yield promiseUpdateDone;
+});
diff --git a/browser/base/content/test/general/browser_gZipOfflineChild.js b/browser/base/content/test/general/browser_gZipOfflineChild.js
new file mode 100644
index 000000000..09691bed8
--- /dev/null
+++ b/browser/base/content/test/general/browser_gZipOfflineChild.js
@@ -0,0 +1,80 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const URL = "http://mochi.test:8888/browser/browser/base/content/test/general/test_offline_gzip.html"
+
+registerCleanupFunction(function() {
+ // Clean up after ourself
+ let uri = Services.io.newURI(URL, null, null);
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+ Services.perms.removeFromPrincipal(principal, "offline-app");
+ Services.prefs.clearUserPref("offline-apps.allow_by_default");
+});
+
+var cacheCount = 0;
+var intervalID = 0;
+
+//
+// Handle "message" events which are posted from the iframe upon
+// offline cache events.
+//
+function handleMessageEvents(event) {
+ cacheCount++;
+ switch (cacheCount) {
+ case 1:
+ // This is the initial caching off offline data.
+ is(event.data, "oncache", "Child was successfully cached.");
+ // Reload the frame; this will generate an error message
+ // in the case of bug 501422.
+ event.source.location.reload();
+ // Use setInterval to repeatedly call a function which
+ // checks that one of two things has occurred: either
+ // the offline cache is udpated (which means our iframe
+ // successfully reloaded), or the string "error" appears
+ // in the iframe, as in the case of bug 501422.
+ intervalID = setInterval(function() {
+ // Sometimes document.body may not exist, and trying to access
+ // it will throw an exception, so handle this case.
+ try {
+ var bodyInnerHTML = event.source.document.body.innerHTML;
+ }
+ catch (e) {
+ bodyInnerHTML = "";
+ }
+ if (cacheCount == 2 || bodyInnerHTML.includes("error")) {
+ clearInterval(intervalID);
+ is(cacheCount, 2, "frame not reloaded successfully");
+ if (cacheCount != 2) {
+ finish();
+ }
+ }
+ }, 100);
+ break;
+ case 2:
+ is(event.data, "onupdate", "Child was successfully updated.");
+ clearInterval(intervalID);
+ finish();
+ break;
+ default:
+ // how'd we get here?
+ ok(false, "cacheCount not 1 or 2");
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("offline-apps.allow_by_default", true);
+
+ // Open a new tab.
+ gBrowser.selectedTab = gBrowser.addTab(URL);
+ registerCleanupFunction(() => gBrowser.removeCurrentTab());
+
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ let window = gBrowser.selectedBrowser.contentWindow;
+
+ window.addEventListener("message", handleMessageEvents, false);
+ });
+}
diff --git a/browser/base/content/test/general/browser_gestureSupport.js b/browser/base/content/test/general/browser_gestureSupport.js
new file mode 100644
index 000000000..b31cad31d
--- /dev/null
+++ b/browser/base/content/test/general/browser_gestureSupport.js
@@ -0,0 +1,670 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Simple gestures tests
+//
+// These tests require the ability to disable the fact that the
+// Firefox chrome intentionally prevents "simple gesture" events from
+// reaching web content.
+
+var test_utils;
+var test_commandset;
+var test_prefBranch = "browser.gesture.";
+
+function test()
+{
+ waitForExplicitFinish();
+
+ // Disable the default gestures support during the test
+ gGestureSupport.init(false);
+
+ test_utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+ getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+ // Run the tests of "simple gesture" events generally
+ test_EnsureConstantsAreDisjoint();
+ test_TestEventListeners();
+ test_TestEventCreation();
+
+ // Reenable the default gestures support. The remaining tests target
+ // the Firefox gesture functionality.
+ gGestureSupport.init(true);
+
+ // Test Firefox's gestures support.
+ test_commandset = document.getElementById("mainCommandSet");
+ test_swipeGestures();
+ test_latchedGesture("pinch", "out", "in", "MozMagnifyGesture");
+ test_thresholdGesture("pinch", "out", "in", "MozMagnifyGesture");
+ test_rotateGestures();
+}
+
+var test_eventCount = 0;
+var test_expectedType;
+var test_expectedDirection;
+var test_expectedDelta;
+var test_expectedModifiers;
+var test_expectedClickCount;
+var test_imageTab;
+
+function test_gestureListener(evt)
+{
+ is(evt.type, test_expectedType,
+ "evt.type (" + evt.type + ") does not match expected value");
+ is(evt.target, test_utils.elementFromPoint(20, 20, false, false),
+ "evt.target (" + evt.target + ") does not match expected value");
+ is(evt.clientX, 20,
+ "evt.clientX (" + evt.clientX + ") does not match expected value");
+ is(evt.clientY, 20,
+ "evt.clientY (" + evt.clientY + ") does not match expected value");
+ isnot(evt.screenX, 0,
+ "evt.screenX (" + evt.screenX + ") does not match expected value");
+ isnot(evt.screenY, 0,
+ "evt.screenY (" + evt.screenY + ") does not match expected value");
+
+ is(evt.direction, test_expectedDirection,
+ "evt.direction (" + evt.direction + ") does not match expected value");
+ is(evt.delta, test_expectedDelta,
+ "evt.delta (" + evt.delta + ") does not match expected value");
+
+ is(evt.shiftKey, (test_expectedModifiers & Components.interfaces.nsIDOMEvent.SHIFT_MASK) != 0,
+ "evt.shiftKey did not match expected value");
+ is(evt.ctrlKey, (test_expectedModifiers & Components.interfaces.nsIDOMEvent.CONTROL_MASK) != 0,
+ "evt.ctrlKey did not match expected value");
+ is(evt.altKey, (test_expectedModifiers & Components.interfaces.nsIDOMEvent.ALT_MASK) != 0,
+ "evt.altKey did not match expected value");
+ is(evt.metaKey, (test_expectedModifiers & Components.interfaces.nsIDOMEvent.META_MASK) != 0,
+ "evt.metaKey did not match expected value");
+
+ if (evt.type == "MozTapGesture") {
+ is(evt.clickCount, test_expectedClickCount, "evt.clickCount does not match");
+ }
+
+ test_eventCount++;
+}
+
+function test_helper1(type, direction, delta, modifiers)
+{
+ // Setup the expected values
+ test_expectedType = type;
+ test_expectedDirection = direction;
+ test_expectedDelta = delta;
+ test_expectedModifiers = modifiers;
+
+ let expectedEventCount = test_eventCount + 1;
+
+ document.addEventListener(type, test_gestureListener, true);
+ test_utils.sendSimpleGestureEvent(type, 20, 20, direction, delta, modifiers);
+ document.removeEventListener(type, test_gestureListener, true);
+
+ is(expectedEventCount, test_eventCount, "Event (" + type + ") was never received by event listener");
+}
+
+function test_clicks(type, clicks)
+{
+ // Setup the expected values
+ test_expectedType = type;
+ test_expectedDirection = 0;
+ test_expectedDelta = 0;
+ test_expectedModifiers = 0;
+ test_expectedClickCount = clicks;
+
+ let expectedEventCount = test_eventCount + 1;
+
+ document.addEventListener(type, test_gestureListener, true);
+ test_utils.sendSimpleGestureEvent(type, 20, 20, 0, 0, 0, clicks);
+ document.removeEventListener(type, test_gestureListener, true);
+
+ is(expectedEventCount, test_eventCount, "Event (" + type + ") was never received by event listener");
+}
+
+function test_TestEventListeners()
+{
+ let e = test_helper1; // easier to type this name
+
+ // Swipe gesture animation events
+ e("MozSwipeGestureStart", 0, -0.7, 0);
+ e("MozSwipeGestureUpdate", 0, -0.4, 0);
+ e("MozSwipeGestureEnd", 0, 0, 0);
+ e("MozSwipeGestureStart", 0, 0.6, 0);
+ e("MozSwipeGestureUpdate", 0, 0.3, 0);
+ e("MozSwipeGestureEnd", 0, 1, 0);
+
+ // Swipe gesture event
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_LEFT, 0.0, 0);
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0.0, 0);
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_UP, 0.0, 0);
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_DOWN, 0.0, 0);
+ e("MozSwipeGesture",
+ SimpleGestureEvent.DIRECTION_UP | SimpleGestureEvent.DIRECTION_LEFT, 0.0, 0);
+ e("MozSwipeGesture",
+ SimpleGestureEvent.DIRECTION_DOWN | SimpleGestureEvent.DIRECTION_RIGHT, 0.0, 0);
+ e("MozSwipeGesture",
+ SimpleGestureEvent.DIRECTION_UP | SimpleGestureEvent.DIRECTION_RIGHT, 0.0, 0);
+ e("MozSwipeGesture",
+ SimpleGestureEvent.DIRECTION_DOWN | SimpleGestureEvent.DIRECTION_LEFT, 0.0, 0);
+
+ // magnify gesture events
+ e("MozMagnifyGestureStart", 0, 50.0, 0);
+ e("MozMagnifyGestureUpdate", 0, -25.0, 0);
+ e("MozMagnifyGestureUpdate", 0, 5.0, 0);
+ e("MozMagnifyGesture", 0, 30.0, 0);
+
+ // rotate gesture events
+ e("MozRotateGestureStart", SimpleGestureEvent.ROTATION_CLOCKWISE, 33.0, 0);
+ e("MozRotateGestureUpdate", SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE, -13.0, 0);
+ e("MozRotateGestureUpdate", SimpleGestureEvent.ROTATION_CLOCKWISE, 13.0, 0);
+ e("MozRotateGesture", SimpleGestureEvent.ROTATION_CLOCKWISE, 33.0, 0);
+
+ // Tap and presstap gesture events
+ test_clicks("MozTapGesture", 1);
+ test_clicks("MozTapGesture", 2);
+ test_clicks("MozTapGesture", 3);
+ test_clicks("MozPressTapGesture", 1);
+
+ // simple delivery test for edgeui gestures
+ e("MozEdgeUIStarted", 0, 0, 0);
+ e("MozEdgeUICanceled", 0, 0, 0);
+ e("MozEdgeUICompleted", 0, 0, 0);
+
+ // event.shiftKey
+ let modifier = Components.interfaces.nsIDOMEvent.SHIFT_MASK;
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier);
+
+ // event.metaKey
+ modifier = Components.interfaces.nsIDOMEvent.META_MASK;
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier);
+
+ // event.altKey
+ modifier = Components.interfaces.nsIDOMEvent.ALT_MASK;
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier);
+
+ // event.ctrlKey
+ modifier = Components.interfaces.nsIDOMEvent.CONTROL_MASK;
+ e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier);
+}
+
+function test_eventDispatchListener(evt)
+{
+ test_eventCount++;
+ evt.stopPropagation();
+}
+
+function test_helper2(type, direction, delta, altKey, ctrlKey, shiftKey, metaKey)
+{
+ let event = null;
+ let successful;
+
+ try {
+ event = document.createEvent("SimpleGestureEvent");
+ successful = true;
+ }
+ catch (ex) {
+ successful = false;
+ }
+ ok(successful, "Unable to create SimpleGestureEvent");
+
+ try {
+ event.initSimpleGestureEvent(type, true, true, window, 1,
+ 10, 10, 10, 10,
+ ctrlKey, altKey, shiftKey, metaKey,
+ 1, window,
+ 0, direction, delta, 0);
+ successful = true;
+ }
+ catch (ex) {
+ successful = false;
+ }
+ ok(successful, "event.initSimpleGestureEvent should not fail");
+
+ // Make sure the event fields match the expected values
+ is(event.type, type, "Mismatch on evt.type");
+ is(event.direction, direction, "Mismatch on evt.direction");
+ is(event.delta, delta, "Mismatch on evt.delta");
+ is(event.altKey, altKey, "Mismatch on evt.altKey");
+ is(event.ctrlKey, ctrlKey, "Mismatch on evt.ctrlKey");
+ is(event.shiftKey, shiftKey, "Mismatch on evt.shiftKey");
+ is(event.metaKey, metaKey, "Mismatch on evt.metaKey");
+ is(event.view, window, "Mismatch on evt.view");
+ is(event.detail, 1, "Mismatch on evt.detail");
+ is(event.clientX, 10, "Mismatch on evt.clientX");
+ is(event.clientY, 10, "Mismatch on evt.clientY");
+ is(event.screenX, 10, "Mismatch on evt.screenX");
+ is(event.screenY, 10, "Mismatch on evt.screenY");
+ is(event.button, 1, "Mismatch on evt.button");
+ is(event.relatedTarget, window, "Mismatch on evt.relatedTarget");
+
+ // Test event dispatch
+ let expectedEventCount = test_eventCount + 1;
+ document.addEventListener(type, test_eventDispatchListener, true);
+ document.dispatchEvent(event);
+ document.removeEventListener(type, test_eventDispatchListener, true);
+ is(expectedEventCount, test_eventCount, "Dispatched event was never received by listener");
+}
+
+function test_TestEventCreation()
+{
+ // Event creation
+ test_helper2("MozMagnifyGesture", SimpleGestureEvent.DIRECTION_RIGHT, 20.0,
+ true, false, true, false);
+ test_helper2("MozMagnifyGesture", SimpleGestureEvent.DIRECTION_LEFT, -20.0,
+ false, true, false, true);
+}
+
+function test_EnsureConstantsAreDisjoint()
+{
+ let up = SimpleGestureEvent.DIRECTION_UP;
+ let down = SimpleGestureEvent.DIRECTION_DOWN;
+ let left = SimpleGestureEvent.DIRECTION_LEFT;
+ let right = SimpleGestureEvent.DIRECTION_RIGHT;
+
+ let clockwise = SimpleGestureEvent.ROTATION_CLOCKWISE;
+ let cclockwise = SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE;
+
+ ok(up ^ down, "DIRECTION_UP and DIRECTION_DOWN are not bitwise disjoint");
+ ok(up ^ left, "DIRECTION_UP and DIRECTION_LEFT are not bitwise disjoint");
+ ok(up ^ right, "DIRECTION_UP and DIRECTION_RIGHT are not bitwise disjoint");
+ ok(down ^ left, "DIRECTION_DOWN and DIRECTION_LEFT are not bitwise disjoint");
+ ok(down ^ right, "DIRECTION_DOWN and DIRECTION_RIGHT are not bitwise disjoint");
+ ok(left ^ right, "DIRECTION_LEFT and DIRECTION_RIGHT are not bitwise disjoint");
+ ok(clockwise ^ cclockwise, "ROTATION_CLOCKWISE and ROTATION_COUNTERCLOCKWISE are not bitwise disjoint");
+}
+
+// Helper for test of latched event processing. Emits the actual
+// gesture events to test whether the commands associated with the
+// gesture will only trigger once for each direction of movement.
+function test_emitLatchedEvents(eventPrefix, initialDelta, cmd)
+{
+ let cumulativeDelta = 0;
+ let isIncreasing = initialDelta > 0;
+
+ let expect = {};
+ // Reset the call counters and initialize expected values
+ for (let dir in cmd)
+ cmd[dir].callCount = expect[dir] = 0;
+
+ let check = (aDir, aMsg) => ok(cmd[aDir].callCount == expect[aDir], aMsg);
+ let checkBoth = function(aNum, aInc, aDec) {
+ let prefix = "Step " + aNum + ": ";
+ check("inc", prefix + aInc);
+ check("dec", prefix + aDec);
+ };
+
+ // Send the "Start" event.
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Start", 0, 0, 0, initialDelta, 0);
+ cumulativeDelta += initialDelta;
+ if (isIncreasing) {
+ expect.inc++;
+ checkBoth(1, "Increasing command was not triggered", "Decreasing command was triggered");
+ } else {
+ expect.dec++;
+ checkBoth(1, "Increasing command was triggered", "Decreasing command was not triggered");
+ }
+
+ // Send random values in the same direction and ensure neither
+ // command triggers.
+ for (let i = 0; i < 5; i++) {
+ let delta = Math.random() * (isIncreasing ? 100 : -100);
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, delta, 0);
+ cumulativeDelta += delta;
+ checkBoth(2, "Increasing command was triggered", "Decreasing command was triggered");
+ }
+
+ // Now go back in the opposite direction.
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0,
+ - initialDelta, 0);
+ cumulativeDelta += - initialDelta;
+ if (isIncreasing) {
+ expect.dec++;
+ checkBoth(3, "Increasing command was triggered", "Decreasing command was not triggered");
+ } else {
+ expect.inc++;
+ checkBoth(3, "Increasing command was not triggered", "Decreasing command was triggered");
+ }
+
+ // Send random values in the opposite direction and ensure neither
+ // command triggers.
+ for (let i = 0; i < 5; i++) {
+ let delta = Math.random() * (isIncreasing ? -100 : 100);
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, delta, 0);
+ cumulativeDelta += delta;
+ checkBoth(4, "Increasing command was triggered", "Decreasing command was triggered");
+ }
+
+ // Go back to the original direction. The original command should trigger.
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0,
+ initialDelta, 0);
+ cumulativeDelta += initialDelta;
+ if (isIncreasing) {
+ expect.inc++;
+ checkBoth(5, "Increasing command was not triggered", "Decreasing command was triggered");
+ } else {
+ expect.dec++;
+ checkBoth(5, "Increasing command was triggered", "Decreasing command was not triggered");
+ }
+
+ // Send the wrap-up event. No commands should be triggered.
+ test_utils.sendSimpleGestureEvent(eventPrefix, 0, 0, 0, cumulativeDelta, 0);
+ checkBoth(6, "Increasing command was triggered", "Decreasing command was triggered");
+}
+
+function test_addCommand(prefName, id)
+{
+ let cmd = test_commandset.appendChild(document.createElement("command"));
+ cmd.setAttribute("id", id);
+ cmd.setAttribute("oncommand", "this.callCount++;");
+
+ cmd.origPrefName = prefName;
+ cmd.origPrefValue = gPrefService.getCharPref(prefName);
+ gPrefService.setCharPref(prefName, id);
+
+ return cmd;
+}
+
+function test_removeCommand(cmd)
+{
+ gPrefService.setCharPref(cmd.origPrefName, cmd.origPrefValue);
+ test_commandset.removeChild(cmd);
+}
+
+// Test whether latched events are only called once per direction of motion.
+function test_latchedGesture(gesture, inc, dec, eventPrefix)
+{
+ let branch = test_prefBranch + gesture + ".";
+
+ // Put the gesture into latched mode.
+ let oldLatchedValue = gPrefService.getBoolPref(branch + "latched");
+ gPrefService.setBoolPref(branch + "latched", true);
+
+ // Install the test commands for increasing and decreasing motion.
+ let cmd = {
+ inc: test_addCommand(branch + inc, "test:incMotion"),
+ dec: test_addCommand(branch + dec, "test:decMotion"),
+ };
+
+ // Test the gestures in each direction.
+ test_emitLatchedEvents(eventPrefix, 500, cmd);
+ test_emitLatchedEvents(eventPrefix, -500, cmd);
+
+ // Restore the gesture to its original configuration.
+ gPrefService.setBoolPref(branch + "latched", oldLatchedValue);
+ for (let dir in cmd)
+ test_removeCommand(cmd[dir]);
+}
+
+// Test whether non-latched events are triggered upon sufficient motion.
+function test_thresholdGesture(gesture, inc, dec, eventPrefix)
+{
+ let branch = test_prefBranch + gesture + ".";
+
+ // Disable latched mode for this gesture.
+ let oldLatchedValue = gPrefService.getBoolPref(branch + "latched");
+ gPrefService.setBoolPref(branch + "latched", false);
+
+ // Set the triggering threshold value to 50.
+ let oldThresholdValue = gPrefService.getIntPref(branch + "threshold");
+ gPrefService.setIntPref(branch + "threshold", 50);
+
+ // Install the test commands for increasing and decreasing motion.
+ let cmdInc = test_addCommand(branch + inc, "test:incMotion");
+ let cmdDec = test_addCommand(branch + dec, "test:decMotion");
+
+ // Send the start event but stop short of triggering threshold.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Start", 0, 0, 0, 49.5, 0);
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Now trigger the threshold.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, 1, 0);
+ ok(cmdInc.callCount == 1, "Increasing command was not triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // The tracking counter should go to zero. Go back the other way and
+ // stop short of triggering the threshold.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, -49.5, 0);
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Now cross the threshold and trigger the decreasing command.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, -1.5, 0);
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 1, "Decreasing command was not triggered");
+
+ // Send the wrap-up event. No commands should trigger.
+ cmdInc.callCount = cmdDec.callCount = 0;
+ test_utils.sendSimpleGestureEvent(eventPrefix, 0, 0, 0, -0.5, 0);
+ ok(cmdInc.callCount == 0, "Increasing command was triggered");
+ ok(cmdDec.callCount == 0, "Decreasing command was triggered");
+
+ // Restore the gesture to its original configuration.
+ gPrefService.setBoolPref(branch + "latched", oldLatchedValue);
+ gPrefService.setIntPref(branch + "threshold", oldThresholdValue);
+ test_removeCommand(cmdInc);
+ test_removeCommand(cmdDec);
+}
+
+function test_swipeGestures()
+{
+ // easier to type names for the direction constants
+ let up = SimpleGestureEvent.DIRECTION_UP;
+ let down = SimpleGestureEvent.DIRECTION_DOWN;
+ let left = SimpleGestureEvent.DIRECTION_LEFT;
+ let right = SimpleGestureEvent.DIRECTION_RIGHT;
+
+ let branch = test_prefBranch + "swipe.";
+
+ // Install the test commands for the swipe gestures.
+ let cmdUp = test_addCommand(branch + "up", "test:swipeUp");
+ let cmdDown = test_addCommand(branch + "down", "test:swipeDown");
+ let cmdLeft = test_addCommand(branch + "left", "test:swipeLeft");
+ let cmdRight = test_addCommand(branch + "right", "test:swipeRight");
+
+ function resetCounts() {
+ cmdUp.callCount = 0;
+ cmdDown.callCount = 0;
+ cmdLeft.callCount = 0;
+ cmdRight.callCount = 0;
+ }
+
+ // UP
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, up, 0, 0);
+ ok(cmdUp.callCount == 1, "Step 1: Up command was not triggered");
+ ok(cmdDown.callCount == 0, "Step 1: Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 1: Left command was triggered");
+ ok(cmdRight.callCount == 0, "Step 1: Right command was triggered");
+
+ // DOWN
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, down, 0, 0);
+ ok(cmdUp.callCount == 0, "Step 2: Up command was triggered");
+ ok(cmdDown.callCount == 1, "Step 2: Down command was not triggered");
+ ok(cmdLeft.callCount == 0, "Step 2: Left command was triggered");
+ ok(cmdRight.callCount == 0, "Step 2: Right command was triggered");
+
+ // LEFT
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, left, 0, 0);
+ ok(cmdUp.callCount == 0, "Step 3: Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 3: Down command was triggered");
+ ok(cmdLeft.callCount == 1, "Step 3: Left command was not triggered");
+ ok(cmdRight.callCount == 0, "Step 3: Right command was triggered");
+
+ // RIGHT
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, right, 0, 0);
+ ok(cmdUp.callCount == 0, "Step 4: Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 4: Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 4: Left command was triggered");
+ ok(cmdRight.callCount == 1, "Step 4: Right command was not triggered");
+
+ // Make sure combinations do not trigger events.
+ let combos = [ up | left, up | right, down | left, down | right];
+ for (let i = 0; i < combos.length; i++) {
+ resetCounts();
+ test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, combos[i], 0, 0);
+ ok(cmdUp.callCount == 0, "Step 5-"+i+": Up command was triggered");
+ ok(cmdDown.callCount == 0, "Step 5-"+i+": Down command was triggered");
+ ok(cmdLeft.callCount == 0, "Step 5-"+i+": Left command was triggered");
+ ok(cmdRight.callCount == 0, "Step 5-"+i+": Right command was triggered");
+ }
+
+ // Remove the test commands.
+ test_removeCommand(cmdUp);
+ test_removeCommand(cmdDown);
+ test_removeCommand(cmdLeft);
+ test_removeCommand(cmdRight);
+}
+
+
+function test_rotateHelperGetImageRotation(aImageElement)
+{
+ // Get the true image rotation from the transform matrix, bounded
+ // to 0 <= result < 360
+ let transformValue = content.window.getComputedStyle(aImageElement, null)
+ .transform;
+ if (transformValue == "none")
+ return 0;
+
+ transformValue = transformValue.split("(")[1]
+ .split(")")[0]
+ .split(",");
+ var rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) *
+ (180 / Math.PI));
+ return (rotation < 0 ? rotation + 360 : rotation);
+}
+
+function test_rotateHelperOneGesture(aImageElement, aCurrentRotation,
+ aDirection, aAmount, aStop)
+{
+ if (aAmount <= 0 || aAmount > 90) // Bound to 0 < aAmount <= 90
+ return;
+
+ // easier to type names for the direction constants
+ let clockwise = SimpleGestureEvent.ROTATION_CLOCKWISE;
+
+ let delta = aAmount * (aDirection == clockwise ? 1 : -1);
+
+ // Kill transition time on image so test isn't wrong and doesn't take 10 seconds
+ aImageElement.style.transitionDuration = "0s";
+
+ // Start the gesture, perform an update, and force flush
+ test_utils.sendSimpleGestureEvent("MozRotateGestureStart", 0, 0, aDirection, .001, 0);
+ test_utils.sendSimpleGestureEvent("MozRotateGestureUpdate", 0, 0, aDirection, delta, 0);
+ aImageElement.clientTop;
+
+ // If stop, check intermediate
+ if (aStop) {
+ // Send near-zero-delta to stop, and force flush
+ test_utils.sendSimpleGestureEvent("MozRotateGestureUpdate", 0, 0, aDirection, .001, 0);
+ aImageElement.clientTop;
+
+ let stopExpectedRotation = (aCurrentRotation + delta) % 360;
+ if (stopExpectedRotation < 0)
+ stopExpectedRotation += 360;
+
+ is(stopExpectedRotation, test_rotateHelperGetImageRotation(aImageElement),
+ "Image rotation at gesture stop/hold: expected=" + stopExpectedRotation +
+ ", observed=" + test_rotateHelperGetImageRotation(aImageElement) +
+ ", init=" + aCurrentRotation +
+ ", amt=" + aAmount +
+ ", dir=" + (aDirection == clockwise ? "cl" : "ccl"));
+ }
+ // End it and force flush
+ test_utils.sendSimpleGestureEvent("MozRotateGesture", 0, 0, aDirection, 0, 0);
+ aImageElement.clientTop;
+
+ let finalExpectedRotation;
+
+ if (aAmount < 45 && aStop) {
+ // Rotate a bit, then stop. Expect no change at end of gesture.
+ finalExpectedRotation = aCurrentRotation;
+ }
+ else {
+ // Either not stopping (expect 90 degree change in aDirection), OR
+ // stopping but after 45, (expect 90 degree change in aDirection)
+ finalExpectedRotation = (aCurrentRotation +
+ (aDirection == clockwise ? 1 : -1) * 90) % 360;
+ if (finalExpectedRotation < 0)
+ finalExpectedRotation += 360;
+ }
+
+ is(finalExpectedRotation, test_rotateHelperGetImageRotation(aImageElement),
+ "Image rotation gesture end: expected=" + finalExpectedRotation +
+ ", observed=" + test_rotateHelperGetImageRotation(aImageElement) +
+ ", init=" + aCurrentRotation +
+ ", amt=" + aAmount +
+ ", dir=" + (aDirection == clockwise ? "cl" : "ccl"));
+}
+
+function test_rotateGesturesOnTab()
+{
+ gBrowser.selectedBrowser.removeEventListener("load", test_rotateGesturesOnTab, true);
+
+ if (!(content.document instanceof ImageDocument)) {
+ ok(false, "Image document failed to open for rotation testing");
+ gBrowser.removeTab(test_imageTab);
+ finish();
+ return;
+ }
+
+ // easier to type names for the direction constants
+ let cl = SimpleGestureEvent.ROTATION_CLOCKWISE;
+ let ccl = SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE;
+
+ let imgElem = content.document.body &&
+ content.document.body.firstElementChild;
+
+ if (!imgElem) {
+ ok(false, "Could not get image element on ImageDocument for rotation!");
+ gBrowser.removeTab(test_imageTab);
+ finish();
+ return;
+ }
+
+ // Quick function to normalize rotation to 0 <= r < 360
+ var normRot = function(rotation) {
+ rotation = rotation % 360;
+ if (rotation < 0)
+ rotation += 360;
+ return rotation;
+ }
+
+ for (var initRot = 0; initRot < 360; initRot += 90) {
+ // Test each case: at each 90 degree snap; cl/ccl;
+ // amount more or less than 45; stop and hold or don't (32 total tests)
+ // The amount added to the initRot is where it is expected to be
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 0), cl, 35, true );
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 0), cl, 35, false);
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 90), cl, 55, true );
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 180), cl, 55, false);
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 270), ccl, 35, true );
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 270), ccl, 35, false);
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 180), ccl, 55, true );
+ test_rotateHelperOneGesture(imgElem, normRot(initRot + 90), ccl, 55, false);
+
+ // Manually rotate it 90 degrees clockwise to prepare for next iteration,
+ // and force flush
+ test_utils.sendSimpleGestureEvent("MozRotateGestureStart", 0, 0, cl, .001, 0);
+ test_utils.sendSimpleGestureEvent("MozRotateGestureUpdate", 0, 0, cl, 90, 0);
+ test_utils.sendSimpleGestureEvent("MozRotateGestureUpdate", 0, 0, cl, .001, 0);
+ test_utils.sendSimpleGestureEvent("MozRotateGesture", 0, 0, cl, 0, 0);
+ imgElem.clientTop;
+ }
+
+ gBrowser.removeTab(test_imageTab);
+ test_imageTab = null;
+ finish();
+}
+
+function test_rotateGestures()
+{
+ test_imageTab = gBrowser.addTab("chrome://branding/content/about-logo.png");
+ gBrowser.selectedTab = test_imageTab;
+
+ gBrowser.selectedBrowser.addEventListener("load", test_rotateGesturesOnTab, true);
+}
diff --git a/browser/base/content/test/general/browser_getshortcutoruri.js b/browser/base/content/test/general/browser_getshortcutoruri.js
new file mode 100644
index 000000000..9ebf8e9ca
--- /dev/null
+++ b/browser/base/content/test/general/browser_getshortcutoruri.js
@@ -0,0 +1,143 @@
+function getPostDataString(aIS) {
+ if (!aIS)
+ return null;
+
+ var sis = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+ sis.init(aIS);
+ var dataLines = sis.read(aIS.available()).split("\n");
+
+ // only want the last line
+ return dataLines[dataLines.length-1];
+}
+
+function keywordResult(aURL, aPostData, aIsUnsafe) {
+ this.url = aURL;
+ this.postData = aPostData;
+ this.isUnsafe = aIsUnsafe;
+}
+
+function keyWordData() {}
+keyWordData.prototype = {
+ init: function(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.keyword = aKeyWord;
+ this.uri = makeURI(aURL);
+ this.postData = aPostData;
+ this.searchWord = aSearchWord;
+
+ this.method = (this.postData ? "POST" : "GET");
+ }
+}
+
+function bmKeywordData(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.init(aKeyWord, aURL, aPostData, aSearchWord);
+}
+bmKeywordData.prototype = new keyWordData();
+
+function searchKeywordData(aKeyWord, aURL, aPostData, aSearchWord) {
+ this.init(aKeyWord, aURL, aPostData, aSearchWord);
+}
+searchKeywordData.prototype = new keyWordData();
+
+var testData = [
+ [new bmKeywordData("bmget", "http://bmget/search=%s", null, "foo"),
+ new keywordResult("http://bmget/search=foo", null)],
+
+ [new bmKeywordData("bmpost", "http://bmpost/", "search=%s", "foo2"),
+ new keywordResult("http://bmpost/", "search=foo2")],
+
+ [new bmKeywordData("bmpostget", "http://bmpostget/search1=%s", "search2=%s", "foo3"),
+ new keywordResult("http://bmpostget/search1=foo3", "search2=foo3")],
+
+ [new bmKeywordData("bmget-nosearch", "http://bmget-nosearch/", null, ""),
+ new keywordResult("http://bmget-nosearch/", null)],
+
+ [new searchKeywordData("searchget", "http://searchget/?search={searchTerms}", null, "foo4"),
+ new keywordResult("http://searchget/?search=foo4", null, true)],
+
+ [new searchKeywordData("searchpost", "http://searchpost/", "search={searchTerms}", "foo5"),
+ new keywordResult("http://searchpost/", "search=foo5", true)],
+
+ [new searchKeywordData("searchpostget", "http://searchpostget/?search1={searchTerms}", "search2={searchTerms}", "foo6"),
+ new keywordResult("http://searchpostget/?search1=foo6", "search2=foo6", true)],
+
+ // Bookmark keywords that don't take parameters should not be activated if a
+ // parameter is passed (bug 420328).
+ [new bmKeywordData("bmget-noparam", "http://bmget-noparam/", null, "foo7"),
+ new keywordResult(null, null, true)],
+ [new bmKeywordData("bmpost-noparam", "http://bmpost-noparam/", "not_a=param", "foo8"),
+ new keywordResult(null, null, true)],
+
+ // Test escaping (%s = escaped, %S = raw)
+ // UTF-8 default
+ [new bmKeywordData("bmget-escaping", "http://bmget/?esc=%s&raw=%S", null, "foé"),
+ new keywordResult("http://bmget/?esc=fo%C3%A9&raw=foé", null)],
+ // Explicitly-defined ISO-8859-1
+ [new bmKeywordData("bmget-escaping2", "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1", null, "foé"),
+ new keywordResult("http://bmget/?esc=fo%E9&raw=foé", null)],
+
+ // Bug 359809: Test escaping +, /, and @
+ // UTF-8 default
+ [new bmKeywordData("bmget-escaping", "http://bmget/?esc=%s&raw=%S", null, "+/@"),
+ new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null)],
+ // Explicitly-defined ISO-8859-1
+ [new bmKeywordData("bmget-escaping2", "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1", null, "+/@"),
+ new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null)],
+
+ // Test using a non-bmKeywordData object, to test the behavior of
+ // getShortcutOrURIAndPostData for non-keywords (setupKeywords only adds keywords for
+ // bmKeywordData objects)
+ [{keyword: "http://gavinsharp.com"},
+ new keywordResult(null, null, true)]
+];
+
+add_task(function* test_getshortcutoruri() {
+ yield setupKeywords();
+
+ for (let item of testData) {
+ let [data, result] = item;
+
+ let query = data.keyword;
+ if (data.searchWord)
+ query += " " + data.searchWord;
+ let returnedData = yield getShortcutOrURIAndPostData(query);
+ // null result.url means we should expect the same query we sent in
+ let expected = result.url || query;
+ is(returnedData.url, expected, "got correct URL for " + data.keyword);
+ is(getPostDataString(returnedData.postData), result.postData, "got correct postData for " + data.keyword);
+ is(returnedData.mayInheritPrincipal, !result.isUnsafe, "got correct mayInheritPrincipal for " + data.keyword);
+ }
+
+ yield cleanupKeywords();
+});
+
+var folder = null;
+var gAddedEngines = [];
+
+function* setupKeywords() {
+ folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "keyword-test" });
+ for (let item of testData) {
+ let data = item[0];
+ if (data instanceof bmKeywordData) {
+ yield PlacesUtils.bookmarks.insert({ url: data.uri, parentGuid: folder.guid });
+ yield PlacesUtils.keywords.insert({ keyword: data.keyword, url: data.uri.spec, postData: data.postData });
+ }
+
+ if (data instanceof searchKeywordData) {
+ Services.search.addEngineWithDetails(data.keyword, "", data.keyword, "", data.method, data.uri.spec);
+ let addedEngine = Services.search.getEngineByName(data.keyword);
+ if (data.postData) {
+ let [paramName, paramValue] = data.postData.split("=");
+ addedEngine.addParam(paramName, paramValue, null);
+ }
+ gAddedEngines.push(addedEngine);
+ }
+ }
+}
+
+function* cleanupKeywords() {
+ PlacesUtils.bookmarks.remove(folder);
+ gAddedEngines.map(Services.search.removeEngine);
+}
diff --git a/browser/base/content/test/general/browser_hide_removing.js b/browser/base/content/test/general/browser_hide_removing.js
new file mode 100644
index 000000000..be62e2d89
--- /dev/null
+++ b/browser/base/content/test/general/browser_hide_removing.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/. */
+
+// Bug 587922: tabs don't get removed if they're hidden
+
+function test() {
+ waitForExplicitFinish();
+
+ // Add a tab that will get removed and hidden
+ let testTab = gBrowser.addTab("about:blank", {skipAnimation: true});
+ is(gBrowser.visibleTabs.length, 2, "just added a tab, so 2 tabs");
+ gBrowser.selectedTab = testTab;
+
+ let numVisBeforeHide, numVisAfterHide;
+ gBrowser.tabContainer.addEventListener("TabSelect", function() {
+ gBrowser.tabContainer.removeEventListener("TabSelect", arguments.callee, false);
+
+ // While the next tab is being selected, hide the removing tab
+ numVisBeforeHide = gBrowser.visibleTabs.length;
+ gBrowser.hideTab(testTab);
+ numVisAfterHide = gBrowser.visibleTabs.length;
+ }, false);
+ gBrowser.removeTab(testTab, {animate: true});
+
+ // Make sure the tab gets removed at the end of the animation by polling
+ (function checkRemoved() {
+ return setTimeout(function() {
+ if (gBrowser.tabs.length != 1) {
+ checkRemoved();
+ return;
+ }
+
+ is(numVisBeforeHide, 1, "animated remove has in 1 tab left");
+ is(numVisAfterHide, 1, "hiding a removing tab is also has 1 tab");
+ finish();
+ }, 50);
+ })();
+}
diff --git a/browser/base/content/test/general/browser_homeDrop.js b/browser/base/content/test/general/browser_homeDrop.js
new file mode 100644
index 000000000..6e87963d5
--- /dev/null
+++ b/browser/base/content/test/general/browser_homeDrop.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function*() {
+ let HOMEPAGE_PREF = "browser.startup.homepage";
+
+ let homepageStr = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ homepageStr.data = "about:mozilla";
+ yield pushPrefs([HOMEPAGE_PREF, homepageStr, Ci.nsISupportsString]);
+
+ let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+ let EventUtils = {};
+ scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button.
+ let dragSrcElement = document.getElementById("downloads-button");
+ ok(dragSrcElement, "Downloads button exists");
+ let homeButton = document.getElementById("home-button");
+ ok(homeButton, "home button present");
+
+ function* drop(dragData, homepage) {
+ let setHomepageDialogPromise = BrowserTestUtils.domWindowOpened();
+
+ EventUtils.synthesizeDrop(dragSrcElement, homeButton, dragData, "copy", window);
+
+ let setHomepageDialog = yield setHomepageDialogPromise;
+ ok(true, "dialog appeared in response to home button drop");
+ yield BrowserTestUtils.waitForEvent(setHomepageDialog, "load", false);
+
+ let setHomepagePromise = new Promise(function(resolve) {
+ let observer = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+ observe: function(subject, topic, data) {
+ is(topic, "nsPref:changed", "observed correct topic");
+ is(data, HOMEPAGE_PREF, "observed correct data");
+ let modified = Services.prefs.getComplexValue(HOMEPAGE_PREF,
+ Ci.nsISupportsString);
+ is(modified.data, homepage, "homepage is set correctly");
+ Services.prefs.removeObserver(HOMEPAGE_PREF, observer);
+
+ Services.prefs.setComplexValue(HOMEPAGE_PREF,
+ Ci.nsISupportsString, homepageStr);
+
+ resolve();
+ }
+ };
+ Services.prefs.addObserver(HOMEPAGE_PREF, observer, false);
+ });
+
+ setHomepageDialog.document.documentElement.acceptDialog();
+
+ yield setHomepagePromise;
+ }
+
+ function dropInvalidURI() {
+ return new Promise(resolve => {
+ let consoleListener = {
+ observe: function (m) {
+ if (m.message.includes("NS_ERROR_DOM_BAD_URI")) {
+ ok(true, "drop was blocked");
+ resolve();
+ }
+ }
+ };
+ Services.console.registerListener(consoleListener);
+ registerCleanupFunction(function () {
+ Services.console.unregisterListener(consoleListener);
+ });
+
+ executeSoon(function () {
+ info("Attempting second drop, of a javascript: URI");
+ // The drop handler throws an exception when dragging URIs that inherit
+ // principal, e.g. javascript:
+ expectUncaughtException();
+ EventUtils.synthesizeDrop(dragSrcElement, homeButton, [[{type: "text/plain", data: "javascript:8888"}]], "copy", window);
+ });
+ });
+ }
+
+ yield* drop([[{type: "text/plain",
+ data: "http://mochi.test:8888/"}]],
+ "http://mochi.test:8888/");
+ yield* drop([[{type: "text/plain",
+ data: "http://mochi.test:8888/\nhttp://mochi.test:8888/b\nhttp://mochi.test:8888/c"}]],
+ "http://mochi.test:8888/|http://mochi.test:8888/b|http://mochi.test:8888/c");
+ yield dropInvalidURI();
+});
+
diff --git a/browser/base/content/test/general/browser_identity_UI.js b/browser/base/content/test/general/browser_identity_UI.js
new file mode 100644
index 000000000..5aacb2e79
--- /dev/null
+++ b/browser/base/content/test/general/browser_identity_UI.js
@@ -0,0 +1,146 @@
+/* Tests for correct behaviour of getEffectiveHost on identity handler */
+
+function test() {
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ ok(gIdentityHandler, "gIdentityHandler should exist");
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", true).then(() => {
+ gBrowser.selectedBrowser.addEventListener("load", checkResult, true);
+ nextTest();
+ });
+}
+
+// Greek IDN for 'example.test'.
+var idnDomain = "\u03C0\u03B1\u03C1\u03AC\u03B4\u03B5\u03B9\u03B3\u03BC\u03B1.\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE";
+var tests = [
+ {
+ name: "normal domain",
+ location: "http://test1.example.org/",
+ effectiveHost: "test1.example.org"
+ },
+ {
+ name: "view-source",
+ location: "view-source:http://example.com/",
+ effectiveHost: null
+ },
+ {
+ name: "normal HTTPS",
+ location: "https://example.com/",
+ effectiveHost: "example.com",
+ isHTTPS: true
+ },
+ {
+ name: "IDN subdomain",
+ location: "http://sub1." + idnDomain + "/",
+ effectiveHost: "sub1." + idnDomain
+ },
+ {
+ name: "subdomain with port",
+ location: "http://sub1.test1.example.org:8000/",
+ effectiveHost: "sub1.test1.example.org"
+ },
+ {
+ name: "subdomain HTTPS",
+ location: "https://test1.example.com/",
+ effectiveHost: "test1.example.com",
+ isHTTPS: true
+ },
+ {
+ name: "view-source HTTPS",
+ location: "view-source:https://example.com/",
+ effectiveHost: null,
+ isHTTPS: true
+ },
+ {
+ name: "IP address",
+ location: "http://127.0.0.1:8888/",
+ effectiveHost: "127.0.0.1"
+ },
+]
+
+var gCurrentTest, gCurrentTestIndex = -1, gTestDesc, gPopupHidden;
+// Go through the tests in both directions, to add additional coverage for
+// transitions between different states.
+var gForward = true;
+var gCheckETLD = false;
+function nextTest() {
+ if (!gCheckETLD) {
+ if (gForward)
+ gCurrentTestIndex++;
+ else
+ gCurrentTestIndex--;
+
+ if (gCurrentTestIndex == tests.length) {
+ // Went too far, reverse
+ gCurrentTestIndex--;
+ gForward = false;
+ }
+
+ if (gCurrentTestIndex == -1) {
+ gBrowser.selectedBrowser.removeEventListener("load", checkResult, true);
+ gBrowser.removeCurrentTab();
+ finish();
+ return;
+ }
+
+ gCurrentTest = tests[gCurrentTestIndex];
+ gTestDesc = "#" + gCurrentTestIndex + " (" + gCurrentTest.name + ")";
+ if (!gForward)
+ gTestDesc += " (second time)";
+ if (gCurrentTest.isHTTPS) {
+ gCheckETLD = true;
+ }
+
+ // Navigate to the next page, which will cause checkResult to fire.
+ let spec = gBrowser.selectedBrowser.currentURI.spec;
+ if (spec == "about:blank" || spec == gCurrentTest.location) {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.location);
+ } else {
+ // Open the Control Center and make sure it closes after nav (Bug 1207542).
+ let popupShown = promisePopupShown(gIdentityHandler._identityPopup);
+ gPopupHidden = promisePopupHidden(gIdentityHandler._identityPopup);
+ gIdentityHandler._identityBox.click();
+ info("Waiting for the Control Center to be shown");
+ popupShown.then(() => {
+ is_element_visible(gIdentityHandler._identityPopup, "Control Center is visible");
+ // Show the subview, which is an easy way in automation to reproduce
+ // Bug 1207542, where the CC wouldn't close on navigation.
+ gBrowser.ownerDocument.querySelector("#identity-popup-security-expander").click();
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.location);
+ });
+ }
+ } else {
+ gCheckETLD = false;
+ gTestDesc = "#" + gCurrentTestIndex + " (" + gCurrentTest.name + " without eTLD in identity icon label)";
+ if (!gForward)
+ gTestDesc += " (second time)";
+ gBrowser.selectedBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY);
+ }
+}
+
+function checkResult() {
+ // Sanity check other values, and the value of gIdentityHandler.getEffectiveHost()
+ is(gIdentityHandler._uri.spec, gCurrentTest.location, "location matches for test " + gTestDesc);
+ // getEffectiveHost can't be called for all modes
+ if (gCurrentTest.effectiveHost === null) {
+ let identityBox = document.getElementById("identity-box");
+ ok(identityBox.className == "unknownIdentity" ||
+ identityBox.className == "chromeUI", "mode matched");
+ } else {
+ is(gIdentityHandler.getEffectiveHost(), gCurrentTest.effectiveHost, "effectiveHost matches for test " + gTestDesc);
+ }
+
+ if (gPopupHidden) {
+ info("Waiting for the Control Center to hide");
+ gPopupHidden.then(() => {
+ gPopupHidden = null;
+ is_element_hidden(gIdentityHandler._identityPopup, "control center is hidden");
+ executeSoon(nextTest);
+ });
+ } else {
+ executeSoon(nextTest);
+ }
+}
diff --git a/browser/base/content/test/general/browser_insecureLoginForms.js b/browser/base/content/test/general/browser_insecureLoginForms.js
new file mode 100644
index 000000000..72db7dbe6
--- /dev/null
+++ b/browser/base/content/test/general/browser_insecureLoginForms.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Load directly from the browser-chrome support files of login tests.
+const TEST_URL_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
+
+/**
+ * Waits for the given number of occurrences of InsecureLoginFormsStateChange
+ * on the given browser element.
+ */
+function waitForInsecureLoginFormsStateChange(browser, count) {
+ return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
+ false, () => --count == 0);
+}
+
+/**
+ * Checks the insecure login forms logic for the identity block.
+ */
+add_task(function* test_simple() {
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({
+ "set": [["security.insecure_password.ui.enabled", true]],
+ }, resolve));
+
+ for (let [origin, expectWarning] of [
+ ["http://example.com", true],
+ ["http://127.0.0.1", false],
+ ["https://example.com", false],
+ ]) {
+ let testUrlPath = origin + TEST_URL_PATH;
+ let tab = gBrowser.addTab(testUrlPath + "form_basic.html");
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ gIdentityHandler._identityBox.click();
+ document.getElementById("identity-popup-security-expander").click();
+
+ if (expectWarning) {
+ is_element_visible(document.getElementById("connection-icon"));
+ let connectionIconImage = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("connection-icon"), "")
+ .getPropertyValue("list-style-image");
+ let securityViewBG = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("identity-popup-securityView"), "")
+ .getPropertyValue("background-image");
+ let securityContentBG = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("identity-popup-security-content"), "")
+ .getPropertyValue("background-image");
+ is(connectionIconImage,
+ "url(\"chrome://browser/skin/connection-mixed-active-loaded.svg#icon\")",
+ "Using expected icon image in the identity block");
+ is(securityViewBG,
+ "url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
+ "Using expected icon image in the Control Center main view");
+ is(securityContentBG,
+ "url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
+ "Using expected icon image in the Control Center subview");
+ is(Array.filter(document.querySelectorAll("[observes=identity-popup-insecure-login-forms-learn-more]"),
+ element => !is_hidden(element)).length, 1,
+ "The 'Learn more' link should be visible once.");
+ }
+
+ // Messages should be visible when the scheme is HTTP, and invisible when
+ // the scheme is HTTPS.
+ is(Array.every(document.querySelectorAll("[when-loginforms=insecure]"),
+ element => !is_hidden(element)),
+ expectWarning,
+ "The relevant messages should be visible or hidden.");
+
+ gIdentityHandler._identityPopup.hidden = true;
+ gBrowser.removeTab(tab);
+ }
+});
+
+/**
+ * Checks that the insecure login forms logic does not regress mixed content
+ * blocking messages when mixed active content is loaded.
+ */
+add_task(function* test_mixedcontent() {
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({
+ "set": [["security.mixed_content.block_active_content", false]],
+ }, resolve));
+
+ // Load the page with the subframe in a new tab.
+ let testUrlPath = "://example.com" + TEST_URL_PATH;
+ let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html");
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // Two events are triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 3),
+ ]);
+
+ assertMixedContentBlockingState(browser, { activeLoaded: true,
+ activeBlocked: false,
+ passiveLoaded: false });
+
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Checks that insecure window.opener does not trigger a warning.
+ */
+add_task(function* test_ignoring_window_opener() {
+ let newTabURL = "https://example.com" + TEST_URL_PATH + "form_basic.html";
+ let path = getRootDirectory(gTestPath)
+ .replace("chrome://mochitests/content", "http://example.com");
+ let url = path + "insecure_opener.html";
+
+ yield BrowserTestUtils.withNewTab(url, function*(browser) {
+ // Clicking the link will spawn a new tab.
+ let loaded = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL);
+ yield ContentTask.spawn(browser, {}, function() {
+ content.document.getElementById("link").click();
+ });
+ let tab = yield loaded;
+ browser = tab.linkedBrowser;
+ yield waitForInsecureLoginFormsStateChange(browser, 2);
+
+ // Open the identity popup.
+ let { gIdentityHandler } = gBrowser.ownerGlobal;
+ gIdentityHandler._identityBox.click();
+ document.getElementById("identity-popup-security-expander").click();
+
+ ok(is_visible(document.getElementById("connection-icon")),
+ "Connection icon is visible");
+
+ // Assert that the identity indicators are still "secure".
+ let connectionIconImage = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("connection-icon"))
+ .getPropertyValue("list-style-image");
+ let securityViewBG = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("identity-popup-securityView"))
+ .getPropertyValue("background-image");
+ let securityContentBG = gBrowser.ownerGlobal
+ .getComputedStyle(document.getElementById("identity-popup-security-content"))
+ .getPropertyValue("background-image");
+ is(connectionIconImage,
+ "url(\"chrome://browser/skin/connection-secure.svg\")",
+ "Using expected icon image in the identity block");
+ is(securityViewBG,
+ "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-secure\")",
+ "Using expected icon image in the Control Center main view");
+ is(securityContentBG,
+ "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-secure\")",
+ "Using expected icon image in the Control Center subview");
+
+ ok(Array.every(document.querySelectorAll("[when-loginforms=insecure]"),
+ element => is_hidden(element)),
+ "All messages should be hidden.");
+
+ gIdentityHandler._identityPopup.hidden = true;
+
+ yield BrowserTestUtils.removeTab(tab);
+ });
+});
diff --git a/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js b/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js
new file mode 100644
index 000000000..8e69e781b
--- /dev/null
+++ b/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js
@@ -0,0 +1,39 @@
+"use strict";
+
+
+/**
+ * Verify that loading an invalid URI does not clobber a previously-loaded page's history
+ * entry, but that the invalid URI gets its own history entry instead. We're checking this
+ * using nsIWebNavigation's canGoBack, as well as actually going back and then checking
+ * canGoForward.
+ */
+add_task(function* checkBackFromInvalidURI() {
+ yield pushPrefs(["keyword.enabled", false]);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots", true);
+ gURLBar.value = "::2600";
+ gURLBar.focus();
+
+ let promiseErrorPageLoaded = new Promise(resolve => {
+ tab.linkedBrowser.addEventListener("DOMContentLoaded", function onLoad() {
+ tab.linkedBrowser.removeEventListener("DOMContentLoaded", onLoad, false, true);
+ resolve();
+ }, false, true);
+ });
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield promiseErrorPageLoaded;
+
+ ok(gBrowser.webNavigation.canGoBack, "Should be able to go back");
+ if (gBrowser.webNavigation.canGoBack) {
+ // Can't use DOMContentLoaded here because the page is bfcached. Can't use pageshow for
+ // the error page because it doesn't seem to fire for those.
+ let promiseOtherPageLoaded = BrowserTestUtils.waitForEvent(tab.linkedBrowser, "pageshow", false,
+ // Be paranoid we *are* actually seeing this other page load, not some kind of race
+ // for if/when we do start firing pageshow for the error page...
+ function(e) { return gBrowser.currentURI.spec == "about:robots" }
+ );
+ gBrowser.goBack();
+ yield promiseOtherPageLoaded;
+ ok(gBrowser.webNavigation.canGoForward, "Should be able to go forward from previous page.");
+ }
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_keywordBookmarklets.js b/browser/base/content/test/general/browser_keywordBookmarklets.js
new file mode 100644
index 000000000..5e94733fe
--- /dev/null
+++ b/browser/base/content/test/general/browser_keywordBookmarklets.js
@@ -0,0 +1,54 @@
+"use strict"
+
+add_task(function* test_keyword_bookmarklet() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmarklet",
+ url: "javascript:'1';" });
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ registerCleanupFunction (function* () {
+ gBrowser.removeTab(tab);
+ yield PlacesUtils.bookmarks.remove(bm);
+ });
+ yield promisePageShow();
+ let originalPrincipal = gBrowser.contentPrincipal;
+
+ function getPrincipalURI() {
+ return ContentTask.spawn(tab.linkedBrowser, null, function() {
+ return content.document.nodePrincipal.URI.spec;
+ });
+ }
+
+ let originalPrincipalURI = yield getPrincipalURI();
+
+ yield PlacesUtils.keywords.insert({ keyword: "bm", url: "javascript:'1';" })
+
+ // Enter bookmarklet keyword in the URL bar
+ gURLBar.value = "bm";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ yield promisePageShow();
+
+ let newPrincipalURI = yield getPrincipalURI();
+ is(newPrincipalURI, originalPrincipalURI, "content has the same principal");
+
+ // In e10s, null principals don't round-trip so the same null principal sent
+ // from the child will be a new null principal. Verify that this is the
+ // case.
+ if (tab.linkedBrowser.isRemoteBrowser) {
+ ok(originalPrincipal.isNullPrincipal && gBrowser.contentPrincipal.isNullPrincipal,
+ "both principals should be null principals in the parent");
+ } else {
+ ok(gBrowser.contentPrincipal.equals(originalPrincipal),
+ "javascript bookmarklet should inherit principal");
+ }
+});
+
+function* promisePageShow() {
+ return new Promise(resolve => {
+ gBrowser.selectedBrowser.addEventListener("pageshow", function listen() {
+ gBrowser.selectedBrowser.removeEventListener("pageshow", listen);
+ resolve();
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_keywordSearch.js b/browser/base/content/test/general/browser_keywordSearch.js
new file mode 100644
index 000000000..cf8bd0c0e
--- /dev/null
+++ b/browser/base/content/test/general/browser_keywordSearch.js
@@ -0,0 +1,88 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ **/
+
+var gTests = [
+ {
+ name: "normal search (search service)",
+ testText: "test search",
+ searchURL: Services.search.defaultEngine.getSubmission("test search", null, "keyword").uri.spec
+ },
+ {
+ name: "?-prefixed search (search service)",
+ testText: "? foo ",
+ searchURL: Services.search.defaultEngine.getSubmission("foo", null, "keyword").uri.spec
+ }
+];
+
+function test() {
+ waitForExplicitFinish();
+
+ let windowObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ ok(false, "Alert window opened");
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ win.addEventListener("load", function() {
+ win.removeEventListener("load", arguments.callee, false);
+ win.close();
+ }, false);
+ executeSoon(finish);
+ }
+ }
+ };
+
+ Services.ww.registerNotification(windowObserver);
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let listener = {
+ onStateChange: function onLocationChange(webProgress, req, flags, status) {
+ // Only care about document starts
+ let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+ Ci.nsIWebProgressListener.STATE_START;
+ if (!(flags & docStart))
+ return;
+
+ info("received document start");
+
+ ok(req instanceof Ci.nsIChannel, "req is a channel");
+ is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
+ info("Actual URI: " + req.URI.spec);
+
+ req.cancel(Components.results.NS_ERROR_FAILURE);
+
+ executeSoon(nextTest);
+ }
+ };
+ gBrowser.addProgressListener(listener);
+
+ registerCleanupFunction(function () {
+ Services.ww.unregisterNotification(windowObserver);
+
+ gBrowser.removeProgressListener(listener);
+ gBrowser.removeTab(tab);
+ });
+
+ nextTest();
+}
+
+var gCurrTest;
+function nextTest() {
+ if (gTests.length) {
+ gCurrTest = gTests.shift();
+ doTest();
+ } else {
+ finish();
+ }
+}
+
+function doTest() {
+ info("Running test: " + gCurrTest.name);
+
+ // Simulate a user entering search terms
+ gURLBar.value = gCurrTest.testText;
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+}
diff --git a/browser/base/content/test/general/browser_keywordSearch_postData.js b/browser/base/content/test/general/browser_keywordSearch_postData.js
new file mode 100644
index 000000000..3f700fa58
--- /dev/null
+++ b/browser/base/content/test/general/browser_keywordSearch_postData.js
@@ -0,0 +1,94 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ **/
+
+var gTests = [
+ {
+ name: "normal search (search service)",
+ testText: "test search",
+ expectText: "test+search"
+ },
+ {
+ name: "?-prefixed search (search service)",
+ testText: "? foo ",
+ expectText: "foo"
+ }
+];
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let searchObserver = function search_observer(aSubject, aTopic, aData) {
+ let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+ info("Observer: " + aData + " for " + engine.name);
+
+ if (aData != "engine-added")
+ return;
+
+ if (engine.name != "POST Search")
+ return;
+
+ Services.search.defaultEngine = engine;
+
+ registerCleanupFunction(function () {
+ Services.search.removeEngine(engine);
+ });
+
+ // ready to execute the tests!
+ executeSoon(nextTest);
+ };
+
+ Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false);
+
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+
+ Services.obs.removeObserver(searchObserver, "browser-search-engine-modified");
+ });
+
+ Services.search.addEngine("http://test:80/browser/browser/base/content/test/general/POSTSearchEngine.xml",
+ null, null, false);
+}
+
+var gCurrTest;
+function nextTest() {
+ if (gTests.length) {
+ gCurrTest = gTests.shift();
+ doTest();
+ } else {
+ finish();
+ }
+}
+
+function doTest() {
+ info("Running test: " + gCurrTest.name);
+
+ waitForLoad(function () {
+ let loadedText = gBrowser.contentDocument.body.textContent;
+ ok(loadedText, "search page loaded");
+ let needle = "searchterms=" + gCurrTest.expectText;
+ is(loadedText, needle, "The query POST data should be returned in the response");
+ nextTest();
+ });
+
+ // Simulate a user entering search terms
+ gURLBar.value = gCurrTest.testText;
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+}
+
+
+function waitForLoad(cb) {
+ let browser = gBrowser.selectedBrowser;
+ browser.addEventListener("load", function listener() {
+ if (browser.currentURI.spec == "about:blank")
+ return;
+ info("Page loaded: " + browser.currentURI.spec);
+ browser.removeEventListener("load", listener, true);
+
+ cb();
+ }, true);
+}
diff --git a/browser/base/content/test/general/browser_lastAccessedTab.js b/browser/base/content/test/general/browser_lastAccessedTab.js
new file mode 100644
index 000000000..57bd330ae
--- /dev/null
+++ b/browser/base/content/test/general/browser_lastAccessedTab.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// gBrowser.selectedTab.lastAccessed and Date.now() called from this test can't
+// run concurrently, and therefore don't always match exactly.
+const CURRENT_TIME_TOLERANCE_MS = 15;
+
+function isCurrent(tab, msg) {
+ const DIFF = Math.abs(Date.now() - tab.lastAccessed);
+ ok(DIFF <= CURRENT_TIME_TOLERANCE_MS, msg + " (difference: " + DIFF + ")");
+}
+
+function nextStep(fn) {
+ setTimeout(fn, CURRENT_TIME_TOLERANCE_MS + 10);
+}
+
+var originalTab;
+var newTab;
+
+function test() {
+ waitForExplicitFinish();
+
+ originalTab = gBrowser.selectedTab;
+ nextStep(step2);
+}
+
+function step2() {
+ isCurrent(originalTab, "selected tab has the current timestamp");
+ newTab = gBrowser.addTab("about:blank", {skipAnimation: true});
+ nextStep(step3);
+}
+
+function step3() {
+ ok(newTab.lastAccessed < Date.now(), "new tab hasn't been selected so far");
+ gBrowser.selectedTab = newTab;
+ isCurrent(newTab, "new tab has the current timestamp after being selected");
+ nextStep(step4);
+}
+
+function step4() {
+ ok(originalTab.lastAccessed < Date.now(),
+ "original tab has old timestamp after being deselected");
+ isCurrent(newTab, "new tab has the current timestamp since it's still selected");
+
+ gBrowser.removeTab(newTab);
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_mcb_redirect.js b/browser/base/content/test/general/browser_mcb_redirect.js
new file mode 100644
index 000000000..41b4e9468
--- /dev/null
+++ b/browser/base/content/test/general/browser_mcb_redirect.js
@@ -0,0 +1,314 @@
+/*
+ * Description of the Tests for
+ * - Bug 418354 - Call Mixed content blocking on redirects
+ *
+ * Single redirect script tests
+ * 1. Load a script over https inside an https page
+ * - the server responds with a 302 redirect to a >> HTTP << script
+ * - the doorhanger should appear!
+ *
+ * 2. Load a script over https inside an http page
+ * - the server responds with a 302 redirect to a >> HTTP << script
+ * - the doorhanger should not appear!
+ *
+ * Single redirect image tests
+ * 3. Load an image over https inside an https page
+ * - the server responds with a 302 redirect to a >> HTTP << image
+ * - the image should not load
+ *
+ * 4. Load an image over https inside an http page
+ * - the server responds with a 302 redirect to a >> HTTP << image
+ * - the image should load and get cached
+ *
+ * Single redirect cached image tests
+ * 5. Using offline mode to ensure we hit the cache, load a cached image over
+ * https inside an http page
+ * - the server would have responded with a 302 redirect to a >> HTTP <<
+ * image, but instead we try to use the cached image.
+ * - the image should load
+ *
+ * 6. Using offline mode to ensure we hit the cache, load a cached image over
+ * https inside an https page
+ * - the server would have responded with a 302 redirect to a >> HTTP <<
+ * image, but instead we try to use the cached image.
+ * - the image should not load
+ *
+ * Double redirect image test
+ * 7. Load an image over https inside an http page
+ * - the server responds with a 302 redirect to a >> HTTP << server
+ * - the HTTP server responds with a 302 redirect to a >> HTTPS << image
+ * - the image should load and get cached
+ *
+ * Double redirect cached image tests
+ * 8. Using offline mode to ensure we hit the cache, load a cached image over
+ * https inside an http page
+ * - the image would have gone through two redirects: HTTPS->HTTP->HTTPS,
+ * but instead we try to use the cached image.
+ * - the image should load
+ *
+ * 9. Using offline mode to ensure we hit the cache, load a cached image over
+ * https inside an https page
+ * - the image would have gone through two redirects: HTTPS->HTTP->HTTPS,
+ * but instead we try to use the cached image.
+ * - the image should not load
+ */
+
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
+const gHttpsTestRoot = "https://example.com/browser/browser/base/content/test/general/";
+const gHttpTestRoot = "http://example.com/browser/browser/base/content/test/general/";
+
+var origBlockActive;
+var origBlockDisplay;
+var gTestBrowser = null;
+
+// ------------------------ Helper Functions ---------------------
+
+registerCleanupFunction(function() {
+ // Set preferences back to their original values
+ Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive);
+ Services.prefs.setBoolPref(PREF_DISPLAY, origBlockDisplay);
+
+ // Make sure we are online again
+ Services.io.offline = false;
+});
+
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+ finish();
+}
+
+function waitForCondition(condition, nextTest, errorMsg, okMsg) {
+ var tries = 0;
+ var interval = setInterval(function() {
+ if (tries >= 30) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ if (condition()) {
+ ok(true, okMsg)
+ moveOn();
+ }
+ tries++;
+ }, 500);
+ var moveOn = function() {
+ clearInterval(interval); nextTest();
+ };
+}
+
+// ------------------------ Test 1 ------------------------------
+
+function test1() {
+ gTestBrowser.addEventListener("load", checkUIForTest1, true);
+ var url = gHttpsTestRoot + "test_mcb_redirect.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkUIForTest1() {
+ gTestBrowser.removeEventListener("load", checkUIForTest1, true);
+
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+
+ var expected = "script blocked";
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test2, "Error: Waited too long for status in Test 1!",
+ "OK: Expected result in innerHTML for Test1!");
+}
+
+// ------------------------ Test 2 ------------------------------
+
+function test2() {
+ gTestBrowser.addEventListener("load", checkUIForTest2, true);
+ var url = gHttpTestRoot + "test_mcb_redirect.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkUIForTest2() {
+ gTestBrowser.removeEventListener("load", checkUIForTest2, true);
+
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: false, passiveLoaded: false});
+
+ var expected = "script executed";
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test3, "Error: Waited too long for status in Test 2!",
+ "OK: Expected result in innerHTML for Test2!");
+}
+
+// ------------------------ Test 3 ------------------------------
+// HTTPS page loading insecure image
+function test3() {
+ gTestBrowser.addEventListener("load", checkLoadEventForTest3, true);
+ var url = gHttpsTestRoot + "test_mcb_redirect_image.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest3() {
+ gTestBrowser.removeEventListener("load", checkLoadEventForTest3, true);
+
+ var expected = "image blocked"
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test4, "Error: Waited too long for status in Test 3!",
+ "OK: Expected result in innerHTML for Test3!");
+}
+
+// ------------------------ Test 4 ------------------------------
+// HTTP page loading insecure image
+function test4() {
+ gTestBrowser.addEventListener("load", checkLoadEventForTest4, true);
+ var url = gHttpTestRoot + "test_mcb_redirect_image.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest4() {
+ gTestBrowser.removeEventListener("load", checkLoadEventForTest4, true);
+
+ var expected = "image loaded"
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test5, "Error: Waited too long for status in Test 4!",
+ "OK: Expected result in innerHTML for Test4!");
+}
+
+// ------------------------ Test 5 ------------------------------
+// HTTP page laoding insecure cached image
+// Assuming test 4 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test5() {
+ gTestBrowser.addEventListener("load", checkLoadEventForTest5, true);
+ // Go into offline mode
+ Services.io.offline = true;
+ var url = gHttpTestRoot + "test_mcb_redirect_image.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest5() {
+ gTestBrowser.removeEventListener("load", checkLoadEventForTest5, true);
+
+ var expected = "image loaded"
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test6, "Error: Waited too long for status in Test 5!",
+ "OK: Expected result in innerHTML for Test5!");
+ // Go back online
+ Services.io.offline = false;
+}
+
+// ------------------------ Test 6 ------------------------------
+// HTTPS page loading insecure cached image
+// Assuming test 4 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test6() {
+ gTestBrowser.addEventListener("load", checkLoadEventForTest6, true);
+ // Go into offline mode
+ Services.io.offline = true;
+ var url = gHttpsTestRoot + "test_mcb_redirect_image.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest6() {
+ gTestBrowser.removeEventListener("load", checkLoadEventForTest6, true);
+
+ var expected = "image blocked"
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test7, "Error: Waited too long for status in Test 6!",
+ "OK: Expected result in innerHTML for Test6!");
+ // Go back online
+ Services.io.offline = false;
+}
+
+// ------------------------ Test 7 ------------------------------
+// HTTP page loading insecure image that went through a double redirect
+function test7() {
+ gTestBrowser.addEventListener("load", checkLoadEventForTest7, true);
+ var url = gHttpTestRoot + "test_mcb_double_redirect_image.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest7() {
+ gTestBrowser.removeEventListener("load", checkLoadEventForTest7, true);
+
+ var expected = "image loaded"
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test8, "Error: Waited too long for status in Test 7!",
+ "OK: Expected result in innerHTML for Test7!");
+}
+
+// ------------------------ Test 8 ------------------------------
+// HTTP page loading insecure cached image that went through a double redirect
+// Assuming test 7 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test8() {
+ gTestBrowser.addEventListener("load", checkLoadEventForTest8, true);
+ // Go into offline mode
+ Services.io.offline = true;
+ var url = gHttpTestRoot + "test_mcb_double_redirect_image.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest8() {
+ gTestBrowser.removeEventListener("load", checkLoadEventForTest8, true);
+
+ var expected = "image loaded"
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ test9, "Error: Waited too long for status in Test 8!",
+ "OK: Expected result in innerHTML for Test8!");
+ // Go back online
+ Services.io.offline = false;
+}
+
+// ------------------------ Test 9 ------------------------------
+// HTTPS page loading insecure cached image that went through a double redirect
+// Assuming test 7 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test9() {
+ gTestBrowser.addEventListener("load", checkLoadEventForTest9, true);
+ // Go into offline mode
+ Services.io.offline = true;
+ var url = gHttpsTestRoot + "test_mcb_double_redirect_image.html"
+ gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest9() {
+ gTestBrowser.removeEventListener("load", checkLoadEventForTest9, true);
+
+ var expected = "image blocked"
+ waitForCondition(
+ () => content.document.getElementById('mctestdiv').innerHTML == expected,
+ cleanUpAfterTests, "Error: Waited too long for status in Test 9!",
+ "OK: Expected result in innerHTML for Test9!");
+ // Go back online
+ Services.io.offline = false;
+}
+
+// ------------------------ SETUP ------------------------------
+
+function test() {
+ // Performing async calls, e.g. 'onload', we have to wait till all of them finished
+ waitForExplicitFinish();
+
+ // Store original preferences so we can restore settings after testing
+ origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE);
+ origBlockDisplay = Services.prefs.getBoolPref(PREF_DISPLAY);
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+ Services.prefs.setBoolPref(PREF_DISPLAY, true);
+
+ pushPrefs(["dom.ipc.processCount", 1]).then(() => {
+ var newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ newTab.linkedBrowser.stop();
+
+ executeSoon(test1);
+ });
+}
diff --git a/browser/base/content/test/general/browser_menuButtonBadgeManager.js b/browser/base/content/test/general/browser_menuButtonBadgeManager.js
new file mode 100644
index 000000000..9afe39ab7
--- /dev/null
+++ b/browser/base/content/test/general/browser_menuButtonBadgeManager.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/. */
+
+var menuButton = document.getElementById("PanelUI-menu-button");
+
+add_task(function* testButtonActivities() {
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
+
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
+ is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-succeeded");
+ is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
+
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-failed");
+ is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, "download-severe");
+ is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
+
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, "download-warning");
+ is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
+
+ gMenuButtonBadgeManager.addBadge("unknownbadge", "attr");
+ is(menuButton.getAttribute("badge-status"), "download-warning", "Should not have changed badge status");
+
+ gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
+ is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+ gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE);
+ is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+ gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+
+ yield PanelUI.show();
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
+ PanelUI.hide();
+
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
+ gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-succeeded");
+ gMenuButtonBadgeManager.clearBadges();
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (clearBadges called)");
+});
diff --git a/browser/base/content/test/general/browser_menuButtonFitts.js b/browser/base/content/test/general/browser_menuButtonFitts.js
new file mode 100644
index 000000000..e2541b925
--- /dev/null
+++ b/browser/base/content/test/general/browser_menuButtonFitts.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test () {
+ waitForExplicitFinish();
+ window.maximize();
+
+ // Find where the nav-bar is vertically.
+ var navBar = document.getElementById("nav-bar");
+ var boundingRect = navBar.getBoundingClientRect();
+ var yPixel = boundingRect.top + Math.floor(boundingRect.height / 2);
+ var xPixel = boundingRect.width - 1; // Use the last pixel of the screen since it is maximized.
+
+ function onPopupHidden() {
+ PanelUI.panel.removeEventListener("popuphidden", onPopupHidden);
+ window.restore();
+ finish();
+ }
+ function onPopupShown() {
+ PanelUI.panel.removeEventListener("popupshown", onPopupShown);
+ ok(true, "Clicking at the far edge of the window opened the menu popup.");
+ PanelUI.panel.addEventListener("popuphidden", onPopupHidden);
+ PanelUI.hide();
+ }
+ registerCleanupFunction(function() {
+ PanelUI.panel.removeEventListener("popupshown", onPopupShown);
+ PanelUI.panel.removeEventListener("popuphidden", onPopupHidden);
+ });
+ PanelUI.panel.addEventListener("popupshown", onPopupShown);
+ EventUtils.synthesizeMouseAtPoint(xPixel, yPixel, {}, window);
+}
diff --git a/browser/base/content/test/general/browser_middleMouse_noJSPaste.js b/browser/base/content/test/general/browser_middleMouse_noJSPaste.js
new file mode 100644
index 000000000..fa0c26f78
--- /dev/null
+++ b/browser/base/content/test/general/browser_middleMouse_noJSPaste.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const middleMousePastePref = "middlemouse.contentLoadURL";
+const autoScrollPref = "general.autoScroll";
+
+add_task(function* () {
+ yield pushPrefs([middleMousePastePref, true], [autoScrollPref, false]);
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let url = "javascript:http://www.example.com/";
+ yield new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(url, () => {
+ Components.classes["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Components.interfaces.nsIClipboardHelper)
+ .copyString(url);
+ }, resolve, () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ });
+ });
+
+ let middlePagePromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Middle click on the content area
+ info("Middle clicking");
+ yield BrowserTestUtils.synthesizeMouse(null, 10, 10, {button: 1}, gBrowser.selectedBrowser);
+ yield middlePagePromise;
+
+ is(gBrowser.currentURI.spec, url.replace(/^javascript:/, ""), "url loaded by middle click doesn't include JS");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_minimize.js b/browser/base/content/test/general/browser_minimize.js
new file mode 100644
index 000000000..1d761c0da
--- /dev/null
+++ b/browser/base/content/test/general/browser_minimize.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function *() {
+ registerCleanupFunction(function() {
+ window.restore();
+ });
+ function waitForActive() { return gBrowser.selectedTab.linkedBrowser.docShellIsActive; }
+ function waitForInactive() { return !gBrowser.selectedTab.linkedBrowser.docShellIsActive; }
+ yield promiseWaitForCondition(waitForActive);
+ is(gBrowser.selectedTab.linkedBrowser.docShellIsActive, true, "Docshell should be active");
+ window.minimize();
+ yield promiseWaitForCondition(waitForInactive);
+ is(gBrowser.selectedTab.linkedBrowser.docShellIsActive, false, "Docshell should be Inactive");
+ window.restore();
+ yield promiseWaitForCondition(waitForActive);
+ is(gBrowser.selectedTab.linkedBrowser.docShellIsActive, true, "Docshell should be active again");
+});
diff --git a/browser/base/content/test/general/browser_misused_characters_in_strings.js b/browser/base/content/test/general/browser_misused_characters_in_strings.js
new file mode 100644
index 000000000..fe8022662
--- /dev/null
+++ b/browser/base/content/test/general/browser_misused_characters_in_strings.js
@@ -0,0 +1,244 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' issues to remain, while we
+ * detect newly occurring issues in shipping files. It is a list of objects
+ * specifying conditions under which an error should be ignored.
+ *
+ * As each issue is found in the whitelist, it is removed from the list. At
+ * the end of the test, there is an assertion that all items have been
+ * removed from the whitelist, thus ensuring there are no stale entries. */
+let gWhitelist = [{
+ file: "search.properties",
+ key: "searchForSomethingWith",
+ type: "single-quote"
+ }, {
+ file: "netError.dtd",
+ key: "certerror.introPara",
+ type: "single-quote"
+ }, {
+ file: "netError.dtd",
+ key: "weakCryptoAdvanced.longDesc",
+ type: "single-quote"
+ }, {
+ file: "netError.dtd",
+ key: "weakCryptoAdvanced.override",
+ type: "single-quote"
+ }, {
+ file: "netError.dtd",
+ key: "inadequateSecurityError.longDesc",
+ type: "single-quote"
+ }, {
+ file: "netError.dtd",
+ key: "certerror.wrongSystemTime",
+ type: "single-quote"
+ }, {
+ file: "phishing-afterload-warning-message.dtd",
+ key: "safeb.blocked.malwarePage.shortDesc",
+ type: "single-quote"
+ }, {
+ file: "phishing-afterload-warning-message.dtd",
+ key: "safeb.blocked.unwantedPage.shortDesc",
+ type: "single-quote"
+ }, {
+ file: "phishing-afterload-warning-message.dtd",
+ key: "safeb.blocked.phishingPage.shortDesc2",
+ type: "single-quote"
+ }, {
+ file: "mathfont.properties",
+ key: "operator.\\u002E\\u002E\\u002E.postfix",
+ type: "ellipsis"
+ }, {
+ file: "layout_errors.properties",
+ key: "ImageMapRectBoundsError",
+ type: "double-quote"
+ }, {
+ file: "layout_errors.properties",
+ key: "ImageMapCircleWrongNumberOfCoords",
+ type: "double-quote"
+ }, {
+ file: "layout_errors.properties",
+ key: "ImageMapCircleNegativeRadius",
+ type: "double-quote"
+ }, {
+ file: "layout_errors.properties",
+ key: "ImageMapPolyWrongNumberOfCoords",
+ type: "double-quote"
+ }, {
+ file: "layout_errors.properties",
+ key: "ImageMapPolyOddNumberOfCoords",
+ type: "double-quote"
+ }, {
+ file: "xbl.properties",
+ key: "CommandNotInChrome",
+ type: "double-quote"
+ }, {
+ file: "dom.properties",
+ key: "PatternAttributeCompileFailure",
+ type: "single-quote"
+ }, {
+ file: "pipnss.properties",
+ key: "certErrorMismatchSingle2",
+ type: "double-quote"
+ }, {
+ file: "pipnss.properties",
+ key: "certErrorCodePrefix2",
+ type: "double-quote"
+ }, {
+ file: "aboutSupport.dtd",
+ key: "aboutSupport.pageSubtitle",
+ type: "single-quote"
+ }, {
+ file: "aboutSupport.dtd",
+ key: "aboutSupport.userJSDescription",
+ type: "single-quote"
+ }, {
+ file: "netError.dtd",
+ key: "inadequateSecurityError.longDesc",
+ type: "single-quote"
+ }, {
+ file: "netErrorApp.dtd",
+ key: "securityOverride.warningContent",
+ type: "single-quote"
+ }, {
+ file: "pocket.properties",
+ key: "tos",
+ type: "double-quote"
+ }, {
+ file: "pocket.properties",
+ key: "tos",
+ type: "apostrophe"
+ }, {
+ file: "aboutNetworking.dtd",
+ key: "aboutNetworking.logTutorial",
+ type: "single-quote"
+ }
+];
+
+var moduleLocation = gTestPath.replace(/\/[^\/]*$/i, "/parsingTestHelpers.jsm");
+var {generateURIsFromDirTree} = Cu.import(moduleLocation, {});
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in gWhitelist.
+ *
+ * @param filepath The URI spec of the locale file
+ * @param key The key of the entity that is being checked
+ * @param type The type of error that has been found
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(filepath, key, type) {
+ for (let index in gWhitelist) {
+ let whitelistItem = gWhitelist[index];
+ if (filepath.endsWith(whitelistItem.file) &&
+ key == whitelistItem.key &&
+ type == whitelistItem.type) {
+ gWhitelist.splice(index, 1);
+ return true;
+ }
+ }
+ return false;
+}
+
+function fetchFile(uri) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, true);
+ xhr.onreadystatechange = function() {
+ if (this.readyState != this.DONE) {
+ return;
+ }
+ try {
+ resolve(this.responseText);
+ } catch (ex) {
+ ok(false, `Script error reading ${uri}: ${ex}`);
+ resolve("");
+ }
+ };
+ xhr.onerror = error => {
+ ok(false, `XHR error reading ${uri}: ${error}`);
+ resolve("");
+ };
+ xhr.send(null);
+ });
+}
+
+function testForError(filepath, key, str, pattern, type, helpText) {
+ if (str.match(pattern) &&
+ !ignoredError(filepath, key, type)) {
+ ok(false, `${filepath} with key=${key} has a misused ${type}. ${helpText}`);
+ }
+}
+
+function testForErrors(filepath, key, str) {
+ testForError(filepath, key, str, /\w'\w/, "apostrophe", "Strings with apostrophes should use foo\u2019s instead of foo's.");
+ testForError(filepath, key, str, /\w\u2018\w/, "incorrect-apostrophe", "Strings with apostrophes should use foo\u2019s instead of foo\u2018s.");
+ testForError(filepath, key, str, /'.+'/, "single-quote", "Single-quoted strings should use Unicode \u2018foo\u2019 instead of 'foo'.");
+ testForError(filepath, key, str, /"/, "double-quote", "Double-quoted strings should use Unicode \u201cfoo\u201d instead of \"foo\".");
+ testForError(filepath, key, str, /\.\.\./, "ellipsis", "Strings with an ellipsis should use the Unicode \u2026 character instead of three periods.");
+}
+
+function* getAllTheFiles(extension) {
+ let appDirGreD = Services.dirsvc.get("GreD", Ci.nsIFile);
+ let appDirXCurProcD = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ if (appDirGreD.contains(appDirXCurProcD)) {
+ return yield generateURIsFromDirTree(appDirGreD, [extension]);
+ }
+ if (appDirXCurProcD.contains(appDirGreD)) {
+ return yield generateURIsFromDirTree(appDirXCurProcD, [extension]);
+ }
+ let urisGreD = yield generateURIsFromDirTree(appDirGreD, [extension]);
+ let urisXCurProcD = yield generateURIsFromDirTree(appDirXCurProcD, [extension]);
+ return Array.from(new Set(urisGreD.concat(appDirXCurProcD)));
+}
+
+add_task(function* checkAllTheProperties() {
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let uris = yield getAllTheFiles(".properties");
+ ok(uris.length, `Found ${uris.length} .properties files to scan for misused characters`);
+
+ for (let uri of uris) {
+ let bundle = new StringBundle(uri.spec);
+ let entities = bundle.getAll();
+ for (let entity of entities) {
+ testForErrors(uri.spec, entity.key, entity.value);
+ }
+ }
+});
+
+var checkDTD = Task.async(function* (aURISpec) {
+ let rawContents = yield fetchFile(aURISpec);
+ // The regular expression below is adapted from:
+ // https://hg.mozilla.org/mozilla-central/file/68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8/python/compare-locales/compare_locales/parser.py#l233
+ let entities = rawContents.match(/<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/g);
+ if (!entities) {
+ // Some files, such as requestAutocomplete.dtd, have no entities defined.
+ return;
+ }
+ for (let entity of entities) {
+ let [, key, str] = entity.match(/<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/);
+ // The matched string includes the enclosing quotation marks,
+ // we need to slice them off.
+ str = str.slice(1, -1);
+ testForErrors(aURISpec, key, str);
+ }
+});
+
+add_task(function* checkAllTheDTDs() {
+ let uris = yield getAllTheFiles(".dtd");
+ ok(uris.length, `Found ${uris.length} .dtd files to scan for misused characters`);
+ for (let uri of uris) {
+ yield checkDTD(uri.spec);
+ }
+
+ // This support DTD file supplies a string with a newline to make sure
+ // the regex in checkDTD works correctly for that case.
+ let dtdLocation = gTestPath.replace(/\/[^\/]*$/i, "/bug1262648_string_with_newlines.dtd");
+ yield checkDTD(dtdLocation);
+});
+
+add_task(function* ensureWhiteListIsEmpty() {
+ is(gWhitelist.length, 0, "No remaining whitelist entries exist");
+});
diff --git a/browser/base/content/test/general/browser_mixedContentFramesOnHttp.js b/browser/base/content/test/general/browser_mixedContentFramesOnHttp.js
new file mode 100644
index 000000000..ac19efd05
--- /dev/null
+++ b/browser/base/content/test/general/browser_mixedContentFramesOnHttp.js
@@ -0,0 +1,34 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Test for Bug 1182551 -
+ *
+ * This test has a top level HTTP page with an HTTPS iframe. The HTTPS iframe
+ * includes an HTTP image. We check that the top level security state is
+ * STATE_IS_INSECURE. The mixed content from the iframe shouldn't "upgrade"
+ * the HTTP top level page to broken HTTPS.
+ */
+
+const gHttpTestUrl = "http://example.com/browser/browser/base/content/test/general/file_mixedContentFramesOnHttp.html";
+
+var gTestBrowser = null;
+
+add_task(function *() {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["security.mixed_content.block_active_content", true],
+ ["security.mixed_content.block_display_content", false]
+ ]
+ }, resolve);
+ });
+ let url = gHttpTestUrl
+ yield BrowserTestUtils.withNewTab({gBrowser, url}, function*() {
+ gTestBrowser = gBrowser.selectedBrowser;
+ // check security state is insecure
+ isSecurityState("insecure");
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: false, passiveLoaded: true});
+ });
+});
+
diff --git a/browser/base/content/test/general/browser_mixedContentFromOnunload.js b/browser/base/content/test/general/browser_mixedContentFromOnunload.js
new file mode 100644
index 000000000..9b39776f4
--- /dev/null
+++ b/browser/base/content/test/general/browser_mixedContentFromOnunload.js
@@ -0,0 +1,49 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Tests for Bug 947079 - Fix bug in nsSecureBrowserUIImpl that sets the wrong
+ * security state on a page because of a subresource load that is not on the
+ * same page.
+ */
+
+// We use different domains for each test and for navigation within each test
+const gHttpTestRoot1 = "http://example.com/browser/browser/base/content/test/general/";
+const gHttpsTestRoot1 = "https://test1.example.com/browser/browser/base/content/test/general/";
+const gHttpTestRoot2 = "http://example.net/browser/browser/base/content/test/general/";
+const gHttpsTestRoot2 = "https://test2.example.com/browser/browser/base/content/test/general/";
+
+var gTestBrowser = null;
+add_task(function *() {
+ let url = gHttpTestRoot1 + "file_mixedContentFromOnunload.html";
+ yield BrowserTestUtils.withNewTab({gBrowser, url}, function*() {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["security.mixed_content.block_active_content", true],
+ ["security.mixed_content.block_display_content", false]
+ ]
+ }, resolve);
+ });
+ gTestBrowser = gBrowser.selectedBrowser;
+ // Navigation from an http page to a https page with no mixed content
+ // The http page loads an http image on unload
+ url = gHttpsTestRoot1 + "file_mixedContentFromOnunload_test1.html";
+ yield BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+ // check security state. Since current url is https and doesn't have any
+ // mixed content resources, we expect it to be secure.
+ isSecurityState("secure");
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: false, passiveLoaded: false});
+ // Navigation from an http page to a https page that has mixed display content
+ // The https page loads an http image on unload
+ url = gHttpTestRoot2 + "file_mixedContentFromOnunload.html";
+ yield BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+ url = gHttpsTestRoot2 + "file_mixedContentFromOnunload_test2.html";
+ yield BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+ isSecurityState("broken");
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: false, passiveLoaded: true});
+ });
+});
diff --git a/browser/base/content/test/general/browser_mixed_content_cert_override.js b/browser/base/content/test/general/browser_mixed_content_cert_override.js
new file mode 100644
index 000000000..037fce5d2
--- /dev/null
+++ b/browser/base/content/test/general/browser_mixed_content_cert_override.js
@@ -0,0 +1,54 @@
+/*
+ * Bug 1253771 - check mixed content blocking in combination with overriden certificates
+ */
+
+"use strict";
+
+const MIXED_CONTENT_URL = "https://self-signed.example.com/browser/browser/base/content/test/general/test-mixedcontent-securityerrors.html";
+
+function getConnectionState() {
+ return document.getElementById("identity-popup").getAttribute("connection");
+}
+
+function getPopupContentVerifier() {
+ return document.getElementById("identity-popup-content-verifier");
+}
+
+function getConnectionIcon() {
+ return window.getComputedStyle(document.getElementById("connection-icon")).listStyleImage;
+}
+
+function checkIdentityPopup(icon) {
+ gIdentityHandler.refreshIdentityPopup();
+ is(getConnectionIcon(), `url("chrome://browser/skin/${icon}")`);
+ is(getConnectionState(), "secure-cert-user-overridden");
+ isnot(getPopupContentVerifier().style.display, "none", "Overridden certificate warning is shown");
+ ok(getPopupContentVerifier().textContent.includes("security exception"), "Text shows overridden certificate warning.");
+}
+
+add_task(function* () {
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ // check that a warning is shown when loading a page with mixed content and an overridden certificate
+ yield loadBadCertPage(MIXED_CONTENT_URL);
+ checkIdentityPopup("connection-mixed-passive-loaded.svg#icon");
+
+ // check that the crossed out icon is shown when disabling mixed content protection
+ gIdentityHandler.disableMixedContentProtection();
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ checkIdentityPopup("connection-mixed-active-loaded.svg#icon");
+
+ // check that a warning is shown even without mixed content
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "https://self-signed.example.com");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ checkIdentityPopup("connection-mixed-passive-loaded.svg#icon");
+
+ // remove cert exception
+ let certOverrideService = Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService);
+ certOverrideService.clearValidityOverride("self-signed.example.com", -1);
+
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
diff --git a/browser/base/content/test/general/browser_mixedcontent_securityflags.js b/browser/base/content/test/general/browser_mixedcontent_securityflags.js
new file mode 100644
index 000000000..1c2614b86
--- /dev/null
+++ b/browser/base/content/test/general/browser_mixedcontent_securityflags.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The test loads a web page with mixed active and mixed display content and
+// makes sure that the mixed content flags on the docshell are set correctly.
+// * Using default about:config prefs (mixed active blocked, mixed display
+// loaded) we load the page and check the flags.
+// * We change the about:config prefs (mixed active blocked, mixed display
+// blocked), reload the page, and check the flags again.
+// * We override protection so all mixed content can load and check the
+// flags again.
+
+const TEST_URI = "https://example.com/browser/browser/base/content/test/general/test-mixedcontent-securityerrors.html";
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+var gTestBrowser = null;
+
+registerCleanupFunction(function() {
+ // Set preferences back to their original values
+ Services.prefs.clearUserPref(PREF_DISPLAY);
+ Services.prefs.clearUserPref(PREF_ACTIVE);
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* blockMixedActiveContentTest() {
+ // Turn on mixed active blocking and mixed display loading and load the page.
+ Services.prefs.setBoolPref(PREF_DISPLAY, false);
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI);
+ gTestBrowser = gBrowser.getBrowserForTab(tab);
+
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ is(docShell.hasMixedDisplayContentBlocked, false, "hasMixedDisplayContentBlocked flag has been set");
+ is(docShell.hasMixedActiveContentBlocked, true, "hasMixedActiveContentBlocked flag has been set");
+ is(docShell.hasMixedDisplayContentLoaded, true, "hasMixedDisplayContentLoaded flag has been set");
+ is(docShell.hasMixedActiveContentLoaded, false, "hasMixedActiveContentLoaded flag has been set");
+ });
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: true});
+
+ // Turn on mixed active and mixed display blocking and reload the page.
+ Services.prefs.setBoolPref(PREF_DISPLAY, true);
+ Services.prefs.setBoolPref(PREF_ACTIVE, true);
+
+ gBrowser.reload();
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ is(docShell.hasMixedDisplayContentBlocked, true, "hasMixedDisplayContentBlocked flag has been set");
+ is(docShell.hasMixedActiveContentBlocked, true, "hasMixedActiveContentBlocked flag has been set");
+ is(docShell.hasMixedDisplayContentLoaded, false, "hasMixedDisplayContentLoaded flag has been set");
+ is(docShell.hasMixedActiveContentLoaded, false, "hasMixedActiveContentLoaded flag has been set");
+ });
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: false, activeBlocked: true, passiveLoaded: false});
+});
+
+add_task(function* overrideMCB() {
+ // Disable mixed content blocking (reloads page) and retest
+ let {gIdentityHandler} = gTestBrowser.ownerGlobal;
+ gIdentityHandler.disableMixedContentProtection();
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ is(docShell.hasMixedDisplayContentLoaded, true, "hasMixedDisplayContentLoaded flag has not been set");
+ is(docShell.hasMixedActiveContentLoaded, true, "hasMixedActiveContentLoaded flag has not been set");
+ is(docShell.hasMixedDisplayContentBlocked, false, "second hasMixedDisplayContentBlocked flag has been set");
+ is(docShell.hasMixedActiveContentBlocked, false, "second hasMixedActiveContentBlocked flag has been set");
+ });
+ assertMixedContentBlockingState(gTestBrowser, {activeLoaded: true, activeBlocked: false, passiveLoaded: true});
+});
diff --git a/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js b/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js
new file mode 100644
index 000000000..3b5a5a149
--- /dev/null
+++ b/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js
@@ -0,0 +1,30 @@
+"use strict";
+
+const kURL =
+ "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+ "data:text/html,<a href=''>Middle-click me</a>";
+
+/*
+ * Check that when manually opening content JS links in new tabs/windows,
+ * we use the correct principal, and we don't clear the URL bar.
+ */
+add_task(function* () {
+ yield BrowserTestUtils.withNewTab(kURL, function* (browser) {
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ yield ContentTask.spawn(browser, null, function* () {
+ let a = content.document.createElement("a");
+ a.href = "javascript:document.write('spoof'); void(0);";
+ a.textContent = "Some link";
+ content.document.body.appendChild(a);
+ });
+ info("Added element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("a", {button: 1}, browser);
+ let newTab = yield newTabPromise;
+ is(newTab.linkedBrowser.contentPrincipal.origin, "http://example.com",
+ "Principal should be for example.com");
+ yield BrowserTestUtils.switchTab(gBrowser, newTab);
+ info(gURLBar.value);
+ isnot(gURLBar.value, "", "URL bar should not be empty.");
+ yield BrowserTestUtils.removeTab(newTab);
+ });
+});
diff --git a/browser/base/content/test/general/browser_newTabDrop.js b/browser/base/content/test/general/browser_newTabDrop.js
new file mode 100644
index 000000000..03c90df3f
--- /dev/null
+++ b/browser/base/content/test/general/browser_newTabDrop.js
@@ -0,0 +1,99 @@
+registerCleanupFunction(function* cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ yield BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+ Services.search.currentEngine = originalEngine;
+ let engine = Services.search.getEngineByName("MozSearch");
+ Services.search.removeEngine(engine);
+});
+
+let originalEngine;
+add_task(function* test_setup() {
+ // Stop search-engine loads from hitting the network
+ Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+ let engine = Services.search.getEngineByName("MozSearch");
+ originalEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+});
+
+// New Tab Button opens any link.
+add_task(function*() { yield dropText("mochi.test/first", 1); });
+add_task(function*() { yield dropText("javascript:'bad'", 1); });
+add_task(function*() { yield dropText("jAvascript:'bad'", 1); });
+add_task(function*() { yield dropText("mochi.test/second", 1); });
+add_task(function*() { yield dropText("data:text/html,bad", 1); });
+add_task(function*() { yield dropText("mochi.test/third", 1); });
+
+// Single text/plain item, with multiple links.
+add_task(function*() { yield dropText("mochi.test/1\nmochi.test/2", 2); });
+add_task(function*() { yield dropText("javascript:'bad1'\nmochi.test/3", 2); });
+add_task(function*() { yield dropText("mochi.test/4\ndata:text/html,bad1", 2); });
+
+// Multiple text/plain items, with single and multiple links.
+add_task(function*() {
+ yield drop([[{type: "text/plain",
+ data: "mochi.test/5"}],
+ [{type: "text/plain",
+ data: "mochi.test/6\nmochi.test/7"}]], 3);
+});
+
+// Single text/x-moz-url item, with multiple links.
+// "text/x-moz-url" has titles in even-numbered lines.
+add_task(function*() {
+ yield drop([[{type: "text/x-moz-url",
+ data: "mochi.test/8\nTITLE8\nmochi.test/9\nTITLE9"}]], 2);
+});
+
+// Single item with multiple types.
+add_task(function*() {
+ yield drop([[{type: "text/plain",
+ data: "mochi.test/10"},
+ {type: "text/x-moz-url",
+ data: "mochi.test/11\nTITLE11"}]], 1);
+});
+
+function dropText(text, expectedTabOpenCount=0) {
+ return drop([[{type: "text/plain", data: text}]], expectedTabOpenCount);
+}
+
+function* drop(dragData, expectedTabOpenCount=0) {
+ let dragDataString = JSON.stringify(dragData);
+ info(`Starting test for datagData:${dragDataString}; expectedTabOpenCount:${expectedTabOpenCount}`);
+ let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+ let EventUtils = {};
+ scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button.
+ let dragSrcElement = document.getElementById("downloads-button");
+ ok(dragSrcElement, "Downloads button exists");
+ let newTabButton = document.getElementById("new-tab-button");
+ ok(newTabButton, "New Tab button exists");
+
+ let awaitDrop = BrowserTestUtils.waitForEvent(newTabButton, "drop");
+ let actualTabOpenCount = 0;
+ let openedTabs = [];
+ let checkCount = function(event) {
+ openedTabs.push(event.target);
+ actualTabOpenCount++;
+ return actualTabOpenCount == expectedTabOpenCount;
+ };
+ let awaitTabOpen = expectedTabOpenCount && BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen", false, checkCount);
+
+ EventUtils.synthesizeDrop(dragSrcElement, newTabButton, dragData, "link", window);
+
+ let tabsOpened = false;
+ if (awaitTabOpen) {
+ yield awaitTabOpen;
+ info("Got TabOpen event");
+ tabsOpened = true;
+ for (let tab of openedTabs) {
+ yield BrowserTestUtils.removeTab(tab);
+ }
+ }
+ is(tabsOpened, !!expectedTabOpenCount, `Tabs for ${dragDataString} should only open if any of dropped items are valid`);
+
+ yield awaitDrop;
+ ok(true, "Got drop event");
+}
diff --git a/browser/base/content/test/general/browser_newWindowDrop.js b/browser/base/content/test/general/browser_newWindowDrop.js
new file mode 100644
index 000000000..f404d4eed
--- /dev/null
+++ b/browser/base/content/test/general/browser_newWindowDrop.js
@@ -0,0 +1,120 @@
+registerCleanupFunction(function* cleanup() {
+ Services.search.currentEngine = originalEngine;
+ let engine = Services.search.getEngineByName("MozSearch");
+ Services.search.removeEngine(engine);
+});
+
+let originalEngine;
+add_task(function* test_setup() {
+ // Opening multiple windows on debug build takes too long time.
+ requestLongerTimeout(10);
+
+ // Stop search-engine loads from hitting the network
+ Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+ let engine = Services.search.getEngineByName("MozSearch");
+ originalEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+
+ // Move New Window button to nav bar, to make it possible to drag and drop.
+ let {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm", {});
+ let origPlacement = CustomizableUI.getPlacementOfWidget("new-window-button");
+ if (!origPlacement || origPlacement.area != CustomizableUI.AREA_NAVBAR) {
+ CustomizableUI.addWidgetToArea("new-window-button",
+ CustomizableUI.AREA_NAVBAR,
+ 0);
+ CustomizableUI.ensureWidgetPlacedInWindow("new-window-button", window);
+ registerCleanupFunction(function () {
+ CustomizableUI.reset();
+ });
+ }
+});
+
+// New Window Button opens any link.
+add_task(function*() { yield dropText("mochi.test/first", 1); });
+add_task(function*() { yield dropText("javascript:'bad'", 1); });
+add_task(function*() { yield dropText("jAvascript:'bad'", 1); });
+add_task(function*() { yield dropText("mochi.test/second", 1); });
+add_task(function*() { yield dropText("data:text/html,bad", 1); });
+add_task(function*() { yield dropText("mochi.test/third", 1); });
+
+// Single text/plain item, with multiple links.
+add_task(function*() { yield dropText("mochi.test/1\nmochi.test/2", 2); });
+add_task(function*() { yield dropText("javascript:'bad1'\nmochi.test/3", 2); });
+add_task(function*() { yield dropText("mochi.test/4\ndata:text/html,bad1", 2); });
+
+// Multiple text/plain items, with single and multiple links.
+add_task(function*() {
+ yield drop([[{type: "text/plain",
+ data: "mochi.test/5"}],
+ [{type: "text/plain",
+ data: "mochi.test/6\nmochi.test/7"}]], 3);
+});
+
+// Single text/x-moz-url item, with multiple links.
+// "text/x-moz-url" has titles in even-numbered lines.
+add_task(function*() {
+ yield drop([[{type: "text/x-moz-url",
+ data: "mochi.test/8\nTITLE8\nmochi.test/9\nTITLE9"}]], 2);
+});
+
+// Single item with multiple types.
+add_task(function*() {
+ yield drop([[{type: "text/plain",
+ data: "mochi.test/10"},
+ {type: "text/x-moz-url",
+ data: "mochi.test/11\nTITLE11"}]], 1);
+});
+
+function dropText(text, expectedWindowOpenCount=0) {
+ return drop([[{type: "text/plain", data: text}]], expectedWindowOpenCount);
+}
+
+function* drop(dragData, expectedWindowOpenCount=0) {
+ let dragDataString = JSON.stringify(dragData);
+ info(`Starting test for datagData:${dragDataString}; expectedWindowOpenCount:${expectedWindowOpenCount}`);
+ let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+ let EventUtils = {};
+ scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+ // Since synthesizeDrop triggers the srcElement, need to use another button.
+ let dragSrcElement = document.getElementById("downloads-button");
+ ok(dragSrcElement, "Downloads button exists");
+ let newWindowButton = document.getElementById("new-window-button");
+ ok(newWindowButton, "New Window button exists");
+
+ let tmp = {};
+ Cu.import("resource://testing-common/TestUtils.jsm", tmp);
+
+ let awaitDrop = BrowserTestUtils.waitForEvent(newWindowButton, "drop");
+ let actualWindowOpenCount = 0;
+ let openedWindows = [];
+ let checkCount = function(window) {
+ // Add observer as soon as domWindow is opened to avoid missing the topic.
+ let awaitStartup = tmp.TestUtils.topicObserved("browser-delayed-startup-finished",
+ subject => subject == window);
+ openedWindows.push([window, awaitStartup]);
+ actualWindowOpenCount++;
+ return actualWindowOpenCount == expectedWindowOpenCount;
+ };
+ let awaitWindowOpen = expectedWindowOpenCount && BrowserTestUtils.domWindowOpened(null, checkCount);
+
+ EventUtils.synthesizeDrop(dragSrcElement, newWindowButton, dragData, "link", window);
+
+ let windowsOpened = false;
+ if (awaitWindowOpen) {
+ yield awaitWindowOpen;
+ info("Got Window opened");
+ windowsOpened = true;
+ for (let [window, awaitStartup] of openedWindows.reverse()) {
+ // Wait for startup before closing, to properly close the browser window.
+ yield awaitStartup;
+ yield BrowserTestUtils.closeWindow(window);
+ }
+ }
+ is(windowsOpened, !!expectedWindowOpenCount, `Windows for ${dragDataString} should only open if any of dropped items are valid`);
+
+ yield awaitDrop;
+ ok(true, "Got drop event");
+}
diff --git a/browser/base/content/test/general/browser_newwindow_focus.js b/browser/base/content/test/general/browser_newwindow_focus.js
new file mode 100644
index 000000000..7880db0bd
--- /dev/null
+++ b/browser/base/content/test/general/browser_newwindow_focus.js
@@ -0,0 +1,96 @@
+"use strict";
+
+/**
+ * These tests are for the auto-focus behaviour on the initial browser
+ * when a window is opened from content.
+ */
+
+const PAGE = `data:text/html,<a id="target" href="%23" onclick="window.open('http://www.example.com', '_blank', 'width=100,height=100');">Click me</a>`;
+
+/**
+ * Returns a Promise that resolves when a new window has
+ * opened, and the "load" event has fired in that window.
+ * We can't use BrowserTestUtils.domWindowOpened directly,
+ * because by the time the "then" on the Promise runs,
+ * DOMContentLoaded and load may have already run in the new
+ * window. However, we want to be very explicit about what
+ * events we're waiting for, and not rely on a quirk of our
+ * Promises infrastructure.
+ */
+function promiseNewWindow() {
+ return new Promise((resolve) => {
+ let observer = (subject, topic, data) => {
+ if (topic == "domwindowopened") {
+ Services.ww.unregisterNotification(observer);
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ resolve(win);
+ });
+ }
+ };
+
+ Services.ww.registerNotification(observer);
+ });
+}
+
+/**
+ * Test that when a new window is opened from content, focus moves
+ * to the initial browser in that window once the window has finished
+ * painting.
+ */
+add_task(function* test_focus_browser() {
+ yield BrowserTestUtils.withNewTab({
+ url: PAGE,
+ gBrowser,
+ }, function*(browser) {
+ let newWinPromise = promiseNewWindow();
+ let delayedStartupPromise = BrowserTestUtils.waitForNewWindow();
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#target", {}, browser);
+ let newWin = yield newWinPromise;
+ yield BrowserTestUtils.contentPainted(newWin.gBrowser.selectedBrowser);
+ yield delayedStartupPromise;
+
+ let focusedElement =
+ Services.focus.getFocusedElementForWindow(newWin, false, {});
+
+ Assert.equal(focusedElement, newWin.gBrowser.selectedBrowser,
+ "Initial browser should be focused");
+
+ yield BrowserTestUtils.closeWindow(newWin);
+ });
+});
+
+/**
+ * Test that when a new window is opened from content and focus
+ * shifts in that window before the content has a chance to paint
+ * that we _don't_ steal focus once content has painted.
+ */
+add_task(function* test_no_steal_focus() {
+ yield BrowserTestUtils.withNewTab({
+ url: PAGE,
+ gBrowser,
+ }, function*(browser) {
+ let newWinPromise = promiseNewWindow();
+ let delayedStartupPromise = BrowserTestUtils.waitForNewWindow();
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#target", {}, browser);
+ let newWin = yield newWinPromise;
+
+ // Because we're switching focus, we shouldn't steal it once
+ // content paints.
+ newWin.gURLBar.focus();
+
+ yield BrowserTestUtils.contentPainted(newWin.gBrowser.selectedBrowser);
+ yield delayedStartupPromise;
+
+ let focusedElement =
+ Services.focus.getFocusedElementForWindow(newWin, false, {});
+
+ Assert.equal(focusedElement, newWin.gURLBar.inputField,
+ "URLBar should be focused");
+
+ yield BrowserTestUtils.closeWindow(newWin);
+ });
+});
diff --git a/browser/base/content/test/general/browser_no_mcb_on_http_site.js b/browser/base/content/test/general/browser_no_mcb_on_http_site.js
new file mode 100644
index 000000000..45fd67379
--- /dev/null
+++ b/browser/base/content/test/general/browser_no_mcb_on_http_site.js
@@ -0,0 +1,106 @@
+/*
+ * Description of the Tests for
+ * - Bug 909920 - Mixed content warning should not show on a HTTP site
+ *
+ * Description of the tests:
+ * Test 1:
+ * 1) Load an http page
+ * 2) The page includes a css file using https
+ * 3) The css file loads an |IMAGE| << over http
+ *
+ * Test 2:
+ * 1) Load an http page
+ * 2) The page includes a css file using https
+ * 3) The css file loads a |FONT| over http
+ *
+ * Test 3:
+ * 1) Load an http page
+ * 2) The page includes a css file using https
+ * 3) The css file imports (@import) another css file using http
+ * 3) The imported css file loads a |FONT| over http
+*
+ * Since the top-domain is >> NOT << served using https, the MCB
+ * should >> NOT << trigger a warning.
+ */
+
+const PREF_ACTIVE = "security.mixed_content.block_active_content";
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
+
+const gHttpTestRoot = "http://example.com/browser/browser/base/content/test/general/";
+
+var gTestBrowser = null;
+
+function cleanUpAfterTests() {
+ gBrowser.removeCurrentTab();
+ window.focus();
+}
+
+add_task(function* init() {
+ yield SpecialPowers.pushPrefEnv({ set: [[ PREF_ACTIVE, true ],
+ [ PREF_DISPLAY, true ]] });
+ let url = gHttpTestRoot + "test_no_mcb_on_http_site_img.html";
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url)
+ gTestBrowser = tab.linkedBrowser;
+});
+
+// ------------- TEST 1 -----------------------------------------
+
+add_task(function* test1() {
+ let expected = "Verifying MCB does not trigger warning/error for an http page ";
+ expected += "with https css that includes http image";
+
+ yield ContentTask.spawn(gTestBrowser, expected, function* (condition) {
+ yield ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("testDiv").innerHTML == condition,
+ "Waited too long for status in Test 1!");
+ });
+
+ // Explicit OKs needed because the harness requires at least one call to ok.
+ ok(true, "test 1 passed");
+
+ // set up test 2
+ let url = gHttpTestRoot + "test_no_mcb_on_http_site_font.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+// ------------- TEST 2 -----------------------------------------
+
+add_task(function* test2() {
+ let expected = "Verifying MCB does not trigger warning/error for an http page ";
+ expected += "with https css that includes http font";
+
+ yield ContentTask.spawn(gTestBrowser, expected, function* (condition) {
+ yield ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("testDiv").innerHTML == condition,
+ "Waited too long for status in Test 2!");
+ });
+
+ ok(true, "test 2 passed");
+
+ // set up test 3
+ let url = gHttpTestRoot + "test_no_mcb_on_http_site_font2.html";
+ BrowserTestUtils.loadURI(gTestBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+// ------------- TEST 3 -----------------------------------------
+
+add_task(function* test3() {
+ let expected = "Verifying MCB does not trigger warning/error for an http page "
+ expected += "with https css that imports another http css which includes http font";
+
+ yield ContentTask.spawn(gTestBrowser, expected, function* (condition) {
+ yield ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("testDiv").innerHTML == condition,
+ "Waited too long for status in Test 3!");
+ });
+
+ ok(true, "test3 passed");
+});
+
+// ------------------------------------------------------
+
+add_task(function* cleanup() {
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/general/browser_offlineQuotaNotification.js b/browser/base/content/test/general/browser_offlineQuotaNotification.js
new file mode 100644
index 000000000..e56bfe9a8
--- /dev/null
+++ b/browser/base/content/test/general/browser_offlineQuotaNotification.js
@@ -0,0 +1,95 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test offline quota warnings - must be run as a mochitest-browser test or
+// else the test runner gets in the way of notifications due to bug 857897.
+
+const URL = "http://mochi.test:8888/browser/browser/base/content/test/general/offlineQuotaNotification.html";
+
+registerCleanupFunction(function() {
+ // Clean up after ourself
+ let uri = Services.io.newURI(URL, null, null);
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+ Services.perms.removeFromPrincipal(principal, "offline-app");
+ Services.prefs.clearUserPref("offline-apps.quota.warn");
+ Services.prefs.clearUserPref("offline-apps.allow_by_default");
+ let {OfflineAppCacheHelper} = Components.utils.import("resource:///modules/offlineAppCache.jsm", {});
+ OfflineAppCacheHelper.clear();
+});
+
+// Same as the other one, but for in-content preferences
+function checkInContentPreferences(win) {
+ let doc = win.document;
+ let sel = doc.getElementById("categories").selectedItems[0].id;
+ let tab = doc.getElementById("advancedPrefs").selectedTab.id;
+ is(gBrowser.currentURI.spec, "about:preferences#advanced", "about:preferences loaded");
+ is(sel, "category-advanced", "Advanced pane was selected");
+ is(tab, "networkTab", "Network tab is selected");
+ // all good, we are done.
+ win.close();
+ finish();
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("offline-apps.allow_by_default", false);
+
+ // Open a new tab.
+ gBrowser.selectedTab = gBrowser.addTab(URL);
+ registerCleanupFunction(() => gBrowser.removeCurrentTab());
+
+
+ Promise.all([
+ // Wait for a notification that asks whether to allow offline storage.
+ promiseNotification(),
+ // Wait for the tab to load.
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser),
+ ]).then(() => {
+ info("Loaded page, adding onCached handler");
+ // Need a promise to keep track of when we've added our handler.
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let onCachedAttached = BrowserTestUtils.waitForMessage(mm, "Test:OnCachedAttached");
+ let gotCached = ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ return new Promise(resolve => {
+ content.window.applicationCache.oncached = function() {
+ setTimeout(resolve, 0);
+ };
+ sendAsyncMessage("Test:OnCachedAttached");
+ });
+ });
+ gotCached.then(function() {
+ // We got cached - now we should have provoked the quota warning.
+ let notification = PopupNotifications.getNotification('offline-app-usage');
+ ok(notification, "have offline-app-usage notification");
+ // select the default action - this should cause the preferences
+ // tab to open - which we track via an "Initialized" event.
+ PopupNotifications.panel.firstElementChild.button.click();
+ let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ newTabBrowser.addEventListener("Initialized", function PrefInit() {
+ newTabBrowser.removeEventListener("Initialized", PrefInit, true);
+ executeSoon(function() {
+ checkInContentPreferences(newTabBrowser.contentWindow);
+ })
+ }, true);
+ });
+ onCachedAttached.then(function() {
+ Services.prefs.setIntPref("offline-apps.quota.warn", 1);
+
+ // Click the notification panel's "Allow" button. This should kick
+ // off updates which will call our oncached handler above.
+ PopupNotifications.panel.firstElementChild.button.click();
+ });
+ });
+}
+
+function promiseNotification() {
+ return new Promise(resolve => {
+ PopupNotifications.panel.addEventListener("popupshown", function onShown() {
+ PopupNotifications.panel.removeEventListener("popupshown", onShown);
+ resolve();
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_overflowScroll.js b/browser/base/content/test/general/browser_overflowScroll.js
new file mode 100644
index 000000000..56932fae2
--- /dev/null
+++ b/browser/base/content/test/general/browser_overflowScroll.js
@@ -0,0 +1,91 @@
+var tabstrip = gBrowser.tabContainer.mTabstrip;
+var scrollbox = tabstrip._scrollbox;
+var originalSmoothScroll = tabstrip.smoothScroll;
+var tabs = gBrowser.tabs;
+
+var rect = ele => ele.getBoundingClientRect();
+var width = ele => rect(ele).width;
+var left = ele => rect(ele).left;
+var right = ele => rect(ele).right;
+var isLeft = (ele, msg) => is(left(ele) + tabstrip._tabMarginLeft, left(scrollbox), msg);
+var isRight = (ele, msg) => is(right(ele) - tabstrip._tabMarginRight, right(scrollbox), msg);
+var elementFromPoint = x => tabstrip._elementFromPoint(x);
+var nextLeftElement = () => elementFromPoint(left(scrollbox) - 1);
+var nextRightElement = () => elementFromPoint(right(scrollbox) + 1);
+var firstScrollable = () => tabs[gBrowser._numPinnedTabs];
+
+function test() {
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+
+ // If the previous (or more) test finished with cleaning up the tabs,
+ // there may be some pending animations. That can cause a failure of
+ // this tests, so, we should test this in another stack.
+ setTimeout(doTest, 0);
+}
+
+function doTest() {
+ tabstrip.smoothScroll = false;
+
+ var tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth);
+ var tabCountForOverflow = Math.ceil(width(tabstrip) / tabMinWidth * 3);
+ while (tabs.length < tabCountForOverflow)
+ gBrowser.addTab("about:blank", {skipAnimation: true});
+ gBrowser.pinTab(tabs[0]);
+
+ tabstrip.addEventListener("overflow", runOverflowTests, false);
+}
+
+function runOverflowTests(aEvent) {
+ if (aEvent.detail != 1)
+ return;
+
+ tabstrip.removeEventListener("overflow", runOverflowTests, false);
+
+ var upButton = tabstrip._scrollButtonUp;
+ var downButton = tabstrip._scrollButtonDown;
+ var element;
+
+ gBrowser.selectedTab = firstScrollable();
+ ok(left(scrollbox) <= left(firstScrollable()), "Selecting the first tab scrolls it into view " +
+ "(" + left(scrollbox) + " <= " + left(firstScrollable()) + ")");
+
+ element = nextRightElement();
+ EventUtils.synthesizeMouseAtCenter(downButton, {});
+ isRight(element, "Scrolled one tab to the right with a single click");
+
+ gBrowser.selectedTab = tabs[tabs.length - 1];
+ ok(right(gBrowser.selectedTab) <= right(scrollbox), "Selecting the last tab scrolls it into view " +
+ "(" + right(gBrowser.selectedTab) + " <= " + right(scrollbox) + ")");
+
+ element = nextLeftElement();
+ EventUtils.synthesizeMouse(upButton, 1, 1, {});
+ isLeft(element, "Scrolled one tab to the left with a single click");
+
+ let elementPoint = left(scrollbox) - width(scrollbox);
+ element = elementFromPoint(elementPoint);
+ if (elementPoint == right(element)) {
+ element = element.nextSibling;
+ }
+ EventUtils.synthesizeMouse(upButton, 1, 1, {clickCount: 2});
+ isLeft(element, "Scrolled one page of tabs with a double click");
+
+ EventUtils.synthesizeMouse(upButton, 1, 1, {clickCount: 3});
+ var firstScrollableLeft = left(firstScrollable());
+ ok(left(scrollbox) <= firstScrollableLeft, "Scrolled to the start with a triple click " +
+ "(" + left(scrollbox) + " <= " + firstScrollableLeft + ")");
+
+ for (var i = 2; i; i--)
+ EventUtils.synthesizeWheel(scrollbox, 1, 1, { deltaX: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE });
+ is(left(firstScrollable()), firstScrollableLeft, "Remained at the start with the mouse wheel");
+
+ element = nextRightElement();
+ EventUtils.synthesizeWheel(scrollbox, 1, 1, { deltaX: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE});
+ isRight(element, "Scrolled one tab to the right with the mouse wheel");
+
+ while (tabs.length > 1)
+ gBrowser.removeTab(tabs[0]);
+
+ tabstrip.smoothScroll = originalSmoothScroll;
+ finish();
+}
diff --git a/browser/base/content/test/general/browser_pageInfo.js b/browser/base/content/test/general/browser_pageInfo.js
new file mode 100644
index 000000000..90fe2e17f
--- /dev/null
+++ b/browser/base/content/test/general/browser_pageInfo.js
@@ -0,0 +1,38 @@
+function test() {
+ waitForExplicitFinish();
+
+ var pageInfo;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function loadListener() {
+ gBrowser.selectedBrowser.removeEventListener("load", loadListener, true);
+
+ Services.obs.addObserver(observer, "page-info-dialog-loaded", false);
+ pageInfo = BrowserPageInfo();
+ }, true);
+ content.location =
+ "https://example.com/browser/browser/base/content/test/general/feed_tab.html";
+
+ function observer(win, topic, data) {
+ Services.obs.removeObserver(observer, "page-info-dialog-loaded");
+ pageInfo.onFinished.push(handlePageInfo);
+ }
+
+ function handlePageInfo() {
+ ok(pageInfo.document.getElementById("feedTab"), "Feed tab");
+ let feedListbox = pageInfo.document.getElementById("feedListbox");
+ ok(feedListbox, "Feed list");
+
+ var feedRowsNum = feedListbox.getRowCount();
+ is(feedRowsNum, 3, "Number of feeds listed");
+
+ for (var i = 0; i < feedRowsNum; i++) {
+ let feedItem = feedListbox.getItemAtIndex(i);
+ is(feedItem.getAttribute("name"), i + 1, "Feed name");
+ }
+
+ pageInfo.close();
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/browser/base/content/test/general/browser_page_style_menu.js b/browser/base/content/test/general/browser_page_style_menu.js
new file mode 100644
index 000000000..cb080d52a
--- /dev/null
+++ b/browser/base/content/test/general/browser_page_style_menu.js
@@ -0,0 +1,97 @@
+"use strict";
+
+/**
+ * Stylesheets are updated for a browser after the pageshow event.
+ * This helper function returns a Promise that waits for that pageshow
+ * event, and then resolves on the next tick to ensure that gPageStyleMenu
+ * has had a chance to update the stylesheets.
+ *
+ * @param browser
+ * The <xul:browser> to wait for.
+ * @return Promise
+ */
+function promiseStylesheetsUpdated(browser) {
+ return ContentTask.spawn(browser, { PAGE }, function*(args) {
+ return new Promise((resolve) => {
+ addEventListener("pageshow", function onPageShow(e) {
+ if (e.target.location == args.PAGE) {
+ removeEventListener("pageshow", onPageShow);
+ content.setTimeout(resolve, 0);
+ }
+ });
+ })
+ });
+}
+
+const PAGE = "http://example.com/browser/browser/base/content/test/general/page_style_sample.html";
+
+/*
+ * Test that the right stylesheets do (and others don't) show up
+ * in the page style menu.
+ */
+add_task(function*() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", false);
+ let browser = tab.linkedBrowser;
+ yield BrowserTestUtils.loadURI(browser, PAGE);
+ yield promiseStylesheetsUpdated(browser);
+
+ let menupopup = document.getElementById("pageStyleMenu").menupopup;
+ gPageStyleMenu.fillPopup(menupopup);
+
+ var items = [];
+ var current = menupopup.getElementsByTagName("menuseparator")[0];
+ while (current.nextSibling) {
+ current = current.nextSibling;
+ items.push(current);
+ }
+
+ items = items.map(el => ({
+ label: el.getAttribute("label"),
+ checked: el.getAttribute("checked") == "true",
+ }));
+
+ let validLinks = yield ContentTask.spawn(gBrowser.selectedBrowser, items, function(contentItems) {
+ let contentValidLinks = 0;
+ Array.forEach(content.document.querySelectorAll("link, style"), function (el) {
+ var title = el.getAttribute("title");
+ var rel = el.getAttribute("rel");
+ var media = el.getAttribute("media");
+ var idstring = el.nodeName + " " + (title ? title : "without title and") +
+ " with rel=\"" + rel + "\"" +
+ (media ? " and media=\"" + media + "\"" : "");
+
+ var item = contentItems.filter(aItem => aItem.label == title);
+ var found = item.length == 1;
+ var checked = found && item[0].checked;
+
+ switch (el.getAttribute("data-state")) {
+ case "0":
+ ok(!found, idstring + " should not show up in page style menu");
+ break;
+ case "0-todo":
+ contentValidLinks++;
+ todo(!found, idstring + " should not show up in page style menu");
+ ok(!checked, idstring + " should not be selected");
+ break;
+ case "1":
+ contentValidLinks++;
+ ok(found, idstring + " should show up in page style menu");
+ ok(!checked, idstring + " should not be selected");
+ break;
+ case "2":
+ contentValidLinks++;
+ ok(found, idstring + " should show up in page style menu");
+ ok(checked, idstring + " should be selected");
+ break;
+ default:
+ throw "data-state attribute is missing or has invalid value";
+ }
+ });
+ return contentValidLinks;
+ });
+
+ ok(items.length, "At least one item in the menu");
+ is(items.length, validLinks, "all valid links found");
+
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_page_style_menu_update.js b/browser/base/content/test/general/browser_page_style_menu_update.js
new file mode 100644
index 000000000..a0c741e48
--- /dev/null
+++ b/browser/base/content/test/general/browser_page_style_menu_update.js
@@ -0,0 +1,67 @@
+"use strict";
+
+const PAGE = "http://example.com/browser/browser/base/content/test/general/page_style_sample.html";
+
+/**
+ * Stylesheets are updated for a browser after the pageshow event.
+ * This helper function returns a Promise that waits for that pageshow
+ * event, and then resolves on the next tick to ensure that gPageStyleMenu
+ * has had a chance to update the stylesheets.
+ *
+ * @param browser
+ * The <xul:browser> to wait for.
+ * @return Promise
+ */
+function promiseStylesheetsUpdated(browser) {
+ return ContentTask.spawn(browser, { PAGE }, function*(args) {
+ return new Promise((resolve) => {
+ addEventListener("pageshow", function onPageShow(e) {
+ if (e.target.location == args.PAGE) {
+ removeEventListener("pageshow", onPageShow);
+ content.setTimeout(resolve, 0);
+ }
+ });
+ })
+ });
+}
+
+/**
+ * Tests that the Page Style menu shows the currently
+ * selected Page Style after a new one has been selected.
+ */
+add_task(function*() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", false);
+ let browser = tab.linkedBrowser;
+
+ yield BrowserTestUtils.loadURI(browser, PAGE);
+ yield promiseStylesheetsUpdated(browser);
+
+ let menupopup = document.getElementById("pageStyleMenu").menupopup;
+ gPageStyleMenu.fillPopup(menupopup);
+
+ // page_style_sample.html should default us to selecting the stylesheet
+ // with the title "6" first.
+ let selected = menupopup.querySelector("menuitem[checked='true']");
+ is(selected.getAttribute("label"), "6", "Should have '6' stylesheet selected by default");
+
+ // Now select stylesheet "1"
+ let target = menupopup.querySelector("menuitem[label='1']");
+ target.click();
+
+ // Now we need to wait for the content process to send its stylesheet
+ // update for the selected tab to the parent. Because messages are
+ // guaranteed to be sent in order, we'll make sure we do the check
+ // after the parent has been updated by yielding until the child
+ // has finished running a ContentTask for us.
+ yield ContentTask.spawn(browser, {}, function*() {
+ dump('\nJust wasting some time.\n');
+ });
+
+ gPageStyleMenu.fillPopup(menupopup);
+ // gPageStyleMenu empties out the menu between opens, so we need
+ // to get a new reference to the selected menuitem
+ selected = menupopup.querySelector("menuitem[checked='true']");
+ is(selected.getAttribute("label"), "1", "Should now have stylesheet 1 selected");
+
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_pageinfo_svg_image.js b/browser/base/content/test/general/browser_pageinfo_svg_image.js
new file mode 100644
index 000000000..02514d79f
--- /dev/null
+++ b/browser/base/content/test/general/browser_pageinfo_svg_image.js
@@ -0,0 +1,38 @@
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function loadListener() {
+ gBrowser.selectedBrowser.removeEventListener("load", loadListener, true);
+ var pageInfo = BrowserPageInfo(gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab");
+
+ pageInfo.addEventListener("load", function loadListener2() {
+ pageInfo.removeEventListener("load", loadListener2, true);
+ pageInfo.onFinished.push(function() {
+ executeSoon(function() {
+ var imageTree = pageInfo.document.getElementById("imagetree");
+ var imageRowsNum = imageTree.view.rowCount;
+
+ ok(imageTree, "Image tree is null (media tab is broken)");
+
+ is(imageRowsNum, 1, "should have one image");
+
+ // Only bother running this if we've got the right number of rows.
+ if (imageRowsNum == 1) {
+ is(imageTree.view.getCellText(0, imageTree.columns[0]),
+ "https://example.com/browser/browser/base/content/test/general/title_test.svg",
+ "The URL should be the svg image.");
+ }
+
+ pageInfo.close();
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ });
+ }, true);
+ }, true);
+
+ content.location =
+ "https://example.com/browser/browser/base/content/test/general/svg_image.html";
+}
diff --git a/browser/base/content/test/general/browser_parsable_css.js b/browser/base/content/test/general/browser_parsable_css.js
new file mode 100644
index 000000000..72954d2e5
--- /dev/null
+++ b/browser/base/content/test/general/browser_parsable_css.js
@@ -0,0 +1,376 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' CSS issues to remain, while we
+ * detect newly occurring issues in shipping CSS. It is a list of objects
+ * specifying conditions under which an error should be ignored.
+ *
+ * Every property of the objects in it needs to consist of a regular expression
+ * matching the offending error. If an object has multiple regex criteria, they
+ * ALL need to match an error in order for that error not to cause a test
+ * failure. */
+let whitelist = [
+ // CodeMirror is imported as-is, see bug 1004423.
+ {sourceName: /codemirror\.css$/i,
+ isFromDevTools: true},
+ // The debugger uses cross-browser CSS.
+ {sourceName: /devtools\/client\/debugger\/new\/styles.css/i,
+ isFromDevTools: true},
+ // PDFjs is futureproofing its pseudoselectors, and those rules are dropped.
+ {sourceName: /web\/viewer\.css$/i,
+ errorMessage: /Unknown pseudo-class.*(fullscreen|selection)/i,
+ isFromDevTools: false},
+ // Tracked in bug 1004428.
+ {sourceName: /aboutaccounts\/(main|normalize)\.css$/i,
+ isFromDevTools: false},
+ // Highlighter CSS uses a UA-only pseudo-class, see bug 985597.
+ {sourceName: /highlighters\.css$/i,
+ errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
+ isFromDevTools: true},
+ // Responsive Design Mode CSS uses a UA-only pseudo-class, see Bug 1241714.
+ {sourceName: /responsive-ua\.css$/i,
+ errorMessage: /Unknown pseudo-class.*moz-dropdown-list/i,
+ isFromDevTools: true},
+
+ {sourceName: /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua)\.css$/i,
+ errorMessage: /Unknown pseudo-class.*-moz-/i,
+ isFromDevTools: false},
+ {sourceName: /\b(html|mathml|ua)\.css$/i,
+ errorMessage: /Unknown property.*-moz-/i,
+ isFromDevTools: false},
+ // Reserved to UA sheets unless layout.css.overflow-clip-box.enabled flipped to true.
+ {sourceName: /res\/forms\.css$/i,
+ errorMessage: /Unknown property.*overflow-clip-box/i,
+ isFromDevTools: false},
+ {sourceName: /res\/(ua|html)\.css$/i,
+ errorMessage: /Unknown pseudo-class .*\bfullscreen\b/i,
+ isFromDevTools: false},
+ {sourceName: /skin\/timepicker\.css$/i,
+ errorMessage: /Error in parsing.*mask/i,
+ isFromDevTools: false},
+];
+
+// Platform can be "linux", "macosx" or "win". If omitted, the exception applies to all platforms.
+let allowedImageReferences = [
+ // Bug 1302691
+ {file: "chrome://devtools/skin/images/dock-bottom-minimize@2x.png",
+ from: "chrome://devtools/skin/toolbox.css",
+ isFromDevTools: true},
+ {file: "chrome://devtools/skin/images/dock-bottom-maximize@2x.png",
+ from: "chrome://devtools/skin/toolbox.css",
+ isFromDevTools: true},
+];
+
+var moduleLocation = gTestPath.replace(/\/[^\/]*$/i, "/parsingTestHelpers.jsm");
+var {generateURIsFromDirTree} = Cu.import(moduleLocation, {});
+
+// Add suffix to stylesheets' URI so that we always load them here and
+// have them parsed. Add a random number so that even if we run this
+// test multiple times, it would be unlikely to affect each other.
+const kPathSuffix = "?always-parse-css-" + Math.random();
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in whitelist
+ *
+ * @param aErrorObject the error to check
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(aErrorObject) {
+ for (let whitelistItem of whitelist) {
+ let matches = true;
+ for (let prop of ["sourceName", "errorMessage"]) {
+ if (whitelistItem.hasOwnProperty(prop) &&
+ !whitelistItem[prop].test(aErrorObject[prop] || "")) {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) {
+ whitelistItem.used = true;
+ return true;
+ }
+ }
+ return false;
+}
+
+function once(target, name) {
+ return new Promise((resolve, reject) => {
+ let cb = () => {
+ target.removeEventListener(name, cb);
+ resolve();
+ };
+ target.addEventListener(name, cb);
+ });
+}
+
+function fetchFile(uri) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.responseType = "text";
+ xhr.open("GET", uri, true);
+ xhr.onreadystatechange = function() {
+ if (this.readyState != this.DONE) {
+ return;
+ }
+ try {
+ resolve(this.responseText);
+ } catch (ex) {
+ ok(false, `Script error reading ${uri}: ${ex}`);
+ resolve("");
+ }
+ };
+ xhr.onerror = error => {
+ ok(false, `XHR error reading ${uri}: ${error}`);
+ resolve("");
+ };
+ xhr.send(null);
+ });
+}
+
+var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIChromeRegistry);
+var gChromeMap = new Map();
+
+function getBaseUriForChromeUri(chromeUri) {
+ let chromeFile = chromeUri + "gobbledygooknonexistentfile.reallynothere";
+ let uri = Services.io.newURI(chromeFile, null, null);
+ let fileUri = gChromeReg.convertChromeURL(uri);
+ return fileUri.resolve(".");
+}
+
+function parseManifest(manifestUri) {
+ return fetchFile(manifestUri.spec).then(data => {
+ for (let line of data.split('\n')) {
+ let [type, ...argv] = line.split(/\s+/);
+ let component;
+ if (type == "content" || type == "skin") {
+ [component] = argv;
+ } else {
+ // skip unrelated lines
+ continue;
+ }
+ let chromeUri = `chrome://${component}/${type}/`;
+ gChromeMap.set(getBaseUriForChromeUri(chromeUri), chromeUri);
+ }
+ });
+}
+
+function convertToChromeUri(fileUri) {
+ let baseUri = fileUri.spec;
+ let path = "";
+ while (true) {
+ let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2);
+ if (slashPos < 0) {
+ info(`File not accessible from chrome protocol: ${fileUri.path}`);
+ return fileUri;
+ }
+ path = baseUri.slice(slashPos + 1) + path;
+ baseUri = baseUri.slice(0, slashPos + 1);
+ if (gChromeMap.has(baseUri)) {
+ let chromeBaseUri = gChromeMap.get(baseUri);
+ let chromeUri = `${chromeBaseUri}${path}`;
+ return Services.io.newURI(chromeUri, null, null);
+ }
+ }
+}
+
+function messageIsCSSError(msg) {
+ // Only care about CSS errors generated by our iframe:
+ if ((msg instanceof Ci.nsIScriptError) &&
+ msg.category.includes("CSS") &&
+ msg.sourceName.endsWith(kPathSuffix)) {
+ let sourceName = msg.sourceName.slice(0, -kPathSuffix.length);
+ let msgInfo = { sourceName, errorMessage: msg.errorMessage };
+ // Check if this error is whitelisted in whitelist
+ if (!ignoredError(msgInfo)) {
+ ok(false, `Got error message for ${sourceName}: ${msg.errorMessage}`);
+ return true;
+ }
+ info(`Ignored error for ${sourceName} because of filter.`);
+ }
+ return false;
+}
+
+let imageURIsToReferencesMap = new Map();
+
+function processCSSRules(sheet) {
+ for (let rule of sheet.cssRules) {
+ if (rule instanceof CSSMediaRule) {
+ processCSSRules(rule);
+ continue;
+ }
+ if (!(rule instanceof CSSStyleRule))
+ continue;
+
+ // Extract urls from the css text.
+ // Note: CSSStyleRule.cssText always has double quotes around URLs even
+ // when the original CSS file didn't.
+ let urls = rule.cssText.match(/url\("[^"]*"\)/g);
+ if (!urls)
+ continue;
+
+ for (let url of urls) {
+ // Remove the url(" prefix and the ") suffix.
+ url = url.replace(/url\("(.*)"\)/, "$1");
+ if (url.startsWith("data:"))
+ continue;
+
+ // Make the url absolute and remove the ref.
+ let baseURI = Services.io.newURI(rule.parentStyleSheet.href, null, null);
+ url = Services.io.newURI(url, null, baseURI).specIgnoringRef;
+
+ // Store the image url along with the css file referencing it.
+ let baseUrl = baseURI.spec.split("?always-parse-css")[0];
+ if (!imageURIsToReferencesMap.has(url)) {
+ imageURIsToReferencesMap.set(url, new Set([baseUrl]));
+ } else {
+ imageURIsToReferencesMap.get(url).add(baseUrl);
+ }
+ }
+ }
+}
+
+function chromeFileExists(aURI)
+{
+ let available = 0;
+ try {
+ let channel = NetUtil.newChannel({uri: aURI, loadUsingSystemPrincipal: true});
+ let stream = channel.open();
+ let sstream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(Ci.nsIScriptableInputStream);
+ sstream.init(stream);
+ available = sstream.available();
+ sstream.close();
+ } catch (e) {
+ if (e.result != Components.results.NS_ERROR_FILE_NOT_FOUND) {
+ dump("Checking " + aURI + ": " + e + "\n");
+ Cu.reportError(e);
+ }
+ }
+ return available > 0;
+}
+
+add_task(function* checkAllTheCSS() {
+ let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let uris = yield generateURIsFromDirTree(appDir, [".css", ".manifest"]);
+
+ // Create a clean iframe to load all the files into. This needs to live at a
+ // chrome URI so that it's allowed to load and parse any styles.
+ let testFile = getRootDirectory(gTestPath) + "dummy_page.html";
+ let windowless = Services.appShell.createWindowlessBrowser();
+ let iframe = windowless.document.createElementNS("http://www.w3.org/1999/xhtml", "html:iframe");
+ windowless.document.documentElement.appendChild(iframe);
+ let iframeLoaded = once(iframe, 'load');
+ iframe.contentWindow.location = testFile;
+ yield iframeLoaded;
+ let doc = iframe.contentWindow.document;
+
+ // Parse and remove all manifests from the list.
+ // NOTE that this must be done before filtering out devtools paths
+ // so that all chrome paths can be recorded.
+ let manifestPromises = [];
+ uris = uris.filter(uri => {
+ if (uri.path.endsWith(".manifest")) {
+ manifestPromises.push(parseManifest(uri));
+ return false;
+ }
+ return true;
+ });
+ // Wait for all manifest to be parsed
+ yield Promise.all(manifestPromises);
+
+ // We build a list of promises that get resolved when their respective
+ // files have loaded and produced no errors.
+ let allPromises = [];
+
+ // filter out either the devtools paths or the non-devtools paths:
+ let isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
+ let devtoolsPathBits = ["webide", "devtools"];
+ uris = uris.filter(uri => isDevtools == devtoolsPathBits.some(path => uri.spec.includes(path)));
+
+ for (let uri of uris) {
+ let linkEl = doc.createElement("link");
+ linkEl.setAttribute("rel", "stylesheet");
+ let promiseForThisSpec = Promise.defer();
+ let onLoad = (e) => {
+ processCSSRules(linkEl.sheet);
+ promiseForThisSpec.resolve();
+ linkEl.removeEventListener("load", onLoad);
+ linkEl.removeEventListener("error", onError);
+ };
+ let onError = (e) => {
+ ok(false, "Loading " + linkEl.getAttribute("href") + " threw an error!");
+ promiseForThisSpec.resolve();
+ linkEl.removeEventListener("load", onLoad);
+ linkEl.removeEventListener("error", onError);
+ };
+ linkEl.addEventListener("load", onLoad);
+ linkEl.addEventListener("error", onError);
+ linkEl.setAttribute("type", "text/css");
+ let chromeUri = convertToChromeUri(uri);
+ linkEl.setAttribute("href", chromeUri.spec + kPathSuffix);
+ allPromises.push(promiseForThisSpec.promise);
+ doc.head.appendChild(linkEl);
+ }
+
+ // Wait for all the files to have actually loaded:
+ yield Promise.all(allPromises);
+
+ // Check if all the files referenced from CSS actually exist.
+ for (let [image, references] of imageURIsToReferencesMap) {
+ if (!chromeFileExists(image)) {
+ for (let ref of references) {
+ let ignored = false;
+ for (let item of allowedImageReferences) {
+ if (image.endsWith(item.file) && ref.endsWith(item.from) &&
+ isDevtools == item.isFromDevTools &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform))) {
+ item.used = true;
+ ignored = true;
+ break;
+ }
+ }
+ if (!ignored)
+ ok(false, "missing " + image + " referenced from " + ref);
+ }
+ }
+ }
+
+ let messages = Services.console.getMessageArray();
+ // Count errors (the test output will list actual issues for us, as well
+ // as the ok(false) in messageIsCSSError.
+ let errors = messages.filter(messageIsCSSError);
+ is(errors.length, 0, "All the styles (" + allPromises.length + ") loaded without errors.");
+
+ // Confirm that all whitelist rules have been used.
+ for (let item of whitelist) {
+ if (!item.used && isDevtools == item.isFromDevTools) {
+ ok(false, "Unused whitelist item. " +
+ (item.sourceName ? " sourceName: " + item.sourceName : "") +
+ (item.errorMessage ? " errorMessage: " + item.errorMessage : ""));
+ }
+ }
+
+ // Confirm that all file whitelist rules have been used.
+ for (let item of allowedImageReferences) {
+ if (!item.used && isDevtools == item.isFromDevTools &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform))) {
+ ok(false, "Unused file whitelist item. " +
+ " file: " + item.file +
+ " from: " + item.from);
+ }
+ }
+
+ // Clean up to avoid leaks:
+ iframe.remove();
+ doc.head.innerHTML = '';
+ doc = null;
+ iframe = null;
+ windowless.close();
+ windowless = null;
+ imageURIsToReferencesMap = null;
+});
diff --git a/browser/base/content/test/general/browser_parsable_script.js b/browser/base/content/test/general/browser_parsable_script.js
new file mode 100644
index 000000000..50333dd65
--- /dev/null
+++ b/browser/base/content/test/general/browser_parsable_script.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' JS issues to remain, while we
+ * detect newly occurring issues in shipping JS. It is a list of regexes
+ * matching files which have errors:
+ */
+const kWhitelist = new Set([
+ /defaults\/profile\/prefs.js$/,
+ /browser\/content\/browser\/places\/controller.js$/,
+]);
+
+
+var moduleLocation = gTestPath.replace(/\/[^\/]*$/i, "/parsingTestHelpers.jsm");
+var {generateURIsFromDirTree} = Cu.import(moduleLocation, {});
+
+// Normally we would use reflect.jsm to get Reflect.parse. However, if
+// we do that, then all the AST data is allocated in reflect.jsm's
+// zone. That exposes a bug in our GC. The GC collects reflect.jsm's
+// zone but not the zone in which our test code lives (since no new
+// data is being allocated in it). The cross-compartment wrappers in
+// our zone that point to the AST data never get collected, and so the
+// AST data itself is never collected. We need to GC both zones at
+// once to fix the problem.
+const init = Components.classes["@mozilla.org/jsreflect;1"].createInstance();
+init();
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in kWhitelist
+ *
+ * @param uri the uri to check against the whitelist
+ * @return true if the uri should be skipped, false otherwise.
+ */
+function uriIsWhiteListed(uri) {
+ for (let whitelistItem of kWhitelist) {
+ if (whitelistItem.test(uri.spec)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function parsePromise(uri) {
+ let promise = new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, true);
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ let scriptText = this.responseText;
+ try {
+ info("Checking " + uri);
+ Reflect.parse(scriptText);
+ resolve(true);
+ } catch (ex) {
+ let errorMsg = "Script error reading " + uri + ": " + ex;
+ ok(false, errorMsg);
+ resolve(false);
+ }
+ }
+ };
+ xhr.onerror = (error) => {
+ ok(false, "XHR error reading " + uri + ": " + error);
+ resolve(false);
+ };
+ xhr.overrideMimeType("application/javascript");
+ xhr.send(null);
+ });
+ return promise;
+}
+
+add_task(function* checkAllTheJS() {
+ // In debug builds, even on a fast machine, collecting the file list may take
+ // more than 30 seconds, and parsing all files may take four more minutes.
+ // For this reason, this test must be explictly requested in debug builds by
+ // using the "--setpref parse=<filter>" argument to mach. You can specify:
+ // - A case-sensitive substring of the file name to test (slow).
+ // - A single absolute URI printed out by a previous run (fast).
+ // - An empty string to run the test on all files (slowest).
+ let parseRequested = Services.prefs.prefHasUserValue("parse");
+ let parseValue = parseRequested && Services.prefs.getCharPref("parse");
+ if (SpecialPowers.isDebugBuild) {
+ if (!parseRequested) {
+ ok(true, "Test disabled on debug build. To run, execute: ./mach" +
+ " mochitest-browser --setpref parse=<case_sensitive_filter>" +
+ " browser/base/content/test/general/browser_parsable_script.js");
+ return;
+ }
+ // Request a 15 minutes timeout (30 seconds * 30) for debug builds.
+ requestLongerTimeout(30);
+ }
+
+ let uris;
+ // If an absolute URI is specified on the command line, use it immediately.
+ if (parseValue && parseValue.includes(":")) {
+ uris = [NetUtil.newURI(parseValue)];
+ } else {
+ let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let startTimeMs = Date.now();
+ info("Collecting URIs");
+ uris = yield generateURIsFromDirTree(appDir, [".js", ".jsm"]);
+ info("Collected URIs in " + (Date.now() - startTimeMs) + "ms");
+
+ // Apply the filter specified on the command line, if any.
+ if (parseValue) {
+ uris = uris.filter(uri => {
+ if (uri.spec.includes(parseValue)) {
+ return true;
+ }
+ info("Not checking filtered out " + uri.spec);
+ return false;
+ });
+ }
+ }
+
+ // We create an array of promises so we can parallelize all our parsing
+ // and file loading activity:
+ let allPromises = [];
+ for (let uri of uris) {
+ if (uriIsWhiteListed(uri)) {
+ info("Not checking whitelisted " + uri.spec);
+ continue;
+ }
+ allPromises.push(parsePromise(uri.spec));
+ }
+
+ let promiseResults = yield Promise.all(allPromises);
+ is(promiseResults.filter((x) => !x).length, 0, "There should be 0 parsing errors");
+});
diff --git a/browser/base/content/test/general/browser_permissions.js b/browser/base/content/test/general/browser_permissions.js
new file mode 100644
index 000000000..721a669d2
--- /dev/null
+++ b/browser/base/content/test/general/browser_permissions.js
@@ -0,0 +1,202 @@
+/*
+ * Test the Permissions section in the Control Center.
+ */
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+const PERMISSIONS_PAGE = "http://example.com/browser/browser/base/content/test/general/permissions.html";
+var {SitePermissions} = Cu.import("resource:///modules/SitePermissions.jsm", {});
+
+registerCleanupFunction(function() {
+ SitePermissions.remove(gBrowser.currentURI, "cookie");
+ SitePermissions.remove(gBrowser.currentURI, "geo");
+ SitePermissions.remove(gBrowser.currentURI, "camera");
+ SitePermissions.remove(gBrowser.currentURI, "microphone");
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function* openIdentityPopup() {
+ let {gIdentityHandler} = gBrowser.ownerGlobal;
+ let promise = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
+ gIdentityHandler._identityBox.click();
+ return promise;
+}
+
+function* closeIdentityPopup() {
+ let {gIdentityHandler} = gBrowser.ownerGlobal;
+ let promise = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
+ gIdentityHandler._identityPopup.hidePopup();
+ return promise;
+}
+
+add_task(function* testMainViewVisible() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+
+ let permissionsList = document.getElementById("identity-popup-permission-list");
+ let emptyLabel = permissionsList.nextSibling.nextSibling;
+
+ yield openIdentityPopup();
+
+ ok(!is_hidden(emptyLabel), "List of permissions is empty");
+
+ yield closeIdentityPopup();
+
+ SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
+
+ yield openIdentityPopup();
+
+ ok(is_hidden(emptyLabel), "List of permissions is not empty");
+
+ let labelText = SitePermissions.getPermissionLabel("camera");
+ let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
+ is(labels.length, 1, "One permission visible in main view");
+ is(labels[0].textContent, labelText, "Correct value");
+
+ let img = permissionsList.querySelector("image.identity-popup-permission-icon");
+ ok(img, "There is an image for the permissions");
+ ok(img.classList.contains("camera-icon"), "proper class is in image class");
+
+ yield closeIdentityPopup();
+
+ SitePermissions.remove(gBrowser.currentURI, "camera");
+
+ yield openIdentityPopup();
+
+ ok(!is_hidden(emptyLabel), "List of permissions is empty");
+
+ yield closeIdentityPopup();
+});
+
+add_task(function* testIdentityIcon() {
+ let {gIdentityHandler} = gBrowser.ownerGlobal;
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+
+ SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
+
+ ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "identity-box signals granted permissions");
+
+ SitePermissions.remove(gBrowser.currentURI, "geo");
+
+ ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "identity-box doesn't signal granted permissions");
+
+ SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
+
+ ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "identity-box doesn't signal granted permissions");
+
+ SitePermissions.set(gBrowser.currentURI, "cookie", SitePermissions.SESSION);
+
+ ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+ "identity-box signals granted permissions");
+
+ SitePermissions.remove(gBrowser.currentURI, "geo");
+ SitePermissions.remove(gBrowser.currentURI, "camera");
+ SitePermissions.remove(gBrowser.currentURI, "cookie");
+});
+
+add_task(function* testCancelPermission() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+
+ let permissionsList = document.getElementById("identity-popup-permission-list");
+ let emptyLabel = permissionsList.nextSibling.nextSibling;
+
+ SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
+ SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
+
+ yield openIdentityPopup();
+
+ ok(is_hidden(emptyLabel), "List of permissions is not empty");
+
+ let cancelButtons = permissionsList
+ .querySelectorAll(".identity-popup-permission-remove-button");
+
+ cancelButtons[0].click();
+ let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
+ is(labels.length, 1, "One permission should be removed");
+ cancelButtons[1].click();
+ labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
+ is(labels.length, 0, "One permission should be removed");
+
+ yield closeIdentityPopup();
+});
+
+add_task(function* testPermissionHints() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+
+ let permissionsList = document.getElementById("identity-popup-permission-list");
+ let emptyHint = document.getElementById("identity-popup-permission-empty-hint");
+ let reloadHint = document.getElementById("identity-popup-permission-reload-hint");
+
+ yield openIdentityPopup();
+
+ ok(!is_hidden(emptyHint), "Empty hint is visible");
+ ok(is_hidden(reloadHint), "Reload hint is hidden");
+
+ yield closeIdentityPopup();
+
+ SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
+ SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
+
+ yield openIdentityPopup();
+
+ ok(is_hidden(emptyHint), "Empty hint is hidden");
+ ok(is_hidden(reloadHint), "Reload hint is hidden");
+
+ let cancelButtons = permissionsList
+ .querySelectorAll(".identity-popup-permission-remove-button");
+ SitePermissions.remove(gBrowser.currentURI, "camera");
+
+ cancelButtons[0].click();
+ ok(is_hidden(emptyHint), "Empty hint is hidden");
+ ok(!is_hidden(reloadHint), "Reload hint is visible");
+
+ cancelButtons[1].click();
+ ok(is_hidden(emptyHint), "Empty hint is hidden");
+ ok(!is_hidden(reloadHint), "Reload hint is visible");
+
+ yield closeIdentityPopup();
+ yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+ yield openIdentityPopup();
+
+ ok(!is_hidden(emptyHint), "Empty hint is visible after reloading");
+ ok(is_hidden(reloadHint), "Reload hint is hidden after reloading");
+
+ yield closeIdentityPopup();
+});
+
+add_task(function* testPermissionIcons() {
+ let {gIdentityHandler} = gBrowser.ownerGlobal;
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+
+ SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
+ SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.BLOCK);
+ SitePermissions.set(gBrowser.currentURI, "microphone", SitePermissions.SESSION);
+
+ let geoIcon = gIdentityHandler._identityBox
+ .querySelector(".blocked-permission-icon[data-permission-id='geo']");
+ ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown");
+
+ let cameraIcon = gIdentityHandler._identityBox
+ .querySelector(".blocked-permission-icon[data-permission-id='camera']");
+ ok(!cameraIcon.hasAttribute("showing"),
+ "allowed permission icon is not shown");
+
+ let microphoneIcon = gIdentityHandler._identityBox
+ .querySelector(".blocked-permission-icon[data-permission-id='microphone']");
+ ok(!microphoneIcon.hasAttribute("showing"),
+ "allowed permission icon is not shown");
+
+ SitePermissions.remove(gBrowser.currentURI, "geo");
+
+ ok(!geoIcon.hasAttribute("showing"),
+ "blocked permission icon is not shown after reset");
+});
diff --git a/browser/base/content/test/general/browser_pinnedTabs.js b/browser/base/content/test/general/browser_pinnedTabs.js
new file mode 100644
index 000000000..e0ddb5072
--- /dev/null
+++ b/browser/base/content/test/general/browser_pinnedTabs.js
@@ -0,0 +1,75 @@
+var tabs;
+
+function index(tab) {
+ return Array.indexOf(gBrowser.tabs, tab);
+}
+
+function indexTest(tab, expectedIndex, msg) {
+ var diag = "tab " + tab + " should be at index " + expectedIndex;
+ if (msg)
+ msg = msg + " (" + diag + ")";
+ else
+ msg = diag;
+ is(index(tabs[tab]), expectedIndex, msg);
+}
+
+function PinUnpinHandler(tab, eventName) {
+ this.eventCount = 0;
+ var self = this;
+ tab.addEventListener(eventName, function() {
+ tab.removeEventListener(eventName, arguments.callee, true);
+
+ self.eventCount++;
+ }, true);
+ gBrowser.tabContainer.addEventListener(eventName, function(e) {
+ gBrowser.tabContainer.removeEventListener(eventName, arguments.callee, true);
+
+ if (e.originalTarget == tab) {
+ self.eventCount++;
+ }
+ }, true);
+}
+
+function test() {
+ tabs = [gBrowser.selectedTab, gBrowser.addTab(), gBrowser.addTab(), gBrowser.addTab()];
+ indexTest(0, 0);
+ indexTest(1, 1);
+ indexTest(2, 2);
+ indexTest(3, 3);
+
+ var eh = new PinUnpinHandler(tabs[3], "TabPinned");
+ gBrowser.pinTab(tabs[3]);
+ is(eh.eventCount, 2, "TabPinned event should be fired");
+ indexTest(0, 1);
+ indexTest(1, 2);
+ indexTest(2, 3);
+ indexTest(3, 0);
+
+ eh = new PinUnpinHandler(tabs[1], "TabPinned");
+ gBrowser.pinTab(tabs[1]);
+ is(eh.eventCount, 2, "TabPinned event should be fired");
+ indexTest(0, 2);
+ indexTest(1, 1);
+ indexTest(2, 3);
+ indexTest(3, 0);
+
+ gBrowser.moveTabTo(tabs[3], 3);
+ indexTest(3, 1, "shouldn't be able to mix a pinned tab into normal tabs");
+
+ gBrowser.moveTabTo(tabs[2], 0);
+ indexTest(2, 2, "shouldn't be able to mix a normal tab into pinned tabs");
+
+ eh = new PinUnpinHandler(tabs[1], "TabUnpinned");
+ gBrowser.unpinTab(tabs[1]);
+ is(eh.eventCount, 2, "TabUnpinned event should be fired");
+ indexTest(1, 1, "unpinning a tab should move a tab to the start of normal tabs");
+
+ eh = new PinUnpinHandler(tabs[3], "TabUnpinned");
+ gBrowser.unpinTab(tabs[3]);
+ is(eh.eventCount, 2, "TabUnpinned event should be fired");
+ indexTest(3, 0, "unpinning a tab should move a tab to the start of normal tabs");
+
+ gBrowser.removeTab(tabs[1]);
+ gBrowser.removeTab(tabs[2]);
+ gBrowser.removeTab(tabs[3]);
+}
diff --git a/browser/base/content/test/general/browser_plainTextLinks.js b/browser/base/content/test/general/browser_plainTextLinks.js
new file mode 100644
index 000000000..7a304fce0
--- /dev/null
+++ b/browser/base/content/test/general/browser_plainTextLinks.js
@@ -0,0 +1,146 @@
+function testExpected(expected, msg) {
+ is(document.getElementById("context-openlinkincurrent").hidden, expected, msg);
+}
+
+function testLinkExpected(expected, msg) {
+ is(gContextMenu.linkURL, expected, msg);
+}
+
+add_task(function *() {
+ const url = "data:text/html;charset=UTF-8,Test For Non-Hyperlinked url selection";
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ yield SimpleTest.promiseFocus(gBrowser.selectedBrowser.contentWindowAsCPOW);
+
+ // Initial setup of the content area.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { }, function* (arg) {
+ let doc = content.document;
+ let range = doc.createRange();
+ let selection = content.getSelection();
+
+ let mainDiv = doc.createElement("div");
+ let div = doc.createElement("div");
+ let div2 = doc.createElement("div");
+ let span1 = doc.createElement("span");
+ let span2 = doc.createElement("span");
+ let span3 = doc.createElement("span");
+ let span4 = doc.createElement("span");
+ let p1 = doc.createElement("p");
+ let p2 = doc.createElement("p");
+ span1.textContent = "http://index.";
+ span2.textContent = "example.com example.com";
+ span3.textContent = " - Test";
+ span4.innerHTML = "<a href='http://www.example.com'>http://www.example.com/example</a>";
+ p1.textContent = "mailto:test.com ftp.example.com";
+ p2.textContent = "example.com -";
+ div.appendChild(span1);
+ div.appendChild(span2);
+ div.appendChild(span3);
+ div.appendChild(span4);
+ div.appendChild(p1);
+ div.appendChild(p2);
+ let p3 = doc.createElement("p");
+ p3.textContent = "main.example.com";
+ div2.appendChild(p3);
+ mainDiv.appendChild(div);
+ mainDiv.appendChild(div2);
+ doc.body.appendChild(mainDiv);
+
+ function setSelection(el1, el2, index1, index2) {
+ while (el1.nodeType != el1.TEXT_NODE)
+ el1 = el1.firstChild;
+ while (el2.nodeType != el1.TEXT_NODE)
+ el2 = el2.firstChild;
+
+ selection.removeAllRanges();
+ range.setStart(el1, index1);
+ range.setEnd(el2, index2);
+ selection.addRange(range);
+
+ return range;
+ }
+
+ // Each of these tests creates a selection and returns a range within it.
+ content.tests = [
+ () => setSelection(span1.firstChild, span2.firstChild, 0, 11),
+ () => setSelection(span1.firstChild, span2.firstChild, 7, 11),
+ () => setSelection(span1.firstChild, span2.firstChild, 8, 11),
+ () => setSelection(span2.firstChild, span2.firstChild, 0, 11),
+ () => setSelection(span2.firstChild, span2.firstChild, 11, 23),
+ () => setSelection(span2.firstChild, span2.firstChild, 0, 10),
+ () => setSelection(span2.firstChild, span3.firstChild, 12, 7),
+ () => setSelection(span2.firstChild, span2.firstChild, 12, 19),
+ () => setSelection(p1.firstChild, p1.firstChild, 0, 15),
+ () => setSelection(p1.firstChild, p1.firstChild, 16, 31),
+ () => setSelection(p2.firstChild, p2.firstChild, 0, 14),
+ () => {
+ selection.selectAllChildren(div2);
+ return selection.getRangeAt(0);
+ },
+ () => {
+ selection.selectAllChildren(span4);
+ return selection.getRangeAt(0);
+ },
+ () => {
+ mainDiv.innerHTML = "(open-suse.ru)";
+ return setSelection(mainDiv, mainDiv, 1, 13);
+ },
+ () => setSelection(mainDiv, mainDiv, 1, 14)
+ ];
+ });
+
+ let checks = [
+ () => testExpected(false, "The link context menu should show for http://www.example.com"),
+ () => testExpected(false, "The link context menu should show for www.example.com"),
+ () => testExpected(true, "The link context menu should not show for ww.example.com"),
+ () => {
+ testExpected(false, "The link context menu should show for example.com");
+ testLinkExpected("http://example.com/", "url for example.com selection should not prepend www");
+ },
+ () => testExpected(false, "The link context menu should show for example.com"),
+ () => testExpected(true, "Link options should not show for selection that's not at a word boundary"),
+ () => testExpected(true, "Link options should not show for selection that has whitespace"),
+ () => testExpected(true, "Link options should not show unless a url is selected"),
+ () => testExpected(true, "Link options should not show for mailto: links"),
+ () => {
+ testExpected(false, "Link options should show for ftp.example.com");
+ testLinkExpected("http://ftp.example.com/", "ftp.example.com should be preceeded with http://");
+ },
+ () => testExpected(false, "Link options should show for www.example.com "),
+ () => testExpected(false, "Link options should show for triple-click selections"),
+ () => testLinkExpected("http://www.example.com/", "Linkified text should open the correct link"),
+ () => {
+ testExpected(false, "Link options should show for open-suse.ru");
+ testLinkExpected("http://open-suse.ru/", "Linkified text should open the correct link");
+ },
+ () => testExpected(true, "Link options should not show for 'open-suse.ru)'")
+ ];
+
+ let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+
+ for (let testid = 0; testid < checks.length; testid++) {
+ let menuPosition = yield ContentTask.spawn(gBrowser.selectedBrowser, { testid: testid }, function* (arg) {
+ let range = content.tests[arg.testid]();
+
+ // Get the range of the selection and determine its coordinates. These
+ // coordinates will be returned to the parent process and the context menu
+ // will be opened at that location.
+ let rangeRect = range.getBoundingClientRect();
+ return [rangeRect.x + 3, rangeRect.y + 3];
+ });
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtPoint(menuPosition[0], menuPosition[1],
+ { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ checks[testid]();
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
+ contentAreaContextMenu.hidePopup();
+ yield popupHiddenPromise;
+ }
+
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/browser/base/content/test/general/browser_printpreview.js b/browser/base/content/test/general/browser_printpreview.js
new file mode 100644
index 000000000..c38fc18be
--- /dev/null
+++ b/browser/base/content/test/general/browser_printpreview.js
@@ -0,0 +1,74 @@
+let ourTab;
+
+function test() {
+ waitForExplicitFinish();
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true).then(function(tab) {
+ ourTab = tab;
+ ok(!gInPrintPreviewMode,
+ "Should NOT be in print preview mode at starting this tests");
+ // Skip access key test on platforms which don't support access key.
+ if (!/Win|Linux/.test(navigator.platform)) {
+ openPrintPreview(testClosePrintPreviewWithEscKey);
+ } else {
+ openPrintPreview(testClosePrintPreviewWithAccessKey);
+ }
+ });
+}
+
+function tidyUp() {
+ BrowserTestUtils.removeTab(ourTab).then(finish);
+}
+
+function testClosePrintPreviewWithAccessKey() {
+ EventUtils.synthesizeKey("c", { altKey: true });
+ checkPrintPreviewClosed(function (aSucceeded) {
+ ok(aSucceeded,
+ "print preview mode should be finished by access key");
+ openPrintPreview(testClosePrintPreviewWithEscKey);
+ });
+}
+
+function testClosePrintPreviewWithEscKey() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ checkPrintPreviewClosed(function (aSucceeded) {
+ ok(aSucceeded,
+ "print preview mode should be finished by Esc key press");
+ openPrintPreview(testClosePrintPreviewWithClosingWindowShortcutKey);
+ });
+}
+
+function testClosePrintPreviewWithClosingWindowShortcutKey() {
+ EventUtils.synthesizeKey("w", { accelKey: true });
+ checkPrintPreviewClosed(function (aSucceeded) {
+ ok(aSucceeded,
+ "print preview mode should be finished by closing window shortcut key");
+ tidyUp();
+ });
+}
+
+function openPrintPreview(aCallback) {
+ document.getElementById("cmd_printPreview").doCommand();
+ executeSoon(function () {
+ if (gInPrintPreviewMode) {
+ executeSoon(aCallback);
+ return;
+ }
+ executeSoon(arguments.callee);
+ });
+}
+
+function checkPrintPreviewClosed(aCallback) {
+ let count = 0;
+ executeSoon(function () {
+ if (!gInPrintPreviewMode) {
+ executeSoon(function () { aCallback(count < 1000); });
+ return;
+ }
+ if (++count == 1000) {
+ // The test might fail.
+ PrintUtils.exitPrintPreview();
+ }
+ executeSoon(arguments.callee);
+ });
+}
diff --git a/browser/base/content/test/general/browser_private_browsing_window.js b/browser/base/content/test/general/browser_private_browsing_window.js
new file mode 100644
index 000000000..607a34060
--- /dev/null
+++ b/browser/base/content/test/general/browser_private_browsing_window.js
@@ -0,0 +1,65 @@
+// Make sure that we can open private browsing windows
+
+function test() {
+ waitForExplicitFinish();
+ var nonPrivateWin = OpenBrowserWindow();
+ ok(!PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin), "OpenBrowserWindow() should open a normal window");
+ nonPrivateWin.close();
+
+ var privateWin = OpenBrowserWindow({private: true});
+ ok(PrivateBrowsingUtils.isWindowPrivate(privateWin), "OpenBrowserWindow({private: true}) should open a private window");
+
+ nonPrivateWin = OpenBrowserWindow({private: false});
+ ok(!PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin), "OpenBrowserWindow({private: false}) should open a normal window");
+ nonPrivateWin.close();
+
+ whenDelayedStartupFinished(privateWin, function() {
+ nonPrivateWin = privateWin.OpenBrowserWindow({private: false});
+ ok(!PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin), "privateWin.OpenBrowserWindow({private: false}) should open a normal window");
+
+ nonPrivateWin.close();
+
+ [
+ { normal: "menu_newNavigator", private: "menu_newPrivateWindow", accesskey: true },
+ { normal: "appmenu_newNavigator", private: "appmenu_newPrivateWindow", accesskey: false },
+ ].forEach(function(menu) {
+ let newWindow = privateWin.document.getElementById(menu.normal);
+ let newPrivateWindow = privateWin.document.getElementById(menu.private);
+ if (newWindow && newPrivateWindow) {
+ ok(!newPrivateWindow.hidden, "New Private Window menu item should be hidden");
+ isnot(newWindow.label, newPrivateWindow.label, "New Window's label shouldn't be overwritten");
+ if (menu.accesskey) {
+ isnot(newWindow.accessKey, newPrivateWindow.accessKey, "New Window's accessKey shouldn't be overwritten");
+ }
+ isnot(newWindow.command, newPrivateWindow.command, "New Window's command shouldn't be overwritten");
+ }
+ });
+
+ privateWin.close();
+
+ Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true);
+ privateWin = OpenBrowserWindow({private: true});
+ whenDelayedStartupFinished(privateWin, function() {
+ [
+ { normal: "menu_newNavigator", private: "menu_newPrivateWindow", accessKey: true },
+ { normal: "appmenu_newNavigator", private: "appmenu_newPrivateWindow", accessKey: false },
+ ].forEach(function(menu) {
+ let newWindow = privateWin.document.getElementById(menu.normal);
+ let newPrivateWindow = privateWin.document.getElementById(menu.private);
+ if (newWindow && newPrivateWindow) {
+ ok(newPrivateWindow.hidden, "New Private Window menu item should be hidden");
+ is(newWindow.label, newPrivateWindow.label, "New Window's label should be overwritten");
+ if (menu.accesskey) {
+ is(newWindow.accessKey, newPrivateWindow.accessKey, "New Window's accessKey should be overwritten");
+ }
+ is(newWindow.command, newPrivateWindow.command, "New Window's command should be overwritten");
+ }
+ });
+
+ privateWin.close();
+ Services.prefs.clearUserPref("browser.privatebrowsing.autostart");
+ finish();
+ });
+ });
+}
+
diff --git a/browser/base/content/test/general/browser_private_no_prompt.js b/browser/base/content/test/general/browser_private_no_prompt.js
new file mode 100644
index 000000000..c6c580f80
--- /dev/null
+++ b/browser/base/content/test/general/browser_private_no_prompt.js
@@ -0,0 +1,12 @@
+function test() {
+ waitForExplicitFinish();
+ var privateWin = OpenBrowserWindow({private: true});
+
+ whenDelayedStartupFinished(privateWin, function () {
+ privateWin.BrowserOpenTab();
+ privateWin.BrowserTryToCloseWindow();
+ ok(true, "didn't prompt");
+
+ executeSoon(finish);
+ });
+}
diff --git a/browser/base/content/test/general/browser_purgehistory_clears_sh.js b/browser/base/content/test/general/browser_purgehistory_clears_sh.js
new file mode 100644
index 000000000..1a1e6554d
--- /dev/null
+++ b/browser/base/content/test/general/browser_purgehistory_clears_sh.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const url = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+
+add_task(function* purgeHistoryTest() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url,
+ }, function* purgeHistoryTestInner(browser) {
+ let backButton = browser.ownerDocument.getElementById("Browser:Back");
+ let forwardButton = browser.ownerDocument.getElementById("Browser:Forward");
+
+ ok(!browser.webNavigation.canGoBack,
+ "Initial value for webNavigation.canGoBack");
+ ok(!browser.webNavigation.canGoForward,
+ "Initial value for webNavigation.canGoBack");
+ ok(backButton.hasAttribute("disabled"), "Back button is disabled");
+ ok(forwardButton.hasAttribute("disabled"), "Forward button is disabled");
+
+ yield ContentTask.spawn(browser, null, function*() {
+ let startHistory = content.history.length;
+ content.history.pushState({}, "");
+ content.history.pushState({}, "");
+ content.history.back();
+ let newHistory = content.history.length;
+ Assert.equal(startHistory, 1, "Initial SHistory size");
+ Assert.equal(newHistory, 3, "New SHistory size");
+ });
+
+ ok(browser.webNavigation.canGoBack, true,
+ "New value for webNavigation.canGoBack");
+ ok(browser.webNavigation.canGoForward, true,
+ "New value for webNavigation.canGoForward");
+ ok(!backButton.hasAttribute("disabled"), "Back button was enabled");
+ ok(!forwardButton.hasAttribute("disabled"), "Forward button was enabled");
+
+
+ let tmp = {};
+ Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tmp);
+
+ let {Sanitizer} = tmp;
+ let sanitizer = new Sanitizer();
+
+ yield sanitizer.sanitize(["history"]);
+
+ yield ContentTask.spawn(browser, null, function*() {
+ Assert.equal(content.history.length, 1, "SHistory correctly cleared");
+ });
+
+ ok(!browser.webNavigation.canGoBack,
+ "webNavigation.canGoBack correctly cleared");
+ ok(!browser.webNavigation.canGoForward,
+ "webNavigation.canGoForward correctly cleared");
+ ok(backButton.hasAttribute("disabled"), "Back button was disabled");
+ ok(forwardButton.hasAttribute("disabled"), "Forward button was disabled");
+ });
+});
diff --git a/browser/base/content/test/general/browser_refreshBlocker.js b/browser/base/content/test/general/browser_refreshBlocker.js
new file mode 100644
index 000000000..ee274f2c2
--- /dev/null
+++ b/browser/base/content/test/general/browser_refreshBlocker.js
@@ -0,0 +1,135 @@
+"use strict";
+
+const META_PAGE = "http://example.org/browser/browser/base/content/test/general/refresh_meta.sjs"
+const HEADER_PAGE = "http://example.org/browser/browser/base/content/test/general/refresh_header.sjs"
+const TARGET_PAGE = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+const PREF = "accessibility.blockautorefresh";
+
+/**
+ * Goes into the content, and simulates a meta-refresh header at a very
+ * low level, and checks to see if it was blocked. This will always cancel
+ * the refresh, regardless of whether or not the refresh was blocked.
+ *
+ * @param browser (<xul:browser>)
+ * The browser to test for refreshing.
+ * @param expectRefresh (bool)
+ * Whether or not we expect the refresh attempt to succeed.
+ * @returns Promise
+ */
+function* attemptFakeRefresh(browser, expectRefresh) {
+ yield ContentTask.spawn(browser, expectRefresh, function*(contentExpectRefresh) {
+ let URI = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
+ let refresher = docShell.QueryInterface(Ci.nsIRefreshURI);
+ refresher.refreshURI(URI, 0, false, true);
+
+ Assert.equal(refresher.refreshPending, contentExpectRefresh,
+ "Got the right refreshPending state");
+
+ if (refresher.refreshPending) {
+ // Cancel the pending refresh
+ refresher.cancelRefreshURITimers();
+ }
+
+ // The RefreshBlocker will wait until onLocationChange has
+ // been fired before it will show any notifications (see bug
+ // 1246291), so we cause this to occur manually here.
+ content.location = URI.spec + "#foo";
+ });
+}
+
+/**
+ * Tests that we can enable the blocking pref and block a refresh
+ * from occurring while showing a notification bar. Also tests that
+ * when we disable the pref, that refreshes can go through again.
+ */
+add_task(function* test_can_enable_and_block() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TARGET_PAGE,
+ }, function*(browser) {
+ // By default, we should be able to reload the page.
+ yield attemptFakeRefresh(browser, true);
+
+ yield pushPrefs(["accessibility.blockautorefresh", true]);
+
+ let notificationPromise =
+ BrowserTestUtils.waitForNotificationBar(gBrowser, browser,
+ "refresh-blocked");
+
+ yield attemptFakeRefresh(browser, false);
+
+ yield notificationPromise;
+
+ yield pushPrefs(["accessibility.blockautorefresh", false]);
+
+ // Page reloads should go through again.
+ yield attemptFakeRefresh(browser, true);
+ });
+});
+
+/**
+ * Attempts a "real" refresh by opening a tab, and then sending it to
+ * an SJS page that will attempt to cause a refresh. This will also pass
+ * a delay amount to the SJS page. The refresh should be blocked, and
+ * the notification should be shown. Once shown, the "Allow" button will
+ * be clicked, and the refresh will go through. Finally, the helper will
+ * close the tab and resolve the Promise.
+ *
+ * @param refreshPage (string)
+ * The SJS page to use. Use META_PAGE for the <meta> tag refresh
+ * case. Use HEADER_PAGE for the HTTP header case.
+ * @param delay (int)
+ * The amount, in ms, for the page to wait before attempting the
+ * refresh.
+ *
+ * @returns Promise
+ */
+function* testRealRefresh(refreshPage, delay) {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "about:blank",
+ }, function*(browser) {
+ yield pushPrefs(["accessibility.blockautorefresh", true]);
+
+ browser.loadURI(refreshPage + "?p=" + TARGET_PAGE + "&d=" + delay);
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ // Once browserLoaded resolves, all nsIWebProgressListener callbacks
+ // should have fired, so the notification should be visible.
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.currentNotification;
+
+ ok(notification, "Notification should be visible");
+ is(notification.value, "refresh-blocked",
+ "Should be showing the right notification");
+
+ // Then click the button to allow the refresh.
+ let buttons = notification.querySelectorAll(".notification-button");
+ is(buttons.length, 1, "Should have one button.");
+
+ // Prepare a Promise that should resolve when the refresh goes through
+ let refreshPromise = BrowserTestUtils.browserLoaded(browser);
+ buttons[0].click();
+
+ yield refreshPromise;
+ });
+}
+
+/**
+ * Tests the meta-tag case for both short and longer delay times.
+ */
+add_task(function* test_can_allow_refresh() {
+ yield testRealRefresh(META_PAGE, 0);
+ yield testRealRefresh(META_PAGE, 100);
+ yield testRealRefresh(META_PAGE, 500);
+});
+
+/**
+ * Tests that when a HTTP header case for both short and longer
+ * delay times.
+ */
+add_task(function* test_can_block_refresh_from_header() {
+ yield testRealRefresh(HEADER_PAGE, 0);
+ yield testRealRefresh(HEADER_PAGE, 100);
+ yield testRealRefresh(HEADER_PAGE, 500);
+});
diff --git a/browser/base/content/test/general/browser_registerProtocolHandler_notification.html b/browser/base/content/test/general/browser_registerProtocolHandler_notification.html
new file mode 100644
index 000000000..241b03b95
--- /dev/null
+++ b/browser/base/content/test/general/browser_registerProtocolHandler_notification.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<html>
+ <head>
+ <title>Protocol registrar page</title>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+ </head>
+ <body>
+ <script type="text/javascript">
+ navigator.registerProtocolHandler("testprotocol",
+ "https://example.com/foobar?uri=%s",
+ "Test Protocol");
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/browser_registerProtocolHandler_notification.js b/browser/base/content/test/general/browser_registerProtocolHandler_notification.js
new file mode 100644
index 000000000..b30ece0f6
--- /dev/null
+++ b/browser/base/content/test/general/browser_registerProtocolHandler_notification.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/. */
+
+function test() {
+ waitForExplicitFinish();
+ let notificationValue = "Protocol Registration: testprotocol";
+ let testURI = "http://example.com/browser/" +
+ "browser/base/content/test/general/browser_registerProtocolHandler_notification.html";
+
+ waitForCondition(function() {
+ // Do not start until the notification is up
+ let notificationBox = window.gBrowser.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue(notificationValue);
+ return notification;
+ },
+ function() {
+
+ let notificationBox = window.gBrowser.getNotificationBox();
+ let notification = notificationBox.getNotificationWithValue(notificationValue);
+ ok(notification, "Notification box should be displayed");
+ if (notification == null) {
+ finish();
+ return;
+ }
+ is(notification.type, "info", "We expect this notification to have the type of 'info'.");
+ isnot(notification.image, null, "We expect this notification to have an icon.");
+
+ let buttons = notification.getElementsByClassName("notification-button-default");
+ is(buttons.length, 1, "We expect see one default button.");
+
+ buttons = notification.getElementsByClassName("notification-button");
+ is(buttons.length, 1, "We expect see one button.");
+
+ let button = buttons[0];
+ isnot(button.label, null, "We expect the add button to have a label.");
+ todo_isnot(button.accesskey, null, "We expect the add button to have a accesskey.");
+
+ finish();
+ }, "Still can not get notification after retry 100 times.", 100);
+
+ window.gBrowser.selectedBrowser.loadURI(testURI);
+}
diff --git a/browser/base/content/test/general/browser_relatedTabs.js b/browser/base/content/test/general/browser_relatedTabs.js
new file mode 100644
index 000000000..97cf51d84
--- /dev/null
+++ b/browser/base/content/test/general/browser_relatedTabs.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/. */
+
+add_task(function*() {
+ is(gBrowser.tabs.length, 1, "one tab is open initially");
+
+ // Add several new tabs in sequence, interrupted by selecting a
+ // different tab, moving a tab around and closing a tab,
+ // returning a list of opened tabs for verifying the expected order.
+ // The new tab behaviour is documented in bug 465673
+ let tabs = [];
+ function addTab(aURL, aReferrer) {
+ let tab = gBrowser.addTab(aURL, {referrerURI: aReferrer});
+ tabs.push(tab);
+ return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ }
+
+ yield addTab("http://mochi.test:8888/#0");
+ gBrowser.selectedTab = tabs[0];
+ yield addTab("http://mochi.test:8888/#1");
+ yield addTab("http://mochi.test:8888/#2", gBrowser.currentURI);
+ yield addTab("http://mochi.test:8888/#3", gBrowser.currentURI);
+ gBrowser.selectedTab = tabs[tabs.length - 1];
+ gBrowser.selectedTab = tabs[0];
+ yield addTab("http://mochi.test:8888/#4", gBrowser.currentURI);
+ gBrowser.selectedTab = tabs[3];
+ yield addTab("http://mochi.test:8888/#5", gBrowser.currentURI);
+ gBrowser.removeTab(tabs.pop());
+ yield addTab("about:blank", gBrowser.currentURI);
+ gBrowser.moveTabTo(gBrowser.selectedTab, 1);
+ yield addTab("http://mochi.test:8888/#6", gBrowser.currentURI);
+ yield addTab();
+ yield addTab("http://mochi.test:8888/#7");
+
+ function testPosition(tabNum, expectedPosition, msg) {
+ is(Array.indexOf(gBrowser.tabs, tabs[tabNum]), expectedPosition, msg);
+ }
+
+ testPosition(0, 3, "tab without referrer was opened to the far right");
+ testPosition(1, 7, "tab without referrer was opened to the far right");
+ testPosition(2, 5, "tab with referrer opened immediately to the right");
+ testPosition(3, 1, "next tab with referrer opened further to the right");
+ testPosition(4, 4, "tab selection changed, tab opens immediately to the right");
+ testPosition(5, 6, "blank tab with referrer opens to the right of 3rd original tab where removed tab was");
+ testPosition(6, 2, "tab has moved, new tab opens immediately to the right");
+ testPosition(7, 8, "blank tab without referrer opens at the end");
+ testPosition(8, 9, "tab without referrer opens at the end");
+
+ tabs.forEach(gBrowser.removeTab, gBrowser);
+});
diff --git a/browser/base/content/test/general/browser_remoteTroubleshoot.js b/browser/base/content/test/general/browser_remoteTroubleshoot.js
new file mode 100644
index 000000000..5c939dbd0
--- /dev/null
+++ b/browser/base/content/test/general/browser_remoteTroubleshoot.js
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {WebChannel} = Cu.import("resource://gre/modules/WebChannel.jsm", {});
+
+const TEST_URL_TAIL = "example.com/browser/browser/base/content/test/general/test_remoteTroubleshoot.html"
+const TEST_URI_GOOD = Services.io.newURI("https://" + TEST_URL_TAIL, null, null);
+const TEST_URI_BAD = Services.io.newURI("http://" + TEST_URL_TAIL, null, null);
+const TEST_URI_GOOD_OBJECT = Services.io.newURI("https://" + TEST_URL_TAIL + "?object", null, null);
+
+// Creates a one-shot web-channel for the test data to be sent back from the test page.
+function promiseChannelResponse(channelID, originOrPermission) {
+ return new Promise((resolve, reject) => {
+ let channel = new WebChannel(channelID, originOrPermission);
+ channel.listen((id, data, target) => {
+ channel.stopListening();
+ resolve(data);
+ });
+ });
+}
+
+// Loads the specified URI in a new tab and waits for it to send us data on our
+// test web-channel and resolves with that data.
+function promiseNewChannelResponse(uri) {
+ let channelPromise = promiseChannelResponse("test-remote-troubleshooting-backchannel",
+ uri);
+ let tab = gBrowser.loadOneTab(uri.spec, { inBackground: false });
+ return promiseTabLoaded(tab).then(
+ () => channelPromise
+ ).then(data => {
+ gBrowser.removeTab(tab);
+ return data;
+ });
+}
+
+add_task(function*() {
+ // We haven't set a permission yet - so even the "good" URI should fail.
+ let got = yield promiseNewChannelResponse(TEST_URI_GOOD);
+ // Should have no data.
+ Assert.ok(got.message === undefined, "should have failed to get any data");
+
+ // Add a permission manager entry for our URI.
+ Services.perms.add(TEST_URI_GOOD,
+ "remote-troubleshooting",
+ Services.perms.ALLOW_ACTION);
+ registerCleanupFunction(() => {
+ Services.perms.remove(TEST_URI_GOOD, "remote-troubleshooting");
+ });
+
+ // Try again - now we are expecting a response with the actual data.
+ got = yield promiseNewChannelResponse(TEST_URI_GOOD);
+
+ // Check some keys we expect to always get.
+ Assert.ok(got.message.extensions, "should have extensions");
+ Assert.ok(got.message.graphics, "should have graphics");
+
+ // Check we have channel and build ID info:
+ Assert.equal(got.message.application.buildID, Services.appinfo.appBuildID,
+ "should have correct build ID");
+
+ let updateChannel = null;
+ try {
+ updateChannel = Cu.import("resource://gre/modules/UpdateUtils.jsm", {}).UpdateUtils.UpdateChannel;
+ } catch (ex) {}
+ if (!updateChannel) {
+ Assert.ok(!('updateChannel' in got.message.application),
+ "should not have update channel where not available.");
+ } else {
+ Assert.equal(got.message.application.updateChannel, updateChannel,
+ "should have correct update channel.");
+ }
+
+
+ // And check some keys we know we decline to return.
+ Assert.ok(!got.message.modifiedPreferences, "should not have a modifiedPreferences key");
+ Assert.ok(!got.message.crashes, "should not have crash info");
+
+ // Now a http:// URI - should get nothing even with the permission setup.
+ got = yield promiseNewChannelResponse(TEST_URI_BAD);
+ Assert.ok(got.message === undefined, "should have failed to get any data");
+
+ // Check that the page can send an object as well if it's in the whitelist
+ let webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+ let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+ let newWhitelist = origWhitelist + " https://example.com";
+ Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(webchannelWhitelistPref);
+ });
+ got = yield promiseNewChannelResponse(TEST_URI_GOOD_OBJECT);
+ Assert.ok(got.message, "should have gotten some data back");
+});
diff --git a/browser/base/content/test/general/browser_remoteWebNavigation_postdata.js b/browser/base/content/test/general/browser_remoteWebNavigation_postdata.js
new file mode 100644
index 000000000..451323f50
--- /dev/null
+++ b/browser/base/content/test/general/browser_remoteWebNavigation_postdata.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/. */
+
+Cu.import("resource://gre/modules/BrowserUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+function makeInputStream(aString) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.data = aString;
+ return stream; // XPConnect will QI this to nsIInputStream for us.
+}
+
+add_task(function* test_remoteWebNavigation_postdata() {
+ let obj = {};
+ Cu.import("resource://testing-common/httpd.js", obj);
+ Cu.import("resource://services-common/utils.js", obj);
+
+ let server = new obj.HttpServer();
+ server.start(-1);
+
+ let loadDeferred = Promise.defer();
+
+ server.registerPathHandler("/test", (request, response) => {
+ let body = obj.CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ is(body, "success", "request body is correct");
+ is(request.method, "POST", "request was a post");
+ response.write("Received from POST: " + body);
+ loadDeferred.resolve();
+ });
+
+ let i = server.identity;
+ let path = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort + "/test";
+
+ let postdata =
+ "Content-Length: 7\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "\r\n" +
+ "success";
+
+ openUILinkIn(path, "tab", null, makeInputStream(postdata));
+
+ yield loadDeferred.promise;
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ let serverStoppedDeferred = Promise.defer();
+ server.stop(function() { serverStoppedDeferred.resolve(); });
+ yield serverStoppedDeferred.promise;
+});
diff --git a/browser/base/content/test/general/browser_removeTabsToTheEnd.js b/browser/base/content/test/general/browser_removeTabsToTheEnd.js
new file mode 100644
index 000000000..351085d74
--- /dev/null
+++ b/browser/base/content/test/general/browser_removeTabsToTheEnd.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test() {
+ // Add two new tabs after the original tab. Pin the first one.
+ let originalTab = gBrowser.selectedTab;
+ let newTab1 = gBrowser.addTab();
+ gBrowser.addTab();
+ gBrowser.pinTab(newTab1);
+
+ // Check that there is only one closable tab from originalTab to the end
+ is(gBrowser.getTabsToTheEndFrom(originalTab).length, 1,
+ "One unpinned tab to the right");
+
+ // Remove tabs to the end
+ gBrowser.removeTabsToTheEndFrom(originalTab);
+ is(gBrowser.tabs.length, 2, "Length is 2");
+ is(gBrowser.tabs[1], originalTab, "Starting tab is not removed");
+ is(gBrowser.tabs[0], newTab1, "Pinned tab is not removed");
+
+ // Remove pinned tab
+ gBrowser.removeTab(newTab1);
+}
diff --git a/browser/base/content/test/general/browser_restore_isAppTab.js b/browser/base/content/test/general/browser_restore_isAppTab.js
new file mode 100644
index 000000000..e20974d80
--- /dev/null
+++ b/browser/base/content/test/general/browser_restore_isAppTab.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+
+const DUMMY = "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+function getMinidumpDirectory() {
+ let dir = Services.dirsvc.get('ProfD', Ci.nsIFile);
+ dir.append("minidumps");
+ return dir;
+}
+
+// This observer is needed so we can clean up all evidence of the crash so
+// the testrunner thinks things are peachy.
+var CrashObserver = {
+ observe: function(subject, topic, data) {
+ is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
+ ok(subject instanceof Ci.nsIPropertyBag2,
+ 'Subject implements nsIPropertyBag2.');
+ // we might see this called as the process terminates due to previous tests.
+ // We are only looking for "abnormal" exits...
+ if (!subject.hasKey("abnormal")) {
+ info("This is a normal termination and isn't the one we are looking for...");
+ return;
+ }
+
+ let dumpID;
+ if ('nsICrashReporter' in Ci) {
+ dumpID = subject.getPropertyAsAString('dumpID');
+ ok(dumpID, "dumpID is present and not an empty string");
+ }
+
+ if (dumpID) {
+ let minidumpDirectory = getMinidumpDirectory();
+ let file = minidumpDirectory.clone();
+ file.append(dumpID + '.dmp');
+ file.remove(true);
+ file = minidumpDirectory.clone();
+ file.append(dumpID + '.extra');
+ file.remove(true);
+ }
+ }
+}
+Services.obs.addObserver(CrashObserver, 'ipc:content-shutdown', false);
+
+registerCleanupFunction(() => {
+ Services.obs.removeObserver(CrashObserver, 'ipc:content-shutdown');
+});
+
+function frameScript() {
+ addMessageListener("Test:GetIsAppTab", function() {
+ sendAsyncMessage("Test:IsAppTab", { isAppTab: docShell.isAppTab });
+ });
+
+ addMessageListener("Test:Crash", function() {
+ privateNoteIntentionalCrash();
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+ let zero = new ctypes.intptr_t(8);
+ let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
+ badptr.contents
+ });
+}
+
+function loadFrameScript(browser) {
+ browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true);
+}
+
+function isBrowserAppTab(browser) {
+ return new Promise(resolve => {
+ function listener({ data }) {
+ browser.messageManager.removeMessageListener("Test:IsAppTab", listener);
+ resolve(data.isAppTab);
+ }
+ // It looks like same-process messages may be reordered by the message
+ // manager, so we need to wait one tick before sending the message.
+ executeSoon(function () {
+ browser.messageManager.addMessageListener("Test:IsAppTab", listener);
+ browser.messageManager.sendAsyncMessage("Test:GetIsAppTab");
+ });
+ });
+}
+
+// Restarts the child process by crashing it then reloading the tab
+var restart = Task.async(function*(browser) {
+ // If the tab isn't remote this would crash the main process so skip it
+ if (!browser.isRemoteBrowser)
+ return;
+
+ // Make sure the main process has all of the current tab state before crashing
+ yield TabStateFlusher.flush(browser);
+
+ browser.messageManager.sendAsyncMessage("Test:Crash");
+ yield promiseWaitForEvent(browser, "AboutTabCrashedLoad", false, true);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ SessionStore.reviveCrashedTab(tab);
+
+ yield promiseTabLoaded(tab);
+});
+
+add_task(function* navigate() {
+ let tab = gBrowser.addTab("about:robots");
+ let browser = tab.linkedBrowser;
+ gBrowser.selectedTab = tab;
+ yield waitForDocLoadComplete();
+ loadFrameScript(browser);
+ let isAppTab = yield isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = yield isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.loadURI(DUMMY);
+ yield waitForDocLoadComplete();
+ loadFrameScript(browser);
+ isAppTab = yield isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.unpinTab(tab);
+ isAppTab = yield isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = yield isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.loadURI("about:robots");
+ yield waitForDocLoadComplete();
+ loadFrameScript(browser);
+ isAppTab = yield isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* crash() {
+ if (!gMultiProcessBrowser || !("nsICrashReporter" in Ci))
+ return;
+
+ let tab = gBrowser.addTab(DUMMY);
+ let browser = tab.linkedBrowser;
+ gBrowser.selectedTab = tab;
+ yield waitForDocLoadComplete();
+ loadFrameScript(browser);
+ let isAppTab = yield isBrowserAppTab(browser);
+ ok(!isAppTab, "Docshell shouldn't think it is an app tab");
+
+ gBrowser.pinTab(tab);
+ isAppTab = yield isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ yield restart(browser);
+ loadFrameScript(browser);
+ isAppTab = yield isBrowserAppTab(browser);
+ ok(isAppTab, "Docshell should think it is an app tab");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js b/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
new file mode 100644
index 000000000..4f4f5c398
--- /dev/null
+++ b/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
@@ -0,0 +1,39 @@
+// Bug 474792 - Clear "Never remember passwords for this site" when
+// clearing site-specific settings in Clear Recent History dialog
+
+var tempScope = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+var Sanitizer = tempScope.Sanitizer;
+
+add_task(function*() {
+ var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+
+ // Add a disabled host
+ pwmgr.setLoginSavingEnabled("http://example.com", false);
+ // Sanity check
+ is(pwmgr.getLoginSavingEnabled("http://example.com"), false,
+ "example.com should be disabled for password saving since we haven't cleared that yet.");
+
+ // Set up the sanitizer to just clear siteSettings
+ let s = new Sanitizer();
+ s.ignoreTimespan = false;
+ s.prefDomain = "privacy.cpd.";
+ var itemPrefs = gPrefService.getBranch(s.prefDomain);
+ itemPrefs.setBoolPref("history", false);
+ itemPrefs.setBoolPref("downloads", false);
+ itemPrefs.setBoolPref("cache", false);
+ itemPrefs.setBoolPref("cookies", false);
+ itemPrefs.setBoolPref("formdata", false);
+ itemPrefs.setBoolPref("offlineApps", false);
+ itemPrefs.setBoolPref("passwords", false);
+ itemPrefs.setBoolPref("sessions", false);
+ itemPrefs.setBoolPref("siteSettings", true);
+
+ // Clear it
+ yield s.sanitize();
+
+ // Make sure it's gone
+ is(pwmgr.getLoginSavingEnabled("http://example.com"), true,
+ "example.com should be enabled for password saving again now that we've cleared.");
+});
diff --git a/browser/base/content/test/general/browser_sanitize-sitepermissions.js b/browser/base/content/test/general/browser_sanitize-sitepermissions.js
new file mode 100644
index 000000000..1b43d62fc
--- /dev/null
+++ b/browser/base/content/test/general/browser_sanitize-sitepermissions.js
@@ -0,0 +1,52 @@
+// Bug 380852 - Delete permission manager entries in Clear Recent History
+
+var tempScope = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+var Sanitizer = tempScope.Sanitizer;
+
+function countPermissions() {
+ let result = 0;
+ let enumerator = Services.perms.enumerator;
+ while (enumerator.hasMoreElements()) {
+ result++;
+ enumerator.getNext();
+ }
+ return result;
+}
+
+add_task(function* test() {
+ // sanitize before we start so we have a good baseline.
+ // Set up the sanitizer to just clear siteSettings
+ let s = new Sanitizer();
+ s.ignoreTimespan = false;
+ s.prefDomain = "privacy.cpd.";
+ var itemPrefs = gPrefService.getBranch(s.prefDomain);
+ itemPrefs.setBoolPref("history", false);
+ itemPrefs.setBoolPref("downloads", false);
+ itemPrefs.setBoolPref("cache", false);
+ itemPrefs.setBoolPref("cookies", false);
+ itemPrefs.setBoolPref("formdata", false);
+ itemPrefs.setBoolPref("offlineApps", false);
+ itemPrefs.setBoolPref("passwords", false);
+ itemPrefs.setBoolPref("sessions", false);
+ itemPrefs.setBoolPref("siteSettings", true);
+ s.sanitize();
+
+ // Count how many permissions we start with - some are defaults that
+ // will not be sanitized.
+ let numAtStart = countPermissions();
+
+ // Add a permission entry
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com"), "testing", pm.ALLOW_ACTION);
+
+ // Sanity check
+ ok(pm.enumerator.hasMoreElements(), "Permission manager should have elements, since we just added one");
+
+ // Clear it
+ yield s.sanitize();
+
+ // Make sure it's gone
+ is(numAtStart, countPermissions(), "Permission manager should have the same count it started with");
+});
diff --git a/browser/base/content/test/general/browser_sanitize-timespans.js b/browser/base/content/test/general/browser_sanitize-timespans.js
new file mode 100644
index 000000000..3712c5e1c
--- /dev/null
+++ b/browser/base/content/test/general/browser_sanitize-timespans.js
@@ -0,0 +1,733 @@
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+requestLongerTimeout(2);
+
+// Bug 453440 - Test the timespan-based logic of the sanitizer code
+var now_mSec = Date.now();
+var now_uSec = now_mSec * 1000;
+
+const kMsecPerMin = 60 * 1000;
+const kUsecPerMin = 60 * 1000000;
+
+var tempScope = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+var Sanitizer = tempScope.Sanitizer;
+
+var FormHistory = (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
+var Downloads = (Components.utils.import("resource://gre/modules/Downloads.jsm", {})).Downloads;
+
+function promiseFormHistoryRemoved() {
+ let deferred = Promise.defer();
+ Services.obs.addObserver(function onfh() {
+ Services.obs.removeObserver(onfh, "satchel-storage-changed", false);
+ deferred.resolve();
+ }, "satchel-storage-changed", false);
+ return deferred.promise;
+}
+
+function promiseDownloadRemoved(list) {
+ let deferred = Promise.defer();
+
+ let view = {
+ onDownloadRemoved: function(download) {
+ list.removeView(view);
+ deferred.resolve();
+ }
+ };
+
+ list.addView(view);
+
+ return deferred.promise;
+}
+
+add_task(function* test() {
+ yield setupDownloads();
+ yield setupFormHistory();
+ yield setupHistory();
+ yield onHistoryReady();
+});
+
+function countEntries(name, message, check) {
+ let deferred = Promise.defer();
+
+ var obj = {};
+ if (name !== null)
+ obj.fieldname = name;
+
+ let count;
+ FormHistory.count(obj, { handleResult: result => count = result,
+ handleError: function (error) {
+ deferred.reject(error)
+ throw new Error("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function (reason) {
+ if (!reason) {
+ check(count, message);
+ deferred.resolve();
+ }
+ },
+ });
+
+ return deferred.promise;
+}
+
+function* onHistoryReady() {
+ var hoursSinceMidnight = new Date().getHours();
+ var minutesSinceMidnight = hoursSinceMidnight * 60 + new Date().getMinutes();
+
+ // Should test cookies here, but nsICookieManager/nsICookieService
+ // doesn't let us fake creation times. bug 463127
+
+ let s = new Sanitizer();
+ s.ignoreTimespan = false;
+ s.prefDomain = "privacy.cpd.";
+ var itemPrefs = gPrefService.getBranch(s.prefDomain);
+ itemPrefs.setBoolPref("history", true);
+ itemPrefs.setBoolPref("downloads", true);
+ itemPrefs.setBoolPref("cache", false);
+ itemPrefs.setBoolPref("cookies", false);
+ itemPrefs.setBoolPref("formdata", true);
+ itemPrefs.setBoolPref("offlineApps", false);
+ itemPrefs.setBoolPref("passwords", false);
+ itemPrefs.setBoolPref("sessions", false);
+ itemPrefs.setBoolPref("siteSettings", false);
+
+ let publicList = yield Downloads.getList(Downloads.PUBLIC);
+ let downloadPromise = promiseDownloadRemoved(publicList);
+ let formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 10 minutes ago
+ s.range = [now_uSec - 10*60*1000000, now_uSec];
+ yield s.sanitize();
+ s.range = null;
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ ok(!(yield promiseIsURIVisited(makeURI("http://10minutes.com"))),
+ "Pretend visit to 10minutes.com should now be deleted");
+ ok((yield promiseIsURIVisited(makeURI("http://1hour.com"))),
+ "Pretend visit to 1hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
+ "Pretend visit to 1hour10minutes.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))),
+ "Pretend visit to 2hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
+ "Pretend visit to 2hour10minutes.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
+ "Pretend visit to 4hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
+ "Pretend visit to 4hour10minutes.com should should still exist");
+ if (minutesSinceMidnight > 10) {
+ ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
+ "Pretend visit to today.com should still exist");
+ }
+ ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should still exist");
+
+ let checkZero = function(num, message) { is(num, 0, message); }
+ let checkOne = function(num, message) { is(num, 1, message); }
+
+ yield countEntries("10minutes", "10minutes form entry should be deleted", checkZero);
+ yield countEntries("1hour", "1hour form entry should still exist", checkOne);
+ yield countEntries("1hour10minutes", "1hour10minutes form entry should still exist", checkOne);
+ yield countEntries("2hour", "2hour form entry should still exist", checkOne);
+ yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+ yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+ yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+ if (minutesSinceMidnight > 10)
+ yield countEntries("today", "today form entry should still exist", checkOne);
+ yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+
+ ok(!(yield downloadExists(publicList, "fakefile-10-minutes")), "10 minute download should now be deleted");
+ ok((yield downloadExists(publicList, "fakefile-1-hour")), "<1 hour download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-1-hour-10-minutes")), "1 hour 10 minute download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
+
+ if (minutesSinceMidnight > 10)
+ ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 1 hour
+ Sanitizer.prefs.setIntPref("timeSpan", 1);
+ yield s.sanitize();
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ ok(!(yield promiseIsURIVisited(makeURI("http://1hour.com"))),
+ "Pretend visit to 1hour.com should now be deleted");
+ ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
+ "Pretend visit to 1hour10minutes.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))),
+ "Pretend visit to 2hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
+ "Pretend visit to 2hour10minutes.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
+ "Pretend visit to 4hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
+ "Pretend visit to 4hour10minutes.com should should still exist");
+ if (hoursSinceMidnight > 1) {
+ ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
+ "Pretend visit to today.com should still exist");
+ }
+ ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should still exist");
+
+ yield countEntries("1hour", "1hour form entry should be deleted", checkZero);
+ yield countEntries("1hour10minutes", "1hour10minutes form entry should still exist", checkOne);
+ yield countEntries("2hour", "2hour form entry should still exist", checkOne);
+ yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+ yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+ yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+ if (hoursSinceMidnight > 1)
+ yield countEntries("today", "today form entry should still exist", checkOne);
+ yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+
+ ok(!(yield downloadExists(publicList, "fakefile-1-hour")), "<1 hour download should now be deleted");
+ ok((yield downloadExists(publicList, "fakefile-1-hour-10-minutes")), "1 hour 10 minute download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
+
+ if (hoursSinceMidnight > 1)
+ ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 1 hour 10 minutes
+ s.range = [now_uSec - 70*60*1000000, now_uSec];
+ yield s.sanitize();
+ s.range = null;
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ ok(!(yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
+ "Pretend visit to 1hour10minutes.com should now be deleted");
+ ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))),
+ "Pretend visit to 2hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
+ "Pretend visit to 2hour10minutes.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
+ "Pretend visit to 4hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
+ "Pretend visit to 4hour10minutes.com should should still exist");
+ if (minutesSinceMidnight > 70) {
+ ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
+ "Pretend visit to today.com should still exist");
+ }
+ ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should still exist");
+
+ yield countEntries("1hour10minutes", "1hour10minutes form entry should be deleted", checkZero);
+ yield countEntries("2hour", "2hour form entry should still exist", checkOne);
+ yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+ yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+ yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+ if (minutesSinceMidnight > 70)
+ yield countEntries("today", "today form entry should still exist", checkOne);
+ yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+
+ ok(!(yield downloadExists(publicList, "fakefile-1-hour-10-minutes")), "1 hour 10 minute old download should now be deleted");
+ ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
+ if (minutesSinceMidnight > 70)
+ ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 2 hours
+ Sanitizer.prefs.setIntPref("timeSpan", 2);
+ yield s.sanitize();
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ ok(!(yield promiseIsURIVisited(makeURI("http://2hour.com"))),
+ "Pretend visit to 2hour.com should now be deleted");
+ ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
+ "Pretend visit to 2hour10minutes.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
+ "Pretend visit to 4hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
+ "Pretend visit to 4hour10minutes.com should should still exist");
+ if (hoursSinceMidnight > 2) {
+ ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
+ "Pretend visit to today.com should still exist");
+ }
+ ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should still exist");
+
+ yield countEntries("2hour", "2hour form entry should be deleted", checkZero);
+ yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+ yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+ yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+ if (hoursSinceMidnight > 2)
+ yield countEntries("today", "today form entry should still exist", checkOne);
+ yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+
+ ok(!(yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should now be deleted");
+ ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
+ if (hoursSinceMidnight > 2)
+ ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 2 hours 10 minutes
+ s.range = [now_uSec - 130*60*1000000, now_uSec];
+ yield s.sanitize();
+ s.range = null;
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ ok(!(yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
+ "Pretend visit to 2hour10minutes.com should now be deleted");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))),
+ "Pretend visit to 4hour.com should should still exist");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
+ "Pretend visit to 4hour10minutes.com should should still exist");
+ if (minutesSinceMidnight > 130) {
+ ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
+ "Pretend visit to today.com should still exist");
+ }
+ ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should still exist");
+
+ yield countEntries("2hour10minutes", "2hour10minutes form entry should be deleted", checkZero);
+ yield countEntries("4hour", "4hour form entry should still exist", checkOne);
+ yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+ if (minutesSinceMidnight > 130)
+ yield countEntries("today", "today form entry should still exist", checkOne);
+ yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+
+ ok(!(yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute old download should now be deleted");
+ ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ if (minutesSinceMidnight > 130)
+ ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 4 hours
+ Sanitizer.prefs.setIntPref("timeSpan", 3);
+ yield s.sanitize();
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ ok(!(yield promiseIsURIVisited(makeURI("http://4hour.com"))),
+ "Pretend visit to 4hour.com should now be deleted");
+ ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
+ "Pretend visit to 4hour10minutes.com should should still exist");
+ if (hoursSinceMidnight > 4) {
+ ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
+ "Pretend visit to today.com should still exist");
+ }
+ ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should still exist");
+
+ yield countEntries("4hour", "4hour form entry should be deleted", checkZero);
+ yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+ if (hoursSinceMidnight > 4)
+ yield countEntries("today", "today form entry should still exist", checkOne);
+ yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+
+ ok(!(yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should now be deleted");
+ ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
+ ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ if (hoursSinceMidnight > 4)
+ ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear 4 hours 10 minutes
+ s.range = [now_uSec - 250*60*1000000, now_uSec];
+ yield s.sanitize();
+ s.range = null;
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ ok(!(yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
+ "Pretend visit to 4hour10minutes.com should now be deleted");
+ if (minutesSinceMidnight > 250) {
+ ok((yield promiseIsURIVisited(makeURI("http://today.com"))),
+ "Pretend visit to today.com should still exist");
+ }
+ ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should still exist");
+
+ yield countEntries("4hour10minutes", "4hour10minutes form entry should be deleted", checkZero);
+ if (minutesSinceMidnight > 250)
+ yield countEntries("today", "today form entry should still exist", checkOne);
+ yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+
+ ok(!(yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should now be deleted");
+ ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ if (minutesSinceMidnight > 250)
+ ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+
+ // The 'Today' download might have been already deleted, in which case we
+ // should not wait for a download removal notification.
+ if (minutesSinceMidnight > 250) {
+ downloadPromise = promiseDownloadRemoved(publicList);
+ } else {
+ downloadPromise = Promise.resolve();
+ }
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Clear Today
+ Sanitizer.prefs.setIntPref("timeSpan", 4);
+ yield s.sanitize();
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ // Be careful. If we add our objectss just before midnight, and sanitize
+ // runs immediately after, they won't be expired. This is expected, but
+ // we should not test in that case. We cannot just test for opposite
+ // condition because we could cross midnight just one moment after we
+ // cache our time, then we would have an even worse random failure.
+ var today = isToday(new Date(now_mSec));
+ if (today) {
+ ok(!(yield promiseIsURIVisited(makeURI("http://today.com"))),
+ "Pretend visit to today.com should now be deleted");
+
+ yield countEntries("today", "today form entry should be deleted", checkZero);
+ ok(!(yield downloadExists(publicList, "fakefile-today")), "'Today' download should now be deleted");
+ }
+
+ ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should still exist");
+ yield countEntries("b4today", "b4today form entry should still exist", checkOne);
+ ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+
+ downloadPromise = promiseDownloadRemoved(publicList);
+ formHistoryPromise = promiseFormHistoryRemoved();
+
+ // Choose everything
+ Sanitizer.prefs.setIntPref("timeSpan", 0);
+ yield s.sanitize();
+
+ yield formHistoryPromise;
+ yield downloadPromise;
+
+ ok(!(yield promiseIsURIVisited(makeURI("http://before-today.com"))),
+ "Pretend visit to before-today.com should now be deleted");
+
+ yield countEntries("b4today", "b4today form entry should be deleted", checkZero);
+
+ ok(!(yield downloadExists(publicList, "fakefile-old")), "Year old download should now be deleted");
+}
+
+function setupHistory() {
+ let deferred = Promise.defer();
+
+ let places = [];
+
+ function addPlace(aURI, aTitle, aVisitDate) {
+ places.push({
+ uri: aURI,
+ title: aTitle,
+ visits: [{
+ visitDate: aVisitDate,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK
+ }]
+ });
+ }
+
+ addPlace(makeURI("http://10minutes.com/"), "10 minutes ago", now_uSec - 10 * kUsecPerMin);
+ addPlace(makeURI("http://1hour.com/"), "Less than 1 hour ago", now_uSec - 45 * kUsecPerMin);
+ addPlace(makeURI("http://1hour10minutes.com/"), "1 hour 10 minutes ago", now_uSec - 70 * kUsecPerMin);
+ addPlace(makeURI("http://2hour.com/"), "Less than 2 hours ago", now_uSec - 90 * kUsecPerMin);
+ addPlace(makeURI("http://2hour10minutes.com/"), "2 hours 10 minutes ago", now_uSec - 130 * kUsecPerMin);
+ addPlace(makeURI("http://4hour.com/"), "Less than 4 hours ago", now_uSec - 180 * kUsecPerMin);
+ addPlace(makeURI("http://4hour10minutes.com/"), "4 hours 10 minutesago", now_uSec - 250 * kUsecPerMin);
+
+ let today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(1);
+ addPlace(makeURI("http://today.com/"), "Today", today.getTime() * 1000);
+
+ let lastYear = new Date();
+ lastYear.setFullYear(lastYear.getFullYear() - 1);
+ addPlace(makeURI("http://before-today.com/"), "Before Today", lastYear.getTime() * 1000);
+ PlacesUtils.asyncHistory.updatePlaces(places, {
+ handleError: () => ok(false, "Unexpected error in adding visit."),
+ handleResult: () => { },
+ handleCompletion: () => deferred.resolve()
+ });
+
+ return deferred.promise;
+}
+
+function* setupFormHistory() {
+
+ function searchEntries(terms, params) {
+ let deferred = Promise.defer();
+
+ let results = [];
+ FormHistory.search(terms, params, { handleResult: result => results.push(result),
+ handleError: function (error) {
+ deferred.reject(error);
+ throw new Error("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function (reason) { deferred.resolve(results); }
+ });
+ return deferred.promise;
+ }
+
+ function update(changes)
+ {
+ let deferred = Promise.defer();
+ FormHistory.update(changes, { handleError: function (error) {
+ deferred.reject(error);
+ throw new Error("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function (reason) { deferred.resolve(); }
+ });
+ return deferred.promise;
+ }
+
+ // Make sure we've got a clean DB to start with, then add the entries we'll be testing.
+ yield update(
+ [{
+ op: "remove"
+ },
+ {
+ op : "add",
+ fieldname : "10minutes",
+ value : "10m"
+ }, {
+ op : "add",
+ fieldname : "1hour",
+ value : "1h"
+ }, {
+ op : "add",
+ fieldname : "1hour10minutes",
+ value : "1h10m"
+ }, {
+ op : "add",
+ fieldname : "2hour",
+ value : "2h"
+ }, {
+ op : "add",
+ fieldname : "2hour10minutes",
+ value : "2h10m"
+ }, {
+ op : "add",
+ fieldname : "4hour",
+ value : "4h"
+ }, {
+ op : "add",
+ fieldname : "4hour10minutes",
+ value : "4h10m"
+ }, {
+ op : "add",
+ fieldname : "today",
+ value : "1d"
+ }, {
+ op : "add",
+ fieldname : "b4today",
+ value : "1y"
+ }]);
+
+ // Artifically age the entries to the proper vintage.
+ let timestamp = now_uSec - 10 * kUsecPerMin;
+ let results = yield searchEntries(["guid"], { fieldname: "10minutes" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 45 * kUsecPerMin;
+ results = yield searchEntries(["guid"], { fieldname: "1hour" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 70 * kUsecPerMin;
+ results = yield searchEntries(["guid"], { fieldname: "1hour10minutes" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 90 * kUsecPerMin;
+ results = yield searchEntries(["guid"], { fieldname: "2hour" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 130 * kUsecPerMin;
+ results = yield searchEntries(["guid"], { fieldname: "2hour10minutes" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 180 * kUsecPerMin;
+ results = yield searchEntries(["guid"], { fieldname: "4hour" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ timestamp = now_uSec - 250 * kUsecPerMin;
+ results = yield searchEntries(["guid"], { fieldname: "4hour10minutes" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ let today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(1);
+ timestamp = today.getTime() * 1000;
+ results = yield searchEntries(["guid"], { fieldname: "today" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ let lastYear = new Date();
+ lastYear.setFullYear(lastYear.getFullYear() - 1);
+ timestamp = lastYear.getTime() * 1000;
+ results = yield searchEntries(["guid"], { fieldname: "b4today" });
+ yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid });
+
+ var checks = 0;
+ let checkOne = function(num, message) { is(num, 1, message); checks++; }
+
+ // Sanity check.
+ yield countEntries("10minutes", "Checking for 10minutes form history entry creation", checkOne);
+ yield countEntries("1hour", "Checking for 1hour form history entry creation", checkOne);
+ yield countEntries("1hour10minutes", "Checking for 1hour10minutes form history entry creation", checkOne);
+ yield countEntries("2hour", "Checking for 2hour form history entry creation", checkOne);
+ yield countEntries("2hour10minutes", "Checking for 2hour10minutes form history entry creation", checkOne);
+ yield countEntries("4hour", "Checking for 4hour form history entry creation", checkOne);
+ yield countEntries("4hour10minutes", "Checking for 4hour10minutes form history entry creation", checkOne);
+ yield countEntries("today", "Checking for today form history entry creation", checkOne);
+ yield countEntries("b4today", "Checking for b4today form history entry creation", checkOne);
+ is(checks, 9, "9 checks made");
+}
+
+function* setupDownloads() {
+
+ let publicList = yield Downloads.getList(Downloads.PUBLIC);
+
+ let download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: "fakefile-10-minutes"
+ });
+ download.startTime = new Date(now_mSec - 10 * kMsecPerMin), // 10 minutes ago
+ download.canceled = true;
+ yield publicList.add(download);
+
+ download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-1-hour"
+ });
+ download.startTime = new Date(now_mSec - 45 * kMsecPerMin), // 45 minutes ago
+ download.canceled = true;
+ yield publicList.add(download);
+
+ download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: "fakefile-1-hour-10-minutes"
+ });
+ download.startTime = new Date(now_mSec - 70 * kMsecPerMin), // 70 minutes ago
+ download.canceled = true;
+ yield publicList.add(download);
+
+ download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-2-hour"
+ });
+ download.startTime = new Date(now_mSec - 90 * kMsecPerMin), // 90 minutes ago
+ download.canceled = true;
+ yield publicList.add(download);
+
+ download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: "fakefile-2-hour-10-minutes"
+ });
+ download.startTime = new Date(now_mSec - 130 * kMsecPerMin), // 130 minutes ago
+ download.canceled = true;
+ yield publicList.add(download);
+
+ download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-4-hour"
+ });
+ download.startTime = new Date(now_mSec - 180 * kMsecPerMin), // 180 minutes ago
+ download.canceled = true;
+ yield publicList.add(download);
+
+ download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: "fakefile-4-hour-10-minutes"
+ });
+ download.startTime = new Date(now_mSec - 250 * kMsecPerMin), // 250 minutes ago
+ download.canceled = true;
+ yield publicList.add(download);
+
+ // Add "today" download
+ let today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(1);
+
+ download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-today"
+ });
+ download.startTime = today, // 12:00:01 AM this morning
+ download.canceled = true;
+ yield publicList.add(download);
+
+ // Add "before today" download
+ let lastYear = new Date();
+ lastYear.setFullYear(lastYear.getFullYear() - 1);
+
+ download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
+ target: "fakefile-old"
+ });
+ download.startTime = lastYear,
+ download.canceled = true;
+ yield publicList.add(download);
+
+ // Confirm everything worked
+ let downloads = yield publicList.getAll();
+ is(downloads.length, 9, "9 Pretend downloads added");
+
+ ok((yield downloadExists(publicList, "fakefile-old")), "Pretend download for everything case should exist");
+ ok((yield downloadExists(publicList, "fakefile-10-minutes")), "Pretend download for 10-minutes case should exist");
+ ok((yield downloadExists(publicList, "fakefile-1-hour")), "Pretend download for 1-hour case should exist");
+ ok((yield downloadExists(publicList, "fakefile-1-hour-10-minutes")), "Pretend download for 1-hour-10-minutes case should exist");
+ ok((yield downloadExists(publicList, "fakefile-2-hour")), "Pretend download for 2-hour case should exist");
+ ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "Pretend download for 2-hour-10-minutes case should exist");
+ ok((yield downloadExists(publicList, "fakefile-4-hour")), "Pretend download for 4-hour case should exist");
+ ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "Pretend download for 4-hour-10-minutes case should exist");
+ ok((yield downloadExists(publicList, "fakefile-today")), "Pretend download for Today case should exist");
+}
+
+/**
+ * Checks to see if the downloads with the specified id exists.
+ *
+ * @param aID
+ * The ids of the downloads to check.
+ */
+let downloadExists = Task.async(function* (list, path) {
+ let listArray = yield list.getAll();
+ return listArray.some(i => i.target.path == path);
+});
+
+function isToday(aDate) {
+ return aDate.getDate() == new Date().getDate();
+}
diff --git a/browser/base/content/test/general/browser_sanitizeDialog.js b/browser/base/content/test/general/browser_sanitizeDialog.js
new file mode 100644
index 000000000..50546be45
--- /dev/null
+++ b/browser/base/content/test/general/browser_sanitizeDialog.js
@@ -0,0 +1,1027 @@
+/* -*- 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/. */
+
+/**
+ * Tests the sanitize dialog (a.k.a. the clear recent history dialog).
+ * See bug 480169.
+ *
+ * The purpose of this test is not to fully flex the sanitize timespan code;
+ * browser/base/content/test/general/browser_sanitize-timespans.js does that. This
+ * test checks the UI of the dialog and makes sure it's correctly connected to
+ * the sanitize timespan code.
+ *
+ * Some of this code, especially the history creation parts, was taken from
+ * browser/base/content/test/general/browser_sanitize-timespans.js.
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+var {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Timer",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+
+var tempScope = {};
+Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+var Sanitizer = tempScope.Sanitizer;
+
+const kMsecPerMin = 60 * 1000;
+const kUsecPerMin = 60 * 1000000;
+
+add_task(function* init() {
+ requestLongerTimeout(3);
+ yield blankSlate();
+ registerCleanupFunction(function* () {
+ yield blankSlate();
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ });
+});
+
+/**
+ * Initializes the dialog to its default state.
+ */
+add_task(function* default_state() {
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ // Select "Last Hour"
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ // Hide details
+ if (!this.getItemList().collapsed)
+ this.toggleDetails();
+ this.acceptDialog();
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+/**
+ * Cancels the dialog, makes sure history not cleared.
+ */
+add_task(function* test_cancel() {
+ // Add history (within the past hour)
+ let uris = [];
+ let places = [];
+ let pURI;
+ for (let i = 0; i < 30; i++) {
+ pURI = makeURI("http://" + i + "-minutes-ago.com/");
+ places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
+ uris.push(pURI);
+ }
+ yield PlacesTestUtils.addVisits(places);
+
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ this.checkPrefCheckbox("history", false);
+ this.checkDetails(false);
+
+ // Show details
+ this.toggleDetails();
+ this.checkDetails(true);
+
+ // Hide details
+ this.toggleDetails();
+ this.checkDetails(false);
+ this.cancelDialog();
+ };
+ wh.onunload = function* () {
+ yield promiseHistoryClearedState(uris, false);
+ yield blankSlate();
+ yield promiseHistoryClearedState(uris, true);
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+/**
+ * Ensures that the combined history-downloads checkbox clears both history
+ * visits and downloads when checked; the dialog respects simple timespan.
+ */
+add_task(function* test_history_downloads_checked() {
+ // Add downloads (within the past hour).
+ let downloadIDs = [];
+ for (let i = 0; i < 5; i++) {
+ yield addDownloadWithMinutesAgo(downloadIDs, i);
+ }
+ // Add downloads (over an hour ago).
+ let olderDownloadIDs = [];
+ for (let i = 0; i < 5; i++) {
+ yield addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i);
+ }
+
+ // Add history (within the past hour).
+ let uris = [];
+ let places = [];
+ let pURI;
+ for (let i = 0; i < 30; i++) {
+ pURI = makeURI("http://" + i + "-minutes-ago.com/");
+ places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
+ uris.push(pURI);
+ }
+ // Add history (over an hour ago).
+ let olderURIs = [];
+ for (let i = 0; i < 5; i++) {
+ pURI = makeURI("http://" + (61 + i) + "-minutes-ago.com/");
+ places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i)});
+ olderURIs.push(pURI);
+ }
+ let promiseSanitized = promiseSanitizationComplete();
+
+ yield PlacesTestUtils.addVisits(places);
+
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ this.checkPrefCheckbox("history", true);
+ this.acceptDialog();
+ };
+ wh.onunload = function* () {
+ intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_HOUR,
+ "timeSpan pref should be hour after accepting dialog with " +
+ "hour selected");
+ boolPrefIs("cpd.history", true,
+ "history pref should be true after accepting dialog with " +
+ "history checkbox checked");
+ boolPrefIs("cpd.downloads", true,
+ "downloads pref should be true after accepting dialog with " +
+ "history checkbox checked");
+
+ yield promiseSanitized;
+
+ // History visits and downloads within one hour should be cleared.
+ yield promiseHistoryClearedState(uris, true);
+ yield ensureDownloadsClearedState(downloadIDs, true);
+
+ // Visits and downloads > 1 hour should still exist.
+ yield promiseHistoryClearedState(olderURIs, false);
+ yield ensureDownloadsClearedState(olderDownloadIDs, false);
+
+ // OK, done, cleanup after ourselves.
+ yield blankSlate();
+ yield promiseHistoryClearedState(olderURIs, true);
+ yield ensureDownloadsClearedState(olderDownloadIDs, true);
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+/**
+ * Ensures that the combined history-downloads checkbox removes neither
+ * history visits nor downloads when not checked.
+ */
+add_task(function* test_history_downloads_unchecked() {
+ // Add form entries
+ let formEntries = [];
+
+ for (let i = 0; i < 5; i++) {
+ formEntries.push((yield promiseAddFormEntryWithMinutesAgo(i)));
+ }
+
+
+ // Add downloads (within the past hour).
+ let downloadIDs = [];
+ for (let i = 0; i < 5; i++) {
+ yield addDownloadWithMinutesAgo(downloadIDs, i);
+ }
+
+ // Add history, downloads, form entries (within the past hour).
+ let uris = [];
+ let places = [];
+ let pURI;
+ for (let i = 0; i < 5; i++) {
+ pURI = makeURI("http://" + i + "-minutes-ago.com/");
+ places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
+ uris.push(pURI);
+ }
+
+ yield PlacesTestUtils.addVisits(places);
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ is(this.isWarningPanelVisible(), false,
+ "Warning panel should be hidden after previously accepting dialog " +
+ "with a predefined timespan");
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+
+ // Remove only form entries, leave history (including downloads).
+ this.checkPrefCheckbox("history", false);
+ this.checkPrefCheckbox("formdata", true);
+ this.acceptDialog();
+ };
+ wh.onunload = function* () {
+ intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_HOUR,
+ "timeSpan pref should be hour after accepting dialog with " +
+ "hour selected");
+ boolPrefIs("cpd.history", false,
+ "history pref should be false after accepting dialog with " +
+ "history checkbox unchecked");
+ boolPrefIs("cpd.downloads", false,
+ "downloads pref should be false after accepting dialog with " +
+ "history checkbox unchecked");
+
+ // Of the three only form entries should be cleared.
+ yield promiseHistoryClearedState(uris, false);
+ yield ensureDownloadsClearedState(downloadIDs, false);
+
+ for (let entry of formEntries) {
+ let exists = yield formNameExists(entry);
+ is(exists, false, "form entry " + entry + " should no longer exist");
+ }
+
+ // OK, done, cleanup after ourselves.
+ yield blankSlate();
+ yield promiseHistoryClearedState(uris, true);
+ yield ensureDownloadsClearedState(downloadIDs, true);
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+/**
+ * Ensures that the "Everything" duration option works.
+ */
+add_task(function* test_everything() {
+ // Add history.
+ let uris = [];
+ let places = [];
+ let pURI;
+ // within past hour, within past two hours, within past four hours and
+ // outside past four hours
+ [10, 70, 130, 250].forEach(function(aValue) {
+ pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
+ places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
+ uris.push(pURI);
+ });
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ yield PlacesTestUtils.addVisits(places);
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ is(this.isWarningPanelVisible(), false,
+ "Warning panel should be hidden after previously accepting dialog " +
+ "with a predefined timespan");
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ this.checkPrefCheckbox("history", true);
+ this.checkDetails(true);
+
+ // Hide details
+ this.toggleDetails();
+ this.checkDetails(false);
+
+ // Show details
+ this.toggleDetails();
+ this.checkDetails(true);
+
+ this.acceptDialog();
+ };
+ wh.onunload = function* () {
+ yield promiseSanitized;
+ intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING,
+ "timeSpan pref should be everything after accepting dialog " +
+ "with everything selected");
+
+ yield promiseHistoryClearedState(uris, true);
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+/**
+ * Ensures that the "Everything" warning is visible on dialog open after
+ * the previous test.
+ */
+add_task(function* test_everything_warning() {
+ // Add history.
+ let uris = [];
+ let places = [];
+ let pURI;
+ // within past hour, within past two hours, within past four hours and
+ // outside past four hours
+ [10, 70, 130, 250].forEach(function(aValue) {
+ pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
+ places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
+ uris.push(pURI);
+ });
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ yield PlacesTestUtils.addVisits(places);
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ is(this.isWarningPanelVisible(), true,
+ "Warning panel should be visible after previously accepting dialog " +
+ "with clearing everything");
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ this.checkPrefCheckbox("history", true);
+ this.acceptDialog();
+ };
+ wh.onunload = function* () {
+ intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING,
+ "timeSpan pref should be everything after accepting dialog " +
+ "with everything selected");
+
+ yield promiseSanitized;
+
+ yield promiseHistoryClearedState(uris, true);
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+/**
+ * The next three tests checks that when a certain history item cannot be
+ * cleared then the checkbox should be both disabled and unchecked.
+ * In addition, we ensure that this behavior does not modify the preferences.
+ */
+add_task(function* test_cannot_clear_history() {
+ // Add form entries
+ let formEntries = [ (yield promiseAddFormEntryWithMinutesAgo(10)) ];
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ // Add history.
+ let pURI = makeURI("http://" + 10 + "-minutes-ago.com/");
+ yield PlacesTestUtils.addVisits({uri: pURI, visitDate: visitTimeForMinutesAgo(10)});
+ let uris = [ pURI ];
+
+ let wh = new WindowHelper();
+ wh.onload = function() {
+ // Check that the relevant checkboxes are enabled
+ var cb = this.win.document.querySelectorAll(
+ "#itemList > [preference='privacy.cpd.formdata']");
+ ok(cb.length == 1 && !cb[0].disabled, "There is formdata, checkbox to " +
+ "clear formdata should be enabled.");
+
+ cb = this.win.document.querySelectorAll(
+ "#itemList > [preference='privacy.cpd.history']");
+ ok(cb.length == 1 && !cb[0].disabled, "There is history, checkbox to " +
+ "clear history should be enabled.");
+
+ this.checkAllCheckboxes();
+ this.acceptDialog();
+ };
+ wh.onunload = function* () {
+ yield promiseSanitized;
+
+ yield promiseHistoryClearedState(uris, true);
+
+ let exists = yield formNameExists(formEntries[0]);
+ is(exists, false, "form entry " + formEntries[0] + " should no longer exist");
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+add_task(function* test_no_formdata_history_to_clear() {
+ let promiseSanitized = promiseSanitizationComplete();
+ let wh = new WindowHelper();
+ wh.onload = function() {
+ boolPrefIs("cpd.history", true,
+ "history pref should be true after accepting dialog with " +
+ "history checkbox checked");
+ boolPrefIs("cpd.formdata", true,
+ "formdata pref should be true after accepting dialog with " +
+ "formdata checkbox checked");
+
+ var cb = this.win.document.querySelectorAll(
+ "#itemList > [preference='privacy.cpd.history']");
+ ok(cb.length == 1 && !cb[0].disabled && cb[0].checked,
+ "There is no history, but history checkbox should always be enabled " +
+ "and will be checked from previous preference.");
+
+ this.acceptDialog();
+ }
+ wh.open();
+ yield wh.promiseClosed;
+ yield promiseSanitized;
+});
+
+add_task(function* test_form_entries() {
+ let formEntry = (yield promiseAddFormEntryWithMinutesAgo(10));
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ let wh = new WindowHelper();
+ wh.onload = function() {
+ boolPrefIs("cpd.formdata", true,
+ "formdata pref should persist previous value after accepting " +
+ "dialog where you could not clear formdata.");
+
+ var cb = this.win.document.querySelectorAll(
+ "#itemList > [preference='privacy.cpd.formdata']");
+
+ info("There exists formEntries so the checkbox should be in sync with the pref.");
+ is(cb.length, 1, "There is only one checkbox for form data");
+ ok(!cb[0].disabled, "The checkbox is enabled");
+ ok(cb[0].checked, "The checkbox is checked");
+
+ this.acceptDialog();
+ };
+ wh.onunload = function* () {
+ yield promiseSanitized;
+ let exists = yield formNameExists(formEntry);
+ is(exists, false, "form entry " + formEntry + " should no longer exist");
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+
+/**
+ * Ensure that toggling details persists
+ * across dialog openings.
+ */
+add_task(function* test_toggling_details_persists() {
+ {
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ // Check all items and select "Everything"
+ this.checkAllCheckboxes();
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+
+ // Hide details
+ this.toggleDetails();
+ this.checkDetails(false);
+ this.acceptDialog();
+ };
+ wh.open();
+ yield wh.promiseClosed;
+ }
+ {
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ // Details should remain closed because all items are checked.
+ this.checkDetails(false);
+
+ // Uncheck history.
+ this.checkPrefCheckbox("history", false);
+ this.acceptDialog();
+ };
+ wh.open();
+ yield wh.promiseClosed;
+ }
+ {
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ // Details should be open because not all items are checked.
+ this.checkDetails(true);
+
+ // Modify the Site Preferences item state (bug 527820)
+ this.checkAllCheckboxes();
+ this.checkPrefCheckbox("siteSettings", false);
+ this.acceptDialog();
+ };
+ wh.open();
+ yield wh.promiseClosed;
+ }
+ {
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ // Details should be open because not all items are checked.
+ this.checkDetails(true);
+
+ // Hide details
+ this.toggleDetails();
+ this.checkDetails(false);
+ this.cancelDialog();
+ };
+ wh.open();
+ yield wh.promiseClosed;
+ }
+ {
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ // Details should be open because not all items are checked.
+ this.checkDetails(true);
+
+ // Select another duration
+ this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+ // Hide details
+ this.toggleDetails();
+ this.checkDetails(false);
+ this.acceptDialog();
+ };
+ wh.open();
+ yield wh.promiseClosed;
+ }
+ {
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ // Details should not be open because "Last Hour" is selected
+ this.checkDetails(false);
+
+ this.cancelDialog();
+ };
+ wh.open();
+ yield wh.promiseClosed;
+ }
+ {
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ // Details should have remained closed
+ this.checkDetails(false);
+
+ // Show details
+ this.toggleDetails();
+ this.checkDetails(true);
+ this.cancelDialog();
+ };
+ wh.open();
+ yield wh.promiseClosed;
+ }
+});
+
+// Test for offline cache deletion
+add_task(function* test_offline_cache() {
+ // Prepare stuff, we will work with www.example.com
+ var URL = "http://www.example.com";
+ var URI = makeURI(URL);
+ var principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(URI);
+
+ // Give www.example.com privileges to store offline data
+ Services.perms.addFromPrincipal(principal, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION);
+ Services.perms.addFromPrincipal(principal, "offline-app", Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN);
+
+ // Store something to the offline cache
+ var appcacheserv = Cc["@mozilla.org/network/application-cache-service;1"]
+ .getService(Ci.nsIApplicationCacheService);
+ var appcachegroupid = appcacheserv.buildGroupIDForInfo(makeURI(URL + "/manifest"), LoadContextInfo.default);
+ var appcache = appcacheserv.createApplicationCache(appcachegroupid);
+ var storage = Services.cache2.appCacheStorage(LoadContextInfo.default, appcache);
+
+ // Open the dialog
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ // Show details
+ this.toggleDetails();
+ // Clear only offlineApps
+ this.uncheckAllCheckboxes();
+ this.checkPrefCheckbox("offlineApps", true);
+ this.acceptDialog();
+ };
+ wh.onunload = function () {
+ // Check if the cache has been deleted
+ var size = -1;
+ var visitor = {
+ onCacheStorageInfo: function (aEntryCount, aConsumption, aCapacity, aDiskDirectory)
+ {
+ size = aConsumption;
+ }
+ };
+ storage.asyncVisitStorage(visitor, false);
+ // Offline cache visit happens synchronously, since it's forwarded to the old code
+ is(size, 0, "offline application cache entries evicted");
+ };
+
+ var cacheListener = {
+ onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; },
+ onCacheEntryAvailable: function (entry, isnew, unused, status) {
+ is(status, Cr.NS_OK);
+ var stream = entry.openOutputStream(0);
+ var content = "content";
+ stream.write(content, content.length);
+ stream.close();
+ entry.close();
+ wh.open();
+ }
+ };
+
+ storage.asyncOpenURI(makeURI(URL), "", Ci.nsICacheStorage.OPEN_TRUNCATE, cacheListener);
+ yield wh.promiseClosed;
+});
+
+// Test for offline apps permission deletion
+add_task(function* test_offline_apps_permissions() {
+ // Prepare stuff, we will work with www.example.com
+ var URL = "http://www.example.com";
+ var URI = makeURI(URL);
+ var principal = Services.scriptSecurityManager.createCodebasePrincipal(URI, {});
+
+ let promiseSanitized = promiseSanitizationComplete();
+
+ // Open the dialog
+ let wh = new WindowHelper();
+ wh.onload = function () {
+ this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+ // Show details
+ this.toggleDetails();
+ // Clear only offlineApps
+ this.uncheckAllCheckboxes();
+ this.checkPrefCheckbox("siteSettings", true);
+ this.acceptDialog();
+ };
+ wh.onunload = function* () {
+ yield promiseSanitized;
+
+ // Check all has been deleted (privileges, data, cache)
+ is(Services.perms.testPermissionFromPrincipal(principal, "offline-app"), 0, "offline-app permissions removed");
+ };
+ wh.open();
+ yield wh.promiseClosed;
+});
+
+var now_mSec = Date.now();
+var now_uSec = now_mSec * 1000;
+
+/**
+ * This wraps the dialog and provides some convenience methods for interacting
+ * with it.
+ *
+ * @param aWin
+ * The dialog's nsIDOMWindow
+ */
+function WindowHelper(aWin) {
+ this.win = aWin;
+ this.promiseClosed = new Promise(resolve => { this._resolveClosed = resolve });
+}
+
+WindowHelper.prototype = {
+ /**
+ * "Presses" the dialog's OK button.
+ */
+ acceptDialog: function () {
+ is(this.win.document.documentElement.getButton("accept").disabled, false,
+ "Dialog's OK button should not be disabled");
+ this.win.document.documentElement.acceptDialog();
+ },
+
+ /**
+ * "Presses" the dialog's Cancel button.
+ */
+ cancelDialog: function () {
+ this.win.document.documentElement.cancelDialog();
+ },
+
+ /**
+ * Ensures that the details progressive disclosure button and the item list
+ * hidden by it match up. Also makes sure the height of the dialog is
+ * sufficient for the item list and warning panel.
+ *
+ * @param aShouldBeShown
+ * True if you expect the details to be shown and false if hidden
+ */
+ checkDetails: function (aShouldBeShown) {
+ let button = this.getDetailsButton();
+ let list = this.getItemList();
+ let hidden = list.hidden || list.collapsed;
+ is(hidden, !aShouldBeShown,
+ "Details should be " + (aShouldBeShown ? "shown" : "hidden") +
+ " but were actually " + (hidden ? "hidden" : "shown"));
+ let dir = hidden ? "down" : "up";
+ is(button.className, "expander-" + dir,
+ "Details button should be " + dir + " because item list is " +
+ (hidden ? "" : "not ") + "hidden");
+ let height = 0;
+ if (!hidden) {
+ ok(list.boxObject.height > 30, "listbox has sufficient size")
+ height += list.boxObject.height;
+ }
+ if (this.isWarningPanelVisible())
+ height += this.getWarningPanel().boxObject.height;
+ ok(height < this.win.innerHeight,
+ "Window should be tall enough to fit warning panel and item list");
+ },
+
+ /**
+ * (Un)checks a history scope checkbox (browser & download history,
+ * form history, etc.).
+ *
+ * @param aPrefName
+ * The final portion of the checkbox's privacy.cpd.* preference name
+ * @param aCheckState
+ * True if the checkbox should be checked, false otherwise
+ */
+ checkPrefCheckbox: function (aPrefName, aCheckState) {
+ var pref = "privacy.cpd." + aPrefName;
+ var cb = this.win.document.querySelectorAll(
+ "#itemList > [preference='" + pref + "']");
+ is(cb.length, 1, "found checkbox for " + pref + " preference");
+ if (cb[0].checked != aCheckState)
+ cb[0].click();
+ },
+
+ /**
+ * Makes sure all the checkboxes are checked.
+ */
+ _checkAllCheckboxesCustom: function (check) {
+ var cb = this.win.document.querySelectorAll("#itemList > [preference]");
+ ok(cb.length > 1, "found checkboxes for preferences");
+ for (var i = 0; i < cb.length; ++i) {
+ var pref = this.win.document.getElementById(cb[i].getAttribute("preference"));
+ if (!!pref.value ^ check)
+ cb[i].click();
+ }
+ },
+
+ checkAllCheckboxes: function () {
+ this._checkAllCheckboxesCustom(true);
+ },
+
+ uncheckAllCheckboxes: function () {
+ this._checkAllCheckboxesCustom(false);
+ },
+
+ /**
+ * @return The details progressive disclosure button
+ */
+ getDetailsButton: function () {
+ return this.win.document.getElementById("detailsExpander");
+ },
+
+ /**
+ * @return The dialog's duration dropdown
+ */
+ getDurationDropdown: function () {
+ return this.win.document.getElementById("sanitizeDurationChoice");
+ },
+
+ /**
+ * @return The item list hidden by the details progressive disclosure button
+ */
+ getItemList: function () {
+ return this.win.document.getElementById("itemList");
+ },
+
+ /**
+ * @return The clear-everything warning box
+ */
+ getWarningPanel: function () {
+ return this.win.document.getElementById("sanitizeEverythingWarningBox");
+ },
+
+ /**
+ * @return True if the "Everything" warning panel is visible (as opposed to
+ * the tree)
+ */
+ isWarningPanelVisible: function () {
+ return !this.getWarningPanel().hidden;
+ },
+
+ /**
+ * Opens the clear recent history dialog. Before calling this, set
+ * this.onload to a function to execute onload. It should close the dialog
+ * when done so that the tests may continue. Set this.onunload to a function
+ * to execute onunload. this.onunload is optional. If it returns true, the
+ * caller is expected to call waitForAsyncUpdates at some point; if false is
+ * returned, waitForAsyncUpdates is called automatically.
+ */
+ open: function () {
+ let wh = this;
+
+ function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened")
+ return;
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ var loaded = false;
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+
+ win.addEventListener("load", function onload(event) {
+ win.removeEventListener("load", onload, false);
+
+ if (win.name !== "SanitizeDialog")
+ return;
+
+ wh.win = win;
+ loaded = true;
+ executeSoon(() => wh.onload());
+ }, false);
+
+ win.addEventListener("unload", function onunload(event) {
+ if (win.name !== "SanitizeDialog") {
+ win.removeEventListener("unload", onunload, false);
+ return;
+ }
+
+ // Why is unload fired before load?
+ if (!loaded)
+ return;
+
+ win.removeEventListener("unload", onunload, false);
+ wh.win = win;
+
+ // Some exceptions that reach here don't reach the test harness, but
+ // ok()/is() do...
+ Task.spawn(function* () {
+ if (wh.onunload) {
+ yield wh.onunload();
+ }
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ wh._resolveClosed();
+ });
+ }, false);
+ }
+ Services.ww.registerNotification(windowObserver);
+ Services.ww.openWindow(null,
+ "chrome://browser/content/sanitize.xul",
+ "SanitizeDialog",
+ "chrome,titlebar,dialog,centerscreen,modal",
+ null);
+ },
+
+ /**
+ * Selects a duration in the duration dropdown.
+ *
+ * @param aDurVal
+ * One of the Sanitizer.TIMESPAN_* values
+ */
+ selectDuration: function (aDurVal) {
+ this.getDurationDropdown().value = aDurVal;
+ if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) {
+ is(this.isWarningPanelVisible(), true,
+ "Warning panel should be visible for TIMESPAN_EVERYTHING");
+ }
+ else {
+ is(this.isWarningPanelVisible(), false,
+ "Warning panel should not be visible for non-TIMESPAN_EVERYTHING");
+ }
+ },
+
+ /**
+ * Toggles the details progressive disclosure button.
+ */
+ toggleDetails: function () {
+ this.getDetailsButton().click();
+ }
+};
+
+function promiseSanitizationComplete() {
+ return promiseTopicObserved("sanitizer-sanitization-complete");
+}
+
+/**
+ * Adds a download to history.
+ *
+ * @param aMinutesAgo
+ * The download will be downloaded this many minutes ago
+ */
+function* addDownloadWithMinutesAgo(aExpectedPathList, aMinutesAgo) {
+ let publicList = yield Downloads.getList(Downloads.PUBLIC);
+
+ let name = "fakefile-" + aMinutesAgo + "-minutes-ago";
+ let download = yield Downloads.createDownload({
+ source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
+ target: name
+ });
+ download.startTime = new Date(now_mSec - (aMinutesAgo * kMsecPerMin));
+ download.canceled = true;
+ publicList.add(download);
+
+ ok((yield downloadExists(name)),
+ "Sanity check: download " + name +
+ " should exist after creating it");
+
+ aExpectedPathList.push(name);
+}
+
+/**
+ * Adds a form entry to history.
+ *
+ * @param aMinutesAgo
+ * The entry will be added this many minutes ago
+ */
+function promiseAddFormEntryWithMinutesAgo(aMinutesAgo) {
+ let name = aMinutesAgo + "-minutes-ago";
+
+ // Artifically age the entry to the proper vintage.
+ let timestamp = now_uSec - (aMinutesAgo * kUsecPerMin);
+
+ return new Promise((resolve, reject) =>
+ FormHistory.update({ op: "add", fieldname: name, value: "dummy", firstUsed: timestamp },
+ { handleError: function (error) {
+ reject();
+ throw new Error("Error occurred updating form history: " + error);
+ },
+ handleCompletion: function (reason) {
+ resolve(name);
+ }
+ })
+ )
+}
+
+/**
+ * Checks if a form entry exists.
+ */
+function formNameExists(name)
+{
+ return new Promise((resolve, reject) => {
+ let count = 0;
+ FormHistory.count({ fieldname: name },
+ { handleResult: result => count = result,
+ handleError: function (error) {
+ reject(error);
+ throw new Error("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function (reason) {
+ if (!reason) {
+ resolve(count);
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Removes all history visits, downloads, and form entries.
+ */
+function* blankSlate() {
+ let publicList = yield Downloads.getList(Downloads.PUBLIC);
+ let downloads = yield publicList.getAll();
+ for (let download of downloads) {
+ yield publicList.remove(download);
+ yield download.finalize(true);
+ }
+
+ yield new Promise((resolve, reject) => {
+ FormHistory.update({op: "remove"}, {
+ handleCompletion(reason) {
+ if (!reason) {
+ resolve();
+ }
+ },
+ handleError(error) {
+ reject(error);
+ throw new Error("Error occurred updating form history: " + error);
+ }
+ });
+ });
+
+ yield PlacesTestUtils.clearHistory();
+}
+
+/**
+ * Ensures that the given pref is the expected value.
+ *
+ * @param aPrefName
+ * The pref's sub-branch under the privacy branch
+ * @param aExpectedVal
+ * The pref's expected value
+ * @param aMsg
+ * Passed to is()
+ */
+function boolPrefIs(aPrefName, aExpectedVal, aMsg) {
+ is(gPrefService.getBoolPref("privacy." + aPrefName), aExpectedVal, aMsg);
+}
+
+/**
+ * Checks to see if the download with the specified path exists.
+ *
+ * @param aPath
+ * The path of the download to check
+ * @return True if the download exists, false otherwise
+ */
+function* downloadExists(aPath) {
+ let publicList = yield Downloads.getList(Downloads.PUBLIC);
+ let listArray = yield publicList.getAll();
+ return listArray.some(i => i.target.path == aPath);
+}
+
+/**
+ * Ensures that the specified downloads are either cleared or not.
+ *
+ * @param aDownloadIDs
+ * Array of download database IDs
+ * @param aShouldBeCleared
+ * True if each download should be cleared, false otherwise
+ */
+function* ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) {
+ let niceStr = aShouldBeCleared ? "no longer" : "still";
+ for (let id of aDownloadIDs) {
+ is((yield downloadExists(id)), !aShouldBeCleared,
+ "download " + id + " should " + niceStr + " exist");
+ }
+}
+
+/**
+ * Ensures that the given pref is the expected value.
+ *
+ * @param aPrefName
+ * The pref's sub-branch under the privacy branch
+ * @param aExpectedVal
+ * The pref's expected value
+ * @param aMsg
+ * Passed to is()
+ */
+function intPrefIs(aPrefName, aExpectedVal, aMsg) {
+ is(gPrefService.getIntPref("privacy." + aPrefName), aExpectedVal, aMsg);
+}
+
+/**
+ * Creates a visit time.
+ *
+ * @param aMinutesAgo
+ * The visit will be visited this many minutes ago
+ */
+function visitTimeForMinutesAgo(aMinutesAgo) {
+ return now_uSec - aMinutesAgo * kUsecPerMin;
+}
diff --git a/browser/base/content/test/general/browser_save_link-perwindowpb.js b/browser/base/content/test/general/browser_save_link-perwindowpb.js
new file mode 100644
index 000000000..5c99ba32a
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_link-perwindowpb.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+// Trigger a save of a link in public mode, then trigger an identical save
+// in private mode and ensure that the second request is differentiated from
+// the first by checking that cookies set by the first response are not sent
+// during the second request.
+function triggerSave(aWindow, aCallback) {
+ info("started triggerSave");
+ var fileName;
+ let testBrowser = aWindow.gBrowser.selectedBrowser;
+ // This page sets a cookie if and only if a cookie does not exist yet
+ let testURI = "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517-2.html";
+ testBrowser.loadURI(testURI);
+ BrowserTestUtils.browserLoaded(testBrowser, false, testURI)
+ .then(() => {
+ waitForFocus(function () {
+ info("register to handle popupshown");
+ aWindow.document.addEventListener("popupshown", contextMenuOpened, false);
+
+ BrowserTestUtils.synthesizeMouseAtCenter("#fff", {type: "contextmenu", button: 2}, testBrowser);
+ info("right clicked!");
+ }, aWindow);
+ });
+
+ function contextMenuOpened(event) {
+ info("contextMenuOpened");
+ aWindow.document.removeEventListener("popupshown", contextMenuOpened);
+
+ // Create the folder the link will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function(fp) {
+ info("showCallback");
+ fileName = fp.defaultString;
+ info("fileName: " + fileName);
+ destFile.append (fileName);
+ MockFilePicker.returnFiles = [destFile];
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ info("done showCallback");
+ };
+
+ mockTransferCallback = function(downloadSuccess) {
+ info("mockTransferCallback");
+ onTransferComplete(aWindow, downloadSuccess, destDir);
+ destDir.remove(true);
+ ok(!destDir.exists(), "Destination dir should be removed");
+ ok(!destFile.exists(), "Destination file should be removed");
+ mockTransferCallback = null;
+ info("done mockTransferCallback");
+ }
+
+ // Select "Save Link As" option from context menu
+ var saveLinkCommand = aWindow.document.getElementById("context-savelink");
+ info("saveLinkCommand: " + saveLinkCommand);
+ saveLinkCommand.doCommand();
+
+ event.target.hidePopup();
+ info("popup hidden");
+ }
+
+ function onTransferComplete(aWindow2, downloadSuccess, destDir) {
+ ok(downloadSuccess, "Link should have been downloaded successfully");
+ aWindow2.close();
+
+ executeSoon(() => aCallback());
+ }
+}
+
+function test() {
+ info("Start the test");
+ waitForExplicitFinish();
+
+ var gNumSet = 0;
+ function testOnWindow(options, callback) {
+ info("testOnWindow(" + options + ")");
+ var win = OpenBrowserWindow(options);
+ info("got " + win);
+ whenDelayedStartupFinished(win, () => callback(win));
+ }
+
+ function whenDelayedStartupFinished(aWindow, aCallback) {
+ info("whenDelayedStartupFinished");
+ Services.obs.addObserver(function obs(aSubject, aTopic) {
+ info("whenDelayedStartupFinished, got topic: " + aTopic + ", got subject: " + aSubject + ", waiting for " + aWindow);
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(obs, aTopic);
+ executeSoon(aCallback);
+ info("whenDelayedStartupFinished found our window");
+ }
+ }, "browser-delayed-startup-finished", false);
+ }
+
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ info("Running the cleanup code");
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ Services.obs.removeObserver(observer, "http-on-examine-response");
+ info("Finished running the cleanup code");
+ });
+
+ function observer(subject, topic, state) {
+ info("observer called with " + topic);
+ if (topic == "http-on-modify-request") {
+ onModifyRequest(subject);
+ } else if (topic == "http-on-examine-response") {
+ onExamineResponse(subject);
+ }
+ }
+
+ function onExamineResponse(subject) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ info("onExamineResponse with " + channel.URI.spec);
+ if (channel.URI.spec != "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.sjs") {
+ info("returning");
+ return;
+ }
+ try {
+ let cookies = channel.getResponseHeader("set-cookie");
+ // From browser/base/content/test/general/bug792715.sjs, we receive a Set-Cookie
+ // header with foopy=1 when there are no cookies for that domain.
+ is(cookies, "foopy=1", "Cookie should be foopy=1");
+ gNumSet += 1;
+ info("gNumSet = " + gNumSet);
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ info("onExamineResponse caught NOTAVAIL" + ex);
+ } else {
+ info("ionExamineResponse caught " + ex);
+ }
+ }
+ }
+
+ function onModifyRequest(subject) {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ info("onModifyRequest with " + channel.URI.spec);
+ if (channel.URI.spec != "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.sjs") {
+ return;
+ }
+ try {
+ let cookies = channel.getRequestHeader("cookie");
+ info("cookies: " + cookies);
+ // From browser/base/content/test/general/bug792715.sjs, we should never send a
+ // cookie because we are making only 2 requests: one in public mode, and
+ // one in private mode.
+ throw "We should never send a cookie in this test";
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ info("onModifyRequest caught NOTAVAIL" + ex);
+ } else {
+ info("ionModifyRequest caught " + ex);
+ }
+ }
+ }
+
+ Services.obs.addObserver(observer, "http-on-modify-request", false);
+ Services.obs.addObserver(observer, "http-on-examine-response", false);
+
+ testOnWindow(undefined, function(win) {
+ // The first save from a regular window sets a cookie.
+ triggerSave(win, function() {
+ is(gNumSet, 1, "1 cookie should be set");
+
+ // The second save from a private window also sets a cookie.
+ testOnWindow({private: true}, function(win2) {
+ triggerSave(win2, function() {
+ is(gNumSet, 2, "2 cookies should be set");
+ finish();
+ });
+ });
+ });
+ });
+}
+
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this);
+
+function createTemporarySaveDirectory() {
+ var saveDir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ info("create testsavedir!");
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ info("return from createTempSaveDir: " + saveDir.path);
+ return saveDir;
+}
diff --git a/browser/base/content/test/general/browser_save_link_when_window_navigates.js b/browser/base/content/test/general/browser_save_link_when_window_navigates.js
new file mode 100644
index 000000000..2fd10b00e
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_link_when_window_navigates.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+const SAVE_PER_SITE_PREF = "browser.download.lastDir.savePerSite";
+const ALWAYS_DOWNLOAD_DIR_PREF = "browser.download.useDownloadDir";
+const UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xul";
+
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this);
+
+function createTemporarySaveDirectory() {
+ var saveDir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ info("create testsavedir!");
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ info("return from createTempSaveDir: " + saveDir.path);
+ return saveDir;
+}
+
+function triggerSave(aWindow, aCallback) {
+ info("started triggerSave, persite downloads: " + (Services.prefs.getBoolPref(SAVE_PER_SITE_PREF) ? "on" : "off"));
+ var fileName;
+ let testBrowser = aWindow.gBrowser.selectedBrowser;
+ let testURI = "http://mochi.test:8888/browser/browser/base/content/test/general/navigating_window_with_download.html";
+ windowObserver.setCallback(onUCTDialog);
+ testBrowser.loadURI(testURI);
+
+ // Create the folder the link will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function(fp) {
+ info("showCallback");
+ fileName = fp.defaultString;
+ info("fileName: " + fileName);
+ destFile.append (fileName);
+ MockFilePicker.returnFiles = [destFile];
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ info("done showCallback");
+ };
+
+ mockTransferCallback = function(downloadSuccess) {
+ info("mockTransferCallback");
+ onTransferComplete(aWindow, downloadSuccess, destDir);
+ destDir.remove(true);
+ ok(!destDir.exists(), "Destination dir should be removed");
+ ok(!destFile.exists(), "Destination file should be removed");
+ mockTransferCallback = null;
+ info("done mockTransferCallback");
+ }
+
+ function onUCTDialog(dialog) {
+ function doLoad() {
+ content.document.querySelector('iframe').remove();
+ }
+ testBrowser.messageManager.loadFrameScript("data:,(" + doLoad.toString() + ")()", false);
+ executeSoon(continueDownloading);
+ }
+
+ function continueDownloading() {
+ let windows = Services.wm.getEnumerator("");
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ if (win.location && win.location.href == UCT_URI) {
+ win.document.documentElement._fireButtonEvent("accept");
+ win.close();
+ return;
+ }
+ }
+ ok(false, "No Unknown Content Type dialog yet?");
+ }
+
+ function onTransferComplete(aWindow2, downloadSuccess) {
+ ok(downloadSuccess, "Link should have been downloaded successfully");
+ aWindow2.close();
+
+ executeSoon(aCallback);
+ }
+}
+
+
+var windowObserver = {
+ setCallback: function(aCallback) {
+ if (this._callback) {
+ ok(false, "Should only be dealing with one callback at a time.");
+ }
+ this._callback = aCallback;
+ },
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+
+ win.addEventListener("load", function onLoad(event) {
+ win.removeEventListener("load", onLoad, false);
+
+ if (win.location == UCT_URI) {
+ SimpleTest.executeSoon(function() {
+ if (windowObserver._callback) {
+ windowObserver._callback(win);
+ delete windowObserver._callback;
+ } else {
+ ok(false, "Unexpected UCT dialog!");
+ }
+ });
+ }
+ }, false);
+ }
+};
+
+Services.ww.registerNotification(windowObserver);
+
+function test() {
+ waitForExplicitFinish();
+
+ function testOnWindow(options, callback) {
+ info("testOnWindow(" + options + ")");
+ var win = OpenBrowserWindow(options);
+ info("got " + win);
+ whenDelayedStartupFinished(win, () => callback(win));
+ }
+
+ function whenDelayedStartupFinished(aWindow, aCallback) {
+ info("whenDelayedStartupFinished");
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ info("whenDelayedStartupFinished, got topic: " + aTopic + ", got subject: " + aSubject + ", waiting for " + aWindow);
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ executeSoon(aCallback);
+ info("whenDelayedStartupFinished found our window");
+ }
+ }, "browser-delayed-startup-finished", false);
+ }
+
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ info("Running the cleanup code");
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ Services.ww.unregisterNotification(windowObserver);
+ Services.prefs.clearUserPref(ALWAYS_DOWNLOAD_DIR_PREF);
+ Services.prefs.clearUserPref(SAVE_PER_SITE_PREF);
+ info("Finished running the cleanup code");
+ });
+
+ Services.prefs.setBoolPref(ALWAYS_DOWNLOAD_DIR_PREF, false);
+ testOnWindow(undefined, function(win) {
+ let windowGonePromise = promiseWindowWillBeClosed(win);
+ Services.prefs.setBoolPref(SAVE_PER_SITE_PREF, true);
+ triggerSave(win, function() {
+ windowGonePromise.then(function() {
+ Services.prefs.setBoolPref(SAVE_PER_SITE_PREF, false);
+ testOnWindow(undefined, function(win2) {
+ triggerSave(win2, finish);
+ });
+ });
+ });
+ });
+}
+
diff --git a/browser/base/content/test/general/browser_save_private_link_perwindowpb.js b/browser/base/content/test/general/browser_save_private_link_perwindowpb.js
new file mode 100644
index 000000000..e7ed5fa34
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_private_link_perwindowpb.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 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 promiseNoCacheEntry(filename) {
+ return new Promise((resolve, reject) => {
+ Visitor.prototype = {
+ onCacheStorageInfo: function(num, consumption)
+ {
+ info("disk storage contains " + num + " entries");
+ },
+ onCacheEntryInfo: function(uri)
+ {
+ let urispec = uri.asciiSpec;
+ info(urispec);
+ is(urispec.includes(filename), false, "web content present in disk cache");
+ },
+ onCacheEntryVisitCompleted: function()
+ {
+ resolve();
+ }
+ };
+ function Visitor() {}
+
+ let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+ .getService(Ci.nsICacheStorageService);
+ let {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", null);
+ let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
+ storage.asyncVisitStorage(new Visitor(), true /* Do walk entries */);
+ });
+}
+
+function promiseImageDownloaded() {
+ return new Promise((resolve, reject) => {
+ let fileName;
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ function onTransferComplete(downloadSuccess) {
+ ok(downloadSuccess, "Image file should have been downloaded successfully " + fileName);
+
+ // Give the request a chance to finish and create a cache entry
+ resolve(fileName);
+ }
+
+ // Create the folder the image will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function(fp) {
+ fileName = fp.defaultString;
+ destFile.append (fileName);
+ MockFilePicker.returnFiles = [destFile];
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ };
+
+ mockTransferCallback = onTransferComplete;
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ mockTransferCallback = null;
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ destDir.remove(true);
+ });
+
+ });
+}
+
+add_task(function* () {
+ let testURI = "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.html";
+ let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+ let tab = yield BrowserTestUtils.openNewForegroundTab(privateWindow.gBrowser, testURI);
+
+ let contextMenu = privateWindow.document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ let popupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#img", {
+ type: "contextmenu",
+ button: 2
+ }, tab.linkedBrowser);
+ yield popupShown;
+
+ let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+ .getService(Ci.nsICacheStorageService);
+ cache.clear();
+
+ let imageDownloaded = promiseImageDownloaded();
+ // Select "Save Image As" option from context menu
+ privateWindow.document.getElementById("context-saveimage").doCommand();
+
+ contextMenu.hidePopup();
+ yield popupHidden;
+
+ // wait for image download
+ let fileName = yield imageDownloaded;
+ yield promiseNoCacheEntry(fileName);
+
+ yield BrowserTestUtils.closeWindow(privateWindow);
+});
+
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this);
diff --git a/browser/base/content/test/general/browser_save_video.js b/browser/base/content/test/general/browser_save_video.js
new file mode 100644
index 000000000..e81286b7a
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_video.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+/**
+ * TestCase for bug 564387
+ * <https://bugzilla.mozilla.org/show_bug.cgi?id=564387>
+ */
+add_task(function* () {
+ var fileName;
+
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.loadURI("http://mochi.test:8888/browser/browser/base/content/test/general/web_video.html");
+ yield loadPromise;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#video1",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser);
+ info("context menu click on video1");
+
+ yield popupShownPromise;
+
+ info("context menu opened on video1");
+
+ // Create the folder the video will be saved into.
+ var destDir = createTemporarySaveDirectory();
+ var destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function(fp) {
+ fileName = fp.defaultString;
+ destFile.append(fileName);
+ MockFilePicker.returnFiles = [destFile];
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ };
+
+ let transferCompletePromise = new Promise((resolve) => {
+ function onTransferComplete(downloadSuccess) {
+ ok(downloadSuccess, "Video file should have been downloaded successfully");
+
+ is(fileName, "web-video1-expectedName.ogv",
+ "Video file name is correctly retrieved from Content-Disposition http header");
+ resolve();
+ }
+
+ mockTransferCallback = onTransferComplete;
+ mockTransferRegisterer.register();
+ });
+
+ registerCleanupFunction(function () {
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ destDir.remove(true);
+ });
+
+ // Select "Save Video As" option from context menu
+ var saveVideoCommand = document.getElementById("context-savevideo");
+ saveVideoCommand.doCommand();
+ info("context-savevideo command executed");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.hidePopup();
+ yield popupHiddenPromise;
+
+ yield transferCompletePromise;
+});
+
+
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this);
+
+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;
+}
diff --git a/browser/base/content/test/general/browser_save_video_frame.js b/browser/base/content/test/general/browser_save_video_frame.js
new file mode 100644
index 000000000..e9b8a0475
--- /dev/null
+++ b/browser/base/content/test/general/browser_save_video_frame.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const VIDEO_URL = "http://mochi.test:8888/browser/browser/base/content/test/general/web_video.html";
+
+/**
+ * mockTransfer.js provides a utility that lets us mock out
+ * the "Save File" dialog.
+ */
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this);
+
+/**
+ * Creates and returns an nsIFile for a new temporary save
+ * directory.
+ *
+ * @return nsIFile
+ */
+function createTemporarySaveDirectory() {
+ let 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;
+}
+/**
+ * MockTransfer exposes a "mockTransferCallback" global which
+ * allows us to define a callback to be called once the mock file
+ * selector has selected where to save the file.
+ */
+function waitForTransferComplete() {
+ return new Promise((resolve) => {
+ mockTransferCallback = () => {
+ ok(true, "Transfer completed");
+ resolve();
+ }
+ });
+}
+
+/**
+ * Given some browser, loads a framescript that right-clicks
+ * on the video1 element to spawn a contextmenu.
+ */
+function rightClickVideo(browser) {
+ let frame_script = () => {
+ const Ci = Components.interfaces;
+ let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ let document = content.document;
+ let video = document.getElementById("video1");
+ let rect = video.getBoundingClientRect();
+
+ /* Synthesize a click in the center of the video. */
+ let left = rect.left + (rect.width / 2);
+ let top = rect.top + (rect.height / 2);
+
+ utils.sendMouseEvent("contextmenu", left, top,
+ 2, /* aButton */
+ 1, /* aClickCount */
+ 0 /* aModifiers */);
+ };
+ let mm = browser.messageManager;
+ mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
+}
+
+/**
+ * Loads a page with a <video> element, right-clicks it and chooses
+ * to save a frame screenshot to the disk. Completes once we've
+ * verified that the frame has been saved to disk.
+ */
+add_task(function*() {
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ // Create the folder the video will be saved into.
+ let destDir = createTemporarySaveDirectory();
+ let destFile = destDir.clone();
+
+ MockFilePicker.displayDirectory = destDir;
+ MockFilePicker.showCallback = function(fp) {
+ destFile.append(fp.defaultString);
+ MockFilePicker.returnFiles = [destFile];
+ MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+ };
+
+ mockTransferRegisterer.register();
+
+ // Make sure that we clean these things up when we're done.
+ registerCleanupFunction(function () {
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ destDir.remove(true);
+ });
+
+ let tab = gBrowser.addTab();
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ info("Loading video tab");
+ yield promiseTabLoadEvent(tab, VIDEO_URL);
+ info("Video tab loaded.");
+
+ let context = document.getElementById("contentAreaContextMenu");
+ let popupPromise = promisePopupShown(context);
+
+ info("Synthesizing right-click on video element");
+ rightClickVideo(browser);
+ info("Waiting for popup to fire popupshown.");
+ yield popupPromise;
+ info("Popup fired popupshown");
+
+ let saveSnapshotCommand = document.getElementById("context-video-saveimage");
+ let promiseTransfer = waitForTransferComplete()
+ info("Firing save snapshot command");
+ saveSnapshotCommand.doCommand();
+ context.hidePopup();
+ info("Waiting for transfer completion");
+ yield promiseTransfer;
+ info("Transfer complete");
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_scope.js b/browser/base/content/test/general/browser_scope.js
new file mode 100644
index 000000000..f8141e5f6
--- /dev/null
+++ b/browser/base/content/test/general/browser_scope.js
@@ -0,0 +1,10 @@
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
+
+function test() {
+ ok(!!gBrowser, "gBrowser exists");
+ is(gBrowser, getBrowser(), "both ways of getting tabbrowser work");
+}
diff --git a/browser/base/content/test/general/browser_selectTabAtIndex.js b/browser/base/content/test/general/browser_selectTabAtIndex.js
new file mode 100644
index 000000000..b6578aec0
--- /dev/null
+++ b/browser/base/content/test/general/browser_selectTabAtIndex.js
@@ -0,0 +1,81 @@
+"use strict";
+
+function test() {
+ const isLinux = navigator.platform.indexOf("Linux") == 0;
+
+ function assertTab(expectedTab) {
+ is(gBrowser.tabContainer.selectedIndex, expectedTab,
+ `tab index ${expectedTab} should be selected`);
+ }
+
+ function sendAccelKey(key) {
+ // Make sure the keystroke goes to chrome.
+ document.activeElement.blur();
+ EventUtils.synthesizeKey(key.toString(), { altKey: isLinux, accelKey: !isLinux });
+ }
+
+ function createTabs(count) {
+ for (let n = 0; n < count; n++)
+ gBrowser.addTab();
+ }
+
+ function testKey(key, expectedTab) {
+ sendAccelKey(key);
+ assertTab(expectedTab);
+ }
+
+ function testIndex(index, expectedTab) {
+ gBrowser.selectTabAtIndex(index);
+ assertTab(expectedTab);
+ }
+
+ // Create fewer tabs than our 9 number keys.
+ is(gBrowser.tabs.length, 1, "should have 1 tab");
+ createTabs(4);
+ is(gBrowser.tabs.length, 5, "should have 5 tabs");
+
+ // Test keyboard shortcuts. Order tests so that no two test cases have the
+ // same expected tab in a row. This ensures that tab selection actually
+ // changed the selected tab.
+ testKey(8, 4);
+ testKey(1, 0);
+ testKey(2, 1);
+ testKey(4, 3);
+ testKey(9, 4);
+
+ // Test index selection.
+ testIndex(0, 0);
+ testIndex(4, 4);
+ testIndex(-5, 0);
+ testIndex(5, 4);
+ testIndex(-4, 1);
+ testIndex(1, 1);
+ testIndex(-1, 4);
+ testIndex(9, 4);
+
+ // Create more tabs than our 9 number keys.
+ createTabs(10);
+ is(gBrowser.tabs.length, 15, "should have 15 tabs");
+
+ // Test keyboard shortcuts.
+ testKey(2, 1);
+ testKey(1, 0);
+ testKey(4, 3);
+ testKey(8, 7);
+ testKey(9, 14);
+
+ // Test index selection.
+ testIndex(-15, 0);
+ testIndex(14, 14);
+ testIndex(-14, 1);
+ testIndex(15, 14);
+ testIndex(-1, 14);
+ testIndex(0, 0);
+ testIndex(1, 1);
+ testIndex(9, 9);
+
+ // Clean up tabs.
+ for (let n = 15; n > 1; n--)
+ gBrowser.removeTab(gBrowser.selectedTab, {skipPermitUnload: true});
+ is(gBrowser.tabs.length, 1, "should have 1 tab");
+}
diff --git a/browser/base/content/test/general/browser_selectpopup.js b/browser/base/content/test/general/browser_selectpopup.js
new file mode 100644
index 000000000..d34254d1c
--- /dev/null
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -0,0 +1,563 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks that a <select> with an <optgroup> opens and can be navigated
+// in a child process. This is different than single-process as a <menulist> is used
+// to implement the dropdown list.
+
+requestLongerTimeout(2);
+
+const XHTML_DTD = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
+
+const PAGECONTENT =
+ "<html xmlns='http://www.w3.org/1999/xhtml'>" +
+ "<body onload='gChangeEvents = 0;gInputEvents = 0; document.body.firstChild.focus()'><select oninput='gInputEvents++' onchange='gChangeEvents++'>" +
+ " <optgroup label='First Group'>" +
+ " <option value='One'>One</option>" +
+ " <option value='Two'>Two</option>" +
+ " </optgroup>" +
+ " <option value='Three'>Three</option>" +
+ " <optgroup label='Second Group' disabled='true'>" +
+ " <option value='Four'>Four</option>" +
+ " <option value='Five'>Five</option>" +
+ " </optgroup>" +
+ " <option value='Six' disabled='true'>Six</option>" +
+ " <optgroup label='Third Group'>" +
+ " <option value='Seven'> Seven </option>" +
+ " <option value='Eight'>&nbsp;&nbsp;Eight&nbsp;&nbsp;</option>" +
+ " </optgroup></select><input />Text" +
+ "</body></html>";
+
+const PAGECONTENT_SMALL =
+ "<html>" +
+ "<body><select id='one'>" +
+ " <option value='One'>One</option>" +
+ " <option value='Two'>Two</option>" +
+ "</select><select id='two'>" +
+ " <option value='Three'>Three</option>" +
+ " <option value='Four'>Four</option>" +
+ "</select><select id='three'>" +
+ " <option value='Five'>Five</option>" +
+ " <option value='Six'>Six</option>" +
+ "</select></body></html>";
+
+const PAGECONTENT_SOMEHIDDEN =
+ "<html><head><style>.hidden { display: none; }</style></head>" +
+ "<body><select id='one'>" +
+ " <option value='One' style='display: none;'>OneHidden</option>" +
+ " <option value='Two' class='hidden'>TwoHidden</option>" +
+ " <option value='Three'>ThreeVisible</option>" +
+ " <option value='Four'style='display: table;'>FourVisible</option>" +
+ " <option value='Five'>FiveVisible</option>" +
+ " <optgroup label='GroupHidden' class='hidden'>" +
+ " <option value='Four'>Six.OneHidden</option>" +
+ " <option value='Five' style='display: block;'>Six.TwoHidden</option>" +
+ " </optgroup>" +
+ " <option value='Six' class='hidden' style='display: block;'>SevenVisible</option>" +
+ "</select></body></html>";
+
+const PAGECONTENT_TRANSLATED =
+ "<html><body>" +
+ "<div id='div'>" +
+ "<iframe id='frame' width='320' height='295' style='border: none;'" +
+ " src='data:text/html,<select id=select autofocus><option>he he he</option><option>boo boo</option><option>baz baz</option></select>'" +
+ "</iframe>" +
+ "</div></body></html>";
+
+function openSelectPopup(selectPopup, withMouse, selector = "select", win = window)
+{
+ let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
+
+ if (withMouse) {
+ return Promise.all([popupShownPromise,
+ BrowserTestUtils.synthesizeMouseAtCenter(selector, { }, win.gBrowser.selectedBrowser)]);
+ }
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, code: "ArrowDown" }, win);
+ return popupShownPromise;
+}
+
+function hideSelectPopup(selectPopup, mode = "enter", win = window)
+{
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+
+ if (mode == "escape") {
+ EventUtils.synthesizeKey("KEY_Escape", { code: "Escape" }, win);
+ }
+ else if (mode == "enter") {
+ EventUtils.synthesizeKey("KEY_Enter", { code: "Enter" }, win);
+ }
+ else if (mode == "click") {
+ EventUtils.synthesizeMouseAtCenter(selectPopup.lastChild, { }, win);
+ }
+
+ return popupHiddenPromise;
+}
+
+function getInputEvents()
+{
+ return ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ return content.wrappedJSObject.gInputEvents;
+ });
+}
+
+function getChangeEvents()
+{
+ return ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ return content.wrappedJSObject.gChangeEvents;
+ });
+}
+
+function* doSelectTests(contentType, dtd)
+{
+ const pageUrl = "data:" + contentType + "," + escape(dtd + "\n" + PAGECONTENT);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ yield openSelectPopup(selectPopup);
+
+ let isWindows = navigator.platform.indexOf("Win") >= 0;
+
+ is(menulist.selectedIndex, 1, "Initial selection");
+ is(selectPopup.firstChild.localName, "menucaption", "optgroup is caption");
+ is(selectPopup.firstChild.getAttribute("label"), "First Group", "optgroup label");
+ is(selectPopup.childNodes[1].localName, "menuitem", "option is menuitem");
+ is(selectPopup.childNodes[1].getAttribute("label"), "One", "option label");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
+ is(menulist.menuBoxObject.activeChild, menulist.getItemAtIndex(2), "Select item 2");
+ is(menulist.selectedIndex, isWindows ? 2 : 1, "Select item 2 selectedIndex");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
+ is(menulist.menuBoxObject.activeChild, menulist.getItemAtIndex(3), "Select item 3");
+ is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex");
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
+
+ // On Windows, one can navigate on disabled menuitems
+ is(menulist.menuBoxObject.activeChild, menulist.getItemAtIndex(9),
+ "Skip optgroup header and disabled items select item 7");
+ is(menulist.selectedIndex, isWindows ? 9 : 1, "Select or skip disabled item selectedIndex");
+
+ for (let i = 0; i < 10; i++) {
+ is(menulist.getItemAtIndex(i).disabled, i >= 4 && i <= 7, "item " + i + " disabled")
+ }
+
+ EventUtils.synthesizeKey("KEY_ArrowUp", { code: "ArrowUp" });
+ is(menulist.menuBoxObject.activeChild, menulist.getItemAtIndex(3), "Select item 3 again");
+ is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex");
+
+ is((yield getInputEvents()), 0, "Before closed - number of input events");
+ is((yield getChangeEvents()), 0, "Before closed - number of change events");
+
+ EventUtils.synthesizeKey("a", { accelKey: true });
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { isWindows }, function(args) {
+ Assert.equal(String(content.getSelection()), args.isWindows ? "Text" : "",
+ "Select all while popup is open");
+ });
+
+ // Backspace should not go back
+ let handleKeyPress = function(event) {
+ ok(false, "Should not get keypress event");
+ }
+ window.addEventListener("keypress", handleKeyPress);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", { });
+ window.removeEventListener("keypress", handleKeyPress);
+
+ yield hideSelectPopup(selectPopup);
+
+ is(menulist.selectedIndex, 3, "Item 3 still selected");
+ is((yield getInputEvents()), 1, "After closed - number of input events");
+ is((yield getChangeEvents()), 1, "After closed - number of change events");
+
+ // Opening and closing the popup without changing the value should not fire a change event.
+ yield openSelectPopup(selectPopup, true);
+ yield hideSelectPopup(selectPopup, "escape");
+ is((yield getInputEvents()), 1, "Open and close with no change - number of input events");
+ is((yield getChangeEvents()), 1, "Open and close with no change - number of change events");
+ EventUtils.synthesizeKey("VK_TAB", { });
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ is((yield getInputEvents()), 1, "Tab away from select with no change - number of input events");
+ is((yield getChangeEvents()), 1, "Tab away from select with no change - number of change events");
+
+ yield openSelectPopup(selectPopup, true);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
+ yield hideSelectPopup(selectPopup, "escape");
+ is((yield getInputEvents()), isWindows ? 2 : 1, "Open and close with change - number of input events");
+ is((yield getChangeEvents()), isWindows ? 2 : 1, "Open and close with change - number of change events");
+ EventUtils.synthesizeKey("VK_TAB", { });
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ is((yield getInputEvents()), isWindows ? 2 : 1, "Tab away from select with change - number of input events");
+ is((yield getChangeEvents()), isWindows ? 2 : 1, "Tab away from select with change - number of change events");
+
+ is(selectPopup.lastChild.previousSibling.label, "Seven", "Spaces collapsed");
+ is(selectPopup.lastChild.label, "\xA0\xA0Eight\xA0\xA0", "Non-breaking spaces not collapsed");
+
+ yield BrowserTestUtils.removeTab(tab);
+}
+
+add_task(function*() {
+ yield doSelectTests("text/html", "");
+});
+
+add_task(function*() {
+ yield doSelectTests("application/xhtml+xml", XHTML_DTD);
+});
+
+// This test opens a select popup and removes the content node of a popup while
+// The popup should close if its node is removed.
+add_task(function*() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // First, try it when a different <select> element than the one that is open is removed
+ yield openSelectPopup(selectPopup, true, "#one");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ content.document.body.removeChild(content.document.getElementById("two"));
+ });
+
+ // Wait a bit just to make sure the popup won't close.
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+
+ is(selectPopup.state, "open", "Different popup did not affect open popup");
+
+ yield hideSelectPopup(selectPopup);
+
+ // Next, try it when the same <select> element than the one that is open is removed
+ yield openSelectPopup(selectPopup, true, "#three");
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ content.document.body.removeChild(content.document.getElementById("three"));
+ });
+ yield popupHiddenPromise;
+
+ ok(true, "Popup hidden when select is removed");
+
+ // Finally, try it when the tab is closed while the select popup is open.
+ yield openSelectPopup(selectPopup, true, "#one");
+
+ popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+ yield BrowserTestUtils.removeTab(tab);
+ yield popupHiddenPromise;
+
+ ok(true, "Popup hidden when tab is closed");
+});
+
+// This test opens a select popup that is isn't a frame and has some translations applied.
+add_task(function*() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_TRANSLATED);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // First, get the position of the select popup when no translations have been applied.
+ yield openSelectPopup(selectPopup, false);
+
+ let rect = selectPopup.getBoundingClientRect();
+ let expectedX = rect.left;
+ let expectedY = rect.top;
+
+ yield hideSelectPopup(selectPopup);
+
+ // Iterate through a set of steps which each add more translation to the select's expected position.
+ let steps = [
+ [ "div", "transform: translateX(7px) translateY(13px);", 7, 13 ],
+ [ "frame", "border-top: 5px solid green; border-left: 10px solid red; border-right: 35px solid blue;", 10, 5 ],
+ [ "frame", "border: none; padding-left: 6px; padding-right: 12px; padding-top: 2px;", -4, -3 ],
+ [ "select", "margin: 9px; transform: translateY(-3px);", 9, 6 ],
+ ];
+
+ for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
+ let step = steps[stepIndex];
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, step, function*(contentStep) {
+ return new Promise(resolve => {
+ let changedWin = content;
+
+ let elem;
+ if (contentStep[0] == "select") {
+ changedWin = content.document.getElementById("frame").contentWindow;
+ elem = changedWin.document.getElementById("select");
+ }
+ else {
+ elem = content.document.getElementById(contentStep[0]);
+ }
+
+ changedWin.addEventListener("MozAfterPaint", function onPaint() {
+ changedWin.removeEventListener("MozAfterPaint", onPaint);
+ resolve();
+ });
+
+ elem.style = contentStep[1];
+ elem.getBoundingClientRect();
+ });
+ });
+
+ yield openSelectPopup(selectPopup, false);
+
+ expectedX += step[2];
+ expectedY += step[3];
+
+ let popupRect = selectPopup.getBoundingClientRect();
+ is(popupRect.left, expectedX, "step " + (stepIndex + 1) + " x");
+ is(popupRect.top, expectedY, "step " + (stepIndex + 1) + " y");
+
+ yield hideSelectPopup(selectPopup);
+ }
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+// Test that we get the right events when a select popup is changed.
+add_task(function* test_event_order() {
+ const URL = "data:text/html," + escape(PAGECONTENT_SMALL);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: URL,
+ }, function*(browser) {
+ let menulist = document.getElementById("ContentSelectDropdown");
+ let selectPopup = menulist.menupopup;
+
+ // According to https://html.spec.whatwg.org/#the-select-element,
+ // we want to fire input, change, and then click events on the
+ // <select> (in that order) when it has changed.
+ let expectedEnter = [
+ {
+ type: "input",
+ cancelable: false,
+ targetIsOption: false,
+ },
+ {
+ type: "change",
+ cancelable: false,
+ targetIsOption: false,
+ },
+ ];
+
+ let expectedClick = [
+ {
+ type: "mousedown",
+ cancelable: true,
+ targetIsOption: true,
+ },
+ {
+ type: "mouseup",
+ cancelable: true,
+ targetIsOption: true,
+ },
+ {
+ type: "input",
+ cancelable: false,
+ targetIsOption: false,
+ },
+ {
+ type: "change",
+ cancelable: false,
+ targetIsOption: false,
+ },
+ {
+ type: "click",
+ cancelable: true,
+ targetIsOption: true,
+ },
+ ];
+
+ for (let mode of ["enter", "click"]) {
+ let expected = mode == "enter" ? expectedEnter : expectedClick;
+ yield openSelectPopup(selectPopup, true, mode == "enter" ? "#one" : "#two");
+
+ let eventsPromise = ContentTask.spawn(browser, [mode, expected], function*([contentMode, contentExpected]) {
+ return new Promise((resolve) => {
+ function onEvent(event) {
+ select.removeEventListener(event.type, onEvent);
+ Assert.ok(contentExpected.length, "Unexpected event " + event.type);
+ let expectation = contentExpected.shift();
+ Assert.equal(event.type, expectation.type,
+ "Expected the right event order");
+ Assert.ok(event.bubbles, "All of these events should bubble");
+ Assert.equal(event.cancelable, expectation.cancelable,
+ "Cancellation property should match");
+ Assert.equal(event.target.localName,
+ expectation.targetIsOption ? "option" : "select",
+ "Target matches");
+ if (!contentExpected.length) {
+ resolve();
+ }
+ }
+
+ let select = content.document.getElementById(contentMode == "enter" ? "one" : "two");
+ for (let event of ["input", "change", "mousedown", "mouseup", "click"]) {
+ select.addEventListener(event, onEvent);
+ }
+ });
+ });
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
+ yield hideSelectPopup(selectPopup, mode);
+ yield eventsPromise;
+ }
+ });
+});
+
+function* performLargePopupTests(win)
+{
+ let browser = win.gBrowser.selectedBrowser;
+
+ yield ContentTask.spawn(browser, null, function*() {
+ let doc = content.document;
+ let select = doc.getElementById("one");
+ for (var i = 0; i < 180; i++) {
+ select.add(new content.Option("Test" + i));
+ }
+
+ select.options[60].selected = true;
+ select.focus();
+ });
+
+ let selectPopup = win.document.getElementById("ContentSelectDropdown").menupopup;
+ let browserRect = browser.getBoundingClientRect();
+
+ let positions = [
+ "margin-top: 300px;",
+ "position: fixed; bottom: 100px;",
+ "width: 100%; height: 9999px;"
+ ];
+
+ let position;
+ while (true) {
+ yield openSelectPopup(selectPopup, false, "select", win);
+
+ let rect = selectPopup.getBoundingClientRect();
+ ok(rect.top >= browserRect.top, "Popup top position in within browser area");
+ ok(rect.bottom <= browserRect.bottom, "Popup bottom position in within browser area");
+
+ // Don't check the scroll position for the last step as the popup will be cut off.
+ if (positions.length > 0) {
+ let cs = win.getComputedStyle(selectPopup);
+ let bpBottom = parseFloat(cs.paddingBottom) + parseFloat(cs.borderBottomWidth);
+
+ is(selectPopup.childNodes[60].getBoundingClientRect().bottom,
+ selectPopup.getBoundingClientRect().bottom - bpBottom,
+ "Popup scroll at correct position " + bpBottom);
+ }
+
+ yield hideSelectPopup(selectPopup, "enter", win);
+
+ position = positions.shift();
+ if (!position) {
+ break;
+ }
+
+ let contentPainted = BrowserTestUtils.contentPainted(browser);
+ yield ContentTask.spawn(browser, position, function*(contentPosition) {
+ let select = content.document.getElementById("one");
+ select.setAttribute("style", contentPosition);
+ select.getBoundingClientRect();
+ });
+ yield contentPainted;
+ }
+}
+
+// This test checks select elements with a large number of options to ensure that
+// the popup appears within the browser area.
+add_task(function* test_large_popup() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ yield* performLargePopupTests(window);
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+// This test checks the same as the previous test but in a new smaller window.
+add_task(function* test_large_popup_in_small_window() {
+ let newwin = yield BrowserTestUtils.openNewBrowserWindow({ width: 400, height: 400 });
+
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(newwin.gBrowser.selectedBrowser);
+ yield BrowserTestUtils.loadURI(newwin.gBrowser.selectedBrowser, pageUrl);
+ yield browserLoadedPromise;
+
+ newwin.gBrowser.selectedBrowser.focus();
+
+ yield* performLargePopupTests(newwin);
+
+ yield BrowserTestUtils.closeWindow(newwin);
+});
+
+// This test checks that a mousemove event is fired correctly at the menu and
+// not at the browser, ensuring that any mouse capture has been cleared.
+add_task(function* test_mousemove_correcttarget() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let selectPopup = document.getElementById("ContentSelectDropdown").menupopup;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mousedown" }, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ yield new Promise(resolve => {
+ window.addEventListener("mousemove", function checkForMouseMove(event) {
+ window.removeEventListener("mousemove", checkForMouseMove, true);
+ is(event.target.localName.indexOf("menu"), 0, "mouse over menu");
+ resolve();
+ }, true);
+
+ EventUtils.synthesizeMouseAtCenter(selectPopup.firstChild, { type: "mousemove" });
+ });
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mouseup" }, gBrowser.selectedBrowser);
+
+ yield hideSelectPopup(selectPopup);
+
+ // The popup should be closed when fullscreen mode is entered or exited.
+ for (let steps = 0; steps < 2; steps++) {
+ yield openSelectPopup(selectPopup, true);
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+ let sizeModeChanged = BrowserTestUtils.waitForEvent(window, "sizemodechange");
+ BrowserFullScreen();
+ yield sizeModeChanged;
+ yield popupHiddenPromise;
+ }
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+// This test checks when a <select> element has some options with altered display values.
+add_task(function* test_somehidden() {
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_SOMEHIDDEN);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let selectPopup = document.getElementById("ContentSelectDropdown").menupopup;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mousedown" }, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ // The exact number is not needed; just ensure the height is larger than 4 items to accomodate any popup borders.
+ ok(selectPopup.getBoundingClientRect().height >= selectPopup.lastChild.getBoundingClientRect().height * 4, "Height contains at least 4 items");
+ ok(selectPopup.getBoundingClientRect().height < selectPopup.lastChild.getBoundingClientRect().height * 5, "Height doesn't contain 5 items");
+
+ // The label contains the substring 'Visible' for items that are visible.
+ // Otherwise, it is expected to be display: none.
+ is(selectPopup.parentNode.itemCount, 9, "Correct number of items");
+ let child = selectPopup.firstChild;
+ let idx = 1;
+ while (child) {
+ is(getComputedStyle(child).display, child.label.indexOf("Visible") > 0 ? "-moz-box" : "none",
+ "Item " + (idx++) + " is visible");
+ child = child.nextSibling;
+ }
+
+ yield hideSelectPopup(selectPopup, "escape");
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/general/browser_ssl_error_reports.js b/browser/base/content/test/general/browser_ssl_error_reports.js
new file mode 100644
index 000000000..b1b1c8b84
--- /dev/null
+++ b/browser/base/content/test/general/browser_ssl_error_reports.js
@@ -0,0 +1,174 @@
+"use strict";
+
+const URL_REPORTS = "https://example.com/browser/browser/base/content/test/general/ssl_error_reports.sjs?";
+const URL_BAD_CHAIN = "https://badchain.include-subdomains.pinning.example.com/";
+const URL_NO_CERT = "https://fail-handshake.example.com/";
+const URL_BAD_CERT = "https://expired.example.com/";
+const URL_BAD_STS_CERT = "https://badchain.include-subdomains.pinning.example.com:443/";
+const ROOT = getRootDirectory(gTestPath);
+const PREF_REPORT_ENABLED = "security.ssl.errorReporting.enabled";
+const PREF_REPORT_AUTOMATIC = "security.ssl.errorReporting.automatic";
+const PREF_REPORT_URL = "security.ssl.errorReporting.url";
+
+SimpleTest.requestCompleteLog();
+
+Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 2);
+
+function cleanup() {
+ Services.prefs.clearUserPref(PREF_REPORT_ENABLED);
+ Services.prefs.clearUserPref(PREF_REPORT_AUTOMATIC);
+ Services.prefs.clearUserPref(PREF_REPORT_URL);
+}
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.cert_pinning.enforcement_level");
+ cleanup();
+});
+
+add_task(function* test_send_report_neterror() {
+ yield testSendReportAutomatically(URL_BAD_CHAIN, "succeed", "neterror");
+ yield testSendReportAutomatically(URL_NO_CERT, "nocert", "neterror");
+ yield testSetAutomatic(URL_NO_CERT, "nocert", "neterror");
+});
+
+
+add_task(function* test_send_report_certerror() {
+ yield testSendReportAutomatically(URL_BAD_CERT, "badcert", "certerror");
+ yield testSetAutomatic(URL_BAD_CERT, "badcert", "certerror");
+});
+
+add_task(function* test_send_disabled() {
+ Services.prefs.setBoolPref(PREF_REPORT_ENABLED, false);
+ Services.prefs.setBoolPref(PREF_REPORT_AUTOMATIC, true);
+ Services.prefs.setCharPref(PREF_REPORT_URL, "https://example.com/invalid");
+
+ // Check with enabled=false but automatic=true.
+ yield testSendReportDisabled(URL_NO_CERT, "neterror");
+ yield testSendReportDisabled(URL_BAD_CERT, "certerror");
+
+ Services.prefs.setBoolPref(PREF_REPORT_AUTOMATIC, false);
+
+ // Check again with both prefs false.
+ yield testSendReportDisabled(URL_NO_CERT, "neterror");
+ yield testSendReportDisabled(URL_BAD_CERT, "certerror");
+ cleanup();
+});
+
+function* testSendReportAutomatically(testURL, suffix, errorURISuffix) {
+ Services.prefs.setBoolPref(PREF_REPORT_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_REPORT_AUTOMATIC, true);
+ Services.prefs.setCharPref(PREF_REPORT_URL, URL_REPORTS + suffix);
+
+ // Add a tab and wait until it's loaded.
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+
+ // Load the page and wait for the error report submission.
+ let promiseStatus = createReportResponseStatusPromise(URL_REPORTS + suffix);
+ browser.loadURI(testURL);
+ yield promiseErrorPageLoaded(browser);
+
+ ok(!isErrorStatus(yield promiseStatus),
+ "SSL error report submitted successfully");
+
+ // Check that we loaded the right error page.
+ yield checkErrorPage(browser, errorURISuffix);
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+ cleanup();
+}
+
+function* testSetAutomatic(testURL, suffix, errorURISuffix) {
+ Services.prefs.setBoolPref(PREF_REPORT_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_REPORT_AUTOMATIC, false);
+ Services.prefs.setCharPref(PREF_REPORT_URL, URL_REPORTS + suffix);
+
+ // Add a tab and wait until it's loaded.
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+
+ // Load the page.
+ browser.loadURI(testURL);
+ yield promiseErrorPageLoaded(browser);
+
+ // Check that we loaded the right error page.
+ yield checkErrorPage(browser, errorURISuffix);
+
+ let statusPromise = createReportResponseStatusPromise(URL_REPORTS + suffix);
+
+ // Click the checkbox, enable automatic error reports.
+ yield ContentTask.spawn(browser, null, function* () {
+ content.document.getElementById("automaticallyReportInFuture").click();
+ });
+
+ // Wait for the error report submission.
+ yield statusPromise;
+
+ let isAutomaticReportingEnabled = () =>
+ Services.prefs.getBoolPref(PREF_REPORT_AUTOMATIC);
+
+ // Check that the pref was flipped.
+ ok(isAutomaticReportingEnabled(), "automatic SSL report submission enabled");
+
+ // Disable automatic error reports.
+ yield ContentTask.spawn(browser, null, function* () {
+ content.document.getElementById("automaticallyReportInFuture").click();
+ });
+
+ // Check that the pref was flipped.
+ ok(!isAutomaticReportingEnabled(), "automatic SSL report submission disabled");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+ cleanup();
+}
+
+function* testSendReportDisabled(testURL, errorURISuffix) {
+ // Add a tab and wait until it's loaded.
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+
+ // Load the page.
+ browser.loadURI(testURL);
+ yield promiseErrorPageLoaded(browser);
+
+ // Check that we loaded the right error page.
+ yield checkErrorPage(browser, errorURISuffix);
+
+ // Check that the error reporting section is hidden.
+ yield ContentTask.spawn(browser, null, function* () {
+ let section = content.document.getElementById("certificateErrorReporting");
+ Assert.equal(content.getComputedStyle(section).display, "none",
+ "error reporting section should be hidden");
+ });
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+}
+
+function isErrorStatus(status) {
+ return status < 200 || status >= 300;
+}
+
+// use the observer service to see when a report is sent
+function createReportResponseStatusPromise(expectedURI) {
+ return new Promise(resolve => {
+ let observer = (subject, topic, data) => {
+ subject.QueryInterface(Ci.nsIHttpChannel);
+ let requestURI = subject.URI.spec;
+ if (requestURI == expectedURI) {
+ Services.obs.removeObserver(observer, "http-on-examine-response");
+ resolve(subject.responseStatus);
+ }
+ };
+ Services.obs.addObserver(observer, "http-on-examine-response", false);
+ });
+}
+
+function checkErrorPage(browser, suffix) {
+ return ContentTask.spawn(browser, { suffix }, function* (args) {
+ let uri = content.document.documentURI;
+ Assert.ok(uri.startsWith(`about:${args.suffix}`), "correct error page loaded");
+ });
+}
diff --git a/browser/base/content/test/general/browser_star_hsts.js b/browser/base/content/test/general/browser_star_hsts.js
new file mode 100644
index 000000000..c52e563bc
--- /dev/null
+++ b/browser/base/content/test/general/browser_star_hsts.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var secureURL = "https://example.com/browser/browser/base/content/test/general/browser_star_hsts.sjs";
+var unsecureURL = "http://example.com/browser/browser/base/content/test/general/browser_star_hsts.sjs";
+
+add_task(function* test_star_redirect() {
+ registerCleanupFunction(function() {
+ // Ensure to remove example.com from the HSTS list.
+ let sss = Cc["@mozilla.org/ssservice;1"]
+ .getService(Ci.nsISiteSecurityService);
+ sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS,
+ NetUtil.newURI("http://example.com/"), 0);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ gBrowser.removeCurrentTab();
+ });
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ // This will add the page to the HSTS cache.
+ yield promiseTabLoadEvent(tab, secureURL, secureURL);
+ // This should transparently be redirected to the secure page.
+ yield promiseTabLoadEvent(tab, unsecureURL, secureURL);
+
+ yield promiseStarState(BookmarkingUI.STATUS_UNSTARRED);
+
+ let promiseBookmark = promiseOnBookmarkItemAdded(gBrowser.currentURI);
+ BookmarkingUI.star.click();
+ // This resolves on the next tick, so the star should have already been
+ // updated at that point.
+ yield promiseBookmark;
+
+ is(BookmarkingUI.status, BookmarkingUI.STATUS_STARRED, "The star is starred");
+});
+
+/**
+ * Waits for the star to reflect the expected state.
+ */
+function promiseStarState(aValue) {
+ let deferred = Promise.defer();
+ let expectedStatus = aValue ? BookmarkingUI.STATUS_STARRED
+ : BookmarkingUI.STATUS_UNSTARRED;
+ (function checkState() {
+ if (BookmarkingUI.status == BookmarkingUI.STATUS_UPDATING ||
+ BookmarkingUI.status != expectedStatus) {
+ info("Waiting for star button change.");
+ setTimeout(checkState, 1000);
+ } else {
+ deferred.resolve();
+ }
+ })();
+ return deferred.promise;
+}
+
+/**
+ * Starts a load in an existing tab and waits for it to finish (via some event).
+ *
+ * @param aTab
+ * The tab to load into.
+ * @param aUrl
+ * The url to load.
+ * @param [optional] aFinalURL
+ * The url to wait for, same as aURL if not defined.
+ * @return {Promise} resolved when the event is handled.
+ */
+function promiseTabLoadEvent(aTab, aURL, aFinalURL)
+{
+ if (!aFinalURL)
+ aFinalURL = aURL;
+ let deferred = Promise.defer();
+ info("Wait for load tab event");
+ aTab.linkedBrowser.addEventListener("load", function load(event) {
+ if (event.originalTarget != aTab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank" ||
+ event.target.location.href != aFinalURL) {
+ info("skipping spurious load event");
+ return;
+ }
+ aTab.linkedBrowser.removeEventListener("load", load, true);
+ info("Tab load event received");
+ deferred.resolve();
+ }, true, true);
+ aTab.linkedBrowser.loadURI(aURL);
+ return deferred.promise;
+}
diff --git a/browser/base/content/test/general/browser_star_hsts.sjs b/browser/base/content/test/general/browser_star_hsts.sjs
new file mode 100644
index 000000000..10c7aae12
--- /dev/null
+++ b/browser/base/content/test/general/browser_star_hsts.sjs
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function handleRequest(request, response)
+{
+ let page = "<!DOCTYPE html><html><body><p>HSTS page</p></body></html>";
+ response.setStatusLine(request.httpVersion, "200", "OK");
+ response.setHeader("Strict-Transport-Security", "max-age=60");
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+}
diff --git a/browser/base/content/test/general/browser_subframe_favicons_not_used.js b/browser/base/content/test/general/browser_subframe_favicons_not_used.js
new file mode 100644
index 000000000..7efe78d9b
--- /dev/null
+++ b/browser/base/content/test/general/browser_subframe_favicons_not_used.js
@@ -0,0 +1,20 @@
+/* Make sure <link rel="..."> isn't respected in sub-frames. */
+
+function test() {
+ waitForExplicitFinish();
+
+ let testPath = getRootDirectory(gTestPath);
+
+ let tab = gBrowser.addTab(testPath + "file_bug970276_popup1.html");
+
+ tab.linkedBrowser.addEventListener("load", function() {
+ tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
+
+ let expectedIcon = testPath + "file_bug970276_favicon1.ico";
+ is(gBrowser.getIcon(tab), expectedIcon, "Correct icon.");
+
+ gBrowser.removeTab(tab);
+
+ finish();
+ }, true);
+}
diff --git a/browser/base/content/test/general/browser_syncui.js b/browser/base/content/test/general/browser_syncui.js
new file mode 100644
index 000000000..daf0fa497
--- /dev/null
+++ b/browser/base/content/test/general/browser_syncui.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
+var {Weave} = Cu.import("resource://services-sync/main.js", {});
+
+var stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]
+ .getService(Ci.nsIStringBundleService)
+ .createBundle("chrome://weave/locale/services/sync.properties");
+
+// ensure test output sees log messages.
+Log.repository.getLogger("browserwindow.syncui").addAppender(new Log.DumpAppender());
+
+// Send the specified sync-related notification and return a promise that
+// resolves once gSyncUI._promiseUpateUI is complete and the UI is ready to check.
+function notifyAndPromiseUIUpdated(topic) {
+ return new Promise(resolve => {
+ // Instrument gSyncUI so we know when the update is complete.
+ let oldPromiseUpdateUI = gSyncUI._promiseUpdateUI.bind(gSyncUI);
+ gSyncUI._promiseUpdateUI = function() {
+ return oldPromiseUpdateUI().then(() => {
+ // Restore our override.
+ gSyncUI._promiseUpdateUI = oldPromiseUpdateUI;
+ // Resolve the promise so the caller knows the update is done.
+ resolve();
+ });
+ };
+ // Now send the notification.
+ Services.obs.notifyObservers(null, topic, null);
+ });
+}
+
+// Sync manages 3 broadcasters so the menus correctly reflect the Sync state.
+// Only one of these 3 should ever be visible - pass the ID of the broadcaster
+// you expect to be visible and it will check it's the only one that is.
+function checkBroadcasterVisible(broadcasterId) {
+ let all = ["sync-reauth-state", "sync-setup-state", "sync-syncnow-state"];
+ Assert.ok(all.indexOf(broadcasterId) >= 0, "valid id");
+ for (let check of all) {
+ let eltHidden = document.getElementById(check).hidden;
+ Assert.equal(eltHidden, check == broadcasterId ? false : true, check);
+ }
+}
+
+function promiseObserver(topic) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ }
+ Services.obs.addObserver(obs, topic, false);
+ });
+}
+
+function checkButtonTooltips(stringPrefix) {
+ for (let butId of ["PanelUI-remotetabs-syncnow", "PanelUI-fxa-icon"]) {
+ let text = document.getElementById(butId).getAttribute("tooltiptext");
+ let desc = `Text is "${text}", expecting it to start with "${stringPrefix}"`
+ Assert.ok(text.startsWith(stringPrefix), desc);
+ }
+}
+
+add_task(function* prepare() {
+ // add the Sync button to the toolbar so we can get it!
+ CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_NAVBAR);
+ registerCleanupFunction(() => {
+ CustomizableUI.removeWidgetFromArea("sync-button");
+ });
+
+ let xps = Components.classes["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ yield xps.whenLoaded();
+ // Put Sync and the UI into a known state.
+ Weave.Status.login = Weave.LOGIN_FAILED_NO_USERNAME;
+ yield notifyAndPromiseUIUpdated("weave:service:login:error");
+
+ checkBroadcasterVisible("sync-setup-state");
+ checkButtonTooltips("Sign In To Sync");
+ // mock out the "_needsSetup()" function so we don't short-circuit.
+ let oldNeedsSetup = window.gSyncUI._needsSetup;
+ window.gSyncUI._needsSetup = () => Promise.resolve(false);
+ registerCleanupFunction(() => {
+ window.gSyncUI._needsSetup = oldNeedsSetup;
+ // and an observer to set the state back to what it should be now we've
+ // restored the stub.
+ Services.obs.notifyObservers(null, "weave:service:login:finish", null);
+ });
+ // and a notification to have the state change away from "needs setup"
+ yield notifyAndPromiseUIUpdated("weave:service:login:finish");
+ checkBroadcasterVisible("sync-syncnow-state");
+ // open the sync-button panel so we can check elements in that.
+ document.getElementById("sync-button").click();
+});
+
+add_task(function* testSyncNeedsVerification() {
+ // mock out the "_needsVerification()" function
+ let oldNeedsVerification = window.gSyncUI._needsVerification;
+ window.gSyncUI._needsVerification = () => true;
+ try {
+ // a notification for the state change
+ yield notifyAndPromiseUIUpdated("weave:service:login:finish");
+ checkButtonTooltips("Verify");
+ } finally {
+ window.gSyncUI._needsVerification = oldNeedsVerification;
+ }
+});
+
+
+add_task(function* testSyncLoginError() {
+ checkBroadcasterVisible("sync-syncnow-state");
+
+ // Pretend we are in a "login failed" error state
+ Weave.Status.sync = Weave.LOGIN_FAILED;
+ Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ yield notifyAndPromiseUIUpdated("weave:ui:sync:error");
+
+ // But the menu *should* reflect the login error.
+ checkBroadcasterVisible("sync-reauth-state");
+ // The tooltips for the buttons should also reflect it.
+ checkButtonTooltips("Reconnect");
+
+ // Now pretend we just had a successful login - the error notification should go away.
+ Weave.Status.sync = Weave.STATUS_OK;
+ Weave.Status.login = Weave.LOGIN_SUCCEEDED;
+ yield notifyAndPromiseUIUpdated("weave:service:login:start");
+ yield notifyAndPromiseUIUpdated("weave:service:login:finish");
+ // The menus should be back to "all good"
+ checkBroadcasterVisible("sync-syncnow-state");
+});
+
+function checkButtonsStatus(shouldBeActive) {
+ for (let eid of [
+ "sync-status", // the broadcaster itself.
+ "sync-button", // the main sync button which observes the broadcaster
+ "PanelUI-fxa-icon", // the sync icon in the fxa footer that observes it.
+ ]) {
+ let elt = document.getElementById(eid);
+ if (shouldBeActive) {
+ Assert.equal(elt.getAttribute("syncstatus"), "active", `${eid} should be active`);
+ } else {
+ Assert.ok(!elt.hasAttribute("syncstatus"), `${eid} should have no status attr`);
+ }
+ }
+}
+
+function* testButtonActions(startNotification, endNotification, expectActive = true) {
+ checkButtonsStatus(false);
+ // pretend a sync is starting.
+ yield notifyAndPromiseUIUpdated(startNotification);
+ checkButtonsStatus(expectActive);
+ // and has stopped
+ yield notifyAndPromiseUIUpdated(endNotification);
+ checkButtonsStatus(false);
+}
+
+function *doTestButtonActivities() {
+ // logins do not "activate" the spinner/button as they may block and make
+ // the UI look like Sync is never completing.
+ yield testButtonActions("weave:service:login:start", "weave:service:login:finish", false);
+ yield testButtonActions("weave:service:login:start", "weave:service:login:error", false);
+
+ // But notifications for Sync itself should activate it.
+ yield testButtonActions("weave:service:sync:start", "weave:service:sync:finish");
+ yield testButtonActions("weave:service:sync:start", "weave:service:sync:error");
+
+ // and ensure the counters correctly handle multiple in-flight syncs
+ yield notifyAndPromiseUIUpdated("weave:service:sync:start");
+ checkButtonsStatus(true);
+ // sync stops.
+ yield notifyAndPromiseUIUpdated("weave:service:sync:finish");
+ // Button should not be active.
+ checkButtonsStatus(false);
+}
+
+add_task(function* testButtonActivitiesInNavBar() {
+ // check the button's functionality while the button is in the NavBar - which
+ // it already is.
+ yield doTestButtonActivities();
+});
+
+add_task(function* testFormatLastSyncDateNow() {
+ let now = new Date();
+ let nowString = gSyncUI.formatLastSyncDate(now);
+ Assert.equal(nowString, "Last sync: " + now.toLocaleDateString(undefined, {weekday: 'long', hour: 'numeric', minute: 'numeric'}));
+});
+
+add_task(function* testFormatLastSyncDateMonthAgo() {
+ let monthAgo = new Date();
+ monthAgo.setMonth(monthAgo.getMonth() - 1);
+ let monthAgoString = gSyncUI.formatLastSyncDate(monthAgo);
+ Assert.equal(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: 'long', day: 'numeric'}));
+});
+
+add_task(function* testButtonActivitiesInPanel() {
+ // check the button's functionality while the button is in the panel - it's
+ // currently in the NavBar - move it to the panel and open it.
+ CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+ yield PanelUI.show();
+ try {
+ yield doTestButtonActivities();
+ } finally {
+ PanelUI.hide();
+ }
+});
diff --git a/browser/base/content/test/general/browser_tabDrop.js b/browser/base/content/test/general/browser_tabDrop.js
new file mode 100644
index 000000000..fd743e6dc
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabDrop.js
@@ -0,0 +1,103 @@
+registerCleanupFunction(function* cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ yield BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+ Services.search.currentEngine = originalEngine;
+ let engine = Services.search.getEngineByName("MozSearch");
+ Services.search.removeEngine(engine);
+});
+
+let originalEngine;
+add_task(function* test_setup() {
+ // Stop search-engine loads from hitting the network
+ Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+ let engine = Services.search.getEngineByName("MozSearch");
+ originalEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+});
+
+add_task(function*() { yield dropText("mochi.test/first", 1); });
+add_task(function*() { yield dropText("javascript:'bad'"); });
+add_task(function*() { yield dropText("jAvascript:'bad'"); });
+add_task(function*() { yield dropText("search this", 1); });
+add_task(function*() { yield dropText("mochi.test/second", 1); });
+add_task(function*() { yield dropText("data:text/html,bad"); });
+add_task(function*() { yield dropText("mochi.test/third", 1); });
+
+// Single text/plain item, with multiple links.
+add_task(function*() { yield dropText("mochi.test/1\nmochi.test/2", 2); });
+add_task(function*() { yield dropText("javascript:'bad1'\nmochi.test/3", 0); });
+add_task(function*() { yield dropText("mochi.test/4\ndata:text/html,bad1", 0); });
+
+// Multiple text/plain items, with single and multiple links.
+add_task(function*() {
+ yield drop([[{type: "text/plain",
+ data: "mochi.test/5"}],
+ [{type: "text/plain",
+ data: "mochi.test/6\nmochi.test/7"}]], 3);
+});
+
+// Single text/x-moz-url item, with multiple links.
+// "text/x-moz-url" has titles in even-numbered lines.
+add_task(function*() {
+ yield drop([[{type: "text/x-moz-url",
+ data: "mochi.test/8\nTITLE8\nmochi.test/9\nTITLE9"}]], 2);
+});
+
+// Single item with multiple types.
+add_task(function*() {
+ yield drop([[{type: "text/plain",
+ data: "mochi.test/10"},
+ {type: "text/x-moz-url",
+ data: "mochi.test/11\nTITLE11"}]], 1);
+});
+
+function dropText(text, expectedTabOpenCount=0) {
+ return drop([[{type: "text/plain", data: text}]], expectedTabOpenCount);
+}
+
+function* drop(dragData, expectedTabOpenCount=0) {
+ let dragDataString = JSON.stringify(dragData);
+ info(`Starting test for datagData:${dragDataString}; expectedTabOpenCount:${expectedTabOpenCount}`);
+ let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+ let EventUtils = {};
+ scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+ let awaitDrop = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "drop");
+ let actualTabOpenCount = 0;
+ let openedTabs = [];
+ let checkCount = function(event) {
+ openedTabs.push(event.target);
+ actualTabOpenCount++;
+ return actualTabOpenCount == expectedTabOpenCount;
+ };
+ let awaitTabOpen = expectedTabOpenCount && BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen", false, checkCount);
+ // A drop type of "link" onto an existing tab would normally trigger a
+ // load in that same tab, but tabbrowser code in _getDragTargetTab treats
+ // drops on the outer edges of a tab differently (loading a new tab
+ // instead). Make events created by synthesizeDrop have all of their
+ // coordinates set to 0 (screenX/screenY), so they're treated as drops
+ // on the outer edge of the tab, thus they open new tabs.
+ var event = {
+ clientX: 0,
+ clientY: 0,
+ screenX: 0,
+ screenY: 0,
+ };
+ EventUtils.synthesizeDrop(gBrowser.selectedTab, gBrowser.selectedTab, dragData, "link", window, undefined, event);
+ let tabsOpened = false;
+ if (awaitTabOpen) {
+ yield awaitTabOpen;
+ info("Got TabOpen event");
+ tabsOpened = true;
+ for (let tab of openedTabs) {
+ yield BrowserTestUtils.removeTab(tab);
+ }
+ }
+ is(tabsOpened, !!expectedTabOpenCount, `Tabs for ${dragDataString} should only open if any of dropped items are valid`);
+
+ yield awaitDrop;
+ ok(true, "Got drop event");
+}
diff --git a/browser/base/content/test/general/browser_tabReorder.js b/browser/base/content/test/general/browser_tabReorder.js
new file mode 100644
index 000000000..9e0503e95
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabReorder.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let initialTabsLength = gBrowser.tabs.length;
+
+ let newTab1 = gBrowser.selectedTab = gBrowser.addTab("about:robots", {skipAnimation: true});
+ let newTab2 = gBrowser.selectedTab = gBrowser.addTab("about:about", {skipAnimation: true});
+ let newTab3 = gBrowser.selectedTab = gBrowser.addTab("about:config", {skipAnimation: true});
+ registerCleanupFunction(function () {
+ while (gBrowser.tabs.length > initialTabsLength) {
+ gBrowser.removeTab(gBrowser.tabs[initialTabsLength]);
+ }
+ });
+
+ is(gBrowser.tabs.length, initialTabsLength + 3, "new tabs are opened");
+ is(gBrowser.tabs[initialTabsLength], newTab1, "newTab1 position is correct");
+ is(gBrowser.tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
+ is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
+
+ let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+ let EventUtils = {};
+ scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+ function dragAndDrop(tab1, tab2, copy) {
+ let rect = tab2.getBoundingClientRect();
+ let event = {
+ ctrlKey: copy,
+ altKey: copy,
+ clientX: rect.left + rect.width / 2 + 10,
+ clientY: rect.top + rect.height / 2,
+ };
+
+ EventUtils.synthesizeDrop(tab1, tab2, null, copy ? "copy" : "move", window, window, event);
+ }
+
+ dragAndDrop(newTab1, newTab2, false);
+ is(gBrowser.tabs.length, initialTabsLength + 3, "tabs are still there");
+ is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
+ is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");
+ is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
+
+ dragAndDrop(newTab2, newTab1, true);
+ is(gBrowser.tabs.length, initialTabsLength + 4, "a tab is duplicated");
+ is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 stays same place");
+ is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 stays same place");
+ is(gBrowser.tabs[initialTabsLength + 3], newTab3, "a new tab is inserted before newTab3");
+}
diff --git a/browser/base/content/test/general/browser_tab_close_dependent_window.js b/browser/base/content/test/general/browser_tab_close_dependent_window.js
new file mode 100644
index 000000000..ab8a960ac
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_close_dependent_window.js
@@ -0,0 +1,24 @@
+"use strict";
+
+add_task(function* closing_tab_with_dependents_should_close_window() {
+ info("Opening window");
+ let win = yield BrowserTestUtils.openNewBrowserWindow();
+
+ info("Opening tab with data URI");
+ let tab = yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, `data:text/html,<html%20onclick="W=window.open()"><body%20onbeforeunload="W.close()">`);
+ info("Closing original tab in this window.");
+ yield BrowserTestUtils.removeTab(win.gBrowser.tabs[0]);
+ info("Clicking into the window");
+ let depTabOpened = BrowserTestUtils.waitForEvent(win.gBrowser.tabContainer, "TabOpen");
+ yield BrowserTestUtils.synthesizeMouse("html", 0, 0, {}, tab.linkedBrowser);
+
+ let openedTab = (yield depTabOpened).target;
+ info("Got opened tab");
+
+ let windowClosedPromise = BrowserTestUtils.windowClosed(win);
+ yield BrowserTestUtils.removeTab(tab);
+ is(Cu.isDeadWrapper(openedTab) || openedTab.linkedBrowser == null, true, "Opened tab should also have closed");
+ info("If we timeout now, the window failed to close - that shouldn't happen!");
+ yield windowClosedPromise;
+});
+
diff --git a/browser/base/content/test/general/browser_tab_detach_restore.js b/browser/base/content/test/general/browser_tab_detach_restore.js
new file mode 100644
index 000000000..d482edc26
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_detach_restore.js
@@ -0,0 +1,34 @@
+"use strict";
+
+const {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+
+add_task(function*() {
+ let uri = "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+ // Clear out the closed windows set to start
+ while (SessionStore.getClosedWindowCount() > 0)
+ SessionStore.forgetClosedWindow(0);
+
+ let tab = gBrowser.addTab();
+ tab.linkedBrowser.loadURI(uri);
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield TabStateFlusher.flush(tab.linkedBrowser);
+
+ let key = tab.linkedBrowser.permanentKey;
+ let win = gBrowser.replaceTabWithWindow(tab);
+ yield new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+
+ is(win.gBrowser.selectedBrowser.permanentKey, key, "Should have properly copied the permanentKey");
+ yield BrowserTestUtils.closeWindow(win);
+
+ is(SessionStore.getClosedWindowCount(), 1, "Should have restore data for the closed window");
+
+ win = SessionStore.undoCloseWindow(0);
+ yield BrowserTestUtils.waitForEvent(win, "load");
+ yield BrowserTestUtils.waitForEvent(win.gBrowser.tabs[0], "SSTabRestored");
+
+ is(win.gBrowser.tabs.length, 1, "Should have restored one tab");
+ is(win.gBrowser.selectedBrowser.currentURI.spec, uri, "Should have restored the right page");
+
+ yield promiseWindowClosed(win);
+});
diff --git a/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js b/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js
new file mode 100644
index 000000000..a8fc34083
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js
@@ -0,0 +1,216 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+requestLongerTimeout(2);
+
+const EVENTUTILS_URL = "chrome://mochikit/content/tests/SimpleTest/EventUtils.js";
+var EventUtils = {};
+
+Services.scriptloader.loadSubScript(EVENTUTILS_URL, EventUtils);
+
+/**
+ * Tests that tabs from Private Browsing windows cannot be dragged
+ * into non-private windows, and vice-versa.
+ */
+add_task(function* test_dragging_private_windows() {
+ let normalWin = yield BrowserTestUtils.openNewBrowserWindow();
+ let privateWin =
+ yield BrowserTestUtils.openNewBrowserWindow({private: true});
+
+ let normalTab =
+ yield BrowserTestUtils.openNewForegroundTab(normalWin.gBrowser);
+ let privateTab =
+ yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser);
+
+ let effect = EventUtils.synthesizeDrop(normalTab, privateTab,
+ [[{type: TAB_DROP_TYPE, data: normalTab}]],
+ null, normalWin, privateWin);
+ is(effect, "none", "Should not be able to drag a normal tab to a private window");
+
+ effect = EventUtils.synthesizeDrop(privateTab, normalTab,
+ [[{type: TAB_DROP_TYPE, data: privateTab}]],
+ null, privateWin, normalWin);
+ is(effect, "none", "Should not be able to drag a private tab to a normal window");
+
+ normalWin.gBrowser.swapBrowsersAndCloseOther(normalTab, privateTab);
+ is(normalWin.gBrowser.tabs.length, 2,
+ "Prevent moving a normal tab to a private tabbrowser");
+ is(privateWin.gBrowser.tabs.length, 2,
+ "Prevent accepting a normal tab in a private tabbrowser");
+
+ privateWin.gBrowser.swapBrowsersAndCloseOther(privateTab, normalTab);
+ is(privateWin.gBrowser.tabs.length, 2,
+ "Prevent moving a private tab to a normal tabbrowser");
+ is(normalWin.gBrowser.tabs.length, 2,
+ "Prevent accepting a private tab in a normal tabbrowser");
+
+ yield BrowserTestUtils.closeWindow(normalWin);
+ yield BrowserTestUtils.closeWindow(privateWin);
+});
+
+/**
+ * Tests that tabs from e10s windows cannot be dragged into non-e10s
+ * windows, and vice-versa.
+ */
+add_task(function* test_dragging_e10s_windows() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ let remoteWin = yield BrowserTestUtils.openNewBrowserWindow({remote: true});
+ let nonRemoteWin = yield BrowserTestUtils.openNewBrowserWindow({remote: false});
+
+ let remoteTab =
+ yield BrowserTestUtils.openNewForegroundTab(remoteWin.gBrowser);
+ let nonRemoteTab =
+ yield BrowserTestUtils.openNewForegroundTab(nonRemoteWin.gBrowser);
+
+ let effect = EventUtils.synthesizeDrop(remoteTab, nonRemoteTab,
+ [[{type: TAB_DROP_TYPE, data: remoteTab}]],
+ null, remoteWin, nonRemoteWin);
+ is(effect, "none", "Should not be able to drag a remote tab to a non-e10s window");
+
+ effect = EventUtils.synthesizeDrop(nonRemoteTab, remoteTab,
+ [[{type: TAB_DROP_TYPE, data: nonRemoteTab}]],
+ null, nonRemoteWin, remoteWin);
+ is(effect, "none", "Should not be able to drag a non-remote tab to an e10s window");
+
+ remoteWin.gBrowser.swapBrowsersAndCloseOther(remoteTab, nonRemoteTab);
+ is(remoteWin.gBrowser.tabs.length, 2,
+ "Prevent moving a normal tab to a private tabbrowser");
+ is(nonRemoteWin.gBrowser.tabs.length, 2,
+ "Prevent accepting a normal tab in a private tabbrowser");
+
+ nonRemoteWin.gBrowser.swapBrowsersAndCloseOther(nonRemoteTab, remoteTab);
+ is(nonRemoteWin.gBrowser.tabs.length, 2,
+ "Prevent moving a private tab to a normal tabbrowser");
+ is(remoteWin.gBrowser.tabs.length, 2,
+ "Prevent accepting a private tab in a normal tabbrowser");
+
+ yield BrowserTestUtils.closeWindow(remoteWin);
+ yield BrowserTestUtils.closeWindow(nonRemoteWin);
+});
+
+/**
+ * Tests that remoteness-blacklisted tabs from e10s windows can
+ * be dragged between e10s windows.
+ */
+add_task(function* test_dragging_blacklisted() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ let remoteWin1 = yield BrowserTestUtils.openNewBrowserWindow({remote: true});
+ remoteWin1.gBrowser.myID = "remoteWin1";
+ let remoteWin2 = yield BrowserTestUtils.openNewBrowserWindow({remote: true});
+ remoteWin2.gBrowser.myID = "remoteWin2";
+
+ // Anything under chrome://mochitests/content/ will be blacklisted, and
+ // open in the parent process.
+ const BLACKLISTED_URL = getRootDirectory(gTestPath) +
+ "browser_tab_drag_drop_perwindow.js";
+ let blacklistedTab =
+ yield BrowserTestUtils.openNewForegroundTab(remoteWin1.gBrowser,
+ BLACKLISTED_URL);
+
+ ok(blacklistedTab.linkedBrowser, "Newly created tab should have a browser.");
+
+ ok(!blacklistedTab.linkedBrowser.isRemoteBrowser,
+ `Expected a non-remote browser for URL: ${BLACKLISTED_URL}`);
+
+ let otherTab =
+ yield BrowserTestUtils.openNewForegroundTab(remoteWin2.gBrowser);
+
+ let effect = EventUtils.synthesizeDrop(blacklistedTab, otherTab,
+ [[{type: TAB_DROP_TYPE, data: blacklistedTab}]],
+ null, remoteWin1, remoteWin2);
+ is(effect, "move", "Should be able to drag the blacklisted tab.");
+
+ // The synthesized drop should also do the work of swapping the
+ // browsers, so no need to call swapBrowsersAndCloseOther manually.
+
+ is(remoteWin1.gBrowser.tabs.length, 1,
+ "Should have moved the blacklisted tab out of this window.");
+ is(remoteWin2.gBrowser.tabs.length, 3,
+ "Should have inserted the blacklisted tab into the other window.");
+
+ // The currently selected tab in the second window should be the
+ // one we just dragged in.
+ let draggedBrowser = remoteWin2.gBrowser.selectedBrowser;
+ ok(!draggedBrowser.isRemoteBrowser,
+ "The browser we just dragged in should not be remote.");
+
+ is(draggedBrowser.currentURI.spec, BLACKLISTED_URL,
+ `Expected the URL of the dragged in tab to be ${BLACKLISTED_URL}`);
+
+ yield BrowserTestUtils.closeWindow(remoteWin1);
+ yield BrowserTestUtils.closeWindow(remoteWin2);
+});
+
+
+/**
+ * Tests that tabs dragged between windows dispatch TabOpen and TabClose
+ * events with the appropriate adoption details.
+ */
+add_task(function* test_dragging_adoption_events() {
+ let win1 = yield BrowserTestUtils.openNewBrowserWindow();
+ let win2 = yield BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = yield BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
+ let tab2 = yield BrowserTestUtils.openNewForegroundTab(win2.gBrowser);
+
+ let awaitCloseEvent = BrowserTestUtils.waitForEvent(tab1, "TabClose");
+ let awaitOpenEvent = BrowserTestUtils.waitForEvent(win2, "TabOpen");
+
+ let effect = EventUtils.synthesizeDrop(tab1, tab2,
+ [[{type: TAB_DROP_TYPE, data: tab1}]],
+ null, win1, win2);
+ is(effect, "move", "Tab should be moved from win1 to win2.");
+
+ let closeEvent = yield awaitCloseEvent;
+ let openEvent = yield awaitOpenEvent;
+
+ is(openEvent.detail.adoptedTab, tab1, "New tab adopted old tab");
+ is(closeEvent.detail.adoptedBy, openEvent.target, "Old tab adopted by new tab");
+
+ yield BrowserTestUtils.closeWindow(win1);
+ yield BrowserTestUtils.closeWindow(win2);
+});
+
+
+/**
+ * Tests that per-site zoom settings remain active after a tab is
+ * dragged between windows.
+ */
+add_task(function* test_dragging_zoom_handling() {
+ const ZOOM_FACTOR = 1.62;
+
+ let win1 = yield BrowserTestUtils.openNewBrowserWindow();
+ let win2 = yield BrowserTestUtils.openNewBrowserWindow();
+
+ let tab1 = yield BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
+ let tab2 = yield BrowserTestUtils.openNewForegroundTab(win2.gBrowser,
+ "http://example.com/");
+
+ win2.FullZoom.setZoom(ZOOM_FACTOR);
+ FullZoomHelper.zoomTest(tab2, ZOOM_FACTOR,
+ "Original tab should have correct zoom factor");
+
+ let effect = EventUtils.synthesizeDrop(tab2, tab1,
+ [[{type: TAB_DROP_TYPE, data: tab2}]],
+ null, win2, win1);
+ is(effect, "move", "Tab should be moved from win2 to win1.");
+
+ // Delay slightly to make sure we've finished executing any promise
+ // chains in the zoom code.
+ yield new Promise(resolve => setTimeout(resolve, 0));
+
+ FullZoomHelper.zoomTest(win1.gBrowser.selectedTab, ZOOM_FACTOR,
+ "Dragged tab should have correct zoom factor");
+
+ win1.FullZoom.reset();
+
+ yield BrowserTestUtils.closeWindow(win1);
+ yield BrowserTestUtils.closeWindow(win2);
+});
diff --git a/browser/base/content/test/general/browser_tab_dragdrop.js b/browser/base/content/test/general/browser_tab_dragdrop.js
new file mode 100644
index 000000000..cfe996e1e
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop.js
@@ -0,0 +1,186 @@
+function swapTabsAndCloseOther(a, b) {
+ gBrowser.swapBrowsersAndCloseOther(gBrowser.tabs[b], gBrowser.tabs[a]);
+}
+
+var getClicks = function(tab) {
+ return ContentTask.spawn(tab.linkedBrowser, {}, function() {
+ return content.wrappedJSObject.clicks;
+ });
+}
+
+var clickTest = Task.async(function*(tab) {
+ let clicks = yield getClicks(tab);
+
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function() {
+ let target = content.document.body;
+ let rect = target.getBoundingClientRect();
+ let left = (rect.left + rect.right) / 2;
+ let top = (rect.top + rect.bottom) / 2;
+
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+
+ let newClicks = yield getClicks(tab);
+ is(newClicks, clicks + 1, "adding 1 more click on BODY");
+});
+
+function loadURI(tab, url) {
+ tab.linkedBrowser.loadURI(url);
+ return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+}
+
+// Creates a framescript which caches the current object value from the plugin
+// in the page. checkObjectValue below verifies that the framescript is still
+// active for the browser and that the cached value matches that from the plugin
+// in the page which tells us the plugin hasn't been reinitialized.
+function* cacheObjectValue(browser) {
+ yield ContentTask.spawn(browser, null, function*() {
+ let plugin = content.document.wrappedJSObject.body.firstChild;
+ info(`plugin is ${plugin}`);
+ let win = content.document.defaultView;
+ info(`win is ${win}`);
+ win.objectValue = plugin.getObjectValue();
+ info(`got objectValue: ${win.objectValue}`);
+ win.checkObjectValueListener = () => {
+ let result;
+ let exception;
+ try {
+ result = plugin.checkObjectValue(win.objectValue);
+ } catch (e) {
+ exception = e.toString();
+ }
+ info(`sending plugin.checkObjectValue(objectValue): ${result}`);
+ sendAsyncMessage("Test:CheckObjectValueResult", {
+ result,
+ exception
+ });
+ };
+
+ addMessageListener("Test:CheckObjectValue", win.checkObjectValueListener);
+ });
+}
+
+// Note, can't run this via registerCleanupFunction because it needs the
+// browser to still be alive and have a messageManager.
+function* cleanupObjectValue(browser) {
+ info("entered cleanupObjectValue")
+ yield ContentTask.spawn(browser, null, function*() {
+ info("in cleanup function");
+ let win = content.document.defaultView;
+ info(`about to delete objectValue: ${win.objectValue}`);
+ delete win.objectValue;
+ removeMessageListener("Test:CheckObjectValue", win.checkObjectValueListener);
+ info(`about to delete checkObjectValueListener: ${win.checkObjectValueListener}`);
+ delete win.checkObjectValueListener;
+ info(`deleted objectValue (${win.objectValue}) and checkObjectValueListener (${win.checkObjectValueListener})`);
+ });
+ info("exiting cleanupObjectValue")
+}
+
+// See the notes for cacheObjectValue above.
+function checkObjectValue(browser) {
+ let mm = browser.messageManager;
+
+ return new Promise((resolve, reject) => {
+ let listener = ({ data }) => {
+ mm.removeMessageListener("Test:CheckObjectValueResult", listener);
+ if (data.result === null) {
+ ok(false, "checkObjectValue threw an exception: " + data.exception);
+ reject(data.exception);
+ } else {
+ resolve(data.result);
+ }
+ };
+
+ mm.addMessageListener("Test:CheckObjectValueResult", listener);
+ mm.sendAsyncMessage("Test:CheckObjectValue");
+ });
+}
+
+add_task(function*() {
+ let embed = '<embed type="application/x-test" allowscriptaccess="always" allowfullscreen="true" wmode="window" width="640" height="480"></embed>'
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
+
+ // create a few tabs
+ let tabs = [
+ gBrowser.tabs[0],
+ gBrowser.addTab("about:blank", {skipAnimation: true}),
+ gBrowser.addTab("about:blank", {skipAnimation: true}),
+ gBrowser.addTab("about:blank", {skipAnimation: true}),
+ gBrowser.addTab("about:blank", {skipAnimation: true})
+ ];
+
+ // Initially 0 1 2 3 4
+ yield loadURI(tabs[1], "data:text/html;charset=utf-8,<title>tab1</title><body>tab1<iframe>");
+ yield loadURI(tabs[2], "data:text/plain;charset=utf-8,tab2");
+ yield loadURI(tabs[3], "data:text/html;charset=utf-8,<title>tab3</title><body>tab3<iframe>");
+ yield loadURI(tabs[4], "data:text/html;charset=utf-8,<body onload='clicks=0' onclick='++clicks'>"+embed);
+ yield BrowserTestUtils.switchTab(gBrowser, tabs[3]);
+
+ swapTabsAndCloseOther(2, 3); // now: 0 1 2 4
+ is(gBrowser.tabs[1], tabs[1], "tab1");
+ is(gBrowser.tabs[2], tabs[3], "tab3");
+ is(gBrowser.tabs[3], tabs[4], "tab4");
+ delete tabs[2];
+
+ info("about to cacheObjectValue")
+ yield cacheObjectValue(tabs[4].linkedBrowser);
+ info("just finished cacheObjectValue")
+
+ swapTabsAndCloseOther(3, 2); // now: 0 1 4
+ is(Array.prototype.indexOf.call(gBrowser.tabs, gBrowser.selectedTab), 2,
+ "The third tab should be selected");
+ delete tabs[4];
+
+
+ ok((yield checkObjectValue(gBrowser.tabs[2].linkedBrowser)), "same plugin instance");
+
+ is(gBrowser.tabs[1], tabs[1], "tab1");
+ is(gBrowser.tabs[2], tabs[3], "tab4");
+
+ let clicks = yield getClicks(gBrowser.tabs[2]);
+ is(clicks, 0, "no click on BODY so far");
+ yield clickTest(gBrowser.tabs[2]);
+
+ swapTabsAndCloseOther(2, 1); // now: 0 4
+ is(gBrowser.tabs[1], tabs[1], "tab1");
+ delete tabs[3];
+
+ ok((yield checkObjectValue(gBrowser.tabs[1].linkedBrowser)), "same plugin instance");
+ yield cleanupObjectValue(gBrowser.tabs[1].linkedBrowser);
+
+ yield clickTest(gBrowser.tabs[1]);
+
+ // Load a new document (about:blank) in tab4, then detach that tab into a new window.
+ // In the new window, navigate back to the original document and click on its <body>,
+ // verify that its onclick was called.
+ is(Array.prototype.indexOf.call(gBrowser.tabs, gBrowser.selectedTab), 1,
+ "The second tab should be selected");
+ is(gBrowser.tabs[1], tabs[1],
+ "The second tab in gBrowser.tabs should be equal to the second tab in our array");
+ is(gBrowser.selectedTab, tabs[1],
+ "The second tab in our array is the selected tab");
+ yield loadURI(tabs[1], "about:blank");
+ let key = tabs[1].linkedBrowser.permanentKey;
+
+ let win = gBrowser.replaceTabWithWindow(tabs[1]);
+ yield new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+ delete tabs[1];
+
+ // Verify that the original window now only has the initial tab left in it.
+ is(gBrowser.tabs[0], tabs[0], "tab0");
+ is(gBrowser.tabs[0].linkedBrowser.currentURI.spec, "about:blank", "tab0 uri");
+
+ let tab = win.gBrowser.tabs[0];
+ is(tab.linkedBrowser.permanentKey, key, "Should have kept the key");
+
+ let awaitPageShow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ win.gBrowser.goBack();
+ yield awaitPageShow;
+
+ yield clickTest(tab);
+ promiseWindowClosed(win);
+});
diff --git a/browser/base/content/test/general/browser_tab_dragdrop2.js b/browser/base/content/test/general/browser_tab_dragdrop2.js
new file mode 100644
index 000000000..2ab622d8b
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop2.js
@@ -0,0 +1,57 @@
+"use strict";
+
+const ROOT = getRootDirectory(gTestPath);
+const URI = ROOT + "browser_tab_dragdrop2_frame1.xul";
+
+// Load the test page (which runs some child popup tests) in a new window.
+// After the tests were run, tear off the tab into a new window and run popup
+// tests a second time. We don't care about tests results, exceptions and
+// crashes will be caught.
+add_task(function* () {
+ // Open a new window.
+ let args = "chrome,all,dialog=no";
+ let win = window.openDialog(getBrowserURL(), "_blank", args, URI);
+
+ // Wait until the tests were run.
+ yield promiseTestsDone(win);
+ ok(true, "tests succeeded");
+
+ // Create a second tab so that we can move the original one out.
+ win.gBrowser.addTab("about:blank", {skipAnimation: true});
+
+ // Tear off the original tab.
+ let browser = win.gBrowser.selectedBrowser;
+ let tabClosed = promiseWaitForEvent(browser, "pagehide", true);
+ let win2 = win.gBrowser.replaceTabWithWindow(win.gBrowser.tabs[0]);
+
+ // Add a 'TestsDone' event listener to ensure that the docShells is properly
+ // swapped to the new window instead of the page being loaded again. If this
+ // works fine we should *NOT* see a TestsDone event.
+ let onTestsDone = () => ok(false, "shouldn't run tests when tearing off");
+ win2.addEventListener("TestsDone", onTestsDone);
+
+ // Wait until the original tab is gone and the new window is ready.
+ yield Promise.all([tabClosed, promiseDelayedStartupFinished(win2)]);
+
+ // Remove the 'TestsDone' event listener as now
+ // we're kicking off a new test run manually.
+ win2.removeEventListener("TestsDone", onTestsDone);
+
+ // Run tests once again.
+ let promise = promiseTestsDone(win2);
+ win2.content.test_panels();
+ yield promise;
+ ok(true, "tests succeeded a second time");
+
+ // Cleanup.
+ yield promiseWindowClosed(win2);
+ yield promiseWindowClosed(win);
+});
+
+function promiseTestsDone(win) {
+ return promiseWaitForEvent(win, "TestsDone");
+}
+
+function promiseDelayedStartupFinished(win) {
+ return new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+}
diff --git a/browser/base/content/test/general/browser_tab_dragdrop2_frame1.xul b/browser/base/content/test/general/browser_tab_dragdrop2_frame1.xul
new file mode 100644
index 000000000..d11709942
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_dragdrop2_frame1.xul
@@ -0,0 +1,169 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+ XUL Widget Test for panels
+ -->
+<window title="Titlebar" width="200" height="200"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<tree id="tree" seltype="single" width="100" height="100">
+ <treecols>
+ <treecol flex="1"/>
+ <treecol flex="1"/>
+ </treecols>
+ <treechildren id="treechildren">
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem>
+ </treechildren>
+</tree>
+
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+var currentTest = null;
+
+var i, waitSteps;
+var my_debug = false;
+function test_panels()
+{
+ i = waitSteps = 0;
+ checkTreeCoords();
+
+ addEventListener("popupshown", popupShown, false);
+ addEventListener("popuphidden", nextTest, false);
+ return nextTest();
+}
+
+function nextTest()
+{
+ ok(true,"popuphidden " + i)
+ if (i == tests.length) {
+ let details = {bubbles: true, cancelable: false};
+ document.dispatchEvent(new CustomEvent("TestsDone", details));
+ return i;
+ }
+
+ currentTest = tests[i];
+ var panel = createPanel(currentTest.attrs);
+ SimpleTest.waitForFocus(() => currentTest.test(panel));
+ return i;
+}
+
+function popupShown(event)
+{
+ var panel = event.target;
+ if (waitSteps > 0 && navigator.platform.indexOf("Linux") >= 0 &&
+ panel.boxObject.screenY == 210) {
+ waitSteps--;
+ setTimeout(popupShown, 10, event);
+ return;
+ }
+ ++i;
+
+ currentTest.result(currentTest.testname + " ", panel);
+ panel.hidePopup();
+}
+
+function createPanel(attrs)
+{
+ var panel = document.createElement("panel");
+ for (var a in attrs) {
+ panel.setAttribute(a, attrs[a]);
+ }
+
+ var button = document.createElement("button");
+ panel.appendChild(button);
+ button.label = "OK";
+ button.width = 120;
+ button.height = 40;
+ button.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;");
+ panel.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;");
+ return document.documentElement.appendChild(panel);
+}
+
+function checkTreeCoords()
+{
+ var tree = $("tree");
+ var treechildren = $("treechildren");
+ tree.currentIndex = 0;
+ tree.treeBoxObject.scrollToRow(0);
+ synthesizeMouse(treechildren, 10, tree.treeBoxObject.rowHeight + 2, { });
+
+ tree.treeBoxObject.scrollToRow(2);
+ synthesizeMouse(treechildren, 10, tree.treeBoxObject.rowHeight + 2, { });
+}
+
+var tests = [
+ {
+ testname: "normal panel",
+ attrs: { },
+ test: function(panel) {
+ panel.openPopupAtScreen(200, 210);
+ },
+ result: function(testname, panel) {
+ if (my_debug) alert(testname);
+ var panelrect = panel.getBoundingClientRect();
+ }
+ },
+ {
+ // only noautohide panels support titlebars, so one shouldn't be shown here
+ testname: "autohide panel with titlebar",
+ attrs: { titlebar: "normal" },
+ test: function(panel) {
+ panel.openPopupAtScreen(200, 210);
+ },
+ result: function(testname, panel) {
+ if (my_debug) alert(testname);
+ var panelrect = panel.getBoundingClientRect();
+ }
+ },
+ {
+ testname: "noautohide panel with titlebar",
+ attrs: { noautohide: true, titlebar: "normal" },
+ test: function(panel) {
+ waitSteps = 25;
+ panel.openPopupAtScreen(200, 210);
+ },
+ result: function(testname, panel) {
+ if (my_debug) alert(testname);
+ var panelrect = panel.getBoundingClientRect();
+
+ var gotMouseEvent = false;
+ function mouseMoved(event)
+ {
+ gotMouseEvent = true;
+ }
+
+ panel.addEventListener("mousemove", mouseMoved, true);
+ synthesizeMouse(panel, 10, 10, { type: "mousemove" });
+ panel.removeEventListener("mousemove", mouseMoved, true);
+
+ var tree = $("tree");
+ tree.currentIndex = 0;
+ panel.appendChild(tree);
+ checkTreeCoords();
+ }
+ }
+];
+
+SimpleTest.waitForFocus(test_panels);
+
+]]>
+</script>
+
+</window>
diff --git a/browser/base/content/test/general/browser_tabbar_big_widgets.js b/browser/base/content/test/general/browser_tabbar_big_widgets.js
new file mode 100644
index 000000000..7a4c45138
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabbar_big_widgets.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const kButtonId = "test-tabbar-size-with-large-buttons";
+
+function test() {
+ registerCleanupFunction(cleanup);
+ let titlebar = document.getElementById("titlebar");
+ let originalHeight = titlebar.getBoundingClientRect().height;
+ let button = document.createElement("toolbarbutton");
+ button.id = kButtonId;
+ button.setAttribute("style", "min-height: 100px");
+ gNavToolbox.palette.appendChild(button);
+ CustomizableUI.addWidgetToArea(kButtonId, CustomizableUI.AREA_TABSTRIP);
+ let currentHeight = titlebar.getBoundingClientRect().height;
+ ok(currentHeight > originalHeight, "Titlebar should have grown");
+ CustomizableUI.removeWidgetFromArea(kButtonId);
+ currentHeight = titlebar.getBoundingClientRect().height;
+ is(currentHeight, originalHeight, "Titlebar should have gone back to its original size.");
+}
+
+function cleanup() {
+ let btn = document.getElementById(kButtonId);
+ if (btn) {
+ btn.remove();
+ }
+}
+
diff --git a/browser/base/content/test/general/browser_tabfocus.js b/browser/base/content/test/general/browser_tabfocus.js
new file mode 100644
index 000000000..4042421e8
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabfocus.js
@@ -0,0 +1,565 @@
+/*
+ * This test checks that focus is adjusted properly when switching tabs.
+ */
+
+var testPage1 = "<html id='html1'><body id='body1'><button id='button1'>Tab 1</button></body></html>";
+var testPage2 = "<html id='html2'><body id='body2'><button id='button2'>Tab 2</button></body></html>";
+var testPage3 = "<html id='html3'><body id='body3'><button id='button3'>Tab 3</button></body></html>";
+
+const fm = Services.focus;
+
+function EventStore() {
+ this["main-window"] = [];
+ this["window1"] = [];
+ this["window2"] = [];
+}
+
+EventStore.prototype = {
+ "push": function (event) {
+ if (event.indexOf("1") > -1) {
+ this["window1"].push(event);
+ } else if (event.indexOf("2") > -1) {
+ this["window2"].push(event);
+ } else {
+ this["main-window"].push(event);
+ }
+ }
+}
+
+var tab1 = null;
+var tab2 = null;
+var browser1 = null;
+var browser2 = null;
+var _lastfocus;
+var _lastfocuswindow = null;
+var actualEvents = new EventStore();
+var expectedEvents = new EventStore();
+var currentTestName = "";
+var _expectedElement = null;
+var _expectedWindow = null;
+
+var currentPromiseResolver = null;
+
+function* getFocusedElementForBrowser(browser, dontCheckExtraFocus = false)
+{
+ if (gMultiProcessBrowser) {
+ return new Promise((resolve, reject) => {
+ messageManager.addMessageListener("Browser:GetCurrentFocus", function getCurrentFocus(message) {
+ messageManager.removeMessageListener("Browser:GetCurrentFocus", getCurrentFocus);
+ resolve(message.data.details);
+ });
+
+ // The dontCheckExtraFocus flag is used to indicate not to check some
+ // additional focus related properties. This is needed as both URLs are
+ // loaded using the same child process and share focus managers.
+ browser.messageManager.sendAsyncMessage("Browser:GetFocusedElement",
+ { dontCheckExtraFocus : dontCheckExtraFocus });
+ });
+ }
+ var focusedWindow = {};
+ var node = fm.getFocusedElementForWindow(browser.contentWindow, false, focusedWindow);
+ return "Focus is " + (node ? node.id : "<none>");
+}
+
+function focusInChild()
+{
+ var contentFM = Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager);
+
+ function getWindowDocId(target)
+ {
+ return (String(target.location).indexOf("1") >= 0) ? "window1" : "window2";
+ }
+
+ function eventListener(event) {
+ var id;
+ if (event.target instanceof Components.interfaces.nsIDOMWindow)
+ id = getWindowDocId(event.originalTarget) + "-window";
+ else if (event.target instanceof Components.interfaces.nsIDOMDocument)
+ id = getWindowDocId(event.originalTarget) + "-document";
+ else
+ id = event.originalTarget.id;
+ sendSyncMessage("Browser:FocusChanged", { details : event.type + ": " + id });
+ }
+
+ addEventListener("focus", eventListener, true);
+ addEventListener("blur", eventListener, true);
+
+ addMessageListener("Browser:ChangeFocus", function changeFocus(message) {
+ content.document.getElementById(message.data.id)[message.data.type]();
+ });
+
+ addMessageListener("Browser:GetFocusedElement", function getFocusedElement(message) {
+ var focusedWindow = {};
+ var node = contentFM.getFocusedElementForWindow(content, false, focusedWindow);
+ var details = "Focus is " + (node ? node.id : "<none>");
+
+ /* Check focus manager properties. Add an error onto the string if they are
+ not what is expected which will cause matching to fail in the parent process. */
+ let doc = content.document;
+ if (!message.data.dontCheckExtraFocus) {
+ if (contentFM.focusedElement != node) {
+ details += "<ERROR: focusedElement doesn't match>";
+ }
+ if (contentFM.focusedWindow && contentFM.focusedWindow != content) {
+ details += "<ERROR: focusedWindow doesn't match>";
+ }
+ if ((contentFM.focusedWindow == content) != doc.hasFocus()) {
+ details += "<ERROR: child hasFocus() is not correct>";
+ }
+ if ((contentFM.focusedElement && doc.activeElement != contentFM.focusedElement) ||
+ (!contentFM.focusedElement && doc.activeElement != doc.body)) {
+ details += "<ERROR: child activeElement is not correct>";
+ }
+ }
+
+ sendSyncMessage("Browser:GetCurrentFocus", { details : details });
+ });
+}
+
+function focusElementInChild(elementid, type)
+{
+ let browser = (elementid.indexOf("1") >= 0) ? browser1 : browser2;
+ if (gMultiProcessBrowser) {
+ browser.messageManager.sendAsyncMessage("Browser:ChangeFocus",
+ { id: elementid, type: type });
+ }
+ else {
+ browser.contentDocument.getElementById(elementid)[type]();
+ }
+}
+
+add_task(function*() {
+ tab1 = gBrowser.addTab();
+ browser1 = gBrowser.getBrowserForTab(tab1);
+
+ tab2 = gBrowser.addTab();
+ browser2 = gBrowser.getBrowserForTab(tab2);
+
+ yield promiseTabLoadEvent(tab1, "data:text/html," + escape(testPage1));
+ yield promiseTabLoadEvent(tab2, "data:text/html," + escape(testPage2));
+
+ var childFocusScript = "data:,(" + focusInChild.toString() + ")();";
+ browser1.messageManager.loadFrameScript(childFocusScript, true);
+ browser2.messageManager.loadFrameScript(childFocusScript, true);
+
+ gURLBar.focus();
+ yield SimpleTest.promiseFocus();
+
+ if (gMultiProcessBrowser) {
+ messageManager.addMessageListener("Browser:FocusChanged", message => {
+ actualEvents.push(message.data.details);
+ compareFocusResults();
+ });
+ }
+
+ _lastfocus = "urlbar";
+ _lastfocuswindow = "main-window";
+
+ window.addEventListener("focus", _browser_tabfocus_test_eventOccured, true);
+ window.addEventListener("blur", _browser_tabfocus_test_eventOccured, true);
+
+ // make sure that the focus initially starts out blank
+ var focusedWindow = {};
+
+ let focused = yield getFocusedElementForBrowser(browser1);
+ is(focused, "Focus is <none>", "initial focus in tab 1");
+
+ focused = yield getFocusedElementForBrowser(browser2);
+ is(focused, "Focus is <none>", "initial focus in tab 2");
+
+ is(document.activeElement, gURLBar.inputField, "focus after loading two tabs");
+
+ yield* expectFocusShiftAfterTabSwitch(tab2, "window2", null, true,
+ "after tab change, focus in new tab");
+
+ focused = yield getFocusedElementForBrowser(browser2);
+ is(focused, "Focus is <none>", "focusedElement after tab change, focus in new tab");
+
+ // switching tabs when nothing in the new tab is focused
+ // should focus the browser
+ yield* expectFocusShiftAfterTabSwitch(tab1, "window1", null, true,
+ "after tab change, focus in original tab");
+
+ focused = yield getFocusedElementForBrowser(browser1);
+ is(focused, "Focus is <none>", "focusedElement after tab change, focus in original tab");
+
+ // focusing a button in the current tab should focus it
+ yield expectFocusShift(() => focusElementInChild("button1", "focus"),
+ "window1", "button1", true,
+ "after button focused");
+
+ focused = yield getFocusedElementForBrowser(browser1);
+ is(focused, "Focus is button1", "focusedElement in first browser after button focused");
+
+ // focusing a button in a background tab should not change the actual
+ // focus, but should set the focus that would be in that background tab to
+ // that button.
+ yield expectFocusShift(() => focusElementInChild("button2", "focus"),
+ "window1", "button1", false,
+ "after button focus in unfocused tab");
+
+ focused = yield getFocusedElementForBrowser(browser1, false);
+ is(focused, "Focus is button1", "focusedElement in first browser after button focus in unfocused tab");
+ focused = yield getFocusedElementForBrowser(browser2, true);
+ is(focused, "Focus is button2", "focusedElement in second browser after button focus in unfocused tab");
+
+ // switching tabs should now make the button in the other tab focused
+ yield* expectFocusShiftAfterTabSwitch(tab2, "window2", "button2", true,
+ "after tab change with button focused");
+
+ // blurring an element in a background tab should not change the active
+ // focus, but should clear the focus in that tab.
+ yield expectFocusShift(() => focusElementInChild("button1", "blur"),
+ "window2", "button2", false,
+ "focusedWindow after blur in unfocused tab");
+
+ focused = yield getFocusedElementForBrowser(browser1, true);
+ is(focused, "Focus is <none>", "focusedElement in first browser after focus in unfocused tab");
+ focused = yield getFocusedElementForBrowser(browser2, false);
+ is(focused, "Focus is button2", "focusedElement in second browser after focus in unfocused tab");
+
+ // When focus is in the tab bar, it should be retained there
+ yield expectFocusShift(() => gBrowser.selectedTab.focus(),
+ "main-window", "tab2", true,
+ "focusing tab element");
+ yield* expectFocusShiftAfterTabSwitch(tab1, "main-window", "tab1", true,
+ "tab change when selected tab element was focused");
+
+ let switchWaiter;
+ if (gMultiProcessBrowser) {
+ switchWaiter = new Promise((resolve, reject) => {
+ gBrowser.addEventListener("TabSwitchDone", function listener() {
+ gBrowser.removeEventListener("TabSwitchDone", listener);
+ executeSoon(resolve);
+ });
+ });
+ }
+
+ yield* expectFocusShiftAfterTabSwitch(tab2, "main-window", "tab2", true,
+ "another tab change when selected tab element was focused");
+
+ // When this a remote browser, wait for the paint on the second browser so that
+ // any post tab-switching stuff has time to complete before blurring the tab.
+ // Otherwise, the _adjustFocusAfterTabSwitch in tabbrowser gets confused and
+ // isn't sure what tab is really focused.
+ if (gMultiProcessBrowser) {
+ yield switchWaiter;
+ }
+
+ yield expectFocusShift(() => gBrowser.selectedTab.blur(),
+ "main-window", null, true,
+ "blurring tab element");
+
+ // focusing the url field should switch active focus away from the browser but
+ // not clear what would be the focus in the browser
+ focusElementInChild("button1", "focus");
+
+ yield expectFocusShift(() => gURLBar.focus(),
+ "main-window", "urlbar", true,
+ "focusedWindow after url field focused");
+ focused = yield getFocusedElementForBrowser(browser1, true);
+ is(focused, "Focus is button1", "focusedElement after url field focused, first browser");
+ focused = yield getFocusedElementForBrowser(browser2, true);
+ is(focused, "Focus is button2", "focusedElement after url field focused, second browser");
+
+ yield expectFocusShift(() => gURLBar.blur(),
+ "main-window", null, true,
+ "blurring url field");
+
+ // when a chrome element is focused, switching tabs to a tab with a button
+ // with the current focus should focus the button
+ yield* expectFocusShiftAfterTabSwitch(tab1, "window1", "button1", true,
+ "after tab change, focus in url field, button focused in new tab");
+
+ focused = yield getFocusedElementForBrowser(browser1, false);
+ is(focused, "Focus is button1", "after switch tab, focus in unfocused tab, first browser");
+ focused = yield getFocusedElementForBrowser(browser2, true);
+ is(focused, "Focus is button2", "after switch tab, focus in unfocused tab, second browser");
+
+ // blurring an element in the current tab should clear the active focus
+ yield expectFocusShift(() => focusElementInChild("button1", "blur"),
+ "window1", null, true,
+ "after blur in focused tab");
+
+ focused = yield getFocusedElementForBrowser(browser1, false);
+ is(focused, "Focus is <none>", "focusedWindow after blur in focused tab, child");
+ focusedWindow = {};
+ is(fm.getFocusedElementForWindow(window, false, focusedWindow), browser1, "focusedElement after blur in focused tab, parent");
+
+ // blurring an non-focused url field should have no effect
+ yield expectFocusShift(() => gURLBar.blur(),
+ "window1", null, false,
+ "after blur in unfocused url field");
+
+ focusedWindow = {};
+ is(fm.getFocusedElementForWindow(window, false, focusedWindow), browser1, "focusedElement after blur in unfocused url field");
+
+ // switch focus to a tab with a currently focused element
+ yield* expectFocusShiftAfterTabSwitch(tab2, "window2", "button2", true,
+ "after switch from unfocused to focused tab");
+ focused = yield getFocusedElementForBrowser(browser2, true);
+ is(focused, "Focus is button2", "focusedElement after switch from unfocused to focused tab");
+
+ // clearing focus on the chrome window should switch the focus to the
+ // chrome window
+ yield expectFocusShift(() => fm.clearFocus(window),
+ "main-window", null, true,
+ "after switch to chrome with no focused element");
+
+ focusedWindow = {};
+ is(fm.getFocusedElementForWindow(window, false, focusedWindow), null, "focusedElement after switch to chrome with no focused element");
+
+ // switch focus to another tab when neither have an active focus
+ yield* expectFocusShiftAfterTabSwitch(tab1, "window1", null, true,
+ "focusedWindow after tab switch from no focus to no focus");
+
+ focused = yield getFocusedElementForBrowser(browser1, false);
+ is(focused, "Focus is <none>", "after tab switch from no focus to no focus, first browser");
+ focused = yield getFocusedElementForBrowser(browser2, true);
+ is(focused, "Focus is button2", "after tab switch from no focus to no focus, second browser");
+
+ // next, check whether navigating forward, focusing the urlbar and then
+ // navigating back maintains the focus in the urlbar.
+ yield expectFocusShift(() => focusElementInChild("button1", "focus"),
+ "window1", "button1", true,
+ "focus button");
+
+ yield promiseTabLoadEvent(tab1, "data:text/html," + escape(testPage3));
+
+ // now go back again
+ gURLBar.focus();
+
+ yield new Promise((resolve, reject) => {
+ window.addEventListener("pageshow", function navigationOccured(event) {
+ window.removeEventListener("pageshow", navigationOccured, true);
+ resolve();
+ }, true);
+ document.getElementById('Browser:Back').doCommand();
+ });
+
+ is(window.document.activeElement, gURLBar.inputField, "urlbar still focused after navigating back");
+
+ // Document navigation with F6 does not yet work in mutli-process browsers.
+ if (!gMultiProcessBrowser) {
+ gURLBar.focus();
+ actualEvents = new EventStore();
+ _lastfocus = "urlbar";
+ _lastfocuswindow = "main-window";
+
+ yield expectFocusShift(() => EventUtils.synthesizeKey("VK_F6", { }),
+ "window1", "html1",
+ true, "switch document forward with f6");
+
+ EventUtils.synthesizeKey("VK_F6", { });
+ is(fm.focusedWindow, window, "switch document forward again with f6");
+
+ browser1.style.MozUserFocus = "ignore";
+ browser1.clientWidth;
+ EventUtils.synthesizeKey("VK_F6", { });
+ is(fm.focusedWindow, window, "switch document forward again with f6 when browser non-focusable");
+
+ browser1.style.MozUserFocus = "normal";
+ browser1.clientWidth;
+ }
+
+ window.removeEventListener("focus", _browser_tabfocus_test_eventOccured, true);
+ window.removeEventListener("blur", _browser_tabfocus_test_eventOccured, true);
+
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+
+ finish();
+});
+
+function _browser_tabfocus_test_eventOccured(event)
+{
+ function getWindowDocId(target)
+ {
+ if (target == browser1.contentWindow || target == browser1.contentDocument) {
+ return "window1";
+ }
+ if (target == browser2.contentWindow || target == browser2.contentDocument) {
+ return "window2";
+ }
+ return "main-window";
+ }
+
+ var id;
+
+ // Some focus events from the child bubble up? Ignore them.
+ if (Cu.isCrossProcessWrapper(event.originalTarget))
+ return;
+
+ if (event.target instanceof Window)
+ id = getWindowDocId(event.originalTarget) + "-window";
+ else if (event.target instanceof Document)
+ id = getWindowDocId(event.originalTarget) + "-document";
+ else if (event.target.id == "urlbar" && event.originalTarget.localName == "input")
+ id = "urlbar";
+ else if (event.originalTarget.localName == "browser")
+ id = (event.originalTarget == browser1) ? "browser1" : "browser2";
+ else if (event.originalTarget.localName == "tab")
+ id = (event.originalTarget == tab1) ? "tab1" : "tab2";
+ else
+ id = event.originalTarget.id;
+
+ actualEvents.push(event.type + ": " + id);
+ compareFocusResults();
+}
+
+function getId(element)
+{
+ if (!element) {
+ return null;
+ }
+
+ if (element.localName == "browser") {
+ return element == browser1 ? "browser1" : "browser2";
+ }
+
+ if (element.localName == "tab") {
+ return element == tab1 ? "tab1" : "tab2";
+ }
+
+ return (element.localName == "input") ? "urlbar" : element.id;
+}
+
+function compareFocusResults()
+{
+ if (!currentPromiseResolver)
+ return;
+
+ let winIds = ["main-window", "window1", "window2"];
+
+ for (let winId of winIds) {
+ if (actualEvents[winId].length < expectedEvents[winId].length)
+ return;
+ }
+
+ for (let winId of winIds) {
+ for (let e = 0; e < expectedEvents.length; e++) {
+ is(actualEvents[winId][e], expectedEvents[winId][e], currentTestName + " events [event " + e + "]");
+ }
+ actualEvents[winId] = [];
+ }
+
+ // Use executeSoon as this will be called during a focus/blur event handler
+ executeSoon(() => {
+ let matchWindow = window;
+ if (gMultiProcessBrowser) {
+ is(_expectedWindow, "main-window", "main-window is always expected");
+ }
+ else if (_expectedWindow != "main-window") {
+ matchWindow = (_expectedWindow == "window1" ? browser1.contentWindow : browser2.contentWindow);
+ }
+
+ var focusedElement = fm.focusedElement;
+ is(getId(focusedElement), _expectedElement, currentTestName + " focusedElement");
+ is(fm.focusedWindow, matchWindow, currentTestName + " focusedWindow");
+ var focusedWindow = {};
+ is(getId(fm.getFocusedElementForWindow(matchWindow, false, focusedWindow)),
+ _expectedElement, currentTestName + " getFocusedElementForWindow");
+ is(focusedWindow.value, matchWindow, currentTestName + " getFocusedElementForWindow frame");
+ is(matchWindow.document.hasFocus(), true, currentTestName + " hasFocus");
+ var expectedActive = _expectedElement;
+ if (!expectedActive) {
+ expectedActive = matchWindow.document instanceof XULDocument ?
+ "main-window" : getId(matchWindow.document.body);
+ }
+ is(getId(matchWindow.document.activeElement), expectedActive, currentTestName + " activeElement");
+
+ currentPromiseResolver();
+ currentPromiseResolver = null;
+ });
+}
+
+function* expectFocusShiftAfterTabSwitch(tab, expectedWindow, expectedElement, focusChanged, testid)
+{
+ let tabSwitchPromise = null;
+ yield expectFocusShift(() => { tabSwitchPromise = BrowserTestUtils.switchTab(gBrowser, tab) },
+ expectedWindow, expectedElement, focusChanged, testid)
+ yield tabSwitchPromise;
+}
+
+function* expectFocusShift(callback, expectedWindow, expectedElement, focusChanged, testid)
+{
+ currentPromiseResolver = null;
+ currentTestName = testid;
+
+ expectedEvents = new EventStore();
+
+ if (focusChanged) {
+ _expectedElement = expectedElement;
+ _expectedWindow = expectedWindow;
+
+ // When the content is in a child process, the expected element in the chrome window
+ // will always be the urlbar or a browser element.
+ if (gMultiProcessBrowser) {
+ if (_expectedWindow == "window1") {
+ _expectedElement = "browser1";
+ }
+ else if (_expectedWindow == "window2") {
+ _expectedElement = "browser2";
+ }
+ _expectedWindow = "main-window";
+ }
+
+ if (gMultiProcessBrowser && _lastfocuswindow != "main-window" &&
+ _lastfocuswindow != expectedWindow) {
+ let browserid = _lastfocuswindow == "window1" ? "browser1" : "browser2";
+ expectedEvents.push("blur: " + browserid);
+ }
+
+ var newElementIsFocused = (expectedElement && !expectedElement.startsWith("html"));
+ if (newElementIsFocused && gMultiProcessBrowser &&
+ _lastfocuswindow != "main-window" &&
+ expectedWindow == "main-window") {
+ // When switching from a child to a chrome element, the focus on the element will arrive first.
+ expectedEvents.push("focus: " + expectedElement);
+ newElementIsFocused = false;
+ }
+
+ if (_lastfocus && _lastfocus != _expectedElement)
+ expectedEvents.push("blur: " + _lastfocus);
+
+ if (_lastfocuswindow &&
+ _lastfocuswindow != expectedWindow) {
+
+ if (!gMultiProcessBrowser || _lastfocuswindow != "main-window") {
+ expectedEvents.push("blur: " + _lastfocuswindow + "-document");
+ expectedEvents.push("blur: " + _lastfocuswindow + "-window");
+ }
+ }
+
+ if (expectedWindow && _lastfocuswindow != expectedWindow) {
+ if (gMultiProcessBrowser && expectedWindow != "main-window") {
+ let browserid = expectedWindow == "window1" ? "browser1" : "browser2";
+ expectedEvents.push("focus: " + browserid);
+ }
+
+ if (!gMultiProcessBrowser || expectedWindow != "main-window") {
+ expectedEvents.push("focus: " + expectedWindow + "-document");
+ expectedEvents.push("focus: " + expectedWindow + "-window");
+ }
+ }
+
+ if (newElementIsFocused) {
+ expectedEvents.push("focus: " + expectedElement);
+ }
+
+ _lastfocus = expectedElement;
+ _lastfocuswindow = expectedWindow;
+ }
+
+ return new Promise((resolve, reject) => {
+ currentPromiseResolver = resolve;
+ callback();
+
+ // No events are expected, so resolve the promise immediately.
+ if (expectedEvents["main-window"].length + expectedEvents["window1"].length + expectedEvents["window2"].length == 0) {
+ currentPromiseResolver();
+ currentPromiseResolver = null;
+ }
+ });
+}
diff --git a/browser/base/content/test/general/browser_tabkeynavigation.js b/browser/base/content/test/general/browser_tabkeynavigation.js
new file mode 100644
index 000000000..d8e51f4b9
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabkeynavigation.js
@@ -0,0 +1,156 @@
+/*
+ * This test checks that keyboard navigation for tabs isn't blocked by content
+ */
+add_task(function* test() {
+
+ let testPage1 = "data:text/html,<html id='tab1'><body><button id='button1'>Tab 1</button></body></html>";
+ let testPage2 = "data:text/html,<html id='tab2'><body><button id='button2'>Tab 2</button><script>function preventDefault(event) { event.preventDefault(); event.stopImmediatePropagation(); } window.addEventListener('keydown', preventDefault, true); window.addEventListener('keypress', preventDefault, true);</script></body></html>";
+ let testPage3 = "data:text/html,<html id='tab3'><body><button id='button3'>Tab 3</button></body></html>";
+
+ let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, testPage1);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+ let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, testPage2);
+ let tab3 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, testPage3);
+
+ // Kill the animation for simpler test.
+ Services.prefs.setBoolPref("browser.tabs.animate", false);
+
+ gBrowser.selectedTab = tab1;
+ browser1.focus();
+
+ is(gBrowser.selectedTab, tab1,
+ "Tab1 should be activated");
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated by pressing Ctrl+Tab on Tab1");
+
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true });
+ is(gBrowser.selectedTab, tab3,
+ "Tab3 should be activated by pressing Ctrl+Tab on Tab2");
+
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated by pressing Ctrl+Shift+Tab on Tab3");
+
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: true });
+ is(gBrowser.selectedTab, tab1,
+ "Tab1 should be activated by pressing Ctrl+Shift+Tab on Tab2");
+
+ gBrowser.selectedTab = tab1;
+ browser1.focus();
+
+ is(gBrowser.selectedTab, tab1,
+ "Tab1 should be activated");
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated by pressing Ctrl+PageDown on Tab1");
+
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true });
+ is(gBrowser.selectedTab, tab3,
+ "Tab3 should be activated by pressing Ctrl+PageDown on Tab2");
+
+ EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated by pressing Ctrl+PageUp on Tab3");
+
+ EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true });
+ is(gBrowser.selectedTab, tab1,
+ "Tab1 should be activated by pressing Ctrl+PageUp on Tab2");
+
+ if (gBrowser.mTabBox._handleMetaAltArrows) {
+ gBrowser.selectedTab = tab1;
+ browser1.focus();
+
+ let ltr = window.getComputedStyle(gBrowser.mTabBox, "").direction == "ltr";
+ let advanceKey = ltr ? "VK_RIGHT" : "VK_LEFT";
+ let reverseKey = ltr ? "VK_LEFT" : "VK_RIGHT";
+
+ is(gBrowser.selectedTab, tab1,
+ "Tab1 should be activated");
+ EventUtils.synthesizeKey(advanceKey, { altKey: true, metaKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated by pressing Ctrl+" + advanceKey + " on Tab1");
+
+ EventUtils.synthesizeKey(advanceKey, { altKey: true, metaKey: true });
+ is(gBrowser.selectedTab, tab3,
+ "Tab3 should be activated by pressing Ctrl+" + advanceKey + " on Tab2");
+
+ EventUtils.synthesizeKey(reverseKey, { altKey: true, metaKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated by pressing Ctrl+" + reverseKey + " on Tab3");
+
+ EventUtils.synthesizeKey(reverseKey, { altKey: true, metaKey: true });
+ is(gBrowser.selectedTab, tab1,
+ "Tab1 should be activated by pressing Ctrl+" + reverseKey + " on Tab2");
+ }
+
+ gBrowser.selectedTab = tab2;
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated");
+ is(gBrowser.tabContainer.selectedIndex, 2,
+ "Tab2 index should be 2");
+
+ EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true, shiftKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated after Ctrl+Shift+PageDown");
+ is(gBrowser.tabContainer.selectedIndex, 3,
+ "Tab2 index should be 1 after Ctrl+Shift+PageDown");
+
+ EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true, shiftKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated after Ctrl+Shift+PageUp");
+ is(gBrowser.tabContainer.selectedIndex, 2,
+ "Tab2 index should be 2 after Ctrl+Shift+PageUp");
+
+ if (navigator.platform.indexOf("Mac") == 0) {
+ gBrowser.selectedTab = tab1;
+ browser1.focus();
+
+ // XXX Currently, Command + "{" and "}" don't work if keydown event is
+ // consumed because following keypress event isn't fired.
+
+ let ltr = window.getComputedStyle(gBrowser.mTabBox, "").direction == "ltr";
+ let advanceKey = ltr ? "}" : "{";
+ let reverseKey = ltr ? "{" : "}";
+
+ is(gBrowser.selectedTab, tab1,
+ "Tab1 should be activated");
+
+ EventUtils.synthesizeKey(advanceKey, { metaKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated by pressing Ctrl+" + advanceKey + " on Tab1");
+
+ EventUtils.synthesizeKey(advanceKey, { metaKey: true });
+ is(gBrowser.selectedTab, tab3,
+ "Tab3 should be activated by pressing Ctrl+" + advanceKey + " on Tab2");
+
+ EventUtils.synthesizeKey(reverseKey, { metaKey: true });
+ is(gBrowser.selectedTab, tab2,
+ "Tab2 should be activated by pressing Ctrl+" + reverseKey + " on Tab3");
+
+ EventUtils.synthesizeKey(reverseKey, { metaKey: true });
+ is(gBrowser.selectedTab, tab1,
+ "Tab1 should be activated by pressing Ctrl+" + reverseKey + " on Tab2");
+ } else {
+ gBrowser.selectedTab = tab2;
+ EventUtils.synthesizeKey("VK_F4", { type: "keydown", ctrlKey: true });
+
+ isnot(gBrowser.selectedTab, tab2,
+ "Tab2 should be closed by pressing Ctrl+F4 on Tab2");
+ is(gBrowser.tabs.length, 3,
+ "The count of tabs should be 3 since tab2 should be closed");
+
+ // NOTE: keypress event shouldn't be fired since the keydown event should
+ // be consumed by tab2.
+ EventUtils.synthesizeKey("VK_F4", { type: "keyup", ctrlKey: true });
+ is(gBrowser.tabs.length, 3,
+ "The count of tabs should be 3 since renaming key events shouldn't close other tabs");
+ }
+
+ gBrowser.selectedTab = tab3;
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+
+ Services.prefs.clearUserPref("browser.tabs.animate");
+});
diff --git a/browser/base/content/test/general/browser_tabopen_reflows.js b/browser/base/content/test/general/browser_tabopen_reflows.js
new file mode 100644
index 000000000..8e04cf12e
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabopen_reflows.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+XPCOMUtils.defineLazyGetter(this, "docShell", () => {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+});
+
+const EXPECTED_REFLOWS = [
+ // tabbrowser.adjustTabstrip() call after tabopen animation has finished
+ "adjustTabstrip@chrome://browser/content/tabbrowser.xml|" +
+ "_handleNewTab@chrome://browser/content/tabbrowser.xml|" +
+ "onxbltransitionend@chrome://browser/content/tabbrowser.xml|",
+
+ // switching focus in updateCurrentBrowser() causes reflows
+ "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml|" +
+ "updateCurrentBrowser@chrome://browser/content/tabbrowser.xml|" +
+ "onselect@chrome://browser/content/browser.xul|",
+
+ // switching focus in openLinkIn() causes reflows
+ "openLinkIn@chrome://browser/content/utilityOverlay.js|" +
+ "openUILinkIn@chrome://browser/content/utilityOverlay.js|" +
+ "BrowserOpenTab@chrome://browser/content/browser.js|",
+
+ // accessing element.scrollPosition in _fillTrailingGap() flushes layout
+ "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml|" +
+ "_fillTrailingGap@chrome://browser/content/tabbrowser.xml|" +
+ "_handleNewTab@chrome://browser/content/tabbrowser.xml|" +
+ "onxbltransitionend@chrome://browser/content/tabbrowser.xml|",
+
+ // SessionStore.getWindowDimensions()
+ "ssi_getWindowDimension@resource:///modules/sessionstore/SessionStore.jsm|" +
+ "ssi_updateWindowFeatures/<@resource:///modules/sessionstore/SessionStore.jsm|" +
+ "ssi_updateWindowFeatures@resource:///modules/sessionstore/SessionStore.jsm|" +
+ "ssi_collectWindowData@resource:///modules/sessionstore/SessionStore.jsm|",
+
+ // selection change notification may cause querying the focused editor content
+ // by IME and that will cause reflow.
+ "select@chrome://global/content/bindings/textbox.xml|" +
+ "focusAndSelectUrlBar@chrome://browser/content/browser.js|" +
+ "openLinkIn@chrome://browser/content/utilityOverlay.js|" +
+ "openUILinkIn@chrome://browser/content/utilityOverlay.js|" +
+ "BrowserOpenTab@chrome://browser/content/browser.js|",
+
+];
+
+const PREF_PRELOAD = "browser.newtab.preload";
+const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directory.source";
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when opening new tabs.
+ */
+add_task(function*() {
+ let DirectoryLinksProvider = Cu.import("resource:///modules/DirectoryLinksProvider.jsm", {}).DirectoryLinksProvider;
+ let NewTabUtils = Cu.import("resource://gre/modules/NewTabUtils.jsm", {}).NewTabUtils;
+ let Promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+
+ // resolves promise when directory links are downloaded and written to disk
+ function watchLinksChangeOnce() {
+ let deferred = Promise.defer();
+ let observer = {
+ onManyLinksChanged: () => {
+ DirectoryLinksProvider.removeObserver(observer);
+ NewTabUtils.links.populateCache(() => {
+ NewTabUtils.allPages.update();
+ deferred.resolve();
+ }, true);
+ }
+ };
+ observer.onDownloadFail = observer.onManyLinksChanged;
+ DirectoryLinksProvider.addObserver(observer);
+ return deferred.promise;
+ }
+
+ let gOrigDirectorySource = Services.prefs.getCharPref(PREF_NEWTAB_DIRECTORYSOURCE);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(PREF_PRELOAD);
+ Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, gOrigDirectorySource);
+ return watchLinksChangeOnce();
+ });
+
+ Services.prefs.setBoolPref(PREF_PRELOAD, false);
+ // set directory source to dummy/empty links
+ Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, 'data:application/json,{"test":1}');
+
+ // run tests when directory source change completes
+ yield watchLinksChangeOnce();
+
+ // Perform a click in the top left of content to ensure the mouse isn't
+ // hovering over any of the tiles
+ let target = gBrowser.selectedBrowser;
+ let rect = target.getBoundingClientRect();
+ let left = rect.left + 1;
+ let top = rect.top + 1;
+
+ let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+
+ // Add a reflow observer and open a new tab.
+ docShell.addWeakReflowObserver(observer);
+ BrowserOpenTab();
+
+ // Wait until the tabopen animation has finished.
+ yield waitForTransitionEnd();
+
+ // Remove reflow observer and clean up.
+ docShell.removeWeakReflowObserver(observer);
+ gBrowser.removeCurrentTab();
+});
+
+var observer = {
+ reflow: function (start, end) {
+ // Gather information about the current code path.
+ let path = (new Error().stack).split("\n").slice(1).map(line => {
+ return line.replace(/:\d+:\d+$/, "");
+ }).join("|");
+ let pathWithLineNumbers = (new Error().stack).split("\n").slice(1).join("|");
+
+ // Stack trace is empty. Reflow was triggered by native code.
+ if (path === "") {
+ return;
+ }
+
+ // Check if this is an expected reflow.
+ for (let stack of EXPECTED_REFLOWS) {
+ if (path.startsWith(stack)) {
+ ok(true, "expected uninterruptible reflow '" + stack + "'");
+ return;
+ }
+ }
+
+ ok(false, "unexpected uninterruptible reflow '" + pathWithLineNumbers + "'");
+ },
+
+ reflowInterruptible: function (start, end) {
+ // We're not interested in interruptible reflows.
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
+ Ci.nsISupportsWeakReference])
+};
+
+function waitForTransitionEnd() {
+ return new Promise(resolve => {
+ let tab = gBrowser.selectedTab;
+ tab.addEventListener("transitionend", function onEnd(event) {
+ if (event.propertyName === "max-width") {
+ tab.removeEventListener("transitionend", onEnd);
+ resolve();
+ }
+ });
+ });
+}
diff --git a/browser/base/content/test/general/browser_tabs_close_beforeunload.js b/browser/base/content/test/general/browser_tabs_close_beforeunload.js
new file mode 100644
index 000000000..b867efd72
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabs_close_beforeunload.js
@@ -0,0 +1,49 @@
+"use strict";
+
+SimpleTest.requestCompleteLog();
+
+SpecialPowers.pushPrefEnv({"set": [["dom.require_user_interaction_for_beforeunload", false]]});
+
+const FIRST_TAB = getRootDirectory(gTestPath) + "close_beforeunload_opens_second_tab.html";
+const SECOND_TAB = getRootDirectory(gTestPath) + "close_beforeunload.html";
+
+add_task(function*() {
+ info("Opening first tab");
+ let firstTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, FIRST_TAB);
+ let secondTabLoadedPromise;
+ let secondTab;
+ let tabOpened = new Promise(resolve => {
+ info("Adding tabopen listener");
+ gBrowser.tabContainer.addEventListener("TabOpen", function tabOpenListener(e) {
+ info("Got tabopen, removing listener and waiting for load");
+ gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener, false, false);
+ secondTab = e.target;
+ secondTabLoadedPromise = BrowserTestUtils.browserLoaded(secondTab.linkedBrowser, false, SECOND_TAB);
+ resolve();
+ }, false, false);
+ });
+ info("Opening second tab using a click");
+ yield ContentTask.spawn(firstTab.linkedBrowser, "", function*() {
+ content.document.getElementsByTagName("a")[0].click();
+ });
+ info("Waiting for the second tab to be opened");
+ yield tabOpened;
+ info("Waiting for the load in that tab to finish");
+ yield secondTabLoadedPromise;
+
+ let closeBtn = document.getAnonymousElementByAttribute(secondTab, "anonid", "close-button");
+ let closePromise = BrowserTestUtils.removeTab(secondTab, {dontRemove: true});
+ info("closing second tab (which will self-close in beforeunload)");
+ closeBtn.click();
+ ok(secondTab.closing, "Second tab should be marked as closing synchronously.");
+ yield closePromise;
+ ok(secondTab.closing, "Second tab should still be marked as closing");
+ ok(!secondTab.linkedBrowser, "Second tab's browser should be dead");
+ ok(!firstTab.closing, "First tab should not be closing");
+ ok(firstTab.linkedBrowser, "First tab's browser should be alive");
+ info("closing first tab");
+ yield BrowserTestUtils.removeTab(firstTab);
+
+ ok(firstTab.closing, "First tab should be marked as closing");
+ ok(!firstTab.linkedBrowser, "First tab's browser should be dead");
+});
diff --git a/browser/base/content/test/general/browser_tabs_isActive.js b/browser/base/content/test/general/browser_tabs_isActive.js
new file mode 100644
index 000000000..0725757e7
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabs_isActive.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test for the docshell active state of local and remote browsers.
+
+const kTestPage = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+
+function promiseNewTabSwitched() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener("TabSwitchDone", function onSwitch() {
+ gBrowser.removeEventListener("TabSwitchDone", onSwitch);
+ executeSoon(resolve);
+ });
+ });
+}
+
+function getParentTabState(aTab) {
+ return aTab.linkedBrowser.docShellIsActive;
+}
+
+function getChildTabState(aTab) {
+ return ContentTask.spawn(aTab.linkedBrowser, {}, function* () {
+ return docShell.isActive;
+ });
+}
+
+function checkState(parentSide, childSide, value, message) {
+ is(parentSide, value, message + " (parent side)");
+ is(childSide, value, message + " (child side)");
+}
+
+function waitForMs(aMs) {
+ return new Promise((resolve) => {
+ setTimeout(done, aMs);
+ function done() {
+ resolve(true);
+ }
+ });
+}
+
+add_task(function *() {
+ let url = kTestPage;
+ let originalTab = gBrowser.selectedTab; // test tab
+ let newTab = gBrowser.addTab(url, {skipAnimation: true});
+ let parentSide, childSide;
+
+ // new tab added but not selected checks
+ parentSide = getParentTabState(newTab);
+ childSide = yield getChildTabState(newTab);
+ checkState(parentSide, childSide, false, "newly added " + url + " tab is not active");
+ parentSide = getParentTabState(originalTab);
+ childSide = yield getChildTabState(originalTab);
+ checkState(parentSide, childSide, true, "original tab is active initially");
+
+ // select the newly added tab and wait for TabSwitchDone event
+ let tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = newTab;
+ yield tabSwitchedPromise;
+
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ ok(newTab.linkedBrowser.isRemoteBrowser, "for testing we need a remote tab");
+ }
+
+ // check active state of both tabs
+ parentSide = getParentTabState(newTab);
+ childSide = yield getChildTabState(newTab);
+ checkState(parentSide, childSide, true, "newly added " + url + " tab is active after selection");
+ parentSide = getParentTabState(originalTab);
+ childSide = yield getChildTabState(originalTab);
+ checkState(parentSide, childSide, false, "original tab is not active while unselected");
+
+ // switch back to the original test tab and wait for TabSwitchDone event
+ tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = originalTab;
+ yield tabSwitchedPromise;
+
+ // check active state of both tabs
+ parentSide = getParentTabState(newTab);
+ childSide = yield getChildTabState(newTab);
+ checkState(parentSide, childSide, false, "newly added " + url + " tab is not active after switch back");
+ parentSide = getParentTabState(originalTab);
+ childSide = yield getChildTabState(originalTab);
+ checkState(parentSide, childSide, true, "original tab is active again after switch back");
+
+ // switch to the new tab and wait for TabSwitchDone event
+ tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = newTab;
+ yield tabSwitchedPromise;
+
+ // check active state of both tabs
+ parentSide = getParentTabState(newTab);
+ childSide = yield getChildTabState(newTab);
+ checkState(parentSide, childSide, true, "newly added " + url + " tab is not active after switch back");
+ parentSide = getParentTabState(originalTab);
+ childSide = yield getChildTabState(originalTab);
+ checkState(parentSide, childSide, false, "original tab is active again after switch back");
+
+ gBrowser.removeTab(newTab);
+});
+
+add_task(function *() {
+ let url = "about:about";
+ let originalTab = gBrowser.selectedTab; // test tab
+ let newTab = gBrowser.addTab(url, {skipAnimation: true});
+ let parentSide, childSide;
+
+ parentSide = getParentTabState(newTab);
+ childSide = yield getChildTabState(newTab);
+ checkState(parentSide, childSide, false, "newly added " + url + " tab is not active");
+ parentSide = getParentTabState(originalTab);
+ childSide = yield getChildTabState(originalTab);
+ checkState(parentSide, childSide, true, "original tab is active initially");
+
+ let tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = newTab;
+ yield tabSwitchedPromise;
+
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ ok(!newTab.linkedBrowser.isRemoteBrowser, "for testing we need a local tab");
+ }
+
+ parentSide = getParentTabState(newTab);
+ childSide = yield getChildTabState(newTab);
+ checkState(parentSide, childSide, true, "newly added " + url + " tab is active after selection");
+ parentSide = getParentTabState(originalTab);
+ childSide = yield getChildTabState(originalTab);
+ checkState(parentSide, childSide, false, "original tab is not active while unselected");
+
+ tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = originalTab;
+ yield tabSwitchedPromise;
+
+ parentSide = getParentTabState(newTab);
+ childSide = yield getChildTabState(newTab);
+ checkState(parentSide, childSide, false, "newly added " + url + " tab is not active after switch back");
+ parentSide = getParentTabState(originalTab);
+ childSide = yield getChildTabState(originalTab);
+ checkState(parentSide, childSide, true, "original tab is active again after switch back");
+
+ tabSwitchedPromise = promiseNewTabSwitched();
+ gBrowser.selectedTab = newTab;
+ yield tabSwitchedPromise;
+
+ parentSide = getParentTabState(newTab);
+ childSide = yield getChildTabState(newTab);
+ checkState(parentSide, childSide, true, "newly added " + url + " tab is not active after switch back");
+ parentSide = getParentTabState(originalTab);
+ childSide = yield getChildTabState(originalTab);
+ checkState(parentSide, childSide, false, "original tab is active again after switch back");
+
+ gBrowser.removeTab(newTab);
+});
diff --git a/browser/base/content/test/general/browser_tabs_owner.js b/browser/base/content/test/general/browser_tabs_owner.js
new file mode 100644
index 000000000..300d783ba
--- /dev/null
+++ b/browser/base/content/test/general/browser_tabs_owner.js
@@ -0,0 +1,44 @@
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: gBrowser._finalizeTabSwitch is not a function");
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: gBrowser._finalizeTabSwitch is not a function");
+
+function test() {
+ gBrowser.addTab();
+ gBrowser.addTab();
+ gBrowser.addTab();
+
+ var tabs = gBrowser.tabs;
+ var owner;
+
+ is(tabs.length, 4, "4 tabs are open");
+
+ owner = gBrowser.selectedTab = tabs[2];
+ BrowserOpenTab();
+ is(gBrowser.selectedTab, tabs[4], "newly opened tab is selected");
+ gBrowser.removeCurrentTab();
+ is(gBrowser.selectedTab, owner, "owner is selected");
+
+ owner = gBrowser.selectedTab;
+ BrowserOpenTab();
+ gBrowser.selectedTab = tabs[1];
+ gBrowser.selectedTab = tabs[4];
+ gBrowser.removeCurrentTab();
+ isnot(gBrowser.selectedTab, owner, "selecting a different tab clears the owner relation");
+
+ owner = gBrowser.selectedTab;
+ BrowserOpenTab();
+ gBrowser.moveTabTo(gBrowser.selectedTab, 0);
+ gBrowser.removeCurrentTab();
+ is(gBrowser.selectedTab, owner, "owner relatitionship persists when tab is moved");
+
+ while (tabs.length > 1)
+ gBrowser.removeCurrentTab();
+}
diff --git a/browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js b/browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js
new file mode 100644
index 000000000..f90f047d3
--- /dev/null
+++ b/browser/base/content/test/general/browser_testOpenNewRemoteTabsFromNonRemoteBrowsers.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const OPEN_LOCATION_PREF = "browser.link.open_newwindow";
+const NON_REMOTE_PAGE = "about:welcomeback";
+
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+requestLongerTimeout(2);
+
+function frame_script() {
+ content.document.body.innerHTML = `
+ <a href="about:home" target="_blank" id="testAnchor">Open a window</a>
+ `;
+
+ let element = content.document.getElementById("testAnchor");
+ element.click();
+}
+
+/**
+ * Takes some browser in some window, and forces that browser
+ * to become non-remote, and then navigates it to a page that
+ * we're not supposed to be displaying remotely. Returns a
+ * Promise that resolves when the browser is no longer remote.
+ */
+function prepareNonRemoteBrowser(aWindow, browser) {
+ browser.loadURI(NON_REMOTE_PAGE);
+ return BrowserTestUtils.browserLoaded(browser);
+}
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(OPEN_LOCATION_PREF);
+});
+
+/**
+ * Test that if we open a new tab from a link in a non-remote
+ * browser in an e10s window, that the new tab will load properly.
+ */
+add_task(function* test_new_tab() {
+ let normalWindow = yield BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ });
+ let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ private: true,
+ });
+
+ for (let testWindow of [normalWindow, privateWindow]) {
+ yield promiseWaitForFocus(testWindow);
+ let testBrowser = testWindow.gBrowser.selectedBrowser;
+ info("Preparing non-remote browser");
+ yield prepareNonRemoteBrowser(testWindow, testBrowser);
+ info("Non-remote browser prepared - sending frame script");
+
+ // Get our framescript ready
+ let mm = testBrowser.messageManager;
+ mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
+
+ let tabOpenEvent = yield waitForNewTabEvent(testWindow.gBrowser);
+ let newTab = tabOpenEvent.target;
+
+ yield promiseTabLoadEvent(newTab);
+
+ // Our framescript opens to about:home which means that the
+ // tab should eventually become remote.
+ ok(newTab.linkedBrowser.isRemoteBrowser,
+ "The opened browser never became remote.");
+
+ testWindow.gBrowser.removeTab(newTab);
+ }
+
+ normalWindow.close();
+ privateWindow.close();
+});
+
+/**
+ * Test that if we open a new window from a link in a non-remote
+ * browser in an e10s window, that the new window is not an e10s
+ * window. Also tests with a private browsing window.
+ */
+add_task(function* test_new_window() {
+ let normalWindow = yield BrowserTestUtils.openNewBrowserWindow({
+ remote: true
+ }, true);
+ let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({
+ remote: true,
+ private: true,
+ }, true);
+
+ // Fiddle with the prefs so that we open target="_blank" links
+ // in new windows instead of new tabs.
+ Services.prefs.setIntPref(OPEN_LOCATION_PREF,
+ Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW);
+
+ for (let testWindow of [normalWindow, privateWindow]) {
+ yield promiseWaitForFocus(testWindow);
+ let testBrowser = testWindow.gBrowser.selectedBrowser;
+ yield prepareNonRemoteBrowser(testWindow, testBrowser);
+
+ // Get our framescript ready
+ let mm = testBrowser.messageManager;
+ mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
+
+ // Click on the link in the browser, and wait for the new window.
+ let {subject: newWindow} =
+ yield promiseTopicObserved("browser-delayed-startup-finished");
+
+ is(PrivateBrowsingUtils.isWindowPrivate(testWindow),
+ PrivateBrowsingUtils.isWindowPrivate(newWindow),
+ "Private browsing state of new window does not match the original!");
+
+ let newTab = newWindow.gBrowser.selectedTab;
+
+ yield promiseTabLoadEvent(newTab);
+
+ // Our framescript opens to about:home which means that the
+ // tab should eventually become remote.
+ ok(newTab.linkedBrowser.isRemoteBrowser,
+ "The opened browser never became remote.");
+ newWindow.close();
+ }
+
+ normalWindow.close();
+ privateWindow.close();
+});
diff --git a/browser/base/content/test/general/browser_trackingUI_1.js b/browser/base/content/test/general/browser_trackingUI_1.js
new file mode 100644
index 000000000..937d607af
--- /dev/null
+++ b/browser/base/content/test/general/browser_trackingUI_1.js
@@ -0,0 +1,170 @@
+/*
+ * Test that the Tracking Protection section is visible in the Control Center
+ * and has the correct state for the cases when:
+ * 1) A page with no tracking elements is loaded.
+ * 2) A page with tracking elements is loaded and they are blocked.
+ * 3) A page with tracking elements is loaded and they are not blocked.
+ * See also Bugs 1175327, 1043801, 1178985
+ */
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+const PREF = "privacy.trackingprotection.enabled";
+const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html";
+const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
+var TrackingProtection = null;
+var tabbrowser = null;
+
+var {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+registerCleanupFunction(function() {
+ TrackingProtection = tabbrowser = null;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ Services.prefs.clearUserPref(PREF);
+ Services.prefs.clearUserPref(PB_PREF);
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function hidden(sel) {
+ let win = tabbrowser.ownerGlobal;
+ let el = win.document.querySelector(sel);
+ let display = win.getComputedStyle(el).getPropertyValue("display", null);
+ let opacity = win.getComputedStyle(el).getPropertyValue("opacity", null);
+ return display === "none" || opacity === "0";
+}
+
+function clickButton(sel) {
+ let win = tabbrowser.ownerGlobal;
+ let el = win.document.querySelector(sel);
+ el.doCommand();
+}
+
+function testBenignPage() {
+ info("Non-tracking content must not be blocked");
+ ok(!TrackingProtection.container.hidden, "The container is visible");
+ ok(!TrackingProtection.content.hasAttribute("state"), "content: no state");
+ ok(!TrackingProtection.icon.hasAttribute("state"), "icon: no state");
+ ok(!TrackingProtection.icon.hasAttribute("tooltiptext"), "icon: no tooltip");
+
+ ok(hidden("#tracking-protection-icon"), "icon is hidden");
+ ok(hidden("#tracking-action-block"), "blockButton is hidden");
+ ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
+
+ // Make sure that the no tracking elements message appears
+ ok(!hidden("#tracking-not-detected"), "labelNoTracking is visible");
+ ok(hidden("#tracking-loaded"), "labelTrackingLoaded is hidden");
+ ok(hidden("#tracking-blocked"), "labelTrackingBlocked is hidden");
+}
+
+function testTrackingPage(window) {
+ info("Tracking content must be blocked");
+ ok(!TrackingProtection.container.hidden, "The container is visible");
+ is(TrackingProtection.content.getAttribute("state"), "blocked-tracking-content",
+ 'content: state="blocked-tracking-content"');
+ is(TrackingProtection.icon.getAttribute("state"), "blocked-tracking-content",
+ 'icon: state="blocked-tracking-content"');
+ is(TrackingProtection.icon.getAttribute("tooltiptext"),
+ gNavigatorBundle.getString("trackingProtection.icon.activeTooltip"), "correct tooltip");
+
+ ok(!hidden("#tracking-protection-icon"), "icon is visible");
+ ok(hidden("#tracking-action-block"), "blockButton is hidden");
+
+
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
+ ok(!hidden("#tracking-action-unblock-private"), "unblockButtonPrivate is visible");
+ } else {
+ ok(!hidden("#tracking-action-unblock"), "unblockButton is visible");
+ ok(hidden("#tracking-action-unblock-private"), "unblockButtonPrivate is hidden");
+ }
+
+ // Make sure that the blocked tracking elements message appears
+ ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
+ ok(hidden("#tracking-loaded"), "labelTrackingLoaded is hidden");
+ ok(!hidden("#tracking-blocked"), "labelTrackingBlocked is visible");
+}
+
+function testTrackingPageUnblocked() {
+ info("Tracking content must be white-listed and not blocked");
+ ok(!TrackingProtection.container.hidden, "The container is visible");
+ is(TrackingProtection.content.getAttribute("state"), "loaded-tracking-content",
+ 'content: state="loaded-tracking-content"');
+ is(TrackingProtection.icon.getAttribute("state"), "loaded-tracking-content",
+ 'icon: state="loaded-tracking-content"');
+ is(TrackingProtection.icon.getAttribute("tooltiptext"),
+ gNavigatorBundle.getString("trackingProtection.icon.disabledTooltip"), "correct tooltip");
+
+ ok(!hidden("#tracking-protection-icon"), "icon is visible");
+ ok(!hidden("#tracking-action-block"), "blockButton is visible");
+ ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
+
+ // Make sure that the blocked tracking elements message appears
+ ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
+ ok(!hidden("#tracking-loaded"), "labelTrackingLoaded is visible");
+ ok(hidden("#tracking-blocked"), "labelTrackingBlocked is hidden");
+}
+
+function* testTrackingProtectionForTab(tab) {
+ info("Load a test page not containing tracking elements");
+ yield promiseTabLoadEvent(tab, BENIGN_PAGE);
+ testBenignPage();
+
+ info("Load a test page containing tracking elements");
+ yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+ testTrackingPage(tab.ownerGlobal);
+
+ info("Disable TP for the page (which reloads the page)");
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ clickButton("#tracking-action-unblock");
+ yield tabReloadPromise;
+ testTrackingPageUnblocked();
+
+ info("Re-enable TP for the page (which reloads the page)");
+ tabReloadPromise = promiseTabLoadEvent(tab);
+ clickButton("#tracking-action-block");
+ yield tabReloadPromise;
+ testTrackingPage(tab.ownerGlobal);
+}
+
+add_task(function* testNormalBrowsing() {
+ yield UrlClassifierTestUtils.addTestTrackers();
+
+ tabbrowser = gBrowser;
+ let tab = tabbrowser.selectedTab = tabbrowser.addTab();
+
+ TrackingProtection = gBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+ is(TrackingProtection.enabled, Services.prefs.getBoolPref(PREF),
+ "TP.enabled is based on the original pref value");
+
+ Services.prefs.setBoolPref(PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ yield testTrackingProtectionForTab(tab);
+
+ Services.prefs.setBoolPref(PREF, false);
+ ok(!TrackingProtection.enabled, "TP is disabled after setting the pref");
+});
+
+add_task(function* testPrivateBrowsing() {
+ let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+ tabbrowser = privateWin.gBrowser;
+ let tab = tabbrowser.selectedTab = tabbrowser.addTab();
+
+ TrackingProtection = tabbrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the private window");
+ is(TrackingProtection.enabled, Services.prefs.getBoolPref(PB_PREF),
+ "TP.enabled is based on the pb pref value");
+
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ yield testTrackingProtectionForTab(tab);
+
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(!TrackingProtection.enabled, "TP is disabled after setting the pref");
+
+ privateWin.close();
+});
diff --git a/browser/base/content/test/general/browser_trackingUI_2.js b/browser/base/content/test/general/browser_trackingUI_2.js
new file mode 100644
index 000000000..96ccb6c2e
--- /dev/null
+++ b/browser/base/content/test/general/browser_trackingUI_2.js
@@ -0,0 +1,96 @@
+/*
+ * Test that the Tracking Protection section is never visible in the
+ * Control Center when the feature is off.
+ * See also Bugs 1175327, 1043801, 1178985.
+ */
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+const PREF = "privacy.trackingprotection.enabled";
+const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html";
+const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
+var TrackingProtection = null;
+var tabbrowser = null;
+
+var {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+registerCleanupFunction(function() {
+ TrackingProtection = tabbrowser = null;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ Services.prefs.clearUserPref(PREF);
+ Services.prefs.clearUserPref(PB_PREF);
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function hidden(el) {
+ let win = el.ownerGlobal;
+ let display = win.getComputedStyle(el).getPropertyValue("display", null);
+ let opacity = win.getComputedStyle(el).getPropertyValue("opacity", null);
+
+ return display === "none" || opacity === "0";
+}
+
+add_task(function* testNormalBrowsing() {
+ yield UrlClassifierTestUtils.addTestTrackers();
+
+ tabbrowser = gBrowser;
+ let {gIdentityHandler} = tabbrowser.ownerGlobal;
+ let tab = tabbrowser.selectedTab = tabbrowser.addTab();
+
+ TrackingProtection = tabbrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+ is(TrackingProtection.enabled, Services.prefs.getBoolPref(PREF),
+ "TP.enabled is based on the original pref value");
+
+ Services.prefs.setBoolPref(PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ Services.prefs.setBoolPref(PREF, false);
+ ok(!TrackingProtection.enabled, "TP is disabled after setting the pref");
+
+ info("Load a test page containing tracking elements");
+ yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+ gIdentityHandler._identityBox.click();
+ ok(hidden(TrackingProtection.container), "The container is hidden");
+ gIdentityHandler._identityPopup.hidden = true;
+
+ info("Load a test page not containing tracking elements");
+ yield promiseTabLoadEvent(tab, BENIGN_PAGE);
+ gIdentityHandler._identityBox.click();
+ ok(hidden(TrackingProtection.container), "The container is hidden");
+ gIdentityHandler._identityPopup.hidden = true;
+});
+
+add_task(function* testPrivateBrowsing() {
+ let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+ tabbrowser = privateWin.gBrowser;
+ let {gIdentityHandler} = tabbrowser.ownerGlobal;
+ let tab = tabbrowser.selectedTab = tabbrowser.addTab();
+
+ TrackingProtection = tabbrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the private window");
+ is(TrackingProtection.enabled, Services.prefs.getBoolPref(PB_PREF),
+ "TP.enabled is based on the pb pref value");
+
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(!TrackingProtection.enabled, "TP is disabled after setting the pref");
+
+ info("Load a test page containing tracking elements");
+ yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+ gIdentityHandler._identityBox.click();
+ ok(hidden(TrackingProtection.container), "The container is hidden");
+ gIdentityHandler._identityPopup.hidden = true;
+
+ info("Load a test page not containing tracking elements");
+ gIdentityHandler._identityBox.click();
+ yield promiseTabLoadEvent(tab, BENIGN_PAGE);
+ ok(hidden(TrackingProtection.container), "The container is hidden");
+ gIdentityHandler._identityPopup.hidden = true;
+
+ privateWin.close();
+});
diff --git a/browser/base/content/test/general/browser_trackingUI_3.js b/browser/base/content/test/general/browser_trackingUI_3.js
new file mode 100644
index 000000000..63f8a13bc
--- /dev/null
+++ b/browser/base/content/test/general/browser_trackingUI_3.js
@@ -0,0 +1,52 @@
+/*
+ * Test that the Tracking Protection is correctly enabled / disabled
+ * in both normal and private windows given all possible states of the prefs:
+ * privacy.trackingprotection.enabled
+ * privacy.trackingprotection.pbmode.enabled
+ * See also Bug 1178985.
+ */
+
+const PREF = "privacy.trackingprotection.enabled";
+const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(PREF);
+ Services.prefs.clearUserPref(PB_PREF);
+});
+
+add_task(function* testNormalBrowsing() {
+ let TrackingProtection = gBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+
+ Services.prefs.setBoolPref(PREF, true);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=true,PB=false)");
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=true,PB=true)");
+
+ Services.prefs.setBoolPref(PREF, false);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(!TrackingProtection.enabled, "TP is disabled (ENABLED=false,PB=false)");
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(!TrackingProtection.enabled, "TP is disabled (ENABLED=false,PB=true)");
+});
+
+add_task(function* testPrivateBrowsing() {
+ let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+ let TrackingProtection = privateWin.gBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+
+ Services.prefs.setBoolPref(PREF, true);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=true,PB=false)");
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=true,PB=true)");
+
+ Services.prefs.setBoolPref(PREF, false);
+ Services.prefs.setBoolPref(PB_PREF, false);
+ ok(!TrackingProtection.enabled, "TP is disabled (ENABLED=false,PB=false)");
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled (ENABLED=false,PB=true)");
+
+ privateWin.close();
+});
diff --git a/browser/base/content/test/general/browser_trackingUI_4.js b/browser/base/content/test/general/browser_trackingUI_4.js
new file mode 100644
index 000000000..93a06913e
--- /dev/null
+++ b/browser/base/content/test/general/browser_trackingUI_4.js
@@ -0,0 +1,109 @@
+/*
+ * Test that the Tracking Protection icon is properly animated in the identity
+ * block when loading tabs and switching between tabs.
+ * See also Bug 1175858.
+ */
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+const PREF = "privacy.trackingprotection.enabled";
+const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html";
+const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
+var TrackingProtection = null;
+var tabbrowser = null;
+
+var {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+registerCleanupFunction(function() {
+ TrackingProtection = tabbrowser = null;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ Services.prefs.clearUserPref(PREF);
+ Services.prefs.clearUserPref(PB_PREF);
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function waitForSecurityChange(numChanges = 1) {
+ return new Promise(resolve => {
+ let n = 0;
+ let listener = {
+ onSecurityChange: function() {
+ n = n + 1;
+ info ("Received onSecurityChange event " + n + " of " + numChanges);
+ if (n >= numChanges) {
+ tabbrowser.removeProgressListener(listener);
+ resolve();
+ }
+ }
+ };
+ tabbrowser.addProgressListener(listener);
+ });
+}
+
+function* testTrackingProtectionAnimation() {
+ info("Load a test page not containing tracking elements");
+ let benignTab = yield BrowserTestUtils.openNewForegroundTab(tabbrowser, BENIGN_PAGE);
+
+ ok(!TrackingProtection.icon.hasAttribute("state"), "icon: no state");
+ ok(TrackingProtection.icon.hasAttribute("animate"), "icon: animate");
+
+ info("Load a test page containing tracking elements");
+ let trackingTab = yield BrowserTestUtils.openNewForegroundTab(tabbrowser, TRACKING_PAGE);
+
+ ok(TrackingProtection.icon.hasAttribute("state"), "icon: state");
+ ok(TrackingProtection.icon.hasAttribute("animate"), "icon: animate");
+
+ info("Switch from tracking -> benign tab");
+ let securityChanged = waitForSecurityChange();
+ tabbrowser.selectedTab = benignTab;
+ yield securityChanged;
+
+ ok(!TrackingProtection.icon.hasAttribute("state"), "icon: no state");
+ ok(!TrackingProtection.icon.hasAttribute("animate"), "icon: no animate");
+
+ info("Switch from benign -> tracking tab");
+ securityChanged = waitForSecurityChange();
+ tabbrowser.selectedTab = trackingTab;
+ yield securityChanged;
+
+ ok(TrackingProtection.icon.hasAttribute("state"), "icon: state");
+ ok(!TrackingProtection.icon.hasAttribute("animate"), "icon: no animate");
+
+ info("Reload tracking tab");
+ securityChanged = waitForSecurityChange(2);
+ tabbrowser.reload();
+ yield securityChanged;
+
+ ok(TrackingProtection.icon.hasAttribute("state"), "icon: state");
+ ok(TrackingProtection.icon.hasAttribute("animate"), "icon: animate");
+}
+
+add_task(function* testNormalBrowsing() {
+ yield UrlClassifierTestUtils.addTestTrackers();
+
+ tabbrowser = gBrowser;
+
+ TrackingProtection = gBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+
+ Services.prefs.setBoolPref(PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ yield testTrackingProtectionAnimation();
+});
+
+add_task(function* testPrivateBrowsing() {
+ let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+ tabbrowser = privateWin.gBrowser;
+
+ TrackingProtection = tabbrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the private window");
+
+ Services.prefs.setBoolPref(PB_PREF, true);
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ yield testTrackingProtectionAnimation();
+
+ privateWin.close();
+});
diff --git a/browser/base/content/test/general/browser_trackingUI_5.js b/browser/base/content/test/general/browser_trackingUI_5.js
new file mode 100644
index 000000000..23164a5b2
--- /dev/null
+++ b/browser/base/content/test/general/browser_trackingUI_5.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that sites added to the Tracking Protection whitelist in private
+// browsing mode don't persist once the private browsing window closes.
+
+const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
+const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
+var TrackingProtection = null;
+var browser = null;
+var {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+registerCleanupFunction(function() {
+ TrackingProtection = browser = null;
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+function hidden(sel) {
+ let win = browser.ownerGlobal;
+ let el = win.document.querySelector(sel);
+ let display = win.getComputedStyle(el).getPropertyValue("display", null);
+ return display === "none";
+}
+
+function identityPopupState() {
+ let win = browser.ownerGlobal;
+ return win.document.getElementById("identity-popup").state;
+}
+
+function clickButton(sel) {
+ let win = browser.ownerGlobal;
+ let el = win.document.querySelector(sel);
+ el.doCommand();
+}
+
+function testTrackingPage(window) {
+ info("Tracking content must be blocked");
+ ok(!TrackingProtection.container.hidden, "The container is visible");
+ is(TrackingProtection.content.getAttribute("state"), "blocked-tracking-content",
+ 'content: state="blocked-tracking-content"');
+ is(TrackingProtection.icon.getAttribute("state"), "blocked-tracking-content",
+ 'icon: state="blocked-tracking-content"');
+
+ ok(!hidden("#tracking-protection-icon"), "icon is visible");
+ ok(hidden("#tracking-action-block"), "blockButton is hidden");
+
+ ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
+ ok(!hidden("#tracking-action-unblock-private"), "unblockButtonPrivate is visible");
+
+ // Make sure that the blocked tracking elements message appears
+ ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
+ ok(hidden("#tracking-loaded"), "labelTrackingLoaded is hidden");
+ ok(!hidden("#tracking-blocked"), "labelTrackingBlocked is visible");
+}
+
+function testTrackingPageUnblocked() {
+ info("Tracking content must be white-listed and not blocked");
+ ok(!TrackingProtection.container.hidden, "The container is visible");
+ is(TrackingProtection.content.getAttribute("state"), "loaded-tracking-content",
+ 'content: state="loaded-tracking-content"');
+ is(TrackingProtection.icon.getAttribute("state"), "loaded-tracking-content",
+ 'icon: state="loaded-tracking-content"');
+
+ ok(!hidden("#tracking-protection-icon"), "icon is visible");
+ ok(!hidden("#tracking-action-block"), "blockButton is visible");
+ ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
+
+ // Make sure that the blocked tracking elements message appears
+ ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
+ ok(!hidden("#tracking-loaded"), "labelTrackingLoaded is visible");
+ ok(hidden("#tracking-blocked"), "labelTrackingBlocked is hidden");
+}
+
+add_task(function* testExceptionAddition() {
+ yield UrlClassifierTestUtils.addTestTrackers();
+ let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+ browser = privateWin.gBrowser;
+ let tab = browser.selectedTab = browser.addTab();
+
+ TrackingProtection = browser.ownerGlobal.TrackingProtection;
+ yield pushPrefs([PB_PREF, true]);
+
+ ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
+
+ info("Load a test page containing tracking elements");
+ yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+
+ testTrackingPage(tab.ownerGlobal);
+
+ info("Disable TP for the page (which reloads the page)");
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ clickButton("#tracking-action-unblock");
+ is(identityPopupState(), "closed", "foobar");
+
+ yield tabReloadPromise;
+ testTrackingPageUnblocked();
+
+ info("Test that the exception is remembered across tabs in the same private window");
+ tab = browser.selectedTab = browser.addTab();
+
+ info("Load a test page containing tracking elements");
+ yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+ testTrackingPageUnblocked();
+
+ yield promiseWindowClosed(privateWin);
+});
+
+add_task(function* testExceptionPersistence() {
+ info("Open another private browsing window");
+ let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+ browser = privateWin.gBrowser;
+ let tab = browser.selectedTab = browser.addTab();
+
+ TrackingProtection = browser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection.enabled, "TP is still enabled");
+
+ info("Load a test page containing tracking elements");
+ yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+
+ testTrackingPage(tab.ownerGlobal);
+
+ info("Disable TP for the page (which reloads the page)");
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ clickButton("#tracking-action-unblock");
+ is(identityPopupState(), "closed", "foobar");
+
+ yield tabReloadPromise;
+ testTrackingPageUnblocked();
+
+ privateWin.close();
+});
diff --git a/browser/base/content/test/general/browser_trackingUI_6.js b/browser/base/content/test/general/browser_trackingUI_6.js
new file mode 100644
index 000000000..be91bc4a0
--- /dev/null
+++ b/browser/base/content/test/general/browser_trackingUI_6.js
@@ -0,0 +1,46 @@
+const URL = "http://mochi.test:8888/browser/browser/base/content/test/general/file_trackingUI_6.html";
+
+function waitForSecurityChange(numChanges = 1) {
+ return new Promise(resolve => {
+ let n = 0;
+ let listener = {
+ onSecurityChange: function() {
+ n = n + 1;
+ info ("Received onSecurityChange event " + n + " of " + numChanges);
+ if (n >= numChanges) {
+ gBrowser.removeProgressListener(listener);
+ resolve();
+ }
+ }
+ };
+ gBrowser.addProgressListener(listener);
+ });
+}
+
+add_task(function* test_fetch() {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: [['privacy.trackingprotection.enabled', true]] },
+ resolve);
+ });
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: URL }, function* (newTabBrowser) {
+ let securityChange = waitForSecurityChange();
+ yield ContentTask.spawn(newTabBrowser, null, function* () {
+ yield content.wrappedJSObject.test_fetch()
+ .then(response => Assert.ok(false, "should have denied the request"))
+ .catch(e => Assert.ok(true, `Caught exception: ${e}`));
+ });
+ yield securityChange;
+
+ var TrackingProtection = newTabBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "got TP object");
+ ok(TrackingProtection.enabled, "TP is enabled");
+
+ is(TrackingProtection.content.getAttribute("state"), "blocked-tracking-content",
+ 'content: state="blocked-tracking-content"');
+ is(TrackingProtection.icon.getAttribute("state"), "blocked-tracking-content",
+ 'icon: state="blocked-tracking-content"');
+ is(TrackingProtection.icon.getAttribute("tooltiptext"),
+ gNavigatorBundle.getString("trackingProtection.icon.activeTooltip"), "correct tooltip");
+ });
+});
diff --git a/browser/base/content/test/general/browser_trackingUI_telemetry.js b/browser/base/content/test/general/browser_trackingUI_telemetry.js
new file mode 100644
index 000000000..d9fce18d4
--- /dev/null
+++ b/browser/base/content/test/general/browser_trackingUI_telemetry.js
@@ -0,0 +1,145 @@
+/*
+ * Test telemetry for Tracking Protection
+ */
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+const PREF = "privacy.trackingprotection.enabled";
+const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html";
+const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
+const {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+/**
+ * Enable local telemetry recording for the duration of the tests.
+ */
+var oldCanRecord = Services.telemetry.canRecordExtended;
+Services.telemetry.canRecordExtended = true;
+Services.prefs.setBoolPref(PREF, false);
+Services.telemetry.getHistogramById("TRACKING_PROTECTION_ENABLED").clear();
+registerCleanupFunction(function () {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.prefs.clearUserPref(PREF);
+});
+
+function getShieldHistogram() {
+ return Services.telemetry.getHistogramById("TRACKING_PROTECTION_SHIELD");
+}
+
+function getEnabledHistogram() {
+ return Services.telemetry.getHistogramById("TRACKING_PROTECTION_ENABLED");
+}
+
+function getEventsHistogram() {
+ return Services.telemetry.getHistogramById("TRACKING_PROTECTION_EVENTS");
+}
+
+function getShieldCounts() {
+ return getShieldHistogram().snapshot().counts;
+}
+
+function getEnabledCounts() {
+ return getEnabledHistogram().snapshot().counts;
+}
+
+function getEventCounts() {
+ return getEventsHistogram().snapshot().counts;
+}
+
+add_task(function* setup() {
+ yield UrlClassifierTestUtils.addTestTrackers();
+
+ let TrackingProtection = gBrowser.ownerGlobal.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+ ok(!TrackingProtection.enabled, "TP is not enabled");
+
+ // Open a window with TP disabled to make sure 'enabled' is logged correctly.
+ let newWin = yield promiseOpenAndLoadWindow({}, true);
+ yield promiseWindowClosed(newWin);
+
+ is(getEnabledCounts()[0], 1, "TP was disabled once on start up");
+ is(getEnabledCounts()[1], 0, "TP was not enabled on start up");
+
+ // Enable TP so the next browser to open will log 'enabled'
+ Services.prefs.setBoolPref(PREF, true);
+});
+
+
+add_task(function* testNewWindow() {
+ let newWin = yield promiseOpenAndLoadWindow({}, true);
+ let tab = newWin.gBrowser.selectedTab = newWin.gBrowser.addTab();
+ let TrackingProtection = newWin.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+
+ is(getEnabledCounts()[0], 1, "TP was disabled once on start up");
+ is(getEnabledCounts()[1], 1, "TP was enabled once on start up");
+
+ // Reset these to make counting easier
+ getEventsHistogram().clear();
+ getShieldHistogram().clear();
+
+ yield promiseTabLoadEvent(tab, BENIGN_PAGE);
+ is(getEventCounts()[0], 1, "Total page loads");
+ is(getEventCounts()[1], 0, "Disable actions");
+ is(getEventCounts()[2], 0, "Enable actions");
+ is(getShieldCounts()[0], 1, "Page loads without tracking");
+
+ yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+ // Note that right now the events and shield histogram is not measuring what
+ // you might think. Since onSecurityChange fires twice for a tracking page,
+ // the total page loads count is double counting, and the shield count
+ // (which is meant to measure times when the shield wasn't shown) fires even
+ // when tracking elements exist on the page.
+ todo_is(getEventCounts()[0], 2, "FIXME: TOTAL PAGE LOADS IS DOUBLE COUNTING");
+ is(getEventCounts()[1], 0, "Disable actions");
+ is(getEventCounts()[2], 0, "Enable actions");
+ todo_is(getShieldCounts()[0], 1, "FIXME: TOTAL PAGE LOADS WITHOUT TRACKING IS DOUBLE COUNTING");
+
+ info("Disable TP for the page (which reloads the page)");
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ newWin.document.querySelector("#tracking-action-unblock").doCommand();
+ yield tabReloadPromise;
+ todo_is(getEventCounts()[0], 3, "FIXME: TOTAL PAGE LOADS IS DOUBLE COUNTING");
+ is(getEventCounts()[1], 1, "Disable actions");
+ is(getEventCounts()[2], 0, "Enable actions");
+ todo_is(getShieldCounts()[0], 1, "FIXME: TOTAL PAGE LOADS WITHOUT TRACKING IS DOUBLE COUNTING");
+
+ info("Re-enable TP for the page (which reloads the page)");
+ tabReloadPromise = promiseTabLoadEvent(tab);
+ newWin.document.querySelector("#tracking-action-block").doCommand();
+ yield tabReloadPromise;
+ todo_is(getEventCounts()[0], 4, "FIXME: TOTAL PAGE LOADS IS DOUBLE COUNTING");
+ is(getEventCounts()[1], 1, "Disable actions");
+ is(getEventCounts()[2], 1, "Enable actions");
+ todo_is(getShieldCounts()[0], 1, "FIXME: TOTAL PAGE LOADS WITHOUT TRACKING IS DOUBLE COUNTING");
+
+ yield promiseWindowClosed(newWin);
+
+ // Reset these to make counting easier for the next test
+ getEventsHistogram().clear();
+ getShieldHistogram().clear();
+ getEnabledHistogram().clear();
+});
+
+add_task(function* testPrivateBrowsing() {
+ let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
+ let tab = privateWin.gBrowser.selectedTab = privateWin.gBrowser.addTab();
+ let TrackingProtection = privateWin.TrackingProtection;
+ ok(TrackingProtection, "TP is attached to the browser window");
+
+ // Do a bunch of actions and make sure that no telemetry data is gathered
+ yield promiseTabLoadEvent(tab, BENIGN_PAGE);
+ yield promiseTabLoadEvent(tab, TRACKING_PAGE);
+ let tabReloadPromise = promiseTabLoadEvent(tab);
+ privateWin.document.querySelector("#tracking-action-unblock").doCommand();
+ yield tabReloadPromise;
+ tabReloadPromise = promiseTabLoadEvent(tab);
+ privateWin.document.querySelector("#tracking-action-block").doCommand();
+ yield tabReloadPromise;
+
+ // Sum up all the counts to make sure that nothing got logged
+ is(getEnabledCounts().reduce((p, c) => p+c), 0, "Telemetry logging off in PB mode");
+ is(getEventCounts().reduce((p, c) => p+c), 0, "Telemetry logging off in PB mode");
+ is(getShieldCounts().reduce((p, c) => p+c), 0, "Telemetry logging off in PB mode");
+
+ yield promiseWindowClosed(privateWin);
+});
diff --git a/browser/base/content/test/general/browser_typeAheadFind.js b/browser/base/content/test/general/browser_typeAheadFind.js
new file mode 100644
index 000000000..1d550944a
--- /dev/null
+++ b/browser/base/content/test/general/browser_typeAheadFind.js
@@ -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/. */
+
+add_task(function *() {
+ let testWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ testWindow.gBrowser.loadURI("data:text/html,<h1>A Page</h1>");
+ yield BrowserTestUtils.browserLoaded(testWindow.gBrowser.selectedBrowser);
+
+ yield SimpleTest.promiseFocus(testWindow.gBrowser.selectedBrowser);
+
+ ok(!testWindow.gFindBarInitialized, "find bar is not initialized");
+
+ let findBarOpenPromise = promiseWaitForEvent(testWindow.gBrowser, "findbaropen");
+ EventUtils.synthesizeKey("/", {}, testWindow);
+ yield findBarOpenPromise;
+
+ ok(testWindow.gFindBarInitialized, "find bar is now initialized");
+
+ yield BrowserTestUtils.closeWindow(testWindow);
+});
diff --git a/browser/base/content/test/general/browser_unknownContentType_title.js b/browser/base/content/test/general/browser_unknownContentType_title.js
new file mode 100644
index 000000000..269406bdb
--- /dev/null
+++ b/browser/base/content/test/general/browser_unknownContentType_title.js
@@ -0,0 +1,33 @@
+const url = "data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3Ctitle%3ETest%20Page%3C%2Ftitle%3E%3C%2Fhead%3E%3C%2Fhtml%3E";
+const unknown_url = "http://example.com/browser/browser/base/content/test/general/unknownContentType_file.pif";
+
+function waitForNewWindow() {
+ return new Promise(resolve => {
+ let listener = (win) => {
+ Services.obs.removeObserver(listener, "toplevel-window-ready");
+ win.addEventListener("load", () => {
+ resolve(win);
+ });
+ };
+
+ Services.obs.addObserver(listener, "toplevel-window-ready", false)
+ });
+}
+
+add_task(function*() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ yield promiseTabLoaded(gBrowser.selectedTab);
+
+ is(gBrowser.contentTitle, "Test Page", "Should have the right title.")
+
+ browser.loadURI(unknown_url);
+ let win = yield waitForNewWindow();
+ is(win.location, "chrome://mozapps/content/downloads/unknownContentType.xul",
+ "Should have seen the unknown content dialog.");
+ is(gBrowser.contentTitle, "Test Page", "Should still have the right title.")
+
+ win.close();
+ yield promiseWaitForFocus(window);
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/general/browser_unloaddialogs.js b/browser/base/content/test/general/browser_unloaddialogs.js
new file mode 100644
index 000000000..bf3790b95
--- /dev/null
+++ b/browser/base/content/test/general/browser_unloaddialogs.js
@@ -0,0 +1,41 @@
+var testUrls =
+ [
+ "data:text/html,<script>" +
+ "function handle(evt) {" +
+ "evt.target.removeEventListener(evt.type, handle, true);" +
+ "try { alert('This should NOT appear'); } catch(e) { }" +
+ "}" +
+ "window.addEventListener('pagehide', handle, true);" +
+ "window.addEventListener('beforeunload', handle, true);" +
+ "window.addEventListener('unload', handle, true);" +
+ "</script><body>Testing alert during pagehide/beforeunload/unload</body>",
+ "data:text/html,<script>" +
+ "function handle(evt) {" +
+ "evt.target.removeEventListener(evt.type, handle, true);" +
+ "try { prompt('This should NOT appear'); } catch(e) { }" +
+ "}" +
+ "window.addEventListener('pagehide', handle, true);" +
+ "window.addEventListener('beforeunload', handle, true);" +
+ "window.addEventListener('unload', handle, true);" +
+ "</script><body>Testing prompt during pagehide/beforeunload/unload</body>",
+ "data:text/html,<script>" +
+ "function handle(evt) {" +
+ "evt.target.removeEventListener(evt.type, handle, true);" +
+ "try { confirm('This should NOT appear'); } catch(e) { }" +
+ "}" +
+ "window.addEventListener('pagehide', handle, true);" +
+ "window.addEventListener('beforeunload', handle, true);" +
+ "window.addEventListener('unload', handle, true);" +
+ "</script><body>Testing confirm during pagehide/beforeunload/unload</body>",
+ ];
+
+add_task(function*() {
+ for (let url of testUrls) {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ ok(true, "Loaded page " + url);
+ // Wait one turn of the event loop before closing, so everything settles.
+ yield new Promise(resolve => setTimeout(resolve, 0));
+ yield BrowserTestUtils.removeTab(tab);
+ ok(true, "Closed page " + url + " without timeout");
+ }
+});
diff --git a/browser/base/content/test/general/browser_utilityOverlay.js b/browser/base/content/test/general/browser_utilityOverlay.js
new file mode 100644
index 000000000..34adc00d9
--- /dev/null
+++ b/browser/base/content/test/general/browser_utilityOverlay.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/. */
+
+const gTests = [
+ test_eventMatchesKey,
+ test_getTopWin,
+ test_getBoolPref,
+ test_openNewTabWith,
+ test_openUILink
+];
+
+function test () {
+ waitForExplicitFinish();
+ executeSoon(runNextTest);
+}
+
+function runNextTest() {
+ if (gTests.length) {
+ let testFun = gTests.shift();
+ info("Running " + testFun.name);
+ testFun()
+ }
+ else {
+ finish();
+ }
+}
+
+function test_eventMatchesKey() {
+ let eventMatchResult;
+ let key;
+ let checkEvent = function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ eventMatchResult = eventMatchesKey(e, key);
+ }
+ document.addEventListener("keypress", checkEvent);
+
+ try {
+ key = document.createElement("key");
+ let keyset = document.getElementById("mainKeyset");
+ key.setAttribute("key", "t");
+ key.setAttribute("modifiers", "accel");
+ keyset.appendChild(key);
+ EventUtils.synthesizeKey("t", {accelKey: true});
+ is(eventMatchResult, true, "eventMatchesKey: one modifier");
+ keyset.removeChild(key);
+
+ key = document.createElement("key");
+ key.setAttribute("key", "g");
+ key.setAttribute("modifiers", "accel,shift");
+ keyset.appendChild(key);
+ EventUtils.synthesizeKey("g", {accelKey: true, shiftKey: true});
+ is(eventMatchResult, true, "eventMatchesKey: combination modifiers");
+ keyset.removeChild(key);
+
+ key = document.createElement("key");
+ key.setAttribute("key", "w");
+ key.setAttribute("modifiers", "accel");
+ keyset.appendChild(key);
+ EventUtils.synthesizeKey("f", {accelKey: true});
+ is(eventMatchResult, false, "eventMatchesKey: mismatch keys");
+ keyset.removeChild(key);
+
+ key = document.createElement("key");
+ key.setAttribute("keycode", "VK_DELETE");
+ keyset.appendChild(key);
+ EventUtils.synthesizeKey("VK_DELETE", {accelKey: true});
+ is(eventMatchResult, false, "eventMatchesKey: mismatch modifiers");
+ keyset.removeChild(key);
+ } finally {
+ // Make sure to remove the event listener so future tests don't
+ // fail when they simulate key presses.
+ document.removeEventListener("keypress", checkEvent);
+ }
+
+ runNextTest();
+}
+
+function test_getTopWin() {
+ is(getTopWin(), window, "got top window");
+ runNextTest();
+}
+
+
+function test_getBoolPref() {
+ is(getBoolPref("browser.search.openintab", false), false, "getBoolPref");
+ is(getBoolPref("this.pref.doesnt.exist", true), true, "getBoolPref fallback");
+ is(getBoolPref("this.pref.doesnt.exist", false), false, "getBoolPref fallback #2");
+ runNextTest();
+}
+
+function test_openNewTabWith() {
+ openNewTabWith("http://example.com/");
+ let tab = gBrowser.selectedTab = gBrowser.tabs[1];
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+ is(tab.linkedBrowser.currentURI.spec, "http://example.com/", "example.com loaded");
+ gBrowser.removeCurrentTab();
+ runNextTest();
+ });
+}
+
+function test_openUILink() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
+ is(tab.linkedBrowser.currentURI.spec, "http://example.org/", "example.org loaded");
+ gBrowser.removeCurrentTab();
+ runNextTest();
+ });
+
+ openUILink("http://example.org/"); // defaults to "current"
+}
diff --git a/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js b/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js
new file mode 100644
index 000000000..c8f3cdc96
--- /dev/null
+++ b/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js
@@ -0,0 +1,55 @@
+function wait_while_tab_is_busy() {
+ return new Promise(resolve => {
+ let progressListener = {
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ gBrowser.removeProgressListener(this);
+ setTimeout(resolve, 0);
+ }
+ }
+ };
+ gBrowser.addProgressListener(progressListener);
+ });
+}
+
+// This function waits for the tab to stop being busy instead of waiting for it
+// to load, since the canViewSource change happens at that time.
+var with_new_tab_opened = Task.async(function* (options, taskFn) {
+ let busyPromise = wait_while_tab_is_busy();
+ let tab = yield BrowserTestUtils.openNewForegroundTab(options.gBrowser, options.url, false);
+ yield busyPromise;
+ yield taskFn(tab.linkedBrowser);
+ gBrowser.removeTab(tab);
+});
+
+add_task(function*() {
+ yield new Promise((resolve) => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["view_source.tab", true],
+ ]}, resolve);
+ });
+});
+
+add_task(function* test_regular_page() {
+ function* test_expect_view_source_enabled(browser) {
+ ok(!XULBrowserWindow.canViewSource.hasAttribute("disabled"),
+ "View Source should be enabled");
+ }
+
+ yield with_new_tab_opened({
+ gBrowser,
+ url: "http://example.com",
+ }, test_expect_view_source_enabled);
+});
+
+add_task(function* test_view_source_page() {
+ function* test_expect_view_source_disabled(browser) {
+ ok(XULBrowserWindow.canViewSource.hasAttribute("disabled"),
+ "View Source should be disabled");
+ }
+
+ yield with_new_tab_opened({
+ gBrowser,
+ url: "view-source:http://example.com",
+ }, test_expect_view_source_disabled);
+});
diff --git a/browser/base/content/test/general/browser_visibleFindSelection.js b/browser/base/content/test/general/browser_visibleFindSelection.js
new file mode 100644
index 000000000..630490644
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleFindSelection.js
@@ -0,0 +1,52 @@
+add_task(function*() {
+ const childContent = "<div style='position: absolute; left: 2200px; background: green; width: 200px; height: 200px;'>" +
+ "div</div><div style='position: absolute; left: 0px; background: red; width: 200px; height: 200px;'>" +
+ "<span id='s'>div</span></div>";
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ yield promiseTabLoadEvent(tab, "data:text/html," + escape(childContent));
+ yield SimpleTest.promiseFocus(gBrowser.selectedBrowser.contentWindowAsCPOW);
+
+ let findBarOpenPromise = promiseWaitForEvent(gBrowser, "findbaropen");
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ yield findBarOpenPromise;
+
+ ok(gFindBarInitialized, "find bar is now initialized");
+
+ // Finds the div in the green box.
+ let scrollPromise = promiseWaitForEvent(gBrowser, "scroll");
+ EventUtils.synthesizeKey("d", {});
+ EventUtils.synthesizeKey("i", {});
+ EventUtils.synthesizeKey("v", {});
+ yield scrollPromise;
+
+ // Wait for one paint to ensure we've processed the previous key events and scrolling.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ return new Promise(
+ resolve => {
+ content.requestAnimationFrame(() => {
+ setTimeout(resolve, 0);
+ });
+ }
+ );
+ });
+
+ // Finds the div in the red box.
+ scrollPromise = promiseWaitForEvent(gBrowser, "scroll");
+ EventUtils.synthesizeKey("g", { accelKey: true });
+ yield scrollPromise;
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ Assert.ok(content.document.getElementById("s").getBoundingClientRect().left >= 0,
+ "scroll should include find result");
+ });
+
+ // clear the find bar
+ EventUtils.synthesizeKey("a", { accelKey: true });
+ EventUtils.synthesizeKey("VK_DELETE", { });
+
+ gFindBar.close();
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/browser/base/content/test/general/browser_visibleTabs.js b/browser/base/content/test/general/browser_visibleTabs.js
new file mode 100644
index 000000000..e9130bc18
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs.js
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(function* () {
+ // There should be one tab when we start the test
+ let [origTab] = gBrowser.visibleTabs;
+
+ // Add a tab that will get pinned
+ let pinned = gBrowser.addTab();
+ gBrowser.pinTab(pinned);
+
+ let testTab = gBrowser.addTab();
+
+ let visible = gBrowser.visibleTabs;
+ is(visible.length, 3, "3 tabs should be open");
+ is(visible[0], pinned, "the pinned tab is first");
+ is(visible[1], origTab, "original tab is next");
+ is(visible[2], testTab, "last created tab is last");
+
+ // Only show the test tab (but also get pinned and selected)
+ is(gBrowser.selectedTab, origTab, "sanity check that we're on the original tab");
+ gBrowser.showOnlyTheseTabs([testTab]);
+ is(gBrowser.visibleTabs.length, 3, "all 3 tabs are still visible");
+
+ // Select the test tab and only show that (and pinned)
+ gBrowser.selectedTab = testTab;
+ gBrowser.showOnlyTheseTabs([testTab]);
+
+ visible = gBrowser.visibleTabs;
+ is(visible.length, 2, "2 tabs should be visible including the pinned");
+ is(visible[0], pinned, "first is pinned");
+ is(visible[1], testTab, "next is the test tab");
+ is(gBrowser.tabs.length, 3, "3 tabs should still be open");
+
+ gBrowser.selectTabAtIndex(1);
+ is(gBrowser.selectedTab, testTab, "second tab is the test tab");
+ gBrowser.selectTabAtIndex(0);
+ is(gBrowser.selectedTab, pinned, "first tab is pinned");
+ gBrowser.selectTabAtIndex(2);
+ is(gBrowser.selectedTab, testTab, "no third tab, so no change");
+ gBrowser.selectTabAtIndex(0);
+ is(gBrowser.selectedTab, pinned, "switch back to the pinned");
+ gBrowser.selectTabAtIndex(2);
+ is(gBrowser.selectedTab, testTab, "no third tab, so select last tab");
+ gBrowser.selectTabAtIndex(-2);
+ is(gBrowser.selectedTab, pinned, "pinned tab is second from left (when orig tab is hidden)");
+ gBrowser.selectTabAtIndex(-1);
+ is(gBrowser.selectedTab, testTab, "last tab is the test tab");
+
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ is(gBrowser.selectedTab, pinned, "wrapped around the end to pinned");
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ is(gBrowser.selectedTab, testTab, "next to test tab");
+ gBrowser.tabContainer.advanceSelectedTab(1, true);
+ is(gBrowser.selectedTab, pinned, "next to pinned again");
+
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, testTab, "going backwards to last tab");
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, pinned, "next to pinned");
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(gBrowser.selectedTab, testTab, "next to test tab again");
+
+ // Try showing all tabs
+ gBrowser.showOnlyTheseTabs(Array.slice(gBrowser.tabs));
+ is(gBrowser.visibleTabs.length, 3, "all 3 tabs are visible again");
+
+ // Select the pinned tab and show the testTab to make sure selection updates
+ gBrowser.selectedTab = pinned;
+ gBrowser.showOnlyTheseTabs([testTab]);
+ is(gBrowser.tabs[1], origTab, "make sure origTab is in the middle");
+ is(origTab.hidden, true, "make sure it's hidden");
+ gBrowser.removeTab(pinned);
+ is(gBrowser.selectedTab, testTab, "making sure origTab was skipped");
+ is(gBrowser.visibleTabs.length, 1, "only testTab is there");
+
+ // Only show one of the non-pinned tabs (but testTab is selected)
+ gBrowser.showOnlyTheseTabs([origTab]);
+ is(gBrowser.visibleTabs.length, 2, "got 2 tabs");
+
+ // Now really only show one of the tabs
+ gBrowser.showOnlyTheseTabs([testTab]);
+ visible = gBrowser.visibleTabs;
+ is(visible.length, 1, "only the original tab is visible");
+ is(visible[0], testTab, "it's the original tab");
+ is(gBrowser.tabs.length, 2, "still have 2 open tabs");
+
+ // Close the last visible tab and make sure we still get a visible tab
+ gBrowser.removeTab(testTab);
+ is(gBrowser.visibleTabs.length, 1, "only orig is left and visible");
+ is(gBrowser.tabs.length, 1, "sanity check that it matches");
+ is(gBrowser.selectedTab, origTab, "got the orig tab");
+ is(origTab.hidden, false, "and it's not hidden -- visible!");
+});
diff --git a/browser/base/content/test/general/browser_visibleTabs_bookmarkAllPages.js b/browser/base/content/test/general/browser_visibleTabs_bookmarkAllPages.js
new file mode 100644
index 000000000..827f86c05
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs_bookmarkAllPages.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ let tabOne = gBrowser.addTab("about:blank");
+ let tabTwo = gBrowser.addTab("http://mochi.test:8888/");
+ gBrowser.selectedTab = tabTwo;
+
+ let browser = gBrowser.getBrowserForTab(tabTwo);
+ let onLoad = function() {
+ browser.removeEventListener("load", onLoad, true);
+
+ gBrowser.showOnlyTheseTabs([tabTwo]);
+
+ is(gBrowser.visibleTabs.length, 1, "Only one tab is visible");
+
+ let uris = PlacesCommandHook.uniqueCurrentPages;
+ is(uris.length, 1, "Only one uri is returned");
+
+ is(uris[0].uri.spec, tabTwo.linkedBrowser.currentURI.spec, "It's the correct URI");
+
+ gBrowser.removeTab(tabOne);
+ gBrowser.removeTab(tabTwo);
+ Array.forEach(gBrowser.tabs, function(tab) {
+ gBrowser.showTab(tab);
+ });
+
+ finish();
+ }
+ browser.addEventListener("load", onLoad, true);
+}
diff --git a/browser/base/content/test/general/browser_visibleTabs_bookmarkAllTabs.js b/browser/base/content/test/general/browser_visibleTabs_bookmarkAllTabs.js
new file mode 100644
index 000000000..0a0ea87bd
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs_bookmarkAllTabs.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test() {
+ waitForExplicitFinish();
+
+ // There should be one tab when we start the test
+ let [origTab] = gBrowser.visibleTabs;
+ is(gBrowser.visibleTabs.length, 1, "1 tab should be open");
+ is(Disabled(), true, "Bookmark All Tabs should be disabled");
+
+ // Add a tab
+ let testTab1 = gBrowser.addTab();
+ is(gBrowser.visibleTabs.length, 2, "2 tabs should be open");
+ is(Disabled(), true, "Bookmark All Tabs should be disabled since there are two tabs with the same address");
+
+ let testTab2 = gBrowser.addTab("about:mozilla");
+ is(gBrowser.visibleTabs.length, 3, "3 tabs should be open");
+ // Wait for tab load, the code checks for currentURI.
+ testTab2.linkedBrowser.addEventListener("load", function () {
+ testTab2.linkedBrowser.removeEventListener("load", arguments.callee, true);
+ is(Disabled(), false, "Bookmark All Tabs should be enabled since there are two tabs with different addresses");
+
+ // Hide the original tab
+ gBrowser.selectedTab = testTab2;
+ gBrowser.showOnlyTheseTabs([testTab2]);
+ is(gBrowser.visibleTabs.length, 1, "1 tab should be visible");
+ is(Disabled(), true, "Bookmark All Tabs should be disabled as there is only one visible tab");
+
+ // Add a tab that will get pinned
+ let pinned = gBrowser.addTab();
+ is(gBrowser.visibleTabs.length, 2, "2 tabs should be visible now");
+ is(Disabled(), false, "Bookmark All Tabs should be available as there are two visible tabs");
+ gBrowser.pinTab(pinned);
+ is(Hidden(), false, "Bookmark All Tabs should be visible on a normal tab");
+ is(Disabled(), true, "Bookmark All Tabs should not be available since one tab is pinned");
+ gBrowser.selectedTab = pinned;
+ is(Hidden(), true, "Bookmark All Tabs should be hidden on a pinned tab");
+
+ // Show all tabs
+ let allTabs = Array.from(gBrowser.tabs);
+ gBrowser.showOnlyTheseTabs(allTabs);
+
+ // reset the environment
+ gBrowser.removeTab(testTab2);
+ gBrowser.removeTab(testTab1);
+ gBrowser.removeTab(pinned);
+ is(gBrowser.visibleTabs.length, 1, "only orig is left and visible");
+ is(gBrowser.tabs.length, 1, "sanity check that it matches");
+ is(Disabled(), true, "Bookmark All Tabs should be hidden");
+ is(gBrowser.selectedTab, origTab, "got the orig tab");
+ is(origTab.hidden, false, "and it's not hidden -- visible!");
+ finish();
+ }, true);
+}
+
+function Disabled() {
+ updateTabContextMenu();
+ return document.getElementById("Browser:BookmarkAllTabs").getAttribute("disabled") == "true";
+}
+
+function Hidden() {
+ updateTabContextMenu();
+ return document.getElementById("context_bookmarkAllTabs").hidden;
+}
diff --git a/browser/base/content/test/general/browser_visibleTabs_contextMenu.js b/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
new file mode 100644
index 000000000..4fdab3d8a
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
+
+add_task(function* test() {
+ // There should be one tab when we start the test
+ let [origTab] = gBrowser.visibleTabs;
+ is(gBrowser.visibleTabs.length, 1, "there is one visible tab");
+ let testTab = gBrowser.addTab();
+ is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs");
+
+ // Check the context menu with two tabs
+ updateTabContextMenu(origTab);
+ is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled");
+ is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled");
+
+
+ if (gFxAccounts.sendTabToDeviceEnabled) {
+ // Check the send tab to device menu item
+ const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
+ yield updateTabContextMenu(origTab, function* () {
+ yield openMenuItemSubmenu("context_sendTabToDevice");
+ });
+ is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
+ let targets = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
+ is(targets[0].getAttribute("label"), "Foo", "Foo target is present");
+ is(targets[1].getAttribute("label"), "Bar", "Bar target is present");
+ is(targets[3].getAttribute("label"), "All Devices", "All Devices target is present");
+ restoreRemoteClients(oldGetter);
+ }
+
+ // Hide the original tab.
+ gBrowser.selectedTab = testTab;
+ gBrowser.showOnlyTheseTabs([testTab]);
+ is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab");
+
+ // Check the context menu with one tab.
+ updateTabContextMenu(testTab);
+ is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled when more than one tab exists");
+ is(document.getElementById("context_reloadAllTabs").disabled, true, "Reload All Tabs is disabled");
+
+ // Add a tab that will get pinned
+ // So now there's one pinned tab, one visible unpinned tab, and one hidden tab
+ let pinned = gBrowser.addTab();
+ gBrowser.pinTab(pinned);
+ is(gBrowser.visibleTabs.length, 2, "now there are two visible tabs");
+
+ // Check the context menu on the unpinned visible tab
+ updateTabContextMenu(testTab);
+ is(document.getElementById("context_closeOtherTabs").disabled, true, "Close Other Tabs is disabled");
+ is(document.getElementById("context_closeTabsToTheEnd").disabled, true, "Close Tabs To The End is disabled");
+
+ // Show all tabs
+ let allTabs = Array.from(gBrowser.tabs);
+ gBrowser.showOnlyTheseTabs(allTabs);
+
+ // Check the context menu now
+ updateTabContextMenu(testTab);
+ is(document.getElementById("context_closeOtherTabs").disabled, false, "Close Other Tabs is enabled");
+ is(document.getElementById("context_closeTabsToTheEnd").disabled, true, "Close Tabs To The End is disabled");
+
+ // Check the context menu of the original tab
+ // Close Tabs To The End should now be enabled
+ updateTabContextMenu(origTab);
+ is(document.getElementById("context_closeTabsToTheEnd").disabled, false, "Close Tabs To The End is enabled");
+
+ gBrowser.removeTab(testTab);
+ gBrowser.removeTab(pinned);
+});
+
diff --git a/browser/base/content/test/general/browser_visibleTabs_tabPreview.js b/browser/base/content/test/general/browser_visibleTabs_tabPreview.js
new file mode 100644
index 000000000..7ce4b143f
--- /dev/null
+++ b/browser/base/content/test/general/browser_visibleTabs_tabPreview.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/. */
+
+add_task(function* test() {
+ gPrefService.setBoolPref("browser.ctrlTab.previews", true);
+
+ let [origTab] = gBrowser.visibleTabs;
+ let tabOne = gBrowser.addTab();
+ let tabTwo = gBrowser.addTab();
+
+ // test the ctrlTab.tabList
+ pressCtrlTab();
+ ok(ctrlTab.tabList.length, 3, "Show 3 tabs in tab preview");
+ releaseCtrl();
+
+ gBrowser.showOnlyTheseTabs([origTab]);
+ pressCtrlTab();
+ ok(ctrlTab.tabList.length, 1, "Show 1 tab in tab preview");
+ ok(!ctrlTab.isOpen, "With 1 tab open, Ctrl+Tab doesn't open the preview panel");
+
+ gBrowser.showOnlyTheseTabs([origTab, tabOne, tabTwo]);
+ pressCtrlTab();
+ ok(ctrlTab.isOpen, "With 3 tabs open, Ctrl+Tab does open the preview panel");
+ releaseCtrl();
+
+ // cleanup
+ gBrowser.removeTab(tabOne);
+ gBrowser.removeTab(tabTwo);
+
+ if (gPrefService.prefHasUserValue("browser.ctrlTab.previews"))
+ gPrefService.clearUserPref("browser.ctrlTab.previews");
+});
+
+function pressCtrlTab(aShiftKey) {
+ EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: !!aShiftKey });
+}
+
+function releaseCtrl() {
+ EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" });
+}
diff --git a/browser/base/content/test/general/browser_web_channel.html b/browser/base/content/test/general/browser_web_channel.html
new file mode 100644
index 000000000..f117ccca2
--- /dev/null
+++ b/browser/base/content/test/general/browser_web_channel.html
@@ -0,0 +1,189 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>web_channel_test</title>
+</head>
+<body>
+<script>
+ var IFRAME_SRC_ROOT = "http://mochi.test:8888/browser/browser/base/content/test/general/browser_web_channel_iframe.html";
+
+ window.onload = function() {
+ var testName = window.location.search.replace(/^\?/, "");
+
+ switch (testName) {
+ case "generic":
+ test_generic();
+ break;
+ case "twoway":
+ test_twoWay();
+ break;
+ case "multichannel":
+ test_multichannel();
+ break;
+ case "iframe":
+ test_iframe();
+ break;
+ case "iframe_pre_redirect":
+ test_iframe_pre_redirect();
+ break;
+ case "unsolicited":
+ test_unsolicited();
+ break;
+ case "bubbles":
+ test_bubbles();
+ break;
+ case "object":
+ test_object();
+ break;
+ default:
+ throw new Error(`INVALID TEST NAME ${testName}`);
+ }
+ };
+
+ function test_generic() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "generic",
+ message: {
+ something: {
+ nested: "hello",
+ },
+ }
+ })
+ });
+
+ window.dispatchEvent(event);
+ }
+
+ function test_twoWay() {
+ var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "one",
+ },
+ })
+ });
+
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "two",
+ detail: e.detail.message,
+ },
+ }),
+ });
+
+ if (!e.detail.message.error) {
+ window.dispatchEvent(secondMessage);
+ }
+ }, true);
+
+ window.dispatchEvent(firstMessage);
+ }
+
+ function test_multichannel() {
+ var event1 = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "wrongchannel",
+ message: {},
+ })
+ });
+
+ var event2 = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "multichannel",
+ message: {},
+ })
+ });
+
+ window.dispatchEvent(event1);
+ window.dispatchEvent(event2);
+ }
+
+ function test_iframe() {
+ // Note that this message is the response to the message sent
+ // by the iframe! This is bad, as this page is *not* trusted.
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ // the test parent will fail if the echo message is received.
+ echoEventToChannel(e, "echo");
+ });
+
+ // only attach the iframe for the iframe test to avoid
+ // interfering with other tests.
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", IFRAME_SRC_ROOT + "?iframe");
+ document.body.appendChild(iframe);
+ }
+
+ function test_iframe_pre_redirect() {
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", IFRAME_SRC_ROOT + "?iframe_pre_redirect");
+ document.body.appendChild(iframe);
+ }
+
+ function test_unsolicited() {
+ // echo any unsolicted events back to chrome.
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ echoEventToChannel(e, "echo");
+ }, true);
+ }
+
+ function test_bubbles() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "not_a_window",
+ message: {
+ command: "start"
+ }
+ })
+ });
+
+ var nonWindowTarget = document.getElementById("not_a_window");
+
+ nonWindowTarget.addEventListener("WebChannelMessageToContent", function(e) {
+ echoEventToChannel(e, "not_a_window");
+ }, true);
+
+
+ nonWindowTarget.dispatchEvent(event);
+ }
+
+ function test_object() {
+ let objectMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: {
+ id: "objects",
+ message: { type: "object" }
+ }
+ });
+
+ let stringMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "objects",
+ message: { type: "string" }
+ })
+ });
+ // Test fails if objectMessage is received, we send stringMessage to know
+ // when we should stop listening for objectMessage
+ window.dispatchEvent(objectMessage);
+ window.dispatchEvent(stringMessage);
+ }
+
+ function echoEventToChannel(e, channelId) {
+ var echoedEvent = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: channelId,
+ message: e.detail.message,
+ })
+ });
+
+ e.target.dispatchEvent(echoedEvent);
+ }
+</script>
+
+<div id="not_a_window"></div>
+</body>
+</html>
diff --git a/browser/base/content/test/general/browser_web_channel.js b/browser/base/content/test/general/browser_web_channel.js
new file mode 100644
index 000000000..abc1c6fef
--- /dev/null
+++ b/browser/base/content/test/general/browser_web_channel.js
@@ -0,0 +1,436 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
+ "resource://gre/modules/WebChannel.jsm");
+
+const HTTP_PATH = "http://example.com";
+const HTTP_ENDPOINT = "/browser/browser/base/content/test/general/browser_web_channel.html";
+const HTTP_MISMATCH_PATH = "http://example.org";
+const HTTP_IFRAME_PATH = "http://mochi.test:8888";
+const HTTP_REDIRECTED_IFRAME_PATH = "http://example.org";
+
+requestLongerTimeout(2); // timeouts in debug builds.
+
+// Keep this synced with /mobile/android/tests/browser/robocop/testWebChannel.js
+// as much as possible. (We only have that since we can't run browser chrome
+// tests on Android. Yet?)
+var gTests = [
+ {
+ desc: "WebChannel generic message",
+ run: function* () {
+ return new Promise(function(resolve, reject) {
+ let tab;
+ let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH, null, null));
+ channel.listen(function (id, message, target) {
+ is(id, "generic");
+ is(message.something.nested, "hello");
+ channel.stopListening();
+ gBrowser.removeTab(tab);
+ resolve();
+ });
+
+ tab = gBrowser.addTab(HTTP_PATH + HTTP_ENDPOINT + "?generic");
+ });
+ }
+ },
+ {
+ desc: "WebChannel generic message in a private window.",
+ run: function* () {
+ let promiseTestDone = new Promise(function(resolve, reject) {
+ let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH, null, null));
+ channel.listen(function(id, message, target) {
+ is(id, "generic");
+ is(message.something.nested, "hello");
+ channel.stopListening();
+ resolve();
+ });
+ });
+
+ const url = HTTP_PATH + HTTP_ENDPOINT + "?generic";
+ let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+ yield BrowserTestUtils.openNewForegroundTab(privateWindow.gBrowser, url);
+ yield promiseTestDone;
+ yield BrowserTestUtils.closeWindow(privateWindow);
+ }
+ },
+ {
+ desc: "WebChannel two way communication",
+ run: function* () {
+ return new Promise(function(resolve, reject) {
+ let tab;
+ let channel = new WebChannel("twoway", Services.io.newURI(HTTP_PATH, null, null));
+
+ channel.listen(function (id, message, sender) {
+ is(id, "twoway", "bad id");
+ ok(message.command, "command not ok");
+
+ if (message.command === "one") {
+ channel.send({ data: { nested: true } }, sender);
+ }
+
+ if (message.command === "two") {
+ is(message.detail.data.nested, true);
+ channel.stopListening();
+ gBrowser.removeTab(tab);
+ resolve();
+ }
+ });
+
+ tab = gBrowser.addTab(HTTP_PATH + HTTP_ENDPOINT + "?twoway");
+ });
+ }
+ },
+ {
+ desc: "WebChannel two way communication in an iframe",
+ run: function* () {
+ let parentChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH, null, null));
+ let iframeChannel = new WebChannel("twoway", Services.io.newURI(HTTP_IFRAME_PATH, null, null));
+ let promiseTestDone = new Promise(function (resolve, reject) {
+ parentChannel.listen(function (id, message, sender) {
+ reject(new Error("WebChannel message incorrectly sent to parent"));
+ });
+
+ iframeChannel.listen(function (id, message, sender) {
+ is(id, "twoway", "bad id (2)");
+ ok(message.command, "command not ok (2)");
+
+ if (message.command === "one") {
+ iframeChannel.send({ data: { nested: true } }, sender);
+ }
+
+ if (message.command === "two") {
+ is(message.detail.data.nested, true);
+ resolve();
+ }
+ });
+ });
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?iframe"
+ }, function* () {
+ yield promiseTestDone;
+ parentChannel.stopListening();
+ iframeChannel.stopListening();
+ });
+ }
+ },
+ {
+ desc: "WebChannel response to a redirected iframe",
+ run: function* () {
+ /**
+ * This test checks that WebChannel responses are only sent
+ * to an iframe if the iframe has not redirected to another origin.
+ * Test flow:
+ * 1. create a page, embed an iframe on origin A.
+ * 2. the iframe sends a message `redirecting`, then redirects to
+ * origin B.
+ * 3. the iframe at origin B is set up to echo any messages back to the
+ * test parent.
+ * 4. the test parent receives the `redirecting` message from origin A.
+ * the test parent creates a new channel with origin B.
+ * 5. when origin B is ready, it sends a `loaded` message to the test
+ * parent, letting the test parent know origin B is ready to echo
+ * messages.
+ * 5. the test parent tries to send a response to origin A. If the
+ * WebChannel does not perform a valid origin check, the response
+ * will be received by origin B. If the WebChannel does perform
+ * a valid origin check, the response will not be sent.
+ * 6. the test parent sends a `done` message to origin B, which origin
+ * B echoes back. If the response to origin A is not echoed but
+ * the message to origin B is, then hooray, the test passes.
+ */
+
+ let preRedirectChannel = new WebChannel("pre_redirect", Services.io.newURI(HTTP_IFRAME_PATH, null, null));
+ let postRedirectChannel = new WebChannel("post_redirect", Services.io.newURI(HTTP_REDIRECTED_IFRAME_PATH, null, null));
+
+ let promiseTestDone = new Promise(function (resolve, reject) {
+ preRedirectChannel.listen(function (id, message, preRedirectSender) {
+ if (message.command === "redirecting") {
+
+ postRedirectChannel.listen(function (aId, aMessage, aPostRedirectSender) {
+ is(aId, "post_redirect");
+ isnot(aMessage.command, "no_response_expected");
+
+ if (aMessage.command === "loaded") {
+ // The message should not be received on the preRedirectChannel
+ // because the target window has redirected.
+ preRedirectChannel.send({ command: "no_response_expected" }, preRedirectSender);
+ postRedirectChannel.send({ command: "done" }, aPostRedirectSender);
+ } else if (aMessage.command === "done") {
+ resolve();
+ } else {
+ reject(new Error(`Unexpected command ${aMessage.command}`));
+ }
+ });
+ } else {
+ reject(new Error(`Unexpected command ${message.command}`));
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?iframe_pre_redirect"
+ }, function* () {
+ yield promiseTestDone;
+ preRedirectChannel.stopListening();
+ postRedirectChannel.stopListening();
+ });
+ }
+ },
+ {
+ desc: "WebChannel multichannel",
+ run: function* () {
+ return new Promise(function(resolve, reject) {
+ let tab;
+ let channel = new WebChannel("multichannel", Services.io.newURI(HTTP_PATH, null, null));
+
+ channel.listen(function (id, message, sender) {
+ is(id, "multichannel");
+ gBrowser.removeTab(tab);
+ resolve();
+ });
+
+ tab = gBrowser.addTab(HTTP_PATH + HTTP_ENDPOINT + "?multichannel");
+ });
+ }
+ },
+ {
+ desc: "WebChannel unsolicited send, using system principal",
+ run: function* () {
+ let channel = new WebChannel("echo", Services.io.newURI(HTTP_PATH, null, null));
+
+ // an unsolicted message is sent from Chrome->Content which is then
+ // echoed back. If the echo is received here, then the content
+ // received the message.
+ let messagePromise = new Promise(function (resolve, reject) {
+ channel.listen(function (id, message, sender) {
+ is(id, "echo");
+ is(message.command, "unsolicited");
+
+ resolve()
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited"
+ }, function* (targetBrowser) {
+ channel.send({ command: "unsolicited" }, {
+ browser: targetBrowser,
+ principal: Services.scriptSecurityManager.getSystemPrincipal()
+ });
+ yield messagePromise;
+ channel.stopListening();
+ });
+ }
+ },
+ {
+ desc: "WebChannel unsolicited send, using target origin's principal",
+ run: function* () {
+ let targetURI = Services.io.newURI(HTTP_PATH, null, null);
+ let channel = new WebChannel("echo", targetURI);
+
+ // an unsolicted message is sent from Chrome->Content which is then
+ // echoed back. If the echo is received here, then the content
+ // received the message.
+ let messagePromise = new Promise(function (resolve, reject) {
+ channel.listen(function (id, message, sender) {
+ is(id, "echo");
+ is(message.command, "unsolicited");
+
+ resolve();
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited"
+ }, function* (targetBrowser) {
+
+ channel.send({ command: "unsolicited" }, {
+ browser: targetBrowser,
+ principal: Services.scriptSecurityManager.getNoAppCodebasePrincipal(targetURI)
+ });
+
+ yield messagePromise;
+ channel.stopListening();
+ });
+ }
+ },
+ {
+ desc: "WebChannel unsolicited send with principal mismatch",
+ run: function* () {
+ let targetURI = Services.io.newURI(HTTP_PATH, null, null);
+ let channel = new WebChannel("echo", targetURI);
+
+ // two unsolicited messages are sent from Chrome->Content. The first,
+ // `unsolicited_no_response_expected` is sent to the wrong principal
+ // and should not be echoed back. The second, `done`, is sent to the
+ // correct principal and should be echoed back.
+ let messagePromise = new Promise(function (resolve, reject) {
+ channel.listen(function (id, message, sender) {
+ is(id, "echo");
+
+ if (message.command === "done") {
+ resolve();
+ } else {
+ reject(new Error(`Unexpected command ${message.command}`));
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser: gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited"
+ }, function* (targetBrowser) {
+
+ let mismatchURI = Services.io.newURI(HTTP_MISMATCH_PATH, null, null);
+ let mismatchPrincipal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(mismatchURI);
+
+ // send a message to the wrong principal. It should not be delivered
+ // to content, and should not be echoed back.
+ channel.send({ command: "unsolicited_no_response_expected" }, {
+ browser: targetBrowser,
+ principal: mismatchPrincipal
+ });
+
+ let targetPrincipal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(targetURI);
+
+ // send the `done` message to the correct principal. It
+ // should be echoed back.
+ channel.send({ command: "done" }, {
+ browser: targetBrowser,
+ principal: targetPrincipal
+ });
+
+ yield messagePromise;
+ channel.stopListening();
+ });
+ }
+ },
+ {
+ desc: "WebChannel non-window target",
+ run: function* () {
+ /**
+ * This test ensures messages can be received from and responses
+ * sent to non-window elements.
+ *
+ * First wait for the non-window element to send a "start" message.
+ * Then send the non-window element a "done" message.
+ * The non-window element will echo the "done" message back, if it
+ * receives the message.
+ * Listen for the response. If received, good to go!
+ */
+ let channel = new WebChannel("not_a_window", Services.io.newURI(HTTP_PATH, null, null));
+
+ let testDonePromise = new Promise(function (resolve, reject) {
+ channel.listen(function (id, message, sender) {
+ if (message.command === "start") {
+ channel.send({ command: "done" }, sender);
+ } else if (message.command === "done") {
+ resolve();
+ } else {
+ reject(new Error(`Unexpected command ${message.command}`));
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?bubbles"
+ }, function* () {
+ yield testDonePromise;
+ channel.stopListening();
+ });
+ }
+ },
+ {
+ desc: "WebChannel disallows non-string message from non-whitelisted origin",
+ run: function* () {
+ /**
+ * This test ensures that non-string messages can't be sent via WebChannels.
+ * We create a page (on a non-whitelisted origin) which should send us two
+ * messages immediately. The first message has an object for it's detail,
+ * and the second has a string. We check that we only get the second
+ * message.
+ */
+ let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH, null, null));
+ let testDonePromise = new Promise((resolve, reject) => {
+ channel.listen((id, message, sender) => {
+ is(id, "objects");
+ is(message.type, "string");
+ resolve();
+ });
+ });
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?object"
+ }, function* () {
+ yield testDonePromise;
+ channel.stopListening();
+ });
+ }
+ },
+ {
+ desc: "WebChannel allows both string and non-string message from whitelisted origin",
+ run: function* () {
+ /**
+ * Same process as above, but we whitelist the origin before loading the page,
+ * and expect to get *both* messages back (each exactly once).
+ */
+ let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH, null, null));
+
+ let testDonePromise = new Promise((resolve, reject) => {
+ let sawObject = false;
+ let sawString = false;
+ channel.listen((id, message, sender) => {
+ is(id, "objects");
+ if (message.type === "object") {
+ ok(!sawObject);
+ sawObject = true;
+ } else if (message.type === "string") {
+ ok(!sawString);
+ sawString = true;
+ } else {
+ reject(new Error(`Unknown message type: ${message.type}`))
+ }
+ if (sawObject && sawString) {
+ resolve();
+ }
+ });
+ });
+ const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+ let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+ let newWhitelist = origWhitelist + " " + HTTP_PATH;
+ Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: HTTP_PATH + HTTP_ENDPOINT + "?object"
+ }, function* () {
+ yield testDonePromise;
+ Services.prefs.setCharPref(webchannelWhitelistPref, origWhitelist);
+ channel.stopListening();
+ });
+ }
+ }
+]; // gTests
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ for (let testCase of gTests) {
+ info("Running: " + testCase.desc);
+ yield testCase.run();
+ }
+ }).then(finish, ex => {
+ ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+}
diff --git a/browser/base/content/test/general/browser_web_channel_iframe.html b/browser/base/content/test/general/browser_web_channel_iframe.html
new file mode 100644
index 000000000..7900e7530
--- /dev/null
+++ b/browser/base/content/test/general/browser_web_channel_iframe.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>web_channel_test (iframe)</title>
+</head>
+<body>
+<script>
+ var REDIRECTED_IFRAME_SRC_ROOT = "http://example.org/browser/browser/base/content/test/general/browser_web_channel_iframe.html";
+
+ window.onload = function() {
+ var testName = window.location.search.replace(/^\?/, "");
+ switch (testName) {
+ case "iframe":
+ test_iframe();
+ break;
+ case "iframe_pre_redirect":
+ test_iframe_pre_redirect();
+ break;
+ case "iframe_post_redirect":
+ test_iframe_post_redirect();
+ break;
+ default:
+ throw new Error(`INVALID TEST NAME ${testName}`);
+ }
+ };
+
+ function test_iframe() {
+ var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "one",
+ },
+ })
+ });
+
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "twoway",
+ message: {
+ command: "two",
+ detail: e.detail.message,
+ },
+ }),
+ });
+
+ if (!e.detail.message.error) {
+ window.dispatchEvent(secondMessage);
+ }
+ }, true);
+
+ window.dispatchEvent(firstMessage);
+ }
+
+
+ function test_iframe_pre_redirect() {
+ var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "pre_redirect",
+ message: {
+ command: "redirecting",
+ },
+ }),
+ });
+ window.dispatchEvent(firstMessage);
+ document.location = REDIRECTED_IFRAME_SRC_ROOT + "?iframe_post_redirect";
+ }
+
+ function test_iframe_post_redirect() {
+ window.addEventListener("WebChannelMessageToContent", function(e) {
+ var echoMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "post_redirect",
+ message: e.detail.message,
+ }),
+ });
+
+ window.dispatchEvent(echoMessage);
+ }, true);
+
+ // Let the test parent know the page has loaded and is ready to echo events
+ var loadedMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: JSON.stringify({
+ id: "post_redirect",
+ message: {
+ command: "loaded",
+ },
+ }),
+ });
+ window.dispatchEvent(loadedMessage);
+ }
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/browser_windowactivation.js b/browser/base/content/test/general/browser_windowactivation.js
new file mode 100644
index 000000000..ae4ba75dc
--- /dev/null
+++ b/browser/base/content/test/general/browser_windowactivation.js
@@ -0,0 +1,183 @@
+/*
+ * This test checks that window activation state is set properly with multiple tabs.
+ */
+
+var testPage = "data:text/html,<body><style>:-moz-window-inactive { background-color: red; }</style><div id='area'></div></body>";
+
+var colorChangeNotifications = 0;
+var otherWindow;
+
+var browser1, browser2;
+
+function test() {
+ waitForExplicitFinish();
+ waitForFocus(reallyRunTests);
+}
+
+function reallyRunTests() {
+
+ let tab1 = gBrowser.addTab();
+ let tab2 = gBrowser.addTab();
+ browser1 = gBrowser.getBrowserForTab(tab1);
+ browser2 = gBrowser.getBrowserForTab(tab2);
+
+ gURLBar.focus();
+
+ var loadCount = 0;
+ function check()
+ {
+ // wait for both tabs to load
+ if (++loadCount != 2) {
+ return;
+ }
+
+ browser1.removeEventListener("load", check, true);
+ browser2.removeEventListener("load", check, true);
+
+ sendGetBackgroundRequest(true);
+ }
+
+ // The test performs four checks, using -moz-window-inactive on two child tabs.
+ // First, the initial state should be transparent. The second check is done
+ // while another window is focused. The third check is done after that window
+ // is closed and the main window focused again. The fourth check is done after
+ // switching to the second tab.
+ window.messageManager.addMessageListener("Test:BackgroundColorChanged", function(message) {
+ colorChangeNotifications++;
+
+ switch (colorChangeNotifications) {
+ case 1:
+ is(message.data.color, "transparent", "first window initial");
+ break;
+ case 2:
+ is(message.data.color, "transparent", "second window initial");
+ runOtherWindowTests();
+ break;
+ case 3:
+ is(message.data.color, "rgb(255, 0, 0)", "first window lowered");
+ break;
+ case 4:
+ is(message.data.color, "rgb(255, 0, 0)", "second window lowered");
+ sendGetBackgroundRequest(true);
+ otherWindow.close();
+ break;
+ case 5:
+ is(message.data.color, "transparent", "first window raised");
+ break;
+ case 6:
+ is(message.data.color, "transparent", "second window raised");
+ gBrowser.selectedTab = tab2;
+ break;
+ case 7:
+ is(message.data.color, "transparent", "first window after tab switch");
+ break;
+ case 8:
+ is(message.data.color, "transparent", "second window after tab switch");
+ finishTest();
+ break;
+ case 9:
+ ok(false, "too many color change notifications");
+ break;
+ }
+ });
+
+ window.messageManager.addMessageListener("Test:FocusReceived", function(message) {
+ // No color change should occur after a tab switch.
+ if (colorChangeNotifications == 6) {
+ sendGetBackgroundRequest(false);
+ }
+ });
+
+ window.messageManager.addMessageListener("Test:ActivateEvent", function(message) {
+ ok(message.data.ok, "Test:ActivateEvent");
+ });
+
+ window.messageManager.addMessageListener("Test:DeactivateEvent", function(message) {
+ ok(message.data.ok, "Test:DeactivateEvent");
+ });
+
+ browser1.addEventListener("load", check, true);
+ browser2.addEventListener("load", check, true);
+ browser1.contentWindow.location = testPage;
+ browser2.contentWindow.location = testPage;
+
+ browser1.messageManager.loadFrameScript("data:,(" + childFunction.toString() + ")();", true);
+ browser2.messageManager.loadFrameScript("data:,(" + childFunction.toString() + ")();", true);
+
+ gBrowser.selectedTab = tab1;
+}
+
+function sendGetBackgroundRequest(ifChanged)
+{
+ browser1.messageManager.sendAsyncMessage("Test:GetBackgroundColor", { ifChanged: ifChanged });
+ browser2.messageManager.sendAsyncMessage("Test:GetBackgroundColor", { ifChanged: ifChanged });
+}
+
+function runOtherWindowTests() {
+ otherWindow = window.open("data:text/html,<body>Hi</body>", "", "chrome");
+ waitForFocus(function () {
+ sendGetBackgroundRequest(true);
+ }, otherWindow);
+}
+
+function finishTest()
+{
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ otherWindow = null;
+ finish();
+}
+
+function childFunction()
+{
+ let oldColor = null;
+
+ let expectingResponse = false;
+ let ifChanged = true;
+
+ addMessageListener("Test:GetBackgroundColor", function(message) {
+ expectingResponse = true;
+ ifChanged = message.data.ifChanged;
+ });
+
+ content.addEventListener("focus", function () {
+ sendAsyncMessage("Test:FocusReceived", { });
+ }, false);
+
+ var windowGotActivate = false;
+ var windowGotDeactivate = false;
+ addEventListener("activate", function() {
+ sendAsyncMessage("Test:ActivateEvent", { ok: !windowGotActivate });
+ windowGotActivate = false;
+ });
+
+ addEventListener("deactivate", function() {
+ sendAsyncMessage("Test:DeactivateEvent", { ok: !windowGotDeactivate });
+ windowGotDeactivate = false;
+ });
+ content.addEventListener("activate", function() {
+ windowGotActivate = true;
+ });
+
+ content.addEventListener("deactivate", function() {
+ windowGotDeactivate = true;
+ });
+
+ content.setInterval(function () {
+ if (!expectingResponse) {
+ return;
+ }
+
+ let area = content.document.getElementById("area");
+ if (!area) {
+ return; /* hasn't loaded yet */
+ }
+
+ let color = content.getComputedStyle(area, "").backgroundColor;
+ if (oldColor != color || !ifChanged) {
+ expectingResponse = false;
+ oldColor = color;
+ sendAsyncMessage("Test:BackgroundColorChanged", { color: color });
+ }
+ }, 20);
+}
diff --git a/browser/base/content/test/general/browser_windowopen_reflows.js b/browser/base/content/test/general/browser_windowopen_reflows.js
new file mode 100644
index 000000000..7dac8aad6
--- /dev/null
+++ b/browser/base/content/test/general/browser_windowopen_reflows.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const EXPECTED_REFLOWS = [
+ // handleEvent flushes layout to get the tabstrip width after a resize.
+ "handleEvent@chrome://browser/content/tabbrowser.xml|",
+
+ // Loading a tab causes a reflow.
+ "loadTabs@chrome://browser/content/tabbrowser.xml|" +
+ "loadOneOrMoreURIs@chrome://browser/content/browser.js|" +
+ "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|",
+
+ // Selecting the address bar causes a reflow.
+ "select@chrome://global/content/bindings/textbox.xml|" +
+ "focusAndSelectUrlBar@chrome://browser/content/browser.js|" +
+ "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|",
+
+ // Focusing the content area causes a reflow.
+ "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|",
+
+ // Sometimes sessionstore collects data during this test, which causes a sync reflow
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=892154 will fix this)
+ "ssi_getWindowDimension@resource:///modules/sessionstore/SessionStore.jsm",
+];
+
+if (Services.appinfo.OS == "WINNT" || Services.appinfo.OS == "Darwin") {
+ // TabsInTitlebar._update causes a reflow on OS X and Windows trying to do calculations
+ // since layout info is already dirty. This doesn't seem to happen before
+ // MozAfterPaint on Linux.
+ EXPECTED_REFLOWS.push("TabsInTitlebar._update/rect@chrome://browser/content/browser-tabsintitlebar.js|" +
+ "TabsInTitlebar._update@chrome://browser/content/browser-tabsintitlebar.js|" +
+ "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js|" +
+ "handleEvent@chrome://browser/content/tabbrowser.xml|");
+}
+
+if (Services.appinfo.OS == "Darwin") {
+ // _onOverflow causes a reflow getting widths.
+ EXPECTED_REFLOWS.push("OverflowableToolbar.prototype._onOverflow@resource:///modules/CustomizableUI.jsm|" +
+ "OverflowableToolbar.prototype.init@resource:///modules/CustomizableUI.jsm|" +
+ "OverflowableToolbar.prototype.observe@resource:///modules/CustomizableUI.jsm|" +
+ "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|");
+ // Same as above since in packaged builds there are no function names and the resource URI includes "app"
+ EXPECTED_REFLOWS.push("@resource://app/modules/CustomizableUI.jsm|" +
+ "@resource://app/modules/CustomizableUI.jsm|" +
+ "@resource://app/modules/CustomizableUI.jsm|" +
+ "gBrowserInit._delayedStartup@chrome://browser/content/browser.js|");
+}
+
+/*
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when opening new windows.
+ */
+function test() {
+ waitForExplicitFinish();
+
+ // Add a reflow observer and open a new window
+ let win = OpenBrowserWindow();
+ let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.addWeakReflowObserver(observer);
+
+ // Wait until the mozafterpaint event occurs.
+ waitForMozAfterPaint(win, function paintListener() {
+ // Remove reflow observer and clean up.
+ docShell.removeWeakReflowObserver(observer);
+ win.close();
+
+ finish();
+ });
+}
+
+var observer = {
+ reflow: function (start, end) {
+ // Gather information about the current code path.
+ let stack = new Error().stack;
+ let path = stack.split("\n").slice(1).map(line => {
+ return line.replace(/:\d+:\d+$/, "");
+ }).join("|");
+ let pathWithLineNumbers = (new Error().stack).split("\n").slice(1).join("|");
+
+ // Stack trace is empty. Reflow was triggered by native code.
+ if (path === "") {
+ return;
+ }
+
+ // Check if this is an expected reflow.
+ for (let expectedStack of EXPECTED_REFLOWS) {
+ if (path.startsWith(expectedStack) ||
+ // Accept an empty function name for gBrowserInit._delayedStartup or TabsInTitlebar._update to workaround bug 906578.
+ path.startsWith(expectedStack.replace(/(^|\|)(gBrowserInit\._delayedStartup|TabsInTitlebar\._update)@/, "$1@"))) {
+ ok(true, "expected uninterruptible reflow '" + expectedStack + "'");
+ return;
+ }
+ }
+
+ ok(false, "unexpected uninterruptible reflow '" + pathWithLineNumbers + "'");
+ },
+
+ reflowInterruptible: function (start, end) {
+ // We're not interested in interruptible reflows.
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
+ Ci.nsISupportsWeakReference])
+};
+
+function waitForMozAfterPaint(win, callback) {
+ win.addEventListener("MozAfterPaint", function onEnd(event) {
+ if (event.target != win)
+ return;
+ win.removeEventListener("MozAfterPaint", onEnd);
+ executeSoon(callback);
+ });
+}
diff --git a/browser/base/content/test/general/browser_zbug569342.js b/browser/base/content/test/general/browser_zbug569342.js
new file mode 100644
index 000000000..2dac5acde
--- /dev/null
+++ b/browser/base/content/test/general/browser_zbug569342.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gTab = null;
+
+function load(url, cb) {
+ gTab = gBrowser.addTab(url);
+ gBrowser.addEventListener("load", function (event) {
+ if (event.target.location != url)
+ return;
+
+ gBrowser.removeEventListener("load", arguments.callee, true);
+ // Trigger onLocationChange by switching tabs.
+ gBrowser.selectedTab = gTab;
+ cb();
+ }, true);
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(gFindBar.hidden, "Find bar should not be visible by default");
+
+ // Open the Find bar before we navigate to pages that shouldn't have it.
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ ok(!gFindBar.hidden, "Find bar should be visible");
+
+ nextTest();
+}
+
+var urls = [
+ "about:config",
+ "about:addons",
+];
+
+function nextTest() {
+ let url = urls.shift();
+ if (url) {
+ testFindDisabled(url, nextTest);
+ } else {
+ // Make sure the find bar is re-enabled after disabled page is closed.
+ testFindEnabled("about:blank", function () {
+ EventUtils.synthesizeKey("VK_ESCAPE", { });
+ ok(gFindBar.hidden, "Find bar should now be hidden");
+ finish();
+ });
+ }
+}
+
+function testFindDisabled(url, cb) {
+ load(url, function() {
+ ok(gFindBar.hidden, "Find bar should not be visible");
+ EventUtils.synthesizeKey("/", {}, gTab.linkedBrowser.contentWindow);
+ ok(gFindBar.hidden, "Find bar should not be visible");
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ ok(gFindBar.hidden, "Find bar should not be visible");
+ ok(document.getElementById("cmd_find").getAttribute("disabled"),
+ "Find command should be disabled");
+
+ gBrowser.removeTab(gTab);
+ cb();
+ });
+}
+
+function testFindEnabled(url, cb) {
+ load(url, function() {
+ ok(!document.getElementById("cmd_find").getAttribute("disabled"),
+ "Find command should not be disabled");
+
+ // Open Find bar and then close it.
+ EventUtils.synthesizeKey("f", { accelKey: true });
+ ok(!gFindBar.hidden, "Find bar should be visible again");
+ EventUtils.synthesizeKey("VK_ESCAPE", { });
+ ok(gFindBar.hidden, "Find bar should now be hidden");
+
+ gBrowser.removeTab(gTab);
+ cb();
+ });
+}
diff --git a/browser/base/content/test/general/bug1262648_string_with_newlines.dtd b/browser/base/content/test/general/bug1262648_string_with_newlines.dtd
new file mode 100644
index 000000000..308072c4e
--- /dev/null
+++ b/browser/base/content/test/general/bug1262648_string_with_newlines.dtd
@@ -0,0 +1,3 @@
+<!ENTITY foo.bar "This string
+contains
+newlines!"> \ No newline at end of file
diff --git a/browser/base/content/test/general/bug364677-data.xml b/browser/base/content/test/general/bug364677-data.xml
new file mode 100644
index 000000000..b48915c05
--- /dev/null
+++ b/browser/base/content/test/general/bug364677-data.xml
@@ -0,0 +1,5 @@
+<rss version="2.0">
+ <channel>
+ <title>t</title>
+ </channel>
+</rss>
diff --git a/browser/base/content/test/general/bug364677-data.xml^headers^ b/browser/base/content/test/general/bug364677-data.xml^headers^
new file mode 100644
index 000000000..f203c6368
--- /dev/null
+++ b/browser/base/content/test/general/bug364677-data.xml^headers^
@@ -0,0 +1 @@
+Content-Type: text/xml
diff --git a/browser/base/content/test/general/bug395533-data.txt b/browser/base/content/test/general/bug395533-data.txt
new file mode 100644
index 000000000..e0ed39850
--- /dev/null
+++ b/browser/base/content/test/general/bug395533-data.txt
@@ -0,0 +1,6 @@
+<rss version="2.0">
+ <channel>
+ <link>http://example.org/</link>
+ <title>t</title>
+ </channel>
+</rss>
diff --git a/browser/base/content/test/general/bug592338.html b/browser/base/content/test/general/bug592338.html
new file mode 100644
index 000000000..159b21a76
--- /dev/null
+++ b/browser/base/content/test/general/bug592338.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+<script type="text/javascript">
+var theme = {
+ id: "test",
+ name: "Test Background",
+ headerURL: "http://example.com/firefox/personas/01/header.jpg",
+ footerURL: "http://example.com/firefox/personas/01/footer.jpg",
+ textcolor: "#fff",
+ accentcolor: "#6b6b6b"
+};
+
+function setTheme(node) {
+ node.setAttribute("data-browsertheme", JSON.stringify(theme));
+ var event = document.createEvent("Events");
+ event.initEvent("InstallBrowserTheme", true, false);
+ node.dispatchEvent(event);
+}
+</script>
+</head>
+<body>
+<a id="theme-install" href="#" onclick="setTheme(this)">Install</a>
+</body>
+</html>
diff --git a/browser/base/content/test/general/bug792517-2.html b/browser/base/content/test/general/bug792517-2.html
new file mode 100644
index 000000000..bfc24d817
--- /dev/null
+++ b/browser/base/content/test/general/bug792517-2.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+<a href="bug792517.sjs" id="fff">this is a link</a>
+</body>
+</html>
diff --git a/browser/base/content/test/general/bug792517.html b/browser/base/content/test/general/bug792517.html
new file mode 100644
index 000000000..e7c040bf1
--- /dev/null
+++ b/browser/base/content/test/general/bug792517.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+<img src="moz.png" id="img">
+</body>
+</html>
diff --git a/browser/base/content/test/general/bug792517.sjs b/browser/base/content/test/general/bug792517.sjs
new file mode 100644
index 000000000..91e5aa23f
--- /dev/null
+++ b/browser/base/content/test/general/bug792517.sjs
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ if (aRequest.hasHeader('Cookie')) {
+ aResponse.write("cookie-present");
+ } else {
+ aResponse.setHeader("Set-Cookie", "foopy=1");
+ aResponse.write("cookie-not-present");
+ }
+}
diff --git a/browser/base/content/test/general/bug839103.css b/browser/base/content/test/general/bug839103.css
new file mode 100644
index 000000000..611907d3d
--- /dev/null
+++ b/browser/base/content/test/general/bug839103.css
@@ -0,0 +1 @@
+* {}
diff --git a/browser/base/content/test/general/clipboard_pastefile.html b/browser/base/content/test/general/clipboard_pastefile.html
new file mode 100644
index 000000000..fcbf60ed2
--- /dev/null
+++ b/browser/base/content/test/general/clipboard_pastefile.html
@@ -0,0 +1,37 @@
+<html><body>
+<script>
+function checkPaste(event)
+{
+ let output = document.getElementById("output");
+ output.textContent = checkPasteHelper(event);
+}
+
+function checkPasteHelper(event)
+{
+ let dt = event.clipboardData;
+ if (dt.types.length != 2)
+ return "Wrong number of types; got " + dt.types.length;
+
+ for (let type of dt.types) {
+ if (type != "Files" && type != "application/x-moz-file")
+ return "Invalid type for types; got" + type;
+ }
+
+ for (let type of dt.mozTypesAt(0)) {
+ if (type != "Files" && type != "application/x-moz-file")
+ return "Invalid type for mozTypesAt; got" + type;
+ }
+
+ if (dt.getData("text/plain"))
+ return "text/plain found with getData";
+ if (dt.mozGetDataAt("text/plain", 0))
+ return "text/plain found with mozGetDataAt";
+
+ return "Passed";
+}
+</script>
+
+<input id="input" onpaste="checkPaste(event)">
+<div id="output"></div>
+
+</body></html>
diff --git a/browser/base/content/test/general/close_beforeunload.html b/browser/base/content/test/general/close_beforeunload.html
new file mode 100644
index 000000000..4b62002cc
--- /dev/null
+++ b/browser/base/content/test/general/close_beforeunload.html
@@ -0,0 +1,8 @@
+<body>
+ <p>I will close myself if you close me.</p>
+ <script>
+ window.onbeforeunload = function() {
+ window.close();
+ };
+ </script>
+</body>
diff --git a/browser/base/content/test/general/close_beforeunload_opens_second_tab.html b/browser/base/content/test/general/close_beforeunload_opens_second_tab.html
new file mode 100644
index 000000000..243307a0e
--- /dev/null
+++ b/browser/base/content/test/general/close_beforeunload_opens_second_tab.html
@@ -0,0 +1,3 @@
+<body>
+ <a href="#" onclick="window.open('close_beforeunload.html', '_blank')">Open second tab</a>
+</body>
diff --git a/browser/base/content/test/general/contentSearchUI.html b/browser/base/content/test/general/contentSearchUI.html
new file mode 100644
index 000000000..3750ac2b0
--- /dev/null
+++ b/browser/base/content/test/general/contentSearchUI.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+<meta charset="utf-8">
+<script type="application/javascript;version=1.8"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
+</script>
+<script type="application/javascript;version=1.8"
+ src="chrome://browser/content/contentSearchUI.js">
+</script>
+<link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css"/>
+</head>
+<body>
+
+<div id="container" style="position: relative;"><input type="text" value=""/></div>
+
+</body>
+</html>
diff --git a/browser/base/content/test/general/contentSearchUI.js b/browser/base/content/test/general/contentSearchUI.js
new file mode 100644
index 000000000..0e46230a2
--- /dev/null
+++ b/browser/base/content/test/general/contentSearchUI.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+(function () {
+
+const TEST_MSG = "ContentSearchUIControllerTest";
+const ENGINE_NAME = "browser_searchSuggestionEngine searchSuggestionEngine.xml";
+var gController;
+
+addMessageListener(TEST_MSG, msg => {
+ messageHandlers[msg.data.type](msg.data.data);
+});
+
+var messageHandlers = {
+
+ init: function() {
+ Services.search.currentEngine = Services.search.getEngineByName(ENGINE_NAME);
+ let input = content.document.querySelector("input");
+ gController =
+ new content.ContentSearchUIController(input, input.parentNode, "test", "test");
+ content.addEventListener("ContentSearchService", function listener(aEvent) {
+ if (aEvent.detail.type == "State" &&
+ gController.defaultEngine.name == ENGINE_NAME) {
+ content.removeEventListener("ContentSearchService", listener);
+ ack("init");
+ }
+ });
+ gController.remoteTimeout = 5000;
+ },
+
+ key: function (arg) {
+ let keyName = typeof(arg) == "string" ? arg : arg.key;
+ content.synthesizeKey(keyName, arg.modifiers || {});
+ let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
+ wait(ack.bind(null, "key"));
+ },
+
+ startComposition: function (arg) {
+ content.synthesizeComposition({ type: "compositionstart", data: "" });
+ ack("startComposition");
+ },
+
+ changeComposition: function (arg) {
+ let data = typeof(arg) == "string" ? arg : arg.data;
+ content.synthesizeCompositionChange({
+ composition: {
+ string: data,
+ clauses: [
+ { length: data.length, attr: content.COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ caret: { start: data.length, length: 0 }
+ });
+ let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
+ wait(ack.bind(null, "changeComposition"));
+ },
+
+ commitComposition: function () {
+ content.synthesizeComposition({ type: "compositioncommitasis" });
+ ack("commitComposition");
+ },
+
+ focus: function () {
+ gController.input.focus();
+ ack("focus");
+ },
+
+ blur: function () {
+ gController.input.blur();
+ ack("blur");
+ },
+
+ waitForSearch: function () {
+ waitForContentSearchEvent("Search", aData => ack("waitForSearch", aData));
+ },
+
+ waitForSearchSettings: function () {
+ waitForContentSearchEvent("ManageEngines",
+ aData => ack("waitForSearchSettings", aData));
+ },
+
+ mousemove: function (itemIndex) {
+ let row;
+ if (itemIndex == -1) {
+ row = gController._table.firstChild;
+ }
+ else {
+ let allElts = [...gController._suggestionsList.children,
+ ...gController._oneOffButtons,
+ content.document.getElementById("contentSearchSettingsButton")];
+ row = allElts[itemIndex];
+ }
+ let event = {
+ type: "mousemove",
+ clickcount: 0,
+ }
+ row.addEventListener("mousemove", function handler() {
+ row.removeEventListener("mousemove", handler);
+ ack("mousemove");
+ });
+ content.synthesizeMouseAtCenter(row, event);
+ },
+
+ click: function (arg) {
+ let eltIdx = typeof(arg) == "object" ? arg.eltIdx : arg;
+ let row;
+ if (eltIdx == -1) {
+ row = gController._table.firstChild;
+ }
+ else {
+ let allElts = [...gController._suggestionsList.children,
+ ...gController._oneOffButtons,
+ content.document.getElementById("contentSearchSettingsButton")];
+ row = allElts[eltIdx];
+ }
+ let event = arg.modifiers || {};
+ // synthesizeMouseAtCenter defaults to sending a mousedown followed by a
+ // mouseup if the event type is not specified.
+ content.synthesizeMouseAtCenter(row, event);
+ ack("click");
+ },
+
+ addInputValueToFormHistory: function () {
+ gController.addInputValueToFormHistory();
+ ack("addInputValueToFormHistory");
+ },
+
+ addDuplicateOneOff: function () {
+ let btn = gController._oneOffButtons[gController._oneOffButtons.length - 1];
+ let newBtn = btn.cloneNode(true);
+ btn.parentNode.appendChild(newBtn);
+ gController._oneOffButtons.push(newBtn);
+ ack("addDuplicateOneOff");
+ },
+
+ removeLastOneOff: function () {
+ gController._oneOffButtons.pop().remove();
+ ack("removeLastOneOff");
+ },
+
+ reset: function () {
+ // Reset both the input and suggestions by select all + delete. If there was
+ // no text entered, this won't have any effect, so also escape to ensure the
+ // suggestions table is closed.
+ gController.input.focus();
+ content.synthesizeKey("a", { accelKey: true });
+ content.synthesizeKey("VK_DELETE", {});
+ content.synthesizeKey("VK_ESCAPE", {});
+ ack("reset");
+ },
+};
+
+function ack(aType, aData) {
+ sendAsyncMessage(TEST_MSG, { type: aType, data: aData || currentState() });
+}
+
+function waitForSuggestions(cb) {
+ let observer = new content.MutationObserver(() => {
+ if (gController.input.getAttribute("aria-expanded") == "true") {
+ observer.disconnect();
+ cb();
+ }
+ });
+ observer.observe(gController.input, {
+ attributes: true,
+ attributeFilter: ["aria-expanded"],
+ });
+}
+
+function waitForContentSearchEvent(messageType, cb) {
+ let mm = content.SpecialPowers.Cc["@mozilla.org/globalmessagemanager;1"].
+ getService(content.SpecialPowers.Ci.nsIMessageListenerManager);
+ mm.addMessageListener("ContentSearch", function listener(aMsg) {
+ if (aMsg.data.type != messageType) {
+ return;
+ }
+ mm.removeMessageListener("ContentSearch", listener);
+ cb(aMsg.data.data);
+ });
+}
+
+function currentState() {
+ let state = {
+ selectedIndex: gController.selectedIndex,
+ selectedButtonIndex: gController.selectedButtonIndex,
+ numSuggestions: gController._table.hidden ? 0 : gController.numSuggestions,
+ suggestionAtIndex: [],
+ isFormHistorySuggestionAtIndex: [],
+
+ tableHidden: gController._table.hidden,
+
+ inputValue: gController.input.value,
+ ariaExpanded: gController.input.getAttribute("aria-expanded"),
+ };
+
+ if (state.numSuggestions) {
+ for (let i = 0; i < gController.numSuggestions; i++) {
+ state.suggestionAtIndex.push(gController.suggestionAtIndex(i));
+ state.isFormHistorySuggestionAtIndex.push(
+ gController.isFormHistorySuggestionAtIndex(i));
+ }
+ }
+
+ return state;
+}
+
+})();
diff --git a/browser/base/content/test/general/content_aboutAccounts.js b/browser/base/content/test/general/content_aboutAccounts.js
new file mode 100644
index 000000000..12ac04934
--- /dev/null
+++ b/browser/base/content/test/general/content_aboutAccounts.js
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 loaded as a "content script" for browser_aboutAccounts tests
+"use strict";
+
+var {interfaces: Ci, utils: Cu} = Components;
+
+addEventListener("load", function load(event) {
+ if (event.target != content.document) {
+ return;
+ }
+// content.document.removeEventListener("load", load, true);
+ sendAsyncMessage("test:document:load");
+ // Opening Sync prefs in tests is a pain as leaks are reported due to the
+ // in-flight promises. For now we just mock the openPrefs() function and have
+ // it send a message back to the test so we know it was called.
+ content.openPrefs = function() {
+ sendAsyncMessage("test:openPrefsCalled");
+ }
+}, true);
+
+addEventListener("DOMContentLoaded", function domContentLoaded(event) {
+ removeEventListener("DOMContentLoaded", domContentLoaded, true);
+ let iframe = content.document.getElementById("remote");
+ if (!iframe) {
+ // at least one test initially loads about:blank - in that case, we are done.
+ return;
+ }
+ // We use DOMContentLoaded here as that fires for our iframe even when we've
+ // arranged for the URL in the iframe to cause an error.
+ addEventListener("DOMContentLoaded", function iframeLoaded(dclEvent) {
+ if (iframe.contentWindow.location.href == "about:blank" ||
+ dclEvent.target != iframe.contentDocument) {
+ return;
+ }
+ removeEventListener("DOMContentLoaded", iframeLoaded, true);
+ sendAsyncMessage("test:iframe:load", {url: iframe.contentDocument.location.href});
+ // And an event listener for the test responses, which we send to the test
+ // via a message.
+ iframe.contentWindow.addEventListener("FirefoxAccountsTestResponse", function (fxAccountsEvent) {
+ sendAsyncMessage("test:response", {data: fxAccountsEvent.detail.data});
+ }, true);
+ }, true);
+}, true);
+
+// Return the visibility state of a list of ids.
+addMessageListener("test:check-visibilities", function (message) {
+ let result = {};
+ for (let id of message.data.ids) {
+ let elt = content.document.getElementById(id);
+ if (elt) {
+ let displayStyle = content.window.getComputedStyle(elt).display;
+ if (displayStyle == 'none') {
+ result[id] = false;
+ } else if (displayStyle == 'block') {
+ result[id] = true;
+ } else {
+ result[id] = "strange: " + displayStyle; // tests should fail!
+ }
+ } else {
+ result[id] = "doesn't exist: " + id;
+ }
+ }
+ sendAsyncMessage("test:check-visibilities-response", result);
+});
+
+addMessageListener("test:load-with-mocked-profile-path", function (message) {
+ addEventListener("DOMContentLoaded", function domContentLoaded(event) {
+ removeEventListener("DOMContentLoaded", domContentLoaded, true);
+ content.getDefaultProfilePath = () => message.data.profilePath;
+ // now wait for the iframe to load.
+ let iframe = content.document.getElementById("remote");
+ iframe.addEventListener("load", function iframeLoaded(loadEvent) {
+ if (iframe.contentWindow.location.href == "about:blank" ||
+ loadEvent.target != iframe) {
+ return;
+ }
+ iframe.removeEventListener("load", iframeLoaded, true);
+ sendAsyncMessage("test:load-with-mocked-profile-path-response",
+ {url: iframe.contentDocument.location.href});
+ }, true);
+ });
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(message.data.url, webNav.LOAD_FLAGS_NONE, null, null, null);
+}, true);
diff --git a/browser/base/content/test/general/contextmenu_common.js b/browser/base/content/test/general/contextmenu_common.js
new file mode 100644
index 000000000..1a0fa931a
--- /dev/null
+++ b/browser/base/content/test/general/contextmenu_common.js
@@ -0,0 +1,324 @@
+var lastElement;
+
+function openContextMenuFor(element, shiftkey, waitForSpellCheck) {
+ // Context menu should be closed before we open it again.
+ is(SpecialPowers.wrap(contextMenu).state, "closed", "checking if popup is closed");
+
+ if (lastElement)
+ lastElement.blur();
+ element.focus();
+
+ // Some elements need time to focus and spellcheck before any tests are
+ // run on them.
+ function actuallyOpenContextMenuFor() {
+ lastElement = element;
+ var eventDetails = { type : "contextmenu", button : 2, shiftKey : shiftkey };
+ synthesizeMouse(element, 2, 2, eventDetails, element.ownerGlobal);
+ }
+
+ if (waitForSpellCheck) {
+ var { onSpellCheck } = SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", {});
+ onSpellCheck(element, actuallyOpenContextMenuFor);
+ }
+ else {
+ actuallyOpenContextMenuFor();
+ }
+}
+
+function closeContextMenu() {
+ contextMenu.hidePopup();
+}
+
+function getVisibleMenuItems(aMenu, aData) {
+ var items = [];
+ var accessKeys = {};
+ for (var i = 0; i < aMenu.childNodes.length; i++) {
+ var item = aMenu.childNodes[i];
+ if (item.hidden)
+ continue;
+
+ var key = item.accessKey;
+ if (key)
+ key = key.toLowerCase();
+
+ var isPageMenuItem = item.hasAttribute("generateditemid");
+
+ if (item.nodeName == "menuitem") {
+ var isGenerated = item.className == "spell-suggestion"
+ || item.className == "sendtab-target";
+ if (isGenerated) {
+ is(item.id, "", "child menuitem #" + i + " is generated");
+ } else if (isPageMenuItem) {
+ is(item.id, "", "child menuitem #" + i + " is a generated page menu item");
+ } else {
+ ok(item.id, "child menuitem #" + i + " has an ID");
+ }
+ var label = item.getAttribute("label");
+ ok(label.length, "menuitem " + item.id + " has a label");
+ if (isGenerated) {
+ is(key, "", "Generated items shouldn't have an access key");
+ items.push("*" + label);
+ } else if (isPageMenuItem) {
+ items.push("+" + label);
+ } else if (item.id.indexOf("spell-check-dictionary-") != 0 &&
+ item.id != "spell-no-suggestions" &&
+ item.id != "spell-add-dictionaries-main" &&
+ item.id != "context-savelinktopocket" &&
+ item.id != "fill-login-saved-passwords" &&
+ item.id != "fill-login-no-logins") {
+ ok(key, "menuitem " + item.id + " has an access key");
+ if (accessKeys[key])
+ ok(false, "menuitem " + item.id + " has same accesskey as " + accessKeys[key]);
+ else
+ accessKeys[key] = item.id;
+ }
+ if (!isGenerated && !isPageMenuItem) {
+ items.push(item.id);
+ }
+ if (isPageMenuItem) {
+ var p = {};
+ p.type = item.getAttribute("type");
+ p.icon = item.getAttribute("image");
+ p.checked = item.hasAttribute("checked");
+ p.disabled = item.hasAttribute("disabled");
+ items.push(p);
+ } else {
+ items.push(!item.disabled);
+ }
+ } else if (item.nodeName == "menuseparator") {
+ ok(true, "--- seperator id is " + item.id);
+ items.push("---");
+ items.push(null);
+ } else if (item.nodeName == "menu") {
+ if (isPageMenuItem) {
+ item.id = "generated-submenu-" + aData.generatedSubmenuId++;
+ }
+ ok(item.id, "child menu #" + i + " has an ID");
+ if (!isPageMenuItem) {
+ ok(key, "menu has an access key");
+ if (accessKeys[key])
+ ok(false, "menu " + item.id + " has same accesskey as " + accessKeys[key]);
+ else
+ accessKeys[key] = item.id;
+ }
+ items.push(item.id);
+ items.push(!item.disabled);
+ // Add a dummy item so that the indexes in checkMenu are the same
+ // for expectedItems and actualItems.
+ items.push([]);
+ items.push(null);
+ } else if (item.nodeName == "menugroup") {
+ ok(item.id, "child menugroup #" + i + " has an ID");
+ items.push(item.id);
+ items.push(!item.disabled);
+ var menugroupChildren = [];
+ for (var child of item.children) {
+ if (child.hidden)
+ continue;
+
+ menugroupChildren.push([child.id, !child.disabled]);
+ }
+ items.push(menugroupChildren);
+ items.push(null);
+ } else {
+ ok(false, "child #" + i + " of menu ID " + aMenu.id +
+ " has an unknown type (" + item.nodeName + ")");
+ }
+ }
+ return items;
+}
+
+function checkContextMenu(expectedItems) {
+ is(contextMenu.state, "open", "checking if popup is open");
+ var data = { generatedSubmenuId: 1 };
+ checkMenu(contextMenu, expectedItems, data);
+}
+
+function checkMenuItem(actualItem, actualEnabled, expectedItem, expectedEnabled, index) {
+ is(actualItem, expectedItem,
+ "checking item #" + index/2 + " (" + expectedItem + ") name");
+
+ if (typeof expectedEnabled == "object" && expectedEnabled != null ||
+ typeof actualEnabled == "object" && actualEnabled != null) {
+
+ ok(!(actualEnabled == null), "actualEnabled is not null");
+ ok(!(expectedEnabled == null), "expectedEnabled is not null");
+ is(typeof actualEnabled, typeof expectedEnabled, "checking types");
+
+ if (typeof actualEnabled != typeof expectedEnabled ||
+ actualEnabled == null || expectedEnabled == null)
+ return;
+
+ is(actualEnabled.type, expectedEnabled.type,
+ "checking item #" + index/2 + " (" + expectedItem + ") type attr value");
+ var icon = actualEnabled.icon;
+ if (icon) {
+ var tmp = "";
+ var j = icon.length - 1;
+ while (j && icon[j] != "/") {
+ tmp = icon[j--] + tmp;
+ }
+ icon = tmp;
+ }
+ is(icon, expectedEnabled.icon,
+ "checking item #" + index/2 + " (" + expectedItem + ") icon attr value");
+ is(actualEnabled.checked, expectedEnabled.checked,
+ "checking item #" + index/2 + " (" + expectedItem + ") has checked attr");
+ is(actualEnabled.disabled, expectedEnabled.disabled,
+ "checking item #" + index/2 + " (" + expectedItem + ") has disabled attr");
+ } else if (expectedEnabled != null)
+ is(actualEnabled, expectedEnabled,
+ "checking item #" + index/2 + " (" + expectedItem + ") enabled state");
+}
+
+/*
+ * checkMenu - checks to see if the specified <menupopup> contains the
+ * expected items and state.
+ * expectedItems is a array of (1) item IDs and (2) a boolean specifying if
+ * the item is enabled or not (or null to ignore it). Submenus can be checked
+ * by providing a nested array entry after the expected <menu> ID.
+ * For example: ["blah", true, // item enabled
+ * "submenu", null, // submenu
+ * ["sub1", true, // submenu contents
+ * "sub2", false], null, // submenu contents
+ * "lol", false] // item disabled
+ *
+ */
+function checkMenu(menu, expectedItems, data) {
+ var actualItems = getVisibleMenuItems(menu, data);
+ // ok(false, "Items are: " + actualItems);
+ for (var i = 0; i < expectedItems.length; i+=2) {
+ var actualItem = actualItems[i];
+ var actualEnabled = actualItems[i + 1];
+ var expectedItem = expectedItems[i];
+ var expectedEnabled = expectedItems[i + 1];
+ if (expectedItem instanceof Array) {
+ ok(true, "Checking submenu/menugroup...");
+ var previousId = expectedItems[i - 2]; // The last item was the menu ID.
+ var previousItem = menu.getElementsByAttribute("id", previousId)[0];
+ ok(previousItem, (previousItem ? previousItem.nodeName : "item") + " with previous id (" + previousId + ") found");
+ if (previousItem && previousItem.nodeName == "menu") {
+ ok(previousItem, "got a submenu element of id='" + previousId + "'");
+ is(previousItem.nodeName, "menu", "submenu element of id='" + previousId +
+ "' has expected nodeName");
+ checkMenu(previousItem.menupopup, expectedItem, data, i);
+ } else if (previousItem && previousItem.nodeName == "menugroup") {
+ ok(expectedItem.length, "menugroup must not be empty");
+ for (var j = 0; j < expectedItem.length / 2; j++) {
+ checkMenuItem(actualItems[i][j][0], actualItems[i][j][1], expectedItem[j*2], expectedItem[j*2+1], i+j*2);
+ }
+ i += j;
+ } else {
+ ok(false, "previous item is not a menu or menugroup");
+ }
+ } else {
+ checkMenuItem(actualItem, actualEnabled, expectedItem, expectedEnabled, i);
+ }
+ }
+ // Could find unexpected extra items at the end...
+ is(actualItems.length, expectedItems.length, "checking expected number of menu entries");
+}
+
+let lastElementSelector = null;
+/**
+ * Right-clicks on the element that matches `selector` and checks the
+ * context menu that appears against the `menuItems` array.
+ *
+ * @param {String} selector
+ * A selector passed to querySelector to find
+ * the element that will be referenced.
+ * @param {Array} menuItems
+ * An array of menuitem ids and their associated enabled state. A state
+ * of null means that it will be ignored. Ids of '---' are used for
+ * menuseparators.
+ * @param {Object} options, optional
+ * skipFocusChange: don't move focus to the element before test, useful
+ * if you want to delay spell-check initialization
+ * offsetX: horizontal mouse offset from the top-left corner of
+ * the element, optional
+ * offsetY: vertical mouse offset from the top-left corner of the
+ * element, optional
+ * centered: if true, mouse position is centered in element, defaults
+ * to true if offsetX and offsetY are not provided
+ * waitForSpellCheck: wait until spellcheck is initialized before
+ * starting test
+ * preCheckContextMenuFn: callback to run before opening menu
+ * onContextMenuShown: callback to run when the context menu is shown
+ * postCheckContextMenuFn: callback to run after opening menu
+ * @return {Promise} resolved after the test finishes
+ */
+function* test_contextmenu(selector, menuItems, options={}) {
+ contextMenu = document.getElementById("contentAreaContextMenu");
+ is(contextMenu.state, "closed", "checking if popup is closed");
+
+ // Default to centered if no positioning is defined.
+ if (!options.offsetX && !options.offsetY) {
+ options.centered = true;
+ }
+
+ if (!options.skipFocusChange) {
+ yield ContentTask.spawn(gBrowser.selectedBrowser,
+ [lastElementSelector, selector],
+ function*([contentLastElementSelector, contentSelector]) {
+ if (contentLastElementSelector) {
+ let contentLastElement = content.document.querySelector(contentLastElementSelector);
+ contentLastElement.blur();
+ }
+ let element = content.document.querySelector(contentSelector);
+ element.focus();
+ });
+ lastElementSelector = selector;
+ info(`Moved focus to ${selector}`);
+ }
+
+ if (options.preCheckContextMenuFn) {
+ yield options.preCheckContextMenuFn();
+ info("Completed preCheckContextMenuFn");
+ }
+
+ if (options.waitForSpellCheck) {
+ info("Waiting for spell check");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, selector, function*(contentSelector) {
+ let {onSpellCheck} = Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", {});
+ let element = content.document.querySelector(contentSelector);
+ yield new Promise(resolve => onSpellCheck(element, resolve));
+ info("Spell check running");
+ });
+ }
+
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouse(selector, options.offsetX || 0, options.offsetY || 0, {
+ type: "contextmenu",
+ button: 2,
+ shiftkey: options.shiftkey,
+ centered: options.centered
+ },
+ gBrowser.selectedBrowser);
+ yield awaitPopupShown;
+ info("Popup Shown");
+
+ if (options.onContextMenuShown) {
+ yield options.onContextMenuShown();
+ info("Completed onContextMenuShown");
+ }
+
+ if (menuItems) {
+ if (Services.prefs.getBoolPref("devtools.inspector.enabled")) {
+ let inspectItems = ["---", null,
+ "context-inspect", true];
+ menuItems = menuItems.concat(inspectItems);
+ }
+
+ checkContextMenu(menuItems);
+ }
+
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+
+ if (options.postCheckContextMenuFn) {
+ yield options.postCheckContextMenuFn();
+ info("Completed postCheckContextMenuFn");
+ }
+
+ contextMenu.hidePopup();
+ yield awaitPopupHidden;
+}
diff --git a/browser/base/content/test/general/ctxmenu-image.png b/browser/base/content/test/general/ctxmenu-image.png
new file mode 100644
index 000000000..4c3be5084
--- /dev/null
+++ b/browser/base/content/test/general/ctxmenu-image.png
Binary files differ
diff --git a/browser/base/content/test/general/discovery.html b/browser/base/content/test/general/discovery.html
new file mode 100644
index 000000000..1679e6545
--- /dev/null
+++ b/browser/base/content/test/general/discovery.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+ <head id="linkparent">
+ <title>Autodiscovery Test</title>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/download_page.html b/browser/base/content/test/general/download_page.html
new file mode 100644
index 000000000..4f9154033
--- /dev/null
+++ b/browser/base/content/test/general/download_page.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=676619
+-->
+ <head>
+ <title>Test for the download attribute</title>
+
+ </head>
+ <body>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=676619">Bug 676619</a>
+ <br/>
+ <ul>
+ <li><a href="data:text/plain,Hey What are you looking for?"
+ download="test.txt" id="link1">Download "test.txt"</a></li>
+ <li><a href="video.ogg"
+ download id="link2">Download "video.ogg"</a></li>
+ <li><a href="video.ogg"
+ download="just some video" id="link3">Download "just some video"</a></li>
+ <li><a href="data:text/plain,test"
+ download="with-target.txt" id="link4">Download "with-target.txt"</a></li>
+ <li><a href="javascript:(1+2)+''"
+ download="javascript.txt" id="link5">Download "javascript.txt"</a></li>
+ </ul>
+ <script>
+ var li = document.createElement('li');
+ var a = document.createElement('a');
+
+ a.href = window.URL.createObjectURL(new Blob(["just text"])) ;
+ a.download = "test.blob";
+ a.id = "link6";
+ a.textContent = 'Download "test.blob"';
+
+ li.appendChild(a);
+ document.getElementsByTagName('ul')[0].appendChild(li);
+
+ window.addEventListener("beforeunload", function (evt) {
+ document.getElementById("unload-flag").textContent = "Fail";
+ });
+ </script>
+ <ul>
+ <li><a href="http://example.com/"
+ download="example.com" id="link7" target="_blank">Download "example.com"</a></li>
+ <ul>
+ <div id="unload-flag">Okay</div>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/dummy_page.html b/browser/base/content/test/general/dummy_page.html
new file mode 100644
index 000000000..1a87e2840
--- /dev/null
+++ b/browser/base/content/test/general/dummy_page.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/browser/base/content/test/general/feed_discovery.html b/browser/base/content/test/general/feed_discovery.html
new file mode 100644
index 000000000..baecba19b
--- /dev/null
+++ b/browser/base/content/test/general/feed_discovery.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=377611
+-->
+ <head>
+ <title>Test for feed discovery</title>
+ <meta charset="utf-8">
+
+ <!-- Straight up standard -->
+ <link rel="alternate" type="application/atom+xml" title="1" href="/1.atom" />
+ <link rel="alternate" type="application/rss+xml" title="2" href="/2.rss" />
+ <link rel="feed" title="3" href="/3.xml" />
+
+ <!-- rel is a space-separated list -->
+ <link rel=" alternate " type="application/atom+xml" title="4" href="/4.atom" />
+ <link rel="foo alternate" type="application/atom+xml" title="5" href="/5.atom" />
+ <link rel="alternate foo" type="application/atom+xml" title="6" href="/6.atom" />
+ <link rel="foo alternate foo" type="application/atom+xml" title="7" href="/7.atom" />
+ <link rel="meat feed cake" title="8" href="/8.atom" />
+
+ <!-- rel is case-insensitive -->
+ <link rel="ALTERNate" type="application/atom+xml" title="9" href="/9.atom" />
+ <link rel="fEEd" title="10" href="/10.atom" />
+
+ <!-- type can have leading and trailing whitespace -->
+ <link rel="alternate" type=" application/atom+xml " title="11" href="/11.atom" />
+
+ <!-- type is case-insensitive -->
+ <link rel="alternate" type="aPPliCAtion/ATom+xML" title="12" href="/12.atom" />
+
+ <!-- "feed stylesheet" is a feed, though "alternate stylesheet" isn't -->
+ <link rel="feed stylesheet" title="13" href="/13.atom" />
+
+ <!-- hyphens or letters around rel not allowed -->
+ <link rel="disabled-alternate" type="application/atom+xml" title="Bogus1" href="/Bogus1" />
+ <link rel="alternates" type="application/atom+xml" title="Bogus2" href="/Bogus2" />
+ <link rel=" alternate-like" type="application/atom+xml" title="Bogus3" href="/Bogus3" />
+
+ <!-- don't tolerate text/xml if title includes 'rss' not as a word -->
+ <link rel="alternate" type="text/xml" title="Bogus4 scissorsshaped" href="/Bogus4" />
+
+ <!-- don't tolerate application/xml if title includes 'rss' not as a word -->
+ <link rel="alternate" type="application/xml" title="Bogus5 scissorsshaped" href="/Bogus5" />
+
+ <!-- don't tolerate application/rdf+xml if title includes 'rss' not as a word -->
+ <link rel="alternate" type="application/rdf+xml" title="Bogus6 scissorsshaped" href="/Bogus6" />
+
+ <!-- don't tolerate random types -->
+ <link rel="alternate" type="text/plain" title="Bogus7 rss" href="/Bogus7" />
+
+ <!-- don't find Atom by title -->
+ <link rel="foopy" type="application/atom+xml" title="Bogus8 Atom and RSS" href="/Bogus8" />
+
+ <!-- don't find application/rss+xml by title -->
+ <link rel="goats" type="application/rss+xml" title="Bogus9 RSS and Atom" href="/Bogus9" />
+
+ <!-- don't find application/rdf+xml by title -->
+ <link rel="alternate" type="application/rdf+xml" title="Bogus10 RSS and Atom" href="/Bogus10" />
+
+ <!-- don't find application/xml by title -->
+ <link rel="alternate" type="application/xml" title="Bogus11 RSS and Atom" href="/Bogus11" />
+
+ <!-- don't find text/xml by title -->
+ <link rel="alternate" type="text/xml" title="Bogus12 RSS and Atom" href="/Bogus12" />
+
+ <!-- alternate and stylesheet isn't a feed -->
+ <link rel="alternate stylesheet" type="application/rss+xml" title="Bogus13 RSS" href="/Bogus13" />
+ </head>
+ <body>
+ </body>
+</html>
+
diff --git a/browser/base/content/test/general/feed_tab.html b/browser/base/content/test/general/feed_tab.html
new file mode 100644
index 000000000..50903f48b
--- /dev/null
+++ b/browser/base/content/test/general/feed_tab.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=458579
+-->
+ <head>
+ <title>Test for page info feeds tab</title>
+
+ <!-- Straight up standard -->
+ <link rel="alternate" type="application/atom+xml" title="1" href="/1.atom" />
+ <link rel="alternate" type="application/rss+xml" title="2" href="/2.rss" />
+ <link rel="feed" title="3" href="/3.xml" />
+
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/file_bug1045809_1.html b/browser/base/content/test/general/file_bug1045809_1.html
new file mode 100644
index 000000000..9baf2d45d
--- /dev/null
+++ b/browser/base/content/test/general/file_bug1045809_1.html
@@ -0,0 +1,7 @@
+<html>
+ <head>
+ </head>
+ <body>
+ <iframe src="http://test1.example.com/browser/browser/base/content/test/general/file_bug1045809_2.html"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/file_bug1045809_2.html b/browser/base/content/test/general/file_bug1045809_2.html
new file mode 100644
index 000000000..67a297dbc
--- /dev/null
+++ b/browser/base/content/test/general/file_bug1045809_2.html
@@ -0,0 +1,7 @@
+<html>
+ <head>
+ </head>
+ <body>
+ <div id="mixedContentContainer">Mixed Content is here</div>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/file_bug822367_1.html b/browser/base/content/test/general/file_bug822367_1.html
new file mode 100644
index 000000000..62f42d226
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 1 for Mixed Content Blocker User Override - Mixed Script
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 822367</title>
+</head>
+<body>
+ <div id="testContent">
+ <p id="p1"></p>
+ </div>
+ <script src="http://example.com/browser/browser/base/content/test/general/file_bug822367_1.js">
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug822367_1.js b/browser/base/content/test/general/file_bug822367_1.js
new file mode 100644
index 000000000..175de363b
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_1.js
@@ -0,0 +1 @@
+document.getElementById('p1').innerHTML="hello";
diff --git a/browser/base/content/test/general/file_bug822367_2.html b/browser/base/content/test/general/file_bug822367_2.html
new file mode 100644
index 000000000..fe56ee213
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_2.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 2 for Mixed Content Blocker User Override - Mixed Display
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 822367 - Mixed Display</title>
+</head>
+<body>
+ <div id="testContent">
+ <img src="http://example.com/tests/image/test/mochitest/blue.png">
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug822367_3.html b/browser/base/content/test/general/file_bug822367_3.html
new file mode 100644
index 000000000..c1ff2c000
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_3.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 3 for Mixed Content Blocker User Override - Mixed Script and Display
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 3 for Bug 822367</title>
+ <script>
+ function foo() {
+ var x = document.createElement('p');
+ x.setAttribute("id", "p2");
+ x.innerHTML = "bye";
+ document.getElementById("testContent").appendChild(x);
+ }
+ </script>
+</head>
+<body>
+ <div id="testContent">
+ <p id="p1"></p>
+ <img src="http://example.com/tests/image/test/mochitest/blue.png" onload="foo()">
+ </div>
+ <script src="http://example.com/browser/browser/base/content/test/general/file_bug822367_1.js">
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug822367_4.html b/browser/base/content/test/general/file_bug822367_4.html
new file mode 100644
index 000000000..9a073143f
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_4.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 4 for Mixed Content Blocker User Override - Mixed Script and Display
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 4 for Bug 822367</title>
+</head>
+<body>
+ <div id="testContent">
+ <p id="p1"></p>
+ </div>
+ <script src="http://example.com/browser/browser/base/content/test/general/file_bug822367_4.js">
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug822367_4.js b/browser/base/content/test/general/file_bug822367_4.js
new file mode 100644
index 000000000..301db89c7
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_4.js
@@ -0,0 +1 @@
+document.location = "https://example.com/browser/browser/base/content/test/general/file_bug822367_4B.html";
diff --git a/browser/base/content/test/general/file_bug822367_4B.html b/browser/base/content/test/general/file_bug822367_4B.html
new file mode 100644
index 000000000..76ea2b623
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_4B.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 4B for Mixed Content Blocker User Override - Location Changed
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 4B Location Change for Bug 822367</title>
+</head>
+<body>
+ <div id="testContent">
+ <p id="p1"></p>
+ </div>
+ <script src="http://example.com/browser/browser/base/content/test/general/file_bug822367_1.js">
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug822367_5.html b/browser/base/content/test/general/file_bug822367_5.html
new file mode 100644
index 000000000..3c9a9317e
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_5.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 5 for Mixed Content Blocker User Override - Mixed Script in document.open()
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 5 for Bug 822367</title>
+ <script>
+ function createDoc()
+ {
+ var doc=document.open("text/html", "replace");
+ doc.write('<!DOCTYPE html><html><body><p id="p1">This is some content</p><script src="http://example.com/browser/browser/base/content/test/general/file_bug822367_1.js">\<\/script\>\<\/body>\<\/html>');
+ doc.close();
+ }
+ </script>
+</head>
+<body>
+ <div id="testContent">
+ <img src="https://example.com/tests/image/test/mochitest/blue.png" onload="createDoc()">
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug822367_6.html b/browser/base/content/test/general/file_bug822367_6.html
new file mode 100644
index 000000000..baa5674c2
--- /dev/null
+++ b/browser/base/content/test/general/file_bug822367_6.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 6 for Mixed Content Blocker User Override - Mixed Script in document.open() within an iframe
+https://bugzilla.mozilla.org/show_bug.cgi?id=822367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 6 for Bug 822367</title>
+</head>
+<body>
+ <div id="testContent">
+ <iframe name="f1" id="f1" src="https://example.com/browser/browser/base/content/test/general/file_bug822367_5.html"></iframe>
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug902156.js b/browser/base/content/test/general/file_bug902156.js
new file mode 100644
index 000000000..f943dd628
--- /dev/null
+++ b/browser/base/content/test/general/file_bug902156.js
@@ -0,0 +1,5 @@
+/*
+ * Once the mixed content blocker is disabled for the page, this scripts loads
+ * and updates the text inside the div container.
+ */
+document.getElementById("mctestdiv").innerHTML = "Mixed Content Blocker disabled";
diff --git a/browser/base/content/test/general/file_bug902156_1.html b/browser/base/content/test/general/file_bug902156_1.html
new file mode 100644
index 000000000..e3625de99
--- /dev/null
+++ b/browser/base/content/test/general/file_bug902156_1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 902156 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=902156
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 902156</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test1.example.com/browser/browser/base/content/test/general/file_bug902156.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug902156_2.html b/browser/base/content/test/general/file_bug902156_2.html
new file mode 100644
index 000000000..25aff3349
--- /dev/null
+++ b/browser/base/content/test/general/file_bug902156_2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 2 for Bug 902156 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=902156
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 902156</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <a href="https://test2.example.com/browser/browser/base/content/test/general/file_bug902156_1.html"
+ id="mctestlink" target="_top">Go to http site</a>
+ <script src="http://test2.example.com/browser/browser/base/content/test/general/file_bug902156.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug902156_3.html b/browser/base/content/test/general/file_bug902156_3.html
new file mode 100644
index 000000000..65805adff
--- /dev/null
+++ b/browser/base/content/test/general/file_bug902156_3.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3 for Bug 902156 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=902156
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 3 for Bug 902156</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test1.example.com/browser/browser/base/content/test/general/file_bug902156.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug906190.js b/browser/base/content/test/general/file_bug906190.js
new file mode 100644
index 000000000..f943dd628
--- /dev/null
+++ b/browser/base/content/test/general/file_bug906190.js
@@ -0,0 +1,5 @@
+/*
+ * Once the mixed content blocker is disabled for the page, this scripts loads
+ * and updates the text inside the div container.
+ */
+document.getElementById("mctestdiv").innerHTML = "Mixed Content Blocker disabled";
diff --git a/browser/base/content/test/general/file_bug906190.sjs b/browser/base/content/test/general/file_bug906190.sjs
new file mode 100644
index 000000000..bff126874
--- /dev/null
+++ b/browser/base/content/test/general/file_bug906190.sjs
@@ -0,0 +1,17 @@
+function handleRequest(request, response) {
+ var page = "<!DOCTYPE html><html><body>bug 906190</body></html>";
+ var path = "https://test1.example.com/browser/browser/base/content/test/general/";
+ var url;
+
+ if (request.queryString.includes('bad-redirection=1')) {
+ url = path + "this_page_does_not_exist.html";
+ } else {
+ url = path + "file_bug906190_redirected.html";
+ }
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "302", "Found");
+ response.setHeader("Location", url, false);
+ response.write(page);
+}
diff --git a/browser/base/content/test/general/file_bug906190_1.html b/browser/base/content/test/general/file_bug906190_1.html
new file mode 100644
index 000000000..cbb3cac26
--- /dev/null
+++ b/browser/base/content/test/general/file_bug906190_1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 906190 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=906190
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 906190</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test1.example.com/browser/browser/base/content/test/general/file_bug906190.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug906190_2.html b/browser/base/content/test/general/file_bug906190_2.html
new file mode 100644
index 000000000..70c7c61cf
--- /dev/null
+++ b/browser/base/content/test/general/file_bug906190_2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 2 for Bug 906190 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=906190
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 906190</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test2.example.com/browser/browser/base/content/test/general/file_bug906190.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug906190_3_4.html b/browser/base/content/test/general/file_bug906190_3_4.html
new file mode 100644
index 000000000..aea6648a9
--- /dev/null
+++ b/browser/base/content/test/general/file_bug906190_3_4.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3 and 4 for Bug 906190 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=906190
+-->
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="refresh" content="0; url=https://test1.example.com/browser/browser/base/content/test/general/file_bug906190_redirected.html">
+ <title>Test 3 and 4 for Bug 906190</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug906190_redirected.html b/browser/base/content/test/general/file_bug906190_redirected.html
new file mode 100644
index 000000000..cc324bd25
--- /dev/null
+++ b/browser/base/content/test/general/file_bug906190_redirected.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Redirected Page of Test 3 to 6 for Bug 906190 - See file browser_bug902156.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=906190
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Redirected Page for Bug 906190</title>
+</head>
+<body>
+ <div id="mctestdiv">Mixed Content Blocker enabled</div>
+ <script src="http://test1.example.com/browser/browser/base/content/test/general/file_bug906190.js" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug970276_favicon1.ico b/browser/base/content/test/general/file_bug970276_favicon1.ico
new file mode 100644
index 000000000..d44438903
--- /dev/null
+++ b/browser/base/content/test/general/file_bug970276_favicon1.ico
Binary files differ
diff --git a/browser/base/content/test/general/file_bug970276_favicon2.ico b/browser/base/content/test/general/file_bug970276_favicon2.ico
new file mode 100644
index 000000000..d44438903
--- /dev/null
+++ b/browser/base/content/test/general/file_bug970276_favicon2.ico
Binary files differ
diff --git a/browser/base/content/test/general/file_bug970276_popup1.html b/browser/base/content/test/general/file_bug970276_popup1.html
new file mode 100644
index 000000000..5ce7dab87
--- /dev/null
+++ b/browser/base/content/test/general/file_bug970276_popup1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bug 970276.</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_bug970276_favicon1.ico">
+</head>
+<body>
+ Test file for bug 970276.
+
+ <iframe src="file_bug970276_popup2.html">
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_bug970276_popup2.html b/browser/base/content/test/general/file_bug970276_popup2.html
new file mode 100644
index 000000000..0b9e5294e
--- /dev/null
+++ b/browser/base/content/test/general/file_bug970276_popup2.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bug 970276.</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_bug970276_favicon2.ico">
+</head>
+<body>
+ Test inner file for bug 970276.
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_csp_block_all_mixedcontent.html b/browser/base/content/test/general/file_csp_block_all_mixedcontent.html
new file mode 100644
index 000000000..93a7f13d9
--- /dev/null
+++ b/browser/base/content/test/general/file_csp_block_all_mixedcontent.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html><head><meta charset="utf-8">
+<title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>
+</head>
+<meta http-equiv="Content-Security-Policy" content="block-all-mixed-content">
+<body>
+<script src="http://example.com/browser/browser/base/content/test/general/file_csp_block_all_mixedcontent.js"/>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_csp_block_all_mixedcontent.js b/browser/base/content/test/general/file_csp_block_all_mixedcontent.js
new file mode 100644
index 000000000..dc6d6a64e
--- /dev/null
+++ b/browser/base/content/test/general/file_csp_block_all_mixedcontent.js
@@ -0,0 +1,3 @@
+// empty script file just used for testing Bug 1122236.
+// Making sure the UI is not degraded when blocking
+// mixed content using the CSP directive: block-all-mixed-content.
diff --git a/browser/base/content/test/general/file_documentnavigation_frameset.html b/browser/base/content/test/general/file_documentnavigation_frameset.html
new file mode 100644
index 000000000..beb01addf
--- /dev/null
+++ b/browser/base/content/test/general/file_documentnavigation_frameset.html
@@ -0,0 +1,12 @@
+<html id="outer">
+
+<frameset rows="30%, 70%">
+ <frame src="data:text/html,&lt;html id='htmlframe1' &gt;&lt;body id='framebody1'&gt;&lt;input id='i1'&gt;&lt;body&gt;&lt;/html&gt;">
+ <frameset cols="30%, 33%, 34%">
+ <frame src="data:text/html,&lt;html id='htmlframe2'&gt;&lt;body id='framebody2'&gt;&lt;input id='i2'&gt;&lt;body&gt;&lt;/html&gt;">
+ <frame src="data:text/html,&lt;html id='htmlframe3'&gt;&lt;body id='framebody3'&gt;&lt;input id='i3'&gt;&lt;body&gt;&lt;/html&gt;">
+ <frame src="data:text/html,&lt;html id='htmlframe4'&gt;&lt;body id='framebody4'&gt;&lt;input id='i4'&gt;&lt;body&gt;&lt;/html&gt;">
+ </frameset>
+</frameset>
+
+</html>
diff --git a/browser/base/content/test/general/file_double_close_tab.html b/browser/base/content/test/general/file_double_close_tab.html
new file mode 100644
index 000000000..0bead5efc
--- /dev/null
+++ b/browser/base/content/test/general/file_double_close_tab.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test page that blocks beforeunload. Used in tests for bug 1050638 and bug 305085</title>
+ </head>
+ <body>
+ This page will block beforeunload. It should still be user-closable at all times.
+ <script>
+ window.onbeforeunload = function() {
+ return "stop";
+ };
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/file_favicon_change.html b/browser/base/content/test/general/file_favicon_change.html
new file mode 100644
index 000000000..18ac6526b
--- /dev/null
+++ b/browser/base/content/test/general/file_favicon_change.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html><head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <link rel="icon" href="http://example.org/one-icon" type="image/ico" id="i">
+</head>
+<body>
+ <script>
+ window.addEventListener("PleaseChangeFavicon", function() {
+ var ico = document.getElementById("i");
+ ico.setAttribute("href", "http://example.org/other-icon");
+ });
+ </script>
+</body></html>
diff --git a/browser/base/content/test/general/file_favicon_change_not_in_document.html b/browser/base/content/test/general/file_favicon_change_not_in_document.html
new file mode 100644
index 000000000..deebb07dc
--- /dev/null
+++ b/browser/base/content/test/general/file_favicon_change_not_in_document.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html><head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <link rel="icon" href="http://example.org/one-icon" type="image/ico" id="i">
+</head>
+<body onload="onload()">
+ <script>
+ function onload() {
+ var ico = document.createElement("link");
+ ico.setAttribute("rel", "icon");
+ ico.setAttribute("type", "image/ico");
+ ico.setAttribute("href", "http://example.org/other-icon");
+ setTimeout(function() {
+ ico.setAttribute("href", "http://example.org/yet-another-icon");
+ document.getElementById("i").remove();
+ document.head.appendChild(ico);
+ }, 1000);
+ }
+ </script>
+</body></html>
+
diff --git a/browser/base/content/test/general/file_fullscreen-window-open.html b/browser/base/content/test/general/file_fullscreen-window-open.html
new file mode 100644
index 000000000..1584f4c98
--- /dev/null
+++ b/browser/base/content/test/general/file_fullscreen-window-open.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test for window.open() when browser is in fullscreen</title>
+ </head>
+ <body>
+ <script>
+ window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad, true);
+
+ document.getElementById("test").addEventListener("click", onClick, true);
+ }, true);
+
+ function onClick(aEvent) {
+ aEvent.preventDefault();
+
+ var dataStr = aEvent.target.getAttribute("data-test-param");
+ var data = JSON.parse(dataStr);
+ window.open(data.uri, data.title, data.option);
+ }
+ </script>
+ <a id="test" href="" data-test-param="">Test</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/file_generic_favicon.ico b/browser/base/content/test/general/file_generic_favicon.ico
new file mode 100644
index 000000000..d44438903
--- /dev/null
+++ b/browser/base/content/test/general/file_generic_favicon.ico
Binary files differ
diff --git a/browser/base/content/test/general/file_mediaPlayback.html b/browser/base/content/test/general/file_mediaPlayback.html
new file mode 100644
index 000000000..a6979287e
--- /dev/null
+++ b/browser/base/content/test/general/file_mediaPlayback.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<audio src="audio.ogg" controls loop>
diff --git a/browser/base/content/test/general/file_mixedContentFramesOnHttp.html b/browser/base/content/test/general/file_mixedContentFramesOnHttp.html
new file mode 100644
index 000000000..3bd16aea5
--- /dev/null
+++ b/browser/base/content/test/general/file_mixedContentFramesOnHttp.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for https://bugzilla.mozilla.org/show_bug.cgi?id=1182551
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1182551</title>
+</head>
+<body>
+ <p>Test for Bug 1182551. This is an HTTP top level page. We include an HTTPS iframe that loads mixed passive content.</p>
+ <iframe src="https://example.org/browser/browser/base/content/test/general/file_mixedPassiveContent.html"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_mixedContentFromOnunload.html b/browser/base/content/test/general/file_mixedContentFromOnunload.html
new file mode 100644
index 000000000..fb28a2889
--- /dev/null
+++ b/browser/base/content/test/general/file_mixedContentFromOnunload.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for https://bugzilla.mozilla.org/show_bug.cgi?id=947079
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 947079</title>
+</head>
+<body>
+ <p>Test for Bug 947079</p>
+ <script>
+ window.addEventListener('unload', function() {
+ new Image().src = 'http://mochi.test:8888/tests/image/test/mochitest/blue.png';
+ });
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_mixedContentFromOnunload_test1.html b/browser/base/content/test/general/file_mixedContentFromOnunload_test1.html
new file mode 100644
index 000000000..1d027b036
--- /dev/null
+++ b/browser/base/content/test/general/file_mixedContentFromOnunload_test1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 1 for https://bugzilla.mozilla.org/show_bug.cgi?id=947079
+Page with no insecure subresources
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 947079</title>
+</head>
+<body>
+ <p>There are no insecure resource loads on this page</p>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_mixedContentFromOnunload_test2.html b/browser/base/content/test/general/file_mixedContentFromOnunload_test2.html
new file mode 100644
index 000000000..4813337cc
--- /dev/null
+++ b/browser/base/content/test/general/file_mixedContentFromOnunload_test2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test 2 for https://bugzilla.mozilla.org/show_bug.cgi?id=947079
+Page with an insecure image load
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 947079</title>
+</head>
+<body>
+ <p>Page with http image load</p>
+ <img src="http://test2.example.com/tests/image/test/mochitest/blue.png">
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_mixedPassiveContent.html b/browser/base/content/test/general/file_mixedPassiveContent.html
new file mode 100644
index 000000000..a60ac94e8
--- /dev/null
+++ b/browser/base/content/test/general/file_mixedPassiveContent.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for https://bugzilla.mozilla.org/show_bug.cgi?id=1182551
+-->
+<head>
+ <meta charset="utf-8">
+ <title>HTTPS page with HTTP image</title>
+</head>
+<body>
+ <img src="http://mochi.test:8888/tests/image/test/mochitest/blue.png">
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_trackingUI_6.html b/browser/base/content/test/general/file_trackingUI_6.html
new file mode 100644
index 000000000..52e1ae63f
--- /dev/null
+++ b/browser/base/content/test/general/file_trackingUI_6.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Testing the shield from fetch and XHR</title>
+</head>
+<body>
+ <p>Hello there!</p>
+ <script type="application/javascript; version=1.8">
+ function test_fetch() {
+ let url = "http://trackertest.org/browser/browser/base/content/test/general/file_trackingUI_6.js";
+ return fetch(url);
+ }
+ </script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/file_trackingUI_6.js b/browser/base/content/test/general/file_trackingUI_6.js
new file mode 100644
index 000000000..f7ac687cf
--- /dev/null
+++ b/browser/base/content/test/general/file_trackingUI_6.js
@@ -0,0 +1,2 @@
+/* Some code goes here! */
+void 0;
diff --git a/browser/base/content/test/general/file_trackingUI_6.js^headers^ b/browser/base/content/test/general/file_trackingUI_6.js^headers^
new file mode 100644
index 000000000..cb762eff8
--- /dev/null
+++ b/browser/base/content/test/general/file_trackingUI_6.js^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/browser/base/content/test/general/file_with_favicon.html b/browser/base/content/test/general/file_with_favicon.html
new file mode 100644
index 000000000..0702b4aab
--- /dev/null
+++ b/browser/base/content/test/general/file_with_favicon.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test file for bugs with favicons</title>
+
+ <!--Set a favicon; that's the whole point of this file.-->
+ <link rel="icon" href="file_generic_favicon.ico">
+</head>
+<body>
+ Test file for bugs with favicons
+</body>
+</html>
diff --git a/browser/base/content/test/general/fxa_profile_handler.sjs b/browser/base/content/test/general/fxa_profile_handler.sjs
new file mode 100644
index 000000000..7160b76d0
--- /dev/null
+++ b/browser/base/content/test/general/fxa_profile_handler.sjs
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is basically an echo server!
+// We just grab responseStatus and responseBody query params!
+
+function reallyHandleRequest(request, response) {
+ var query = "?" + request.queryString;
+
+ var responseStatus = 200;
+ var match = /responseStatus=([^&]*)/.exec(query);
+ if (match) {
+ responseStatus = parseInt(match[1]);
+ }
+
+ var responseBody = "";
+ match = /responseBody=([^&]*)/.exec(query);
+ if (match) {
+ responseBody = decodeURIComponent(match[1]);
+ }
+
+ response.setStatusLine("1.0", responseStatus, "OK");
+ response.write(responseBody);
+}
+
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 500, "NotOK");
+ response.write("Error handling request: " + e);
+ }
+}
diff --git a/browser/base/content/test/general/gZipOfflineChild.cacheManifest b/browser/base/content/test/general/gZipOfflineChild.cacheManifest
new file mode 100644
index 000000000..ae0545d12
--- /dev/null
+++ b/browser/base/content/test/general/gZipOfflineChild.cacheManifest
@@ -0,0 +1,2 @@
+CACHE MANIFEST
+gZipOfflineChild.html
diff --git a/browser/base/content/test/general/gZipOfflineChild.cacheManifest^headers^ b/browser/base/content/test/general/gZipOfflineChild.cacheManifest^headers^
new file mode 100644
index 000000000..257f2eb60
--- /dev/null
+++ b/browser/base/content/test/general/gZipOfflineChild.cacheManifest^headers^
@@ -0,0 +1 @@
+Content-Type: text/cache-manifest
diff --git a/browser/base/content/test/general/gZipOfflineChild.html b/browser/base/content/test/general/gZipOfflineChild.html
new file mode 100644
index 000000000..ea2caa125
--- /dev/null
+++ b/browser/base/content/test/general/gZipOfflineChild.html
Binary files differ
diff --git a/browser/base/content/test/general/gZipOfflineChild.html^headers^ b/browser/base/content/test/general/gZipOfflineChild.html^headers^
new file mode 100644
index 000000000..4204d8601
--- /dev/null
+++ b/browser/base/content/test/general/gZipOfflineChild.html^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/html
+Content-Encoding: gzip
diff --git a/browser/base/content/test/general/gZipOfflineChild_uncompressed.html b/browser/base/content/test/general/gZipOfflineChild_uncompressed.html
new file mode 100644
index 000000000..4ab8f8d5e
--- /dev/null
+++ b/browser/base/content/test/general/gZipOfflineChild_uncompressed.html
@@ -0,0 +1,21 @@
+<html manifest="gZipOfflineChild.cacheManifest">
+<head>
+ <!-- This file is gzipped to create gZipOfflineChild.html -->
+<title></title>
+<script type="text/javascript">
+
+function finish(success) {
+ window.parent.postMessage(success, "*");
+}
+
+applicationCache.oncached = function() { finish("oncache"); }
+applicationCache.onnoupdate = function() { finish("onupdate"); }
+applicationCache.onerror = function() { finish("onerror"); }
+
+</script>
+</head>
+
+<body>
+<h1>Child</h1>
+</body>
+</html>
diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js
new file mode 100644
index 000000000..6c28615fe
--- /dev/null
+++ b/browser/base/content/test/general/head.js
@@ -0,0 +1,1069 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler",
+ "resource:///modules/ContentCrashHandlers.jsm");
+
+/**
+ * Wait for a <notification> to be closed then call the specified callback.
+ */
+function waitForNotificationClose(notification, cb) {
+ let parent = notification.parentNode;
+
+ let observer = new MutationObserver(function onMutatations(mutations) {
+ for (let mutation of mutations) {
+ for (let i = 0; i < mutation.removedNodes.length; i++) {
+ let node = mutation.removedNodes.item(i);
+ if (node != notification) {
+ continue;
+ }
+ observer.disconnect();
+ cb();
+ }
+ }
+ });
+ observer.observe(parent, {childList: true});
+}
+
+function closeAllNotifications () {
+ let notificationBox = document.getElementById("global-notificationbox");
+
+ if (!notificationBox || !notificationBox.currentNotification) {
+ return Promise.resolve();
+ }
+
+ let deferred = Promise.defer();
+ for (let notification of notificationBox.allNotifications) {
+ waitForNotificationClose(notification, function () {
+ if (notificationBox.allNotifications.length === 0) {
+ deferred.resolve();
+ }
+ });
+ notification.close();
+ }
+
+ return deferred.promise;
+}
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ executeSoon(aCallback);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+function updateTabContextMenu(tab, onOpened) {
+ let menu = document.getElementById("tabContextMenu");
+ if (!tab)
+ tab = gBrowser.selectedTab;
+ var evt = new Event("");
+ tab.dispatchEvent(evt);
+ menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
+ is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
+ const onFinished = () => menu.hidePopup();
+ if (onOpened) {
+ return Task.spawn(function*() {
+ yield onOpened();
+ onFinished();
+ });
+ }
+ onFinished();
+ return Promise.resolve();
+}
+
+function openToolbarCustomizationUI(aCallback, aBrowserWin) {
+ if (!aBrowserWin)
+ aBrowserWin = window;
+
+ aBrowserWin.gCustomizeMode.enter();
+
+ aBrowserWin.gNavToolbox.addEventListener("customizationready", function UI_loaded() {
+ aBrowserWin.gNavToolbox.removeEventListener("customizationready", UI_loaded);
+ executeSoon(function() {
+ aCallback(aBrowserWin)
+ });
+ });
+}
+
+function closeToolbarCustomizationUI(aCallback, aBrowserWin) {
+ aBrowserWin.gNavToolbox.addEventListener("aftercustomization", function unloaded() {
+ aBrowserWin.gNavToolbox.removeEventListener("aftercustomization", unloaded);
+ executeSoon(aCallback);
+ });
+
+ aBrowserWin.gCustomizeMode.exit();
+}
+
+function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
+ retryTimes = typeof retryTimes !== 'undefined' ? retryTimes : 30;
+ var tries = 0;
+ var interval = setInterval(function() {
+ if (tries >= retryTimes) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = function() { clearInterval(interval); nextTest(); };
+}
+
+function promiseWaitForCondition(aConditionFn) {
+ let deferred = Promise.defer();
+ waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass.");
+ return deferred.promise;
+}
+
+function promiseWaitForEvent(object, eventName, capturing = false, chrome = false) {
+ return new Promise((resolve) => {
+ function listener(event) {
+ info("Saw " + eventName);
+ object.removeEventListener(eventName, listener, capturing, chrome);
+ resolve(event);
+ }
+
+ info("Waiting for " + eventName);
+ object.addEventListener(eventName, listener, capturing, chrome);
+ });
+}
+
+/**
+ * Allows setting focus on a window, and waiting for that window to achieve
+ * focus.
+ *
+ * @param aWindow
+ * The window to focus and wait for.
+ *
+ * @return {Promise}
+ * @resolves When the window is focused.
+ * @rejects Never.
+ */
+function promiseWaitForFocus(aWindow) {
+ return new Promise((resolve) => {
+ waitForFocus(resolve, aWindow);
+ });
+}
+
+function getTestPlugin(aName) {
+ var pluginName = aName || "Test Plug-in";
+ var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+ var tags = ph.getPluginTags();
+
+ // Find the test plugin
+ for (var i = 0; i < tags.length; i++) {
+ if (tags[i].name == pluginName)
+ return tags[i];
+ }
+ ok(false, "Unable to find plugin");
+ return null;
+}
+
+// call this to set the test plugin(s) initially expected enabled state.
+// it will automatically be reset to it's previous value after the test
+// ends
+function setTestPluginEnabledState(newEnabledState, pluginName) {
+ var plugin = getTestPlugin(pluginName);
+ var oldEnabledState = plugin.enabledState;
+ plugin.enabledState = newEnabledState;
+ SimpleTest.registerCleanupFunction(function() {
+ getTestPlugin(pluginName).enabledState = oldEnabledState;
+ });
+}
+
+function pushPrefs(...aPrefs) {
+ let deferred = Promise.defer();
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
+ return deferred.promise;
+}
+
+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 _originalTestBlocklistURL = null;
+function setAndUpdateBlocklist(aURL, aCallback) {
+ if (!_originalTestBlocklistURL)
+ _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url");
+ Services.prefs.setCharPref("extensions.blocklist.url", aURL);
+ updateBlocklist(aCallback);
+}
+
+function resetBlocklist() {
+ Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL);
+}
+
+function whenNewWindowLoaded(aOptions, aCallback) {
+ let win = OpenBrowserWindow(aOptions);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ aCallback(win);
+ }, false);
+}
+
+function promiseWindowWillBeClosed(win) {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function observe(subject, topic) {
+ if (subject == win) {
+ Services.obs.removeObserver(observe, topic);
+ resolve();
+ }
+ }, "domwindowclosed", false);
+ });
+}
+
+function promiseWindowClosed(win) {
+ let promise = promiseWindowWillBeClosed(win);
+ win.close();
+ return promise;
+}
+
+function promiseOpenAndLoadWindow(aOptions, aWaitForDelayedStartup=false) {
+ let deferred = Promise.defer();
+ let win = OpenBrowserWindow(aOptions);
+ if (aWaitForDelayedStartup) {
+ Services.obs.addObserver(function onDS(aSubject, aTopic, aData) {
+ if (aSubject != win) {
+ return;
+ }
+ Services.obs.removeObserver(onDS, "browser-delayed-startup-finished");
+ deferred.resolve(win);
+ }, "browser-delayed-startup-finished", false);
+
+ } else {
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ deferred.resolve(win);
+ });
+ }
+ return deferred.promise;
+}
+
+/**
+ * Waits for all pending async statements on the default connection, before
+ * proceeding with aCallback.
+ *
+ * @param aCallback
+ * Function to be called when done.
+ * @param aScope
+ * Scope for the callback.
+ * @param aArguments
+ * Arguments array for the callback.
+ *
+ * @note The result is achieved by asynchronously executing a query requiring
+ * a write lock. Since all statements on the same connection are
+ * serialized, the end of this write operation means that all writes are
+ * complete. Note that WAL makes so that writers don't block readers, but
+ * this is a problem only across different connections.
+ */
+function waitForAsyncUpdates(aCallback, aScope, aArguments) {
+ let scope = aScope || this;
+ let args = aArguments || [];
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
+ begin.executeAsync();
+ begin.finalize();
+
+ let commit = db.createAsyncStatement("COMMIT");
+ commit.executeAsync({
+ handleResult: function() {},
+ handleError: function() {},
+ handleCompletion: function(aReason) {
+ aCallback.apply(scope, args);
+ }
+ });
+ commit.finalize();
+}
+
+/**
+ * Asynchronously check a url is visited.
+
+ * @param aURI The URI.
+ * @param aExpectedValue The expected value.
+ * @return {Promise}
+ * @resolves When the check has been added successfully.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aURI, aExpectedValue) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) {
+ deferred.resolve(aIsVisited);
+ });
+
+ return deferred.promise;
+}
+
+function whenNewTabLoaded(aWindow, aCallback) {
+ aWindow.BrowserOpenTab();
+
+ let browser = aWindow.gBrowser.selectedBrowser;
+ if (browser.contentDocument.readyState === "complete") {
+ aCallback();
+ return;
+ }
+
+ whenTabLoaded(aWindow.gBrowser.selectedTab, aCallback);
+}
+
+function whenTabLoaded(aTab, aCallback) {
+ promiseTabLoadEvent(aTab).then(aCallback);
+}
+
+function promiseTabLoaded(aTab) {
+ let deferred = Promise.defer();
+ whenTabLoaded(aTab, deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Ensures that the specified URIs are either cleared or not.
+ *
+ * @param aURIs
+ * Array of page URIs
+ * @param aShouldBeCleared
+ * True if each visit to the URI should be cleared, false otherwise
+ */
+function promiseHistoryClearedState(aURIs, aShouldBeCleared) {
+ let deferred = Promise.defer();
+ let callbackCount = 0;
+ let niceStr = aShouldBeCleared ? "no longer" : "still";
+ function callbackDone() {
+ if (++callbackCount == aURIs.length)
+ deferred.resolve();
+ }
+ aURIs.forEach(function (aURI) {
+ PlacesUtils.asyncHistory.isURIVisited(aURI, function(uri, isVisited) {
+ is(isVisited, !aShouldBeCleared,
+ "history visit " + uri.spec + " should " + niceStr + " exist");
+ callbackDone();
+ });
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Waits for the next top-level document load in the current browser. The URI
+ * of the document is compared against aExpectedURL. The load is then stopped
+ * before it actually starts.
+ *
+ * @param aExpectedURL
+ * The URL of the document that is expected to load.
+ * @param aStopFromProgressListener
+ * Whether to cancel the load directly from the progress listener. Defaults to true.
+ * If you're using this method to avoid hitting the network, you want the default (true).
+ * However, the browser UI will behave differently for loads stopped directly from
+ * the progress listener (effectively in the middle of a call to loadURI) and so there
+ * are cases where you may want to avoid stopping the load directly from within the
+ * progress listener callback.
+ * @return promise
+ */
+function waitForDocLoadAndStopIt(aExpectedURL, aBrowser=gBrowser.selectedBrowser, aStopFromProgressListener=true) {
+ function content_script(contentStopFromProgressListener) {
+ let { interfaces: Ci, utils: Cu } = Components;
+ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+ let wp = docShell.QueryInterface(Ci.nsIWebProgress);
+
+ function stopContent(now, uri) {
+ if (now) {
+ /* Hammer time. */
+ content.stop();
+
+ /* Let the parent know we're done. */
+ sendAsyncMessage("Test:WaitForDocLoadAndStopIt", { uri });
+ } else {
+ setTimeout(stopContent.bind(null, true, uri), 0);
+ }
+ }
+
+ let progressListener = {
+ onStateChange: function (webProgress, req, flags, status) {
+ dump("waitForDocLoadAndStopIt: onStateChange " + flags.toString(16) + ": " + req.name + "\n");
+
+ if (webProgress.isTopLevel &&
+ flags & Ci.nsIWebProgressListener.STATE_START) {
+ wp.removeProgressListener(progressListener);
+
+ let chan = req.QueryInterface(Ci.nsIChannel);
+ dump(`waitForDocLoadAndStopIt: Document start: ${chan.URI.spec}\n`);
+
+ stopContent(contentStopFromProgressListener, chan.originalURI.spec);
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"])
+ };
+ wp.addProgressListener(progressListener, wp.NOTIFY_STATE_WINDOW);
+
+ /**
+ * As |this| is undefined and we can't extend |docShell|, adding an unload
+ * event handler is the easiest way to ensure the weakly referenced
+ * progress listener is kept alive as long as necessary.
+ */
+ addEventListener("unload", function () {
+ try {
+ wp.removeProgressListener(progressListener);
+ } catch (e) { /* Will most likely fail. */ }
+ });
+ }
+
+ return new Promise((resolve, reject) => {
+ function complete({ data }) {
+ is(data.uri, aExpectedURL, "waitForDocLoadAndStopIt: The expected URL was loaded");
+ mm.removeMessageListener("Test:WaitForDocLoadAndStopIt", complete);
+ resolve();
+ }
+
+ let mm = aBrowser.messageManager;
+ mm.loadFrameScript("data:,(" + content_script.toString() + ")(" + aStopFromProgressListener + ");", true);
+ mm.addMessageListener("Test:WaitForDocLoadAndStopIt", complete);
+ info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL);
+ });
+}
+
+/**
+ * Waits for the next load to complete in any browser or the given browser.
+ * If a <tabbrowser> is given it waits for a load in any of its browsers.
+ *
+ * @return promise
+ */
+function waitForDocLoadComplete(aBrowser=gBrowser) {
+ return new Promise(resolve => {
+ let listener = {
+ onStateChange: function (webProgress, req, flags, status) {
+ let docStop = Ci.nsIWebProgressListener.STATE_IS_NETWORK |
+ Ci.nsIWebProgressListener.STATE_STOP;
+ info("Saw state " + flags.toString(16) + " and status " + status.toString(16));
+
+ // When a load needs to be retargetted to a new process it is cancelled
+ // with NS_BINDING_ABORTED so ignore that case
+ if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) {
+ aBrowser.removeProgressListener(this);
+ waitForDocLoadComplete.listeners.delete(this);
+
+ let chan = req.QueryInterface(Ci.nsIChannel);
+ info("Browser loaded " + chan.originalURI.spec);
+ resolve();
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+ };
+ aBrowser.addProgressListener(listener);
+ waitForDocLoadComplete.listeners.add(listener);
+ info("Waiting for browser load");
+ });
+}
+
+// Keep a set of progress listeners for waitForDocLoadComplete() to make sure
+// they're not GC'ed before we saw the page load.
+waitForDocLoadComplete.listeners = new Set();
+registerCleanupFunction(() => waitForDocLoadComplete.listeners.clear());
+
+var FullZoomHelper = {
+
+ selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) {
+ if (!tab)
+ throw new Error("tab must be given.");
+ if (gBrowser.selectedTab == tab)
+ return Promise.resolve();
+
+ return Promise.all([BrowserTestUtils.switchTab(gBrowser, tab),
+ this.waitForLocationChange()]);
+ },
+
+ removeTabAndWaitForLocationChange: function removeTabAndWaitForLocationChange(tab) {
+ tab = tab || gBrowser.selectedTab;
+ let selected = gBrowser.selectedTab == tab;
+ gBrowser.removeTab(tab);
+ if (selected)
+ return this.waitForLocationChange();
+ return Promise.resolve();
+ },
+
+ waitForLocationChange: function waitForLocationChange() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(subj, topic, data) {
+ Services.obs.removeObserver(obs, topic);
+ resolve();
+ }, "browser-fullZoom:location-change", false);
+ });
+ },
+
+ load: function load(tab, url) {
+ return new Promise(resolve => {
+ let didLoad = false;
+ let didZoom = false;
+
+ promiseTabLoadEvent(tab).then(event => {
+ didLoad = true;
+ if (didZoom)
+ resolve();
+ }, true);
+
+ this.waitForLocationChange().then(function () {
+ didZoom = true;
+ if (didLoad)
+ resolve();
+ });
+
+ tab.linkedBrowser.loadURI(url);
+ });
+ },
+
+ zoomTest: function zoomTest(tab, val, msg) {
+ is(ZoomManager.getZoomForBrowser(tab.linkedBrowser), val, msg);
+ },
+
+ enlarge: function enlarge() {
+ return new Promise(resolve => FullZoom.enlarge(resolve));
+ },
+
+ reduce: function reduce() {
+ return new Promise(resolve => FullZoom.reduce(resolve));
+ },
+
+ reset: function reset() {
+ return FullZoom.reset();
+ },
+
+ BACK: 0,
+ FORWARD: 1,
+ navigate: function navigate(direction) {
+ return new Promise(resolve => {
+ let didPs = false;
+ let didZoom = false;
+
+ gBrowser.addEventListener("pageshow", function listener(event) {
+ gBrowser.removeEventListener("pageshow", listener, true);
+ didPs = true;
+ if (didZoom)
+ resolve();
+ }, true);
+
+ if (direction == this.BACK)
+ gBrowser.goBack();
+ else if (direction == this.FORWARD)
+ gBrowser.goForward();
+
+ this.waitForLocationChange().then(function () {
+ didZoom = true;
+ if (didPs)
+ resolve();
+ });
+ });
+ },
+
+ failAndContinue: function failAndContinue(func) {
+ return function (err) {
+ ok(false, err);
+ func();
+ };
+ },
+};
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url)
+{
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url)
+ BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+
+ return loaded;
+}
+
+/**
+ * Returns a Promise that resolves once a new tab has been opened in
+ * a xul:tabbrowser.
+ *
+ * @param aTabBrowser
+ * The xul:tabbrowser to monitor for a new tab.
+ * @return {Promise}
+ * Resolved when the new tab has been opened.
+ * @resolves to the TabOpen event that was fired.
+ * @rejects Never.
+ */
+function waitForNewTabEvent(aTabBrowser) {
+ return promiseWaitForEvent(aTabBrowser.tabContainer, "TabOpen");
+}
+
+/**
+ * Test the state of the identity box and control center to make
+ * sure they are correctly showing the expected mixed content states.
+ *
+ * @note The checks are done synchronously, but new code should wait on the
+ * returned Promise object to ensure the identity panel has closed.
+ * Bug 1221114 is filed to fix the existing code.
+ *
+ * @param tabbrowser
+ * @param Object states
+ * MUST include the following properties:
+ * {
+ * activeLoaded: true|false,
+ * activeBlocked: true|false,
+ * passiveLoaded: true|false,
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished and the identity panel has closed.
+ */
+function assertMixedContentBlockingState(tabbrowser, states = {}) {
+ if (!tabbrowser || !("activeLoaded" in states) ||
+ !("activeBlocked" in states) || !("passiveLoaded" in states)) {
+ throw new Error("assertMixedContentBlockingState requires a browser and a states object");
+ }
+
+ let {passiveLoaded, activeLoaded, activeBlocked} = states;
+ let {gIdentityHandler} = tabbrowser.ownerGlobal;
+ let doc = tabbrowser.ownerDocument;
+ let identityBox = gIdentityHandler._identityBox;
+ let classList = identityBox.classList;
+ let connectionIcon = doc.getElementById("connection-icon");
+ let connectionIconImage = tabbrowser.ownerGlobal.getComputedStyle(connectionIcon).
+ getPropertyValue("list-style-image");
+
+ let stateSecure = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
+ let stateBroken = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
+ let stateInsecure = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_INSECURE;
+ let stateActiveBlocked = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
+ let stateActiveLoaded = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT;
+ let statePassiveLoaded = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT;
+
+ is(activeBlocked, !!stateActiveBlocked, "Expected state for activeBlocked matches UI state");
+ is(activeLoaded, !!stateActiveLoaded, "Expected state for activeLoaded matches UI state");
+ is(passiveLoaded, !!statePassiveLoaded, "Expected state for passiveLoaded matches UI state");
+
+ if (stateInsecure) {
+ // HTTP request, there should be no MCB classes for the identity box and the non secure icon
+ // should always be visible regardless of MCB state.
+ ok(classList.contains("unknownIdentity"), "unknownIdentity on HTTP page");
+ is_element_hidden(connectionIcon);
+
+ ok(!classList.contains("mixedActiveContent"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedActiveBlocked"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedDisplayContent"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedDisplayContentLoadedActiveBlocked"), "No MCB icon on HTTP page");
+ } else {
+ // Make sure the identity box UI has the correct mixedcontent states and icons
+ is(classList.contains("mixedActiveContent"), activeLoaded,
+ "identityBox has expected class for activeLoaded");
+ is(classList.contains("mixedActiveBlocked"), activeBlocked && !passiveLoaded,
+ "identityBox has expected class for activeBlocked && !passiveLoaded");
+ is(classList.contains("mixedDisplayContent"), passiveLoaded && !(activeLoaded || activeBlocked),
+ "identityBox has expected class for passiveLoaded && !(activeLoaded || activeBlocked)");
+ is(classList.contains("mixedDisplayContentLoadedActiveBlocked"), passiveLoaded && activeBlocked,
+ "identityBox has expected class for passiveLoaded && activeBlocked");
+
+ is_element_visible(connectionIcon);
+ if (activeLoaded) {
+ is(connectionIconImage, "url(\"chrome://browser/skin/connection-mixed-active-loaded.svg#icon\")",
+ "Using active loaded icon");
+ }
+ if (activeBlocked && !passiveLoaded) {
+ is(connectionIconImage, "url(\"chrome://browser/skin/connection-secure.svg\")",
+ "Using active blocked icon");
+ }
+ if (passiveLoaded && !(activeLoaded || activeBlocked)) {
+ is(connectionIconImage, "url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg#icon\")",
+ "Using passive loaded icon");
+ }
+ if (passiveLoaded && activeBlocked) {
+ is(connectionIconImage, "url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg#icon\")",
+ "Using active blocked and passive loaded icon");
+ }
+ }
+
+ // Make sure the identity popup has the correct mixedcontent states
+ gIdentityHandler._identityBox.click();
+ let popupAttr = doc.getElementById("identity-popup").getAttribute("mixedcontent");
+ let bodyAttr = doc.getElementById("identity-popup-securityView-body").getAttribute("mixedcontent");
+
+ is(popupAttr.includes("active-loaded"), activeLoaded,
+ "identity-popup has expected attr for activeLoaded");
+ is(bodyAttr.includes("active-loaded"), activeLoaded,
+ "securityView-body has expected attr for activeLoaded");
+
+ is(popupAttr.includes("active-blocked"), activeBlocked,
+ "identity-popup has expected attr for activeBlocked");
+ is(bodyAttr.includes("active-blocked"), activeBlocked,
+ "securityView-body has expected attr for activeBlocked");
+
+ is(popupAttr.includes("passive-loaded"), passiveLoaded,
+ "identity-popup has expected attr for passiveLoaded");
+ is(bodyAttr.includes("passive-loaded"), passiveLoaded,
+ "securityView-body has expected attr for passiveLoaded");
+
+ // Make sure the correct icon is visible in the Control Center.
+ // This logic is controlled with CSS, so this helps prevent regressions there.
+ let securityView = doc.getElementById("identity-popup-securityView");
+ let securityViewBG = tabbrowser.ownerGlobal.getComputedStyle(securityView).
+ getPropertyValue("background-image");
+ let securityContentBG = tabbrowser.ownerGlobal.getComputedStyle(securityView).
+ getPropertyValue("background-image");
+
+ if (stateInsecure) {
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/conn-not-secure.svg\")",
+ "CC using 'not secure' icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/conn-not-secure.svg\")",
+ "CC using 'not secure' icon");
+ }
+
+ if (stateSecure) {
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-secure\")",
+ "CC using secure icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-secure\")",
+ "CC using secure icon");
+ }
+
+ if (stateBroken) {
+ if (activeLoaded) {
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
+ "CC using active loaded icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
+ "CC using active loaded icon");
+ } else if (activeBlocked || passiveLoaded) {
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-degraded\")",
+ "CC using degraded icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-degraded\")",
+ "CC using degraded icon");
+ } else {
+ // There is a case here with weak ciphers, but no bc tests are handling this yet.
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-degraded\")",
+ "CC using degraded icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-degraded\")",
+ "CC using degraded icon");
+ }
+ }
+
+ if (activeLoaded || activeBlocked || passiveLoaded) {
+ doc.getElementById("identity-popup-security-expander").click();
+ is(Array.filter(doc.querySelectorAll("[observes=identity-popup-mcb-learn-more]"),
+ element => !is_hidden(element)).length, 1,
+ "The 'Learn more' link should be visible once.");
+ }
+
+ gIdentityHandler._identityPopup.hidden = true;
+
+ // Wait for the panel to be closed before continuing. The promisePopupHidden
+ // function cannot be used because it's unreliable unless promisePopupShown is
+ // also called before closing the panel. This cannot be done until all callers
+ // are made asynchronous (bug 1221114).
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+function is_hidden(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none")
+ return true;
+ if (style.visibility != "visible")
+ return true;
+ if (style.display == "-moz-popup")
+ return ["hiding", "closed"].indexOf(element.state) != -1;
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument)
+ return is_hidden(element.parentNode);
+
+ return false;
+}
+
+function is_visible(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none")
+ return false;
+ if (style.visibility != "visible")
+ return false;
+ if (style.display == "-moz-popup" && element.state != "open")
+ return false;
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument)
+ return is_visible(element.parentNode);
+
+ return true;
+}
+
+function is_element_visible(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_visible(element), msg || "Element should be visible");
+}
+
+function is_element_hidden(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(element), msg || "Element should be hidden");
+}
+
+function promisePopupEvent(popup, eventSuffix) {
+ let endState = {shown: "open", hidden: "closed"}[eventSuffix];
+
+ if (popup.state == endState)
+ return Promise.resolve();
+
+ let eventType = "popup" + eventSuffix;
+ let deferred = Promise.defer();
+ popup.addEventListener(eventType, function onPopupShown(event) {
+ popup.removeEventListener(eventType, onPopupShown);
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function promisePopupShown(popup) {
+ return promisePopupEvent(popup, "shown");
+}
+
+function promisePopupHidden(popup) {
+ return promisePopupEvent(popup, "hidden");
+}
+
+function promiseNotificationShown(notification) {
+ let win = notification.browser.ownerGlobal;
+ if (win.PopupNotifications.panel.state == "open") {
+ return Promise.resolve();
+ }
+ let panelPromise = promisePopupShown(win.PopupNotifications.panel);
+ notification.reshow();
+ return panelPromise;
+}
+
+/**
+ * Allows waiting for an observer notification once.
+ *
+ * @param aTopic
+ * Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves An object with subject and data properties from the observed
+ * notification.
+ * @rejects Never.
+ */
+function promiseTopicObserved(aTopic)
+{
+ return new Promise((resolve) => {
+ Services.obs.addObserver(
+ function PTO_observe(aSubject, aTopic2, aData) {
+ Services.obs.removeObserver(PTO_observe, aTopic2);
+ resolve({subject: aSubject, data: aData});
+ }, aTopic, false);
+ });
+}
+
+function promiseNewSearchEngine(basename) {
+ return new Promise((resolve, reject) => {
+ info("Waiting for engine to be added: " + basename);
+ let url = getRootDirectory(gTestPath) + basename;
+ Services.search.addEngine(url, null, "", false, {
+ onSuccess: function (engine) {
+ info("Search engine added: " + basename);
+ registerCleanupFunction(() => Services.search.removeEngine(engine));
+ resolve(engine);
+ },
+ onError: function (errCode) {
+ Assert.ok(false, "addEngine failed with error code " + errCode);
+ reject();
+ },
+ });
+ });
+}
+
+// Compares the security state of the page with what is expected
+function isSecurityState(expectedState) {
+ let ui = gTestBrowser.securityUI;
+ if (!ui) {
+ ok(false, "No security UI to get the security state");
+ return;
+ }
+
+ const wpl = Components.interfaces.nsIWebProgressListener;
+
+ // determine the security state
+ let isSecure = ui.state & wpl.STATE_IS_SECURE;
+ let isBroken = ui.state & wpl.STATE_IS_BROKEN;
+ let isInsecure = ui.state & wpl.STATE_IS_INSECURE;
+
+ let actualState;
+ if (isSecure && !(isBroken || isInsecure)) {
+ actualState = "secure";
+ } else if (isBroken && !(isSecure || isInsecure)) {
+ actualState = "broken";
+ } else if (isInsecure && !(isSecure || isBroken)) {
+ actualState = "insecure";
+ } else {
+ actualState = "unknown";
+ }
+
+ is(expectedState, actualState, "Expected state " + expectedState + " and the actual state is " + actualState + ".");
+}
+
+/**
+ * Resolves when a bookmark with the given uri is added.
+ */
+function promiseOnBookmarkItemAdded(aExpectedURI) {
+ return new Promise((resolve, reject) => {
+ let bookmarksObserver = {
+ onItemAdded: function (aItemId, aFolderId, aIndex, aItemType, aURI) {
+ info("Added a bookmark to " + aURI.spec);
+ PlacesUtils.bookmarks.removeObserver(bookmarksObserver);
+ if (aURI.equals(aExpectedURI)) {
+ resolve();
+ }
+ else {
+ reject(new Error("Added an unexpected bookmark"));
+ }
+ },
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onItemRemoved: function () {},
+ onItemChanged: function () {},
+ onItemVisited: function () {},
+ onItemMoved: function () {},
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ ])
+ };
+ info("Waiting for a bookmark to be added");
+ PlacesUtils.bookmarks.addObserver(bookmarksObserver, false);
+ });
+}
+
+function promiseErrorPageLoaded(browser) {
+ return new Promise(resolve => {
+ browser.addEventListener("DOMContentLoaded", function onLoad() {
+ browser.removeEventListener("DOMContentLoaded", onLoad, false, true);
+ resolve();
+ }, false, true);
+ });
+}
+
+function* loadBadCertPage(url) {
+ const EXCEPTION_DIALOG_URI = "chrome://pippki/content/exceptionDialog.xul";
+ let exceptionDialogResolved = new Promise(function(resolve) {
+ // When the certificate exception dialog has opened, click the button to add
+ // an exception.
+ let certExceptionDialogObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "cert-exception-ui-ready") {
+ Services.obs.removeObserver(this, "cert-exception-ui-ready");
+ let certExceptionDialog = getCertExceptionDialog(EXCEPTION_DIALOG_URI);
+ ok(certExceptionDialog, "found exception dialog");
+ executeSoon(function() {
+ certExceptionDialog.documentElement.getButton("extra1").click();
+ resolve();
+ });
+ }
+ }
+ };
+
+ Services.obs.addObserver(certExceptionDialogObserver,
+ "cert-exception-ui-ready", false);
+ });
+
+ let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+ yield loaded;
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ content.document.getElementById("exceptionDialogButton").click();
+ });
+ yield exceptionDialogResolved;
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+}
+
+// Utility function to get a handle on the certificate exception dialog.
+// Modified from toolkit/components/passwordmgr/test/prompt_common.js
+function getCertExceptionDialog(aLocation) {
+ let enumerator = Services.wm.getXULWindowEnumerator(null);
+
+ while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext();
+ let windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
+
+ let containedDocShells = windowDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeChrome,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS);
+ while (containedDocShells.hasMoreElements()) {
+ // Get the corresponding document for this docshell
+ let childDocShell = containedDocShells.getNext();
+ let childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
+ .contentViewer
+ .DOMDocument;
+
+ if (childDoc.location.href == aLocation) {
+ return childDoc;
+ }
+ }
+ }
+ return undefined;
+}
+
+function setupRemoteClientsFixture(fixture) {
+ let oldRemoteClientsGetter =
+ Object.getOwnPropertyDescriptor(gFxAccounts, "remoteClients").get;
+
+ Object.defineProperty(gFxAccounts, "remoteClients", {
+ get: function() { return fixture; }
+ });
+ return oldRemoteClientsGetter;
+}
+
+function restoreRemoteClients(getter) {
+ Object.defineProperty(gFxAccounts, "remoteClients", {
+ get: getter
+ });
+}
+
+function* openMenuItemSubmenu(id) {
+ let menuPopup = document.getElementById(id).menupopup;
+ let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
+ menuPopup.showPopup();
+ yield menuPopupPromise;
+}
diff --git a/browser/base/content/test/general/head_plain.js b/browser/base/content/test/general/head_plain.js
new file mode 100644
index 000000000..3796c7d2b
--- /dev/null
+++ b/browser/base/content/test/general/head_plain.js
@@ -0,0 +1,27 @@
+
+function getTestPlugin(pluginName) {
+ var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"]
+ .getService(SpecialPowers.Ci.nsIPluginHost);
+ var tags = ph.getPluginTags();
+ var name = pluginName || "Test Plug-in";
+ for (var tag of tags) {
+ if (tag.name == name) {
+ return tag;
+ }
+ }
+
+ ok(false, "Could not find plugin tag with plugin name '" + name + "'");
+ return null;
+}
+
+// call this to set the test plugin(s) initially expected enabled state.
+// it will automatically be reset to it's previous value after the test
+// ends
+function setTestPluginEnabledState(newEnabledState, pluginName) {
+ var plugin = getTestPlugin(pluginName);
+ var oldEnabledState = plugin.enabledState;
+ plugin.enabledState = newEnabledState;
+ SimpleTest.registerCleanupFunction(function() {
+ getTestPlugin(pluginName).enabledState = oldEnabledState;
+ });
+}
diff --git a/browser/base/content/test/general/healthreport_pingData.js b/browser/base/content/test/general/healthreport_pingData.js
new file mode 100644
index 000000000..1737baba1
--- /dev/null
+++ b/browser/base/content/test/general/healthreport_pingData.js
@@ -0,0 +1,17 @@
+var TEST_PINGS = [
+ {
+ type: "test-telemetryArchive-1",
+ payload: { foo: "bar" },
+ date: new Date(2010, 1, 1, 10, 0, 0),
+ },
+ {
+ type: "test-telemetryArchive-2",
+ payload: { x: { y: "z"} },
+ date: new Date(2010, 1, 1, 11, 0, 0),
+ },
+ {
+ type: "test-telemetryArchive-3",
+ payload: { moo: "meh" },
+ date: new Date(2010, 1, 1, 12, 0, 0),
+ },
+];
diff --git a/browser/base/content/test/general/healthreport_testRemoteCommands.html b/browser/base/content/test/general/healthreport_testRemoteCommands.html
new file mode 100644
index 000000000..7978914f2
--- /dev/null
+++ b/browser/base/content/test/general/healthreport_testRemoteCommands.html
@@ -0,0 +1,243 @@
+<html>
+ <head>
+ <meta charset="utf-8">
+<script type="application/javascript;version=1.7"
+ src="healthreport_pingData.js">
+</script>
+<script type="application/javascript;version=1.7">
+
+function init() {
+ window.addEventListener("message", doTest, false);
+ doTest();
+}
+
+function checkSubmissionValue(payload, expectedValue) {
+ return payload.enabled == expectedValue;
+}
+
+function isArray(arg) {
+ return Object.prototype.toString.call(arg) === '[object Array]';
+}
+
+function writeDiagnostic(text) {
+ let node = document.createTextNode(text);
+ let br = document.createElement("br");
+ document.body.appendChild(node);
+ document.body.appendChild(br);
+}
+
+function validateCurrentTelemetryEnvironment(data) {
+ // Simple check for now: check that the received object has the expected
+ // top-level properties.
+ const expectedKeys = ["profile", "settings", "system", "build", "partner", "addons"];
+ return expectedKeys.every(key => (key in data));
+}
+
+function validateCurrentTelemetryPingData(ping) {
+ // Simple check for now: check that the received object has the expected
+ // top-level properties and that the type and reason match.
+ const expectedKeys = ["environment", "clientId", "payload", "application",
+ "version", "type", "id"];
+ return expectedKeys.every(key => (key in ping)) &&
+ (ping.type == "main") &&
+ ("info" in ping.payload) &&
+ ("reason" in ping.payload.info) &&
+ (ping.payload.info.reason == "gather-subsession-payload");
+}
+
+function validateTelemetryPingList(list) {
+ if (!isArray(list)) {
+ console.log("Telemetry ping list is not an array.");
+ return false;
+ }
+
+ // Telemetry may generate other pings (e.g. "deletion" pings), so filter those
+ // out.
+ const TEST_TYPES_REGEX = /^test-telemetryArchive/;
+ list = list.filter(p => TEST_TYPES_REGEX.test(p.type));
+
+ if (list.length != TEST_PINGS.length) {
+ console.log("Telemetry ping length is not correct.");
+ return false;
+ }
+
+ let valid = true;
+ for (let i=0; i<list.length; ++i) {
+ let received = list[i];
+ let expected = TEST_PINGS[i];
+ if (received.type != expected.type ||
+ received.timestampCreated != expected.date.getTime()) {
+ writeDiagnostic("Telemetry ping " + i + " does not match.");
+ writeDiagnostic("Expected: " + JSON.stringify(expected));
+ writeDiagnostic("Received: " + JSON.stringify(received));
+ valid = false;
+ } else {
+ writeDiagnostic("Telemetry ping " + i + " matches.");
+ }
+ }
+
+ return true;
+}
+
+function validateTelemetryPingData(expected, received) {
+ const receivedDate = new Date(received.creationDate);
+ if (received.id != expected.id ||
+ received.type != expected.type ||
+ receivedDate.getTime() != expected.date.getTime()) {
+ writeDiagnostic("Telemetry ping data for " + expected.id + " doesn't match.");
+ writeDiagnostic("Expected: " + JSON.stringify(expected));
+ writeDiagnostic("Received: " + JSON.stringify(received));
+ return false;
+ }
+
+ writeDiagnostic("Telemetry ping data for " + expected.id + " matched.");
+ return true;
+}
+
+var tests = [
+{
+ info: "Checking initial value is enabled",
+ event: "RequestCurrentPrefs",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, true);
+ },
+},
+{
+ info: "Verifying disabling works",
+ event: "DisableDataSubmission",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, false);
+ },
+},
+{
+ info: "Verifying we're still disabled",
+ event: "RequestCurrentPrefs",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, false);
+ },
+},
+{
+ info: "Verifying that we can get the current ping data while submission is disabled",
+ event: "RequestCurrentPingData",
+ payloadType: "telemetry-current-ping-data",
+ validateResponse: function(payload) {
+ return validateCurrentTelemetryPingData(payload);
+ },
+},
+{
+ info: "Verifying enabling works",
+ event: "EnableDataSubmission",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, true);
+ },
+},
+{
+ info: "Verifying we're still re-enabled",
+ event: "RequestCurrentPrefs",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, true);
+ },
+},
+{
+ info: "Verifying that we can get the current Telemetry environment data",
+ event: "RequestCurrentEnvironment",
+ payloadType: "telemetry-current-environment-data",
+ validateResponse: function(payload) {
+ return validateCurrentTelemetryEnvironment(payload);
+ },
+},
+{
+ info: "Verifying that we can get the current Telemetry ping data",
+ event: "RequestCurrentPingData",
+ payloadType: "telemetry-current-ping-data",
+ validateResponse: function(payload) {
+ return validateCurrentTelemetryPingData(payload);
+ },
+},
+{
+ info: "Verifying that we get the proper Telemetry ping list",
+ event: "RequestTelemetryPingList",
+ payloadType: "telemetry-ping-list",
+ validateResponse: function(payload) {
+ // Validate the ping list
+ if (!validateTelemetryPingList(payload)) {
+ return false;
+ }
+
+ // Now that we received the ping ids, set up additional test tasks
+ // that check loading the individual pings.
+ for (let i=0; i<TEST_PINGS.length; ++i) {
+ TEST_PINGS[i].id = payload[i].id;
+ tests.push({
+ info: "Verifying that we can get the proper Telemetry ping data #" + (i + 1),
+ event: "RequestTelemetryPingData",
+ eventData: { id: TEST_PINGS[i].id },
+ payloadType: "telemetry-ping-data",
+ validateResponse: function(payload) {
+ return validateTelemetryPingData(TEST_PINGS[i], payload.pingData);
+ },
+ });
+ }
+
+ return true;
+ },
+},
+];
+
+var currentTest = -1;
+function doTest(evt) {
+ if (evt) {
+ if (currentTest < 0 || !evt.data.content)
+ return; // not yet testing
+
+ var test = tests[currentTest];
+ if (evt.data.type != test.payloadType)
+ return; // skip unrequested events
+
+ var error = JSON.stringify(evt.data.content);
+ var pass = false;
+ try {
+ pass = test.validateResponse(evt.data.content)
+ } catch (e) {}
+ reportResult(test.info, pass, error);
+ }
+ // start the next test if there are any left
+ if (tests[++currentTest])
+ sendToBrowser(tests[currentTest].event, tests[currentTest].eventData);
+ else
+ reportFinished();
+}
+
+function reportResult(info, pass, error) {
+ var data = {type: "testResult", info: info, pass: pass, error: error};
+ var event = new CustomEvent("FirefoxHealthReportTestResponse", {detail: {data: data}, bubbles: true});
+ document.dispatchEvent(event);
+}
+
+function reportFinished(cmd) {
+ var data = {type: "testsComplete", count: tests.length};
+ var event = new CustomEvent("FirefoxHealthReportTestResponse", {detail: {data: data}, bubbles: true});
+ document.dispatchEvent(event);
+}
+
+function sendToBrowser(type, eventData) {
+ eventData = eventData || {};
+ let detail = {command: type};
+ for (let key of Object.keys(eventData)) {
+ detail[key] = eventData[key];
+ }
+
+ var event = new CustomEvent("RemoteHealthReportCommand", {detail: detail, bubbles: true});
+ document.dispatchEvent(event);
+}
+
+</script>
+ </head>
+ <body onload="init()">
+ </body>
+</html>
diff --git a/browser/base/content/test/general/insecure_opener.html b/browser/base/content/test/general/insecure_opener.html
new file mode 100644
index 000000000..26ed014f6
--- /dev/null
+++ b/browser/base/content/test/general/insecure_opener.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <a id="link" target="_blank" href="https://example.com/browser/toolkit/components/passwordmgr/test/browser/form_basic.html">Click me, I'm "secure".</a>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/mochitest.ini b/browser/base/content/test/general/mochitest.ini
new file mode 100644
index 000000000..a07a01b87
--- /dev/null
+++ b/browser/base/content/test/general/mochitest.ini
@@ -0,0 +1,27 @@
+[DEFAULT]
+support-files =
+ audio.ogg
+ bug364677-data.xml
+ bug364677-data.xml^headers^
+ bug395533-data.txt
+ contextmenu_common.js
+ ctxmenu-image.png
+ head_plain.js
+ offlineByDefault.js
+ offlineChild.cacheManifest
+ offlineChild.cacheManifest^headers^
+ offlineChild.html
+ offlineChild2.cacheManifest
+ offlineChild2.cacheManifest^headers^
+ offlineChild2.html
+ offlineEvent.cacheManifest
+ offlineEvent.cacheManifest^headers^
+ offlineEvent.html
+ subtst_contextmenu.html
+ video.ogg
+ !/image/test/mochitest/blue.png
+
+[test_bug364677.html]
+[test_bug395533.html]
+[test_offlineNotification.html]
+skip-if = e10s # Bug 1257785
diff --git a/browser/base/content/test/general/moz.png b/browser/base/content/test/general/moz.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/browser/base/content/test/general/moz.png
Binary files differ
diff --git a/browser/base/content/test/general/navigating_window_with_download.html b/browser/base/content/test/general/navigating_window_with_download.html
new file mode 100644
index 000000000..6b0918941
--- /dev/null
+++ b/browser/base/content/test/general/navigating_window_with_download.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <head><title>This window will navigate while you're downloading something</title></head>
+ <body>
+ <iframe src="http://mochi.test:8888/browser/browser/base/content/test/general/unknownContentType_file.pif"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/offlineByDefault.js b/browser/base/content/test/general/offlineByDefault.js
new file mode 100644
index 000000000..72f7e52a0
--- /dev/null
+++ b/browser/base/content/test/general/offlineByDefault.js
@@ -0,0 +1,17 @@
+var offlineByDefault = {
+ defaultValue: false,
+ prefBranch: SpecialPowers.Cc["@mozilla.org/preferences-service;1"].getService(SpecialPowers.Ci.nsIPrefBranch),
+ set: function(allow) {
+ try {
+ this.defaultValue = this.prefBranch.getBoolPref("offline-apps.allow_by_default");
+ } catch (e) {
+ this.defaultValue = false
+ }
+ this.prefBranch.setBoolPref("offline-apps.allow_by_default", allow);
+ },
+ reset: function() {
+ this.prefBranch.setBoolPref("offline-apps.allow_by_default", this.defaultValue);
+ }
+}
+
+offlineByDefault.set(false);
diff --git a/browser/base/content/test/general/offlineChild.cacheManifest b/browser/base/content/test/general/offlineChild.cacheManifest
new file mode 100644
index 000000000..091fe7194
--- /dev/null
+++ b/browser/base/content/test/general/offlineChild.cacheManifest
@@ -0,0 +1,2 @@
+CACHE MANIFEST
+offlineChild.html
diff --git a/browser/base/content/test/general/offlineChild.cacheManifest^headers^ b/browser/base/content/test/general/offlineChild.cacheManifest^headers^
new file mode 100644
index 000000000..257f2eb60
--- /dev/null
+++ b/browser/base/content/test/general/offlineChild.cacheManifest^headers^
@@ -0,0 +1 @@
+Content-Type: text/cache-manifest
diff --git a/browser/base/content/test/general/offlineChild.html b/browser/base/content/test/general/offlineChild.html
new file mode 100644
index 000000000..43f225b3b
--- /dev/null
+++ b/browser/base/content/test/general/offlineChild.html
@@ -0,0 +1,20 @@
+<html manifest="offlineChild.cacheManifest">
+<head>
+<title></title>
+<script type="text/javascript">
+
+function finish(success) {
+ window.parent.postMessage(success ? "success" : "failure", "*");
+}
+
+applicationCache.oncached = function() { finish(true); }
+applicationCache.onnoupdate = function() { finish(true); }
+applicationCache.onerror = function() { finish(false); }
+
+</script>
+</head>
+
+<body>
+<h1>Child</h1>
+</body>
+</html>
diff --git a/browser/base/content/test/general/offlineChild2.cacheManifest b/browser/base/content/test/general/offlineChild2.cacheManifest
new file mode 100644
index 000000000..19efe54fe
--- /dev/null
+++ b/browser/base/content/test/general/offlineChild2.cacheManifest
@@ -0,0 +1,2 @@
+CACHE MANIFEST
+offlineChild2.html
diff --git a/browser/base/content/test/general/offlineChild2.cacheManifest^headers^ b/browser/base/content/test/general/offlineChild2.cacheManifest^headers^
new file mode 100644
index 000000000..257f2eb60
--- /dev/null
+++ b/browser/base/content/test/general/offlineChild2.cacheManifest^headers^
@@ -0,0 +1 @@
+Content-Type: text/cache-manifest
diff --git a/browser/base/content/test/general/offlineChild2.html b/browser/base/content/test/general/offlineChild2.html
new file mode 100644
index 000000000..ac762e759
--- /dev/null
+++ b/browser/base/content/test/general/offlineChild2.html
@@ -0,0 +1,20 @@
+<html manifest="offlineChild2.cacheManifest">
+<head>
+<title></title>
+<script type="text/javascript">
+
+function finish(success) {
+ window.parent.postMessage(success ? "success" : "failure", "*");
+}
+
+applicationCache.oncached = function() { finish(true); }
+applicationCache.onnoupdate = function() { finish(true); }
+applicationCache.onerror = function() { finish(false); }
+
+</script>
+</head>
+
+<body>
+<h1>Child</h1>
+</body>
+</html>
diff --git a/browser/base/content/test/general/offlineEvent.cacheManifest b/browser/base/content/test/general/offlineEvent.cacheManifest
new file mode 100644
index 000000000..091fe7194
--- /dev/null
+++ b/browser/base/content/test/general/offlineEvent.cacheManifest
@@ -0,0 +1,2 @@
+CACHE MANIFEST
+offlineChild.html
diff --git a/browser/base/content/test/general/offlineEvent.cacheManifest^headers^ b/browser/base/content/test/general/offlineEvent.cacheManifest^headers^
new file mode 100644
index 000000000..257f2eb60
--- /dev/null
+++ b/browser/base/content/test/general/offlineEvent.cacheManifest^headers^
@@ -0,0 +1 @@
+Content-Type: text/cache-manifest
diff --git a/browser/base/content/test/general/offlineEvent.html b/browser/base/content/test/general/offlineEvent.html
new file mode 100644
index 000000000..f6e2494e2
--- /dev/null
+++ b/browser/base/content/test/general/offlineEvent.html
@@ -0,0 +1,9 @@
+<html manifest="offlineEvent.cacheManifest">
+<head>
+<title></title>
+</head>
+
+<body>
+<h1>Child</h1>
+</body>
+</html>
diff --git a/browser/base/content/test/general/offlineQuotaNotification.cacheManifest b/browser/base/content/test/general/offlineQuotaNotification.cacheManifest
new file mode 100644
index 000000000..2e210abd2
--- /dev/null
+++ b/browser/base/content/test/general/offlineQuotaNotification.cacheManifest
@@ -0,0 +1,7 @@
+CACHE MANIFEST
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+# store a "large" file so an "over quota warning" will be issued - any file
+# larger than 1kb and in '_BROWSER_FILES' should be right...
+title_test.svg
diff --git a/browser/base/content/test/general/offlineQuotaNotification.html b/browser/base/content/test/general/offlineQuotaNotification.html
new file mode 100644
index 000000000..b1b91bf9e
--- /dev/null
+++ b/browser/base/content/test/general/offlineQuotaNotification.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html manifest="offlineQuotaNotification.cacheManifest">
+<head>
+ <meta charset="utf-8">
+ <title>Test offline app quota notification</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+</html>
diff --git a/browser/base/content/test/general/page_style_sample.html b/browser/base/content/test/general/page_style_sample.html
new file mode 100644
index 000000000..54cbaa9e6
--- /dev/null
+++ b/browser/base/content/test/general/page_style_sample.html
@@ -0,0 +1,41 @@
+<html>
+ <head>
+ <title>Test for page style menu</title>
+ <!-- data-state values:
+ 0: should not appear in the page style menu
+ 0-todo: should not appear in the page style menu, but does
+ 1: should appear in the page style menu
+ 2: should appear in the page style menu as the selected stylesheet -->
+ <link data-state="1" href="404.css" title="1" rel="alternate stylesheet">
+ <link data-state="0" title="2" rel="alternate stylesheet">
+ <link data-state="0" href="404.css" rel="alternate stylesheet">
+ <link data-state="0" href="404.css" title="" rel="alternate stylesheet">
+ <link data-state="1" href="404.css" title="3" rel="stylesheet alternate">
+ <link data-state="1" href="404.css" title="4" rel=" alternate stylesheet ">
+ <link data-state="1" href="404.css" title="5" rel="alternate stylesheet">
+ <link data-state="2" href="404.css" title="6" rel="stylesheet">
+ <link data-state="1" href="404.css" title="7" rel="foo stylesheet">
+ <link data-state="0" href="404.css" title="8" rel="alternate">
+ <link data-state="1" href="404.css" title="9" rel="alternate STYLEsheet">
+ <link data-state="1" href="404.css" title="10" rel="alternate stylesheet" media="">
+ <link data-state="1" href="404.css" title="11" rel="alternate stylesheet" media="all">
+ <link data-state="1" href="404.css" title="12" rel="alternate stylesheet" media="ALL ">
+ <link data-state="1" href="404.css" title="13" rel="alternate stylesheet" media="screen">
+ <link data-state="1" href="404.css" title="14" rel="alternate stylesheet" media=" Screen">
+ <link data-state="0" href="404.css" title="15" rel="alternate stylesheet" media="screen foo">
+ <link data-state="0" href="404.css" title="16" rel="alternate stylesheet" media="all screen">
+ <link data-state="0" href="404.css" title="17" rel="alternate stylesheet" media="foo bar">
+ <link data-state="1" href="404.css" title="18" rel="alternate stylesheet" media="all,screen">
+ <link data-state="1" href="404.css" title="19" rel="alternate stylesheet" media="all, screen">
+ <link data-state="0" href="404.css" title="20" rel="alternate stylesheet" media="all screen">
+ <link data-state="0" href="404.css" title="21" rel="alternate stylesheet" media="foo">
+ <link data-state="0" href="404.css" title="22" rel="alternate stylesheet" media="allscreen">
+ <link data-state="0" href="404.css" title="23" rel="alternate stylesheet" media="_all">
+ <link data-state="0" href="404.css" title="24" rel="alternate stylesheet" media="not screen">
+ <link data-state="1" href="404.css" title="25" rel="alternate stylesheet" media="only screen">
+ <link data-state="1" href="404.css" title="26" rel="alternate stylesheet" media="screen and (min-device-width: 1px)">
+ <link data-state="0" href="404.css" title="27" rel="alternate stylesheet" media="screen and (max-device-width: 1px)">
+ <style data-state="1" title="28">/* some more styles */</style>
+ </head>
+ <body></body>
+</html>
diff --git a/browser/base/content/test/general/parsingTestHelpers.jsm b/browser/base/content/test/general/parsingTestHelpers.jsm
new file mode 100644
index 000000000..69c764483
--- /dev/null
+++ b/browser/base/content/test/general/parsingTestHelpers.jsm
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["generateURIsFromDirTree"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* Shorthand constructors to construct an nsI(Local)File and zip reader: */
+const LocalFile = new Components.Constructor("@mozilla.org/file/local;1", Ci.nsIFile, "initWithPath");
+const ZipReader = new Components.Constructor("@mozilla.org/libjar/zip-reader;1", "nsIZipReader", "open");
+
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+
+/**
+ * Returns a promise that is resolved with a list of files that have one of the
+ * extensions passed, represented by their nsIURI objects, which exist inside
+ * the directory passed.
+ *
+ * @param dir the directory which to scan for files (nsIFile)
+ * @param extensions the extensions of files we're interested in (Array).
+ */
+function generateURIsFromDirTree(dir, extensions) {
+ if (!Array.isArray(extensions)) {
+ extensions = [extensions];
+ }
+ let dirQueue = [dir.path];
+ return Task.spawn(function*() {
+ let rv = [];
+ while (dirQueue.length) {
+ let nextDir = dirQueue.shift();
+ let {subdirs, files} = yield iterateOverPath(nextDir, extensions);
+ dirQueue.push(...subdirs);
+ rv.push(...files);
+ }
+ return rv;
+ });
+}
+
+/**
+ * Uses OS.File.DirectoryIterator to asynchronously iterate over a directory.
+ * It returns a promise that is resolved with an object with two properties:
+ * - files: an array of nsIURIs corresponding to files that match the extensions passed
+ * - subdirs: an array of paths for subdirectories we need to recurse into
+ * (handled by generateURIsFromDirTree above)
+ *
+ * @param path the path to check (string)
+ * @param extensions the file extensions we're interested in.
+ */
+function iterateOverPath(path, extensions) {
+ let iterator = new OS.File.DirectoryIterator(path);
+ let parentDir = new LocalFile(path);
+ let subdirs = [];
+ let files = [];
+
+ let pathEntryIterator = (entry) => {
+ if (entry.isDir) {
+ subdirs.push(entry.path);
+ } else if (extensions.some((extension) => entry.name.endsWith(extension))) {
+ let file = parentDir.clone();
+ file.append(entry.name);
+ // the build system might leave dead symlinks hanging around, which are
+ // returned as part of the directory iterator, but don't actually exist:
+ if (file.exists()) {
+ let uriSpec = getURLForFile(file);
+ files.push(Services.io.newURI(uriSpec, null, null));
+ }
+ } else if (entry.name.endsWith(".ja") || entry.name.endsWith(".jar") ||
+ entry.name.endsWith(".zip") || entry.name.endsWith(".xpi")) {
+ let file = parentDir.clone();
+ file.append(entry.name);
+ for (let extension of extensions) {
+ let jarEntryIterator = generateEntriesFromJarFile(file, extension);
+ files.push(...jarEntryIterator);
+ }
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ Task.spawn(function* () {
+ try {
+ // Iterate through the directory
+ yield iterator.forEach(pathEntryIterator);
+ resolve({files: files, subdirs: subdirs});
+ } catch (ex) {
+ reject(ex);
+ } finally {
+ iterator.close();
+ }
+ });
+ });
+}
+
+/* Helper function to generate a URI spec (NB: not an nsIURI yet!)
+ * given an nsIFile object */
+function getURLForFile(file) {
+ let fileHandler = Services.io.getProtocolHandler("file");
+ fileHandler = fileHandler.QueryInterface(Ci.nsIFileProtocolHandler);
+ return fileHandler.getURLSpecFromActualFile(file);
+}
+
+/**
+ * A generator that generates nsIURIs for particular files found in jar files
+ * like omni.ja.
+ *
+ * @param jarFile an nsIFile object for the jar file that needs checking.
+ * @param extension the extension we're interested in.
+ */
+function* generateEntriesFromJarFile(jarFile, extension) {
+ let zr = new ZipReader(jarFile);
+ let entryEnumerator = zr.findEntries("*" + extension + "$");
+
+ const kURIStart = getURLForFile(jarFile);
+ while (entryEnumerator.hasMore()) {
+ let entry = entryEnumerator.getNext();
+ // Ignore the JS cache which is stored in omni.ja
+ if (entry.startsWith("jsloader") || entry.startsWith("jssubloader")) {
+ continue;
+ }
+ let entryURISpec = "jar:" + kURIStart + "!/" + entry;
+ yield Services.io.newURI(entryURISpec, null, null);
+ }
+ zr.close();
+}
+
+
diff --git a/browser/base/content/test/general/permissions.html b/browser/base/content/test/general/permissions.html
new file mode 100644
index 000000000..46436a006
--- /dev/null
+++ b/browser/base/content/test/general/permissions.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <!-- This page could eventually request permissions from content
+ and make sure that chrome responds appropriately -->
+ <button id="geo" onclick="navigator.geolocation.getCurrentPosition(() => {})">Geolocation</button>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/pinning_headers.sjs b/browser/base/content/test/general/pinning_headers.sjs
new file mode 100644
index 000000000..51496183a
--- /dev/null
+++ b/browser/base/content/test/general/pinning_headers.sjs
@@ -0,0 +1,23 @@
+const INVALIDPIN1 = "pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\";";
+const INVALIDPIN2 = "pin-sha256=\"AAAAAAAAAAAAAAAAAAAAAAAAAj0e1Md7GkYYkVoZWmM=\";";
+const VALIDPIN = "pin-sha256=\"hXweb81C3HnmM2Ai1dnUzFba40UJMhuu8qZmvN/6WWc=\";";
+
+function handleRequest(request, response)
+{
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ switch (request.queryString) {
+ case "zeromaxagevalid":
+ response.setHeader("Public-Key-Pins", "max-age=0;" + VALIDPIN +
+ INVALIDPIN2 + "includeSubdomains");
+ break;
+ case "valid":
+ default:
+ response.setHeader("Public-Key-Pins", "max-age=50000;" + VALIDPIN +
+ INVALIDPIN2 + "includeSubdomains");
+ }
+
+ response.write("Hello world!" + request.host);
+}
diff --git a/browser/base/content/test/general/print_postdata.sjs b/browser/base/content/test/general/print_postdata.sjs
new file mode 100644
index 000000000..4175a2480
--- /dev/null
+++ b/browser/base/content/test/general/print_postdata.sjs
@@ -0,0 +1,22 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream");
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ if (request.method == "GET") {
+ response.write(request.queryString);
+ } else {
+ var body = new BinaryInputStream(request.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0)
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+ var data = String.fromCharCode.apply(null, bytes);
+ response.bodyOutputStream.write(data, data.length);
+ }
+}
diff --git a/browser/base/content/test/general/refresh_header.sjs b/browser/base/content/test/general/refresh_header.sjs
new file mode 100644
index 000000000..327372f9b
--- /dev/null
+++ b/browser/base/content/test/general/refresh_header.sjs
@@ -0,0 +1,24 @@
+/**
+ * Will cause an auto-refresh to the URL provided in the query string
+ * after some delay using the refresh HTTP header.
+ *
+ * Expects the query string to be in the format:
+ *
+ * ?p=[URL of the page to redirect to]&d=[delay]
+ *
+ * Example:
+ *
+ * ?p=http%3A%2F%2Fexample.org%2Fbrowser%2Fbrowser%2Fbase%2Fcontent%2Ftest%2Fgeneral%2Frefresh_meta.sjs&d=200
+ */
+function handleRequest(request, response) {
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let page = query.get("p");
+ let delay = query.get("d");
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "200", "Found");
+ response.setHeader("refresh", `${delay}; url=${page}`);
+ response.write("OK");
+} \ No newline at end of file
diff --git a/browser/base/content/test/general/refresh_meta.sjs b/browser/base/content/test/general/refresh_meta.sjs
new file mode 100644
index 000000000..648fac1a3
--- /dev/null
+++ b/browser/base/content/test/general/refresh_meta.sjs
@@ -0,0 +1,36 @@
+/**
+ * Will cause an auto-refresh to the URL provided in the query string
+ * after some delay using a <meta> tag.
+ *
+ * Expects the query string to be in the format:
+ *
+ * ?p=[URL of the page to redirect to]&d=[delay]
+ *
+ * Example:
+ *
+ * ?p=http%3A%2F%2Fexample.org%2Fbrowser%2Fbrowser%2Fbase%2Fcontent%2Ftest%2Fgeneral%2Frefresh_meta.sjs&d=200
+ */
+function handleRequest(request, response) {
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let page = query.get("p");
+ let delay = query.get("d");
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <META http-equiv='refresh' content='${delay}; url=${page}'>
+ <title>Gonna refresh you, folks.</title>
+ </head>
+ <body>
+ <h1>Wait for it...</h1>
+ </body>
+ </html>`;
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "200", "Found");
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(html);
+} \ No newline at end of file
diff --git a/browser/base/content/test/general/searchSuggestionEngine.sjs b/browser/base/content/test/general/searchSuggestionEngine.sjs
new file mode 100644
index 000000000..1978b4f66
--- /dev/null
+++ b/browser/base/content/test/general/searchSuggestionEngine.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+ let suffixes = ["foo", "bar"];
+ let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
diff --git a/browser/base/content/test/general/searchSuggestionEngine.xml b/browser/base/content/test/general/searchSuggestionEngine.xml
new file mode 100644
index 000000000..3d1f294f5
--- /dev/null
+++ b/browser/base/content/test/general/searchSuggestionEngine.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/base/content/test/general/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/base/content/test/general/searchSuggestionEngine2.xml b/browser/base/content/test/general/searchSuggestionEngine2.xml
new file mode 100644
index 000000000..05644649a
--- /dev/null
+++ b/browser/base/content/test/general/searchSuggestionEngine2.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine2.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/base/content/test/general/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://www.browser-searchSuggestionEngine.com/searchSuggestionEngine2&amp;terms={searchTerms}" rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/base/content/test/general/ssl_error_reports.sjs b/browser/base/content/test/general/ssl_error_reports.sjs
new file mode 100644
index 000000000..e2e5bafc0
--- /dev/null
+++ b/browser/base/content/test/general/ssl_error_reports.sjs
@@ -0,0 +1,91 @@
+const EXPECTED_CHAIN = [
+ "MIIDCjCCAfKgAwIBAgIENUiGYDANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwHhcNMTQxMDAxMjExNDE5WhcNMjQxMDAxMjExNDE5WjAxMS8wLQYDVQQDEyZpbmNsdWRlLXN1YmRvbWFpbnMucGlubmluZy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxYrge8C4eVfTb6/lJ4k/+/4J6wlnWpp5Szxy1MHhsLB+LJh/HRHqkO/tsigT204kTeU3dxuAfQHz0g+Td8dr6KICLLNVFUPw+XjhBV4AtxV8wcprs6EmdBhJgAjkFB4M76BL7/Ow0NfH012WNESn8TTbsp3isgkmrXjTZhWR33vIL1eDNimykp/Os/+JO+x9KVfdCtDCrPwO9Yusial5JiaW7qemRtVuUDL87NSJ7xokPEOSc9luv/fBamZ3rgqf3K6epqg+0o3nNCCcNFnfLW52G0t69+dIjr39WISHnqqZj3Sb7JPU6OmxTd13ByoLkoM3ZUQ2Lpas+RJvQyGXkCAwEAAaM1MDMwMQYDVR0RBCowKIImaW5jbHVkZS1zdWJkb21haW5zLnBpbm5pbmcuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAAmzXfeoOS59FkNABRonFPRyFl7BoGpVJENUteFfTa2pdAhGYdo19Y4uILTTj+vtDAa5yryb5Uvd+YuJnExosbMMkzCrmZ9+VJCJdqUTb+idwk9/sgPl2gtGeRmefB0hXSUFHc/p1CDufSpYOmj9NCUZD2JEsybgJQNulkfAsVnS3lzDcxAwcO+RC/1uJDSiUtcBpWS4FW58liuDYE7PD67kLJHZPVUV2WCMuIl4VM2tKPtvShz1JkZ5UytOLs6jPfviNAk/ftXczaE2/RJgM2MnDX9nGzOxG6ONcVNCljL8avhFBCosutE6i5LYSZR6V14YY/xOn15WDSuWdnIsJCo=",
+ "MIIC2jCCAcKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwHhcNMTQwOTI1MjEyMTU0WhcNMjQwOTI1MjEyMTU0WjAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBT+BwAhO52IWgSIdZZifU9LHOs3IR/+8DCC0WP5d/OuyKlZ6Rqd0tsd3i7durhQyjHSbLf2lJStcnFjcVEbEnNI76RuvlN8xLLn5eV+2Ayr4cZYKztudwRmw+DV/iYAiMSy0hs7m3ssfX7qpoi1aNRjUanwU0VTCPQhF1bEKAC2du+C5Z8e92zN5t87w7bYr7lt+m8197XliXEu+0s9RgnGwGaZ296BIRz6NOoJYTa43n06LU1I1+Z4d6lPdzUFrSR0GBaMhUSurUBtOin3yWiMhg1VHX/KwqGc4als5GyCVXy8HGrA/0zQPOhetxrlhEVAdK/xBt7CZvByj1Rcc7AgMBAAGjEzARMA8GA1UdEwQIMAYBAf8CAQAwDQYJKoZIhvcNAQELBQADggEBAJq/hogSRqzPWTwX4wTn/DVSNdWwFLv53qep9YrSMJ8ZsfbfK9Es4VP4dBLRQAVMJ0Z5mW1I6d/n0KayTanuUBvemYdxPi/qQNSs8UJcllqdhqWzmzAg6a0LxrMnEeKzPBPD6q8PwQ7tYP+B4sBN9tnnsnyPgti9ZiNZn5FwXZliHXseQ7FE9/SqHlLw5LXW3YtKjuti6RmuV6fq3j+D4oeC5vb1mKgIyoTqGN6ze57v8RHi+pQ8Q+kmoUn/L3Z2YmFe4SKN/4WoyXr8TdejpThGOCGCAd3565s5gOx5QfSQX11P8NZKO8hcN0tme3VzmGpHK0Z/6MTmdpNaTwQ6odk="
+ ];
+
+const MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE = -16384;
+
+function parseReport(request) {
+ // read the report from the request
+ let inputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
+ inputStream.init(request.bodyInputStream, 0x01, 0004, 0);
+
+ let body = "";
+ if (inputStream) {
+ while (inputStream.available()) {
+ body = body + inputStream.read(inputStream.available());
+ }
+ }
+ // parse the report
+ return JSON.parse(body);
+}
+
+function handleRequest(request, response) {
+ let report = {};
+ let certChain = [];
+
+ switch (request.queryString) {
+ case "succeed":
+ report = parseReport(request);
+ certChain = report.failedCertChain;
+
+ // ensure the cert chain is what we expect
+ for (idx in certChain) {
+ if (certChain[idx] !== EXPECTED_CHAIN[idx]) {
+ // if the chain differs, send an error response to cause test
+ // failure
+ response.setStatusLine("1.1", 500, "Server error");
+ response.write("<html>The report contained an unexpected chain</html>");
+ return;
+ }
+ }
+
+ if (report.errorCode !== MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE) {
+ response.setStatusLine("1.1", 500, "Server error");
+ response.write("<html>The report contained an unexpected error code</html>");
+ return;
+ }
+
+ // if all is as expected, send the 201 the client expects
+ response.setStatusLine("1.1", 201, "Created");
+ response.write("<html>OK</html>");
+ break;
+ case "nocert":
+ report = parseReport(request);
+ certChain = report.failedCertChain;
+
+ if (certChain && certChain.length > 0) {
+ // We're not expecting a chain; if there is one, send an error
+ response.setStatusLine("1.1", 500, "Server error");
+ response.write("<html>The report contained an unexpected chain</html>");
+ return;
+ }
+
+ // if all is as expected, send the 201 the client expects
+ response.setStatusLine("1.1", 201, "Created");
+ response.write("<html>OK</html>");
+ break;
+ case "badcert":
+ report = parseReport(request);
+ certChain = report.failedCertChain;
+
+ if (!certChain || certChain.length != 2) {
+ response.setStatusLine("1.1", 500, "Server error");
+ response.write("<html>The report contained an unexpected chain</html>");
+ return;
+ }
+
+ // if all is as expected, send the 201 the client expects
+ response.setStatusLine("1.1", 201, "Created");
+ response.write("<html>OK</html>");
+ break;
+ case "error":
+ response.setStatusLine("1.1", 500, "Server error");
+ response.write("<html>server error</html>");
+ break;
+ default:
+ response.setStatusLine("1.1", 500, "Server error");
+ response.write("<html>succeed, nocert or error expected (got " + request.queryString + ")</html>");
+ break;
+ }
+}
diff --git a/browser/base/content/test/general/subtst_contextmenu.html b/browser/base/content/test/general/subtst_contextmenu.html
new file mode 100644
index 000000000..1768f399f
--- /dev/null
+++ b/browser/base/content/test/general/subtst_contextmenu.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Subtest for browser context menu</title>
+</head>
+<body>
+Browser context menu subtest.
+
+<div id="test-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
+<a id="test-link" href="http://mozilla.com">Click the monkey!</a>
+<a id="test-mailto" href="mailto:codemonkey@mozilla.com">Mail the monkey!</a><br>
+<input id="test-input"><br>
+<img id="test-image" src="ctxmenu-image.png">
+<canvas id="test-canvas" width="100" height="100" style="background-color: blue"></canvas>
+<video controls id="test-video-ok" src="video.ogg" width="100" height="100" style="background-color: green"></video>
+<video id="test-audio-in-video" src="audio.ogg" width="100" height="100" style="background-color: red"></video>
+<video controls id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video>
+<video controls id="test-video-bad2" width="100" height="100" style="background-color: yellow">
+ <source src="bogus.duh" type="video/durrrr;">
+</video>
+<iframe id="test-iframe" width="98" height="98" style="border: 1px solid black"></iframe>
+<iframe id="test-video-in-iframe" src="video.ogg" width="98" height="98" style="border: 1px solid black"></iframe>
+<iframe id="test-audio-in-iframe" src="audio.ogg" width="98" height="98" style="border: 1px solid black"></iframe>
+<iframe id="test-image-in-iframe" src="ctxmenu-image.png" width="98" height="98" style="border: 1px solid black"></iframe>
+<textarea id="test-textarea">chssseesbbbie</textarea> <!-- a weird word which generates only one suggestion -->
+<div id="test-contenteditable" contenteditable="true">chssseefsbbbie</div> <!-- a more weird word which generates no suggestions -->
+<div id="test-contenteditable-spellcheck-false" contenteditable="true" spellcheck="false">test</div> <!-- No Check Spelling menu item -->
+<div id="test-dom-full-screen">DOM full screen FTW</div>
+<div contextmenu="myMenu">
+ <p id="test-pagemenu" hopeless="true">I've got a context menu!</p>
+ <menu id="myMenu" type="context">
+ <menuitem label="Plain item" onclick="document.getElementById('test-pagemenu').removeAttribute('hopeless');"></menuitem>
+ <menuitem label="Disabled item" disabled></menuitem>
+ <menuitem> Item w/ textContent</menuitem>
+ <menu>
+ <menuitem type="checkbox" label="Checkbox" checked></menuitem>
+ </menu>
+ <menu>
+ <menuitem type="radio" label="Radio1" checked></menuitem>
+ <menuitem type="radio" label="Radio2"></menuitem>
+ <menuitem type="radio" label="Radio3"></menuitem>
+ </menu>
+ <menu>
+ <menuitem label="Item w/ icon" icon="favicon.ico"></menuitem>
+ <menuitem label="Item w/ bad icon" icon="data://www.mozilla.org/favicon.ico"></menuitem>
+ </menu>
+ <menu label="Submenu">
+ <menuitem type="radio" label="Radio1" radiogroup="rg"></menuitem>
+ <menuitem type="radio" label="Radio2" checked radiogroup="rg"></menuitem>
+ <menuitem type="radio" label="Radio3" radiogroup="rg"></menuitem>
+ <menu>
+ <menuitem type="checkbox" label="Checkbox"></menuitem>
+ </menu>
+ </menu>
+ <menu hidden>
+ <menuitem label="Bogus item"></menuitem>
+ </menu>
+ <menu>
+ </menu>
+ <menuitem label="Hidden item" hidden></menuitem>
+ <menuitem></menuitem>
+ </menu>
+</div>
+<div id="test-select-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
+<div id="test-select-text-link">http://mozilla.com</div>
+<a id="test-image-link" href="#"><img src="ctxmenu-image.png"></a>
+<input id="test-select-input-text" type="text" value="input">
+<input id="test-select-input-text-type-password" type="password" value="password">
+<embed id="test-plugin" style="width: 200px; height: 200px;" type="application/x-test"></embed>
+<img id="test-longdesc" src="ctxmenu-image.png" longdesc="http://www.mozilla.org"></embed>
+<iframe id="test-srcdoc" width="98" height="98" srcdoc="Hello World" style="border: 1px solid black"></iframe>
+</body>
+</html>
diff --git a/browser/base/content/test/general/subtst_contextmenu_input.html b/browser/base/content/test/general/subtst_contextmenu_input.html
new file mode 100644
index 000000000..c5be977ea
--- /dev/null
+++ b/browser/base/content/test/general/subtst_contextmenu_input.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Subtest for browser context menu</title>
+</head>
+<body>
+ Browser context menu subtest.
+ <input id="input_text">
+ <input id="input_spellcheck_no_value">
+ <input id="input_spellcheck_incorrect" spellcheck="true" value="prodkjfgigrty">
+ <input id="input_spellcheck_correct" spellcheck="true" value="foo">
+ <input id="input_disabled" disabled="true">
+ <input id="input_password">
+ <input id="input_email" type="email">
+ <input id="input_tel" type="tel">
+ <input id="input_url" type="url">
+ <input id="input_number" type="number">
+ <input id="input_date" type="date">
+ <input id="input_time" type="time">
+ <input id="input_color" type="color">
+ <input id="input_range" type="range">
+ <input id="input_search" type="search">
+ <input id="input_datetime" type="datetime">
+ <input id="input_month" type="month">
+ <input id="input_week" type="week">
+ <input id="input_datetime-local" type="datetime-local">
+ <input id="input_readonly" readonly="true">
+</body>
+</html>
diff --git a/browser/base/content/test/general/subtst_contextmenu_xul.xul b/browser/base/content/test/general/subtst_contextmenu_xul.xul
new file mode 100644
index 000000000..5a2ab42e8
--- /dev/null
+++ b/browser/base/content/test/general/subtst_contextmenu_xul.xul
@@ -0,0 +1,9 @@
+<?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/. -->
+
+<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <label id="test-xul-text-link-label" class="text-link" value="XUL text-link label" href="https://www.mozilla.com"/>
+</page>
diff --git a/browser/base/content/test/general/svg_image.html b/browser/base/content/test/general/svg_image.html
new file mode 100644
index 000000000..7ab17c33a
--- /dev/null
+++ b/browser/base/content/test/general/svg_image.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test for page info svg images</title>
+ </head>
+ <body>
+ <svg width="20" height="20">
+ <image xlink:href="title_test.svg" width="20" height="20">
+ </svg>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/test-mixedcontent-securityerrors.html b/browser/base/content/test/general/test-mixedcontent-securityerrors.html
new file mode 100644
index 000000000..cb8cfdaaf
--- /dev/null
+++ b/browser/base/content/test/general/test-mixedcontent-securityerrors.html
@@ -0,0 +1,21 @@
+<!--
+ Bug 875456 - Log mixed content messages from the Mixed Content Blocker to the
+ Security Pane in the Web Console
+-->
+
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ <title>Mixed Content test - http on https</title>
+ <script src="testscript.js"></script>
+ <!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ -->
+ </head>
+ <body>
+ <iframe src="http://example.com"></iframe>
+ <img src="http://example.com/tests/image/test/mochitest/blue.png"></img>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/test_bug364677.html b/browser/base/content/test/general/test_bug364677.html
new file mode 100644
index 000000000..67b9729d1
--- /dev/null
+++ b/browser/base/content/test/general/test_bug364677.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=364677
+-->
+<head>
+ <title>Test for Bug 364677</title>
+ <script type="text/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=364677">Mozilla Bug 364677</a>
+<p id="display"><iframe id="testFrame" src="bug364677-data.xml"></iframe></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 364677 **/
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ is(SpecialPowers.wrap($("testFrame")).contentDocument.documentElement.id, "feedHandler",
+ "Feed served as text/xml without a channel/link should have been sniffed");
+});
+addLoadEvent(SimpleTest.finish);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/browser/base/content/test/general/test_bug395533.html b/browser/base/content/test/general/test_bug395533.html
new file mode 100644
index 000000000..ad6209047
--- /dev/null
+++ b/browser/base/content/test/general/test_bug395533.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=395533
+-->
+<head>
+ <title>Test for Bug 395533</title>
+ <script type="text/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=395533">Mozilla Bug 395533</a>
+<p id="display"><iframe id="testFrame" src="bug395533-data.txt"></iframe></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 395533 **/
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Need privs because the feed seems to have an about:feeds principal or some
+ // such. It's not same-origin with us in any case.
+ is(SpecialPowers.wrap($("testFrame")).contentDocument.documentElement.id, "",
+ "Text got sniffed as a feed?");
+});
+addLoadEvent(SimpleTest.finish);
+
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/browser/base/content/test/general/test_bug435035.html b/browser/base/content/test/general/test_bug435035.html
new file mode 100644
index 000000000..a6624db15
--- /dev/null
+++ b/browser/base/content/test/general/test_bug435035.html
@@ -0,0 +1 @@
+<img src="http://example.com/browser/browser/base/content/test/general/moz.png">
diff --git a/browser/base/content/test/general/test_bug462673.html b/browser/base/content/test/general/test_bug462673.html
new file mode 100644
index 000000000..d864990e4
--- /dev/null
+++ b/browser/base/content/test/general/test_bug462673.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+<script>
+var w;
+function openIt() {
+ w = window.open("", "window2");
+}
+function closeIt() {
+ if (w) {
+ w.close();
+ w = null;
+ }
+}
+</script>
+</head>
+<body onload="openIt();" onunload="closeIt();">
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_bug628179.html b/browser/base/content/test/general/test_bug628179.html
new file mode 100644
index 000000000..d35e17a7c
--- /dev/null
+++ b/browser/base/content/test/general/test_bug628179.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test for closing the Find bar in subdocuments</title>
+ </head>
+ <body>
+ <iframe id=iframe src="http://example.com/" width=320 height=240></iframe>
+ </body>
+</html>
+
diff --git a/browser/base/content/test/general/test_bug839103.html b/browser/base/content/test/general/test_bug839103.html
new file mode 100644
index 000000000..3639d4bda
--- /dev/null
+++ b/browser/base/content/test/general/test_bug839103.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Document for Bug 839103</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style></style>
+</head>
+<body>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_bug959531.html b/browser/base/content/test/general/test_bug959531.html
new file mode 100644
index 000000000..e749b198a
--- /dev/null
+++ b/browser/base/content/test/general/test_bug959531.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test for content page with settings button</title>
+ </head>
+ <body>
+ <button name="settings" id="settings">Settings</button>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/test_mcb_double_redirect_image.html b/browser/base/content/test/general/test_mcb_double_redirect_image.html
new file mode 100644
index 000000000..1b54774ec
--- /dev/null
+++ b/browser/base/content/test/general/test_mcb_double_redirect_image.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 7-9 for Bug 1082837 - See file browser_mcb_redirect.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1082837
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1082837</title>
+ <script>
+ function image_loaded() {
+ document.getElementById("mctestdiv").innerHTML = "image loaded";
+ }
+ function image_blocked() {
+ document.getElementById("mctestdiv").innerHTML = "image blocked";
+ }
+ </script>
+</head>
+<body>
+ <div id="mctestdiv"></div>
+ <img src="https://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs?image_redirect_http_sjs" onload="image_loaded()" onerror="image_blocked()" ></image>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_mcb_redirect.html b/browser/base/content/test/general/test_mcb_redirect.html
new file mode 100644
index 000000000..88af791a3
--- /dev/null
+++ b/browser/base/content/test/general/test_mcb_redirect.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 418354 - See file browser_mcb_redirect.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=418354
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 418354</title>
+</head>
+<body>
+ <div id="mctestdiv">script blocked</div>
+ <script src="https://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs?script" ></script>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_mcb_redirect.js b/browser/base/content/test/general/test_mcb_redirect.js
new file mode 100644
index 000000000..48538c940
--- /dev/null
+++ b/browser/base/content/test/general/test_mcb_redirect.js
@@ -0,0 +1,5 @@
+/*
+ * Once the mixed content blocker is disabled for the page, this scripts loads
+ * and updates the text inside the div container.
+ */
+document.getElementById("mctestdiv").innerHTML = "script executed";
diff --git a/browser/base/content/test/general/test_mcb_redirect.sjs b/browser/base/content/test/general/test_mcb_redirect.sjs
new file mode 100644
index 000000000..9a1811dfa
--- /dev/null
+++ b/browser/base/content/test/general/test_mcb_redirect.sjs
@@ -0,0 +1,22 @@
+function handleRequest(request, response) {
+ var page = "<!DOCTYPE html><html><body>bug 418354 and bug 1082837</body></html>";
+
+ if (request.queryString === "script") {
+ var redirect = "http://example.com/browser/browser/base/content/test/general/test_mcb_redirect.js";
+ response.setHeader("Cache-Control", "no-cache", false);
+ } else if (request.queryString === "image_http") {
+ var redirect = "http://example.com/tests/image/test/mochitest/blue.png";
+ response.setHeader("Cache-Control", "max-age=3600", false);
+ } else if (request.queryString === "image_redirect_http_sjs") {
+ var redirect = "http://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs?image_redirect_https";
+ response.setHeader("Cache-Control", "max-age=3600", false);
+ } else if (request.queryString === "image_redirect_https") {
+ var redirect = "https://example.com/tests/image/test/mochitest/blue.png";
+ response.setHeader("Cache-Control", "max-age=3600", false);
+ }
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setStatusLine(request.httpVersion, "302", "Found");
+ response.setHeader("Location", redirect, false);
+ response.write(page);
+}
diff --git a/browser/base/content/test/general/test_mcb_redirect_image.html b/browser/base/content/test/general/test_mcb_redirect_image.html
new file mode 100644
index 000000000..c70cd8987
--- /dev/null
+++ b/browser/base/content/test/general/test_mcb_redirect_image.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3-6 for Bug 1082837 - See file browser_mcb_redirect.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1082837
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1082837</title>
+ <script>
+ function image_loaded() {
+ document.getElementById("mctestdiv").innerHTML = "image loaded";
+ }
+ function image_blocked() {
+ document.getElementById("mctestdiv").innerHTML = "image blocked";
+ }
+ </script>
+</head>
+<body>
+ <div id="mctestdiv"></div>
+ <img src="https://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs?image_http" onload="image_loaded()" onerror="image_blocked()" ></image>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_no_mcb_on_http_site_font.css b/browser/base/content/test/general/test_no_mcb_on_http_site_font.css
new file mode 100644
index 000000000..68a6954cc
--- /dev/null
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_font.css
@@ -0,0 +1,10 @@
+@font-face {
+ font-family: testFont;
+ src: url(http://example.com/browser/devtools/client/fontinspector/test/browser_font.woff);
+}
+body {
+ font-family: Arial;
+}
+div {
+ font-family: testFont;
+}
diff --git a/browser/base/content/test/general/test_no_mcb_on_http_site_font.html b/browser/base/content/test/general/test_no_mcb_on_http_site_font.html
new file mode 100644
index 000000000..28a9cb2c0
--- /dev/null
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_font.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 2 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 2 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/general/test_no_mcb_on_http_site_font.css" />
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ function checkLoadStates() {
+ var ui = SpecialPowers.wrap(window)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell)
+ .securityUI;
+
+ var loadedMixedActive = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page with https css that includes http font";
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that includes http font
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_no_mcb_on_http_site_font2.css b/browser/base/content/test/general/test_no_mcb_on_http_site_font2.css
new file mode 100644
index 000000000..f73b573b4
--- /dev/null
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_font2.css
@@ -0,0 +1 @@
+@import url(http://example.com/browser/browser/base/content/test/general/test_no_mcb_on_http_site_font.css);
diff --git a/browser/base/content/test/general/test_no_mcb_on_http_site_font2.html b/browser/base/content/test/general/test_no_mcb_on_http_site_font2.html
new file mode 100644
index 000000000..2b3164902
--- /dev/null
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_font2.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 3 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 3 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/general/test_no_mcb_on_http_site_font2.css" />
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ function checkLoadStates() {
+ var ui = SpecialPowers.wrap(window)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell)
+ .securityUI;
+
+ var loadedMixedActive = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page ";
+ newValue += "with https css that imports another http css which includes http font";
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that imports another http css which includes http font
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_no_mcb_on_http_site_img.css b/browser/base/content/test/general/test_no_mcb_on_http_site_img.css
new file mode 100644
index 000000000..d045e21ba
--- /dev/null
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_img.css
@@ -0,0 +1,3 @@
+#testDiv {
+ background: url(http://example.com/tests/image/test/mochitest/blue.png)
+}
diff --git a/browser/base/content/test/general/test_no_mcb_on_http_site_img.html b/browser/base/content/test/general/test_no_mcb_on_http_site_img.html
new file mode 100644
index 000000000..741573260
--- /dev/null
+++ b/browser/base/content/test/general/test_no_mcb_on_http_site_img.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test 1 for Bug 909920 - See file browser_no_mcb_on_http_site.js for description.
+ https://bugzilla.mozilla.org/show_bug.cgi?id=909920
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test 1 for Bug 909920</title>
+ <link rel="stylesheet" type="text/css" href="https://example.com/browser/browser/base/content/test/general/test_no_mcb_on_http_site_img.css" />
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="text/javascript">
+ function checkLoadStates() {
+ var ui = SpecialPowers.wrap(window)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+ .QueryInterface(SpecialPowers.Ci.nsIDocShell)
+ .securityUI;
+
+ var loadedMixedActive = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT);
+ is(loadedMixedActive, false, "OK: Should not load mixed active content!");
+
+ var blockedMixedActive = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT);
+ is(blockedMixedActive, false, "OK: Should not block mixed active content!");
+
+ var loadedMixedDisplay = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
+ is(loadedMixedDisplay, false, "OK: Should not load mixed display content!");
+
+ var blockedMixedDisplay = ui &&
+ !!(ui.state & SpecialPowers.Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT);
+ is(blockedMixedDisplay, false, "OK: Should not block mixed display content!");
+
+ var newValue = "Verifying MCB does not trigger warning/error for an http page with https css that includes http image";
+ document.getElementById("testDiv").innerHTML = newValue;
+ }
+</script>
+</head>
+<body onload="checkLoadStates()">
+ <div class="testDiv" id="testDiv">
+ Testing MCB does not trigger warning/error for an http page with https css that includes http image
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_offlineNotification.html b/browser/base/content/test/general/test_offlineNotification.html
new file mode 100644
index 000000000..4f78184b4
--- /dev/null
+++ b/browser/base/content/test/general/test_offlineNotification.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=462856
+-->
+<head>
+ <title>Test offline app notification</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="offlineByDefault.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display">
+<!-- Load the test frame twice from the same domain,
+ to make sure we get notifications for both -->
+<iframe name="testFrame" src="offlineChild.html"></iframe>
+<iframe name="testFrame2" src="offlineChild2.html"></iframe>
+<!-- Load from another domain to make sure we get a second allow/deny
+ notification -->
+<iframe name="testFrame3" src="http://example.com/tests/browser/base/content/test/general/offlineChild.html"></iframe>
+
+<iframe id="eventsTestFrame" src="offlineEvent.html"></iframe>
+
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+const Cc = SpecialPowers.Cc;
+
+var numFinished = 0;
+
+window.addEventListener("message", function(event) {
+ is(event.data, "success", "Child was successfully cached.");
+
+ if (++numFinished == 3) {
+ // Clean up after ourself
+ var pm = Cc["@mozilla.org/permissionmanager;1"].
+ getService(SpecialPowers.Ci.nsIPermissionManager);
+ var ioService = Cc["@mozilla.org/network/io-service;1"]
+ .getService(SpecialPowers.Ci.nsIIOService);
+ var uri1 = ioService.newURI(frames.testFrame.location, null, null);
+ var uri2 = ioService.newURI(frames.testFrame3.location, null, null);
+
+ var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(SpecialPowers.Ci.nsIScriptSecurityManager);
+ var principal1 = ssm.createCodebasePrincipal(uri1, {});
+ var principal2 = ssm.createCodebasePrincipal(uri2, {});
+
+ pm.removeFromPrincipal(principal1, "offline-app");
+ pm.removeFromPrincipal(principal2, "offline-app");
+
+ offlineByDefault.reset();
+
+ SimpleTest.finish();
+ }
+ }, false);
+
+var count = 0;
+var expectedEvent = "";
+function eventHandler(evt) {
+ ++count;
+ is(evt.type, expectedEvent, "Wrong event!");
+}
+
+function testEventHandling() {
+ var events = [ "checking",
+ "error",
+ "noupdate",
+ "downloading",
+ "progress",
+ "updateready",
+ "cached",
+ "obsolete"];
+ var w = document.getElementById("eventsTestFrame").contentWindow;
+ var e;
+ for (var i = 0; i < events.length; ++i) {
+ count = 0;
+ expectedEvent = events[i];
+ e = w.document.createEvent("event");
+ e.initEvent(expectedEvent, true, true);
+ w.applicationCache["on" + expectedEvent] = eventHandler;
+ w.applicationCache.addEventListener(expectedEvent, eventHandler, true);
+ w.applicationCache.dispatchEvent(e);
+ is(count, 2, "Wrong number events!");
+ w.applicationCache["on" + expectedEvent] = null;
+ w.applicationCache.removeEventListener(expectedEvent, eventHandler, true);
+ w.applicationCache.dispatchEvent(e);
+ is(count, 2, "Wrong number events!");
+ }
+
+ // Test some random event.
+ count = 0;
+ expectedEvent = "foo";
+ e = w.document.createEvent("event");
+ e.initEvent(expectedEvent, true, true);
+ w.applicationCache.addEventListener(expectedEvent, eventHandler, true);
+ w.applicationCache.dispatchEvent(e);
+ is(count, 1, "Wrong number events!");
+ w.applicationCache.removeEventListener(expectedEvent, eventHandler, true);
+ w.applicationCache.dispatchEvent(e);
+ is(count, 1, "Wrong number events!");
+}
+
+function loaded() {
+ testEventHandling();
+
+ // Click the notification panel's "Allow" button. This should kick
+ // off updates, which will eventually lead to getting messages from
+ // the children.
+ var wm = SpecialPowers.Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(SpecialPowers.Ci.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("navigator:browser");
+ var panel = win.PopupNotifications.panel;
+ is(panel.childElementCount, 2, "2 notifications being displayed");
+ panel.firstElementChild.button.click();
+
+ // should have dismissed one of the notifications.
+ is(panel.childElementCount, 1, "1 notification now being displayed");
+ panel.firstElementChild.button.click();
+}
+
+SimpleTest.waitForFocus(loaded);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_offline_gzip.html b/browser/base/content/test/general/test_offline_gzip.html
new file mode 100644
index 000000000..a18d6604e
--- /dev/null
+++ b/browser/base/content/test/general/test_offline_gzip.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=501422
+
+When content which was transported over the network with
+Content-Type: gzip is added to the offline
+cache, it can be fetched from the cache successfully.
+-->
+<head>
+ <title>Test gzipped offline resources</title>
+ <meta charset="utf-8">
+</head>
+<body>
+<p id="display">
+<iframe name="testFrame" src="gZipOfflineChild.html"></iframe>
+
+<div id="content" style="display: none">
+</div>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_process_flags_chrome.html b/browser/base/content/test/general/test_process_flags_chrome.html
new file mode 100644
index 000000000..adcbf0340
--- /dev/null
+++ b/browser/base/content/test/general/test_process_flags_chrome.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p>chrome: test page</p>
+<p><a href="chrome://mochitests/content/browser/browser/base/content/test/general/test_process_flags_chrome.html">chrome</a></p>
+<p><a href="chrome://mochitests-any/content/browser/browser/base/content/test/general/test_process_flags_chrome.html">canremote</a></p>
+<p><a href="chrome://mochitests-content/content/browser/browser/base/content/test/general/test_process_flags_chrome.html">mustremote</a></p>
+</body>
+</html>
diff --git a/browser/base/content/test/general/test_remoteTroubleshoot.html b/browser/base/content/test/general/test_remoteTroubleshoot.html
new file mode 100644
index 000000000..7ba1c5268
--- /dev/null
+++ b/browser/base/content/test/general/test_remoteTroubleshoot.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<script>
+// This test is run multiple times, once with only strings allowed through the
+// WebChannel, and once with objects allowed. This function allows us to handle
+// both cases without too much pain.
+function makeDetails(object) {
+ if (window.location.search.indexOf("object") >= 0) {
+ return object;
+ }
+ return JSON.stringify(object)
+}
+// Add a listener for responses to our remote requests.
+window.addEventListener("WebChannelMessageToContent", function (event) {
+ if (event.detail.id == "remote-troubleshooting") {
+ // Send what we got back to the test.
+ var backEvent = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: makeDetails({
+ id: "test-remote-troubleshooting-backchannel",
+ message: {
+ message: event.detail.message,
+ },
+ }),
+ });
+ window.dispatchEvent(backEvent);
+ // and stick it in our DOM just for good measure/diagnostics.
+ document.getElementById("troubleshooting").textContent =
+ JSON.stringify(event.detail.message, null, 2);
+ }
+});
+
+// Make a request for the troubleshooting data as we load.
+window.onload = function() {
+ var event = new window.CustomEvent("WebChannelMessageToChrome", {
+ detail: makeDetails({
+ id: "remote-troubleshooting",
+ message: {
+ command: "request",
+ },
+ }),
+ });
+ window.dispatchEvent(event);
+}
+</script>
+
+<body>
+ <pre id="troubleshooting"/>
+</body>
+
+</html>
diff --git a/browser/base/content/test/general/title_test.svg b/browser/base/content/test/general/title_test.svg
new file mode 100644
index 000000000..7638fd5cc
--- /dev/null
+++ b/browser/base/content/test/general/title_test.svg
@@ -0,0 +1,59 @@
+<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>This is a root SVG element's title</title>
+ <foreignObject>
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <body>
+ <svg xmlns="http://www.w3.org/2000/svg" id="svg1">
+ <title>This is a non-root SVG element title</title>
+ </svg>
+ </body>
+ </html>
+ </foreignObject>
+ <text id="text1" x="10px" y="32px" font-size="24px">
+ This contains only &lt;title&gt;
+ <title>
+
+
+ This is a title
+
+ </title>
+ </text>
+ <text id="text2" x="10px" y="96px" font-size="24px">
+ This contains only &lt;desc&gt;
+ <desc>This is a desc</desc>
+ </text>
+ <text id="text3" x="10px" y="128px" font-size="24px" title="ignored for SVG">
+ This contains nothing.
+ </text>
+ <a id="link1" xlink:href="#">
+ This link contains &lt;title&gt;
+ <title>
+ This is a title
+ </title>
+ <text id="text4" x="10px" y="192px" font-size="24px">
+ </text>
+ </a>
+ <a id="link2" xlink:href="#">
+ <text x="10px" y="192px" font-size="24px">
+ This text contains &lt;title&gt;
+ <title>
+ This is a title
+ </title>
+ </text>
+ </a>
+ <a id="link3" xlink:href="#" xlink:title="This is an xlink:title attribute">
+ <text x="10px" y="224px" font-size="24px">
+ This link contains &lt;title&gt; &amp; xlink:title attr.
+ <title>This is a title</title>
+ </text>
+ </a>
+ <a id="link4" xlink:href="#" xlink:title="This is an xlink:title attribute">
+ <text x="10px" y="256px" font-size="24px">
+ This link contains xlink:title attr.
+ </text>
+ </a>
+ <text id="text5" x="10px" y="160px" font-size="24px"
+ xlink:title="This is an xlink:title attribute but it isn't on a link" >
+ This contains nothing.
+ </text>
+</svg>
diff --git a/browser/base/content/test/general/trackingPage.html b/browser/base/content/test/general/trackingPage.html
new file mode 100644
index 000000000..17f0e459e
--- /dev/null
+++ b/browser/base/content/test/general/trackingPage.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <iframe src="http://tracking.example.com/"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/unknownContentType_file.pif b/browser/base/content/test/general/unknownContentType_file.pif
new file mode 100644
index 000000000..9353d1312
--- /dev/null
+++ b/browser/base/content/test/general/unknownContentType_file.pif
@@ -0,0 +1 @@
+Dummy content for unknownContentType_dialog_layout_data.pif
diff --git a/browser/base/content/test/general/unknownContentType_file.pif^headers^ b/browser/base/content/test/general/unknownContentType_file.pif^headers^
new file mode 100644
index 000000000..09b22facc
--- /dev/null
+++ b/browser/base/content/test/general/unknownContentType_file.pif^headers^
@@ -0,0 +1 @@
+Content-Type: application/octet-stream
diff --git a/browser/base/content/test/general/video.ogg b/browser/base/content/test/general/video.ogg
new file mode 100644
index 000000000..ac7ece351
--- /dev/null
+++ b/browser/base/content/test/general/video.ogg
Binary files differ
diff --git a/browser/base/content/test/general/web_video.html b/browser/base/content/test/general/web_video.html
new file mode 100644
index 000000000..467fb0ce1
--- /dev/null
+++ b/browser/base/content/test/general/web_video.html
@@ -0,0 +1,10 @@
+<html>
+ <head>
+ <title>Document with Web Video</title>
+ </head>
+ <body>
+ This document has some web video in it.
+ <br>
+ <video src="web_video1.ogv" id="video1"> </video>
+ </body>
+</html>
diff --git a/browser/base/content/test/general/web_video1.ogv b/browser/base/content/test/general/web_video1.ogv
new file mode 100644
index 000000000..093158432
--- /dev/null
+++ b/browser/base/content/test/general/web_video1.ogv
Binary files differ
diff --git a/browser/base/content/test/general/web_video1.ogv^headers^ b/browser/base/content/test/general/web_video1.ogv^headers^
new file mode 100644
index 000000000..4511e9255
--- /dev/null
+++ b/browser/base/content/test/general/web_video1.ogv^headers^
@@ -0,0 +1,3 @@
+Content-Disposition: filename="web-video1-expectedName.ogv"
+Content-Type: video/ogg
+
diff --git a/browser/base/content/test/general/zoom_test.html b/browser/base/content/test/general/zoom_test.html
new file mode 100644
index 000000000..bf80490ca
--- /dev/null
+++ b/browser/base/content/test/general/zoom_test.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=416661
+-->
+ <head>
+ <title>Test for zoom setting</title>
+
+ </head>
+ <body>
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=416661">Bug 416661</a>
+ <p>Site specific zoom settings should not apply to image documents.</p>
+ </body>
+</html>
diff --git a/browser/base/content/test/newtab/.eslintrc.js b/browser/base/content/test/newtab/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/newtab/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/newtab/browser.ini b/browser/base/content/test/newtab/browser.ini
new file mode 100644
index 000000000..2d14d208d
--- /dev/null
+++ b/browser/base/content/test/newtab/browser.ini
@@ -0,0 +1,55 @@
+[DEFAULT]
+skip-if = (os == 'linux') # Bug 1243103, Bug 1243398, etc.
+support-files =
+ head.js
+
+[browser_newtab_1188015.js]
+[browser_newtab_background_captures.js]
+[browser_newtab_block.js]
+[browser_newtab_bug721442.js]
+[browser_newtab_bug722273.js]
+skip-if = (os == "mac" && debug) # temporary skip-if due to increase in intermittent failures on Mac debug - bug 1119906
+[browser_newtab_bug723102.js]
+[browser_newtab_bug723121.js]
+[browser_newtab_bug725996.js]
+[browser_newtab_bug734043.js]
+[browser_newtab_bug735987.js]
+[browser_newtab_bug752841.js]
+[browser_newtab_bug765628.js]
+[browser_newtab_bug876313.js]
+[browser_newtab_bug991111.js]
+[browser_newtab_bug991210.js]
+[browser_newtab_bug998387.js]
+[browser_newtab_bug1145428.js]
+[browser_newtab_bug1178586.js]
+[browser_newtab_bug1194895.js]
+[browser_newtab_bug1271075.js]
+[browser_newtab_disable.js]
+[browser_newtab_drag_drop.js]
+[browser_newtab_drag_drop_ext.js]
+# temporary until determine why more intermittent on VM
+subsuite = clipboard
+[browser_newtab_drop_preview.js]
+[browser_newtab_enhanced.js]
+[browser_newtab_focus.js]
+[browser_newtab_perwindow_private_browsing.js]
+[browser_newtab_reportLinkAction.js]
+[browser_newtab_reflow_load.js]
+support-files =
+ content-reflows.js
+[browser_newtab_search.js]
+support-files =
+ searchEngineNoLogo.xml
+ searchEngineFavicon.xml
+ searchEngine1xLogo.xml
+ searchEngine2xLogo.xml
+ searchEngine1x2xLogo.xml
+ ../general/searchSuggestionEngine.xml
+ ../general/searchSuggestionEngine.sjs
+[browser_newtab_sponsored_icon_click.js]
+skip-if = true # Bug 1314619
+[browser_newtab_undo.js]
+# temporary until determine why more intermittent on VM
+subsuite = clipboard
+[browser_newtab_unpin.js]
+[browser_newtab_update.js]
diff --git a/browser/base/content/test/newtab/browser_newtab_1188015.js b/browser/base/content/test/newtab/browser_newtab_1188015.js
new file mode 100644
index 000000000..f19aae1b9
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_1188015.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+ "directory": [{
+ url: "http://example1.com/",
+ enhancedImageURI: "data:image/png;base64,helloWORLD2",
+ title: "title1",
+ type: "affiliate",
+ titleBgColor: "green"
+ }]
+});
+
+add_task(function* () {
+ yield pushPrefs(["browser.newtab.preload", false]);
+
+ // Make the page have a directory link
+ yield setLinks([]);
+ yield* addNewTabPageTab();
+
+ let color = yield performOnCell(0, cell => {
+ return cell.node.querySelector(".newtab-title").style.backgroundColor;
+ });
+
+ is(color, "green", "title bg color is green");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_background_captures.js b/browser/base/content/test/newtab/browser_newtab_background_captures.js
new file mode 100644
index 000000000..5e838196e
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_background_captures.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Verifies that hidden, pre-loaded newtabs don't allow background captures, and
+ * when unhidden, do allow background captures.
+ */
+
+const CAPTURE_PREF = "browser.pagethumbnails.capturing_disabled";
+
+add_task(function* () {
+ let imports = {};
+ Cu.import("resource://gre/modules/PageThumbs.jsm", imports);
+
+ // Disable captures.
+ yield pushPrefs([CAPTURE_PREF, false]);
+
+ // Make sure the thumbnail doesn't exist yet.
+ let url = "http://example.com/";
+ let path = imports.PageThumbsStorage.getFilePathForURL(url);
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(path);
+ try {
+ file.remove(false);
+ }
+ catch (err) {}
+
+ // Add a top site.
+ yield setLinks("-1");
+
+ // We need a handle to a hidden, pre-loaded newtab so we can verify that it
+ // doesn't allow background captures. Ensure we have a preloaded browser.
+ gBrowser._createPreloadBrowser();
+
+ // Wait for the preloaded browser to load.
+ if (gBrowser._preloadedBrowser.contentDocument.readyState != "complete") {
+ yield BrowserTestUtils.waitForEvent(gBrowser._preloadedBrowser, "load", true);
+ }
+
+ // We're now ready to use the preloaded browser.
+ BrowserOpenTab();
+ let tab = gBrowser.selectedTab;
+
+ let thumbnailCreatedPromise = new Promise(resolve => {
+ // Showing the preloaded tab should trigger thumbnail capture.
+ Services.obs.addObserver(function onCreate(subj, topic, data) {
+ if (data != url)
+ return;
+ Services.obs.removeObserver(onCreate, "page-thumbnail:create");
+ ok(true, "thumbnail created after preloaded tab was shown");
+
+ resolve();
+ }, "page-thumbnail:create", false);
+ });
+
+ // Enable captures.
+ yield pushPrefs([CAPTURE_PREF, false]);
+
+ yield thumbnailCreatedPromise;
+
+ // Test finished!
+ gBrowser.removeTab(tab);
+ file.remove(false);
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_block.js b/browser/base/content/test/newtab/browser_newtab_block.js
new file mode 100644
index 000000000..70656462a
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_block.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(2);
+
+/*
+ * These tests make sure that blocking/removing sites from the grid works
+ * as expected. Pinned tabs should not be moved. Gaps will be re-filled
+ * if more sites are available.
+ */
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+ "suggested": [{
+ url: "http://suggested.com/",
+ imageURI: "data:image/png;base64,helloWORLD3",
+ title: "title",
+ type: "affiliate",
+ adgroup_name: "test",
+ frecent_sites: ["example0.com"]
+ }]
+});
+
+add_task(function* () {
+ let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+ DirectoryLinksProvider.getFrecentSitesName = () => "";
+ let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
+ NewTabUtils.isTopPlacesSite = (site) => false;
+
+ // we remove sites and expect the gaps to be filled as long as there still
+ // are some sites available
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ yield customizeNewTabPage("enhanced"); // Toggle enhanced off
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ yield blockCell(4);
+ yield* checkGrid("0,1,2,3,5,6,7,8,9");
+
+ yield blockCell(4);
+ yield* checkGrid("0,1,2,3,6,7,8,9,");
+
+ yield blockCell(4);
+ yield* checkGrid("0,1,2,3,7,8,9,,");
+
+ // we removed a pinned site
+ yield restore();
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",1");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1p,2,3,4,5,6,7,8");
+
+ yield blockCell(1);
+ yield* checkGrid("0,2,3,4,5,6,7,8,");
+
+ // we remove the last site on the grid (which is pinned) and expect the gap
+ // to be re-filled and the new site to be unpinned
+ yield restore();
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+ setPinnedLinks(",,,,,,,,8");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8p");
+
+ yield blockCell(8);
+ yield* checkGrid("0,1,2,3,4,5,6,7,9");
+
+ // we remove the first site on the grid with the last one pinned. all cells
+ // but the last one should shift to the left and a new site fades in
+ yield restore();
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+ setPinnedLinks(",,,,,,,,8");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8p");
+
+ yield blockCell(0);
+ yield* checkGrid("1,2,3,4,5,6,7,9,8p");
+
+ // Test that blocking the targeted site also removes its associated suggested tile
+ NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
+ yield restore();
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+ yield customizeNewTabPage("enhanced"); // Toggle enhanced on
+ yield* addNewTabPageTab();
+
+ yield* checkGrid("http://suggested.com/,0,1,2,3,4,5,6,7,8,9");
+
+ yield blockCell(1);
+ yield* addNewTabPageTab();
+ yield* checkGrid("1,2,3,4,5,6,7,8,9");
+ DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug1145428.js b/browser/base/content/test/newtab/browser_newtab_bug1145428.js
new file mode 100644
index 000000000..72fe70212
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug1145428.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that pinning suggested tile results in:
+ * - making suggested tile a history tile and replacing enhancedImageURI with imageURI
+ * - upond end of campaign, replaces landing url with baseDomain and switches
+ * background image to thumbnail
+ */
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+ "suggested": [{
+ url: "http://example.com/landing/page.html",
+ imageURI: "data:image/png;base64,helloWORLD3",
+ enhancedImageURI: "data:image/png;base64,helloWORLD2",
+ title: "title",
+ type: "affiliate",
+ adgroup_name: "example",
+ frecent_sites: ["example0.com"],
+ }]
+});
+
+add_task(function* () {
+ let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+ DirectoryLinksProvider.getFrecentSitesName = () => "";
+
+ function getData(cellNum) {
+ return performOnCell(cellNum, cell => {
+ if (!cell.site)
+ return null;
+ let siteNode = cell.site.node;
+ return {
+ type: siteNode.getAttribute("type"),
+ thumbnail: siteNode.querySelector(".newtab-thumbnail.thumbnail").style.backgroundImage,
+ enhanced: siteNode.querySelector(".enhanced-content").style.backgroundImage,
+ title: siteNode.querySelector(".newtab-title").textContent,
+ suggested: siteNode.getAttribute("suggested"),
+ url: siteNode.querySelector(".newtab-link").getAttribute("href"),
+ };
+ });
+ }
+
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ // load another newtab since the first may not get suggested tile
+ yield* addNewTabPageTab();
+ yield* checkGrid("http://example.com/landing/page.html,0,1,2,3,4,5,6,7,8,9");
+ // evaluate suggested tile
+ let tileData = yield getData(0);
+ is(tileData.type, "affiliate", "unpinned type");
+ is(tileData.thumbnail, "url(\"data:image/png;base64,helloWORLD3\")", "unpinned thumbnail");
+ is(tileData.enhanced, "url(\"data:image/png;base64,helloWORLD2\")", "unpinned enhanced");
+ is(tileData.suggested, "true", "has suggested set", "unpinned suggested exists");
+ is(tileData.url, "http://example.com/landing/page.html", "unpinned landing page");
+
+ // suggested tile should not be pinned
+ is(NewTabUtils.pinnedLinks.isPinned({url: "http://example.com/landing/page.html"}), false, "suggested tile is not pinned");
+
+ // pin suggested tile
+ let updatedPromise = whenPagesUpdated();
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-site > .newtab-control-pin", {}, gBrowser.selectedBrowser);
+ yield updatedPromise;
+
+ // tile should be pinned and turned into history tile
+ is(NewTabUtils.pinnedLinks.isPinned({url: "http://example.com/landing/page.html"}), true, "suggested tile is pinned");
+ tileData = yield getData(0);
+ is(tileData.type, "history", "pinned type");
+ is(tileData.suggested, null, "no suggested attribute");
+ is(tileData.url, "http://example.com/landing/page.html", "original landing page");
+
+ // set pinned tile endTime into past and reload the page
+ NewTabUtils.pinnedLinks._links[0].endTime = Date.now() - 1000;
+ yield* addNewTabPageTab();
+
+ // check that url is reset to base domain and thumbnail points to moz-page-thumb service
+ is(NewTabUtils.pinnedLinks.isPinned({url: "http://example.com/"}), true, "baseDomain url is pinned");
+ tileData = yield getData(0);
+ is(tileData.type, "history", "type is history");
+ is(tileData.title, "example.com", "title changed to baseDomain");
+ is(tileData.thumbnail.indexOf("moz-page-thumb") != -1, true, "thumbnail contains moz-page-thumb");
+ is(tileData.enhanced, "", "no enhanced image");
+ is(tileData.url, "http://example.com/", "url points to baseDomian");
+
+ DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug1178586.js b/browser/base/content/test/newtab/browser_newtab_bug1178586.js
new file mode 100644
index 000000000..84d5cb577
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug1178586.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that pinned suggested tile turns into history tile
+ * and remains a history tile after a user clicks on it
+ */
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+ "suggested": [{
+ url: "http://example.com/hardlanding/page.html",
+ imageURI: "data:image/png;base64,helloWORLD3",
+ enhancedImageURI: "data:image/png;base64,helloWORLD2",
+ title: "title",
+ type: "affiliate",
+ adgroup_name: "example",
+ frecent_sites: ["example0.com"],
+ }]
+});
+
+add_task(function* () {
+ let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+ DirectoryLinksProvider.getFrecentSitesName = () => "";
+
+ function getData(cellNum) {
+ return performOnCell(cellNum, cell => {
+ if (!cell.site)
+ return null;
+ let siteNode = cell.site.node;
+ return {
+ type: siteNode.getAttribute("type"),
+ thumbnail: siteNode.querySelector(".newtab-thumbnail.thumbnail").style.backgroundImage,
+ enhanced: siteNode.querySelector(".enhanced-content").style.backgroundImage,
+ title: siteNode.querySelector(".newtab-title").textContent,
+ suggested: siteNode.getAttribute("suggested"),
+ url: siteNode.querySelector(".newtab-link").getAttribute("href"),
+ };
+ });
+ }
+
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ // load another newtab since the first may not get suggested tile
+ yield* addNewTabPageTab();
+ yield* checkGrid("http://example.com/hardlanding/page.html,0,1,2,3,4,5,6,7,8,9");
+ // evaluate suggested tile
+ let tileData = yield getData(0);
+ is(tileData.type, "affiliate", "unpinned type");
+ is(tileData.thumbnail, "url(\"data:image/png;base64,helloWORLD3\")", "unpinned thumbnail");
+ is(tileData.enhanced, "url(\"data:image/png;base64,helloWORLD2\")", "unpinned enhanced");
+ is(tileData.suggested, "true", "has suggested set", "unpinned suggested exists");
+ is(tileData.url, "http://example.com/hardlanding/page.html", "unpinned landing page");
+
+ // suggested tile should not be pinned
+ is(NewTabUtils.pinnedLinks.isPinned({url: "http://example.com/hardlanding/page.html"}), false, "suggested tile is not pinned");
+
+ // pin suggested tile
+ let updatedPromise = whenPagesUpdated();
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-site > .newtab-control-pin", {}, gBrowser.selectedBrowser);
+ yield updatedPromise;
+
+ // tile should be pinned and turned into history tile
+ is(NewTabUtils.pinnedLinks.isPinned({url: "http://example.com/hardlanding/page.html"}), true, "suggested tile is pinned");
+ tileData = yield getData(0);
+ is(tileData.type, "history", "pinned type");
+ is(tileData.suggested, null, "no suggested attribute");
+ is(tileData.url, "http://example.com/hardlanding/page.html", "original landing page");
+
+ // click the pinned tile
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-site", {}, gBrowser.selectedBrowser);
+ // add new page twice to avoid using cached version
+ yield* addNewTabPageTab();
+ yield* addNewTabPageTab();
+
+ // check that type and suggested did not change
+ tileData = yield getData(0);
+ is(tileData.type, "history", "pinned type");
+ is(tileData.suggested, null, "no suggested attribute");
+
+ DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug1194895.js b/browser/base/content/test/newtab/browser_newtab_bug1194895.js
new file mode 100644
index 000000000..c08b23185
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug1194895.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PRELOAD_PREF = "browser.newtab.preload";
+const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
+const PREF_NEWTAB_ROWS = "browser.newtabpage.rows";
+
+function populateDirectoryTiles() {
+ let directoryTiles = [];
+ let i = 0;
+ while (i++ < 14) {
+ directoryTiles.push({
+ directoryId: i,
+ url: "http://example" + i + ".com/",
+ enhancedImageURI: "data:image/png;base64,helloWORLD",
+ title: "dirtitle" + i,
+ type: "affiliate"
+ });
+ }
+ return directoryTiles;
+}
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+ "directory": populateDirectoryTiles()
+});
+
+
+add_task(function* () {
+ requestLongerTimeout(4);
+ let origEnhanced = NewTabUtils.allPages.enhanced;
+ let origCompareLinks = NewTabUtils.links.compareLinks;
+ registerCleanupFunction(() => {
+ NewTabUtils.allPages.enhanced = origEnhanced;
+ NewTabUtils.links.compareLinks = origCompareLinks;
+ });
+
+ // turn off preload to ensure grid updates on every setLinks
+ yield pushPrefs([PRELOAD_PREF, false]);
+ // set newtab to have three columns only
+ yield pushPrefs([PREF_NEWTAB_COLUMNS, 3]);
+ yield pushPrefs([PREF_NEWTAB_ROWS, 5]);
+
+ yield* addNewTabPageTab();
+ yield customizeNewTabPage("enhanced"); // Toggle enhanced off
+
+ // Testing history tiles
+
+ // two rows of tiles should always fit on any screen
+ yield setLinks("0,1,2,3,4,5");
+ yield* addNewTabPageTab();
+
+ // should do not see scrollbar since tiles fit into visible space
+ yield* checkGrid("0,1,2,3,4,5");
+ let scrolling = yield hasScrollbar();
+ ok(!scrolling, "no scrollbar");
+
+ // add enough tiles to cause extra two rows and observe scrollbar
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8,9");
+ scrolling = yield hasScrollbar();
+ ok(scrolling, "document has scrollbar");
+
+ // pin the last tile to make it stay at the bottom of the newtab
+ yield pinCell(9);
+ // block first 6 tiles, which should not remove the scroll bar
+ // since the last tile is pinned in the nineth position
+ for (let i = 0; i < 6; i++) {
+ yield blockCell(0);
+ }
+ yield* addNewTabPageTab();
+ yield* checkGrid("6,7,8,,,,,,,9p");
+ scrolling = yield hasScrollbar();
+ ok(scrolling, "document has scrollbar when tile is pinned to the last row");
+
+ // unpin the site: this will move tile up and make scrollbar disappear
+ yield unpinCell(9);
+ yield* addNewTabPageTab();
+ yield* checkGrid("6,7,8,9");
+ scrolling = yield hasScrollbar();
+ ok(!scrolling, "no scrollbar when bottom row tile is unpinned");
+
+ // reset everything to clean slate
+ NewTabUtils.restore();
+
+ // Testing directory tiles
+ yield customizeNewTabPage("enhanced"); // Toggle enhanced on
+
+ // setup page with no history tiles to test directory only display
+ yield setLinks([]);
+ yield* addNewTabPageTab();
+ ok(!scrolling, "no scrollbar for directory tiles");
+
+ // introduce one history tile - it should occupy the last
+ // available slot at the bottom of newtab and cause scrollbar
+ yield setLinks("41");
+ yield* addNewTabPageTab();
+ scrolling = yield hasScrollbar();
+ ok(scrolling, "adding low frecency history site causes scrollbar");
+
+ // set PREF_NEWTAB_ROWS to 4, that should clip off the history tile
+ // and remove scroll bar
+ yield pushPrefs([PREF_NEWTAB_ROWS, 4]);
+ yield* addNewTabPageTab();
+
+ scrolling = yield hasScrollbar();
+ ok(!scrolling, "no scrollbar if history tiles falls past max rows");
+
+ // restore max rows and watch scrollbar re-appear
+ yield pushPrefs([PREF_NEWTAB_ROWS, 5]);
+ yield* addNewTabPageTab();
+ scrolling = yield hasScrollbar();
+ ok(scrolling, "scrollbar is back when max rows allow for bottom history tile");
+
+ // block that history tile, and watch scrollbar disappear
+ yield blockCell(14);
+ yield* addNewTabPageTab();
+ scrolling = yield hasScrollbar();
+ ok(!scrolling, "no scrollbar after bottom history tiles is blocked");
+
+ // Test well-populated user history - newtab has highly-frecent history sites
+ // redefine compareLinks to always choose history tiles first
+ NewTabUtils.links.compareLinks = function (aLink1, aLink2) {
+ if (aLink1.type == aLink2.type) {
+ return aLink2.frecency - aLink1.frecency ||
+ aLink2.lastVisitDate - aLink1.lastVisitDate;
+ }
+ if (aLink2.type == "history") {
+ return 1;
+ }
+ return -1;
+ };
+
+ // add a row of history tiles, directory tiles will be clipped off, hence no scrollbar
+ yield setLinks("31,32,33");
+ yield* addNewTabPageTab();
+ scrolling = yield hasScrollbar();
+ ok(!scrolling, "no scrollbar when directory tiles follow history tiles");
+
+ // fill first four rows with history tiles and observer scrollbar
+ yield setLinks("30,31,32,33,34,35,36,37,38,39");
+ yield* addNewTabPageTab();
+ scrolling = yield hasScrollbar();
+ ok(scrolling, "scrollbar appears when history tiles need extra row");
+});
+
diff --git a/browser/base/content/test/newtab/browser_newtab_bug1271075.js b/browser/base/content/test/newtab/browser_newtab_bug1271075.js
new file mode 100644
index 000000000..723b48fc6
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug1271075.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ is(gBrowser.tabs.length, 1, "one tab is open initially");
+
+ // Add a few tabs.
+ let tabs = [];
+ function addTab(aURL, aReferrer) {
+ let tab = gBrowser.addTab(aURL, {referrerURI: aReferrer});
+ tabs.push(tab);
+ return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ }
+
+ yield addTab("http://mochi.test:8888/#0");
+ yield addTab("http://mochi.test:8888/#1");
+ yield addTab("http://mochi.test:8888/#2");
+ yield addTab("http://mochi.test:8888/#3");
+
+ // Create a new tab page with a "www.example.com" tile and move it to the 2nd tab position.
+ yield setLinks("-1");
+ yield* addNewTabPageTab();
+ gBrowser.moveTabTo(gBrowser.selectedTab, 1);
+
+ // Send a middle-click and confirm that the clicked site opens immediately next to the new tab page.
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-cell",
+ {button: 1}, gBrowser.selectedBrowser);
+
+ yield BrowserTestUtils.browserLoaded(gBrowser.getBrowserAtIndex(2));
+ is(gBrowser.getBrowserAtIndex(2).currentURI.spec, "http://example.com/",
+ "Middle click opens site in a new tab immediately to the right.");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug721442.js b/browser/base/content/test/newtab/browser_newtab_bug721442.js
new file mode 100644
index 000000000..99bd8d930
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug721442.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks([
+ {url: "http://example7.com/", title: ""},
+ {url: "http://example8.com/", title: "title"},
+ {url: "http://example9.com/", title: "http://example9.com/"}
+ ]);
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("7p,8p,9p,0,1,2,3,4,5");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ function checkTooltip(aIndex, aExpected, aMessage) {
+ let cell = content.gGrid.cells[aIndex];
+
+ let link = cell.node.querySelector(".newtab-link");
+ Assert.equal(link.getAttribute("title"), aExpected, aMessage);
+ }
+
+ checkTooltip(0, "http://example7.com/", "1st tooltip is correct");
+ checkTooltip(1, "title\nhttp://example8.com/", "2nd tooltip is correct");
+ checkTooltip(2, "http://example9.com/", "3rd tooltip is correct");
+ });
+});
+
diff --git a/browser/base/content/test/newtab/browser_newtab_bug722273.js b/browser/base/content/test/newtab/browser_newtab_bug722273.js
new file mode 100644
index 000000000..5cbfcd3ff
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug722273.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NOW = Date.now() * 1000;
+const URL = "http://fake-site.com/";
+
+var tmp = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tmp);
+
+var {Sanitizer} = tmp;
+
+add_task(function* () {
+ yield promiseSanitizeHistory();
+ yield promiseAddFakeVisits();
+ yield* addNewTabPageTab();
+
+ let cellUrl = yield performOnCell(0, cell => { return cell.site.url; });
+ is(cellUrl, URL, "first site is our fake site");
+
+ let updatedPromise = whenPagesUpdated();
+ yield promiseSanitizeHistory();
+ yield updatedPromise;
+
+ let isGone = yield performOnCell(0, cell => { return cell.site == null; });
+ ok(isGone, "fake site is gone");
+});
+
+function promiseAddFakeVisits() {
+ let visits = [];
+ for (let i = 59; i > 0; i--) {
+ visits.push({
+ visitDate: NOW - i * 60 * 1000000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK
+ });
+ }
+ let place = {
+ uri: makeURI(URL),
+ title: "fake site",
+ visits: visits
+ };
+ return new Promise((resolve, reject) => {
+ PlacesUtils.asyncHistory.updatePlaces(place, {
+ handleError: () => reject(new Error("Couldn't add visit")),
+ handleResult: function () {},
+ handleCompletion: function () {
+ NewTabUtils.links.populateCache(function () {
+ NewTabUtils.allPages.update();
+ resolve();
+ }, true);
+ }
+ });
+ });
+}
+
+function promiseSanitizeHistory() {
+ let s = new Sanitizer();
+ s.prefDomain = "privacy.cpd.";
+
+ let prefs = gPrefService.getBranch(s.prefDomain);
+ prefs.setBoolPref("history", true);
+ prefs.setBoolPref("downloads", false);
+ prefs.setBoolPref("cache", false);
+ prefs.setBoolPref("cookies", false);
+ prefs.setBoolPref("formdata", false);
+ prefs.setBoolPref("offlineApps", false);
+ prefs.setBoolPref("passwords", false);
+ prefs.setBoolPref("sessions", false);
+ prefs.setBoolPref("siteSettings", false);
+
+ return s.sanitize();
+}
diff --git a/browser/base/content/test/newtab/browser_newtab_bug723102.js b/browser/base/content/test/newtab/browser_newtab_bug723102.js
new file mode 100644
index 000000000..02282dc97
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug723102.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ // create a new tab page and hide it.
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ let firstTab = gBrowser.selectedTab;
+
+ yield* addNewTabPageTab();
+ yield BrowserTestUtils.removeTab(firstTab);
+
+ ok(NewTabUtils.allPages.enabled, "page is enabled");
+ NewTabUtils.allPages.enabled = false;
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ Assert.ok(content.gGrid.node.hasAttribute("page-disabled"), "page is disabled");
+ });
+
+ NewTabUtils.allPages.enabled = true;
+});
+
diff --git a/browser/base/content/test/newtab/browser_newtab_bug723121.js b/browser/base/content/test/newtab/browser_newtab_bug723121.js
new file mode 100644
index 000000000..82f45ebd5
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug723121.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ let grid = content.gGrid;
+ let cell = grid.cells[0];
+ let site = cell.site.node;
+ let link = site.querySelector(".newtab-link");
+
+ function checkGridLocked(aLocked, aMessage) {
+ Assert.equal(grid.node.hasAttribute("locked"), aLocked, aMessage);
+ }
+
+ function sendDragEvent(aEventType, aTarget) {
+ let dataTransfer = new content.DataTransfer(aEventType, false);
+ let event = content.document.createEvent("DragEvent");
+ event.initDragEvent(aEventType, true, true, content, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+ aTarget.dispatchEvent(event);
+ }
+
+ checkGridLocked(false, "grid is unlocked");
+
+ sendDragEvent("dragstart", link);
+ checkGridLocked(true, "grid is now locked");
+
+ sendDragEvent("dragend", link);
+ checkGridLocked(false, "grid isn't locked anymore");
+
+ sendDragEvent("dragstart", cell.node);
+ checkGridLocked(false, "grid isn't locked - dragstart was ignored");
+
+ sendDragEvent("dragstart", site);
+ checkGridLocked(false, "grid isn't locked - dragstart was ignored");
+ });
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug725996.js b/browser/base/content/test/newtab/browser_newtab_bug725996.js
new file mode 100644
index 000000000..e0de809c8
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug725996.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ function doDrop(data) {
+ return ContentTask.spawn(gBrowser.selectedBrowser, { data: data }, function*(args) {
+ let dataTransfer = new content.DataTransfer("dragstart", false);
+ dataTransfer.mozSetDataAt("text/x-moz-url", args.data, 0);
+ let event = content.document.createEvent("DragEvent");
+ event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+
+ let target = content.gGrid.cells[0].node;
+ target.dispatchEvent(event);
+ });
+ }
+
+ yield doDrop("http://example99.com/\nblank");
+ is(NewTabUtils.pinnedLinks.links[0].url, "http://example99.com/",
+ "first cell is pinned and contains the dropped site");
+
+ yield whenPagesUpdated();
+ yield* checkGrid("99p,0,1,2,3,4,5,6,7");
+
+ yield doDrop("");
+ is(NewTabUtils.pinnedLinks.links[0].url, "http://example99.com/",
+ "first cell is still pinned with the site we dropped before");
+});
+
diff --git a/browser/base/content/test/newtab/browser_newtab_bug734043.js b/browser/base/content/test/newtab/browser_newtab_bug734043.js
new file mode 100644
index 000000000..02f765274
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug734043.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.addEventListener("error", function () {
+ sendAsyncMessage("test:newtab-error", {});
+ });
+ });
+
+ let receivedError = false;
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.addMessageListener("test:newtab-error", function onResponse(message) {
+ mm.removeMessageListener("test:newtab-error", onResponse);
+ ok(false, "Error event happened");
+ receivedError = true;
+ });
+
+ let pagesUpdatedPromise = whenPagesUpdated();
+
+ for (let i = 0; i < 3; i++) {
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-control-block", {}, gBrowser.selectedBrowser);
+ }
+
+ yield pagesUpdatedPromise;
+
+ ok(!receivedError, "we got here without any errors");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug735987.js b/browser/base/content/test/newtab/browser_newtab_bug735987.js
new file mode 100644
index 000000000..2ae541c70
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug735987.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ yield* simulateExternalDrop(1);
+ yield* checkGrid("0,99p,1,2,3,4,5,6,7");
+
+ yield blockCell(1);
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ yield* simulateExternalDrop(1);
+ yield* checkGrid("0,99p,1,2,3,4,5,6,7");
+
+ // Simulate a restart and force the next about:newtab
+ // instance to read its data from the storage again.
+ NewTabUtils.blockedLinks.resetCache();
+
+ // Update all open pages, e.g. preloaded ones.
+ NewTabUtils.allPages.update();
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,99p,1,2,3,4,5,6,7");
+
+ yield blockCell(1);
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug752841.js b/browser/base/content/test/newtab/browser_newtab_bug752841.js
new file mode 100644
index 000000000..e3faad13f
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug752841.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PREF_NEWTAB_ROWS = "browser.newtabpage.rows";
+const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
+
+function getCellsCount()
+{
+ return ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ return content.gGrid.cells.length;
+ });
+}
+
+add_task(function* () {
+ let testValues = [
+ {row: 0, column: 0},
+ {row: -1, column: -1},
+ {row: -1, column: 0},
+ {row: 0, column: -1},
+ {row: 2, column: 4},
+ {row: 2, column: 5},
+ ];
+
+ // Expected length of grid
+ let expectedValues = [1, 1, 1, 1, 8, 10];
+
+ // Values before setting new pref values (15 is the default value -> 5 x 3)
+ let previousValues = [15, 1, 1, 1, 1, 8];
+
+ yield* addNewTabPageTab();
+ let existingTab = gBrowser.selectedTab;
+
+ for (let i = 0; i < expectedValues.length; i++) {
+ let existingTabGridLength = yield getCellsCount();
+ is(existingTabGridLength, previousValues[i],
+ "Grid length of existing page before update is correctly.");
+
+ yield pushPrefs([PREF_NEWTAB_ROWS, testValues[i].row]);
+ yield pushPrefs([PREF_NEWTAB_COLUMNS, testValues[i].column]);
+
+ existingTabGridLength = yield getCellsCount();
+ is(existingTabGridLength, expectedValues[i],
+ "Existing page grid is updated correctly.");
+
+ yield* addNewTabPageTab();
+ let newTab = gBrowser.selectedTab;
+ let newTabGridLength = yield getCellsCount();
+ is(newTabGridLength, expectedValues[i],
+ "New page grid is updated correctly.");
+
+ yield BrowserTestUtils.removeTab(newTab);
+ }
+
+ gBrowser.removeTab(existingTab);
+});
+
diff --git a/browser/base/content/test/newtab/browser_newtab_bug765628.js b/browser/base/content/test/newtab/browser_newtab_bug765628.js
new file mode 100644
index 000000000..25afd32a9
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug765628.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ yield checkGrid("0,1,2,3,4,5,6,7,8");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ const BAD_DRAG_DATA = "javascript:alert('h4ck0rz');\nbad stuff";
+ const GOOD_DRAG_DATA = "http://example99.com/\nsite 99";
+
+ function sendDropEvent(aCellIndex, aDragData) {
+ let dataTransfer = new content.DataTransfer("dragstart", false);
+ dataTransfer.mozSetDataAt("text/x-moz-url", aDragData, 0);
+ let event = content.document.createEvent("DragEvent");
+ event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+
+ let target = content.gGrid.cells[aCellIndex].node;
+ target.dispatchEvent(event);
+ }
+
+ sendDropEvent(0, BAD_DRAG_DATA);
+ sendDropEvent(1, GOOD_DRAG_DATA);
+ });
+
+ yield whenPagesUpdated();
+ yield* checkGrid("0,99p,1,2,3,4,5,6,7");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug876313.js b/browser/base/content/test/newtab/browser_newtab_bug876313.js
new file mode 100644
index 000000000..1c0b0f501
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug876313.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * This test makes sure that the changes made by unpinning
+ * a site are actually written to NewTabUtils' storage.
+ */
+add_task(function* () {
+ // Second cell is pinned with page #99.
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",99");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,99p,1,2,3,4,5,6,7");
+
+ // Unpin the second cell's site.
+ yield unpinCell(1);
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ // Clear the pinned cache to force NewTabUtils to read the pref again.
+ NewTabUtils.pinnedLinks.resetCache();
+ NewTabUtils.allPages.update();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug991111.js b/browser/base/content/test/newtab/browser_newtab_bug991111.js
new file mode 100644
index 000000000..37aa8213b
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug991111.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ // set max rows to 1, to avoid scroll events by clicking middle button
+ yield pushPrefs(["browser.newtabpage.rows", 1]);
+ yield setLinks("-1");
+ yield* addNewTabPageTab();
+ // we need a second newtab to honor max rows
+ yield* addNewTabPageTab();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {index: 0}, function* (args) {
+ let {site} = content.wrappedJSObject.gGrid.cells[args.index];
+
+ let origOnClick = site.onClick;
+ site.onClick = e => {
+ origOnClick.call(site, e);
+ sendAsyncMessage("test:clicked-on-cell", {});
+ };
+ });
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let messagePromise = new Promise(resolve => {
+ mm.addMessageListener("test:clicked-on-cell", function onResponse(message) {
+ mm.removeMessageListener("test:clicked-on-cell", onResponse);
+ resolve();
+ });
+ });
+
+ // Send a middle-click and make sure it happened
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-cell",
+ {button: 1}, gBrowser.selectedBrowser);
+ yield messagePromise;
+ ok(true, "middle click triggered click listener");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug991210.js b/browser/base/content/test/newtab/browser_newtab_bug991210.js
new file mode 100644
index 000000000..367c49f5c
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug991210.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ // turn off preload to ensure that a newtab page loads
+ yield pushPrefs(["browser.newtab.preload", false]);
+
+ // add a test provider that waits for load
+ let afterLoadProvider = {
+ getLinks: function(callback) {
+ this.callback = callback;
+ },
+ addObserver: function() {},
+ };
+ NewTabUtils.links.addProvider(afterLoadProvider);
+
+ // wait until about:newtab loads before calling provider callback
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab");
+
+ afterLoadProvider.callback([]);
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let {_cellHeight, _cellWidth, node} = content.gGrid;
+ Assert.notEqual(_cellHeight, null, "grid has a computed cell height");
+ Assert.notEqual(_cellWidth, null, "grid has a computed cell width");
+ let {height, maxHeight, maxWidth} = node.style;
+ Assert.notEqual(height, "", "grid has a computed grid height");
+ Assert.notEqual(maxHeight, "", "grid has a computed grid max-height");
+ Assert.notEqual(maxWidth, "", "grid has a computed grid max-width");
+ });
+
+ // restore original state
+ NewTabUtils.links.removeProvider(afterLoadProvider);
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_bug998387.js b/browser/base/content/test/newtab/browser_newtab_bug998387.js
new file mode 100644
index 000000000..30424c2e5
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug998387.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ // set max rows to 1, to avoid scroll events by clicking middle button
+ yield pushPrefs(["browser.newtabpage.rows", 1]);
+ yield setLinks("0");
+ yield* addNewTabPageTab();
+ // we need a second newtab to honor max rows
+ yield* addNewTabPageTab();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {index: 0}, function* (args) {
+ let {site} = content.wrappedJSObject.gGrid.cells[args.index];
+
+ let origOnClick = site.onClick;
+ site.onClick = e => {
+ origOnClick.call(site, e);
+ sendAsyncMessage("test:clicked-on-cell", {});
+ };
+ });
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let messagePromise = new Promise(resolve => {
+ mm.addMessageListener("test:clicked-on-cell", function onResponse(message) {
+ mm.removeMessageListener("test:clicked-on-cell", onResponse);
+ resolve();
+ });
+ });
+
+ // Send a middle-click and make sure it happened
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-control-block",
+ {button: 1}, gBrowser.selectedBrowser);
+
+ yield messagePromise;
+ ok(true, "middle click triggered click listener");
+
+ // Make sure the cell didn't actually get blocked
+ yield* checkGrid("0");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_disable.js b/browser/base/content/test/newtab/browser_newtab_disable.js
new file mode 100644
index 000000000..58b9a18af
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_disable.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that the 'New Tab Page' feature can be disabled if the
+ * decides not to use it.
+ */
+add_task(function* () {
+ // create a new tab page and hide it.
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ let firstTab = yield* addNewTabPageTab();
+
+ function isGridDisabled(browser = gBrowser.selectedBrowser)
+ {
+ return ContentTask.spawn(browser, {}, function*() {
+ return content.gGrid.node.hasAttribute("page-disabled");
+ });
+ }
+
+ let isDisabled = yield isGridDisabled();
+ ok(!isDisabled, "page is not disabled");
+
+ NewTabUtils.allPages.enabled = false;
+
+ isDisabled = yield isGridDisabled();
+ ok(isDisabled, "page is disabled");
+
+ // create a second new tab page and make sure it's disabled. enable it
+ // again and check if the former page gets enabled as well.
+ yield* addNewTabPageTab();
+ isDisabled = yield isGridDisabled(firstTab.linkedBrowser);
+ ok(isDisabled, "page is disabled");
+
+ // check that no sites have been rendered
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+ Assert.equal(content.document.querySelectorAll(".site").length, 0,
+ "no sites have been rendered");
+ });
+
+ NewTabUtils.allPages.enabled = true;
+
+ isDisabled = yield isGridDisabled();
+ ok(!isDisabled, "page is not disabled");
+
+ isDisabled = yield isGridDisabled(firstTab.linkedBrowser);
+ ok(!isDisabled, "old page is not disabled");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_drag_drop.js b/browser/base/content/test/newtab/browser_newtab_drag_drop.js
new file mode 100644
index 000000000..da9d89de7
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_drag_drop.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that dragging and dropping sites works as expected.
+ * Sites contained in the grid need to shift around to indicate the result
+ * of the drag-and-drop operation. If the grid is full and we're dragging
+ * a new site into it another one gets pushed out.
+ */
+add_task(function* () {
+ requestLongerTimeout(2);
+ yield* addNewTabPageTab();
+
+ // test a simple drag-and-drop scenario
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ yield doDragEvent(0, 1);
+ yield* checkGrid("1,0p,2,3,4,5,6,7,8");
+
+ // drag a cell to its current cell and make sure it's not pinned afterwards
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ yield doDragEvent(0, 0);
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ // ensure that pinned pages aren't moved if that's not necessary
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",1,2");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1p,2p,3,4,5,6,7,8");
+
+ yield doDragEvent(0, 3);
+ yield* checkGrid("3,1p,2p,0p,4,5,6,7,8");
+
+ // pinned sites should always be moved around as blocks. if a pinned site is
+ // moved around, neighboring pinned are affected as well
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("0,1");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0p,1p,2,3,4,5,6,7,8");
+
+ yield doDragEvent(2, 0);
+ yield* checkGrid("2p,0p,1p,3,4,5,6,7,8");
+
+ // pinned sites should not be pushed out of the grid (unless there are only
+ // pinned ones left on the grid)
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",,,,,,,7,8");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7p,8p");
+
+ yield doDragEvent(2, 5);
+ yield* checkGrid("0,1,3,4,5,2p,6,7p,8p");
+
+ // make sure that pinned sites are re-positioned correctly
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("0,1,2,,,5");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0p,1p,2p,3,4,5p,6,7,8");
+
+ yield doDragEvent(0, 4);
+ yield* checkGrid("3,1p,2p,4,0p,5p,6,7,8");
+});
+
+function doDragEvent(sourceIndex, dropIndex) {
+ return ContentTask.spawn(gBrowser.selectedBrowser,
+ { sourceIndex: sourceIndex, dropIndex: dropIndex }, function*(args) {
+ let dataTransfer = new content.DataTransfer("dragstart", false);
+ let event = content.document.createEvent("DragEvent");
+ event.initDragEvent("dragstart", true, true, content, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+
+ let target = content.gGrid.cells[args.sourceIndex].site.node;
+ target.dispatchEvent(event);
+
+ event = content.document.createEvent("DragEvent");
+ event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+
+ target = content.gGrid.cells[args.dropIndex].node;
+ target.dispatchEvent(event);
+ });
+}
diff --git a/browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js b/browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js
new file mode 100644
index 000000000..4e7b062cb
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(2);
+
+const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
+
+/*
+ * These tests make sure that dragging and dropping sites works as expected.
+ * Sites contained in the grid need to shift around to indicate the result
+ * of the drag-and-drop operation. If the grid is full and we're dragging
+ * a new site into it another one gets pushed out.
+ * This is a continuation of browser_newtab_drag_drop.js
+ * to decrease test run time, focusing on external sites.
+ */
+ add_task(function* () {
+ yield* addNewTabPageTab();
+
+ // drag a new site onto the very first cell
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",,,,,,,7,8");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7p,8p");
+
+ yield* simulateExternalDrop(0);
+ yield* checkGrid("99p,0,1,2,3,4,5,7p,8p");
+
+ // drag a new site onto the grid and make sure that pinned cells don't get
+ // pushed out
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",,,,,,,7,8");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7p,8p");
+
+ // force the grid to be small enough that a pinned cell could be pushed out
+ yield pushPrefs([PREF_NEWTAB_COLUMNS, 3]);
+ yield* simulateExternalDrop(5);
+ yield* checkGrid("0,1,2,3,4,99p,5,7p,8p");
+
+ // drag a new site beneath a pinned cell and make sure the pinned cell is
+ // not moved
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",,,,,,,,8");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1,2,3,4,5,6,7,8p");
+
+ yield* simulateExternalDrop(5);
+ yield* checkGrid("0,1,2,3,4,99p,5,6,8p");
+
+ // drag a new site onto a block of pinned sites and make sure they're shifted
+ // around accordingly
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("0,1,2,,,,,,");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0p,1p,2p");
+
+ yield* simulateExternalDrop(1);
+ yield* checkGrid("0p,99p,1p,2p,3,4,5,6,7");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_drop_preview.js b/browser/base/content/test/newtab/browser_newtab_drop_preview.js
new file mode 100644
index 000000000..f9e37f629
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_drop_preview.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests ensure that the drop preview correctly arranges sites when
+ * dragging them around.
+ */
+add_task(function* () {
+ yield* addNewTabPageTab();
+
+ // the first three sites are pinned - make sure they're re-arranged correctly
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("0,1,2,,,5");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0p,1p,2p,3,4,5p,6,7,8");
+
+ let foundSites = yield ContentTask.spawn(gWindow.gBrowser.selectedBrowser, {}, function*() {
+ let cells = content.gGrid.cells;
+ content.gDrag._draggedSite = cells[0].site;
+ let sites = content.gDropPreview.rearrange(cells[4]);
+ content.gDrag._draggedSite = null;
+
+ sites = sites.slice(0, 9);
+ return sites.map(function (aSite) {
+ if (!aSite)
+ return "";
+
+ let pinned = aSite.isPinned();
+ if (pinned != aSite.node.hasAttribute("pinned")) {
+ Assert.ok(false, "invalid state (site.isPinned() != site[pinned])");
+ }
+
+ return aSite.url.replace(/^http:\/\/example(\d+)\.com\/$/, "$1") + (pinned ? "p" : "");
+ });
+ });
+
+ let expectedSites = "3,1p,2p,4,0p,5p,6,7,8"
+ is(foundSites, expectedSites, "grid status = " + expectedSites);
+});
+
diff --git a/browser/base/content/test/newtab/browser_newtab_enhanced.js b/browser/base/content/test/newtab/browser_newtab_enhanced.js
new file mode 100644
index 000000000..5ac07ce55
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_enhanced.js
@@ -0,0 +1,228 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(2);
+
+const PRELOAD_PREF = "browser.newtab.preload";
+
+var suggestedLink = {
+ url: "http://example1.com/2",
+ imageURI: "data:image/png;base64,helloWORLD3",
+ title: "title2",
+ type: "affiliate",
+ adgroup_name: "Technology",
+ frecent_sites: ["classroom.google.com", "codeacademy.org", "codecademy.com", "codeschool.com", "codeyear.com", "elearning.ut.ac.id", "how-to-build-websites.com", "htmlcodetutorial.com", "htmldog.com", "htmlplayground.com", "learn.jquery.com", "quackit.com", "roseindia.net", "teamtreehouse.com", "tizag.com", "tutorialspoint.com", "udacity.com", "w3schools.com", "webdevelopersnotes.com"]
+};
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+ "enhanced": [{
+ url: "http://example.com/",
+ enhancedImageURI: "data:image/png;base64,helloWORLD",
+ title: "title",
+ type: "organic",
+ }],
+ "directory": [{
+ url: "http://example1.com/",
+ enhancedImageURI: "data:image/png;base64,helloWORLD2",
+ title: "title1",
+ type: "organic"
+ }],
+ "suggested": [suggestedLink]
+});
+
+add_task(function* () {
+ let origEnhanced = NewTabUtils.allPages.enhanced;
+ registerCleanupFunction(() => {
+ NewTabUtils.allPages.enhanced = origEnhanced;
+ });
+
+ yield pushPrefs([PRELOAD_PREF, false]);
+
+ function getData(cellNum) {
+ return performOnCell(cellNum, cell => {
+ if (!cell.site)
+ return null;
+ let siteNode = cell.site.node;
+ return {
+ type: siteNode.getAttribute("type"),
+ enhanced: siteNode.querySelector(".enhanced-content").style.backgroundImage,
+ title: siteNode.querySelector(".newtab-title").textContent,
+ suggested: siteNode.querySelector(".newtab-suggested").innerHTML
+ };
+ });
+ }
+
+ // Make the page have a directory link, enhanced link, and history link
+ yield setLinks("-1");
+
+ // Test with enhanced = false
+ yield* addNewTabPageTab();
+ yield customizeNewTabPage("classic");
+ yield customizeNewTabPage("enhanced"); // Toggle enhanced off
+ let {type, enhanced, title, suggested} = yield getData(0);
+ isnot(type, "enhanced", "history link is not enhanced");
+ is(enhanced, "", "history link has no enhanced image");
+ is(title, "example.com");
+ is(suggested, "", "There is no suggested explanation");
+
+ let data = yield getData(1);
+ is(data, null, "there is only one link and it's a history link");
+
+ // Test with enhanced = true
+ yield* addNewTabPageTab();
+ yield customizeNewTabPage("enhanced"); // Toggle enhanced on
+ ({type, enhanced, title, suggested} = yield getData(0));
+ is(type, "organic", "directory link is organic");
+ isnot(enhanced, "", "directory link has enhanced image");
+ is(title, "title1");
+ is(suggested, "", "There is no suggested explanation");
+
+ ({type, enhanced, title, suggested} = yield getData(1));
+ is(type, "enhanced", "history link is enhanced");
+ isnot(enhanced, "", "history link has enhanced image");
+ is(title, "title");
+ is(suggested, "", "There is no suggested explanation");
+
+ data = yield getData(2);
+ is(data, null, "there are only 2 links, directory and enhanced history");
+
+ // Test with a pinned link
+ setPinnedLinks("-1");
+ yield* addNewTabPageTab();
+ ({type, enhanced, title, suggested} = yield getData(0));
+ is(type, "enhanced", "pinned history link is enhanced");
+ isnot(enhanced, "", "pinned history link has enhanced image");
+ is(title, "title");
+ is(suggested, "", "There is no suggested explanation");
+
+ ({type, enhanced, title, suggested} = yield getData(1));
+ is(type, "organic", "directory link is organic");
+ isnot(enhanced, "", "directory link has enhanced image");
+ is(title, "title1");
+ is(suggested, "", "There is no suggested explanation");
+
+ data = yield getData(2);
+ is(data, null, "directory link pushed out by pinned history link");
+
+ // Test pinned link with enhanced = false
+ yield* addNewTabPageTab();
+ yield customizeNewTabPage("enhanced"); // Toggle enhanced off
+ ({type, enhanced, title, suggested} = yield getData(0));
+ isnot(type, "enhanced", "history link is not enhanced");
+ is(enhanced, "", "history link has no enhanced image");
+ is(title, "example.com");
+ is(suggested, "", "There is no suggested explanation");
+
+ data = yield getData(1);
+ is(data, null, "directory link still pushed out by pinned history link");
+
+ yield unpinCell(0);
+
+
+
+ // Test that a suggested tile is not enhanced by a directory tile
+ NewTabUtils.isTopPlacesSite = () => true;
+ yield setLinks("-1,2,3,4,5,6,7,8");
+
+ // Test with enhanced = false
+ yield* addNewTabPageTab();
+ ({type, enhanced, title, suggested} = yield getData(0));
+ isnot(type, "enhanced", "history link is not enhanced");
+ is(enhanced, "", "history link has no enhanced image");
+ is(title, "example.com");
+ is(suggested, "", "There is no suggested explanation");
+
+ data = yield getData(7);
+ isnot(data, null, "there are 8 history links");
+ data = yield getData(8);
+ is(data, null, "there are 8 history links");
+
+
+ // Test with enhanced = true
+ yield* addNewTabPageTab();
+ yield customizeNewTabPage("enhanced");
+
+ // Suggested link was not enhanced by directory link with same domain
+ ({type, enhanced, title, suggested} = yield getData(0));
+ is(type, "affiliate", "suggested link is affiliate");
+ is(enhanced, "", "suggested link has no enhanced image");
+ is(title, "title2");
+ ok(suggested.indexOf("Suggested for <strong> Technology </strong> visitors") > -1, "Suggested for 'Technology'");
+
+ // Enhanced history link shows up second
+ ({type, enhanced, title, suggested} = yield getData(1));
+ is(type, "enhanced", "pinned history link is enhanced");
+ isnot(enhanced, "", "pinned history link has enhanced image");
+ is(title, "title");
+ is(suggested, "", "There is no suggested explanation");
+
+ data = yield getData(9);
+ is(data, null, "there is a suggested link followed by an enhanced history link and the remaining history links");
+
+
+
+ // Test no override category/adgroup name.
+ let linksChangedPromise = watchLinksChangeOnce();
+ yield pushPrefs([PREF_NEWTAB_DIRECTORYSOURCE,
+ "data:application/json," + JSON.stringify({"suggested": [suggestedLink]})]);
+ yield linksChangedPromise;
+
+ yield* addNewTabPageTab();
+ ({type, enhanced, title, suggested} = yield getData(0));
+ Cu.reportError("SUGGEST " + suggested);
+ ok(suggested.indexOf("Suggested for <strong> Technology </strong> visitors") > -1, "Suggested for 'Technology'");
+
+
+ // Test server provided explanation string.
+ suggestedLink.explanation = "Suggested for %1$S enthusiasts who visit sites like %2$S";
+ linksChangedPromise = watchLinksChangeOnce();
+ yield pushPrefs([PREF_NEWTAB_DIRECTORYSOURCE,
+ "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]}))]);
+ yield linksChangedPromise;
+
+ yield* addNewTabPageTab();
+ ({type, enhanced, title, suggested} = yield getData(0));
+ Cu.reportError("SUGGEST " + suggested);
+ ok(suggested.indexOf("Suggested for <strong> Technology </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'Technology' enthusiasts");
+
+
+ // Test server provided explanation string with category override.
+ suggestedLink.adgroup_name = "webdev education";
+ linksChangedPromise = watchLinksChangeOnce();
+ yield pushPrefs([PREF_NEWTAB_DIRECTORYSOURCE,
+ "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]}))]);
+ yield linksChangedPromise;
+
+ yield* addNewTabPageTab();
+ ({type, enhanced, title, suggested} = yield getData(0));
+ Cu.reportError("SUGGEST " + suggested);
+ ok(suggested.indexOf("Suggested for <strong> webdev education </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'webdev education' enthusiasts");
+
+
+
+ // Test with xml entities in category name
+ suggestedLink.url = "http://example1.com/3";
+ suggestedLink.adgroup_name = ">angles< & \"quotes\'";
+ linksChangedPromise = watchLinksChangeOnce();
+ yield pushPrefs([PREF_NEWTAB_DIRECTORYSOURCE,
+ "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]}))]);
+ yield linksChangedPromise;
+
+ yield* addNewTabPageTab();
+ ({type, enhanced, title, suggested} = yield getData(0));
+ Cu.reportError("SUGGEST " + suggested);
+ ok(suggested.indexOf("Suggested for <strong> &gt;angles&lt; &amp; \"quotes\' </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'xml entities' enthusiasts");
+
+
+ // Test with xml entities in explanation.
+ suggestedLink.explanation = "Testing junk explanation &<>\"'";
+ linksChangedPromise = watchLinksChangeOnce();
+ yield pushPrefs([PREF_NEWTAB_DIRECTORYSOURCE,
+ "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]}))]);
+ yield linksChangedPromise;
+
+ yield* addNewTabPageTab();
+ ({type, enhanced, title, suggested} = yield getData(0));
+ Cu.reportError("SUGGEST " + suggested);
+ ok(suggested.indexOf("Testing junk explanation &amp;&lt;&gt;\"'") > -1, "Junk test");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_focus.js b/browser/base/content/test/newtab/browser_newtab_focus.js
new file mode 100644
index 000000000..ae0dd8d29
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_focus.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that focusing the 'New Tab Page' works as expected.
+ */
+add_task(function* () {
+ yield pushPrefs(["accessibility.tabfocus", 7]);
+
+ // Focus count in new tab page.
+ // 30 = 9 * 3 + 3 = 9 sites, each with link, pin and remove buttons; search
+ // bar; search button; and toggle button. Additionaly there may or may not be
+ // a scroll bar caused by fix to 1180387, which will eat an extra focus
+ let FOCUS_COUNT = 30;
+
+ // Create a new tab page.
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("");
+
+ yield* addNewTabPageTab();
+ gURLBar.focus();
+
+ // Count the focus with the enabled page.
+ countFocus(FOCUS_COUNT);
+
+ // Disable page and count the focus with the disabled page.
+ NewTabUtils.allPages.enabled = false;
+
+ countFocus(4);
+
+ NewTabUtils.allPages.enabled = true;
+});
+
+/**
+ * Focus the urlbar and count how many focus stops to return again to the urlbar.
+ */
+function countFocus(aExpectedCount) {
+ let focusCount = 0;
+ do {
+ EventUtils.synthesizeKey("VK_TAB", {});
+ if (document.activeElement == gBrowser.selectedBrowser) {
+ focusCount++;
+ }
+ } while (document.activeElement != gURLBar.inputField);
+
+ ok(focusCount == aExpectedCount || focusCount == (aExpectedCount + 1),
+ "Validate focus count in the new tab page.");
+}
diff --git a/browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js b/browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js
new file mode 100644
index 000000000..b330bec13
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests ensure that all changes made to the new tab page in private
+ * browsing mode are discarded after switching back to normal mode again.
+ * The private browsing mode should start with the current grid shown in normal
+ * mode.
+ */
+
+add_task(function* () {
+ // prepare the grid
+ yield testOnWindow(undefined);
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+
+ yield* addNewTabPageTab();
+ yield pinCell(0);
+ yield* checkGrid("0p,1,2,3,4,5,6,7,8");
+
+ // open private window
+ yield testOnWindow({private: true});
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0p,1,2,3,4,5,6,7,8");
+
+ // modify the grid while we're in pb mode
+ yield blockCell(1);
+ yield* checkGrid("0p,2,3,4,5,6,7,8");
+
+ yield unpinCell(0);
+ yield* checkGrid("0,2,3,4,5,6,7,8");
+
+ // open normal window
+ yield testOnWindow(undefined);
+
+ // check that the grid is the same as before entering pb mode
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,2,3,4,5,6,7,8")
+});
+
+var windowsToClose = [];
+function* testOnWindow(options) {
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow();
+ var win = OpenBrowserWindow(options);
+ windowsToClose.push(win);
+ gWindow = win;
+ yield newWindowPromise;
+}
+
+registerCleanupFunction(function () {
+ gWindow = window;
+ windowsToClose.forEach(function(win) {
+ win.close();
+ });
+});
+
diff --git a/browser/base/content/test/newtab/browser_newtab_reflow_load.js b/browser/base/content/test/newtab/browser_newtab_reflow_load.js
new file mode 100644
index 000000000..b8a24595e
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_reflow_load.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const FRAME_SCRIPT = getRootDirectory(gTestPath) + "content-reflows.js";
+const ADDITIONAL_WAIT_MS = 2000;
+
+/*
+ * Ensure that loading about:newtab doesn't cause uninterruptible reflows.
+ */
+add_task(function* () {
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ return gBrowser.selectedTab = gBrowser.addTab("about:blank", {animate: false});
+ }, false);
+
+ let browser = gBrowser.selectedBrowser;
+ let mm = browser.messageManager;
+ mm.loadFrameScript(FRAME_SCRIPT, true);
+ mm.addMessageListener("newtab-reflow", ({data: stack}) => {
+ ok(false, `unexpected uninterruptible reflow ${stack}`);
+ });
+
+ let browserLoadedPromise = BrowserTestUtils.waitForEvent(browser, "load", true);
+ browser.loadURI("about:newtab");
+ yield browserLoadedPromise;
+
+ // Wait some more to catch sync reflows after the page has loaded.
+ yield new Promise(resolve => {
+ setTimeout(resolve, ADDITIONAL_WAIT_MS);
+ });
+
+ // Clean up.
+ gBrowser.removeCurrentTab({animate: false});
+
+ ok(true, "Each test requires at least one pass, fail or todo so here is a pass.");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_reportLinkAction.js b/browser/base/content/test/newtab/browser_newtab_reportLinkAction.js
new file mode 100644
index 000000000..24e1be369
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_reportLinkAction.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PRELOAD_PREF = "browser.newtab.preload";
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+ "directory": [{
+ url: "http://example.com/organic",
+ type: "organic"
+ }, {
+ url: "http://localhost/sponsored",
+ type: "sponsored"
+ }]
+});
+
+add_task(function* () {
+ yield pushPrefs([PRELOAD_PREF, false]);
+
+ let originalReportSitesAction = DirectoryLinksProvider.reportSitesAction;
+ registerCleanupFunction(() => {
+ DirectoryLinksProvider.reportSitesAction = originalReportSitesAction;
+ });
+
+ let expected = {};
+
+ function expectReportSitesAction() {
+ return new Promise(resolve => {
+ DirectoryLinksProvider.reportSitesAction = function(sites, action, siteIndex) {
+ let {link} = sites[siteIndex];
+ is(link.type, expected.type, "got expected type");
+ is(action, expected.action, "got expected action");
+ is(NewTabUtils.pinnedLinks.isPinned(link), expected.pinned, "got expected pinned");
+ resolve();
+ }
+ });
+ }
+
+ // Test that the last visible site (index 1) is reported
+ let reportSitesPromise = expectReportSitesAction();
+ expected.type = "sponsored";
+ expected.action = "view";
+ expected.pinned = false;
+ yield* addNewTabPageTab();
+ yield reportSitesPromise;
+
+ // Click the pin button on the link in the 1th tile spot
+ expected.action = "pin";
+ // tiles become "history" when pinned
+ expected.type = "history";
+ expected.pinned = true;
+ let pagesUpdatedPromise = whenPagesUpdated();
+ reportSitesPromise = expectReportSitesAction();
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-cell + .newtab-cell .newtab-control-pin", {}, gBrowser.selectedBrowser);
+ yield pagesUpdatedPromise;
+ yield reportSitesPromise;
+
+ // Unpin that link
+ expected.action = "unpin";
+ expected.pinned = false;
+ pagesUpdatedPromise = whenPagesUpdated();
+ reportSitesPromise = expectReportSitesAction();
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-cell + .newtab-cell .newtab-control-pin", {}, gBrowser.selectedBrowser);
+ yield pagesUpdatedPromise;
+ yield reportSitesPromise;
+
+ // Block the site in the 0th tile spot
+ expected.type = "organic";
+ expected.action = "block";
+ expected.pinned = false;
+ pagesUpdatedPromise = whenPagesUpdated();
+ reportSitesPromise = expectReportSitesAction();
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-site .newtab-control-block", {}, gBrowser.selectedBrowser);
+ yield pagesUpdatedPromise;
+ yield reportSitesPromise;
+
+ // Click the 1th link now in the 0th tile spot
+ expected.type = "history";
+ expected.action = "click";
+ reportSitesPromise = expectReportSitesAction();
+ yield BrowserTestUtils.synthesizeMouseAtCenter(".newtab-site", {}, gBrowser.selectedBrowser);
+ yield reportSitesPromise;
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_search.js b/browser/base/content/test/newtab/browser_newtab_search.js
new file mode 100644
index 000000000..19ed4ba74
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_search.js
@@ -0,0 +1,247 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// See browser/components/search/test/browser_*_behavior.js for tests of actual
+// searches.
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+const ENGINE_NO_LOGO = {
+ name: "searchEngineNoLogo.xml",
+ numLogos: 0,
+};
+
+const ENGINE_FAVICON = {
+ name: "searchEngineFavicon.xml",
+ logoPrefix1x: "data:image/png;base64,AAABAAIAICAAAAEAIACoEAAAJgAAABAQAAABACAAaAQAAM4QAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAATCwAAEwsA",
+ numLogos: 1,
+};
+ENGINE_FAVICON.logoPrefix2x = ENGINE_FAVICON.logoPrefix1x;
+
+const ENGINE_1X_LOGO = {
+ name: "searchEngine1xLogo.xml",
+ logoPrefix1x: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAAAaCAIAAABn3KYmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkTADEw",
+ numLogos: 1,
+};
+ENGINE_1X_LOGO.logoPrefix2x = ENGINE_1X_LOGO.logoPrefix1x;
+
+const ENGINE_2X_LOGO = {
+ name: "searchEngine2xLogo.xml",
+ logoPrefix2x: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIIAAAA0CAIAAADJ8nfCAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkTADMU",
+ numLogos: 1,
+};
+ENGINE_2X_LOGO.logoPrefix1x = ENGINE_2X_LOGO.logoPrefix2x;
+
+const ENGINE_1X_2X_LOGO = {
+ name: "searchEngine1x2xLogo.xml",
+ logoPrefix1x: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAAAaCAIAAABn3KYmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkTADIG",
+ logoPrefix2x: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIIAAAA0CAIAAADJ8nfCAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkTADMo",
+ numLogos: 2,
+};
+
+const ENGINE_SUGGESTIONS = {
+ name: "searchSuggestionEngine.xml",
+ numLogos: 0,
+};
+
+// The test has an expected search event queue and a search event listener.
+// Search events that are expected to happen are added to the queue, and the
+// listener consumes the queue and ensures that each event it receives is at
+// the head of the queue.
+let gExpectedSearchEventQueue = [];
+let gExpectedSearchEventResolver = null;
+
+let gNewEngines = [];
+
+add_task(function* () {
+ let oldCurrentEngine = Services.search.currentEngine;
+
+ yield* addNewTabPageTab();
+
+ // The tab is removed at the end of the test, so there's no need to remove
+ // this listener at the end of the test.
+ info("Adding search event listener");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ const SERVICE_EVENT_NAME = "ContentSearchService";
+ content.addEventListener(SERVICE_EVENT_NAME, function (event) {
+ sendAsyncMessage("test:search-event", { eventType: event.detail.type });
+ });
+ });
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.addMessageListener("test:search-event", function (message) {
+ let eventType = message.data.eventType;
+ if (!gExpectedSearchEventResolver) {
+ ok(false, "Got search event " + eventType + " with no promise assigned");
+ }
+
+ let expectedEventType = gExpectedSearchEventQueue.shift();
+ is(eventType, expectedEventType, "Got expected search event " + expectedEventType);
+ if (!gExpectedSearchEventQueue.length) {
+ gExpectedSearchEventResolver();
+ gExpectedSearchEventResolver = null;
+ }
+ });
+
+ // Add the engine without any logos and switch to it.
+ let noLogoEngine = yield promiseNewSearchEngine(ENGINE_NO_LOGO);
+ let searchEventsPromise = promiseSearchEvents(["CurrentEngine"]);
+ Services.search.currentEngine = noLogoEngine;
+ yield searchEventsPromise;
+ yield* checkCurrentEngine(ENGINE_NO_LOGO);
+
+ // Add the engine with favicon and switch to it.
+ let faviconEngine = yield promiseNewSearchEngine(ENGINE_FAVICON);
+ searchEventsPromise = promiseSearchEvents(["CurrentEngine"]);
+ Services.search.currentEngine = faviconEngine;
+ yield searchEventsPromise;
+ yield* checkCurrentEngine(ENGINE_FAVICON);
+
+ // Add the engine with a 1x-DPI logo and switch to it.
+ let logo1xEngine = yield promiseNewSearchEngine(ENGINE_1X_LOGO);
+ searchEventsPromise = promiseSearchEvents(["CurrentEngine"]);
+ Services.search.currentEngine = logo1xEngine;
+ yield searchEventsPromise;
+ yield* checkCurrentEngine(ENGINE_1X_LOGO);
+
+ // Add the engine with a 2x-DPI logo and switch to it.
+ let logo2xEngine = yield promiseNewSearchEngine(ENGINE_2X_LOGO);
+ searchEventsPromise = promiseSearchEvents(["CurrentEngine"]);
+ Services.search.currentEngine = logo2xEngine;
+ yield searchEventsPromise;
+ yield* checkCurrentEngine(ENGINE_2X_LOGO);
+
+ // Add the engine with 1x- and 2x-DPI logos and switch to it.
+ let logo1x2xEngine = yield promiseNewSearchEngine(ENGINE_1X_2X_LOGO);
+ searchEventsPromise = promiseSearchEvents(["CurrentEngine"]);
+ Services.search.currentEngine = logo1x2xEngine;
+ yield searchEventsPromise;
+ yield* checkCurrentEngine(ENGINE_1X_2X_LOGO);
+
+ // Add the engine that provides search suggestions and switch to it.
+ let suggestionEngine = yield promiseNewSearchEngine(ENGINE_SUGGESTIONS);
+ searchEventsPromise = promiseSearchEvents(["CurrentEngine"]);
+ Services.search.currentEngine = suggestionEngine;
+ yield searchEventsPromise;
+ yield* checkCurrentEngine(ENGINE_SUGGESTIONS);
+
+ // Avoid intermittent failures.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ content.gSearch._contentSearchController.remoteTimeout = 5000;
+ });
+
+ // Type an X in the search input. This is only a smoke test. See
+ // browser_searchSuggestionUI.js for comprehensive content search suggestion
+ // UI tests.
+ let suggestionsOpenPromise = new Promise(resolve => {
+ mm.addMessageListener("test:newtab-suggestions-open", function onResponse(message) {
+ mm.removeMessageListener("test:newtab-suggestions-open", onResponse);
+ resolve();
+ });
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let table = content.document.getElementById("searchSuggestionTable");
+
+ let input = content.document.getElementById("newtab-search-text");
+ input.focus();
+
+ info("Waiting for suggestions table to open");
+ let observer = new content.MutationObserver(() => {
+ if (input.getAttribute("aria-expanded") == "true") {
+ observer.disconnect();
+ Assert.ok(!table.hidden, "Search suggestion table unhidden");
+ sendAsyncMessage("test:newtab-suggestions-open", {});
+ }
+ });
+ observer.observe(input, {
+ attributes: true,
+ attributeFilter: ["aria-expanded"],
+ });
+ });
+
+ let suggestionsPromise = promiseSearchEvents(["Suggestions"]);
+
+ EventUtils.synthesizeKey("x", {});
+
+ // Wait for the search suggestions to become visible and for the Suggestions
+ // message.
+ yield suggestionsOpenPromise;
+ yield suggestionsPromise;
+
+ // Empty the search input, causing the suggestions to be hidden.
+ EventUtils.synthesizeKey("a", { accelKey: true });
+ EventUtils.synthesizeKey("VK_DELETE", {});
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ Assert.ok(content.document.getElementById("searchSuggestionTable").hidden,
+ "Search suggestion table hidden");
+ });
+
+ // Done. Revert the current engine and remove the new engines.
+ searchEventsPromise = promiseSearchEvents(["CurrentEngine"]);
+ Services.search.currentEngine = oldCurrentEngine;
+ yield searchEventsPromise;
+
+ let events = Array(gNewEngines.length).fill("CurrentState", 0, gNewEngines.length);
+ searchEventsPromise = promiseSearchEvents(events);
+
+ for (let engine of gNewEngines) {
+ Services.search.removeEngine(engine);
+ }
+ yield searchEventsPromise;
+});
+
+function promiseSearchEvents(events) {
+ info("Expecting search events: " + events);
+ return new Promise(resolve => {
+ gExpectedSearchEventQueue.push(...events);
+ gExpectedSearchEventResolver = resolve;
+ });
+}
+
+function promiseNewSearchEngine({name: basename, numLogos}) {
+ info("Waiting for engine to be added: " + basename);
+
+ // Wait for the search events triggered by adding the new engine.
+ // engine-added engine-loaded
+ let expectedSearchEvents = ["CurrentState", "CurrentState"];
+ // engine-changed for each of the logos
+ for (let i = 0; i < numLogos; i++) {
+ expectedSearchEvents.push("CurrentState");
+ }
+ let eventPromise = promiseSearchEvents(expectedSearchEvents);
+
+ // Wait for addEngine().
+ let addEnginePromise = new Promise((resolve, reject) => {
+ let url = getRootDirectory(gTestPath) + basename;
+ Services.search.addEngine(url, null, "", false, {
+ onSuccess: function (engine) {
+ info("Search engine added: " + basename);
+ gNewEngines.push(engine);
+ resolve(engine);
+ },
+ onError: function (errCode) {
+ ok(false, "addEngine failed with error code " + errCode);
+ reject();
+ },
+ });
+ });
+
+ return Promise.all([addEnginePromise, eventPromise]).then(([newEngine, _]) => {
+ return newEngine;
+ });
+}
+
+function* checkCurrentEngine(engineInfo)
+{
+ let engine = Services.search.currentEngine;
+ ok(engine.name.includes(engineInfo.name),
+ "Sanity check: current engine: engine.name=" + engine.name +
+ " basename=" + engineInfo.name);
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { name: engine.name }, function* (args) {
+ Assert.equal(content.gSearch._contentSearchController.defaultEngine.name,
+ args.name, "currentEngineName: " + args.name);
+ });
+}
diff --git a/browser/base/content/test/newtab/browser_newtab_sponsored_icon_click.js b/browser/base/content/test/newtab/browser_newtab_sponsored_icon_click.js
new file mode 100644
index 000000000..f6bb85d47
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_sponsored_icon_click.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* () {
+ yield setLinks("0");
+ yield* addNewTabPageTab();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ var EventUtils = {};
+ EventUtils.window = {};
+ EventUtils.parent = EventUtils.window;
+ EventUtils._EU_Ci = Components.interfaces;
+
+ Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+ let cell = content.gGrid.cells[0];
+
+ let site = cell.node.querySelector(".newtab-site");
+ site.setAttribute("type", "sponsored");
+
+ // test explain text appearing upon a click
+ let sponsoredButton = site.querySelector(".newtab-sponsored");
+ EventUtils.synthesizeMouseAtCenter(sponsoredButton, {}, content);
+ let explain = site.querySelector(".sponsored-explain");
+ Assert.notEqual(explain, null, "Sponsored explanation shown");
+ Assert.ok(explain.querySelector("input").classList.contains("newtab-control-block"),
+ "sponsored tiles show blocked image");
+ Assert.ok(sponsoredButton.hasAttribute("active"), "Sponsored button has active attribute");
+
+ // test dismissing sponsored explain
+ EventUtils.synthesizeMouseAtCenter(sponsoredButton, {}, content);
+ Assert.equal(site.querySelector(".sponsored-explain"), null,
+ "Sponsored explanation no longer shown");
+ Assert.ok(!sponsoredButton.hasAttribute("active"),
+ "Sponsored button does not have active attribute");
+
+ // test with enhanced tile
+ site.setAttribute("type", "enhanced");
+ EventUtils.synthesizeMouseAtCenter(sponsoredButton, {}, content);
+ explain = site.querySelector(".sponsored-explain");
+ Assert.notEqual(explain, null, "Sponsored explanation shown");
+ Assert.ok(explain.querySelector("input").classList.contains("newtab-customize"),
+ "enhanced tiles show customize image");
+ Assert.ok(sponsoredButton.hasAttribute("active"), "Sponsored button has active attribute");
+
+ // test dismissing enhanced explain
+ EventUtils.synthesizeMouseAtCenter(sponsoredButton, {}, content);
+ Assert.equal(site.querySelector(".sponsored-explain"), null,
+ "Sponsored explanation no longer shown");
+ Assert.ok(!sponsoredButton.hasAttribute("active"),
+ "Sponsored button does not have active attribute");
+ });
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_undo.js b/browser/base/content/test/newtab/browser_newtab_undo.js
new file mode 100644
index 000000000..ba094cb26
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_undo.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that the undo dialog works as expected.
+ */
+add_task(function* () {
+ // remove unpinned sites and undo it
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks("5");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("5p,0,1,2,3,4,6,7,8");
+
+ yield blockCell(4);
+ yield blockCell(4);
+ yield* checkGrid("5p,0,1,2,6,7,8");
+
+ yield* undo();
+ yield* checkGrid("5p,0,1,2,4,6,7,8");
+
+ // now remove a pinned site and undo it
+ yield blockCell(0);
+ yield* checkGrid("0,1,2,4,6,7,8");
+
+ yield* undo();
+ yield* checkGrid("5p,0,1,2,4,6,7,8");
+
+ // remove a site and restore all
+ yield blockCell(1);
+ yield* checkGrid("5p,1,2,4,6,7,8");
+
+ yield* undoAll();
+ yield* checkGrid("5p,0,1,2,3,4,6,7,8");
+});
+
+function* undo() {
+ let updatedPromise = whenPagesUpdated();
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#newtab-undo-button", {}, gBrowser.selectedBrowser);
+ yield updatedPromise;
+}
+
+function* undoAll() {
+ let updatedPromise = whenPagesUpdated();
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#newtab-undo-restore-button", {}, gBrowser.selectedBrowser);
+ yield updatedPromise;
+}
diff --git a/browser/base/content/test/newtab/browser_newtab_unpin.js b/browser/base/content/test/newtab/browser_newtab_unpin.js
new file mode 100644
index 000000000..14751465f
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_unpin.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that when a site gets unpinned it is either moved to
+ * its actual place in the grid or removed in case it's not on the grid anymore.
+ */
+add_task(function* () {
+ // we have a pinned link that didn't change its position since it was pinned.
+ // nothing should happen when we unpin it.
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",1");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,1p,2,3,4,5,6,7,8");
+
+ yield unpinCell(1);
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ // we have a pinned link that is not anymore in the list of the most-visited
+ // links. this should disappear, the remaining links adjust their positions
+ // and a new link will appear at the end of the grid.
+ yield setLinks("0,1,2,3,4,5,6,7,8");
+ setPinnedLinks(",99");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("0,99p,1,2,3,4,5,6,7");
+
+ yield unpinCell(1);
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+
+ // we have a pinned link that changed its position since it was pinned. it
+ // should be moved to its new position after being unpinned.
+ yield setLinks("0,1,2,3,4,5,6,7");
+ setPinnedLinks(",1,,,,,,,0");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("2,1p,3,4,5,6,7,,0p");
+
+ yield unpinCell(1);
+ yield* checkGrid("1,2,3,4,5,6,7,,0p");
+
+ yield unpinCell(8);
+ yield* checkGrid("0,1,2,3,4,5,6,7,");
+
+ // we have pinned link that changed its position since it was pinned. the
+ // link will disappear from the grid because it's now a much lower priority
+ yield setLinks("0,1,2,3,4,5,6,7,8,9");
+ setPinnedLinks("9");
+
+ yield* addNewTabPageTab();
+ yield* checkGrid("9p,0,1,2,3,4,5,6,7");
+
+ yield unpinCell(0);
+ yield* checkGrid("0,1,2,3,4,5,6,7,8");
+});
diff --git a/browser/base/content/test/newtab/browser_newtab_update.js b/browser/base/content/test/newtab/browser_newtab_update.js
new file mode 100644
index 000000000..6cf089dfd
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_update.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Checks that newtab is updated as its links change.
+ */
+add_task(function* () {
+ // First, start with an empty page. setLinks will trigger a hidden page
+ // update because it calls clearHistory. We need to wait for that update to
+ // happen so that the next time we wait for a page update below, we catch the
+ // right update and not the one triggered by setLinks.
+ let updatedPromise = whenPagesUpdated();
+ let setLinksPromise = setLinks([]);
+ yield Promise.all([updatedPromise, setLinksPromise]);
+
+ // Strategy: Add some visits, open a new page, check the grid, repeat.
+ yield fillHistoryAndWaitForPageUpdate([1]);
+ yield* addNewTabPageTab();
+ yield* checkGrid("1,,,,,,,,");
+
+ yield fillHistoryAndWaitForPageUpdate([2]);
+ yield* addNewTabPageTab();
+ yield* checkGrid("2,1,,,,,,,");
+
+ yield fillHistoryAndWaitForPageUpdate([1]);
+ yield* addNewTabPageTab();
+ yield* checkGrid("1,2,,,,,,,");
+
+ yield fillHistoryAndWaitForPageUpdate([2, 3, 4]);
+ yield* addNewTabPageTab();
+ yield* checkGrid("2,1,3,4,,,,,");
+
+ // Make sure these added links have the right type
+ let type = yield performOnCell(1, cell => { return cell.site.link.type });
+ is(type, "history", "added link is history");
+});
+
+function fillHistoryAndWaitForPageUpdate(links) {
+ let updatedPromise = whenPagesUpdated;
+ let fillHistoryPromise = fillHistory(links.map(link));
+ return Promise.all([updatedPromise, fillHistoryPromise]);
+}
+
+function link(id) {
+ return { url: "http://example" + id + ".com/", title: "site#" + id };
+}
diff --git a/browser/base/content/test/newtab/content-reflows.js b/browser/base/content/test/newtab/content-reflows.js
new file mode 100644
index 000000000..f1a53782e
--- /dev/null
+++ b/browser/base/content/test/newtab/content-reflows.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/. */
+
+(function () {
+ "use strict";
+
+ const Ci = Components.interfaces;
+
+ docShell.addWeakReflowObserver({
+ reflow() {
+ // Gather information about the current code path.
+ let path = (new Error().stack).split("\n").slice(1).join("\n");
+ if (path) {
+ sendSyncMessage("newtab-reflow", path);
+ }
+ },
+
+ reflowInterruptible() {
+ // We're not interested in interruptible reflows.
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
+ Ci.nsISupportsWeakReference])
+ });
+})();
diff --git a/browser/base/content/test/newtab/head.js b/browser/base/content/test/newtab/head.js
new file mode 100644
index 000000000..d702103a0
--- /dev/null
+++ b/browser/base/content/test/newtab/head.js
@@ -0,0 +1,552 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
+const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directory.source";
+
+Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true);
+
+var tmp = {};
+Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp);
+Cu.import("resource:///modules/DirectoryLinksProvider.jsm", tmp);
+Cu.import("resource://testing-common/PlacesTestUtils.jsm", tmp);
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tmp);
+var {NewTabUtils, Sanitizer, DirectoryLinksProvider, PlacesTestUtils} = tmp;
+
+var gWindow = window;
+
+// Default to dummy/empty directory links
+var gDirectorySource = 'data:application/json,{"test":1}';
+var gOrigDirectorySource;
+
+// The tests assume all 3 rows and all 3 columns of sites are shown, but the
+// window may be too small to actually show everything. Resize it if necessary.
+var requiredSize = {};
+requiredSize.innerHeight =
+ 40 + 32 + // undo container + bottom margin
+ 44 + 32 + // search bar + bottom margin
+ (3 * (180 + 32)) + // 3 rows * (tile height + title and bottom margin)
+ 100; // breathing room
+requiredSize.innerWidth =
+ (3 * (290 + 20)) + // 3 cols * (tile width + side margins)
+ 100; // breathing room
+
+var oldSize = {};
+Object.keys(requiredSize).forEach(prop => {
+ info([prop, gBrowser.contentWindow[prop], requiredSize[prop]]);
+ if (gBrowser.contentWindow[prop] < requiredSize[prop]) {
+ oldSize[prop] = gBrowser.contentWindow[prop];
+ info("Changing browser " + prop + " from " + oldSize[prop] + " to " +
+ requiredSize[prop]);
+ gBrowser.contentWindow[prop] = requiredSize[prop];
+ }
+});
+
+var screenHeight = {};
+var screenWidth = {};
+Cc["@mozilla.org/gfx/screenmanager;1"].
+ getService(Ci.nsIScreenManager).
+ primaryScreen.
+ GetAvailRectDisplayPix({}, {}, screenWidth, screenHeight);
+screenHeight = screenHeight.value;
+screenWidth = screenWidth.value;
+
+if (screenHeight < gBrowser.contentWindow.outerHeight) {
+ info("Warning: Browser outer height is now " +
+ gBrowser.contentWindow.outerHeight + ", which is larger than the " +
+ "available screen height, " + screenHeight +
+ ". That may cause problems.");
+}
+
+if (screenWidth < gBrowser.contentWindow.outerWidth) {
+ info("Warning: Browser outer width is now " +
+ gBrowser.contentWindow.outerWidth + ", which is larger than the " +
+ "available screen width, " + screenWidth +
+ ". That may cause problems.");
+}
+
+registerCleanupFunction(function () {
+ while (gWindow.gBrowser.tabs.length > 1)
+ gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]);
+
+ Object.keys(oldSize).forEach(prop => {
+ if (oldSize[prop]) {
+ gBrowser.contentWindow[prop] = oldSize[prop];
+ }
+ });
+
+ // Stop any update timers to prevent unexpected updates in later tests
+ let timer = NewTabUtils.allPages._scheduleUpdateTimeout;
+ if (timer) {
+ clearTimeout(timer);
+ delete NewTabUtils.allPages._scheduleUpdateTimeout;
+ }
+
+ Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
+ Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, gOrigDirectorySource);
+
+ return watchLinksChangeOnce();
+});
+
+function pushPrefs(...aPrefs) {
+ return new Promise(resolve =>
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve));
+}
+
+/**
+ * Resolves promise when directory links are downloaded and written to disk
+ */
+function watchLinksChangeOnce() {
+ return new Promise(resolve => {
+ let observer = {
+ onManyLinksChanged: () => {
+ DirectoryLinksProvider.removeObserver(observer);
+ resolve();
+ }
+ };
+ observer.onDownloadFail = observer.onManyLinksChanged;
+ DirectoryLinksProvider.addObserver(observer);
+ });
+}
+
+add_task(function* setup() {
+ registerCleanupFunction(function() {
+ return new Promise(resolve => {
+ function cleanupAndFinish() {
+ PlacesTestUtils.clearHistory().then(() => {
+ whenPagesUpdated().then(resolve);
+ NewTabUtils.restore();
+ });
+ }
+
+ let callbacks = NewTabUtils.links._populateCallbacks;
+ let numCallbacks = callbacks.length;
+
+ if (numCallbacks)
+ callbacks.splice(0, numCallbacks, cleanupAndFinish);
+ else
+ cleanupAndFinish();
+ });
+ });
+
+ let promiseReady = Task.spawn(function*() {
+ yield watchLinksChangeOnce();
+ yield whenPagesUpdated();
+ });
+
+ // Save the original directory source (which is set globally for tests)
+ gOrigDirectorySource = Services.prefs.getCharPref(PREF_NEWTAB_DIRECTORYSOURCE);
+ Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, gDirectorySource);
+ yield promiseReady;
+});
+
+/** Perform an action on a cell within the newtab page.
+ * @param aIndex index of cell
+ * @param aFn function to call in child process or tab.
+ * @returns result of calling the function.
+ */
+function performOnCell(aIndex, aFn) {
+ return ContentTask.spawn(gWindow.gBrowser.selectedBrowser,
+ { index: aIndex, fn: aFn.toString() }, function* (args) {
+ let cell = content.gGrid.cells[args.index];
+ return eval(args.fn)(cell);
+ });
+}
+
+/**
+ * Allows to provide a list of links that is used to construct the grid.
+ * @param aLinksPattern the pattern (see below)
+ *
+ * Example: setLinks("-1,0,1,2,3")
+ * Result: [{url: "http://example.com/", title: "site#-1"},
+ * {url: "http://example0.com/", title: "site#0"},
+ * {url: "http://example1.com/", title: "site#1"},
+ * {url: "http://example2.com/", title: "site#2"},
+ * {url: "http://example3.com/", title: "site#3"}]
+ */
+function setLinks(aLinks) {
+ return new Promise(resolve => {
+ let links = aLinks;
+
+ if (typeof links == "string") {
+ links = aLinks.split(/\s*,\s*/).map(function (id) {
+ return {url: "http://example" + (id != "-1" ? id : "") + ".com/",
+ title: "site#" + id};
+ });
+ }
+
+ // Call populateCache() once to make sure that all link fetching that is
+ // currently in progress has ended. We clear the history, fill it with the
+ // given entries and call populateCache() now again to make sure the cache
+ // has the desired contents.
+ NewTabUtils.links.populateCache(function () {
+ PlacesTestUtils.clearHistory().then(() => {
+ fillHistory(links).then(() => {
+ NewTabUtils.links.populateCache(function () {
+ NewTabUtils.allPages.update();
+ resolve();
+ }, true);
+ });
+ });
+ });
+ });
+}
+
+function fillHistory(aLinks) {
+ return new Promise(resolve => {
+ let numLinks = aLinks.length;
+ if (!numLinks) {
+ executeSoon(resolve);
+ return;
+ }
+
+ let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK;
+
+ // Important: To avoid test failures due to clock jitter on Windows XP, call
+ // Date.now() once here, not each time through the loop.
+ let now = Date.now() * 1000;
+
+ for (let i = 0; i < aLinks.length; i++) {
+ let link = aLinks[i];
+ let place = {
+ uri: makeURI(link.url),
+ title: link.title,
+ // Links are secondarily sorted by visit date descending, so decrease the
+ // visit date as we progress through the array so that links appear in the
+ // grid in the order they're present in the array.
+ visits: [{visitDate: now - i, transitionType: transitionLink}]
+ };
+
+ PlacesUtils.asyncHistory.updatePlaces(place, {
+ handleError: () => ok(false, "couldn't add visit to history"),
+ handleResult: function () {},
+ handleCompletion: function () {
+ if (--numLinks == 0) {
+ resolve();
+ }
+ }
+ });
+ }
+ });
+}
+
+/**
+ * Allows to specify the list of pinned links (that have a fixed position in
+ * the grid.
+ * @param aLinksPattern the pattern (see below)
+ *
+ * Example: setPinnedLinks("3,,1")
+ * Result: 'http://example3.com/' is pinned in the first cell. 'http://example1.com/' is
+ * pinned in the third cell.
+ */
+function setPinnedLinks(aLinks) {
+ let links = aLinks;
+
+ if (typeof links == "string") {
+ links = aLinks.split(/\s*,\s*/).map(function (id) {
+ if (id)
+ return {url: "http://example" + (id != "-1" ? id : "") + ".com/",
+ title: "site#" + id,
+ type: "history"};
+ return undefined;
+ });
+ }
+
+ let string = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ string.data = JSON.stringify(links);
+ Services.prefs.setComplexValue("browser.newtabpage.pinned",
+ Ci.nsISupportsString, string);
+
+ NewTabUtils.pinnedLinks.resetCache();
+ NewTabUtils.allPages.update();
+}
+
+/**
+ * Restore the grid state.
+ */
+function restore() {
+ return new Promise(resolve => {
+ whenPagesUpdated().then(resolve);
+ NewTabUtils.restore();
+ });
+}
+
+/**
+ * Wait until a given condition becomes true.
+ */
+function waitForCondition(aConditionFn, aMaxTries=50, aCheckInterval=100) {
+ return new Promise((resolve, reject) => {
+ let tries = 0;
+
+ function tryNow() {
+ tries++;
+
+ if (aConditionFn()) {
+ resolve();
+ } else if (tries < aMaxTries) {
+ tryAgain();
+ } else {
+ reject("Condition timed out: " + aConditionFn.toSource());
+ }
+ }
+
+ function tryAgain() {
+ setTimeout(tryNow, aCheckInterval);
+ }
+
+ tryAgain();
+ });
+}
+
+/**
+ * Creates a new tab containing 'about:newtab'.
+ */
+function* addNewTabPageTab() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gWindow.gBrowser, "about:newtab", false);
+ let browser = tab.linkedBrowser;
+
+ // Wait for the document to become visible in case it was preloaded.
+ yield waitForCondition(() => !browser.contentDocument.hidden)
+
+ yield new Promise(resolve => {
+ if (NewTabUtils.allPages.enabled) {
+ // Continue when the link cache has been populated.
+ NewTabUtils.links.populateCache(function () {
+ whenSearchInitDone().then(resolve);
+ });
+ } else {
+ resolve();
+ }
+ });
+
+ return tab;
+}
+
+/**
+ * Compares the current grid arrangement with the given pattern.
+ * @param the pattern (see below)
+ *
+ * Example: checkGrid("3p,2,,1p")
+ * Result: We expect the first cell to contain the pinned site 'http://example3.com/'.
+ * The second cell contains 'http://example2.com/'. The third cell is empty.
+ * The fourth cell contains the pinned site 'http://example4.com/'.
+ */
+function* checkGrid(pattern) {
+ let length = pattern.split(",").length;
+
+ yield ContentTask.spawn(gWindow.gBrowser.selectedBrowser,
+ { length, pattern }, function* (args) {
+ let grid = content.wrappedJSObject.gGrid;
+
+ let sites = grid.sites.slice(0, args.length);
+ let foundPattern = sites.map(function (aSite) {
+ if (!aSite)
+ return "";
+
+ let pinned = aSite.isPinned();
+ let hasPinnedAttr = aSite.node.hasAttribute("pinned");
+
+ if (pinned != hasPinnedAttr)
+ ok(false, "invalid state (site.isPinned() != site[pinned])");
+
+ return aSite.url.replace(/^http:\/\/example(\d+)\.com\/$/, "$1") + (pinned ? "p" : "");
+ });
+
+ Assert.equal(foundPattern, args.pattern, "grid status = " + args.pattern);
+ });
+}
+
+/**
+ * Blocks a site from the grid.
+ * @param aIndex The cell index.
+ */
+function blockCell(aIndex) {
+ return new Promise(resolve => {
+ whenPagesUpdated().then(resolve);
+ performOnCell(aIndex, cell => {
+ return cell.site.block();
+ });
+ });
+}
+
+/**
+ * Pins a site on a given position.
+ * @param aIndex The cell index.
+ * @param aPinIndex The index the defines where the site should be pinned.
+ */
+function pinCell(aIndex) {
+ performOnCell(aIndex, cell => {
+ cell.site.pin();
+ });
+}
+
+/**
+ * Unpins the given cell's site.
+ * @param aIndex The cell index.
+ */
+function unpinCell(aIndex) {
+ return new Promise(resolve => {
+ whenPagesUpdated().then(resolve);
+ performOnCell(aIndex, cell => {
+ cell.site.unpin();
+ });
+ });
+}
+
+/**
+ * Simulates a drag and drop operation. Instead of rearranging a site that is
+ * is already contained in the newtab grid, this is used to simulate dragging
+ * an external link onto the grid e.g. the text from the URL bar.
+ * @param aDestIndex The cell index of the drop target.
+ */
+function* simulateExternalDrop(aDestIndex) {
+ let pagesUpdatedPromise = whenPagesUpdated();
+
+ yield ContentTask.spawn(gWindow.gBrowser.selectedBrowser, aDestIndex, function*(dropIndex) {
+ return new Promise(resolve => {
+ const url = "data:text/html;charset=utf-8," +
+ "<a id='link' href='http://example99.com/'>link</a>";
+
+ let doc = content.document;
+ let iframe = doc.createElement("iframe");
+
+ function iframeLoaded() {
+ let dataTransfer = new iframe.contentWindow.DataTransfer("dragstart", false);
+ dataTransfer.mozSetDataAt("text/x-moz-url", "http://example99.com/", 0);
+
+ let event = content.document.createEvent("DragEvent");
+ event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0,
+ false, false, false, false, 0, null, dataTransfer);
+
+ let target = content.gGrid.cells[dropIndex].node;
+ target.dispatchEvent(event);
+
+ iframe.remove();
+
+ resolve();
+ }
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad);
+ content.setTimeout(iframeLoaded, 0);
+ });
+
+ iframe.setAttribute("src", url);
+ iframe.style.width = "50px";
+ iframe.style.height = "50px";
+ iframe.style.position = "absolute";
+ iframe.style.zIndex = 50;
+
+ // the frame has to be attached to a visible element
+ let margin = doc.getElementById("newtab-search-container");
+ margin.appendChild(iframe);
+ });
+ });
+
+ yield pagesUpdatedPromise;
+}
+
+/**
+ * Resumes testing when all pages have been updated.
+ */
+function whenPagesUpdated() {
+ return new Promise(resolve => {
+ let page = {
+ observe: _ => _,
+
+ update() {
+ NewTabUtils.allPages.unregister(this);
+ executeSoon(resolve);
+ }
+ };
+
+ NewTabUtils.allPages.register(page);
+ registerCleanupFunction(function () {
+ NewTabUtils.allPages.unregister(page);
+ });
+ });
+}
+
+/**
+ * Waits for the response to the page's initial search state request.
+ */
+function whenSearchInitDone() {
+ return ContentTask.spawn(gWindow.gBrowser.selectedBrowser, {}, function*() {
+ return new Promise(resolve => {
+ if (content.gSearch) {
+ let searchController = content.gSearch._contentSearchController;
+ if (searchController.defaultEngine) {
+ resolve();
+ return;
+ }
+ }
+
+ let eventName = "ContentSearchService";
+ content.addEventListener(eventName, function onEvent(event) {
+ if (event.detail.type == "State") {
+ content.removeEventListener(eventName, onEvent);
+ let resolver = function() {
+ // Wait for the search controller to receive the event, then resolve.
+ if (content.gSearch._contentSearchController.defaultEngine) {
+ resolve();
+ return;
+ }
+ }
+ content.setTimeout(resolver, 0);
+ }
+ });
+ });
+ });
+}
+
+/**
+ * Changes the newtab customization option and waits for the panel to open and close
+ *
+ * @param {string} aTheme
+ * Can be any of("blank"|"classic"|"enhanced")
+ */
+function customizeNewTabPage(aTheme) {
+ return ContentTask.spawn(gWindow.gBrowser.selectedBrowser, aTheme, function*(aTheme) {
+
+ let document = content.document;
+ let panel = document.getElementById("newtab-customize-panel");
+ let customizeButton = document.getElementById("newtab-customize-button");
+
+ function panelOpened(opened) {
+ return new Promise( (resolve) => {
+ let options = {attributes: true, oldValue: true};
+ let observer = new content.MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ document.getElementById("newtab-customize-" + aTheme).click();
+ observer.disconnect();
+ if (opened == panel.hasAttribute("open")) {
+ resolve();
+ }
+ });
+ });
+ observer.observe(panel, options);
+ });
+ }
+
+ let opened = panelOpened(true);
+ customizeButton.click();
+ yield opened;
+
+ let closed = panelOpened(false);
+ customizeButton.click();
+ yield closed;
+ });
+}
+
+/**
+ * Reports presence of a scrollbar
+ */
+function hasScrollbar() {
+ return ContentTask.spawn(gWindow.gBrowser.selectedBrowser, {}, function* () {
+ let docElement = content.document.documentElement;
+ return docElement.scrollHeight > docElement.clientHeight;
+ });
+}
diff --git a/browser/base/content/test/newtab/searchEngine1x2xLogo.xml b/browser/base/content/test/newtab/searchEngine1x2xLogo.xml
new file mode 100644
index 000000000..c8b6749b3
--- /dev/null
+++ b/browser/base/content/test/newtab/searchEngine1x2xLogo.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_newtab_search searchEngine1x2xLogo.xml</ShortName>
+<Url type="text/html" method="GET" template="http://browser-newtab-search.com/1x2xlogo" rel="searchform"/>
+<!-- #00FF00 -->
+<Image width="65" height="26">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAAAaCAIAAABn3KYmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkTADIGr0+8uwAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAL0lEQVRYw+3PAREAAAQEsKd/Z3I4W4NVJtd14uDg4ODg4ODg4ODg4ODg4ODg8Pqw7M0BM+n9I0oAAAAASUVORK5CYII=</Image>
+<!-- #00FFFF -->
+<Image width="130" height="52">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIIAAAA0CAIAAADJ8nfCAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkTADMoaoKANQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAYklEQVR42u3RQQ0AAAjEMMC/58MECZ9OwtqVlL4bCzAIAwZhwCAMGIQBgzBgEAYMwoBBGDAIAwZhwCAMGIQBgzBgEAYMwoBBGIQBgzBgEAYMwoBBGDAIAwZhwCAMGIQBg+5ar7sCZiri9VUAAAAASUVORK5CYII=</Image>
+</SearchPlugin>
diff --git a/browser/base/content/test/newtab/searchEngine1xLogo.xml b/browser/base/content/test/newtab/searchEngine1xLogo.xml
new file mode 100644
index 000000000..19ac03f48
--- /dev/null
+++ b/browser/base/content/test/newtab/searchEngine1xLogo.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_newtab_search searchEngine1xLogo.xml</ShortName>
+<Url type="text/html" method="GET" template="http://browser-newtab-search.com/1xlogo" rel="searchform"/>
+<!-- #FF0000 -->
+<Image width="65" height="26">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAAAaCAIAAABn3KYmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkTADEwS9h64QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAL0lEQVRYw+3PAREAAAQEsKd/Z3I4W4PV5LyOg4ODg4ODg4ODg4ODg4ODg4PD78MC7cwBM02qquMAAAAASUVORK5CYII=</Image>
+</SearchPlugin>
diff --git a/browser/base/content/test/newtab/searchEngine2xLogo.xml b/browser/base/content/test/newtab/searchEngine2xLogo.xml
new file mode 100644
index 000000000..941bf040d
--- /dev/null
+++ b/browser/base/content/test/newtab/searchEngine2xLogo.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_newtab_search searchEngine2xLogo.xml</ShortName>
+<Url type="text/html" method="GET" template="http://browser-newtab-search.com/2xlogo" rel="searchform"/>
+<!-- #0000FF -->
+<Image width="130" height="52">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIIAAAA0CAIAAADJ8nfCAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gkTADMURe38sgAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAYklEQVR42u3RQQ0AAAjEsAP/nsEECZ9OwlrJRN+1BRiEAYMwYBAGDMKAQRgwCAMGYcAgDBiEAYMwYBAGDMKAQRgwCAMGYcAgDMKAQRgwCAMGYcAgDBiEAYMwYBAGDMKAQXctkIQBZ/1YP3YAAAAASUVORK5CYII=</Image>
+</SearchPlugin>
diff --git a/browser/base/content/test/newtab/searchEngineFavicon.xml b/browser/base/content/test/newtab/searchEngineFavicon.xml
new file mode 100644
index 000000000..6f2a970f5
--- /dev/null
+++ b/browser/base/content/test/newtab/searchEngineFavicon.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_newtab_search searchEngineFavicon.xml</ShortName>
+<Url type="text/html" method="GET" template="http://browser-newtab-search.com/1xlogo" rel="searchform"/>
+<Image width="16" height="16">data:application/ico;base64,AAABAAIAICAAAAEAIACoEAAAJgAAABAQAAABACAAaAQAAM4QAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAATCwAAEwsAAAAAAAAAAAAA/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAABAAAAAgAAAAAQAgAAAAAAAABAAAEwsAABMLAAAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</Image>
+</SearchPlugin>
diff --git a/browser/base/content/test/newtab/searchEngineNoLogo.xml b/browser/base/content/test/newtab/searchEngineNoLogo.xml
new file mode 100644
index 000000000..bbff6cf8f
--- /dev/null
+++ b/browser/base/content/test/newtab/searchEngineNoLogo.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_newtab_search searchEngineNoLogo.xml</ShortName>
+<Url type="text/html" method="GET" template="http://browser-newtab-search.com/nologo" rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/base/content/test/plugins/.eslintrc.js b/browser/base/content/test/plugins/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/plugins/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/plugins/blockNoPlugins.xml b/browser/base/content/test/plugins/blockNoPlugins.xml
new file mode 100644
index 000000000..e4e191b37
--- /dev/null
+++ b/browser/base/content/test/plugins/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/browser/base/content/test/plugins/blockPluginHard.xml b/browser/base/content/test/plugins/blockPluginHard.xml
new file mode 100644
index 000000000..24eb5bc6f
--- /dev/null
+++ b/browser/base/content/test/plugins/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/browser/base/content/test/plugins/blockPluginInfoURL.xml b/browser/base/content/test/plugins/blockPluginInfoURL.xml
new file mode 100644
index 000000000..c16808896
--- /dev/null
+++ b/browser/base/content/test/plugins/blockPluginInfoURL.xml
@@ -0,0 +1,12 @@
+<?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>
+ <infoURL>http://test.url.com/</infoURL>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/browser/base/content/test/plugins/blockPluginVulnerableNoUpdate.xml b/browser/base/content/test/plugins/blockPluginVulnerableNoUpdate.xml
new file mode 100644
index 000000000..bf8545afe
--- /dev/null
+++ b/browser/base/content/test/plugins/blockPluginVulnerableNoUpdate.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="0" vulnerabilitystatus="2"></versionRange>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/browser/base/content/test/plugins/blockPluginVulnerableUpdatable.xml b/browser/base/content/test/plugins/blockPluginVulnerableUpdatable.xml
new file mode 100644
index 000000000..5545162b1
--- /dev/null
+++ b/browser/base/content/test/plugins/blockPluginVulnerableUpdatable.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="0" vulnerabilitystatus="1"></versionRange>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/browser/base/content/test/plugins/blocklist_proxy.js b/browser/base/content/test/plugins/blocklist_proxy.js
new file mode 100644
index 000000000..1a4ed4726
--- /dev/null
+++ b/browser/base/content/test/plugins/blocklist_proxy.js
@@ -0,0 +1,78 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cm = Components.manager;
+
+const kBlocklistServiceUUID = "{66354bc9-7ed1-4692-ae1d-8da97d6b205e}";
+const kBlocklistServiceContractID = "@mozilla.org/extensions/blocklist;1";
+const kBlocklistServiceFactory = Cm.getClassObject(Cc[kBlocklistServiceContractID], Ci.nsIFactory);
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+/*
+ * A lightweight blocklist proxy for the testing purposes.
+ */
+var BlocklistProxy = {
+ _uuid: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIBlocklistService,
+ Ci.nsITimerCallback]),
+
+ init: function() {
+ if (!this._uuid) {
+ this._uuid =
+ Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator)
+ .generateUUID();
+ Cm.nsIComponentRegistrar.registerFactory(this._uuid, "",
+ "@mozilla.org/extensions/blocklist;1",
+ this);
+ }
+ },
+
+ uninit: function() {
+ if (this._uuid) {
+ Cm.nsIComponentRegistrar.unregisterFactory(this._uuid, this);
+ Cm.nsIComponentRegistrar.registerFactory(Components.ID(kBlocklistServiceUUID),
+ "Blocklist Service",
+ "@mozilla.org/extensions/blocklist;1",
+ kBlocklistServiceFactory);
+ this._uuid = null;
+ }
+ },
+
+ notify: function (aTimer) {
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ },
+
+ isAddonBlocklisted: function (aAddon, aAppVersion, aToolkitVersion) {
+ return false;
+ },
+
+ getAddonBlocklistState: function (aAddon, aAppVersion, aToolkitVersion) {
+ return 0; // STATE_NOT_BLOCKED
+ },
+
+ getPluginBlocklistState: function (aPluginTag, aAppVersion, aToolkitVersion) {
+ return 0; // STATE_NOT_BLOCKED
+ },
+
+ getAddonBlocklistURL: function (aAddon, aAppVersion, aToolkitVersion) {
+ return "";
+ },
+
+ getPluginBlocklistURL: function (aPluginTag) {
+ return "";
+ },
+
+ getPluginInfoURL: function (aPluginTag) {
+ return "";
+ },
+}
+
+BlocklistProxy.init();
+addEventListener("unload", () => {
+ BlocklistProxy.uninit();
+});
diff --git a/browser/base/content/test/plugins/browser.ini b/browser/base/content/test/plugins/browser.ini
new file mode 100644
index 000000000..cfc1f769c
--- /dev/null
+++ b/browser/base/content/test/plugins/browser.ini
@@ -0,0 +1,78 @@
+[DEFAULT]
+support-files =
+ blocklist_proxy.js
+ blockNoPlugins.xml
+ blockPluginHard.xml
+ blockPluginInfoURL.xml
+ blockPluginVulnerableNoUpdate.xml
+ blockPluginVulnerableUpdatable.xml
+ browser_clearplugindata.html
+ browser_clearplugindata_noage.html
+ head.js
+ plugin_add_dynamically.html
+ plugin_alternate_content.html
+ plugin_big.html
+ plugin_both.html
+ plugin_both2.html
+ plugin_bug744745.html
+ plugin_bug749455.html
+ plugin_bug787619.html
+ plugin_bug797677.html
+ plugin_bug820497.html
+ plugin_clickToPlayAllow.html
+ plugin_clickToPlayDeny.html
+ plugin_data_url.html
+ plugin_hidden_to_visible.html
+ plugin_iframe.html
+ plugin_outsideScrollArea.html
+ plugin_overlayed.html
+ plugin_positioned.html
+ plugin_small.html
+ plugin_small_2.html
+ plugin_syncRemoved.html
+ plugin_test.html
+ plugin_test2.html
+ plugin_test3.html
+ plugin_two_types.html
+ plugin_unknown.html
+ plugin_crashCommentAndURL.html
+ plugin_zoom.html
+
+[browser_bug743421.js]
+[browser_bug744745.js]
+[browser_bug787619.js]
+[browser_bug797677.js]
+[browser_bug812562.js]
+[browser_bug818118.js]
+[browser_bug820497.js]
+[browser_clearplugindata.js]
+[browser_CTP_context_menu.js]
+skip-if = toolkit == "gtk2" || toolkit == "gtk3" # fails intermittently on Linux (bug 909342)
+[browser_CTP_crashreporting.js]
+skip-if = !crashreporter
+[browser_CTP_data_urls.js]
+[browser_CTP_drag_drop.js]
+[browser_CTP_hide_overlay.js]
+[browser_CTP_iframe.js]
+[browser_CTP_multi_allow.js]
+[browser_CTP_nonplugins.js]
+[browser_CTP_notificationBar.js]
+[browser_CTP_outsideScrollArea.js]
+[browser_CTP_remove_navigate.js]
+[browser_CTP_resize.js]
+[browser_CTP_zoom.js]
+[browser_blocking.js]
+[browser_plugins_added_dynamically.js]
+[browser_pluginnotification.js]
+[browser_plugin_reloading.js]
+[browser_blocklist_content.js]
+skip-if = !e10s
+[browser_globalplugin_crashinfobar.js]
+skip-if = !crashreporter
+[browser_pluginCrashCommentAndURL.js]
+skip-if = !crashreporter
+[browser_pageInfo_plugins.js]
+[browser_pluginCrashReportNonDeterminism.js]
+skip-if = !crashreporter || os == 'linux' # Bug 1152811
+[browser_private_clicktoplay.js]
+
diff --git a/browser/base/content/test/plugins/browser_CTP_context_menu.js b/browser/base/content/test/plugins/browser_CTP_context_menu.js
new file mode 100644
index 000000000..03f3e18ef
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_context_menu.js
@@ -0,0 +1,69 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ });
+});
+
+// Test that the activate action in content menus for CTP plugins works
+add_task(function* () {
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ let bindingPromise = waitForEvent(gBrowser.selectedBrowser, "PluginBindingAttached", null, true, true);
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+ yield bindingPromise;
+
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+ ok(popupNotification, "Test 1, Should have a click-to-play notification");
+
+ // check plugin state
+ let pluginInfo = yield promiseForPluginInfo("test", gBrowser.selectedBrowser);
+ ok(!pluginInfo.activated, "plugin should not be activated");
+
+ // Display a context menu on the test plugin so we can test
+ // activation menu options.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ let bounds = plugin.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("contextmenu", left, top, 2, 1, 0);
+ });
+
+ popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+ ok(popupNotification, "Should have a click-to-play notification");
+ ok(popupNotification.dismissed, "notification should be dismissed");
+
+ // fixes a occasional test timeout on win7 opt
+ yield promiseForCondition(() => document.getElementById("context-ctp-play"));
+
+ let actMenuItem = document.getElementById("context-ctp-play");
+ ok(actMenuItem, "Should have a context menu entry for activating the plugin");
+
+ // Activate the plugin via the context menu
+ EventUtils.synthesizeMouseAtCenter(actMenuItem, {});
+
+ yield promiseForCondition(() => !PopupNotifications.panel.dismissed && PopupNotifications.panel.firstChild);
+
+ // Activate the plugin
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ // check plugin state
+ pluginInfo = yield promiseForPluginInfo("test", gBrowser.selectedBrowser);
+ ok(pluginInfo.activated, "plugin should not be activated");
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_crashreporting.js b/browser/base/content/test/plugins/browser_CTP_crashreporting.js
new file mode 100644
index 000000000..bb52d5704
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_crashreporting.js
@@ -0,0 +1,233 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
+const PLUGIN_PAGE = gTestRoot + "plugin_big.html";
+const PLUGIN_SMALL_PAGE = gTestRoot + "plugin_small.html";
+
+/**
+ * Takes an nsIPropertyBag and converts it into a JavaScript Object. It
+ * will also convert any nsIPropertyBag's within the nsIPropertyBag
+ * recursively.
+ *
+ * @param aBag
+ * The nsIPropertyBag to convert.
+ * @return Object
+ * Keyed on the names of the nsIProperty's within the nsIPropertyBag,
+ * and mapping to their values.
+ */
+function convertPropertyBag(aBag) {
+ let result = {};
+ let enumerator = aBag.enumerator;
+ while (enumerator.hasMoreElements()) {
+ let { name, value } = enumerator.getNext().QueryInterface(Ci.nsIProperty);
+ if (value instanceof Ci.nsIPropertyBag) {
+ value = convertPropertyBag(value);
+ }
+ result[name] = value;
+ }
+ return result;
+}
+
+add_task(function* setup() {
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ // The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables plugin
+ // crash reports. This test needs them enabled. The test also needs a mock
+ // report server, and fortunately one is already set up by toolkit/
+ // crashreporter/test/Makefile.in. Assign its URL to MOZ_CRASHREPORTER_URL,
+ // which CrashSubmit.jsm uses as a server override.
+ let env = Cc["@mozilla.org/process/environment;1"].
+ getService(Components.interfaces.nsIEnvironment);
+ let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
+ let serverURL = env.get("MOZ_CRASHREPORTER_URL");
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
+ env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ registerCleanupFunction(function cleanUp() {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
+ env.set("MOZ_CRASHREPORTER_URL", serverURL);
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ window.focus();
+ });
+});
+
+/**
+ * Test that plugin crash submissions still work properly after
+ * click-to-play activation.
+ */
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PLUGIN_PAGE,
+ }, function* (browser) {
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(browser);
+
+ let pluginInfo = yield promiseForPluginInfo("test", browser);
+ ok(!pluginInfo.activated, "Plugin should not be activated");
+
+ // Simulate clicking the "Allow Always" button.
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", browser);
+ yield promiseForNotificationShown(notification, browser);
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ // Prepare a crash report topic observer that only returns when
+ // the crash report has been successfully sent.
+ let crashReportChecker = (subject, data) => {
+ return (data == "success");
+ };
+ let crashReportPromise = TestUtils.topicObserved("crash-report-status",
+ crashReportChecker);
+
+ yield ContentTask.spawn(browser, null, function*() {
+ let plugin = content.document.getElementById("test");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return plugin.activated;
+ }, "Waited too long for plugin to activate.");
+
+ try {
+ Components.utils.waiveXrays(plugin).crash();
+ } catch (e) {
+ }
+
+ let doc = plugin.ownerDocument;
+
+ let getUI = (anonid) => {
+ return doc.getAnonymousElementByAttribute(plugin, "anonid", anonid);
+ };
+
+ // Now wait until the plugin crash report UI shows itself, which is
+ // asynchronous.
+ let statusDiv;
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ statusDiv = getUI("submitStatus");
+ return statusDiv.getAttribute("status") == "please";
+ }, "Waited too long for plugin to show crash report UI");
+
+ // Make sure the UI matches our expectations...
+ let style = content.getComputedStyle(getUI("pleaseSubmit"));
+ if (style.display != "block") {
+ throw new Error(`Submission UI visibility is not correct. ` +
+ `Expected block style, got ${style.display}.`);
+ }
+
+ // Fill the crash report in with some test values that we'll test for in
+ // the parent.
+ getUI("submitComment").value = "a test comment";
+ let optIn = getUI("submitURLOptIn");
+ if (!optIn.checked) {
+ throw new Error("URL opt-in should default to true.");
+ }
+
+ // Submit the report.
+ optIn.click();
+ getUI("submitButton").click();
+
+ // And wait for the parent to say that the crash report was submitted
+ // successfully.
+ yield ContentTaskUtils.waitForCondition(() => {
+ return statusDiv.getAttribute("status") == "success";
+ }, "Timed out waiting for plugin binding to be in success state");
+ });
+
+ let [subject, ] = yield crashReportPromise;
+
+ ok(subject instanceof Ci.nsIPropertyBag,
+ "The crash report subject should be an nsIPropertyBag.");
+
+ let crashData = convertPropertyBag(subject);
+ ok(crashData.serverCrashID, "Should have a serverCrashID set.");
+
+ // Remove the submitted report file after ensuring it exists.
+ let file = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
+ file.initWithPath(Services.crashmanager._submittedDumpsDir);
+ file.append(crashData.serverCrashID + ".txt");
+ ok(file.exists(), "Submitted report file should exist");
+ file.remove(false);
+
+ ok(crashData.extra, "Extra data should exist");
+ is(crashData.extra.PluginUserComment, "a test comment",
+ "Comment in extra data should match comment in textbox");
+
+ is(crashData.extra.PluginContentURL, undefined,
+ "URL should be absent from extra data when opt-in not checked");
+ });
+});
+
+/**
+ * Test that plugin crash submissions still work properly after
+ * click-to-play with the notification bar.
+ */
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PLUGIN_SMALL_PAGE,
+ }, function* (browser) {
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(browser);
+
+ let pluginInfo = yield promiseForPluginInfo("test", browser);
+ ok(pluginInfo.activated, "Plugin should be activated from previous test");
+
+ // Prepare a crash report topic observer that only returns when
+ // the crash report has been successfully sent.
+ let crashReportChecker = (subject, data) => {
+ return (data == "success");
+ };
+ let crashReportPromise = TestUtils.topicObserved("crash-report-status",
+ crashReportChecker);
+
+ yield ContentTask.spawn(browser, null, function*() {
+ let plugin = content.document.getElementById("test");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return plugin.activated;
+ }, "Waited too long for plugin to activate.");
+
+ try {
+ Components.utils.waiveXrays(plugin).crash();
+ } catch (e) {}
+ });
+
+ // Wait for the notification bar to be displayed.
+ let notification = yield waitForNotificationBar("plugin-crashed", browser);
+
+ // Then click the button to submit the crash report.
+ let buttons = notification.querySelectorAll(".notification-button");
+ is(buttons.length, 2, "Should have two buttons.");
+
+ // The "Submit Crash Report" button should be the second one.
+ let submitButton = buttons[1];
+ submitButton.click();
+
+ let [subject, ] = yield crashReportPromise;
+
+ ok(subject instanceof Ci.nsIPropertyBag,
+ "The crash report subject should be an nsIPropertyBag.");
+
+ let crashData = convertPropertyBag(subject);
+ ok(crashData.serverCrashID, "Should have a serverCrashID set.");
+
+ // Remove the submitted report file after ensuring it exists.
+ let file = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
+ file.initWithPath(Services.crashmanager._submittedDumpsDir);
+ file.append(crashData.serverCrashID + ".txt");
+ ok(file.exists(), "Submitted report file should exist");
+ file.remove(false);
+
+ is(crashData.extra.PluginContentURL, undefined,
+ "URL should be absent from extra data when opt-in not checked");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_data_urls.js b/browser/base/content/test/plugins/browser_CTP_data_urls.js
new file mode 100644
index 000000000..0f4747b1e
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_data_urls.js
@@ -0,0 +1,255 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+var gTestBrowser = null;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
+});
+
+// Test that the click-to-play doorhanger still works when navigating to data URLs
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_data_url.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(popupNotification, "Test 1a, Should have a click-to-play notification");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 1a, plugin should not be activated");
+
+ let loadPromise = promiseTabLoadEvent(gBrowser.selectedTab);
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ // navigate forward to a page with 'test' in it
+ content.document.getElementById("data-link-1").click();
+ });
+ yield loadPromise;
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(popupNotification, "Test 1b, Should have a click-to-play notification");
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 1b, plugin should not be activated");
+
+ let promise = promisePopupNotification("click-to-play-plugins");
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ let bounds = plugin.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+ yield promise;
+
+ // Simulate clicking the "Allow Always" button.
+ let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed &&
+ PopupNotifications.panel.firstChild;
+ yield promiseForCondition(condition);
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ // check plugin state
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 1b, plugin should be activated");
+});
+
+// Test that the click-to-play notification doesn't break when navigating
+// to data URLs with multiple plugins.
+add_task(function* () {
+ // We click activated above
+ clearAllPluginPermissions();
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_data_url.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 2a, Should have a click-to-play notification");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 2a, plugin should not be activated");
+
+ let loadPromise = promiseTabLoadEvent(gBrowser.selectedTab);
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ // navigate forward to a page with 'test1' & 'test2' in it
+ content.document.getElementById("data-link-2").click();
+ });
+ yield loadPromise;
+
+ // Work around for delayed PluginBindingAttached
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ content.document.getElementById("test1").clientTop;
+ content.document.getElementById("test2").clientTop;
+ });
+
+ pluginInfo = yield promiseForPluginInfo("test1");
+ ok(!pluginInfo.activated, "Test 2a, test1 should not be activated");
+ pluginInfo = yield promiseForPluginInfo("test2");
+ ok(!pluginInfo.activated, "Test 2a, test2 should not be activated");
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 2b, Should have a click-to-play notification");
+
+ yield promiseForNotificationShown(notification);
+
+ // Simulate choosing "Allow now" for the test plugin
+ is(notification.options.pluginData.size, 2, "Test 2b, Should have two types of plugin in the notification");
+
+ let centerAction = null;
+ for (let action of notification.options.pluginData.values()) {
+ if (action.pluginName == "Test") {
+ centerAction = action;
+ break;
+ }
+ }
+ ok(centerAction, "Test 2b, found center action for the Test plugin");
+
+ let centerItem = null;
+ for (let item of PopupNotifications.panel.firstChild.childNodes) {
+ is(item.value, "block", "Test 2b, all plugins should start out blocked");
+ if (item.action == centerAction) {
+ centerItem = item;
+ break;
+ }
+ }
+ ok(centerItem, "Test 2b, found center item for the Test plugin");
+
+ // "click" the button to activate the Test plugin
+ centerItem.value = "allownow";
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // check plugin state
+ pluginInfo = yield promiseForPluginInfo("test1");
+ ok(pluginInfo.activated, "Test 2b, plugin should be activated");
+});
+
+add_task(function* () {
+ // We click activated above
+ clearAllPluginPermissions();
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_data_url.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+});
+
+// Test that when navigating to a data url, the plugin permission is inherited
+add_task(function* () {
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 3a, Should have a click-to-play notification");
+
+ // check plugin state
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 3a, plugin should not be activated");
+
+ let promise = promisePopupNotification("click-to-play-plugins");
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ let bounds = plugin.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+ yield promise;
+
+ // Simulate clicking the "Allow Always" button.
+ let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed &&
+ PopupNotifications.panel.firstChild;
+ yield promiseForCondition(condition);
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ // check plugin state
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 3a, plugin should be activated");
+
+ let loadPromise = promiseTabLoadEvent(gBrowser.selectedTab);
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ // navigate forward to a page with 'test' in it
+ content.document.getElementById("data-link-1").click();
+ });
+ yield loadPromise;
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // check plugin state
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 3b, plugin should be activated");
+
+ clearAllPluginPermissions();
+});
+
+// Test that the click-to-play doorhanger still works
+// when directly navigating to data URLs.
+// Fails, bug XXX. Plugins plus a data url don't fire a load event.
+/*
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab,
+ "data:text/html,Hi!<embed id='test' style='width:200px; height:200px' type='application/x-test'/>");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 4a, Should have a click-to-play notification");
+
+ // check plugin state
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 4a, plugin should not be activated");
+
+ let promise = promisePopupNotification("click-to-play-plugins");
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ let bounds = plugin.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+ yield promise;
+
+ // Simulate clicking the "Allow Always" button.
+ let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed &&
+ PopupNotifications.panel.firstChild;
+ yield promiseForCondition(condition);
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ // check plugin state
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 4a, plugin should be activated");
+});
+*/
diff --git a/browser/base/content/test/plugins/browser_CTP_drag_drop.js b/browser/base/content/test/plugins/browser_CTP_drag_drop.js
new file mode 100644
index 000000000..7c9858e27
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_drag_drop.js
@@ -0,0 +1,96 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gNewWindow = null;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gNewWindow.close();
+ gNewWindow = null;
+ window.focus();
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+});
+
+add_task(function* () {
+ gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+
+ // XXX technically can't load fire before we get this call???
+ yield waitForEvent(gNewWindow, "load", null, true);
+
+ yield promisePopupNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser);
+
+ ok(PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser), "Should have a click-to-play notification in the tab in the new window");
+ ok(!PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should not have a click-to-play notification in the old window now");
+});
+
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, gNewWindow.gBrowser.selectedTab);
+
+ yield promisePopupNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+
+ ok(PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should have a click-to-play notification in the initial tab again");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+});
+
+add_task(function* () {
+ yield promisePopupNotification("click-to-play-plugins");
+
+ gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+
+ yield promiseWaitForFocus(gNewWindow);
+
+ yield promisePopupNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser);
+});
+
+add_task(function* () {
+ ok(PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser), "Should have a click-to-play notification in the tab in the new window");
+ ok(!PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should not have a click-to-play notification in the old window now");
+
+ let pluginInfo = yield promiseForPluginInfo("test", gNewWindow.gBrowser.selectedBrowser);
+ ok(!pluginInfo.activated, "plugin should not be activated");
+
+ yield ContentTask.spawn(gNewWindow.gBrowser.selectedBrowser, {}, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let bounds = plugin.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+
+ let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser).dismissed && gNewWindow.PopupNotifications.panel.firstChild;
+ yield promiseForCondition(condition);
+});
+
+add_task(function* () {
+ // Click the activate button on doorhanger to make sure it works
+ gNewWindow.PopupNotifications.panel.firstChild._primaryButton.click();
+
+ let pluginInfo = yield promiseForPluginInfo("test", gNewWindow.gBrowser.selectedBrowser);
+ ok(pluginInfo.activated, "plugin should be activated");
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_hide_overlay.js b/browser/base/content/test/plugins/browser_CTP_hide_overlay.js
new file mode 100644
index 000000000..5fab7f6ed
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_hide_overlay.js
@@ -0,0 +1,88 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+ // Tests that the overlay can be hidden for plugins using the close icon.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ let closeIcon = doc.getAnonymousElementByAttribute(plugin, "anonid", "closeIcon")
+ let bounds = closeIcon.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+
+ Assert.ok(!overlay.classList.contains("visible"), "overlay should be hidden.");
+ });
+});
+
+// Test that the overlay cannot be interacted with after the user closes the overlay
+add_task(function* () {
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ let closeIcon = doc.getAnonymousElementByAttribute(plugin, "anonid", "closeIcon")
+ let closeIconBounds = closeIcon.getBoundingClientRect();
+ let overlayBounds = overlay.getBoundingClientRect();
+ let overlayLeft = (overlayBounds.left + overlayBounds.right) / 2;
+ let overlayTop = (overlayBounds.left + overlayBounds.right) / 2 ;
+ let closeIconLeft = (closeIconBounds.left + closeIconBounds.right) / 2;
+ let closeIconTop = (closeIconBounds.top + closeIconBounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ // Simulate clicking on the close icon.
+ utils.sendMouseEvent("mousedown", closeIconLeft, closeIconTop, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", closeIconLeft, closeIconTop, 0, 1, 0, false, 0, 0);
+
+ // Simulate clicking on the overlay.
+ utils.sendMouseEvent("mousedown", overlayLeft, overlayTop, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", overlayLeft, overlayTop, 0, 1, 0, false, 0, 0);
+
+ Assert.ok(overlay.hasAttribute("dismissed") && !overlay.classList.contains("visible"),
+ "Overlay should be hidden");
+ });
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins");
+
+ ok(notification.dismissed, "No notification should be shown");
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_iframe.js b/browser/base/content/test/plugins/browser_CTP_iframe.js
new file mode 100644
index 000000000..58565559f
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_iframe.js
@@ -0,0 +1,48 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_iframe.html");
+
+ // Tests that the overlays are visible and actionable if the plugin is in an iframe.
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let frame = content.document.getElementById("frame");
+ let doc = frame.contentDocument;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(plugin && overlay.classList.contains("visible"),
+ "Test 1, Plugin overlay should exist, not be hidden");
+
+ let closeIcon = doc.getAnonymousElementByAttribute(plugin, "anonid", "closeIcon");
+ let bounds = closeIcon.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = doc.defaultView.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ Assert.ok(!overlay.classList.contains("visible"),
+ "Test 1, Plugin overlay should exist, be hidden");
+ });
+});
+
diff --git a/browser/base/content/test/plugins/browser_CTP_multi_allow.js b/browser/base/content/test/plugins/browser_CTP_multi_allow.js
new file mode 100644
index 000000000..7bc6aaabf
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_multi_allow.js
@@ -0,0 +1,99 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_two_types.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+ // Test that the click-to-play doorhanger for multiple plugins shows the correct
+ // state when re-opening without reloads or navigation.
+
+ let pluginInfo = yield promiseForPluginInfo("test", gBrowser.selectedBrowser);
+ ok(!pluginInfo.activated, "plugin should be activated");
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+ ok(notification, "Test 1a, Should have a click-to-play notification");
+
+ yield promiseForNotificationShown(notification);
+
+ is(notification.options.pluginData.size, 2,
+ "Test 1a, Should have two types of plugin in the notification");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+ is(PopupNotifications.panel.firstChild.childNodes.length, 2, "have child nodes");
+
+ let pluginItem = null;
+ for (let item of PopupNotifications.panel.firstChild.childNodes) {
+ is(item.value, "block", "Test 1a, all plugins should start out blocked");
+ if (item.action.pluginName == "Test") {
+ pluginItem = item;
+ }
+ }
+
+ // Choose "Allow now" for the test plugin
+ pluginItem.value = "allownow";
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test", gBrowser.selectedBrowser);
+ ok(pluginInfo.activated, "plugin should be activated");
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+ ok(notification, "Test 1b, Should have a click-to-play notification");
+
+ yield promiseForNotificationShown(notification);
+
+ pluginItem = null;
+ for (let item of PopupNotifications.panel.firstChild.childNodes) {
+ if (item.action.pluginName == "Test") {
+ is(item.value, "allownow", "Test 1b, Test plugin should now be set to 'Allow now'");
+ } else {
+ is(item.value, "block", "Test 1b, Second Test plugin should still be blocked");
+ pluginItem = item;
+ }
+ }
+
+ // Choose "Allow and remember" for the Second Test plugin
+ pluginItem.value = "allowalways";
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("secondtestA", gBrowser.selectedBrowser);
+ ok(pluginInfo.activated, "plugin should be activated");
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+ ok(notification, "Test 1c, Should have a click-to-play notification");
+
+ yield promiseForNotificationShown(notification);
+
+ for (let item of PopupNotifications.panel.firstChild.childNodes) {
+ if (item.action.pluginName == "Test") {
+ is(item.value, "allownow", "Test 1c, Test plugin should be set to 'Allow now'");
+ } else {
+ is(item.value, "allowalways", "Test 1c, Second Test plugin should be set to 'Allow always'");
+ }
+ }
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_nonplugins.js b/browser/base/content/test/plugins/browser_CTP_nonplugins.js
new file mode 100644
index 000000000..cdef44d9d
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_nonplugins.js
@@ -0,0 +1,58 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_DISABLED, "Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_two_types.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+ // Test that the click-to-play notification is not shown for non-plugin object elements
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+ ok(popupNotification, "Test 1, Should have a click-to-play notification");
+
+ let pluginRemovedPromise = waitForEvent(gBrowser.selectedBrowser, "PluginRemoved", null, true, true);
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let plugin = content.document.getElementById("secondtestA");
+ plugin.parentNode.removeChild(plugin);
+ plugin = content.document.getElementById("secondtestB");
+ plugin.parentNode.removeChild(plugin);
+
+ let image = content.document.createElement("object");
+ image.type = "image/png";
+ image.data = "moz.png";
+ content.document.body.appendChild(image);
+ });
+ yield pluginRemovedPromise;
+
+ popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+ ok(popupNotification, "Test 2, Should have a click-to-play notification");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ plugin.parentNode.removeChild(plugin);
+ });
+
+ popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser);
+ ok(popupNotification, "Test 3, Should still have a click-to-play notification");
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_notificationBar.js b/browser/base/content/test/plugins/browser_CTP_notificationBar.js
new file mode 100644
index 000000000..3c7bd911c
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_notificationBar.js
@@ -0,0 +1,151 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_small.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ // Expecting a notification bar for hidden plugins
+ yield promiseForNotificationBar("plugin-hidden", gTestBrowser);
+});
+
+add_task(function* () {
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_small.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+ yield promiseForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null);
+});
+
+add_task(function* () {
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_overlayed.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // Expecting a plugin notification bar when plugins are overlaid.
+ yield promiseForNotificationBar("plugin-hidden", gTestBrowser);
+});
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_overlayed.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.equal(plugin.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+ "Test 3b, plugin fallback type should be PLUGIN_CLICK_TO_PLAY");
+ });
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 1a, plugin should not be activated");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(!(overlay && overlay.classList.contains("visible")),
+ "Test 3b, overlay should be hidden.");
+ });
+});
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_positioned.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // Expecting a plugin notification bar when plugins are overlaid offscreen.
+ yield promisePopupNotification("click-to-play-plugins");
+ yield promiseForNotificationBar("plugin-hidden", gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.equal(plugin.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+ "Test 4b, plugin fallback type should be PLUGIN_CLICK_TO_PLAY");
+ });
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(!(overlay && overlay.classList.contains("visible")),
+ "Test 4b, overlay should be hidden.");
+ });
+});
+
+// Test that the notification bar is getting dismissed when directly activating plugins
+// via the doorhanger.
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_small.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // Expecting a plugin notification bar when plugins are overlaid offscreen.
+ yield promisePopupNotification("click-to-play-plugins");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.equal(plugin.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+ "Test 6, Plugin should be click-to-play");
+ });
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 6, Should have a click-to-play notification");
+
+ // simulate "always allow"
+ yield promiseForNotificationShown(notification);
+
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+ yield promiseForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null);
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 7, plugin should be activated");
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js b/browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js
new file mode 100644
index 000000000..ccb4d11d7
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_outsideScrollArea.js
@@ -0,0 +1,120 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(!popupNotification, "Test 1, Should not have a click-to-play notification");
+});
+
+// Test that the click-to-play overlay is not hidden for elements
+// partially or fully outside the viewport.
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_outsideScrollArea.html");
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let doc = content.document;
+ let p = doc.createElement('embed');
+
+ p.setAttribute('id', 'test');
+ p.setAttribute('type', 'application/x-test');
+ p.style.left = "0";
+ p.style.bottom = "200px";
+
+ doc.getElementById('container').appendChild(p);
+ });
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ let doc = content.document;
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(overlay && overlay.classList.contains("visible"),
+ "Test 2, overlay should be visible.");
+ });
+});
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_outsideScrollArea.html");
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let doc = content.document;
+ let p = doc.createElement('embed');
+
+ p.setAttribute('id', 'test');
+ p.setAttribute('type', 'application/x-test');
+ p.style.left = "0";
+ p.style.bottom = "-410px";
+
+ doc.getElementById('container').appendChild(p);
+ });
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let plugin = content.document.getElementById("test");
+ let doc = content.document;
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(overlay && overlay.classList.contains("visible"),
+ "Test 3, overlay should be visible.");
+ });
+});
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_outsideScrollArea.html");
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let doc = content.document;
+ let p = doc.createElement('embed');
+
+ p.setAttribute('id', 'test');
+ p.setAttribute('type', 'application/x-test');
+ p.style.left = "-600px";
+ p.style.bottom = "0";
+
+ doc.getElementById('container').appendChild(p);
+ });
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let plugin = content.document.getElementById("test");
+ let doc = content.document;
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(!(overlay && overlay.classList.contains("visible")),
+ "Test 4, overlay should be hidden.");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_remove_navigate.js b/browser/base/content/test/plugins/browser_CTP_remove_navigate.js
new file mode 100644
index 000000000..8ee1c5b5a
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_remove_navigate.js
@@ -0,0 +1,79 @@
+const gTestRoot = getRootDirectory(gTestPath);
+const gHttpTestRoot = gTestRoot.replace("chrome://mochitests/content/",
+ "http://127.0.0.1:8888/");
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ });
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
+});
+
+/**
+ * Tests that if a plugin is removed just as we transition to
+ * a different page, that we don't show the hidden plugin
+ * notification bar on the new page.
+ */
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ // Load up a page with a plugin...
+ let notificationPromise = waitForNotificationBar("plugin-hidden", gBrowser.selectedBrowser);
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gHttpTestRoot + "plugin_small.html");
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+ yield notificationPromise;
+
+ // Trigger the PluginRemoved event to be fired, and then immediately
+ // browse to a new page.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ plugin.remove();
+ });
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "about:mozilla");
+
+ // There should be no hidden plugin notification bar at about:mozilla.
+ let notificationBox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
+ is(notificationBox.getNotificationWithValue("plugin-hidden"), null,
+ "Expected no notification box");
+});
+
+/**
+ * Tests that if a plugin is removed just as we transition to
+ * a different page with a plugin, that we show the right notification
+ * for the new page.
+ */
+add_task(function* () {
+ // Load up a page with a plugin...
+ let notificationPromise = waitForNotificationBar("plugin-hidden", gBrowser.selectedBrowser);
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gHttpTestRoot + "plugin_small.html");
+ yield promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+ yield notificationPromise;
+
+ // Trigger the PluginRemoved event to be fired, and then immediately
+ // browse to a new page.
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ plugin.remove();
+ });
+});
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gHttpTestRoot + "plugin_small_2.html");
+ let notification = yield waitForNotificationBar("plugin-hidden", gBrowser.selectedBrowser);
+ ok(notification, "There should be a notification shown for the new page.");
+ // Ensure that the notification is showing information about
+ // the x-second-test plugin.
+ let label = notification.label;
+ ok(label.includes("Second Test"), "Should mention the second plugin");
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_resize.js b/browser/base/content/test/plugins/browser_CTP_resize.js
new file mode 100644
index 000000000..9b2a2cd82
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_resize.js
@@ -0,0 +1,130 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(!popupNotification, "Test 1, Should not have a click-to-play notification");
+
+ yield promiseTabLoadEvent(newTab, gTestRoot + "plugin_small.html"); // 10x10 plugin
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+});
+
+// Test that the overlay is hidden for "small" plugin elements and is shown
+// once they are resized to a size that can hold the overlay
+add_task(function* () {
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(popupNotification, "Test 2, Should have a click-to-play notification");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(!(overlay && overlay.classList.contains("visible")),
+ "Test 2, overlay should be hidden.");
+ });
+});
+
+add_task(function* () {
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ plugin.style.width = "300px";
+ });
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(!(overlay && overlay.classList.contains("visible")),
+ "Test 3, overlay should be hidden.");
+ });
+});
+
+
+add_task(function* () {
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ plugin.style.height = "300px";
+ });
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ content.document.getElementById("test").clientTop;
+ });
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(overlay && overlay.classList.contains("visible"),
+ "Test 4, overlay should be visible.");
+ });
+});
+
+add_task(function* () {
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ plugin.style.width = "10px";
+ plugin.style.height = "10px";
+ });
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ content.document.getElementById("test").clientTop;
+ });
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(!(overlay && overlay.classList.contains("visible")),
+ "Test 5, overlay should be hidden.");
+ });
+});
+
+add_task(function* () {
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ plugin.style.height = "300px";
+ plugin.style.width = "300px";
+ });
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ content.document.getElementById("test").clientTop;
+ });
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(overlay && overlay.classList.contains("visible"),
+ "Test 6, overlay should be visible.");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_CTP_zoom.js b/browser/base/content/test/plugins/browser_CTP_zoom.js
new file mode 100644
index 000000000..8b353232d
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_CTP_zoom.js
@@ -0,0 +1,62 @@
+"use strict";
+
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+
+var gTestBrowser = null;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ FullZoom.reset(); // must be called before closing the tab we zoomed!
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(!popupNotification, "Test 1, Should not have a click-to-play notification");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_zoom.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+});
+
+// Enlarges the zoom level 4 times and tests that the overlay is
+// visible after each enlargement.
+add_task(function* () {
+ for (let count = 0; count < 4; count++) {
+
+ FullZoom.enlarge();
+
+ // Reload the page
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_zoom.html");
+ yield promiseUpdatePluginBindings(gTestBrowser);
+ yield ContentTask.spawn(gTestBrowser, { count }, function* (args) {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(overlay && overlay.classList.contains("visible"),
+ "Overlay should be visible for zoom change count " + args.count);
+ });
+ }
+});
+
+
diff --git a/browser/base/content/test/plugins/browser_blocking.js b/browser/base/content/test/plugins/browser_blocking.js
new file mode 100644
index 000000000..334ed9f2e
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_blocking.js
@@ -0,0 +1,349 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+function updateAllTestPlugins(aState) {
+ setTestPluginEnabledState(aState, "Test Plug-in");
+ setTestPluginEnabledState(aState, "Second Test Plug-in");
+}
+
+add_task(function* () {
+ registerCleanupFunction(Task.async(function*() {
+ clearAllPluginPermissions();
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_ENABLED);
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+ resetBlocklist();
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ }));
+});
+
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ // Prime the content process
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html>hi</html>");
+
+ // Make sure the blocklist service(s) are running
+ Components.classes["@mozilla.org/extensions/blocklist;1"]
+ .getService(Components.interfaces.nsIBlocklistService);
+ let exmsg = yield promiseInitContentBlocklistSvc(gBrowser.selectedBrowser);
+ ok(!exmsg, "exception: " + exmsg);
+});
+
+add_task(function* () {
+ // enable hard blocklisting for the next test
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginHard.xml", gTestBrowser);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins");
+ ok(notification.dismissed, "Test 5: The plugin notification should be dismissed by default");
+
+ yield promiseForNotificationShown(notification);
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED, "Test 5, plugin fallback type should be PLUGIN_BLOCKLISTED");
+
+ is(notification.options.pluginData.size, 1, "Test 5: Only the blocked plugin should be present in the notification");
+ ok(PopupNotifications.panel.firstChild._buttonContainer.hidden, "Part 5: The blocked plugins notification should not have any buttons visible.");
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+});
+
+// Tests a vulnerable, updatable plugin
+
+add_task(function* () {
+ // enable hard blocklisting of test
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginVulnerableUpdatable.xml", gTestBrowser);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE,
+ "Test 18a, plugin fallback type should be PLUGIN_VULNERABLE_UPDATABLE");
+ ok(!pluginInfo.activated, "Test 18a, Plugin should not be activated");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(overlay && overlay.classList.contains("visible"),
+ "Test 18a, Plugin overlay should exist, not be hidden");
+
+ let updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink");
+ Assert.ok(updateLink.style.visibility != "hidden",
+ "Test 18a, Plugin should have an update link");
+ });
+
+ let promise = waitForEvent(gBrowser.tabContainer, "TabOpen", null, true);
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink");
+ let bounds = updateLink.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+ yield promise;
+
+ promise = waitForEvent(gBrowser.tabContainer, "TabClose", null, true);
+ gBrowser.removeCurrentTab();
+ yield promise;
+});
+
+add_task(function* () {
+ // clicking the update link should not activate the plugin
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE,
+ "Test 18a, plugin fallback type should be PLUGIN_VULNERABLE_UPDATABLE");
+ ok(!pluginInfo.activated, "Test 18b, Plugin should not be activated");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(overlay && overlay.classList.contains("visible"),
+ "Test 18b, Plugin overlay should exist, not be hidden");
+ });
+});
+
+// Tests a vulnerable plugin with no update
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginVulnerableNoUpdate.xml", gTestBrowser);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 18c, Should have a click-to-play notification");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE,
+ "Test 18c, plugin fallback type should be PLUGIN_VULNERABLE_NO_UPDATE");
+ ok(!pluginInfo.activated, "Test 18c, Plugin should not be activated");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(overlay && overlay.classList.contains("visible"),
+ "Test 18c, Plugin overlay should exist, not be hidden");
+
+ let updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink");
+ Assert.ok(updateLink && updateLink.style.display != "block",
+ "Test 18c, Plugin should not have an update link");
+ });
+
+ // check that click "Always allow" works with blocked plugins
+ yield promiseForNotificationShown(notification);
+
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE,
+ "Test 18c, plugin fallback type should be PLUGIN_VULNERABLE_NO_UPDATE");
+ ok(pluginInfo.activated, "Test 18c, Plugin should be activated");
+ let enabledState = getTestPluginEnabledState();
+ ok(enabledState, "Test 18c, Plugin enabled state should be STATE_CLICKTOPLAY");
+});
+
+// continue testing "Always allow", make sure it sticks.
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 18d, Waited too long for plugin to activate");
+
+ clearAllPluginPermissions();
+});
+
+// clicking the in-content overlay of a vulnerable plugin should bring
+// up the notification and not directly activate the plugin
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 18f, Should have a click-to-play notification");
+ ok(notification.dismissed, "Test 18f, notification should start dismissed");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 18f, Waited too long for plugin to activate");
+
+ var oldEventCallback = notification.options.eventCallback;
+ let promise = promiseForCondition(() => oldEventCallback == null);
+ notification.options.eventCallback = function() {
+ if (oldEventCallback) {
+ oldEventCallback();
+ }
+ oldEventCallback = null;
+ };
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let bounds = plugin.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+ yield promise;
+
+ ok(notification, "Test 18g, Should have a click-to-play notification");
+ ok(!notification.dismissed, "Test 18g, notification should be open");
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 18g, Plugin should not be activated");
+});
+
+// Test that "always allow"-ing a plugin will not allow it when it becomes
+// blocklisted.
+add_task(function* () {
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 24a, Should have a click-to-play notification");
+
+ // Plugin should start as CTP
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+ "Test 24a, plugin fallback type should be PLUGIN_CLICK_TO_PLAY");
+ ok(!pluginInfo.activated, "Test 24a, Plugin should not be active.");
+
+ // simulate "always allow"
+ yield promiseForNotificationShown(notification);
+
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 24a, Plugin should be active.");
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginVulnerableUpdatable.xml", gTestBrowser);
+});
+
+// the plugin is now blocklisted, so it should not automatically load
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 24b, Should have a click-to-play notification");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE,
+ "Test 24b, plugin fallback type should be PLUGIN_VULNERABLE_UPDATABLE");
+ ok(!pluginInfo.activated, "Test 24b, Plugin should not be active.");
+
+ // simulate "always allow"
+ yield promiseForNotificationShown(notification);
+
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 24b, Plugin should be active.");
+
+ clearAllPluginPermissions();
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+});
+
+// Plugin sync removal test. Note this test produces a notification drop down since
+// the plugin we add has zero dims.
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_syncRemoved.html");
+
+ // Maybe there some better trick here, we need to wait for the page load, then
+ // wait for the js to execute in the page.
+ yield waitForMs(500);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins");
+ ok(notification, "Test 25: There should be a plugin notification even if the plugin was immediately removed");
+ ok(notification.dismissed, "Test 25: The notification should be dismissed by default");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html>hi</html>");
+});
+
+// Tests a page with a blocked plugin in it and make sure the infoURL property
+// the blocklist file gets used.
+add_task(function* () {
+ clearAllPluginPermissions();
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginInfoURL.xml", gTestBrowser);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins");
+
+ // Since the plugin notification is dismissed by default, reshow it.
+ yield promiseForNotificationShown(notification);
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED,
+ "Test 26, plugin fallback type should be PLUGIN_BLOCKLISTED");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let plugin = content.document.getElementById("test");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.ok(!objLoadingContent.activated, "Plugin should not be activated.");
+ });
+
+ const testUrl = "http://test.url.com/";
+
+ let firstPanelChild = PopupNotifications.panel.firstChild;
+ let infoLink = document.getAnonymousElementByAttribute(firstPanelChild, "anonid",
+ "click-to-play-plugins-notification-link");
+ is(infoLink.href, testUrl,
+ "Test 26, the notification URL needs to match the infoURL from the blocklist file.");
+});
+
diff --git a/browser/base/content/test/plugins/browser_blocklist_content.js b/browser/base/content/test/plugins/browser_blocklist_content.js
new file mode 100644
index 000000000..bf4e159bc
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_blocklist_content.js
@@ -0,0 +1,104 @@
+var gTestBrowser = null;
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gChromeRoot = getRootDirectory(gTestPath);
+
+add_task(function* () {
+ registerCleanupFunction(Task.async(function*() {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+ resetBlocklist();
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ }));
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+
+ // Prime the blocklist service, the remote service doesn't launch on startup.
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html></html>");
+ let exmsg = yield promiseInitContentBlocklistSvc(gBrowser.selectedBrowser);
+ ok(!exmsg, "exception: " + exmsg);
+});
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let test = content.document.getElementById("test");
+ Assert.ok(test.activated, "task 1a: test plugin should be activated!");
+ });
+});
+
+// Load a fresh page, load a new plugin blocklist, then load the same page again.
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html>GO!</html>");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginHard.xml", gTestBrowser);
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let test = content.document.getElementById("test");
+ ok(!test.activated, "task 2a: test plugin shouldn't activate!");
+ });
+});
+
+// Unload the block list and lets do this again, only this time lets
+// hack around in the content blocklist service maliciously.
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html>GO!</html>");
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+
+ // Hack the planet! Load our blocklist shim, so we can mess with blocklist
+ // return results in the content process. Active until we close our tab.
+ let mm = gTestBrowser.messageManager;
+ info("test 3a: loading " + gChromeRoot + "blocklist_proxy.js" + "\n");
+ mm.loadFrameScript(gChromeRoot + "blocklist_proxy.js", true);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let test = content.document.getElementById("test");
+ Assert.ok(test.activated, "task 3a: test plugin should be activated!");
+ });
+});
+
+// Load a fresh page, load a new plugin blocklist, then load the same page again.
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html>GO!</html>");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginHard.xml", gTestBrowser);
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let test = content.document.getElementById("test");
+ Assert.ok(!test.activated, "task 4a: test plugin shouldn't activate!");
+ });
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+});
diff --git a/browser/base/content/test/plugins/browser_bug743421.js b/browser/base/content/test/plugins/browser_bug743421.js
new file mode 100644
index 000000000..966e7b012
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_bug743421.js
@@ -0,0 +1,119 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+
+add_task(function* () {
+ registerCleanupFunction(Task.async(function*() {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+ resetBlocklist();
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ }));
+});
+
+add_task(function* () {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
+
+ // Prime the blocklist service, the remote service doesn't launch on startup.
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html></html>");
+
+ let exmsg = yield promiseInitContentBlocklistSvc(gBrowser.selectedBrowser);
+ ok(!exmsg, "exception: " + exmsg);
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+});
+
+// Tests that navigation within the page and the window.history API doesn't break click-to-play state.
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_add_dynamically.html");
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(!notification, "Test 1a, Should not have a click-to-play notification");
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ new XPCNativeWrapper(XPCNativeWrapper.unwrap(content).addPlugin());
+ });
+
+ yield promisePopupNotification("click-to-play-plugins");
+});
+
+add_task(function* () {
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementsByTagName("embed")[0];
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.ok(!objLoadingContent.activated, "Test 1b, Plugin should not be activated");
+ });
+
+ // Click the activate button on doorhanger to make sure it works
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+
+ yield promiseForNotificationShown(notification);
+
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementsByTagName("embed")[0];
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.ok(objLoadingContent.activated, "Test 1b, Plugin should be activated");
+ });
+});
+
+add_task(function* () {
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 1c, Should still have a click-to-play notification");
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ new XPCNativeWrapper(XPCNativeWrapper.unwrap(content).addPlugin());
+ let plugin = content.document.getElementsByTagName("embed")[1];
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.ok(objLoadingContent.activated,
+ "Test 1c, Newly inserted plugin in activated page should be activated");
+ });
+});
+
+add_task(function* () {
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementsByTagName("embed")[1];
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.ok(objLoadingContent.activated, "Test 1d, Plugin should be activated");
+
+ let promise = ContentTaskUtils.waitForEvent(content, "hashchange");
+ content.location += "#anchorNavigation";
+ yield promise;
+ });
+});
+
+add_task(function* () {
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ new XPCNativeWrapper(XPCNativeWrapper.unwrap(content).addPlugin());
+ let plugin = content.document.getElementsByTagName("embed")[2];
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.ok(objLoadingContent.activated, "Test 1e, Plugin should be activated");
+ });
+});
+
+add_task(function* () {
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementsByTagName("embed")[2];
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.ok(objLoadingContent.activated, "Test 1f, Plugin should be activated");
+
+ content.history.replaceState({}, "", "replacedState");
+ new XPCNativeWrapper(XPCNativeWrapper.unwrap(content).addPlugin());
+ plugin = content.document.getElementsByTagName("embed")[3];
+ objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ Assert.ok(objLoadingContent.activated, "Test 1g, Plugin should be activated");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_bug744745.js b/browser/base/content/test/plugins/browser_bug744745.js
new file mode 100644
index 000000000..c9f552a4e
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_bug744745.js
@@ -0,0 +1,50 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+var gNumPluginBindingsAttached = 0;
+
+function pluginBindingAttached() {
+ gNumPluginBindingsAttached++;
+ if (gNumPluginBindingsAttached != 1) {
+ ok(false, "if we've gotten here, something is quite wrong");
+ }
+}
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ gTestBrowser.removeEventListener("PluginBindingAttached", pluginBindingAttached, true, true);
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ gTestBrowser.addEventListener("PluginBindingAttached", pluginBindingAttached, true, true);
+
+ let testRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+ yield promiseTabLoadEvent(gBrowser.selectedTab, testRoot + "plugin_bug744745.html");
+
+ yield promiseForCondition(function () { return gNumPluginBindingsAttached == 1; });
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("test");
+ if (!plugin) {
+ Assert.ok(false, "plugin element not available.");
+ return;
+ }
+ // We can't use MochiKit's routine
+ let style = content.getComputedStyle(plugin);
+ Assert.ok(("opacity" in style) && style.opacity == 1, "plugin style properly configured.");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_bug787619.js b/browser/base/content/test/plugins/browser_bug787619.js
new file mode 100644
index 000000000..bfd52258c
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_bug787619.js
@@ -0,0 +1,65 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+var gWrapperClickCount = 0;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ let testRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+ yield promiseTabLoadEvent(gBrowser.selectedTab, testRoot + "plugin_bug787619.html");
+
+ // Due to layout being async, "PluginBindAttached" may trigger later.
+ // This forces a layout flush, thus triggering it, and schedules the
+ // test so it is definitely executed afterwards.
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // check plugin state
+ let pluginInfo = yield promiseForPluginInfo("plugin");
+ ok(!pluginInfo.activated, "1a plugin should not be activated");
+
+ // click the overlay to prompt
+ let promise = promisePopupNotification("click-to-play-plugins");
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ let plugin = content.document.getElementById("plugin");
+ let bounds = plugin.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+ yield promise;
+
+ // check plugin state
+ pluginInfo = yield promiseForPluginInfo("plugin");
+ ok(!pluginInfo.activated, "1b plugin should not be activated");
+
+ let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed &&
+ PopupNotifications.panel.firstChild;
+ yield promiseForCondition(condition);
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ // check plugin state
+ pluginInfo = yield promiseForPluginInfo("plugin");
+ ok(pluginInfo.activated, "plugin should be activated");
+
+ is(gWrapperClickCount, 0, 'wrapper should not have received any clicks');
+});
diff --git a/browser/base/content/test/plugins/browser_bug797677.js b/browser/base/content/test/plugins/browser_bug797677.js
new file mode 100644
index 000000000..1ae9f5047
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_bug797677.js
@@ -0,0 +1,43 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+var gConsoleErrors = 0;
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ consoleService.unregisterListener(errorListener);
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ let consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+ let errorListener = {
+ observe: function(aMessage) {
+ if (aMessage.message.includes("NS_ERROR_FAILURE"))
+ gConsoleErrors++;
+ }
+ };
+ consoleService.registerListener(errorListener);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_bug797677.html");
+
+ let pluginInfo = yield promiseForPluginInfo("plugin");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED, "plugin should not have been found.");
+
+ // simple cpows
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ let plugin = content.document.getElementById("plugin");
+ ok(plugin, "plugin should be in the page");
+ });
+ is(gConsoleErrors, 0, "should have no console errors");
+});
diff --git a/browser/base/content/test/plugins/browser_bug812562.js b/browser/base/content/test/plugins/browser_bug812562.js
new file mode 100644
index 000000000..be7b00b22
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_bug812562.js
@@ -0,0 +1,80 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+
+add_task(function* () {
+ registerCleanupFunction(Task.async(function*() {
+ clearAllPluginPermissions();
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+ resetBlocklist();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ // Prime the blocklist service, the remote service doesn't launch on startup.
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html></html>");
+ let exmsg = yield promiseInitContentBlocklistSvc(gBrowser.selectedBrowser);
+ ok(!exmsg, "exception: " + exmsg);
+});
+
+// Tests that the going back will reshow the notification for click-to-play
+// blocklisted plugins
+add_task(function* () {
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginVulnerableUpdatable.xml", gTestBrowser);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(popupNotification, "test part 1: Should have a click-to-play notification");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "plugin should be marked as VULNERABLE");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ Assert.ok(!!content.document.getElementById("test"),
+ "test part 1: plugin should not be activated");
+ });
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html></html>");
+});
+
+add_task(function* () {
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(!popupNotification, "test part 2: Should not have a click-to-play notification");
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ Assert.ok(!content.document.getElementById("test"),
+ "test part 2: plugin should not be activated");
+ });
+
+ let obsPromise = TestUtils.topicObserved("PopupNotifications-updateNotShowing");
+ let overlayPromise = promisePopupNotification("click-to-play-plugins");
+ gTestBrowser.goBack();
+ yield obsPromise;
+ yield overlayPromise;
+});
+
+add_task(function* () {
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(popupNotification, "test part 3: Should have a click-to-play notification");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "plugin should be marked as VULNERABLE");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ Assert.ok(!!content.document.getElementById("test"),
+ "test part 3: plugin should not be activated");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_bug818118.js b/browser/base/content/test/plugins/browser_bug818118.js
new file mode 100644
index 000000000..9dd6e22e7
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_bug818118.js
@@ -0,0 +1,40 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_both.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(popupNotification, "should have a click-to-play notification");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "plugin should be click to play");
+ ok(!pluginInfo.activated, "plugin should not be activated");
+
+ yield ContentTask.spawn(gTestBrowser, null, () => {
+ let unknown = content.document.getElementById("unknown");
+ ok(unknown, "should have unknown plugin in page");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_bug820497.js b/browser/base/content/test/plugins/browser_bug820497.js
new file mode 100644
index 000000000..b2e0f5268
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_bug820497.js
@@ -0,0 +1,71 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+var gNumPluginBindingsAttached = 0;
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
+
+ gTestBrowser.addEventListener("PluginBindingAttached", function () { gNumPluginBindingsAttached++ }, true, true);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_bug820497.html");
+
+ yield promiseForCondition(function () { return gNumPluginBindingsAttached == 1; });
+
+ yield ContentTask.spawn(gTestBrowser, null, () => {
+ // Note we add the second plugin in the code farther down, so there's
+ // no way we got here with anything but one plugin loaded.
+ let doc = content.document;
+ let testplugin = doc.getElementById("test");
+ ok(testplugin, "should have test plugin");
+ let secondtestplugin = doc.getElementById("secondtest");
+ ok(!secondtestplugin, "should not yet have second test plugin");
+ });
+
+ yield promisePopupNotification("click-to-play-plugins");
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "should have a click-to-play notification");
+
+ yield promiseForNotificationShown(notification);
+
+ is(notification.options.pluginData.size, 1, "should be 1 type of plugin in the popup notification");
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ XPCNativeWrapper.unwrap(content).addSecondPlugin();
+ });
+
+ yield promiseForCondition(function () { return gNumPluginBindingsAttached == 2; });
+
+ yield ContentTask.spawn(gTestBrowser, null, () => {
+ let doc = content.document;
+ let testplugin = doc.getElementById("test");
+ ok(testplugin, "should have test plugin");
+ let secondtestplugin = doc.getElementById("secondtest");
+ ok(secondtestplugin, "should have second test plugin");
+ });
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+
+ ok(notification, "should have popup notification");
+
+ yield promiseForNotificationShown(notification);
+
+ is(notification.options.pluginData.size, 2, "aited too long for 2 types of plugins in popup notification");
+});
diff --git a/browser/base/content/test/plugins/browser_clearplugindata.html b/browser/base/content/test/plugins/browser_clearplugindata.html
new file mode 100644
index 000000000..243350ba4
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_clearplugindata.html
@@ -0,0 +1,30 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <title>Plugin Clear Site Data sanitize test</title>
+
+ <embed id="plugin1" type="application/x-test" width="200" height="200"></embed>
+
+ <script type="application/javascript">
+ function testSteps()
+ {
+ // Make sure clearing by timerange is supported.
+ var p = document.getElementById("plugin1");
+ p.setSitesWithDataCapabilities(true);
+
+ p.setSitesWithData(
+ "foo.com:0:5," +
+ "bar.com:0:100," +
+ "baz.com:1:5," +
+ "qux.com:1:100"
+ );
+ }
+ </script>
+ </head>
+
+ <body onload="testSteps();"></body>
+
+</html>
diff --git a/browser/base/content/test/plugins/browser_clearplugindata.js b/browser/base/content/test/plugins/browser_clearplugindata.js
new file mode 100644
index 000000000..69d474fed
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_clearplugindata.js
@@ -0,0 +1,127 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+var gTestBrowser = null;
+
+// Test clearing plugin data using sanitize.js.
+const testURL1 = gTestRoot + "browser_clearplugindata.html";
+const testURL2 = gTestRoot + "browser_clearplugindata_noage.html";
+
+var tempScope = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+var Sanitizer = tempScope.Sanitizer;
+
+const pluginHostIface = Ci.nsIPluginHost;
+var pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+pluginHost.QueryInterface(pluginHostIface);
+
+var pluginTag = getTestPlugin();
+var sanitizer = null;
+
+function stored(needles) {
+ let something = pluginHost.siteHasData(this.pluginTag, null);
+ if (!needles)
+ return something;
+
+ if (!something)
+ return false;
+
+ for (let i = 0; i < needles.length; ++i) {
+ if (!pluginHost.siteHasData(this.pluginTag, needles[i]))
+ return false;
+ }
+ return true;
+}
+
+add_task(function* () {
+ registerCleanupFunction(function () {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ if (gTestBrowser) {
+ gBrowser.removeCurrentTab();
+ }
+ window.focus();
+ gTestBrowser = null;
+ });
+});
+
+add_task(function* () {
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+
+ sanitizer = new Sanitizer();
+ sanitizer.ignoreTimespan = false;
+ sanitizer.prefDomain = "privacy.cpd.";
+ let itemPrefs = gPrefService.getBranch(sanitizer.prefDomain);
+ itemPrefs.setBoolPref("history", false);
+ itemPrefs.setBoolPref("downloads", false);
+ itemPrefs.setBoolPref("cache", false);
+ itemPrefs.setBoolPref("cookies", true); // plugin data
+ itemPrefs.setBoolPref("formdata", false);
+ itemPrefs.setBoolPref("offlineApps", false);
+ itemPrefs.setBoolPref("passwords", false);
+ itemPrefs.setBoolPref("sessions", false);
+ itemPrefs.setBoolPref("siteSettings", false);
+});
+
+add_task(function* () {
+ // Load page to set data for the plugin.
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, testURL1);
+
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
+ "Data stored for sites");
+
+ // Clear 20 seconds ago
+ let now_uSec = Date.now() * 1000;
+ sanitizer.range = [now_uSec - 20*1000000, now_uSec];
+ yield sanitizer.sanitize();
+
+ ok(stored(["bar.com", "qux.com"]), "Data stored for sites");
+ ok(!stored(["foo.com"]), "Data cleared for foo.com");
+ ok(!stored(["baz.com"]), "Data cleared for baz.com");
+
+ // Clear everything
+ sanitizer.range = null;
+ yield sanitizer.sanitize();
+
+ ok(!stored(null), "All data cleared");
+
+ gBrowser.removeCurrentTab();
+ gTestBrowser = null;
+});
+
+add_task(function* () {
+ // Load page to set data for the plugin.
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, testURL2);
+
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
+ "Data stored for sites");
+
+ // Attempt to clear 20 seconds ago. The plugin will throw
+ // NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED, which should result in us
+ // clearing all data regardless of age.
+ let now_uSec = Date.now() * 1000;
+ sanitizer.range = [now_uSec - 20*1000000, now_uSec];
+ yield sanitizer.sanitize();
+
+ ok(!stored(null), "All data cleared");
+
+ gBrowser.removeCurrentTab();
+ gTestBrowser = null;
+});
+
diff --git a/browser/base/content/test/plugins/browser_clearplugindata_noage.html b/browser/base/content/test/plugins/browser_clearplugindata_noage.html
new file mode 100644
index 000000000..820979541
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_clearplugindata_noage.html
@@ -0,0 +1,30 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <title>Plugin Clear Site Data sanitize test without age</title>
+
+ <embed id="plugin1" type="application/x-test" width="200" height="200"></embed>
+
+ <script type="application/javascript">
+ function testSteps()
+ {
+ // Make sure clearing by timerange is disabled.
+ var p = document.getElementById("plugin1");
+ p.setSitesWithDataCapabilities(false);
+
+ p.setSitesWithData(
+ "foo.com:0:5," +
+ "bar.com:0:100," +
+ "baz.com:1:5," +
+ "qux.com:1:100"
+ );
+ }
+ </script>
+ </head>
+
+ <body onload="testSteps();"></body>
+
+</html>
diff --git a/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js b/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
new file mode 100644
index 000000000..bdca32e70
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_globalplugin_crashinfobar.js
@@ -0,0 +1,34 @@
+/**
+ * Test that the notification bar for crashed GMPs works.
+ */
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "about:blank",
+ }, function* (browser) {
+ yield ContentTask.spawn(browser, null, function* () {
+ const GMP_CRASH_EVENT = {
+ pluginID: 1,
+ pluginName: "GlobalTestPlugin",
+ submittedCrashReport: false,
+ bubbles: true,
+ cancelable: true,
+ gmpPlugin: true,
+ };
+
+ let crashEvent = new content.PluginCrashedEvent("PluginCrashed",
+ GMP_CRASH_EVENT);
+ content.dispatchEvent(crashEvent);
+ });
+
+ let notification = yield waitForNotificationBar("plugin-crashed", browser);
+
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ ok(notification, "Infobar was shown.");
+ is(notification.priority, notificationBox.PRIORITY_WARNING_MEDIUM,
+ "Correct priority.");
+ is(notification.getAttribute("label"),
+ "The GlobalTestPlugin plugin has crashed.",
+ "Correct message.");
+ });
+});
diff --git a/browser/base/content/test/plugins/browser_pageInfo_plugins.js b/browser/base/content/test/plugins/browser_pageInfo_plugins.js
new file mode 100644
index 000000000..0d941e0fa
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_pageInfo_plugins.js
@@ -0,0 +1,191 @@
+var gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gPageInfo = null;
+var gNextTest = null;
+var gTestBrowser = null;
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"]
+ .getService(Components.interfaces.nsIPluginHost);
+var gPermissionManager = Components.classes["@mozilla.org/permissionmanager;1"]
+ .getService(Components.interfaces.nsIPermissionManager);
+var gTestPermissionString = gPluginHost.getPermissionStringForType("application/x-test");
+var gSecondTestPermissionString = gPluginHost.getPermissionStringForType("application/x-second-test");
+
+function doOnPageLoad(url, continuation) {
+ gNextTest = continuation;
+ gTestBrowser.addEventListener("load", pageLoad, true);
+ gTestBrowser.contentWindow.location = url;
+}
+
+function pageLoad() {
+ gTestBrowser.removeEventListener("load", pageLoad);
+ // The plugin events are async dispatched and can come after the load event
+ // This just allows the events to fire before we then go on to test the states
+ executeSoon(gNextTest);
+}
+
+function doOnOpenPageInfo(continuation) {
+ Services.obs.addObserver(pageInfoObserve, "page-info-dialog-loaded", false);
+ gNextTest = continuation;
+ // An explanation: it looks like the test harness complains about leaked
+ // windows if we don't keep a reference to every window we've opened.
+ // So, don't reuse pointers to opened Page Info windows - simply append
+ // to this list.
+ gPageInfo = BrowserPageInfo(null, "permTab");
+}
+
+function pageInfoObserve(win, topic, data) {
+ Services.obs.removeObserver(pageInfoObserve, "page-info-dialog-loaded");
+ gPageInfo.onFinished.push(() => executeSoon(gNextTest));
+}
+
+function finishTest() {
+ gPermissionManager.remove(makeURI("http://127.0.0.1:8888/"), gTestPermissionString);
+ gPermissionManager.remove(makeURI("http://127.0.0.1:8888/"), gSecondTestPermissionString);
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ gBrowser.removeCurrentTab();
+
+ gPageInfo = null;
+ gNextTest = null;
+ gTestBrowser = null;
+ gPluginHost = null;
+ gPermissionManager = null;
+
+ executeSoon(finish);
+}
+
+function test() {
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+ gPermissionManager.remove(makeURI("http://127.0.0.1:8888/"), gTestPermissionString);
+ gPermissionManager.remove(makeURI("http://127.0.0.1:8888/"), gSecondTestPermissionString);
+ doOnPageLoad(gHttpTestRoot + "plugin_two_types.html", testPart1a);
+}
+
+// The first test plugin is CtP and the second test plugin is enabled.
+function testPart1a() {
+ let test = gTestBrowser.contentDocument.getElementById("test");
+ let objLoadingContent = test.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(!objLoadingContent.activated, "part 1a: Test plugin should not be activated");
+ let secondtest = gTestBrowser.contentDocument.getElementById("secondtestA");
+ objLoadingContent = secondtest.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(objLoadingContent.activated, "part 1a: Second Test plugin should be activated");
+
+ doOnOpenPageInfo(testPart1b);
+}
+
+function testPart1b() {
+ let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup");
+ let testRadioDefault = gPageInfo.document.getElementById(gTestPermissionString + "#0");
+
+ is(testRadioGroup.selectedItem, testRadioDefault, "part 1b: Test radio group should be set to 'Default'");
+ let testRadioAllow = gPageInfo.document.getElementById(gTestPermissionString + "#1");
+ testRadioGroup.selectedItem = testRadioAllow;
+ testRadioAllow.doCommand();
+
+ let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup");
+ let secondtestRadioDefault = gPageInfo.document.getElementById(gSecondTestPermissionString + "#0");
+ is(secondtestRadioGroup.selectedItem, secondtestRadioDefault, "part 1b: Second Test radio group should be set to 'Default'");
+ let secondtestRadioAsk = gPageInfo.document.getElementById(gSecondTestPermissionString + "#3");
+ secondtestRadioGroup.selectedItem = secondtestRadioAsk;
+ secondtestRadioAsk.doCommand();
+
+ doOnPageLoad(gHttpTestRoot + "plugin_two_types.html", testPart2);
+}
+
+// Now, the Test plugin should be allowed, and the Test2 plugin should be CtP
+function testPart2() {
+ let test = gTestBrowser.contentDocument.getElementById("test").
+ QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(test.activated, "part 2: Test plugin should be activated");
+
+ let secondtest = gTestBrowser.contentDocument.getElementById("secondtestA").
+ QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(!secondtest.activated, "part 2: Second Test plugin should not be activated");
+ is(secondtest.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+ "part 2: Second test plugin should be click-to-play.");
+
+ let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup");
+ let testRadioAllow = gPageInfo.document.getElementById(gTestPermissionString + "#1");
+ is(testRadioGroup.selectedItem, testRadioAllow, "part 2: Test radio group should be set to 'Allow'");
+ let testRadioBlock = gPageInfo.document.getElementById(gTestPermissionString + "#2");
+ testRadioGroup.selectedItem = testRadioBlock;
+ testRadioBlock.doCommand();
+
+ let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup");
+ let secondtestRadioAsk = gPageInfo.document.getElementById(gSecondTestPermissionString + "#3");
+ is(secondtestRadioGroup.selectedItem, secondtestRadioAsk, "part 2: Second Test radio group should be set to 'Always Ask'");
+ let secondtestRadioBlock = gPageInfo.document.getElementById(gSecondTestPermissionString + "#2");
+ secondtestRadioGroup.selectedItem = secondtestRadioBlock;
+ secondtestRadioBlock.doCommand();
+
+ doOnPageLoad(gHttpTestRoot + "plugin_two_types.html", testPart3);
+}
+
+// Now, all the things should be blocked
+function testPart3() {
+ let test = gTestBrowser.contentDocument.getElementById("test").
+ QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(!test.activated, "part 3: Test plugin should not be activated");
+ is(test.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_DISABLED,
+ "part 3: Test plugin should be marked as PLUGIN_DISABLED");
+
+ let secondtest = gTestBrowser.contentDocument.getElementById("secondtestA").
+ QueryInterface(Ci.nsIObjectLoadingContent);
+
+ ok(!secondtest.activated, "part 3: Second Test plugin should not be activated");
+ is(secondtest.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_DISABLED,
+ "part 3: Second test plugin should be marked as PLUGIN_DISABLED");
+
+ // reset permissions
+ gPermissionManager.remove(makeURI("http://127.0.0.1:8888/"), gTestPermissionString);
+ gPermissionManager.remove(makeURI("http://127.0.0.1:8888/"), gSecondTestPermissionString);
+ // check that changing the permissions affects the radio state in the
+ // open Page Info window
+ let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup");
+ let testRadioDefault = gPageInfo.document.getElementById(gTestPermissionString + "#0");
+ is(testRadioGroup.selectedItem, testRadioDefault, "part 3: Test radio group should be set to 'Default'");
+ let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup");
+ let secondtestRadioDefault = gPageInfo.document.getElementById(gSecondTestPermissionString + "#0");
+ is(secondtestRadioGroup.selectedItem, secondtestRadioDefault, "part 3: Second Test radio group should be set to 'Default'");
+
+ doOnPageLoad(gHttpTestRoot + "plugin_two_types.html", testPart4a);
+}
+
+// Now test that setting permission directly (as from the popup notification)
+// immediately influences Page Info.
+function testPart4a() {
+ // simulate "allow" from the doorhanger
+ gPermissionManager.add(gTestBrowser.currentURI, gTestPermissionString, Ci.nsIPermissionManager.ALLOW_ACTION);
+ gPermissionManager.add(gTestBrowser.currentURI, gSecondTestPermissionString, Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ // check (again) that changing the permissions affects the radio state in the
+ // open Page Info window
+ let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup");
+ let testRadioAllow = gPageInfo.document.getElementById(gTestPermissionString + "#1");
+ is(testRadioGroup.selectedItem, testRadioAllow, "part 4a: Test radio group should be set to 'Allow'");
+ let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup");
+ let secondtestRadioAllow = gPageInfo.document.getElementById(gSecondTestPermissionString + "#1");
+ is(secondtestRadioGroup.selectedItem, secondtestRadioAllow, "part 4a: Second Test radio group should be set to 'Always Allow'");
+
+ // now close Page Info and see that it opens with the right settings
+ gPageInfo.close();
+ doOnOpenPageInfo(testPart4b);
+}
+
+// check that "always allow" resulted in the radio buttons getting set to allow
+function testPart4b() {
+ let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup");
+ let testRadioAllow = gPageInfo.document.getElementById(gTestPermissionString + "#1");
+ is(testRadioGroup.selectedItem, testRadioAllow, "part 4b: Test radio group should be set to 'Allow'");
+
+ let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup");
+ let secondtestRadioAllow = gPageInfo.document.getElementById(gSecondTestPermissionString + "#1");
+ is(secondtestRadioGroup.selectedItem, secondtestRadioAllow, "part 4b: Second Test radio group should be set to 'Allow'");
+
+ Services.prefs.setBoolPref("plugins.click_to_play", false);
+ gPageInfo.close();
+ finishTest();
+}
diff --git a/browser/base/content/test/plugins/browser_pluginCrashCommentAndURL.js b/browser/base/content/test/plugins/browser_pluginCrashCommentAndURL.js
new file mode 100644
index 000000000..ab4743f6f
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_pluginCrashCommentAndURL.js
@@ -0,0 +1,207 @@
+Cu.import("resource://gre/modules/CrashSubmit.jsm", this);
+
+const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
+
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gTestBrowser = null;
+var config = {};
+
+add_task(function* () {
+ // The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables plugin
+ // crash reports. This test needs them enabled. The test also needs a mock
+ // report server, and fortunately one is already set up by toolkit/
+ // crashreporter/test/Makefile.in. Assign its URL to MOZ_CRASHREPORTER_URL,
+ // which CrashSubmit.jsm uses as a server override.
+ let env = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
+ let serverUrl = env.get("MOZ_CRASHREPORTER_URL");
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
+ env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ // Crash immediately
+ Services.prefs.setIntPref("dom.ipc.plugins.timeoutSecs", 0);
+
+ registerCleanupFunction(Task.async(function*() {
+ Services.prefs.clearUserPref("dom.ipc.plugins.timeoutSecs");
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
+ env.set("MOZ_CRASHREPORTER_URL", serverUrl);
+ env = null;
+ config = null;
+ gTestBrowser = null;
+ gBrowser.removeCurrentTab();
+ window.focus();
+ }));
+});
+
+add_task(function* () {
+ config = {
+ shouldSubmissionUIBeVisible: true,
+ comment: "",
+ urlOptIn: false
+ };
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
+
+ let pluginCrashed = promisePluginCrashed();
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_crashCommentAndURL.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // Wait for the plugin to crash
+ yield pluginCrashed;
+
+ let crashReportStatus = TestUtils.topicObserved("crash-report-status", onSubmitStatus);
+
+ // Test that the crash submission UI is actually visible and submit the crash report.
+ yield ContentTask.spawn(gTestBrowser, config, function* (aConfig) {
+ let doc = content.document;
+ let plugin = doc.getElementById("plugin");
+ let pleaseSubmit = doc.getAnonymousElementByAttribute(plugin, "anonid", "pleaseSubmit");
+ let submitButton = doc.getAnonymousElementByAttribute(plugin, "anonid", "submitButton");
+ // Test that we don't send the URL when urlOptIn is false.
+ doc.getAnonymousElementByAttribute(plugin, "anonid", "submitURLOptIn").checked = aConfig.urlOptIn;
+ submitButton.click();
+ Assert.equal(content.getComputedStyle(pleaseSubmit).display == "block",
+ aConfig.shouldSubmissionUIBeVisible, "The crash UI should be visible");
+ });
+
+ yield crashReportStatus;
+});
+
+add_task(function* () {
+ config = {
+ shouldSubmissionUIBeVisible: true,
+ comment: "a test comment",
+ urlOptIn: true
+ };
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
+
+ let pluginCrashed = promisePluginCrashed();
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_crashCommentAndURL.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // Wait for the plugin to crash
+ yield pluginCrashed;
+
+ let crashReportStatus = TestUtils.topicObserved("crash-report-status", onSubmitStatus);
+
+ // Test that the crash submission UI is actually visible and submit the crash report.
+ yield ContentTask.spawn(gTestBrowser, config, function* (aConfig) {
+ let doc = content.document;
+ let plugin = doc.getElementById("plugin");
+ let pleaseSubmit = doc.getAnonymousElementByAttribute(plugin, "anonid", "pleaseSubmit");
+ let submitButton = doc.getAnonymousElementByAttribute(plugin, "anonid", "submitButton");
+ // Test that we send the URL when urlOptIn is true.
+ doc.getAnonymousElementByAttribute(plugin, "anonid", "submitURLOptIn").checked = aConfig.urlOptIn;
+ doc.getAnonymousElementByAttribute(plugin, "anonid", "submitComment").value = aConfig.comment;
+ submitButton.click();
+ Assert.equal(content.getComputedStyle(pleaseSubmit).display == "block",
+ aConfig.shouldSubmissionUIBeVisible, "The crash UI should be visible");
+ });
+
+ yield crashReportStatus;
+});
+
+add_task(function* () {
+ config = {
+ shouldSubmissionUIBeVisible: false,
+ comment: "",
+ urlOptIn: true
+ };
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
+
+ let pluginCrashed = promisePluginCrashed();
+
+ // Make sure that the plugin container is too small to display the crash submission UI
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_crashCommentAndURL.html?" +
+ encodeURIComponent(JSON.stringify({width: 300, height: 300})));
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ // Wait for the plugin to crash
+ yield pluginCrashed;
+
+ // Test that the crash submission UI is not visible and do not submit a crash report.
+ yield ContentTask.spawn(gTestBrowser, config, function* (aConfig) {
+ let doc = content.document;
+ let plugin = doc.getElementById("plugin");
+ let pleaseSubmit = doc.getAnonymousElementByAttribute(plugin, "anonid", "pleaseSubmit");
+ Assert.equal(!!pleaseSubmit && content.getComputedStyle(pleaseSubmit).display == "block",
+ aConfig.shouldSubmissionUIBeVisible, "Plugin crash UI should not be visible");
+ });
+});
+
+function promisePluginCrashed() {
+ return new ContentTask.spawn(gTestBrowser, {}, function* () {
+ yield new Promise((resolve) => {
+ addEventListener("PluginCrashReporterDisplayed", function onPluginCrashed() {
+ removeEventListener("PluginCrashReporterDisplayed", onPluginCrashed);
+ resolve();
+ });
+ });
+ })
+}
+
+function onSubmitStatus(aSubject, aData) {
+ // Wait for success or failed, doesn't matter which.
+ if (aData != "success" && aData != "failed")
+ return false;
+
+ let propBag = aSubject.QueryInterface(Ci.nsIPropertyBag);
+ if (aData == "success") {
+ let remoteID = getPropertyBagValue(propBag, "serverCrashID");
+ ok(!!remoteID, "serverCrashID should be set");
+
+ // Remove the submitted report file.
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(Services.crashmanager._submittedDumpsDir);
+ file.append(remoteID + ".txt");
+ ok(file.exists(), "Submitted report file should exist");
+ file.remove(false);
+ }
+
+ let extra = getPropertyBagValue(propBag, "extra");
+ ok(extra instanceof Ci.nsIPropertyBag, "Extra data should be property bag");
+
+ let val = getPropertyBagValue(extra, "PluginUserComment");
+ if (config.comment)
+ is(val, config.comment,
+ "Comment in extra data should match comment in textbox");
+ else
+ ok(val === undefined,
+ "Comment should be absent from extra data when textbox is empty");
+
+ val = getPropertyBagValue(extra, "PluginContentURL");
+ if (config.urlOptIn)
+ is(val, gBrowser.currentURI.spec,
+ "URL in extra data should match browser URL when opt-in checked");
+ else
+ ok(val === undefined,
+ "URL should be absent from extra data when opt-in not checked");
+
+ return true;
+}
+
+function getPropertyBagValue(bag, key) {
+ try {
+ var val = bag.getProperty(key);
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+ return val;
+}
diff --git a/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js b/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
new file mode 100644
index 000000000..42ef57314
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_pluginCrashReportNonDeterminism.js
@@ -0,0 +1,254 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * With e10s, plugins must run in their own process. This means we have
+ * three processes at a minimum when we're running a plugin:
+ *
+ * 1) The main browser, or "chrome" process
+ * 2) The content process hosting the plugin instance
+ * 3) The plugin process
+ *
+ * If the plugin process crashes, we cannot be sure if the chrome process
+ * will hear about it first, or the content process will hear about it
+ * first. Because of how IPC works, that's really up to the operating system,
+ * and we assume any guarantees about it, so we have to account for both
+ * possibilities.
+ *
+ * This test exercises the browser's reaction to both possibilities.
+ */
+
+const CRASH_URL = "http://example.com/browser/browser/base/content/test/plugins/plugin_crashCommentAndURL.html";
+const CRASHED_MESSAGE = "BrowserPlugins:NPAPIPluginProcessCrashed";
+
+/**
+ * In order for our test to work, we need to be able to put a plugin
+ * in a very specific state. Specifically, we need it to match the
+ * :-moz-handler-crashed pseudoselector. The only way I can find to
+ * do that is by actually crashing the plugin. So we wait for the
+ * plugin to crash and show the "please" state (since that will
+ * only show if both the message from the parent has been received
+ * AND the PluginCrashed event has fired).
+ *
+ * Once in that state, we try to rewind the clock a little bit - we clear
+ * out the crashData cache in the PluginContent with a message, and we also
+ * override the pluginFallbackState of the <object> to fool PluginContent
+ * into believing that the plugin is in a particular state.
+ *
+ * @param browser
+ * The browser that has loaded the CRASH_URL that we need to
+ * prepare to be in the special state.
+ * @param pluginFallbackState
+ * The value we should override the <object>'s pluginFallbackState
+ * with.
+ * @return Promise
+ * The Promise resolves when the plugin has officially been put into
+ * the crash reporter state, and then "rewound" to have the "status"
+ * attribute of the statusDiv removed. The resolved Promise returns
+ * the run ID for the crashed plugin. It rejects if we never get into
+ * the crash reporter state.
+ */
+function preparePlugin(browser, pluginFallbackState) {
+ return ContentTask.spawn(browser, pluginFallbackState, function* (pluginFallbackState) {
+ let plugin = content.document.getElementById("plugin");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ // CRASH_URL will load a plugin that crashes immediately. We
+ // wait until the plugin has finished being put into the crash
+ // state.
+ let statusDiv;
+ yield ContentTaskUtils.waitForCondition(() => {
+ statusDiv = plugin.ownerDocument
+ .getAnonymousElementByAttribute(plugin, "anonid",
+ "submitStatus");
+ return statusDiv && statusDiv.getAttribute("status") == "please";
+ }, "Timed out waiting for plugin to be in crash report state");
+
+ // "Rewind", by wiping out the status attribute...
+ statusDiv.removeAttribute("status");
+ // Somehow, I'm able to get away with overriding the getter for
+ // this XPCOM object. Probably because I've got chrome privledges.
+ Object.defineProperty(plugin, "pluginFallbackType", {
+ get: function() {
+ return pluginFallbackState;
+ }
+ });
+ return plugin.runID;
+ }).then((runID) => {
+ browser.messageManager.sendAsyncMessage("BrowserPlugins:Test:ClearCrashData");
+ return runID;
+ });
+}
+
+add_task(function* setup() {
+ // Bypass click-to-play
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
+
+ // Clear out any minidumps we create from plugins - we really don't care
+ // about them.
+ let crashObserver = (subject, topic, data) => {
+ if (topic != "plugin-crashed") {
+ return;
+ }
+
+ let propBag = subject.QueryInterface(Ci.nsIPropertyBag2);
+ let minidumpID = propBag.getPropertyAsAString("pluginDumpID");
+
+ let minidumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ minidumpDir.append("minidumps");
+
+ let pluginDumpFile = minidumpDir.clone();
+ pluginDumpFile.append(minidumpID + ".dmp");
+
+ let extraFile = minidumpDir.clone();
+ extraFile.append(minidumpID + ".extra");
+
+ ok(pluginDumpFile.exists(), "Found minidump");
+ ok(extraFile.exists(), "Found extra file");
+
+ pluginDumpFile.remove(false);
+ extraFile.remove(false);
+ };
+
+ Services.obs.addObserver(crashObserver, "plugin-crashed", false);
+ // plugins.testmode will make BrowserPlugins:Test:ClearCrashData work.
+ Services.prefs.setBoolPref("plugins.testmode", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("plugins.testmode");
+ Services.obs.removeObserver(crashObserver, "plugin-crashed");
+ });
+});
+
+/**
+ * In this case, the chrome process hears about the crash first.
+ */
+add_task(function* testChromeHearsPluginCrashFirst() {
+ // Open a remote window so that we can run this test even if e10s is not
+ // enabled by default.
+ let win = yield BrowserTestUtils.openNewBrowserWindow({remote: true});
+ let browser = win.gBrowser.selectedBrowser;
+
+ browser.loadURI(CRASH_URL);
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ // In this case, we want the <object> to match the -moz-handler-crashed
+ // pseudoselector, but we want it to seem still active, because the
+ // content process is not yet supposed to know that the plugin has
+ // crashed.
+ let runID = yield preparePlugin(browser,
+ Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE);
+
+ // Send the message down to PluginContent.jsm saying that the plugin has
+ // crashed, and that we have a crash report.
+ let mm = browser.messageManager;
+ mm.sendAsyncMessage(CRASHED_MESSAGE,
+ { pluginName: "", runID, state: "please" });
+
+ yield ContentTask.spawn(browser, null, function* () {
+ // At this point, the content process should have heard the
+ // plugin crash message from the parent, and we are OK to emit
+ // the PluginCrashed event.
+ let plugin = content.document.getElementById("plugin");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ let statusDiv = plugin.ownerDocument
+ .getAnonymousElementByAttribute(plugin, "anonid",
+ "submitStatus");
+
+ if (statusDiv.getAttribute("status") == "please") {
+ Assert.ok(false, "Did not expect plugin to be in crash report mode yet.");
+ return;
+ }
+
+ // Now we need the plugin to seem crashed to PluginContent.jsm, without
+ // actually crashing the plugin again. We hack around this by overriding
+ // the pluginFallbackType again.
+ Object.defineProperty(plugin, "pluginFallbackType", {
+ get: function() {
+ return Ci.nsIObjectLoadingContent.PLUGIN_CRASHED;
+ },
+ });
+
+ let event = new content.PluginCrashedEvent("PluginCrashed", {
+ pluginName: "",
+ pluginDumpID: "",
+ browserDumpID: "",
+ submittedCrashReport: false,
+ bubbles: true,
+ cancelable: true,
+ });
+
+ plugin.dispatchEvent(event);
+ Assert.equal(statusDiv.getAttribute("status"), "please",
+ "Should have been showing crash report UI");
+ });
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * In this case, the content process hears about the crash first.
+ */
+add_task(function* testContentHearsCrashFirst() {
+ // Open a remote window so that we can run this test even if e10s is not
+ // enabled by default.
+ let win = yield BrowserTestUtils.openNewBrowserWindow({remote: true});
+ let browser = win.gBrowser.selectedBrowser;
+
+ browser.loadURI(CRASH_URL);
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ // In this case, we want the <object> to match the -moz-handler-crashed
+ // pseudoselector, and we want the plugin to seem crashed, since the
+ // content process in this case has heard about the crash first.
+ let runID = yield preparePlugin(browser,
+ Ci.nsIObjectLoadingContent.PLUGIN_CRASHED);
+
+ yield ContentTask.spawn(browser, null, function* () {
+ // At this point, the content process has not yet heard from the
+ // parent about the crash report. Let's ensure that by making sure
+ // we're not showing the plugin crash report UI.
+ let plugin = content.document.getElementById("plugin");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ let statusDiv = plugin.ownerDocument
+ .getAnonymousElementByAttribute(plugin, "anonid",
+ "submitStatus");
+
+ if (statusDiv.getAttribute("status") == "please") {
+ Assert.ok(false, "Did not expect plugin to be in crash report mode yet.");
+ }
+
+ let event = new content.PluginCrashedEvent("PluginCrashed", {
+ pluginName: "",
+ pluginDumpID: "",
+ browserDumpID: "",
+ submittedCrashReport: false,
+ bubbles: true,
+ cancelable: true,
+ });
+
+ plugin.dispatchEvent(event);
+
+ Assert.notEqual(statusDiv.getAttribute("status"), "please",
+ "Should not yet be showing crash report UI");
+ });
+
+ // Now send the message down to PluginContent.jsm that the plugin has
+ // crashed...
+ let mm = browser.messageManager;
+ mm.sendAsyncMessage(CRASHED_MESSAGE,
+ { pluginName: "", runID, state: "please"});
+
+ yield ContentTask.spawn(browser, null, function* () {
+ // At this point, the content process will have heard the message
+ // from the parent and reacted to it. We should be showing the plugin
+ // crash report UI now.
+ let plugin = content.document.getElementById("plugin");
+ plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ let statusDiv = plugin.ownerDocument
+ .getAnonymousElementByAttribute(plugin, "anonid",
+ "submitStatus");
+
+ Assert.equal(statusDiv.getAttribute("status"), "please",
+ "Should have been showing crash report UI");
+ });
+
+ yield BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/plugins/browser_plugin_reloading.js b/browser/base/content/test/plugins/browser_plugin_reloading.js
new file mode 100644
index 000000000..7327d4cf9
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_plugin_reloading.js
@@ -0,0 +1,85 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+var gTestBrowser = null;
+
+function updateAllTestPlugins(aState) {
+ setTestPluginEnabledState(aState, "Test Plug-in");
+ setTestPluginEnabledState(aState, "Second Test Plug-in");
+}
+
+add_task(function* () {
+ registerCleanupFunction(Task.async(function*() {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+ resetBlocklist();
+ gTestBrowser = null;
+ gBrowser.removeCurrentTab();
+ window.focus();
+ }));
+});
+
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ // Prime the blocklist service, the remote service doesn't launch on startup.
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html></html>");
+ let exmsg = yield promiseInitContentBlocklistSvc(gBrowser.selectedBrowser);
+ ok(!exmsg, "exception: " + exmsg);
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+});
+
+// Tests that a click-to-play plugin retains its activated state upon reloading
+add_task(function* () {
+ clearAllPluginPermissions();
+
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 1, Should have a click-to-play notification");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+ "Test 2, plugin fallback type should be PLUGIN_CLICK_TO_PLAY");
+
+ // run the plugin
+ yield promisePlayObject("test");
+
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.displayedType, Ci.nsIObjectLoadingContent.TYPE_PLUGIN, "Test 3, plugin should have started");
+ ok(pluginInfo.activated, "Test 4, plugin node should not be activated");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let plugin = content.document.getElementById("test");
+ let npobj1 = Components.utils.waiveXrays(plugin).getObjectValue();
+ plugin.src = plugin.src;
+ let pluginsDiffer = false;
+ try {
+ Components.utils.waiveXrays(plugin).checkObjectValue(npobj1);
+ } catch (e) {
+ pluginsDiffer = true;
+ }
+
+ Assert.ok(pluginsDiffer, "Test 5, plugins differ.");
+ });
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 6, Plugin should have retained activated state.");
+ is(pluginInfo.displayedType, Ci.nsIObjectLoadingContent.TYPE_PLUGIN, "Test 7, plugin should have started");
+});
diff --git a/browser/base/content/test/plugins/browser_pluginnotification.js b/browser/base/content/test/plugins/browser_pluginnotification.js
new file mode 100644
index 000000000..bf32f37a4
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_pluginnotification.js
@@ -0,0 +1,626 @@
+var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+var gTestBrowser = null;
+
+function updateAllTestPlugins(aState) {
+ setTestPluginEnabledState(aState, "Test Plug-in");
+ setTestPluginEnabledState(aState, "Second Test Plug-in");
+}
+
+add_task(function* () {
+ registerCleanupFunction(Task.async(function*() {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+ resetBlocklist();
+ gTestBrowser = null;
+ gBrowser.removeCurrentTab();
+ window.focus();
+ }));
+});
+
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab();
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ // Prime the blocklist service, the remote service doesn't launch on startup.
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html></html>");
+ let exmsg = yield promiseInitContentBlocklistSvc(gBrowser.selectedBrowser);
+ ok(!exmsg, "exception: " + exmsg);
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+});
+
+// Tests a page with an unknown plugin in it.
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_unknown.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let pluginInfo = yield promiseForPluginInfo("unknown");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED,
+ "Test 1a, plugin fallback type should be PLUGIN_UNSUPPORTED");
+});
+
+// Test that the doorhanger is shown when the user clicks on the overlay
+// after having previously blocked the plugin.
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Plugin should not be activated");
+
+ // Simulate clicking the "Allow Now" button.
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+
+ yield promiseForNotificationShown(notification);
+
+ PopupNotifications.panel.firstChild._secondaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Plugin should be activated");
+
+ // Simulate clicking the "Block" button.
+ yield promiseForNotificationShown(notification);
+
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Plugin should not be activated");
+
+ // Simulate clicking the overlay
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let bounds = doc.getAnonymousElementByAttribute(plugin, "anonid", "main").getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+
+ ok(!notification.dismissed, "A plugin notification should be shown.");
+
+ clearAllPluginPermissions();
+});
+
+// Tests that going back will reshow the notification for click-to-play plugins
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html>hi</html>");
+
+ // make sure the notification is gone
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(!notification, "Test 11b, Should not have a click-to-play notification");
+
+ gTestBrowser.webNavigation.goBack();
+
+ yield promisePopupNotification("click-to-play-plugins");
+});
+
+// Tests that the "Allow Always" permission works for click-to-play plugins
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 12a, Plugin should not be activated");
+
+ // Simulate clicking the "Allow Always" button.
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+
+ yield promiseForNotificationShown(notification);
+
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 12a, Plugin should be activated");
+});
+
+// Test that the "Always" permission, when set for just the Test plugin,
+// does not also allow the Second Test plugin.
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_two_types.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let test = content.document.getElementById("test");
+ let secondtestA = content.document.getElementById("secondtestA");
+ let secondtestB = content.document.getElementById("secondtestB");
+ Assert.ok(test.activated && !secondtestA.activated && !secondtestB.activated,
+ "Content plugins are set up");
+ });
+
+ clearAllPluginPermissions();
+});
+
+// Tests that the plugin's "activated" property is true for working plugins
+// with click-to-play disabled.
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_ENABLED);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test2.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let pluginInfo = yield promiseForPluginInfo("test1");
+ ok(pluginInfo.activated, "Test 14, Plugin should be activated");
+});
+
+// Tests that the overlay is shown instead of alternate content when
+// plugins are click to play.
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_alternate_content.html");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let mainBox = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(!!mainBox, "Test 15, Plugin overlay should exist");
+ });
+});
+
+// Tests that mContentType is used for click-to-play plugins, and not the
+// inspected type.
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_bug749455.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 17, Should have a click-to-play notification");
+});
+
+// Tests that clicking the icon of the overlay activates the doorhanger
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 18g, Plugin should not be activated");
+
+ ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed,
+ "Test 19a, Doorhanger should start out dismissed");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let icon = doc.getAnonymousElementByAttribute(plugin, "class", "icon");
+ let bounds = icon.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+
+ let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed;
+ yield promiseForCondition(condition);
+});
+
+// Tests that clicking the text of the overlay activates the plugin
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 18g, Plugin should not be activated");
+
+ ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed,
+ "Test 19c, Doorhanger should start out dismissed");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let text = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgClickToPlay");
+ let bounds = text.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+
+ let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed;
+ yield promiseForCondition(condition);
+});
+
+// Tests that clicking the box of the overlay activates the doorhanger
+// (just to be thorough)
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 18g, Plugin should not be activated");
+
+ ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed,
+ "Test 19e, Doorhanger should start out dismissed");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", 50, 50, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", 50, 50, 0, 1, 0, false, 0, 0);
+ });
+
+ let condition = () => !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed &&
+ PopupNotifications.panel.firstChild;
+ yield promiseForCondition(condition);
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 19e, Plugin should not be activated");
+
+ clearAllPluginPermissions();
+});
+
+// Tests that a plugin in a div that goes from style="display: none" to
+// "display: block" can be clicked to activate.
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_hidden_to_visible.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 20a, Should have a click-to-play notification");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ Assert.ok(!!overlay, "Test 20a, Plugin overlay should exist");
+ });
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let mainBox = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ let overlayRect = mainBox.getBoundingClientRect();
+ Assert.ok(overlayRect.width == 0 && overlayRect.height == 0,
+ "Test 20a, plugin should have an overlay with 0px width and height");
+ });
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 20b, plugin should not be activated");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let div = doc.getElementById("container");
+ Assert.equal(div.style.display, "none",
+ "Test 20b, container div should be display: none");
+ });
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let div = doc.getElementById("container");
+ div.style.display = "block";
+ });
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let mainBox = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
+ let overlayRect = mainBox.getBoundingClientRect();
+ Assert.ok(overlayRect.width == 200 && overlayRect.height == 200,
+ "Test 20c, plugin should have overlay dims of 200px");
+ });
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(!pluginInfo.activated, "Test 20b, plugin should not be activated");
+
+ ok(notification.dismissed, "Test 20c, Doorhanger should start out dismissed");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let bounds = plugin.getBoundingClientRect();
+ let left = (bounds.left + bounds.right) / 2;
+ let top = (bounds.top + bounds.bottom) / 2;
+ let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+ });
+
+ let condition = () => !notification.dismissed && !!PopupNotifications.panel.firstChild;
+ yield promiseForCondition(condition);
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 20c, plugin should be activated");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlayRect = doc.getAnonymousElementByAttribute(plugin, "anonid", "main").getBoundingClientRect();
+ Assert.ok(overlayRect.width == 0 && overlayRect.height == 0,
+ "Test 20c, plugin should have overlay dims of 0px");
+ });
+
+ clearAllPluginPermissions();
+});
+
+// Test having multiple different types of plugin on one page
+add_task(function* () {
+ // contains three plugins, application/x-test, application/x-second-test x 2
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_two_types.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 21a, Should have a click-to-play notification");
+
+ // confirm all three are blocked by ctp at this point
+ let ids = ["test", "secondtestA", "secondtestB"];
+ for (let id of ids) {
+ yield ContentTask.spawn(gTestBrowser, { id }, function* (args) {
+ let doc = content.document;
+ let plugin = doc.getElementById(args.id);
+ let overlayRect = doc.getAnonymousElementByAttribute(plugin, "anonid", "main").getBoundingClientRect();
+ Assert.ok(overlayRect.width == 200 && overlayRect.height == 200,
+ "Test 21a, plugin " + args.id + " should have click-to-play overlay with dims");
+ });
+
+ let pluginInfo = yield promiseForPluginInfo(id);
+ ok(!pluginInfo.activated, "Test 21a, Plugin with id=" + id + " should not be activated");
+ }
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 21a, Should have a click-to-play notification");
+
+ // we have to actually show the panel to get the bindings to instantiate
+ yield promiseForNotificationShown(notification);
+
+ is(notification.options.pluginData.size, 2, "Test 21a, Should have two types of plugin in the notification");
+
+ let centerAction = null;
+ for (let action of notification.options.pluginData.values()) {
+ if (action.pluginName == "Test") {
+ centerAction = action;
+ break;
+ }
+ }
+ ok(centerAction, "Test 21b, found center action for the Test plugin");
+
+ let centerItem = null;
+ for (let item of PopupNotifications.panel.firstChild.childNodes) {
+ is(item.value, "block", "Test 21b, all plugins should start out blocked");
+ if (item.action == centerAction) {
+ centerItem = item;
+ break;
+ }
+ }
+ ok(centerItem, "Test 21b, found center item for the Test plugin");
+
+ // Select the allow now option in the select drop down for Test Plugin
+ centerItem.value = "allownow";
+
+ // "click" the button to activate the Test plugin
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ ok(pluginInfo.activated, "Test 21b, plugin should be activated");
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 21b, Should have a click-to-play notification");
+
+ yield promiseForNotificationShown(notification);
+
+ ok(notification.options.pluginData.size == 2, "Test 21c, Should have one type of plugin in the notification");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ let overlayRect = doc.getAnonymousElementByAttribute(plugin, "anonid", "main").getBoundingClientRect();
+ Assert.ok(overlayRect.width == 0 && overlayRect.height == 0,
+ "Test 21c, plugin should have overlay dims of 0px");
+ });
+
+ ids = ["secondtestA", "secondtestB"];
+ for (let id of ids) {
+ yield ContentTask.spawn(gTestBrowser, { id }, function* (args) {
+ let doc = content.document;
+ let plugin = doc.getElementById(args.id);
+ let overlayRect = doc.getAnonymousElementByAttribute(plugin, "anonid", "main").getBoundingClientRect();
+ Assert.ok(overlayRect.width == 200 && overlayRect.height == 200,
+ "Test 21c, plugin " + args.id + " should have click-to-play overlay with zero dims");
+ });
+
+
+ let pluginInfo = yield promiseForPluginInfo(id);
+ ok(!pluginInfo.activated, "Test 21c, Plugin with id=" + id + " should not be activated");
+ }
+
+ centerAction = null;
+ for (let action of notification.options.pluginData.values()) {
+ if (action.pluginName == "Second Test") {
+ centerAction = action;
+ break;
+ }
+ }
+ ok(centerAction, "Test 21d, found center action for the Second Test plugin");
+
+ centerItem = null;
+ for (let item of PopupNotifications.panel.firstChild.childNodes) {
+ if (item.action == centerAction) {
+ is(item.value, "block", "Test 21d, test plugin 2 should start blocked");
+ centerItem = item;
+ break;
+ }
+ else {
+ is(item.value, "allownow", "Test 21d, test plugin should be enabled");
+ }
+ }
+ ok(centerItem, "Test 21d, found center item for the Second Test plugin");
+
+ // Select the allow now option in the select drop down for Second Test Plguins
+ centerItem.value = "allownow";
+
+ // "click" the button to activate the Second Test plugins
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 21d, Should have a click-to-play notification");
+
+ ids = ["test", "secondtestA", "secondtestB"];
+ for (let id of ids) {
+ yield ContentTask.spawn(gTestBrowser, { id }, function* (args) {
+ let doc = content.document;
+ let plugin = doc.getElementById(args.id);
+ let overlayRect = doc.getAnonymousElementByAttribute(plugin, "anonid", "main").getBoundingClientRect();
+ Assert.ok(overlayRect.width == 0 && overlayRect.height == 0,
+ "Test 21d, plugin " + args.id + " should have click-to-play overlay with zero dims");
+ });
+
+ let pluginInfo = yield promiseForPluginInfo(id);
+ ok(pluginInfo.activated, "Test 21d, Plugin with id=" + id + " should not be activated");
+ }
+});
+
+// Tests that a click-to-play plugin resets its activated state when changing types
+add_task(function* () {
+ clearAllPluginPermissions();
+
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 22, Should have a click-to-play notification");
+
+ // Plugin should start as CTP
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+ "Test 23, plugin fallback type should be PLUGIN_CLICK_TO_PLAY");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ plugin.type = null;
+ // We currently don't properly change state just on type change,
+ // so rebind the plugin to tree. bug 767631
+ plugin.parentNode.appendChild(plugin);
+ });
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.displayedType, Ci.nsIObjectLoadingContent.TYPE_NULL, "Test 23, plugin should be TYPE_NULL");
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let doc = content.document;
+ let plugin = doc.getElementById("test");
+ plugin.type = "application/x-test";
+ plugin.parentNode.appendChild(plugin);
+ });
+
+ pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.displayedType, Ci.nsIObjectLoadingContent.TYPE_NULL, "Test 23, plugin should be TYPE_NULL");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
+ "Test 23, plugin fallback type should be PLUGIN_CLICK_TO_PLAY");
+ ok(!pluginInfo.activated, "Test 23, plugin node should not be activated");
+});
+
+// Plugin sync removal test. Note this test produces a notification drop down since
+// the plugin we add has zero dims.
+add_task(function* () {
+ updateAllTestPlugins(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_syncRemoved.html");
+
+ // Maybe there some better trick here, we need to wait for the page load, then
+ // wait for the js to execute in the page.
+ yield waitForMs(500);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins");
+ ok(notification, "Test 25: There should be a plugin notification even if the plugin was immediately removed");
+ ok(notification.dismissed, "Test 25: The notification should be dismissed by default");
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html,<html>hi</html>");
+});
+
+// Tests a page with a blocked plugin in it and make sure the infoURL property
+// the blocklist file gets used.
+add_task(function* () {
+ clearAllPluginPermissions();
+
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockPluginInfoURL.xml", gTestBrowser);
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html");
+
+ // Work around for delayed PluginBindingAttached
+ yield promiseUpdatePluginBindings(gTestBrowser);
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins");
+
+ // Since the plugin notification is dismissed by default, reshow it.
+ yield promiseForNotificationShown(notification);
+
+ let pluginInfo = yield promiseForPluginInfo("test");
+ is(pluginInfo.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED,
+ "Test 26, plugin fallback type should be PLUGIN_BLOCKLISTED");
+ ok(!pluginInfo.activated, "Plugin should be activated.");
+
+ const testUrl = "http://test.url.com/";
+
+ let firstPanelChild = PopupNotifications.panel.firstChild;
+ let infoLink = document.getAnonymousElementByAttribute(firstPanelChild, "anonid",
+ "click-to-play-plugins-notification-link");
+ is(infoLink.href, testUrl,
+ "Test 26, the notification URL needs to match the infoURL from the blocklist file.");
+});
diff --git a/browser/base/content/test/plugins/browser_plugins_added_dynamically.js b/browser/base/content/test/plugins/browser_plugins_added_dynamically.js
new file mode 100644
index 000000000..22077a54d
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_plugins_added_dynamically.js
@@ -0,0 +1,137 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir.replace("chrome://mochitests/content/", "http://mochi.test:8888/");
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+var gTestBrowser = null;
+
+add_task(function* () {
+ registerCleanupFunction(Task.async(function*() {
+ clearAllPluginPermissions();
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+ yield asyncSetAndUpdateBlocklist(gTestRoot + "blockNoPlugins.xml", gTestBrowser);
+ resetBlocklist();
+ gBrowser.removeCurrentTab();
+ window.focus();
+ gTestBrowser = null;
+ }));
+});
+
+// "Activate" of a given type -> plugins of that type dynamically added should
+// automatically play.
+add_task(function* () {
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+
+ Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Test Plug-in");
+ setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
+});
+
+add_task(function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_add_dynamically.html");
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(!notification, "Test 1a, Should not have a click-to-play notification");
+
+ // Add a plugin of type test
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ new XPCNativeWrapper(XPCNativeWrapper.unwrap(content).addPlugin("pluginone", "application/x-test"));
+ });
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 1a, Should not have a click-to-play notification");
+
+ yield promiseForNotificationShown(notification);
+
+ let centerAction = null;
+ for (let action of notification.options.pluginData.values()) {
+ if (action.pluginName == "Test") {
+ centerAction = action;
+ break;
+ }
+ }
+ ok(centerAction, "Test 2, found center action for the Test plugin");
+
+ let centerItem = null;
+ for (let item of PopupNotifications.panel.firstChild.childNodes) {
+ is(item.value, "block", "Test 3, all plugins should start out blocked");
+ if (item.action == centerAction) {
+ centerItem = item;
+ break;
+ }
+ }
+ ok(centerItem, "Test 4, found center item for the Test plugin");
+
+ // Select the allow now option in the select drop down for Test Plugin
+ centerItem.value = "allownow";
+
+ // "click" the button to activate the Test plugin
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ let pluginInfo = yield promiseForPluginInfo("pluginone");
+ ok(pluginInfo.activated, "Test 5, plugin should be activated");
+
+ // Add another plugin of type test
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ new XPCNativeWrapper(XPCNativeWrapper.unwrap(content).addPlugin("plugintwo", "application/x-test"));
+ });
+
+ pluginInfo = yield promiseForPluginInfo("plugintwo");
+ ok(pluginInfo.activated, "Test 6, plugins should be activated");
+});
+
+// "Activate" of a given type -> plugins of other types dynamically added
+// should not automatically play.
+add_task(function* () {
+ clearAllPluginPermissions();
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_add_dynamically.html");
+
+ let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(!notification, "Test 7, Should not have a click-to-play notification");
+
+ // Add a plugin of type test
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ new XPCNativeWrapper(XPCNativeWrapper.unwrap(content).addPlugin("pluginone", "application/x-test"));
+ });
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(notification, "Test 8, Should not have a click-to-play notification");
+
+ yield promiseForNotificationShown(notification);
+
+ is(notification.options.pluginData.size, 1, "Should be one plugin action");
+
+ let pluginInfo = yield promiseForPluginInfo("pluginone");
+ ok(!pluginInfo.activated, "Test 8, test plugin should be activated");
+
+ let condition = () => !notification.dismissed &&
+ PopupNotifications.panel.firstChild;
+ yield promiseForCondition(condition);
+
+ // "click" the button to activate the Test plugin
+ PopupNotifications.panel.firstChild._primaryButton.click();
+
+ pluginInfo = yield promiseForPluginInfo("pluginone");
+ ok(pluginInfo.activated, "Test 9, test plugin should be activated");
+
+ yield ContentTask.spawn(gTestBrowser, {}, function* () {
+ new XPCNativeWrapper(XPCNativeWrapper.unwrap(content).addPlugin("plugintwo", "application/x-second-test"));
+ });
+
+ yield promisePopupNotification("click-to-play-plugins");
+
+ pluginInfo = yield promiseForPluginInfo("pluginone");
+ ok(pluginInfo.activated, "Test 10, plugins should be activated");
+ pluginInfo = yield promiseForPluginInfo("plugintwo");
+ ok(!pluginInfo.activated, "Test 11, plugins should be activated");
+});
diff --git a/browser/base/content/test/plugins/browser_private_clicktoplay.js b/browser/base/content/test/plugins/browser_private_clicktoplay.js
new file mode 100644
index 000000000..785b1bb31
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_private_clicktoplay.js
@@ -0,0 +1,216 @@
+var rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir;
+const gHttpTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+
+var gTestBrowser = null;
+var gNextTest = null;
+var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var gPrivateWindow = null;
+var gPrivateBrowser = null;
+
+function finishTest() {
+ clearAllPluginPermissions();
+ gBrowser.removeCurrentTab();
+ if (gPrivateWindow) {
+ gPrivateWindow.close();
+ }
+ window.focus();
+}
+
+let createPrivateWindow = Task.async(function* createPrivateWindow(url) {
+ gPrivateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+ ok(!!gPrivateWindow, "should have created a private window.");
+ gPrivateBrowser = gPrivateWindow.getBrowser().selectedBrowser;
+
+ BrowserTestUtils.loadURI(gPrivateBrowser, url);
+ yield BrowserTestUtils.browserLoaded(gPrivateBrowser);
+});
+
+add_task(function* test() {
+ registerCleanupFunction(function() {
+ clearAllPluginPermissions();
+ getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ });
+
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ gTestBrowser = gBrowser.selectedBrowser;
+ let promise = BrowserTestUtils.browserLoaded(gTestBrowser);
+
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+ getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+ yield promise;
+});
+
+add_task(function* test1a() {
+ yield createPrivateWindow(gHttpTestRoot + "plugin_test.html");
+});
+
+add_task(function* test1b() {
+ let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
+ ok(popupNotification, "Test 1b, Should have a click-to-play notification");
+
+ yield ContentTask.spawn(gPrivateBrowser, null, function() {
+ let plugin = content.document.getElementById("test");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(!objLoadingContent.activated, "Test 1b, Plugin should not be activated");
+ });
+
+ // Check the button status
+ let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
+ "Shown");
+ popupNotification.reshow();
+
+ yield promiseShown;
+ let button1 = gPrivateWindow.PopupNotifications.panel.firstChild._primaryButton;
+ let button2 = gPrivateWindow.PopupNotifications.panel.firstChild._secondaryButton;
+ is(button1.getAttribute("action"), "_singleActivateNow", "Test 1b, Blocked plugin in private window should have a activate now button");
+ ok(button2.hidden, "Test 1b, Blocked plugin in a private window should not have a secondary button")
+
+ gPrivateWindow.close();
+ BrowserTestUtils.loadURI(gTestBrowser, gHttpTestRoot + "plugin_test.html");
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* test2a() {
+ // enable test plugin on this site
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(popupNotification, "Test 2a, Should have a click-to-play notification");
+
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ let plugin = content.document.getElementById("test");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(!objLoadingContent.activated, "Test 2a, Plugin should not be activated");
+ });
+
+ // Simulate clicking the "Allow Now" button.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "Shown");
+ popupNotification.reshow();
+ yield promiseShown;
+
+ PopupNotifications.panel.firstChild._secondaryButton.click();
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let plugin = content.document.getElementById("test");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ let condition = () => objLoadingContent.activated;
+ yield ContentTaskUtils.waitForCondition(condition, "Test 2a, Waited too long for plugin to activate");
+ });
+});
+
+add_task(function* test2c() {
+ let topicObserved = TestUtils.topicObserved("PopupNotifications-updateNotShowing");
+ yield createPrivateWindow(gHttpTestRoot + "plugin_test.html");
+ yield topicObserved;
+
+ let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
+ ok(popupNotification, "Test 2c, Should have a click-to-play notification");
+
+ yield ContentTask.spawn(gPrivateBrowser, null, function() {
+ let plugin = content.document.getElementById("test");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(objLoadingContent.activated, "Test 2c, Plugin should be activated");
+ });
+
+ // Check the button status
+ let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
+ "Shown");
+ popupNotification.reshow();
+ yield promiseShown;
+ let buttonContainer = gPrivateWindow.PopupNotifications.panel.firstChild._buttonContainer;
+ ok(buttonContainer.hidden, "Test 2c, Activated plugin in a private window should not have visible buttons");
+
+ clearAllPluginPermissions();
+ gPrivateWindow.close();
+
+ BrowserTestUtils.loadURI(gTestBrowser, gHttpTestRoot + "plugin_test.html");
+ yield BrowserTestUtils.browserLoaded(gTestBrowser);
+});
+
+add_task(function* test3a() {
+ // enable test plugin on this site
+ let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+ ok(popupNotification, "Test 3a, Should have a click-to-play notification");
+
+ yield ContentTask.spawn(gTestBrowser, null, function() {
+ let plugin = content.document.getElementById("test");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(!objLoadingContent.activated, "Test 3a, Plugin should not be activated");
+ });
+
+ // Simulate clicking the "Allow Always" button.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "Shown");
+ popupNotification.reshow();
+ yield promiseShown;
+ PopupNotifications.panel.firstChild._secondaryButton.click();
+
+ yield ContentTask.spawn(gTestBrowser, null, function* () {
+ let plugin = content.document.getElementById("test");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ let condition = () => objLoadingContent.activated;
+ yield ContentTaskUtils.waitForCondition(condition, "Test 3a, Waited too long for plugin to activate");
+ });
+});
+
+add_task(function* test3c() {
+ let topicObserved = TestUtils.topicObserved("PopupNotifications-updateNotShowing");
+ yield createPrivateWindow(gHttpTestRoot + "plugin_test.html");
+ yield topicObserved;
+
+ let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
+ ok(popupNotification, "Test 3c, Should have a click-to-play notification");
+
+ // Check the button status
+ let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
+ "Shown");
+ popupNotification.reshow();
+ yield promiseShown;
+ let buttonContainer = gPrivateWindow.PopupNotifications.panel.firstChild._buttonContainer;
+ ok(buttonContainer.hidden, "Test 3c, Activated plugin in a private window should not have visible buttons");
+
+ BrowserTestUtils.loadURI(gPrivateBrowser, gHttpTestRoot + "plugin_two_types.html");
+ yield BrowserTestUtils.browserLoaded(gPrivateBrowser);
+});
+
+add_task(function* test3d() {
+ let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
+ ok(popupNotification, "Test 3d, Should have a click-to-play notification");
+
+ // Check the list item status
+ let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
+ "Shown");
+ popupNotification.reshow();
+ yield promiseShown;
+ let doc = gPrivateWindow.document;
+ for (let item of gPrivateWindow.PopupNotifications.panel.firstChild.childNodes) {
+ let allowalways = doc.getAnonymousElementByAttribute(item, "anonid", "allowalways");
+ ok(allowalways, "Test 3d, should have list item for allow always");
+ let allownow = doc.getAnonymousElementByAttribute(item, "anonid", "allownow");
+ ok(allownow, "Test 3d, should have list item for allow now");
+ let block = doc.getAnonymousElementByAttribute(item, "anonid", "block");
+ ok(block, "Test 3d, should have list item for block");
+
+ if (item.action.pluginName === "Test") {
+ is(item.value, "allowalways", "Test 3d, Plugin should bet set to 'allow always'");
+ ok(!allowalways.hidden, "Test 3d, Plugin set to 'always allow' should have a visible 'always allow' action.");
+ ok(allownow.hidden, "Test 3d, Plugin set to 'always allow' should have an invisible 'allow now' action.");
+ ok(block.hidden, "Test 3d, Plugin set to 'always allow' should have an invisible 'block' action.");
+ } else if (item.action.pluginName === "Second Test") {
+ is(item.value, "block", "Test 3d, Second plugin should bet set to 'block'");
+ ok(allowalways.hidden, "Test 3d, Plugin set to 'block' should have a visible 'always allow' action.");
+ ok(!allownow.hidden, "Test 3d, Plugin set to 'block' should have a visible 'allow now' action.");
+ ok(!block.hidden, "Test 3d, Plugin set to 'block' should have a visible 'block' action.");
+ } else {
+ ok(false, "Test 3d, Unexpected plugin '"+item.action.pluginName+"'");
+ }
+ }
+
+ finishTest();
+});
diff --git a/browser/base/content/test/plugins/head.js b/browser/base/content/test/plugins/head.js
new file mode 100644
index 000000000..4995c4dc6
--- /dev/null
+++ b/browser/base/content/test/plugins/head.js
@@ -0,0 +1,396 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+
+// The blocklist shim running in the content process does not initialize at
+// start up, so it's not active until we load content that needs to do a
+// check. This helper bypasses the delay to get the svc up and running
+// immediately. Note, call this after remote content has loaded.
+function promiseInitContentBlocklistSvc(aBrowser)
+{
+ return ContentTask.spawn(aBrowser, {}, function* () {
+ try {
+ Cc["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsIBlocklistService);
+ } catch (ex) {
+ return ex.message;
+ }
+ return null;
+ });
+}
+
+/**
+ * Waits a specified number of miliseconds.
+ *
+ * Usage:
+ * let wait = yield waitForMs(2000);
+ * ok(wait, "2 seconds should now have elapsed");
+ *
+ * @param aMs the number of miliseconds to wait for
+ * @returns a Promise that resolves to true after the time has elapsed
+ */
+function waitForMs(aMs) {
+ return new Promise((resolve) => {
+ setTimeout(done, aMs);
+ function done() {
+ resolve(true);
+ }
+ });
+}
+
+function waitForEvent(subject, eventName, checkFn, useCapture, useUntrusted) {
+ return new Promise((resolve, reject) => {
+ subject.addEventListener(eventName, function listener(event) {
+ try {
+ if (checkFn && !checkFn(event)) {
+ return;
+ }
+ subject.removeEventListener(eventName, listener, useCapture);
+ resolve(event);
+ } catch (ex) {
+ try {
+ subject.removeEventListener(eventName, listener, useCapture);
+ } catch (ex2) {
+ // Maybe the provided object does not support removeEventListener.
+ }
+ reject(ex);
+ }
+ }, useCapture, useUntrusted);
+ });
+}
+
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url)
+ BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+
+ return loaded;
+}
+
+function waitForCondition(condition, nextTest, errorMsg, aTries, aWait) {
+ let tries = 0;
+ let maxTries = aTries || 100; // 100 tries
+ let maxWait = aWait || 100; // 100 msec x 100 tries = ten seconds
+ let interval = setInterval(function() {
+ if (tries >= maxTries) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ let conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, maxWait);
+ let moveOn = function() { clearInterval(interval); nextTest(); };
+}
+
+// Waits for a conditional function defined by the caller to return true.
+function promiseForCondition(aConditionFn, aMessage, aTries, aWait) {
+ return new Promise((resolve) => {
+ waitForCondition(aConditionFn, resolve,
+ (aMessage || "Condition didn't pass."),
+ aTries, aWait);
+ });
+}
+
+// Returns the chrome side nsIPluginTag for this plugin
+function getTestPlugin(aName) {
+ let pluginName = aName || "Test Plug-in";
+ 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 == pluginName)
+ return tags[i];
+ }
+ ok(false, "Unable to find plugin");
+ return null;
+}
+
+// Set the 'enabledState' on the nsIPluginTag stored in the main or chrome
+// process.
+function setTestPluginEnabledState(newEnabledState, pluginName) {
+ let name = pluginName || "Test Plug-in";
+ let plugin = getTestPlugin(name);
+ plugin.enabledState = newEnabledState;
+}
+
+// Get the 'enabledState' on the nsIPluginTag stored in the main or chrome
+// process.
+function getTestPluginEnabledState(pluginName) {
+ let name = pluginName || "Test Plug-in";
+ let plugin = getTestPlugin(name);
+ return plugin.enabledState;
+}
+
+// Returns a promise for nsIObjectLoadingContent props data.
+function promiseForPluginInfo(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return ContentTask.spawn(browser, aId, function* (aId) {
+ let plugin = content.document.getElementById(aId);
+ if (!(plugin instanceof Ci.nsIObjectLoadingContent))
+ throw new Error("no plugin found");
+ return {
+ pluginFallbackType: plugin.pluginFallbackType,
+ activated: plugin.activated,
+ hasRunningPlugin: plugin.hasRunningPlugin,
+ displayedType: plugin.displayedType,
+ };
+ });
+}
+
+// Return a promise and call the plugin's nsIObjectLoadingContent
+// playPlugin() method.
+function promisePlayObject(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return ContentTask.spawn(browser, aId, function* (aId) {
+ let plugin = content.document.getElementById(aId);
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ objLoadingContent.playPlugin();
+ });
+}
+
+function promiseCrashObject(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return ContentTask.spawn(browser, aId, function* (aId) {
+ let plugin = content.document.getElementById(aId);
+ Components.utils.waiveXrays(plugin).crash();
+ });
+}
+
+// Return a promise and call the plugin's getObjectValue() method.
+function promiseObjectValueResult(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return ContentTask.spawn(browser, aId, function* (aId) {
+ let plugin = content.document.getElementById(aId);
+ return Components.utils.waiveXrays(plugin).getObjectValue();
+ });
+}
+
+// Return a promise and reload the target plugin in the page
+function promiseReloadPlugin(aId, aBrowser) {
+ let browser = aBrowser || gTestBrowser;
+ return ContentTask.spawn(browser, aId, function* (aId) {
+ let plugin = content.document.getElementById(aId);
+ plugin.src = plugin.src;
+ });
+}
+
+// after a test is done using the plugin doorhanger, we should just clear
+// any permissions that may have crept in
+function clearAllPluginPermissions() {
+ let perms = Services.perms.enumerator;
+ while (perms.hasMoreElements()) {
+ let perm = perms.getNext();
+ if (perm.type.startsWith('plugin')) {
+ info("removing permission:" + perm.principal.origin + " " + perm.type + "\n");
+ Services.perms.removePermission(perm);
+ }
+ }
+}
+
+function updateBlocklist(aCallback) {
+ let blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsITimerCallback);
+ let observer = function() {
+ Services.obs.removeObserver(observer, "blocklist-updated");
+ SimpleTest.executeSoon(aCallback);
+ };
+ Services.obs.addObserver(observer, "blocklist-updated", false);
+ blocklistNotifier.notify(null);
+}
+
+var _originalTestBlocklistURL = null;
+function setAndUpdateBlocklist(aURL, aCallback) {
+ if (!_originalTestBlocklistURL) {
+ _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url");
+ }
+ Services.prefs.setCharPref("extensions.blocklist.url", aURL);
+ updateBlocklist(aCallback);
+}
+
+// A generator that insures a new blocklist is loaded (in both
+// processes if applicable).
+function* asyncSetAndUpdateBlocklist(aURL, aBrowser) {
+ info("*** loading new blocklist: " + aURL);
+ let doTestRemote = aBrowser ? aBrowser.isRemoteBrowser : false;
+ if (!_originalTestBlocklistURL) {
+ _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url");
+ }
+ Services.prefs.setCharPref("extensions.blocklist.url", aURL);
+ let localPromise = TestUtils.topicObserved("blocklist-updated");
+ let remotePromise;
+ if (doTestRemote) {
+ remotePromise = TestUtils.topicObserved("content-blocklist-updated");
+ }
+ let blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsITimerCallback);
+ blocklistNotifier.notify(null);
+ info("*** waiting on local load");
+ yield localPromise;
+ if (doTestRemote) {
+ info("*** waiting on remote load");
+ yield remotePromise;
+ }
+ info("*** blocklist loaded.");
+}
+
+// Reset back to the blocklist we had at the start of the test run.
+function resetBlocklist() {
+ Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL);
+}
+
+// Insure there's a popup notification present. This test does not indicate
+// open state. aBrowser can be undefined.
+function promisePopupNotification(aName, aBrowser) {
+ return new Promise((resolve) => {
+ waitForCondition(() => PopupNotifications.getNotification(aName, aBrowser),
+ () => {
+ ok(!!PopupNotifications.getNotification(aName, aBrowser),
+ aName + " notification appeared");
+
+ resolve();
+ }, "timeout waiting for popup notification " + aName);
+ });
+}
+
+/**
+ * Allows setting focus on a window, and waiting for that window to achieve
+ * focus.
+ *
+ * @param aWindow
+ * The window to focus and wait for.
+ *
+ * @return {Promise}
+ * @resolves When the window is focused.
+ * @rejects Never.
+ */
+function promiseWaitForFocus(aWindow) {
+ return new Promise((resolve) => {
+ waitForFocus(resolve, aWindow);
+ });
+}
+
+/**
+ * Returns a Promise that resolves when a notification bar
+ * for a browser is shown. Alternatively, for old-style callers,
+ * can automatically call a callback before it resolves.
+ *
+ * @param notificationID
+ * The ID of the notification to look for.
+ * @param browser
+ * The browser to check for the notification bar.
+ * @param callback (optional)
+ * A function to be called just before the Promise resolves.
+ *
+ * @return Promise
+ */
+function waitForNotificationBar(notificationID, browser, callback) {
+ return new Promise((resolve, reject) => {
+ let notification;
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ waitForCondition(
+ () => (notification = notificationBox.getNotificationWithValue(notificationID)),
+ () => {
+ ok(notification, `Successfully got the ${notificationID} notification bar`);
+ if (callback) {
+ callback(notification);
+ }
+ resolve(notification);
+ },
+ `Waited too long for the ${notificationID} notification bar`
+ );
+ });
+}
+
+function promiseForNotificationBar(notificationID, browser) {
+ return new Promise((resolve) => {
+ waitForNotificationBar(notificationID, browser, resolve);
+ });
+}
+
+/**
+ * Reshow a notification and call a callback when it is reshown.
+ * @param notification
+ * The notification to reshow
+ * @param callback
+ * A function to be called when the notification has been reshown
+ */
+function waitForNotificationShown(notification, callback) {
+ if (PopupNotifications.panel.state == "open") {
+ executeSoon(callback);
+ return;
+ }
+ PopupNotifications.panel.addEventListener("popupshown", function onShown(e) {
+ PopupNotifications.panel.removeEventListener("popupshown", onShown);
+ callback();
+ }, false);
+ notification.reshow();
+}
+
+function promiseForNotificationShown(notification) {
+ return new Promise((resolve) => {
+ waitForNotificationShown(notification, resolve);
+ });
+}
+
+/**
+ * Due to layout being async, "PluginBindAttached" may trigger later. This
+ * returns a Promise that resolves once we've forced a layout flush, which
+ * triggers the PluginBindAttached event to fire. This trick only works if
+ * there is some sort of plugin in the page.
+ * @param browser
+ * The browser to force plugin bindings in.
+ * @return Promise
+ */
+function promiseUpdatePluginBindings(browser) {
+ return ContentTask.spawn(browser, {}, function* () {
+ let doc = content.document;
+ let elems = doc.getElementsByTagName('embed');
+ if (!elems || elems.length < 1) {
+ elems = doc.getElementsByTagName('object');
+ }
+ if (elems && elems.length > 0) {
+ elems[0].clientTop;
+ }
+ });
+}
diff --git a/browser/base/content/test/plugins/plugin_add_dynamically.html b/browser/base/content/test/plugins/plugin_add_dynamically.html
new file mode 100644
index 000000000..863d36e09
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_add_dynamically.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<script>
+function addPlugin(aId, aType="application/x-test") {
+ var embed = document.createElement("embed");
+ embed.setAttribute("id", aId);
+ embed.style.width = "200px";
+ embed.style.height = "200px";
+ embed.setAttribute("type", aType);
+ return document.body.appendChild(embed);
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_alternate_content.html b/browser/base/content/test/plugins/plugin_alternate_content.html
new file mode 100644
index 000000000..f8acc833c
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_alternate_content.html
@@ -0,0 +1,9 @@
+<!-- bug 739575 -->
+<html>
+<head><meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+</head>
+<body>
+<object id="test" type="application/x-test" style="height: 200px; width:200px">
+<p><a href="about:blank">you should not see this link when plugins are click-to-play</a></p>
+</object>
+</body></html>
diff --git a/browser/base/content/test/plugins/plugin_big.html b/browser/base/content/test/plugins/plugin_big.html
new file mode 100644
index 000000000..d11506176
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_big.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 500px; height: 500px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_both.html b/browser/base/content/test/plugins/plugin_both.html
new file mode 100644
index 000000000..2335366dc
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_both.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="unknown" style="width: 100px; height: 100px" type="application/x-unknown">
+<embed id="test" style="width: 100px; height: 100px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_both2.html b/browser/base/content/test/plugins/plugin_both2.html
new file mode 100644
index 000000000..ba605d6e8
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_both2.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 100px; height: 100px" type="application/x-test">
+<embed id="unknown" style="width: 100px; height: 100px" type="application/x-unknown">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_bug744745.html b/browser/base/content/test/plugins/plugin_bug744745.html
new file mode 100644
index 000000000..d0691c9c0
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_bug744745.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"/></head>
+<body>
+<style>
+.x {
+ opacity: 0 !important;
+}
+</style>
+<object id="test" class="x" type="application/x-test" width=200 height=200></object>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_bug749455.html b/browser/base/content/test/plugins/plugin_bug749455.html
new file mode 100644
index 000000000..831dc82f7
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_bug749455.html
@@ -0,0 +1,8 @@
+<!-- bug 749455 -->
+<html>
+<head><meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+</head>
+<body>
+<embed src="plugin_bug749455.html" type="application/x-test" width="100px" height="100px"></embed>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_bug787619.html b/browser/base/content/test/plugins/plugin_bug787619.html
new file mode 100644
index 000000000..cb91116f0
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_bug787619.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"/></head>
+<body>
+ <a id="wrapper">
+ <embed id="plugin" style="width: 200px; height: 200px" type="application/x-test">
+ </a>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_bug797677.html b/browser/base/content/test/plugins/plugin_bug797677.html
new file mode 100644
index 000000000..1545f3647
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_bug797677.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"/></head>
+<body><embed id="plugin" type="9000"></embed></body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_bug820497.html b/browser/base/content/test/plugins/plugin_bug820497.html
new file mode 100644
index 000000000..4884e9dbe
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_bug820497.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"/></head>
+<body>
+<object id="test" type="application/x-test" width=200 height=200></object>
+<script>
+ function addSecondPlugin() {
+ var object = document.createElement("object");
+ object.type = "application/x-second-test";
+ object.width = 200;
+ object.height = 200;
+ object.id = "secondtest";
+ document.body.appendChild(object);
+ }
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_clickToPlayAllow.html b/browser/base/content/test/plugins/plugin_clickToPlayAllow.html
new file mode 100644
index 000000000..3f5df1984
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_clickToPlayAllow.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 200px; height: 200px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_clickToPlayDeny.html b/browser/base/content/test/plugins/plugin_clickToPlayDeny.html
new file mode 100644
index 000000000..3f5df1984
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_clickToPlayDeny.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 200px; height: 200px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_crashCommentAndURL.html b/browser/base/content/test/plugins/plugin_crashCommentAndURL.html
new file mode 100644
index 000000000..711a19ed3
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_crashCommentAndURL.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <script type="text/javascript">
+ function crash() {
+ var plugin = document.getElementById("plugin");
+ var argStr = decodeURIComponent(window.location.search.substr(1));
+ if (argStr) {
+ var args = JSON.parse(argStr);
+ for (var key in args)
+ plugin.setAttribute(key, args[key]);
+ }
+ try {
+ plugin.crash();
+ }
+ catch (err) {}
+ }
+ </script>
+ </head>
+ <body onload="crash();">
+ <embed id="plugin" type="application/x-test"
+ width="400" height="400"
+ drawmode="solid" color="FF00FFFF">
+ </embed>
+ </body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_data_url.html b/browser/base/content/test/plugins/plugin_data_url.html
new file mode 100644
index 000000000..77e101144
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_data_url.html
@@ -0,0 +1,11 @@
+<html>
+<body>
+ <a id="data-link-1" href='data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>'>
+ data: with one plugin
+ </a><br />
+ <a id="data-link-2" href='data:text/html,<embed id="test1" style="width: 200px; height: 200px" type="application/x-test"/><embed id="test2" style="width: 200px; height: 200px" type="application/x-second-test"/>'>
+ data: with two plugins
+ </a><br />
+ <object id="test" style="width: 200px; height: 200px" type="application/x-test"></object>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_hidden_to_visible.html b/browser/base/content/test/plugins/plugin_hidden_to_visible.html
new file mode 100644
index 000000000..eeacc1874
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_hidden_to_visible.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+ <div id="container" style="display: none">
+ <object id="test" type="application/x-test" style="width: 200px; height: 200px;"></object>
+ </div>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_iframe.html b/browser/base/content/test/plugins/plugin_iframe.html
new file mode 100644
index 000000000..239c9a771
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<iframe id="frame" with="400" height="400" src="plugin_test.html">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_outsideScrollArea.html b/browser/base/content/test/plugins/plugin_outsideScrollArea.html
new file mode 100644
index 000000000..c6ef50d5d
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_outsideScrollArea.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<style type="text/css">
+#container {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ background: blue;
+}
+
+#test {
+ width: 400px;
+ height: 400px;
+ position: absolute;
+}
+</style>
+</head>
+<body>
+ <div id="container"></div>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_overlayed.html b/browser/base/content/test/plugins/plugin_overlayed.html
new file mode 100644
index 000000000..11c127093
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_overlayed.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8">
+ <style type="text/css">
+ .absthing {
+ width: 400px;
+ height: 400px;
+ position: absolute;
+ left: 20px;
+ top: 20px;
+ }
+ #d1 {
+ z-index: 1;
+ }
+ #d2 {
+ z-index: 2;
+ background-color: rgba(0,0,255,0.5);
+ border: 1px solid red;
+ }
+ </style>
+<body>
+ <div class="absthing" id="d1">
+ <embed id="test" type="application/x-test">
+ </div>
+ <div class="absthing" id="d2">
+ <p>This is overlaying
+ </div>
diff --git a/browser/base/content/test/plugins/plugin_positioned.html b/browser/base/content/test/plugins/plugin_positioned.html
new file mode 100644
index 000000000..1bad7ee46
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_positioned.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<head>
+ <meta charset="utf-8">
+ <style type="text/css">
+ #test {
+ position: absolute;
+ left: -1000px;
+ top: -1000px;
+ }
+ </style>
+<body>
+ <embed id="test" type="application/x-test">
diff --git a/browser/base/content/test/plugins/plugin_small.html b/browser/base/content/test/plugins/plugin_small.html
new file mode 100644
index 000000000..f37ee28c7
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_small.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 10px; height: 10px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_small_2.html b/browser/base/content/test/plugins/plugin_small_2.html
new file mode 100644
index 000000000..ebc5ffe84
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_small_2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 10px; height: 10px" type="application/x-second-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_syncRemoved.html b/browser/base/content/test/plugins/plugin_syncRemoved.html
new file mode 100644
index 000000000..d97787056
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_syncRemoved.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<body>
+<script type="text/javascript">
+ // create an embed, insert it in the doc and immediately remove it
+ var embed = document.createElement('embed');
+ embed.setAttribute("id", "test");
+ embed.setAttribute("type", "application/x-test");
+ embed.setAttribute("style", "width: 0px; height: 0px;");
+ document.body.appendChild(embed);
+ window.getComputedStyle(embed, null).top;
+ document.body.remove(embed);
+</script>
diff --git a/browser/base/content/test/plugins/plugin_test.html b/browser/base/content/test/plugins/plugin_test.html
new file mode 100644
index 000000000..d4b5b6ca7
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_test.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 200px; height: 200px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_test2.html b/browser/base/content/test/plugins/plugin_test2.html
new file mode 100644
index 000000000..95614c930
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_test2.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test1" style="width: 200px; height: 200px" type="application/x-test">
+<embed id="test2" style="width: 200px; height: 200px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_test3.html b/browser/base/content/test/plugins/plugin_test3.html
new file mode 100644
index 000000000..215c02326
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_test3.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="test" style="width: 0px; height: 0px" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_two_types.html b/browser/base/content/test/plugins/plugin_two_types.html
new file mode 100644
index 000000000..2359d2ec1
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_two_types.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"/></head>
+<body>
+<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>
+<embed id="secondtestA" style="width: 200px; height: 200px" type="application/x-second-test"/>
+<embed id="secondtestB" style="width: 200px; height: 200px" type="application/x-second-test"/>
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_unknown.html b/browser/base/content/test/plugins/plugin_unknown.html
new file mode 100644
index 000000000..578f455cc
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_unknown.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<embed id="unknown" style="width: 100px; height: 100px" type="application/x-unknown">
+</body>
+</html>
diff --git a/browser/base/content/test/plugins/plugin_zoom.html b/browser/base/content/test/plugins/plugin_zoom.html
new file mode 100644
index 000000000..f9e598658
--- /dev/null
+++ b/browser/base/content/test/plugins/plugin_zoom.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<!-- The odd width and height are here to trigger bug 972237. -->
+<embed id="test" style="width: 99.789%; height: 99.123%" type="application/x-test">
+</body>
+</html>
diff --git a/browser/base/content/test/popupNotifications/.eslintrc.js b/browser/base/content/test/popupNotifications/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/popupNotifications/browser.ini b/browser/base/content/test/popupNotifications/browser.ini
new file mode 100644
index 000000000..83bb7c517
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_displayURI.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_2.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_3.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_4.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_checkbox.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_reshow_in_background.js]
+skip-if = (os == "linux" && (debug || asan))
diff --git a/browser/base/content/test/popupNotifications/browser_displayURI.js b/browser/base/content/test/popupNotifications/browser_displayURI.js
new file mode 100644
index 000000000..48222be19
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_displayURI.js
@@ -0,0 +1,28 @@
+/*
+ * Make sure that the origin is shown for ContentPermissionPrompt
+ * consumers e.g. geolocation.
+*/
+
+add_task(function* test_displayURI() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://test1.example.com/",
+ }, function*(browser) {
+ let popupShownPromise = new Promise((resolve, reject) => {
+ onPopupEvent("popupshown", function() {
+ resolve(this);
+ });
+ });
+ yield ContentTask.spawn(browser, null, function*() {
+ content.navigator.geolocation.getCurrentPosition(function (pos) {
+ // Do nothing
+ });
+ });
+ let panel = yield popupShownPromise;
+ let notification = panel.children[0];
+ let body = document.getAnonymousElementByAttribute(notification,
+ "class",
+ "popup-notification-body");
+ ok(body.innerHTML.includes("example.com"), "Check that at least the eTLD+1 is present in the markup");
+ });
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification.js b/browser/base/content/test/popupNotifications/browser_popupNotification.js
new file mode 100644
index 000000000..6be3e4205
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These are shared between test #4 to #5
+var wrongBrowserNotificationObject = new BasicNotification("wrongBrowser");
+var wrongBrowserNotification;
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+ goNext();
+}
+
+var tests = [
+ { id: "Test#1",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.mainActionClicked, "mainAction was clicked");
+ ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ { id: "Test#2",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
+ ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ { id: "Test#3",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ // test opening a notification for a background browser
+ // Note: test 4 to 6 share a tab.
+ { id: "Test#4",
+ run: function* () {
+ let tab = gBrowser.addTab("about:blank");
+ isnot(gBrowser.selectedTab, tab, "new tab isn't selected");
+ wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(tab);
+ let promiseTopic = promiseTopicObserved("PopupNotifications-backgroundShow");
+ wrongBrowserNotification = showNotification(wrongBrowserNotificationObject);
+ yield promiseTopic;
+ is(PopupNotifications.isPanelOpen, false, "panel isn't open");
+ ok(!wrongBrowserNotificationObject.mainActionClicked, "main action wasn't clicked");
+ ok(!wrongBrowserNotificationObject.secondaryActionClicked, "secondary action wasn't clicked");
+ ok(!wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback wasn't called");
+ goNext();
+ }
+ },
+ // now select that browser and test to see that the notification appeared
+ { id: "Test#5",
+ run: function () {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ },
+ onShown: function (popup) {
+ checkPopup(popup, wrongBrowserNotificationObject);
+ is(PopupNotifications.isPanelOpen, true, "isPanelOpen getter doesn't lie");
+
+ // switch back to the old browser
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ onHidden: function (popup) {
+ // actually remove the notification to prevent it from reappearing
+ ok(wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback triggered due to tab switch");
+ wrongBrowserNotification.remove();
+ ok(wrongBrowserNotificationObject.removedCallbackTriggered, "removed callback triggered");
+ wrongBrowserNotification = null;
+ }
+ },
+ // test that the removed notification isn't shown on browser re-select
+ { id: "Test#6",
+ run: function* () {
+ let promiseTopic = promiseTopicObserved("PopupNotifications-updateNotShowing");
+ gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ yield promiseTopic;
+ is(PopupNotifications.isPanelOpen, false, "panel isn't open");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ goNext();
+ }
+ },
+ // Test that two notifications with the same ID result in a single displayed
+ // notification.
+ { id: "Test#7",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ // Show the same notification twice
+ this.notification1 = showNotification(this.notifyObj);
+ this.notification2 = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ this.notification2.remove();
+ },
+ onHidden: function (popup) {
+ ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ // Test that two notifications with different IDs are displayed
+ { id: "Test#8",
+ run: function () {
+ this.testNotif1 = new BasicNotification(this.id);
+ this.testNotif1.message += " 1";
+ showNotification(this.testNotif1);
+ this.testNotif2 = new BasicNotification(this.id);
+ this.testNotif2.message += " 2";
+ this.testNotif2.id += "-2";
+ showNotification(this.testNotif2);
+ },
+ onShown: function (popup) {
+ is(popup.childNodes.length, 2, "two notifications are shown");
+ // Trigger the main command for the first notification, and the secondary
+ // for the second. Need to do mainCommand first since the secondaryCommand
+ // triggering is async.
+ triggerMainCommand(popup);
+ is(popup.childNodes.length, 1, "only one notification left");
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden: function (popup) {
+ ok(this.testNotif1.mainActionClicked, "main action #1 was clicked");
+ ok(!this.testNotif1.secondaryActionClicked, "secondary action #1 wasn't clicked");
+ ok(!this.testNotif1.dismissalCallbackTriggered, "dismissal callback #1 wasn't called");
+
+ ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked");
+ ok(this.testNotif2.secondaryActionClicked, "secondary action #2 was clicked");
+ ok(!this.testNotif2.dismissalCallbackTriggered, "dismissal callback #2 wasn't called");
+ }
+ },
+ // Test notification without mainAction
+ { id: "Test#9",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction = null;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ this.notification.remove();
+ }
+ },
+ // Test two notifications with different anchors
+ { id: "Test#10",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.firstNotification = showNotification(this.notifyObj);
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "-2";
+ this.notifyObj2.anchorID = "addons-notification-icon";
+ // Second showNotification() overrides the first
+ this.secondNotification = showNotification(this.notifyObj2);
+ },
+ onShown: function (popup) {
+ // This also checks that only one element is shown.
+ checkPopup(popup, this.notifyObj2);
+ is(document.getElementById("geo-notification-icon").boxObject.width, 0,
+ "geo anchor shouldn't be visible");
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ // Remove the notifications
+ this.firstNotification.remove();
+ this.secondNotification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
+ }
+ }
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_2.js b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js
new file mode 100644
index 000000000..d77098895
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+ goNext();
+}
+
+var tests = [
+ // Test optional params
+ { id: "Test#1",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions = undefined;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ // Test that icons appear
+ { id: "Test#2",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.id = "geolocation";
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
+ "geo anchor should be visible");
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ let icon = document.getElementById("geo-notification-icon");
+ isnot(icon.boxObject.width, 0,
+ "geo anchor should be visible after dismissal");
+ this.notification.remove();
+ is(icon.boxObject.width, 0,
+ "geo anchor should not be visible after removal");
+ }
+ },
+
+ // Test that persistence allows the notification to persist across reloads
+ { id: "Test#3",
+ run: function* () {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistence: 2
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function* (popup) {
+ this.complete = false;
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/")
+ // Next load will remove the notification
+ this.complete = true;
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ },
+ onHidden: function (popup) {
+ ok(this.complete, "Should only have hidden the notification after 3 page loads");
+ ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ }
+ },
+ // Test that a timeout allows the notification to persist across reloads
+ { id: "Test#4",
+ run: function* () {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ this.notifyObj = new BasicNotification(this.id);
+ // Set a timeout of 10 minutes that should never be hit
+ this.notifyObj.addOptions({
+ timeout: Date.now() + 600000
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function* (popup) {
+ this.complete = false;
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Next load will hide the notification
+ this.notification.options.timeout = Date.now() - 1;
+ this.complete = true;
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ },
+ onHidden: function (popup) {
+ ok(this.complete, "Should only have hidden the notification after the timeout was passed");
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ }
+ },
+ // Test that setting persistWhileVisible allows a visible notification to
+ // persist across location changes
+ { id: "Test#5",
+ run: function* () {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistWhileVisible: true
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function* (popup) {
+ this.complete = false;
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Notification should persist across location changes
+ this.complete = true;
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ ok(this.complete, "Should only have hidden the notification after it was dismissed");
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ }
+ },
+
+ // Test that nested icon nodes correctly activate popups
+ { id: "Test#6",
+ run: function() {
+ // Add a temporary box as the anchor with a button
+ this.box = document.createElement("box");
+ PopupNotifications.iconBox.appendChild(this.box);
+
+ let button = document.createElement("button");
+ button.setAttribute("label", "Please click me!");
+ this.box.appendChild(button);
+
+ // The notification should open up on the box
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = this.box.id = "nested-box";
+ this.notifyObj.addOptions({dismissed: true});
+ this.notification = showNotification(this.notifyObj);
+
+ // This test places a normal button in the notification area, which has
+ // standard GTK styling and dimensions. Due to the clip-path, this button
+ // gets clipped off, which makes it necessary to synthesize the mouse click
+ // a little bit downward. To be safe, I adjusted the x-offset with the same
+ // amount.
+ EventUtils.synthesizeMouse(button, 4, 4, {});
+ },
+ onShown: function(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden: function(popup) {
+ this.notification.remove();
+ this.box.parentNode.removeChild(this.box);
+ }
+ },
+ // Test that popupnotifications without popups have anchor icons shown
+ { id: "Test#7",
+ run: function* () {
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.anchorID = "geo-notification-icon";
+ notifyObj.addOptions({neverShow: true});
+ let promiseTopic = promiseTopicObserved("PopupNotifications-updateNotShowing");
+ showNotification(notifyObj);
+ yield promiseTopic;
+ isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
+ "geo anchor should be visible");
+ goNext();
+ }
+ },
+ // Test notification "Not Now" menu item
+ { id: "Test#8",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 1);
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ // Test notification close button
+ { id: "Test#9",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+ EventUtils.synthesizeMouseAtCenter(notification.closebutton, {});
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ // Test notification when chrome is hidden
+ { id: "Test#10",
+ run: function () {
+ window.locationbar.visible = false;
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ is(popup.anchorNode.className, "tabbrowser-tab", "notification anchored to tab");
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ window.locationbar.visible = true;
+ }
+ },
+ // Test that dismissed popupnotifications can be opened on about:blank
+ // (where the rest of the identity block is disabled)
+ { id: "Test#11",
+ run: function() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({dismissed: true});
+ this.notification = showNotification(this.notifyObj);
+
+ EventUtils.synthesizeMouse(document.getElementById("geo-notification-icon"), 0, 0, {});
+ },
+ onShown: function(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden: function(popup) {
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ }
+ }
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_3.js b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js
new file mode 100644
index 000000000..33ec3f714
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_3.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/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+ goNext();
+}
+
+var tests = [
+ // Test notification is removed when dismissed if removeOnDismissal is true
+ { id: "Test#1",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ removeOnDismissal: true
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ // Test multiple notification icons are shown
+ { id: "Test#2",
+ run: function () {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notification2 = showNotification(this.notifyObj2);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj2);
+
+ // check notifyObj1 anchor icon is showing
+ isnot(document.getElementById("default-notification-icon").boxObject.width, 0,
+ "default anchor should be visible");
+ // check notifyObj2 anchor icon is showing
+ isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
+ "geo anchor should be visible");
+
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ this.notification1.remove();
+ ok(this.notifyObj1.removedCallbackTriggered, "removed callback triggered");
+
+ this.notification2.remove();
+ ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ // Test that multiple notification icons are removed when switching tabs
+ { id: "Test#3",
+ run: function () {
+ // show the notification on old tab.
+ this.notifyObjOld = new BasicNotification(this.id);
+ this.notifyObjOld.anchorID = "default-notification-icon";
+ this.notificationOld = showNotification(this.notifyObjOld);
+
+ // switch tab
+ this.oldSelectedTab = gBrowser.selectedTab;
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+
+ // show the notification on new tab.
+ this.notifyObjNew = new BasicNotification(this.id);
+ this.notifyObjNew.anchorID = "geo-notification-icon";
+ this.notificationNew = showNotification(this.notifyObjNew);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObjNew);
+
+ // check notifyObjOld anchor icon is removed
+ is(document.getElementById("default-notification-icon").boxObject.width, 0,
+ "default anchor shouldn't be visible");
+ // check notifyObjNew anchor icon is showing
+ isnot(document.getElementById("geo-notification-icon").boxObject.width, 0,
+ "geo anchor should be visible");
+
+ dismissNotification(popup);
+ },
+ onHidden: function (popup) {
+ this.notificationNew.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ gBrowser.selectedTab = this.oldSelectedTab;
+ this.notificationOld.remove();
+ }
+ },
+ // test security delay - too early
+ { id: "Test#4",
+ run: function () {
+ // Set the security delay to 100s
+ PopupNotifications.buttonDelay = 100000;
+
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+
+ // Wait to see if the main command worked
+ executeSoon(function delayedDismissal() {
+ dismissNotification(popup);
+ });
+
+ },
+ onHidden: function (popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked because it was too soon");
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
+ }
+ },
+ // test security delay - after delay
+ { id: "Test#5",
+ run: function () {
+ // Set the security delay to 10ms
+ PopupNotifications.buttonDelay = 10;
+
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+
+ // Wait until after the delay to trigger the main action
+ setTimeout(function delayedDismissal() {
+ triggerMainCommand(popup);
+ }, 500);
+
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.mainActionClicked, "mainAction was clicked after the delay");
+ ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback was not triggered");
+ PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
+ }
+ },
+ // reload removes notification
+ { id: "Test#6",
+ run: function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(true, "Notification removed in background tab after reloading");
+ goNext();
+ }
+ };
+ showNotification(notifyObj);
+ executeSoon(function () {
+ gBrowser.selectedBrowser.reload();
+ });
+ }
+ },
+ // location change in background tab removes notification
+ { id: "Test#7",
+ run: function* () {
+ let oldSelectedTab = gBrowser.selectedTab;
+ let newTab = gBrowser.addTab("about:blank");
+ gBrowser.selectedTab = newTab;
+
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ gBrowser.selectedTab = oldSelectedTab;
+ let browser = gBrowser.getBrowserForTab(newTab);
+
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.browser = browser;
+ notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(true, "Notification removed in background tab after reloading");
+ executeSoon(function () {
+ gBrowser.removeTab(newTab);
+ goNext();
+ });
+ }
+ };
+ showNotification(notifyObj);
+ executeSoon(function () {
+ browser.reload();
+ });
+ }
+ },
+ // Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab
+ { id: "Test#8",
+ run: function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ let originalTab = gBrowser.selectedTab;
+ let bgTab = gBrowser.addTab("about:blank");
+ gBrowser.selectedTab = bgTab;
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ let anchor = document.createElement("box");
+ anchor.id = "test26-anchor";
+ anchor.className = "notification-anchor-icon";
+ PopupNotifications.iconBox.appendChild(anchor);
+
+ gBrowser.selectedTab = originalTab;
+
+ let fgNotifyObj = new BasicNotification(this.id);
+ fgNotifyObj.anchorID = anchor.id;
+ fgNotifyObj.options.dismissed = true;
+ let fgNotification = showNotification(fgNotifyObj);
+
+ let bgNotifyObj = new BasicNotification(this.id);
+ bgNotifyObj.anchorID = anchor.id;
+ bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab);
+ // show the notification in the background tab ...
+ let bgNotification = showNotification(bgNotifyObj);
+ // ... and re-show it
+ bgNotification = showNotification(bgNotifyObj);
+
+ ok(fgNotification.id, "notification has id");
+ is(fgNotification.id, bgNotification.id, "notification ids are the same");
+ is(anchor.getAttribute("showing"), "true", "anchor still showing");
+
+ fgNotification.remove();
+ gBrowser.removeTab(bgTab);
+ goNext();
+ }
+ },
+ // location change in an embedded frame should not remove a notification
+ { id: "Test#9",
+ run: function* () {
+ yield promiseTabLoadEvent(gBrowser.selectedTab, "data:text/html;charset=utf8,<iframe%20id='iframe'%20src='http://example.com/'>");
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(false, "Notification removed from browser when subframe navigated");
+ }
+ };
+ showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ let self = this;
+ let progressListener = {
+ onLocationChange: function onLocationChange() {
+ gBrowser.removeProgressListener(progressListener);
+
+ executeSoon(() => {
+ let notification = PopupNotifications.getNotification(self.notifyObj.id,
+ self.notifyObj.browser);
+ ok(notification != null, "Notification remained when subframe navigated");
+ self.notifyObj.options.eventCallback = undefined;
+
+ notification.remove();
+ });
+ },
+ };
+
+ info("Adding progress listener and performing navigation");
+ gBrowser.addProgressListener(progressListener);
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+ content.document.getElementById("iframe")
+ .setAttribute("src", "http://example.org/");
+ });
+ },
+ onHidden: function () {}
+ },
+ // Popup Notifications should catch exceptions from callbacks
+ { id: "Test#10",
+ run: function () {
+ this.testNotif1 = new BasicNotification(this.id);
+ this.testNotif1.message += " 1";
+ this.notification1 = showNotification(this.testNotif1);
+ this.testNotif1.options.eventCallback = function (eventName) {
+ info("notifyObj1.options.eventCallback: " + eventName);
+ if (eventName == "dismissed") {
+ throw new Error("Oops 1!");
+ }
+ };
+
+ this.testNotif2 = new BasicNotification(this.id);
+ this.testNotif2.message += " 2";
+ this.testNotif2.id += "-2";
+ this.testNotif2.options.eventCallback = function (eventName) {
+ info("notifyObj2.options.eventCallback: " + eventName);
+ if (eventName == "dismissed") {
+ throw new Error("Oops 2!");
+ }
+ };
+ this.notification2 = showNotification(this.testNotif2);
+ },
+ onShown: function (popup) {
+ is(popup.childNodes.length, 2, "two notifications are shown");
+ dismissNotification(popup);
+ },
+ onHidden: function () {
+ this.notification1.remove();
+ this.notification2.remove();
+ }
+ }
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
new file mode 100644
index 000000000..750ad82fd
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
@@ -0,0 +1,294 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+ goNext();
+}
+
+var tests = [
+ // Popup Notifications main actions should catch exceptions from callbacks
+ { id: "Test#1",
+ run: function () {
+ this.testNotif = new ErrorNotification();
+ showNotification(this.testNotif);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.testNotif);
+ triggerMainCommand(popup);
+ },
+ onHidden: function (popup) {
+ ok(this.testNotif.mainActionClicked, "main action has been triggered");
+ }
+ },
+ // Popup Notifications secondary actions should catch exceptions from callbacks
+ { id: "Test#2",
+ run: function () {
+ this.testNotif = new ErrorNotification();
+ showNotification(this.testNotif);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.testNotif);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden: function (popup) {
+ ok(this.testNotif.secondaryActionClicked, "secondary action has been triggered");
+ }
+ },
+ // Existing popup notification shouldn't disappear when adding a dismissed notification
+ { id: "Test#3",
+ run: function () {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notification1 = showNotification(this.notifyObj1);
+ },
+ onShown: function (popup) {
+ // Now show a dismissed notification, and check that it doesn't clobber
+ // the showing one.
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.dismissed = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ checkPopup(popup, this.notifyObj1);
+
+ // check that both anchor icons are showing
+ is(document.getElementById("default-notification-icon").getAttribute("showing"), "true",
+ "notification1 anchor should be visible");
+ is(document.getElementById("geo-notification-icon").getAttribute("showing"), "true",
+ "notification2 anchor should be visible");
+
+ dismissNotification(popup);
+ },
+ onHidden: function(popup) {
+ this.notification1.remove();
+ this.notification2.remove();
+ }
+ },
+ // Showing should be able to modify the popup data
+ { id: "Test#4",
+ run: function() {
+ this.notifyObj = new BasicNotification(this.id);
+ let normalCallback = this.notifyObj.options.eventCallback;
+ this.notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "showing") {
+ this.mainAction.label = "Alternate Label";
+ }
+ normalCallback.call(this, eventName);
+ };
+ showNotification(this.notifyObj);
+ },
+ onShown: function(popup) {
+ // checkPopup checks for the matching label. Note that this assumes that
+ // this.notifyObj.mainAction is the same as notification.mainAction,
+ // which could be a problem if we ever decided to deep-copy.
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden: function() { }
+ },
+ // Moving a tab to a new window should remove non-swappable notifications.
+ { id: "Test#5",
+ run: function() {
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ let notifyObj = new BasicNotification(this.id);
+ showNotification(notifyObj);
+ let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ whenDelayedStartupFinished(win, function() {
+ let anchor = win.document.getElementById("default-notification-icon");
+ win.PopupNotifications._reshowNotifications(anchor);
+ ok(win.PopupNotifications.panel.childNodes.length == 0,
+ "no notification displayed in new window");
+ ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
+ ok(notifyObj.removedCallbackTriggered, "the removed callback was triggered");
+ win.close();
+ goNext();
+ });
+ }
+ },
+ // Moving a tab to a new window should preserve swappable notifications.
+ { id: "Test#6",
+ run: function* () {
+ let originalBrowser = gBrowser.selectedBrowser;
+ let originalWindow = window;
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ let notifyObj = new BasicNotification(this.id);
+ let originalCallback = notifyObj.options.eventCallback;
+ notifyObj.options.eventCallback = function (eventName) {
+ originalCallback(eventName);
+ return eventName == "swapping";
+ };
+
+ let notification = showNotification(notifyObj);
+ let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ yield whenDelayedStartupFinished(win);
+
+ yield new Promise(resolve => {
+ let originalCallback = notification.options.eventCallback;
+ notification.options.eventCallback = function (eventName) {
+ originalCallback(eventName);
+ if (eventName == "shown") {
+ resolve();
+ }
+ };
+ info("Showing the notification again");
+ notification.reshow();
+ });
+
+ checkPopup(win.PopupNotifications.panel, notifyObj);
+ ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
+ yield BrowserTestUtils.closeWindow(win);
+
+ // These are the same checks that PopupNotifications.jsm makes before it
+ // allows a notification to open. Do not go to the next test until we are
+ // sure that its attempt to display a notification will not fail.
+ yield BrowserTestUtils.waitForCondition(() => originalBrowser.docShellIsActive,
+ "The browser should be active");
+ let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
+ yield BrowserTestUtils.waitForCondition(() => fm.activeWindow == originalWindow,
+ "The window should be active")
+
+ goNext();
+ }
+ },
+ // the hideNotNow option
+ { id: "Test#7",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.hideNotNow = true;
+ this.notifyObj.mainAction.dismiss = true;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ // checkPopup verifies that the Not Now item is hidden, and that no separator is added.
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden: function (popup) {
+ this.notification.remove();
+ }
+ },
+ // the main action callback can keep the notification.
+ { id: "Test#8",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction.dismiss = true;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
+ ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
+ this.notification.remove();
+ }
+ },
+ // a secondary action callback can keep the notification.
+ { id: "Test#9",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions[0].dismiss = true;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden: function (popup) {
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
+ ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
+ this.notification.remove();
+ }
+ },
+ // returning true in the showing callback should dismiss the notification.
+ { id: "Test#10",
+ run: function() {
+ let notifyObj = new BasicNotification(this.id);
+ let originalCallback = notifyObj.options.eventCallback;
+ notifyObj.options.eventCallback = function (eventName) {
+ originalCallback(eventName);
+ return eventName == "showing";
+ };
+
+ let notification = showNotification(notifyObj);
+ ok(notifyObj.showingCallbackTriggered, "the showing callback was triggered");
+ ok(!notifyObj.shownCallbackTriggered, "the shown callback wasn't triggered");
+ notification.remove();
+ goNext();
+ }
+ },
+ // panel updates should fire the showing and shown callbacks again.
+ { id: "Test#11",
+ run: function() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+
+ this.notifyObj.showingCallbackTriggered = false;
+ this.notifyObj.shownCallbackTriggered = false;
+
+ // Force an update of the panel. This is typically called
+ // automatically when receiving 'activate' or 'TabSelect' events,
+ // but from a setTimeout, which is inconvenient for the test.
+ PopupNotifications._update();
+
+ checkPopup(popup, this.notifyObj);
+
+ this.notification.remove();
+ },
+ onHidden: function() { }
+ },
+ // A first dismissed notification shouldn't stop _update from showing a second notification
+ { id: "Test#12",
+ run: function () {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.dismissed = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.dismissed = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notification2.dismissed = false;
+ PopupNotifications._update();
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj2);
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ onHidden: function(popup) { }
+ },
+ // The anchor icon should be shown for notifications in background windows.
+ { id: "Test#13",
+ run: function() {
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.dismissed = true;
+ let win = gBrowser.replaceTabWithWindow(gBrowser.addTab("about:blank"));
+ whenDelayedStartupFinished(win, function() {
+ showNotification(notifyObj);
+ let anchor = document.getElementById("default-notification-icon");
+ is(anchor.getAttribute("showing"), "true", "the anchor is shown");
+ win.close();
+ goNext();
+ });
+ }
+ }
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
new file mode 100644
index 000000000..bcc51fcd7
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
@@ -0,0 +1,211 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+ goNext();
+}
+
+function checkCheckbox(checkbox, label, checked=false, hidden=false) {
+ is(checkbox.label, label, "Checkbox should have the correct label");
+ is(checkbox.hidden, hidden, "Checkbox should be shown");
+ is(checkbox.checked, checked, "Checkbox should be checked by default");
+}
+
+function checkMainAction(notification, disabled=false) {
+ let mainAction = notification.button;
+ let warningLabel = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-warning");
+ is(warningLabel.hidden, !disabled, "Warning label should be shown");
+ is(mainAction.disabled, disabled, "MainAction should be disabled");
+}
+
+function promiseElementVisible(element) {
+ // HTMLElement.offsetParent is null when the element is not visisble
+ // (or if the element has |position: fixed|). See:
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
+ return BrowserTestUtils.waitForCondition(() => element.offsetParent !== null,
+ "Waiting for element to be visible");
+}
+
+var gNotification;
+
+var tests = [
+ // Test that passing the checkbox field shows the checkbox.
+ { id: "show_checkbox",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+ checkCheckbox(notification.checkbox, "This is a checkbox");
+ triggerMainCommand(popup);
+ },
+ onHidden: function () { }
+ },
+
+ // Test checkbox being checked by default
+ { id: "checkbox_checked",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "Check this",
+ checked: true,
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown: function (popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+ checkCheckbox(notification.checkbox, "Check this", true);
+ triggerMainCommand(popup);
+ },
+ onHidden: function () { }
+ },
+
+ // Test checkbox passing the checkbox state on mainAction
+ { id: "checkbox_passCheckboxChecked_mainAction",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction.callback = ({checkboxChecked}) => this.mainActionChecked = checkboxChecked;
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown: function* (popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ yield promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ triggerMainCommand(popup);
+ },
+ onHidden: function () {
+ is(this.mainActionChecked, true, "mainAction callback is passed the correct checkbox value");
+ }
+ },
+
+ // Test checkbox passing the checkbox state on secondaryAction
+ { id: "checkbox_passCheckboxChecked_secondaryAction",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions = [{
+ label: "Test Secondary",
+ accessKey: "T",
+ callback: ({checkboxChecked}) => this.secondaryActionChecked = checkboxChecked,
+ }];
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown: function* (popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ yield promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden: function () {
+ is(this.secondaryActionChecked, true, "secondaryAction callback is passed the correct checkbox value");
+ }
+ },
+
+ // Test checkbox preserving its state through re-opening the doorhanger
+ { id: "checkbox_reopen",
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ checkedState: {
+ disableMainAction: true,
+ warningLabel: "Testing disable",
+ },
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown: function* (popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ yield promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ dismissNotification(popup);
+ },
+ onHidden: function* (popup) {
+ let icon = document.getElementById("default-notification-icon");
+ let shown = waitForNotificationPanel();
+ EventUtils.synthesizeMouseAtCenter(icon, {});
+ yield shown;
+ let notification = popup.childNodes[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ checkMainAction(notification, true);
+ gNotification.remove();
+ }
+ },
+];
+
+// Test checkbox disabling the main action in different combinations
+["checkedState", "uncheckedState"].forEach(function (state) {
+ [true, false].forEach(function (checked) {
+ tests.push(
+ { id: `checkbox_disableMainAction_${state}_${checked ? 'checked' : 'unchecked'}`,
+ run: function () {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ checked: checked,
+ [state]: {
+ disableMainAction: true,
+ warningLabel: "Testing disable",
+ },
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown: function* (popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.childNodes[0];
+ let checkbox = notification.checkbox;
+ let disabled = (state === "checkedState" && checked) ||
+ (state === "uncheckedState" && !checked);
+
+ checkCheckbox(checkbox, "This is a checkbox", checked);
+ checkMainAction(notification, disabled);
+ yield promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", !checked);
+ checkMainAction(notification, !disabled);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", checked);
+ checkMainAction(notification, disabled);
+
+ // Unblock the main command if it's currently disabled.
+ if (disabled) {
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ }
+ triggerMainCommand(popup);
+ },
+ onHidden: function () { }
+ }
+ );
+ });
+});
+
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
new file mode 100644
index 000000000..0f5b57ced
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Test that for persistent notifications,
+ // the secondary action is triggered by pressing the escape key.
+ { id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.persistent = true;
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
+ ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ }
+ },
+ // Test that for non-persistent notifications, the escape key dismisses the notification.
+ { id: "Test#2",
+ *run() {
+ yield waitForWindowReadyForPopupNotifications(window);
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(!this.notifyObj.secondaryActionClicked, "secondaryAction was not clicked");
+ ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
+ ok(!this.notifyObj.removedCallbackTriggered, "removed callback was not triggered");
+ this.notification.remove();
+ }
+ },
+ // Test that the space key on an anchor element focuses an active notification
+ { id: "Test#3",
+ *run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ persistent: true
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ *onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let anchor = document.getElementById(this.notifyObj.anchorID);
+ anchor.focus();
+ is(document.activeElement, anchor);
+ EventUtils.synthesizeKey(" ", {});
+ is(document.activeElement, popup.childNodes[0].button);
+ this.notification.remove();
+ },
+ onHidden(popup) { }
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_reshow_in_background.js b/browser/base/content/test/popupNotifications/browser_reshow_in_background.js
new file mode 100644
index 000000000..6f415f62e
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_reshow_in_background.js
@@ -0,0 +1,52 @@
+"use strict";
+
+/**
+ * Tests that when PopupNotifications for background tabs are reshown, they
+ * don't show up in the foreground tab, but only in the background tab that
+ * they belong to.
+ */
+add_task(function* test_background_notifications_dont_reshow_in_foreground() {
+ // Our initial tab will be A. Let's open two more tabs B and C, but keep
+ // A selected. Then, we'll trigger a PopupNotification in C, and then make
+ // it reshow.
+ let tabB = gBrowser.addTab("about:blank");
+ let tabC = gBrowser.addTab("about:blank");
+
+ let seenEvents = [];
+
+ let options = {
+ dismissed: false,
+ eventCallback(popupEvent) {
+ seenEvents.push(popupEvent);
+ },
+ };
+
+ let notification =
+ PopupNotifications.show(tabC.linkedBrowser, "test-notification",
+ "", "plugins-notification-icon",
+ null, null, options);
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ yield BrowserTestUtils.switchTab(gBrowser, tabB);
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ notification.reshow();
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ let panelShown =
+ BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ yield BrowserTestUtils.switchTab(gBrowser, tabC);
+ yield panelShown;
+
+ Assert.equal(seenEvents.length, 2, "Should have seen two events.");
+ Assert.equal(seenEvents[0], "showing", "Should have said popup was showing.");
+ Assert.equal(seenEvents[1], "shown", "Should have said popup was shown.");
+
+ let panelHidden =
+ BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden");
+ PopupNotifications.remove(notification);
+ yield panelHidden;
+
+ yield BrowserTestUtils.removeTab(tabB);
+ yield BrowserTestUtils.removeTab(tabC);
+});
diff --git a/browser/base/content/test/popupNotifications/head.js b/browser/base/content/test/popupNotifications/head.js
new file mode 100644
index 000000000..4a803d6af
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -0,0 +1,303 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ return new Promise(resolve => {
+ info("Waiting for delayed startup to finish");
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ if (aCallback) {
+ executeSoon(aCallback);
+ }
+ resolve();
+ }
+ }, "browser-delayed-startup-finished", false);
+ });
+}
+
+/**
+ * Allows waiting for an observer notification once.
+ *
+ * @param topic
+ * Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves The array [subject, data] from the observed notification.
+ * @rejects Never.
+ */
+function promiseTopicObserved(topic)
+{
+ let deferred = Promise.defer();
+ info("Waiting for observer topic " + topic);
+ Services.obs.addObserver(function PTO_observe(subject, topic, data) {
+ Services.obs.removeObserver(PTO_observe, topic);
+ deferred.resolve([subject, data]);
+ }, topic, false);
+ return deferred.promise;
+}
+
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url)
+{
+ let browser = tab.linkedBrowser;
+
+ if (url) {
+ browser.loadURI(url);
+ }
+
+ return BrowserTestUtils.browserLoaded(browser, false, url);
+}
+
+const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref("security.notification_enable_delay");
+
+function setup() {
+ // Disable transitions as they slow the test down and we want to click the
+ // mouse buttons in a predictable location.
+
+ registerCleanupFunction(() => {
+ PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
+ });
+}
+
+function goNext() {
+ executeSoon(() => executeSoon(Task.async(runNextTest)));
+}
+
+function* runNextTest() {
+ if (tests.length == 0) {
+ executeSoon(finish);
+ return;
+ }
+
+ let nextTest = tests.shift();
+ if (nextTest.onShown) {
+ let shownState = false;
+ onPopupEvent("popupshowing", function () {
+ info("[" + nextTest.id + "] popup showing");
+ });
+ onPopupEvent("popupshown", function () {
+ shownState = true;
+ info("[" + nextTest.id + "] popup shown");
+ Task.spawn(() => nextTest.onShown(this))
+ .then(undefined, ex => Assert.ok(false, "onShown failed: " + ex));
+ });
+ onPopupEvent("popuphidden", function () {
+ info("[" + nextTest.id + "] popup hidden");
+ Task.spawn(() => nextTest.onHidden(this))
+ .then(() => goNext(), ex => Assert.ok(false, "onHidden failed: " + ex));
+ }, () => shownState);
+ info("[" + nextTest.id + "] added listeners; panel is open: " + PopupNotifications.isPanelOpen);
+ }
+
+ info("[" + nextTest.id + "] running test");
+ yield nextTest.run();
+}
+
+function showNotification(notifyObj) {
+ info("Showing notification " + notifyObj.id);
+ return PopupNotifications.show(notifyObj.browser,
+ notifyObj.id,
+ notifyObj.message,
+ notifyObj.anchorID,
+ notifyObj.mainAction,
+ notifyObj.secondaryActions,
+ notifyObj.options);
+}
+
+function dismissNotification(popup) {
+ info("Dismissing notification " + popup.childNodes[0].id);
+ executeSoon(() => EventUtils.synthesizeKey("VK_ESCAPE", {}));
+}
+
+function BasicNotification(testId) {
+ this.browser = gBrowser.selectedBrowser;
+ this.id = "test-notification-" + testId;
+ this.message = "This is popup notification for " + testId;
+ this.anchorID = null;
+ this.mainAction = {
+ label: "Main Action",
+ accessKey: "M",
+ callback: () => this.mainActionClicked = true
+ };
+ this.secondaryActions = [
+ {
+ label: "Secondary Action",
+ accessKey: "S",
+ callback: () => this.secondaryActionClicked = true
+ }
+ ];
+ this.options = {
+ eventCallback: eventName => {
+ switch (eventName) {
+ case "dismissed":
+ this.dismissalCallbackTriggered = true;
+ break;
+ case "showing":
+ this.showingCallbackTriggered = true;
+ break;
+ case "shown":
+ this.shownCallbackTriggered = true;
+ break;
+ case "removed":
+ this.removedCallbackTriggered = true;
+ break;
+ case "swapping":
+ this.swappingCallbackTriggered = true;
+ break;
+ }
+ }
+ };
+}
+
+BasicNotification.prototype.addOptions = function(options) {
+ for (let [name, value] of Object.entries(options))
+ this.options[name] = value;
+};
+
+function ErrorNotification() {
+ this.mainAction.callback = () => {
+ this.mainActionClicked = true;
+ throw new Error("Oops!");
+ };
+ this.secondaryActions[0].callback = () => {
+ this.secondaryActionClicked = true;
+ throw new Error("Oops!");
+ };
+}
+
+ErrorNotification.prototype = new BasicNotification();
+ErrorNotification.prototype.constructor = ErrorNotification;
+
+function checkPopup(popup, notifyObj) {
+ info("Checking notification " + notifyObj.id);
+
+ ok(notifyObj.showingCallbackTriggered, "showing callback was triggered");
+ ok(notifyObj.shownCallbackTriggered, "shown callback was triggered");
+
+ let notifications = popup.childNodes;
+ is(notifications.length, 1, "one notification displayed");
+ let notification = notifications[0];
+ if (!notification)
+ return;
+ let icon = document.getAnonymousElementByAttribute(notification, "class",
+ "popup-notification-icon");
+ if (notifyObj.id == "geolocation") {
+ isnot(icon.boxObject.width, 0, "icon for geo displayed");
+ ok(popup.anchorNode.classList.contains("notification-anchor-icon"),
+ "notification anchored to icon");
+ }
+ is(notification.getAttribute("label"), notifyObj.message, "message matches");
+ is(notification.id, notifyObj.id + "-notification", "id matches");
+ if (notifyObj.mainAction) {
+ is(notification.getAttribute("buttonlabel"), notifyObj.mainAction.label,
+ "main action label matches");
+ is(notification.getAttribute("buttonaccesskey"),
+ notifyObj.mainAction.accessKey, "main action accesskey matches");
+ }
+ let actualSecondaryActions =
+ Array.filter(notification.childNodes, child => child.nodeName == "menuitem");
+ let secondaryActions = notifyObj.secondaryActions || [];
+ let actualSecondaryActionsCount = actualSecondaryActions.length;
+ if (notifyObj.options.hideNotNow) {
+ is(notification.getAttribute("hidenotnow"), "true", "'Not Now' item hidden");
+ if (secondaryActions.length)
+ is(notification.lastChild.tagName, "menuitem", "no menuseparator");
+ }
+ else if (secondaryActions.length) {
+ is(notification.lastChild.tagName, "menuseparator", "menuseparator exists");
+ }
+ is(actualSecondaryActionsCount, secondaryActions.length,
+ actualSecondaryActions.length + " secondary actions");
+ secondaryActions.forEach(function (a, i) {
+ is(actualSecondaryActions[i].getAttribute("label"), a.label,
+ "label for secondary action " + i + " matches");
+ is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey,
+ "accessKey for secondary action " + i + " matches");
+ });
+}
+
+XPCOMUtils.defineLazyGetter(this, "gActiveListeners", () => {
+ let listeners = new Map();
+ registerCleanupFunction(() => {
+ for (let [listener, eventName] of listeners) {
+ PopupNotifications.panel.removeEventListener(eventName, listener, false);
+ }
+ });
+ return listeners;
+});
+
+function onPopupEvent(eventName, callback, condition) {
+ let listener = event => {
+ if (event.target != PopupNotifications.panel ||
+ (condition && !condition()))
+ return;
+ PopupNotifications.panel.removeEventListener(eventName, listener, false);
+ gActiveListeners.delete(listener);
+ executeSoon(() => callback.call(PopupNotifications.panel));
+ }
+ gActiveListeners.set(listener, eventName);
+ PopupNotifications.panel.addEventListener(eventName, listener, false);
+}
+
+function waitForNotificationPanel() {
+ return new Promise(resolve => {
+ onPopupEvent("popupshown", function() {
+ resolve(this);
+ });
+ });
+}
+
+function triggerMainCommand(popup) {
+ let notifications = popup.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ let notification = notifications[0];
+ info("Triggering main command for notification " + notification.id);
+ // 20, 10 so that the inner button is hit
+ EventUtils.synthesizeMouse(notification.button, 20, 10, {});
+}
+
+function triggerSecondaryCommand(popup, index) {
+ let notifications = popup.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ let notification = notifications[0];
+ info("Triggering secondary command for notification " + notification.id);
+ // Cancel the arrow panel slide-in transition (bug 767133) such that
+ // it won't interfere with us interacting with the dropdown.
+ document.getAnonymousNodes(popup)[0].style.transition = "none";
+
+ notification.button.focus();
+
+ popup.addEventListener("popupshown", function handle() {
+ popup.removeEventListener("popupshown", handle, false);
+ info("Command popup open for notification " + notification.id);
+ // Press down until the desired command is selected
+ for (let i = 0; i <= index; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ }
+ // Activate
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }, false);
+
+ // One down event to open the popup
+ info("Open the popup to trigger secondary command for notification " + notification.id);
+ EventUtils.synthesizeKey("VK_DOWN", { altKey: !navigator.platform.includes("Mac") });
+}
diff --git a/browser/base/content/test/popups/browser.ini b/browser/base/content/test/popups/browser.ini
new file mode 100644
index 000000000..46a32783b
--- /dev/null
+++ b/browser/base/content/test/popups/browser.ini
@@ -0,0 +1,4 @@
+[browser_popupUI.js]
+[browser_popup_blocker.js]
+support-files = popup_blocker.html
+skip-if = (os == 'linux') || (e10s && debug) # Frequent bug 1081925 and bug 1125520 failures
diff --git a/browser/base/content/test/popups/browser_popupUI.js b/browser/base/content/test/popups/browser_popupUI.js
new file mode 100644
index 000000000..7c6805f60
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popupUI.js
@@ -0,0 +1,37 @@
+function test() {
+ waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({ set: [[ "dom.disable_open_during_load", false ]] });
+
+ let popupOpened = BrowserTestUtils.waitForNewWindow(true, "about:blank");
+ BrowserTestUtils.openNewForegroundTab(gBrowser,
+ "data:text/html,<html><script>popup=open('about:blank','','width=300,height=200')</script>"
+ );
+ popupOpened.then((win) => testPopupUI(win));
+}
+
+function testPopupUI(win) {
+ var doc = win.document;
+
+ ok(win.gURLBar, "location bar exists in the popup");
+ isnot(win.gURLBar.clientWidth, 0, "location bar is visible in the popup");
+ ok(win.gURLBar.readOnly, "location bar is read-only in the popup");
+ isnot(doc.getElementById("Browser:OpenLocation").getAttribute("disabled"), "true",
+ "'open location' command is not disabled in the popup");
+
+ let historyButton = doc.getAnonymousElementByAttribute(win.gURLBar, "anonid",
+ "historydropmarker");
+ is(historyButton.clientWidth, 0, "history dropdown button is hidden in the popup");
+
+ EventUtils.synthesizeKey("t", { accelKey: true }, win);
+ is(win.gBrowser.browsers.length, 1, "Accel+T doesn't open a new tab in the popup");
+ is(gBrowser.browsers.length, 3, "Accel+T opened a new tab in the parent window");
+ gBrowser.removeCurrentTab();
+
+ EventUtils.synthesizeKey("w", { accelKey: true }, win);
+ ok(win.closed, "Accel+W closes the popup");
+
+ if (!win.closed)
+ win.close();
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/base/content/test/popups/browser_popup_blocker.js b/browser/base/content/test/popups/browser_popup_blocker.js
new file mode 100644
index 000000000..8cadfea57
--- /dev/null
+++ b/browser/base/content/test/popups/browser_popup_blocker.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/. */
+
+const baseURL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+
+function clearAllPermissionsByPrefix(aPrefix) {
+ let perms = Services.perms.enumerator;
+ while (perms.hasMoreElements()) {
+ let perm = perms.getNext();
+ if (perm.type.startsWith(aPrefix)) {
+ Services.perms.removePermission(perm);
+ }
+ }
+}
+
+add_task(function* test_opening_blocked_popups() {
+ // Enable the popup blocker.
+ yield SpecialPowers.pushPrefEnv({set: [["dom.disable_open_during_load", true]]});
+
+ // Open the test page.
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, baseURL + "popup_blocker.html");
+
+ // Wait for the popup-blocked notification.
+ let notification;
+ yield BrowserTestUtils.waitForCondition(() =>
+ notification = gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked"));
+
+ // Show the menu.
+ let popupShown = BrowserTestUtils.waitForEvent(window, "popupshown");
+ let popupFilled = BrowserTestUtils.waitForMessage(gBrowser.selectedBrowser.messageManager,
+ "PopupBlocking:ReplyGetBlockedPopupList");
+ notification.querySelector("button").doCommand();
+ let popup_event = yield popupShown;
+ let menu = popup_event.target;
+ is(menu.id, "blockedPopupOptions", "Blocked popup menu shown");
+
+ yield popupFilled;
+ // The menu is filled on the same message that we waited for, so let's ensure that it
+ // had a chance of running before this test code.
+ yield new Promise(resolve => executeSoon(resolve));
+
+ // Check the menu contents.
+ let sep = menu.querySelector("menuseparator");
+ let popupCount = 0;
+ for (let i = sep.nextElementSibling; i; i = i.nextElementSibling) {
+ popupCount++;
+ }
+ is(popupCount, 2, "Two popups were blocked");
+
+ // Pressing "allow" should open all blocked popups.
+ let popupTabs = [];
+ function onTabOpen(event) {
+ popupTabs.push(event.target);
+ }
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen);
+
+ // Press the button.
+ let allow = menu.querySelector("[observes='blockedPopupAllowSite']");
+ allow.doCommand();
+ yield BrowserTestUtils.waitForCondition(() =>
+ popupTabs.length == 2 &&
+ popupTabs.every(aTab => aTab.linkedBrowser.currentURI.spec != "about:blank"));
+
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen);
+
+ is(popupTabs[0].linkedBrowser.currentURI.spec, "data:text/plain;charset=utf-8,a", "Popup a");
+ is(popupTabs[1].linkedBrowser.currentURI.spec, "data:text/plain;charset=utf-8,b", "Popup b");
+
+ // Clean up.
+ gBrowser.removeTab(tab);
+ for (let popup of popupTabs) {
+ gBrowser.removeTab(popup);
+ }
+ clearAllPermissionsByPrefix("popup");
+ // Ensure the menu closes.
+ menu.hidePopup();
+});
+
+add_task(function* check_icon_hides() {
+ // Enable the popup blocker.
+ yield SpecialPowers.pushPrefEnv({set: [["dom.disable_open_during_load", true]]});
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, baseURL + "popup_blocker.html");
+
+ let button = document.getElementById("page-report-button");
+ yield BrowserTestUtils.waitForCondition(() =>
+ gBrowser.getNotificationBox().getNotificationWithValue("popup-blocked"));
+ ok(!button.hidden, "Button should be visible");
+
+ let otherPageLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ openLinkIn(baseURL, "current", {});
+ yield otherPageLoaded;
+ ok(button.hidden, "Button should have hidden again after another page loaded.");
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/popups/popup_blocker.html b/browser/base/content/test/popups/popup_blocker.html
new file mode 100644
index 000000000..6e2b7db15
--- /dev/null
+++ b/browser/base/content/test/popups/popup_blocker.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page creating two popups</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ window.open("data:text/plain;charset=utf-8,a", "a");
+ window.open("data:text/plain;charset=utf-8,b", "b");
+ </script>
+ </body>
+</html>
diff --git a/browser/base/content/test/referrer/.eslintrc.js b/browser/base/content/test/referrer/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/referrer/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/referrer/browser.ini b/browser/base/content/test/referrer/browser.ini
new file mode 100644
index 000000000..13b712850
--- /dev/null
+++ b/browser/base/content/test/referrer/browser.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+support-files =
+ file_referrer_policyserver.sjs
+ file_referrer_policyserver_attr.sjs
+ file_referrer_testserver.sjs
+ head.js
+
+[browser_referrer_middle_click.js]
+[browser_referrer_middle_click_in_container.js]
+[browser_referrer_open_link_in_private.js]
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_open_link_in_tab.js]
+skip-if = os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_window.js]
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_open_link_in_window_in_container.js]
+skip-if = os == 'linux' # Bug 1145199
+[browser_referrer_simple_click.js]
+[browser_referrer_open_link_in_container_tab.js]
+skip-if = os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_container_tab2.js]
+skip-if = os == 'linux' # Bug 1144816
+[browser_referrer_open_link_in_container_tab3.js]
+skip-if = os == 'linux' # Bug 1144816
diff --git a/browser/base/content/test/referrer/browser_referrer_middle_click.js b/browser/base/content/test/referrer/browser_referrer_middle_click.js
new file mode 100644
index 000000000..e6e01c6a3
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_middle_click.js
@@ -0,0 +1,20 @@
+// Tests referrer on middle-click navigation.
+// Middle-clicks on the link, which opens it in a new tab.
+
+function startMiddleClickTestCase(aTestNumber) {
+ info("browser_referrer_middle_click: " +
+ getReferrerTestDescription(aTestNumber));
+ someTabLoaded(gTestWindow).then(function(aNewTab) {
+ BrowserTestUtils.switchTab(gTestWindow.gBrowser, aNewTab).then(() => {
+ checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
+ startMiddleClickTestCase);
+ });
+ });
+
+ clickTheLink(gTestWindow, "testlink", {button: 1});
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startMiddleClickTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js b/browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js
new file mode 100644
index 000000000..e89b891f3
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js
@@ -0,0 +1,27 @@
+// Tests referrer on middle-click navigation.
+// Middle-clicks on the link, which opens it in a new tab, same container.
+
+function startMiddleClickTestCase(aTestNumber) {
+ info("browser_referrer_middle_click: " +
+ getReferrerTestDescription(aTestNumber));
+ someTabLoaded(gTestWindow).then(function(aNewTab) {
+ BrowserTestUtils.switchTab(gTestWindow.gBrowser, aNewTab).then(() => {
+ checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
+ startMiddleClickTestCase,
+ { userContextId: 3 });
+ });
+ });
+
+ clickTheLink(gTestWindow, "testlink", {button: 1});
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ {set: [["privacy.userContext.enabled", true]]},
+ function() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startMiddleClickTestCase, { userContextId: 3 });
+ });
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab.js
new file mode 100644
index 000000000..deaf90fb9
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab.js
@@ -0,0 +1,59 @@
+// Tests referrer on context menu navigation - open link in new container tab.
+// Selects "open link in new container tab" from the context menu.
+
+function getReferrerTest(aTestNumber) {
+ let test = _referrerTests[aTestNumber];
+ if (test) {
+ // We want all the referrer tests to fail!
+ test.result = "";
+ }
+
+ return test;
+}
+
+function startNewTabTestCase(aTestNumber) {
+ info("browser_referrer_open_link_in_container_tab: " +
+ getReferrerTestDescription(aTestNumber));
+ contextMenuOpened(gTestWindow, "testlink").then(function(aContextMenu) {
+ someTabLoaded(gTestWindow).then(function(aNewTab) {
+ gTestWindow.gBrowser.selectedTab = aNewTab;
+
+ checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
+ startNewTabTestCase);
+ });
+
+ let menu = gTestWindow.document.getElementById("context-openlinkinusercontext-menu");
+
+ let menupopup = menu.menupopup;
+ menu.addEventListener("popupshown", function onPopupShown() {
+ menu.removeEventListener("popupshown", onPopupShown);
+
+ is(menupopup.nodeType, Node.ELEMENT_NODE, "We have a menupopup.");
+ ok(menupopup.firstChild, "We have a first container entry.");
+
+ let firstContext = menupopup.firstChild;
+ is(firstContext.nodeType, Node.ELEMENT_NODE, "We have a first container entry.");
+ ok(firstContext.hasAttribute("data-usercontextid"), "We have a usercontextid value.");
+
+ aContextMenu.addEventListener("popuphidden", function onPopupHidden() {
+ aContextMenu.removeEventListener("popuphidden", onPopupHidden);
+ firstContext.doCommand();
+ });
+
+ aContextMenu.hidePopup();
+ });
+
+ menupopup.showPopup();
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ {set: [["privacy.userContext.enabled", true]]},
+ function() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewTabTestCase);
+ });
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab2.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab2.js
new file mode 100644
index 000000000..77a5645c6
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab2.js
@@ -0,0 +1,31 @@
+// Tests referrer on context menu navigation - open link in new container tab.
+// Selects "open link in new container tab" from the context menu.
+
+// The test runs from a container ID 1.
+// Output: we have the correct referrer policy applied.
+
+function startNewTabTestCase(aTestNumber) {
+ info("browser_referrer_open_link_in_container_tab: " +
+ getReferrerTestDescription(aTestNumber));
+ contextMenuOpened(gTestWindow, "testlink").then(function(aContextMenu) {
+ someTabLoaded(gTestWindow).then(function(aNewTab) {
+ gTestWindow.gBrowser.selectedTab = aNewTab;
+
+ checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
+ startNewTabTestCase, { userContextId: 1 });
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlinkincontainertab");
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ {set: [["privacy.userContext.enabled", true]]},
+ function() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewTabTestCase, { userContextId: 1 });
+ });
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab3.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab3.js
new file mode 100644
index 000000000..c0a73d828
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_container_tab3.js
@@ -0,0 +1,63 @@
+// Tests referrer on context menu navigation - open link in new container tab.
+// Selects "open link in new container tab" from the context menu.
+
+// The test runs from a container ID 2.
+// Output: we have no referrer.
+
+function getReferrerTest(aTestNumber) {
+ let test = _referrerTests[aTestNumber];
+ if (test) {
+ // We want all the referrer tests to fail!
+ test.result = "";
+ }
+
+ return test;
+}
+
+function startNewTabTestCase(aTestNumber) {
+ info("browser_referrer_open_link_in_container_tab: " +
+ getReferrerTestDescription(aTestNumber));
+ contextMenuOpened(gTestWindow, "testlink").then(function(aContextMenu) {
+ someTabLoaded(gTestWindow).then(function(aNewTab) {
+ gTestWindow.gBrowser.selectedTab = aNewTab;
+
+ checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
+ startNewTabTestCase, { userContextId: 2 });
+ });
+
+ let menu = gTestWindow.document.getElementById("context-openlinkinusercontext-menu");
+
+ let menupopup = menu.menupopup;
+ menu.addEventListener("popupshown", function onPopupShown() {
+ menu.removeEventListener("popupshown", onPopupShown);
+
+ is(menupopup.nodeType, Node.ELEMENT_NODE, "We have a menupopup.");
+ ok(menupopup.firstChild, "We have a first container entry.");
+
+ let firstContext = menupopup.firstChild;
+ is(firstContext.nodeType, Node.ELEMENT_NODE, "We have a first container entry.");
+ ok(firstContext.hasAttribute("data-usercontextid"), "We have a usercontextid value.");
+ is("0", firstContext.getAttribute("data-usercontextid"), "We have the right usercontextid value.");
+
+ aContextMenu.addEventListener("popuphidden", function onPopupHidden() {
+ aContextMenu.removeEventListener("popuphidden", onPopupHidden);
+ firstContext.doCommand();
+ });
+
+ aContextMenu.hidePopup();
+ });
+
+ menupopup.showPopup();
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ {set: [["privacy.userContext.enabled", true]]},
+ function() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewTabTestCase, { userContextId: 2 });
+ });
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_private.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_private.js
new file mode 100644
index 000000000..8f12e3824
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_private.js
@@ -0,0 +1,22 @@
+// Tests referrer on context menu navigation - open link in new private window.
+// Selects "open link in new private window" from the context menu.
+
+function startNewPrivateWindowTestCase(aTestNumber) {
+ info("browser_referrer_open_link_in_private: " +
+ getReferrerTestDescription(aTestNumber));
+ contextMenuOpened(gTestWindow, "testlink").then(function(aContextMenu) {
+ newWindowOpened().then(function(aNewWindow) {
+ BrowserTestUtils.firstBrowserLoaded(aNewWindow, false).then(function() {
+ checkReferrerAndStartNextTest(aTestNumber, aNewWindow, null,
+ startNewPrivateWindowTestCase);
+ });
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlinkprivate");
+ });
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewPrivateWindowTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js
new file mode 100644
index 000000000..03119cb57
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js
@@ -0,0 +1,21 @@
+// Tests referrer on context menu navigation - open link in new tab.
+// Selects "open link in new tab" from the context menu.
+
+function startNewTabTestCase(aTestNumber) {
+ info("browser_referrer_open_link_in_tab: " +
+ getReferrerTestDescription(aTestNumber));
+ contextMenuOpened(gTestWindow, "testlink").then(function(aContextMenu) {
+ someTabLoaded(gTestWindow).then(function(aNewTab) {
+ gTestWindow.gBrowser.selectedTab = aNewTab;
+ checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
+ startNewTabTestCase);
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlinkintab");
+ });
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewTabTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_window.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_window.js
new file mode 100644
index 000000000..81e7b2648
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_window.js
@@ -0,0 +1,22 @@
+// Tests referrer on context menu navigation - open link in new window.
+// Selects "open link in new window" from the context menu.
+
+function startNewWindowTestCase(aTestNumber) {
+ info("browser_referrer_open_link_in_window: " +
+ getReferrerTestDescription(aTestNumber));
+ contextMenuOpened(gTestWindow, "testlink").then(function(aContextMenu) {
+ newWindowOpened().then(function(aNewWindow) {
+ BrowserTestUtils.firstBrowserLoaded(aNewWindow, false).then(function() {
+ checkReferrerAndStartNextTest(aTestNumber, aNewWindow, null,
+ startNewWindowTestCase);
+ });
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlink");
+ });
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewWindowTestCase);
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_open_link_in_window_in_container.js b/browser/base/content/test/referrer/browser_referrer_open_link_in_window_in_container.js
new file mode 100644
index 000000000..d5ce87952
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_window_in_container.js
@@ -0,0 +1,32 @@
+// Tests referrer on context menu navigation - open link in new window.
+// Selects "open link in new window" from the context menu.
+
+// This test runs from a container tab. The new tab/window will be loaded in
+// the same container.
+
+function startNewWindowTestCase(aTestNumber) {
+ info("browser_referrer_open_link_in_window: " +
+ getReferrerTestDescription(aTestNumber));
+ contextMenuOpened(gTestWindow, "testlink").then(function(aContextMenu) {
+ newWindowOpened().then(function(aNewWindow) {
+ BrowserTestUtils.firstBrowserLoaded(aNewWindow, false).then(function() {
+ checkReferrerAndStartNextTest(aTestNumber, aNewWindow, null,
+ startNewWindowTestCase,
+ { userContextId: 1 });
+ });
+ });
+
+ doContextMenuCommand(gTestWindow, aContextMenu, "context-openlink");
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv(
+ {set: [["privacy.userContext.enabled", true]]},
+ function() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startNewWindowTestCase, { userContextId: 1 });
+ });
+}
diff --git a/browser/base/content/test/referrer/browser_referrer_simple_click.js b/browser/base/content/test/referrer/browser_referrer_simple_click.js
new file mode 100644
index 000000000..7f3784e64
--- /dev/null
+++ b/browser/base/content/test/referrer/browser_referrer_simple_click.js
@@ -0,0 +1,20 @@
+// Tests referrer on simple click navigation.
+// Clicks on the link, which opens it in the same tab.
+
+function startSimpleClickTestCase(aTestNumber) {
+ info("browser_referrer_simple_click: " +
+ getReferrerTestDescription(aTestNumber));
+ BrowserTestUtils.browserLoaded(gTestWindow.gBrowser.selectedBrowser, false,
+ (url) => url.endsWith("file_referrer_testserver.sjs"))
+ .then(function() {
+ checkReferrerAndStartNextTest(aTestNumber, null, null,
+ startSimpleClickTestCase);
+ });
+
+ clickTheLink(gTestWindow, "testlink", {});
+}
+
+function test() {
+ requestLongerTimeout(10); // slowwww shutdown on e10s
+ startReferrerTest(startSimpleClickTestCase);
+}
diff --git a/browser/base/content/test/referrer/file_referrer_policyserver.sjs b/browser/base/content/test/referrer/file_referrer_policyserver.sjs
new file mode 100644
index 000000000..e07965675
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_policyserver.sjs
@@ -0,0 +1,37 @@
+/**
+ * Renders a link with the provided referrer policy.
+ * Used in browser_referrer_*.js, bug 1113431.
+ * Arguments: ?scheme=http://&policy=origin&rel=noreferrer
+ */
+function handleRequest(request, response)
+{
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let scheme = query.get("scheme");
+ let policy = query.get("policy");
+ let rel = query.get("rel");
+
+ let linkUrl = scheme +
+ "test1.example.com/browser/browser/base/content/test/referrer/" +
+ "file_referrer_testserver.sjs";
+ let metaReferrerTag =
+ policy ? `<meta name='referrer' content='${policy}'>` : "";
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ ${metaReferrerTag}
+ <title>Test referrer</title>
+ </head>
+ <body>
+ <a id='testlink' href='${linkUrl}' ${rel ? ` rel='${rel}'` : ""}>
+ referrer test link</a>
+ </body>
+ </html>`;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs b/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs
new file mode 100644
index 000000000..25a58188a
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_policyserver_attr.sjs
@@ -0,0 +1,36 @@
+/**
+ * Renders a link with the provided referrer policy.
+ * Used in browser_referrer_*.js, bug 1113431.
+ * Arguments: ?scheme=http://&policy=origin&rel=noreferrer
+ */
+function handleRequest(request, response)
+{
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ let scheme = query.get("scheme");
+ let policy = query.get("policy");
+ let rel = query.get("rel");
+
+ let linkUrl = scheme +
+ "test1.example.com/browser/browser/base/content/test/referrer/" +
+ "file_referrer_testserver.sjs";
+ let referrerPolicy =
+ policy ? `referrerpolicy="${policy}"` : "";
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test referrer</title>
+ </head>
+ <body>
+ <a id='testlink' href='${linkUrl}' ${referrerPolicy} ${rel ? ` rel='${rel}'` : ""}>
+ referrer test link</a>
+ </body>
+ </html>`;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/referrer/file_referrer_testserver.sjs b/browser/base/content/test/referrer/file_referrer_testserver.sjs
new file mode 100644
index 000000000..0cfc53b2c
--- /dev/null
+++ b/browser/base/content/test/referrer/file_referrer_testserver.sjs
@@ -0,0 +1,31 @@
+/**
+ * Renders the HTTP Referer header up to the second path slash.
+ * Used in browser_referrer_*.js, bug 1113431.
+ */
+function handleRequest(request, response)
+{
+ let referrer = "";
+ try {
+ referrer = request.getHeader("referer");
+ } catch (e) {
+ referrer = "";
+ }
+
+ // Strip it past the first path slash. Makes tests easier to read.
+ referrer = referrer.split("/").slice(0, 4).join("/");
+
+ let html = `<!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test referrer</title>
+ </head>
+ <body>
+ <div id='testdiv'>${referrer}</div>
+ </body>
+ </html>`;
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(html);
+}
diff --git a/browser/base/content/test/referrer/head.js b/browser/base/content/test/referrer/head.js
new file mode 100644
index 000000000..1a5d5b051
--- /dev/null
+++ b/browser/base/content/test/referrer/head.js
@@ -0,0 +1,265 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserTestUtils",
+ "resource://testing-common/BrowserTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ContentTask",
+ "resource://testing-common/ContentTask.jsm");
+
+const REFERRER_URL_BASE = "/browser/browser/base/content/test/referrer/";
+const REFERRER_POLICYSERVER_URL =
+ "test1.example.com" + REFERRER_URL_BASE + "file_referrer_policyserver.sjs";
+const REFERRER_POLICYSERVER_URL_ATTRIBUTE =
+ "test1.example.com" + REFERRER_URL_BASE + "file_referrer_policyserver_attr.sjs";
+
+SpecialPowers.pushPrefEnv({"set": [['network.http.enablePerElementReferrer', true]]});
+
+var gTestWindow = null;
+var rounds = 0;
+
+// We test that the UI code propagates three pieces of state - the referrer URI
+// itself, the referrer policy, and the triggering principal. After that, we
+// trust nsIWebNavigation to do the right thing with the info it's given, which
+// is covered more exhaustively by dom/base/test/test_bug704320.html (which is
+// a faster content-only test). So, here, we limit ourselves to cases that
+// would break when the UI code drops either of these pieces; we don't try to
+// duplicate the entire cross-product test in bug 704320 - that would be slow,
+// especially when we're opening a new window for each case.
+var _referrerTests = [
+ // 1. Normal cases - no referrer policy, no special attributes.
+ // We expect a full referrer normally, and no referrer on downgrade.
+ {
+ fromScheme: "http://",
+ toScheme: "http://",
+ result: "http://test1.example.com/browser" // full referrer
+ },
+ {
+ fromScheme: "https://",
+ toScheme: "http://",
+ result: "" // no referrer when downgrade
+ },
+ // 2. Origin referrer policy - we expect an origin referrer,
+ // even on downgrade. But rel=noreferrer trumps this.
+ {
+ fromScheme: "https://",
+ toScheme: "http://",
+ policy: "origin",
+ result: "https://test1.example.com/" // origin, even on downgrade
+ },
+ {
+ fromScheme: "https://",
+ toScheme: "http://",
+ policy: "origin",
+ rel: "noreferrer",
+ result: "" // rel=noreferrer trumps meta-referrer
+ },
+ // 3. XXX: using no-referrer here until we support all attribute values (bug 1178337)
+ // Origin-when-cross-origin policy - this depends on the triggering
+ // principal. We expect full referrer for same-origin requests,
+ // and origin referrer for cross-origin requests.
+ {
+ fromScheme: "https://",
+ toScheme: "https://",
+ policy: "no-referrer",
+ result: "" // same origin https://test1.example.com/browser
+ },
+ {
+ fromScheme: "http://",
+ toScheme: "https://",
+ policy: "no-referrer",
+ result: "" // cross origin http://test1.example.com
+ },
+];
+
+/**
+ * Returns the test object for a given test number.
+ * @param aTestNumber The test number - 0, 1, 2, ...
+ * @return The test object, or undefined if the number is out of range.
+ */
+function getReferrerTest(aTestNumber) {
+ return _referrerTests[aTestNumber];
+}
+
+/**
+ * Returns a brief summary of the test, for logging.
+ * @param aTestNumber The test number - 0, 1, 2...
+ * @return The test description.
+ */
+function getReferrerTestDescription(aTestNumber) {
+ let test = getReferrerTest(aTestNumber);
+ return "policy=[" + test.policy + "] " +
+ "rel=[" + test.rel + "] " +
+ test.fromScheme + " -> " + test.toScheme;
+}
+
+/**
+ * Clicks the link.
+ * @param aWindow The window to click the link in.
+ * @param aLinkId The id of the link element.
+ * @param aOptions The options for synthesizeMouseAtCenter.
+ */
+function clickTheLink(aWindow, aLinkId, aOptions) {
+ return BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + aLinkId, aOptions, aWindow.gBrowser.selectedBrowser);
+}
+
+/**
+ * Extracts the referrer result from the target window.
+ * @param aWindow The window where the referrer target has loaded.
+ * @return {Promise}
+ * @resolves When extacted, with the text of the (trimmed) referrer.
+ */
+function referrerResultExtracted(aWindow) {
+ return ContentTask.spawn(aWindow.gBrowser.selectedBrowser, {}, function() {
+ return content.document.getElementById("testdiv").textContent;
+ });
+}
+
+/**
+ * Waits for browser delayed startup to finish.
+ * @param aWindow The window to wait for.
+ * @return {Promise}
+ * @resolves When the window is loaded.
+ */
+function delayedStartupFinished(aWindow) {
+ return new Promise(function(resolve) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ resolve();
+ }
+ }, "browser-delayed-startup-finished", false);
+ });
+}
+
+/**
+ * Waits for some (any) tab to load. The caller triggers the load.
+ * @param aWindow The window where to wait for a tab to load.
+ * @return {Promise}
+ * @resolves With the tab once it's loaded.
+ */
+function someTabLoaded(aWindow) {
+ return BrowserTestUtils.waitForNewTab(gTestWindow.gBrowser).then((tab) => {
+ return BrowserTestUtils.browserStopped(tab.linkedBrowser).then(() => tab);
+ });
+}
+
+/**
+ * Waits for a new window to open and load. The caller triggers the open.
+ * @return {Promise}
+ * @resolves With the new window once it's open and loaded.
+ */
+function newWindowOpened() {
+ return TestUtils.topicObserved("browser-delayed-startup-finished")
+ .then(([win]) => win);
+}
+
+/**
+ * Opens the context menu.
+ * @param aWindow The window to open the context menu in.
+ * @param aLinkId The id of the link to open the context menu on.
+ * @return {Promise}
+ * @resolves With the menu popup when the context menu is open.
+ */
+function contextMenuOpened(aWindow, aLinkId) {
+ let popupShownPromise = BrowserTestUtils.waitForEvent(aWindow.document,
+ "popupshown");
+ // Simulate right-click.
+ clickTheLink(aWindow, aLinkId, { type: "contextmenu", button: 2 });
+ return popupShownPromise.then(e => e.target);
+}
+
+/**
+ * Performs a context menu command.
+ * @param aWindow The window with the already open context menu.
+ * @param aMenu The menu popup to hide.
+ * @param aItemId The id of the menu item to activate.
+ */
+function doContextMenuCommand(aWindow, aMenu, aItemId) {
+ let command = aWindow.document.getElementById(aItemId);
+ command.doCommand();
+ aMenu.hidePopup();
+}
+
+/**
+ * Loads a single test case, i.e., a source url into gTestWindow.
+ * @param aTestNumber The test case number - 0, 1, 2...
+ * @return {Promise}
+ * @resolves When the source url for this test case is loaded.
+ */
+function referrerTestCaseLoaded(aTestNumber, aParams) {
+ let test = getReferrerTest(aTestNumber);
+ let server = rounds == 0 ? REFERRER_POLICYSERVER_URL :
+ REFERRER_POLICYSERVER_URL_ATTRIBUTE;
+ let url = test.fromScheme + server +
+ "?scheme=" + escape(test.toScheme) +
+ "&policy=" + escape(test.policy || "") +
+ "&rel=" + escape(test.rel || "");
+ let browser = gTestWindow.gBrowser;
+ return BrowserTestUtils.openNewForegroundTab(browser, () => {
+ browser.selectedTab = browser.addTab(url, aParams);
+ }, false, true);
+}
+
+/**
+ * Checks the result of the referrer test, and moves on to the next test.
+ * @param aTestNumber The test number - 0, 1, 2, ...
+ * @param aNewWindow The new window where the referrer target opened, or null.
+ * @param aNewTab The new tab where the referrer target opened, or null.
+ * @param aStartTestCase The callback to start the next test, aTestNumber + 1.
+ */
+function checkReferrerAndStartNextTest(aTestNumber, aNewWindow, aNewTab,
+ aStartTestCase, aParams = {}) {
+ referrerResultExtracted(aNewWindow || gTestWindow).then(function(result) {
+ // Compare the actual result against the expected one.
+ let test = getReferrerTest(aTestNumber);
+ let desc = getReferrerTestDescription(aTestNumber);
+ is(result, test.result, desc);
+
+ // Clean up - close new tab / window, and then the source tab.
+ aNewTab && (aNewWindow || gTestWindow).gBrowser.removeTab(aNewTab);
+ aNewWindow && aNewWindow.close();
+ is(gTestWindow.gBrowser.tabs.length, 2, "two tabs open");
+ gTestWindow.gBrowser.removeTab(gTestWindow.gBrowser.tabs[1]);
+
+ // Move on to the next test. Or finish if we're done.
+ var nextTestNumber = aTestNumber + 1;
+ if (getReferrerTest(nextTestNumber)) {
+ referrerTestCaseLoaded(nextTestNumber, aParams).then(function() {
+ aStartTestCase(nextTestNumber);
+ });
+ } else if (rounds == 0) {
+ nextTestNumber = 0;
+ rounds = 1;
+ referrerTestCaseLoaded(nextTestNumber, aParams).then(function() {
+ aStartTestCase(nextTestNumber);
+ });
+ } else {
+ finish();
+ }
+ });
+}
+
+/**
+ * Fires up the complete referrer test.
+ * @param aStartTestCase The callback to start a single test case, called with
+ * the test number - 0, 1, 2... Needs to trigger the navigation from the source
+ * page, and call checkReferrerAndStartNextTest() when the target is loaded.
+ */
+function startReferrerTest(aStartTestCase, params = {}) {
+ waitForExplicitFinish();
+
+ // Open the window where we'll load the source URLs.
+ gTestWindow = openDialog(location, "", "chrome,all,dialog=no", "about:blank");
+ registerCleanupFunction(function() {
+ gTestWindow && gTestWindow.close();
+ });
+
+ // Load and start the first test.
+ delayedStartupFinished(gTestWindow).then(function() {
+ referrerTestCaseLoaded(0, params).then(function() {
+ aStartTestCase(0);
+ });
+ });
+}
diff --git a/browser/base/content/test/siteIdentity/browser.ini b/browser/base/content/test/siteIdentity/browser.ini
new file mode 100644
index 000000000..6ad3668fd
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_identityBlock_focus.js]
+skip-if = os == 'mac' # Bug 1334418 (try only)
+support-files = ../general/permissions.html
+[browser_identityPopup_focus.js]
diff --git a/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js b/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js
new file mode 100644
index 000000000..e1e4e537a
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js
@@ -0,0 +1,62 @@
+/* Tests that the identity block can be reached via keyboard
+ * shortcuts and that it has the correct tab order.
+ */
+
+const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com");
+const PERMISSIONS_PAGE = TEST_PATH + "permissions.html";
+
+function synthesizeKeyAndWaitForFocus(element, keyCode, options) {
+ let focused = BrowserTestUtils.waitForEvent(element, "focus");
+ EventUtils.synthesizeKey(keyCode, options);
+ return focused;
+}
+
+// Checks that the identity block is the next element after the urlbar
+// to be focused if there are no active notification anchors.
+add_task(function* testWithoutNotifications() {
+ yield BrowserTestUtils.withNewTab("https://example.com", function*() {
+ yield synthesizeKeyAndWaitForFocus(gURLBar, "l", {accelKey: true})
+ is(document.activeElement, gURLBar.inputField, "urlbar should be focused");
+ yield synthesizeKeyAndWaitForFocus(gIdentityHandler._identityBox, "VK_TAB", {shiftKey: true})
+ is(document.activeElement, gIdentityHandler._identityBox,
+ "identity block should be focused");
+ });
+});
+
+// Checks that when there is a notification anchor, it will receive
+// focus before the identity block.
+add_task(function* testWithoutNotifications() {
+
+ yield BrowserTestUtils.withNewTab(PERMISSIONS_PAGE, function*(browser) {
+ let popupshown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ // Request a permission;
+ BrowserTestUtils.synthesizeMouseAtCenter("#geo", {}, browser);
+ yield popupshown;
+
+ yield synthesizeKeyAndWaitForFocus(gURLBar, "l", {accelKey: true})
+ is(document.activeElement, gURLBar.inputField, "urlbar should be focused");
+ let geoIcon = document.getElementById("geo-notification-icon");
+ yield synthesizeKeyAndWaitForFocus(geoIcon, "VK_TAB", {shiftKey: true})
+ is(document.activeElement, geoIcon, "notification anchor should be focused");
+ yield synthesizeKeyAndWaitForFocus(gIdentityHandler._identityBox, "VK_TAB", {shiftKey: true})
+ is(document.activeElement, gIdentityHandler._identityBox,
+ "identity block should be focused");
+ });
+});
+
+// Checks that with invalid pageproxystate the identity block is ignored.
+add_task(function* testInvalidPageProxyState() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ // Loading about:blank will automatically focus the urlbar, which, however, can
+ // race with the test code. So we only send the shortcut if the urlbar isn't focused yet.
+ if (document.activeElement != gURLBar.inputField) {
+ yield synthesizeKeyAndWaitForFocus(gURLBar, "l", {accelKey: true})
+ }
+ is(document.activeElement, gURLBar.inputField, "urlbar should be focused");
+ yield synthesizeKeyAndWaitForFocus(gBrowser.getTabForBrowser(browser), "VK_TAB", {shiftKey: true})
+ isnot(document.activeElement, gIdentityHandler._identityBox,
+ "identity block should not be focused");
+ // Restore focus to the url bar.
+ gURLBar.focus();
+ });
+});
diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_focus.js b/browser/base/content/test/siteIdentity/browser_identityPopup_focus.js
new file mode 100644
index 000000000..eea06f079
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_identityPopup_focus.js
@@ -0,0 +1,27 @@
+/* Tests the focus behavior of the identity popup. */
+
+// Access the identity popup via mouseclick. Focus should not be moved inside.
+add_task(function* testIdentityPopupFocusClick() {
+ yield SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]});
+ yield BrowserTestUtils.withNewTab("https://example.com", function*() {
+ let shown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gIdentityHandler._identityBox, {});
+ yield shown;
+ isnot(Services.focus.focusedElement, document.getElementById("identity-popup-security-expander"));
+ });
+});
+
+// Access the identity popup via keyboard. Focus should be moved inside.
+add_task(function* testIdentityPopupFocusKeyboard() {
+ yield SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]});
+ yield BrowserTestUtils.withNewTab("https://example.com", function*() {
+ let focused = BrowserTestUtils.waitForEvent(gIdentityHandler._identityBox, "focus");
+ gIdentityHandler._identityBox.focus();
+ yield focused;
+ let shown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
+ EventUtils.synthesizeKey(" ", {});
+ yield shown;
+ is(Services.focus.focusedElement, document.getElementById("identity-popup-security-expander"));
+ });
+});
+
diff --git a/browser/base/content/test/siteIdentity/head.js b/browser/base/content/test/siteIdentity/head.js
new file mode 100644
index 000000000..12a0547ee
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/head.js
@@ -0,0 +1,6 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
diff --git a/browser/base/content/test/social/.eslintrc.js b/browser/base/content/test/social/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/social/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/social/blocklist.xml b/browser/base/content/test/social/blocklist.xml
new file mode 100644
index 000000000..2e3665c36
--- /dev/null
+++ b/browser/base/content/test/social/blocklist.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem blockID="s1" id="test1.example.com@services.mozilla.org"></emItem>
+ </emItems>
+</blocklist>
diff --git a/browser/base/content/test/social/browser.ini b/browser/base/content/test/social/browser.ini
new file mode 100644
index 000000000..91f931602
--- /dev/null
+++ b/browser/base/content/test/social/browser.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+support-files =
+ blocklist.xml
+ head.js
+ opengraph/og_invalid_url.html
+ opengraph/opengraph.html
+ opengraph/shortlink_linkrel.html
+ opengraph/shorturl_link.html
+ opengraph/shorturl_linkrel.html
+ microformats.html
+ share.html
+ share_activate.html
+ social_activate.html
+ social_activate_basic.html
+ social_activate_iframe.html
+ social_postActivation.html
+ !/browser/base/content/test/plugins/blockNoPlugins.xml
+
+[browser_aboutHome_activation.js]
+[browser_addons.js]
+[browser_blocklist.js]
+[browser_share.js]
+[browser_social_activation.js]
diff --git a/browser/base/content/test/social/browser_aboutHome_activation.js b/browser/base/content/test/social/browser_aboutHome_activation.js
new file mode 100644
index 000000000..37cca53d2
--- /dev/null
+++ b/browser/base/content/test/social/browser_aboutHome_activation.js
@@ -0,0 +1,229 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
+ "resource:///modules/AboutHome.jsm");
+
+var snippet =
+' <script>' +
+' var manifest = {' +
+' "name": "Demo Social Service",' +
+' "origin": "https://example.com",' +
+' "iconURL": "chrome://branding/content/icon16.png",' +
+' "icon32URL": "chrome://branding/content/icon32.png",' +
+' "icon64URL": "chrome://branding/content/icon64.png",' +
+' "shareURL": "https://example.com/browser/browser/base/content/test/social/social_share.html",' +
+' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
+' };' +
+' function activateProvider(node) {' +
+' node.setAttribute("data-service", JSON.stringify(manifest));' +
+' var event = new CustomEvent("ActivateSocialFeature");' +
+' node.dispatchEvent(event);' +
+' }' +
+' </script>' +
+' <div id="activationSnippet" onclick="activateProvider(this)">' +
+' <img src="chrome://branding/content/icon32.png"></img>' +
+' </div>';
+
+// enable one-click activation
+var snippet2 =
+' <script>' +
+' var manifest = {' +
+' "name": "Demo Social Service",' +
+' "origin": "https://example.com",' +
+' "iconURL": "chrome://branding/content/icon16.png",' +
+' "icon32URL": "chrome://branding/content/icon32.png",' +
+' "icon64URL": "chrome://branding/content/icon64.png",' +
+' "shareURL": "https://example.com/browser/browser/base/content/test/social/social_share.html",' +
+' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
+' "oneclick": true' +
+' };' +
+' function activateProvider(node) {' +
+' node.setAttribute("data-service", JSON.stringify(manifest));' +
+' var event = new CustomEvent("ActivateSocialFeature");' +
+' node.dispatchEvent(event);' +
+' }' +
+' </script>' +
+' <div id="activationSnippet" onclick="activateProvider(this)">' +
+' <img src="chrome://branding/content/icon32.png"></img>' +
+' </div>';
+
+var gTests = [
+
+{
+ desc: "Test activation with enable panel",
+ snippet: snippet,
+ panel: true
+},
+
+{
+ desc: "Test activation bypassing enable panel",
+ snippet: snippet2,
+ panel: false
+}
+];
+
+function test()
+{
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+ ignoreAllUncaughtExceptions();
+ PopupNotifications.panel.setAttribute("animate", "false");
+ registerCleanupFunction(function () {
+ PopupNotifications.panel.removeAttribute("animate");
+ });
+
+ Task.spawn(function* () {
+ for (let test of gTests) {
+ info(test.desc);
+
+ // Create a tab to run the test.
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+
+ // Add an event handler to modify the snippets map once it's ready.
+ let snippetsPromise = promiseSetupSnippetsMap(tab, test.snippet);
+
+ // Start loading about:home and wait for it to complete, snippets should be loaded
+ yield promiseTabLoadEvent(tab, "about:home", "AboutHomeLoadSnippetsCompleted");
+
+ yield snippetsPromise;
+
+ // ensure our activation snippet is indeed available
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function*(arg) {
+ ok(!!content.document.getElementById("snippets"), "Found snippets element");
+ ok(!!content.document.getElementById("activationSnippet"), "The snippet is present.");
+ });
+
+ yield new Promise(resolve => {
+ activateProvider(tab, test.panel).then(() => {
+ checkSocialUI();
+ SocialService.uninstallProvider("https://example.com", function () {
+ info("provider uninstalled");
+ resolve();
+ });
+ });
+ });
+
+ // activation opened a post-activation info tab, close it.
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ yield BrowserTestUtils.removeTab(tab);
+ }
+ }).then(finish, ex => {
+ ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+}
+
+/**
+ * Starts a load in an existing tab and waits for it to finish (via some event).
+ *
+ * @param aTab
+ * The tab to load into.
+ * @param aUrl
+ * The url to load.
+ * @param aEvent
+ * The load event type to wait for. Defaults to "load".
+ * @return {Promise} resolved when the event is handled.
+ */
+function promiseTabLoadEvent(aTab, aURL, aEventType="load")
+{
+ return new Promise(resolve => {
+ info("Wait tab event: " + aEventType);
+ aTab.linkedBrowser.addEventListener(aEventType, function load(event) {
+ if (event.originalTarget != aTab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ info("skipping spurious load event");
+ return;
+ }
+ aTab.linkedBrowser.removeEventListener(aEventType, load, true);
+ info("Tab event received: " + aEventType);
+ resolve();
+ }, true, true);
+ aTab.linkedBrowser.loadURI(aURL);
+ });
+}
+
+/**
+ * Cleans up snippets and ensures that by default we don't try to check for
+ * remote snippets since that may cause network bustage or slowness.
+ *
+ * @param aTab
+ * The tab containing about:home.
+ * @param aSetupFn
+ * The setup function to be run.
+ * @return {Promise} resolved when the snippets are ready. Gets the snippets map.
+ */
+function promiseSetupSnippetsMap(aTab, aSnippet)
+{
+ info("Waiting for snippets map");
+
+ return ContentTask.spawn(aTab.linkedBrowser,
+ {snippetsVersion: AboutHomeUtils.snippetsVersion,
+ snippet: aSnippet},
+ function*(arg) {
+ return new Promise(resolve => {
+ addEventListener("AboutHomeLoadSnippets", function load(event) {
+ removeEventListener("AboutHomeLoadSnippets", load, true);
+
+ let cw = content.window.wrappedJSObject;
+
+ // The snippets should already be ready by this point. Here we're
+ // just obtaining a reference to the snippets map.
+ cw.ensureSnippetsMapThen(function (aSnippetsMap) {
+ aSnippetsMap = Cu.waiveXrays(aSnippetsMap);
+ console.log("Got snippets map: " +
+ "{ last-update: " + aSnippetsMap.get("snippets-last-update") +
+ ", cached-version: " + aSnippetsMap.get("snippets-cached-version") +
+ " }");
+ // Don't try to update.
+ aSnippetsMap.set("snippets-last-update", Date.now());
+ aSnippetsMap.set("snippets-cached-version", arg.snippetsVersion);
+ // Clear snippets.
+ aSnippetsMap.delete("snippets");
+ aSnippetsMap.set("snippets", arg.snippet);
+ resolve();
+ });
+ }, true, true);
+ });
+ });
+}
+
+
+function sendActivationEvent(tab) {
+ // hack Social.lastEventReceived so we don't hit the "too many events" check.
+ Social.lastEventReceived = 0;
+ let doc = tab.linkedBrowser.contentDocument;
+ // if our test has a frame, use it
+ if (doc.defaultView.frames[0])
+ doc = doc.defaultView.frames[0].document;
+ let button = doc.getElementById("activationSnippet");
+ BrowserTestUtils.synthesizeMouseAtCenter(button, {}, tab.linkedBrowser);
+}
+
+function activateProvider(tab, expectPanel, aCallback) {
+ return new Promise(resolve => {
+ if (expectPanel) {
+ BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+ let panel = document.getElementById("servicesInstall-notification");
+ panel.button.click();
+ });
+ }
+ waitForProviderLoad().then(() => {
+ checkSocialUI();
+ resolve();
+ });
+ sendActivationEvent(tab);
+ });
+}
+
+function waitForProviderLoad(cb) {
+ return Promise.all([
+ promiseObserverNotified("social:provider-enabled"),
+ ensureFrameLoaded(gBrowser, "https://example.com/browser/browser/base/content/test/social/social_postActivation.html"),
+ ]);
+}
diff --git a/browser/base/content/test/social/browser_addons.js b/browser/base/content/test/social/browser_addons.js
new file mode 100644
index 000000000..5a75d1d67
--- /dev/null
+++ b/browser/base/content/test/social/browser_addons.js
@@ -0,0 +1,217 @@
+var AddonManager = Cu.import("resource://gre/modules/AddonManager.jsm", {}).AddonManager;
+var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+
+var manifest = {
+ name: "provider 1",
+ origin: "https://example.com",
+ shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html",
+ iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+};
+var manifest2 = { // used for testing install
+ name: "provider 2",
+ origin: "https://test1.example.com",
+ shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html",
+ iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png",
+ version: "1.0"
+};
+var manifestUpgrade = { // used for testing install
+ name: "provider 3",
+ origin: "https://test2.example.com",
+ shareURL: "https://test2.example.com/browser/browser/base/content/test/social/social_share.html",
+ iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png",
+ version: "1.0"
+};
+
+function test() {
+ waitForExplicitFinish();
+ PopupNotifications.panel.setAttribute("animate", "false");
+ registerCleanupFunction(function () {
+ PopupNotifications.panel.removeAttribute("animate");
+ });
+
+ let prefname = getManifestPrefname(manifest);
+ // ensure that manifest2 is NOT showing as builtin
+ is(SocialService.getOriginActivationType(manifest.origin), "foreign", "manifest is foreign");
+ is(SocialService.getOriginActivationType(manifest2.origin), "foreign", "manifest2 is foreign");
+
+ Services.prefs.setBoolPref("social.remote-install.enabled", true);
+ runSocialTests(tests, undefined, undefined, function () {
+ Services.prefs.clearUserPref("social.remote-install.enabled");
+ ok(!Services.prefs.prefHasUserValue(prefname), "manifest is not in user-prefs");
+ // just in case the tests failed, clear these here as well
+ Services.prefs.clearUserPref("social.directories");
+ finish();
+ });
+}
+
+function installListener(next, aManifest) {
+ let expectEvent = "onInstalling";
+ let prefname = getManifestPrefname(aManifest);
+ // wait for the actual removal to call next
+ SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
+ if (topic == "provider-disabled") {
+ SocialService.unregisterProviderListener(providerListener);
+ is(origin, aManifest.origin, "provider disabled");
+ executeSoon(next);
+ }
+ });
+
+ return {
+ onInstalling: function(addon) {
+ is(expectEvent, "onInstalling", "install started");
+ is(addon.manifest.origin, aManifest.origin, "provider about to be installed");
+ ok(!Services.prefs.prefHasUserValue(prefname), "manifest is not in user-prefs");
+ expectEvent = "onInstalled";
+ },
+ onInstalled: function(addon) {
+ is(addon.manifest.origin, aManifest.origin, "provider installed");
+ ok(addon.installDate.getTime() > 0, "addon has installDate");
+ ok(addon.updateDate.getTime() > 0, "addon has updateDate");
+ ok(Services.prefs.prefHasUserValue(prefname), "manifest is in user-prefs");
+ expectEvent = "onUninstalling";
+ },
+ onUninstalling: function(addon) {
+ is(expectEvent, "onUninstalling", "uninstall started");
+ is(addon.manifest.origin, aManifest.origin, "provider about to be uninstalled");
+ ok(Services.prefs.prefHasUserValue(prefname), "manifest is in user-prefs");
+ expectEvent = "onUninstalled";
+ },
+ onUninstalled: function(addon) {
+ is(expectEvent, "onUninstalled", "provider has been uninstalled");
+ is(addon.manifest.origin, aManifest.origin, "provider uninstalled");
+ ok(!Services.prefs.prefHasUserValue(prefname), "manifest is not in user-prefs");
+ AddonManager.removeAddonListener(this);
+ }
+ };
+}
+
+var tests = {
+ testHTTPInstallFailure: function(next) {
+ let installFrom = "http://example.com";
+ is(SocialService.getOriginActivationType(installFrom), "foreign", "testing foriegn install");
+ let data = {
+ origin: installFrom,
+ url: installFrom+"/activate",
+ manifest: manifest,
+ window: window
+ }
+ Social.installProvider(data, function(addonManifest) {
+ ok(!addonManifest, "unable to install provider over http");
+ next();
+ });
+ },
+ testAddonEnableToggle: function(next) {
+ let expectEvent;
+ let prefname = getManifestPrefname(manifest);
+ let listener = {
+ onEnabled: function(addon) {
+ is(expectEvent, "onEnabled", "provider onEnabled");
+ ok(!addon.userDisabled, "provider enabled");
+ executeSoon(function() {
+ expectEvent = "onDisabling";
+ addon.userDisabled = true;
+ });
+ },
+ onEnabling: function(addon) {
+ is(expectEvent, "onEnabling", "provider onEnabling");
+ expectEvent = "onEnabled";
+ },
+ onDisabled: function(addon) {
+ is(expectEvent, "onDisabled", "provider onDisabled");
+ ok(addon.userDisabled, "provider disabled");
+ AddonManager.removeAddonListener(listener);
+ // clear the provider user-level pref
+ Services.prefs.clearUserPref(prefname);
+ executeSoon(next);
+ },
+ onDisabling: function(addon) {
+ is(expectEvent, "onDisabling", "provider onDisabling");
+ expectEvent = "onDisabled";
+ }
+ };
+ AddonManager.addAddonListener(listener);
+
+ // we're only testing enable disable, so we quickly set the user-level pref
+ // for this provider and test enable/disable toggling
+ setManifestPref(prefname, manifest);
+ ok(Services.prefs.prefHasUserValue(prefname), "manifest is in user-prefs");
+ AddonManager.getAddonsByTypes(["service"], function(addons) {
+ for (let addon of addons) {
+ if (addon.userDisabled) {
+ expectEvent = "onEnabling";
+ addon.userDisabled = false;
+ // only test with one addon
+ return;
+ }
+ }
+ ok(false, "no addons toggled");
+ next();
+ });
+ },
+ testProviderEnableToggle: function(next) {
+ // enable and disabel a provider from the SocialService interface, check
+ // that the addon manager is updated
+
+ let expectEvent;
+ let prefname = getManifestPrefname(manifest);
+
+ let listener = {
+ onEnabled: function(addon) {
+ is(expectEvent, "onEnabled", "provider onEnabled");
+ is(addon.manifest.origin, manifest.origin, "provider enabled");
+ ok(!addon.userDisabled, "provider !userDisabled");
+ },
+ onEnabling: function(addon) {
+ is(expectEvent, "onEnabling", "provider onEnabling");
+ is(addon.manifest.origin, manifest.origin, "provider about to be enabled");
+ expectEvent = "onEnabled";
+ },
+ onDisabled: function(addon) {
+ is(expectEvent, "onDisabled", "provider onDisabled");
+ is(addon.manifest.origin, manifest.origin, "provider disabled");
+ ok(addon.userDisabled, "provider userDisabled");
+ },
+ onDisabling: function(addon) {
+ is(expectEvent, "onDisabling", "provider onDisabling");
+ is(addon.manifest.origin, manifest.origin, "provider about to be disabled");
+ expectEvent = "onDisabled";
+ }
+ };
+ AddonManager.addAddonListener(listener);
+
+ expectEvent = "onEnabling";
+ setManifestPref(prefname, manifest);
+ SocialService.enableProvider(manifest.origin, function(provider) {
+ expectEvent = "onDisabling";
+ SocialService.disableProvider(provider.origin, function() {
+ AddonManager.removeAddonListener(listener);
+ Services.prefs.clearUserPref(prefname);
+ next();
+ });
+ });
+ },
+ testDirectoryInstall: function(next) {
+ AddonManager.addAddonListener(installListener(next, manifest2));
+
+ BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+ let panel = document.getElementById("servicesInstall-notification");
+ info("servicesInstall-notification panel opened");
+ panel.button.click();
+ });
+
+ Services.prefs.setCharPref("social.directories", manifest2.origin);
+ is(SocialService.getOriginActivationType(manifest2.origin), "directory", "testing directory install");
+ let data = {
+ origin: manifest2.origin,
+ url: manifest2.origin + "/directory",
+ manifest: manifest2,
+ window: window
+ }
+ Social.installProvider(data, function(addonManifest) {
+ Services.prefs.clearUserPref("social.directories");
+ SocialService.enableProvider(addonManifest.origin, function(provider) {
+ Social.uninstallProvider(addonManifest.origin);
+ });
+ });
+ }
+}
diff --git a/browser/base/content/test/social/browser_blocklist.js b/browser/base/content/test/social/browser_blocklist.js
new file mode 100644
index 000000000..b67d5efb3
--- /dev/null
+++ b/browser/base/content/test/social/browser_blocklist.js
@@ -0,0 +1,211 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// a place for miscellaneous social tests
+
+var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+var blocklistURL = "http://example.com/browser/browser/base/content/test/social/blocklist.xml";
+
+var manifest = { // normal provider
+ name: "provider ok",
+ origin: "https://example.com",
+ shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html",
+ iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+};
+var manifest_bad = { // normal provider
+ name: "provider blocked",
+ origin: "https://test1.example.com",
+ shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html",
+ iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png"
+};
+
+// blocklist testing
+function updateBlocklist() {
+ var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsITimerCallback);
+ let promise = promiseObserverNotified("blocklist-updated");
+ blocklistNotifier.notify(null);
+ return promise;
+}
+
+var _originalTestBlocklistURL = null;
+function setAndUpdateBlocklist(aURL) {
+ if (!_originalTestBlocklistURL)
+ _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url");
+ Services.prefs.setCharPref("extensions.blocklist.url", aURL);
+ return updateBlocklist();
+}
+
+function resetBlocklist() {
+ // XXX - this has "forked" from the head.js helpers in our parent directory :(
+ // But let's reuse their blockNoPlugins.xml. Later, we should arrange to
+ // use their head.js helpers directly
+ let noBlockedURL = "http://example.com/browser/browser/base/content/test/plugins/blockNoPlugins.xml";
+ return new Promise(resolve => {
+ setAndUpdateBlocklist(noBlockedURL).then(() => {
+ Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL);
+ resolve();
+ });
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+ // turn on logging for nsBlocklistService.js
+ Services.prefs.setBoolPref("extensions.logging.enabled", true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("extensions.logging.enabled");
+ });
+
+ runSocialTests(tests, undefined, undefined, function () {
+ resetBlocklist().then(finish); // restore to original pref
+ });
+}
+
+var tests = {
+ testSimpleBlocklist: function(next) {
+ // this really just tests adding and clearing our blocklist for later tests
+ setAndUpdateBlocklist(blocklistURL).then(() => {
+ ok(Services.blocklist.isAddonBlocklisted(SocialService.createWrapper(manifest_bad)), "blocking 'blocked'");
+ ok(!Services.blocklist.isAddonBlocklisted(SocialService.createWrapper(manifest)), "not blocking 'good'");
+ resetBlocklist().then(() => {
+ ok(!Services.blocklist.isAddonBlocklisted(SocialService.createWrapper(manifest_bad)), "blocklist cleared");
+ next();
+ });
+ });
+ },
+ testAddingNonBlockedProvider: function(next) {
+ function finishTest(isgood) {
+ ok(isgood, "adding non-blocked provider ok");
+ Services.prefs.clearUserPref("social.manifest.good");
+ resetBlocklist().then(next);
+ }
+ setManifestPref("social.manifest.good", manifest);
+ setAndUpdateBlocklist(blocklistURL).then(() => {
+ try {
+ SocialService.addProvider(manifest, function(provider) {
+ try {
+ SocialService.disableProvider(provider.origin, function() {
+ ok(true, "added and removed provider");
+ finishTest(true);
+ });
+ } catch (e) {
+ ok(false, "SocialService.disableProvider threw exception: " + e);
+ finishTest(false);
+ }
+ });
+ } catch (e) {
+ ok(false, "SocialService.addProvider threw exception: " + e);
+ finishTest(false);
+ }
+ });
+ },
+ testAddingBlockedProvider: function(next) {
+ function finishTest(good) {
+ ok(good, "Unable to add blocklisted provider");
+ Services.prefs.clearUserPref("social.manifest.blocked");
+ resetBlocklist().then(next);
+ }
+ setManifestPref("social.manifest.blocked", manifest_bad);
+ setAndUpdateBlocklist(blocklistURL).then(() => {
+ try {
+ SocialService.addProvider(manifest_bad, function(provider) {
+ SocialService.disableProvider(provider.origin, function() {
+ ok(false, "SocialService.addProvider should throw blocklist exception");
+ finishTest(false);
+ });
+ });
+ } catch (e) {
+ ok(true, "SocialService.addProvider should throw blocklist exception: " + e);
+ finishTest(true);
+ }
+ });
+ },
+ testInstallingBlockedProvider: function(next) {
+ function finishTest(good) {
+ ok(good, "Unable to install blocklisted provider");
+ resetBlocklist().then(next);
+ }
+ let activationURL = manifest_bad.origin + "/browser/browser/base/content/test/social/social_activate.html"
+ setAndUpdateBlocklist(blocklistURL).then(() => {
+ try {
+ // expecting an exception when attempting to install a hard blocked
+ // provider
+ let data = {
+ origin: manifest_bad.origin,
+ url: activationURL,
+ manifest: manifest_bad,
+ window: window
+ }
+ Social.installProvider(data, function(addonManifest) {
+ finishTest(false);
+ });
+ } catch (e) {
+ finishTest(true);
+ }
+ });
+ },
+ testBlockingExistingProvider: function(next) {
+ let listener = {
+ _window: null,
+ onOpenWindow: function(aXULWindow) {
+ Services.wm.removeListener(this);
+ this._window = aXULWindow;
+ let domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+ domwindow.addEventListener("load", function _load() {
+ domwindow.removeEventListener("load", _load, false);
+
+ domwindow.addEventListener("unload", function _unload() {
+ domwindow.removeEventListener("unload", _unload, false);
+ info("blocklist window was closed");
+ Services.wm.removeListener(listener);
+ next();
+ }, false);
+
+ is(domwindow.document.location.href, URI_EXTENSION_BLOCKLIST_DIALOG, "dialog opened and focused");
+ // wait until after load to cancel so the dialog has initalized. we
+ // don't want to accept here since that restarts the browser.
+ executeSoon(() => {
+ let cancelButton = domwindow.document.documentElement.getButton("cancel");
+ info("***** hit the cancel button\n");
+ cancelButton.doCommand();
+ });
+ }, false);
+ },
+ onCloseWindow: function(aXULWindow) { },
+ onWindowTitleChange: function(aXULWindow, aNewTitle) { }
+ };
+
+ Services.wm.addListener(listener);
+
+ setManifestPref("social.manifest.blocked", manifest_bad);
+ try {
+ SocialService.addProvider(manifest_bad, function(provider) {
+ // the act of blocking should cause a 'provider-disabled' notification
+ // from SocialService.
+ SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
+ if (topic != "provider-disabled")
+ return;
+ SocialService.unregisterProviderListener(providerListener);
+ is(origin, provider.origin, "provider disabled");
+ SocialService.getProvider(provider.origin, function(p) {
+ ok(p == null, "blocklisted provider disabled");
+ Services.prefs.clearUserPref("social.manifest.blocked");
+ resetBlocklist();
+ });
+ });
+ // no callback - the act of updating should cause the listener above
+ // to fire.
+ setAndUpdateBlocklist(blocklistURL);
+ });
+ } catch (e) {
+ ok(false, "unable to add provider " + e);
+ next();
+ }
+ }
+}
diff --git a/browser/base/content/test/social/browser_share.js b/browser/base/content/test/social/browser_share.js
new file mode 100644
index 000000000..19dca519b
--- /dev/null
+++ b/browser/base/content/test/social/browser_share.js
@@ -0,0 +1,396 @@
+
+var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+
+var baseURL = "https://example.com/browser/browser/base/content/test/social/";
+
+var manifest = { // normal provider
+ name: "provider 1",
+ origin: "https://example.com",
+ iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png",
+ shareURL: "https://example.com/browser/browser/base/content/test/social/share.html"
+};
+var activationPage = "https://example.com/browser/browser/base/content/test/social/share_activate.html";
+
+function sendActivationEvent(subframe) {
+ // hack Social.lastEventReceived so we don't hit the "too many events" check.
+ Social.lastEventReceived = 0;
+ let doc = subframe.contentDocument;
+ // if our test has a frame, use it
+ let button = doc.getElementById("activation");
+ ok(!!button, "got the activation button");
+ EventUtils.synthesizeMouseAtCenter(button, {}, doc.defaultView);
+}
+
+function test() {
+ waitForExplicitFinish();
+ Services.prefs.setCharPref("social.shareDirectory", activationPage);
+
+ let frameScript = "data:,(" + function frame_script() {
+ addEventListener("OpenGraphData", function (aEvent) {
+ sendAsyncMessage("sharedata", aEvent.detail);
+ }, true, true);
+ /* bug 1042991, ensure history is available by calling history.back on close */
+ addMessageListener("closeself", function(e) {
+ content.history.back();
+ content.close();
+ }, true);
+ /* if text is entered into field, onbeforeunload will cause a modal dialog
+ unless dialogs have been disabled for the iframe. */
+ content.onbeforeunload = function(e) {
+ return 'FAIL.';
+ };
+ }.toString() + ")();";
+ let mm = getGroupMessageManager("social");
+ mm.loadFrameScript(frameScript, true);
+
+ // Animation on the panel can cause intermittent failures such as bug 1115131.
+ SocialShare.panel.setAttribute("animate", "false");
+ registerCleanupFunction(function () {
+ SocialShare.panel.removeAttribute("animate");
+ mm.removeDelayedFrameScript(frameScript);
+ Services.prefs.clearUserPref("social.directories");
+ Services.prefs.clearUserPref("social.shareDirectory");
+ Services.prefs.clearUserPref("social.share.activationPanelEnabled");
+ });
+ runSocialTests(tests, undefined, function(next) {
+ let shareButton = SocialShare.shareButton;
+ if (shareButton) {
+ CustomizableUI.removeWidgetFromArea("social-share-button", CustomizableUI.AREA_NAVBAR)
+ shareButton.remove();
+ }
+ next();
+ });
+}
+
+var corpus = [
+ {
+ url: baseURL+"opengraph/opengraph.html",
+ options: {
+ // og:title
+ title: ">This is my title<",
+ // og:description
+ description: "A test corpus file for open graph tags we care about",
+ // medium: this.getPageMedium(),
+ // source: this.getSourceURL(),
+ // og:url
+ url: "https://www.mozilla.org/",
+ // shortUrl: this.getShortURL(),
+ // og:image
+ previews:["https://www.mozilla.org/favicon.png"],
+ // og:site_name
+ siteName: ">My simple test page<"
+ }
+ },
+ {
+ // tests that og:url doesn't override the page url if it is bad
+ url: baseURL+"opengraph/og_invalid_url.html",
+ options: {
+ description: "A test corpus file for open graph tags passing a bad url",
+ url: baseURL+"opengraph/og_invalid_url.html",
+ previews: [],
+ siteName: "Evil chrome delivering website"
+ }
+ },
+ {
+ url: baseURL+"opengraph/shorturl_link.html",
+ options: {
+ previews: ["http://example.com/1234/56789.jpg"],
+ url: "http://www.example.com/photos/56789/",
+ shortUrl: "http://imshort/p/abcde"
+ }
+ },
+ {
+ url: baseURL+"opengraph/shorturl_linkrel.html",
+ options: {
+ previews: ["http://example.com/1234/56789.jpg"],
+ url: "http://www.example.com/photos/56789/",
+ shortUrl: "http://imshort/p/abcde"
+ }
+ },
+ {
+ url: baseURL+"opengraph/shortlink_linkrel.html",
+ options: {
+ previews: ["http://example.com/1234/56789.jpg"],
+ url: "http://www.example.com/photos/56789/",
+ shortUrl: "http://imshort/p/abcde"
+ }
+ }
+];
+
+function hasoptions(testOptions, options) {
+ for (let option in testOptions) {
+ let data = testOptions[option];
+ info("data: "+JSON.stringify(data));
+ let message_data = options[option];
+ info("message_data: "+JSON.stringify(message_data));
+ if (Array.isArray(data)) {
+ // the message may have more array elements than we are testing for, this
+ // is ok since some of those are hard to test. So we just test that
+ // anything in our test data IS in the message.
+ ok(Array.every(data, function(item) { return message_data.indexOf(item) >= 0 }), "option "+option);
+ } else {
+ is(message_data, data, "option "+option);
+ }
+ }
+}
+
+var tests = {
+ testShareDisabledOnActivation: function(next) {
+ // starting on about:blank page, share should be visible but disabled when
+ // adding provider
+ is(gBrowser.currentURI.spec, "about:blank");
+
+ // initialize the button into the navbar
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // ensure correct state
+ SocialUI.onCustomizeEnd(window);
+
+ SocialService.addProvider(manifest, function(provider) {
+ is(SocialUI.enabled, true, "SocialUI is enabled");
+ checkSocialUI();
+ // share should not be enabled since we only have about:blank page
+ let shareButton = SocialShare.shareButton;
+ // verify the attribute for proper css
+ is(shareButton.getAttribute("disabled"), "true", "share button attribute is disabled");
+ // button should be visible
+ is(shareButton.hidden, false, "share button is visible");
+ SocialService.disableProvider(manifest.origin, next);
+ });
+ },
+ testShareEnabledOnActivation: function(next) {
+ // starting from *some* page, share should be visible and enabled when
+ // activating provider
+ // initialize the button into the navbar
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // ensure correct state
+ SocialUI.onCustomizeEnd(window);
+
+ let testData = corpus[0];
+ BrowserTestUtils.openNewForegroundTab(gBrowser, testData.url).then(tab => {
+ SocialService.addProvider(manifest, function(provider) {
+ is(SocialUI.enabled, true, "SocialUI is enabled");
+ checkSocialUI();
+ // share should not be enabled since we only have about:blank page
+ let shareButton = SocialShare.shareButton;
+ // verify the attribute for proper css
+ ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
+ // button should be visible
+ is(shareButton.hidden, false, "share button is visible");
+ BrowserTestUtils.removeTab(tab).then(next);
+ });
+ });
+ },
+ testSharePage: function(next) {
+ let testTab;
+ let testIndex = 0;
+ let testData = corpus[testIndex++];
+
+ // initialize the button into the navbar
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // ensure correct state
+ SocialUI.onCustomizeEnd(window);
+
+ let mm = getGroupMessageManager("social");
+ mm.addMessageListener("sharedata", function handler(msg) {
+ BrowserTestUtils.removeTab(testTab).then(() => {
+ hasoptions(testData.options, JSON.parse(msg.data));
+ testData = corpus[testIndex++];
+ BrowserTestUtils.waitForCondition(() => { return SocialShare.currentShare == null; }, "share panel closed").then(() => {
+ if (testData) {
+ runOneTest();
+ } else {
+ mm.removeMessageListener("sharedata", handler);
+ SocialService.disableProvider(manifest.origin, next);
+ }
+ });
+ SocialShare.iframe.messageManager.sendAsyncMessage("closeself", {});
+ });
+ });
+
+ function runOneTest() {
+ BrowserTestUtils.openNewForegroundTab(gBrowser, testData.url).then(tab => {
+ testTab = tab;
+
+ let shareButton = SocialShare.shareButton;
+ // verify the attribute for proper css
+ ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
+ // button should be visible
+ is(shareButton.hidden, false, "share button is visible");
+
+ SocialShare.sharePage(manifest.origin);
+ });
+ }
+ executeSoon(runOneTest);
+ },
+ testShareMicroformats: function(next) {
+ // initialize the button into the navbar
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // ensure correct state
+ SocialUI.onCustomizeEnd(window);
+
+ SocialService.addProvider(manifest, function(provider) {
+ let target, testTab;
+
+ let expecting = JSON.stringify({
+ "url": "https://example.com/browser/browser/base/content/test/social/microformats.html",
+ "title": "Raspberry Pi Page",
+ "previews": ["https://example.com/someimage.jpg"],
+ "microformats": {
+ "items": [{
+ "type": ["h-product"],
+ "properties": {
+ "name": ["Raspberry Pi"],
+ "photo": ["https://example.com/someimage.jpg"],
+ "description": [{
+ "value": "The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It's a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.",
+ "html": "The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It's a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming."
+ }
+ ],
+ "url": ["https://example.com/"],
+ "price": ["29.95"],
+ "review": [{
+ "value": "4.5 out of 5",
+ "type": ["h-review"],
+ "properties": {
+ "rating": ["4.5"]
+ }
+ }
+ ],
+ "category": ["Computer", "Education"]
+ }
+ }
+ ],
+ "rels": {
+ "tag": ["https://example.com/wiki/computer", "https://example.com/wiki/education"]
+ },
+ "rel-urls": {
+ "https://example.com/wiki/computer": {
+ "text": "Computer",
+ "rels": ["tag"]
+ },
+ "https://example.com/wiki/education": {
+ "text": "Education",
+ "rels": ["tag"]
+ }
+ }
+ }
+ });
+
+ let mm = getGroupMessageManager("social");
+ mm.addMessageListener("sharedata", function handler(msg) {
+ is(msg.data, expecting, "microformats data ok");
+ BrowserTestUtils.waitForCondition(() => { return SocialShare.currentShare == null; },
+ "share panel closed").then(() => {
+ mm.removeMessageListener("sharedata", handler);
+ BrowserTestUtils.removeTab(testTab).then(() => {
+ SocialService.disableProvider(manifest.origin, next);
+ });
+ });
+ SocialShare.iframe.messageManager.sendAsyncMessage("closeself", {});
+ });
+
+ let url = "https://example.com/browser/browser/base/content/test/social/microformats.html"
+ BrowserTestUtils.openNewForegroundTab(gBrowser, url).then(tab => {
+ testTab = tab;
+
+ let shareButton = SocialShare.shareButton;
+ // verify the attribute for proper css
+ ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
+ // button should be visible
+ is(shareButton.hidden, false, "share button is visible");
+
+ let doc = tab.linkedBrowser.contentDocument;
+ target = doc.getElementById("simple-hcard");
+ SocialShare.sharePage(manifest.origin, null, target);
+ });
+ });
+ },
+ testSharePanelActivation: function(next) {
+ let testTab;
+ // cleared in the cleanup function
+ Services.prefs.setCharPref("social.directories", "https://example.com");
+ Services.prefs.setBoolPref("social.share.activationPanelEnabled", true);
+ // make the iframe so we can wait on the load
+ SocialShare._createFrame();
+ let iframe = SocialShare.iframe;
+
+ // initialize the button into the navbar
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // ensure correct state
+ SocialUI.onCustomizeEnd(window);
+
+ ensureFrameLoaded(iframe).then(() => {
+ let subframe = iframe.contentDocument.getElementById("activation-frame");
+ ensureFrameLoaded(subframe, activationPage).then(() => {
+ is(subframe.contentDocument.location.href, activationPage, "activation page loaded");
+ promiseObserverNotified("social:provider-enabled").then(() => {
+ let mm = getGroupMessageManager("social");
+ mm.addMessageListener("sharedata", function handler(msg) {
+ ok(true, "share completed");
+
+ BrowserTestUtils.waitForCondition(() => { return SocialShare.currentShare == null; },
+ "share panel closed").then(() => {
+ BrowserTestUtils.removeTab(testTab).then(() => {
+ mm.removeMessageListener("sharedata", handler);
+ SocialService.uninstallProvider(manifest.origin, next);
+ });
+ });
+ SocialShare.iframe.messageManager.sendAsyncMessage("closeself", {});
+ });
+ });
+ sendActivationEvent(subframe);
+ });
+ });
+ BrowserTestUtils.openNewForegroundTab(gBrowser, activationPage).then(tab => {
+ let shareButton = SocialShare.shareButton;
+ // verify the attribute for proper css
+ ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
+ // button should be visible
+ is(shareButton.hidden, false, "share button is visible");
+
+ testTab = tab;
+ SocialShare.sharePage();
+ });
+ },
+ testSharePanelDialog: function(next) {
+ let testTab;
+ // initialize the button into the navbar
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // ensure correct state
+ SocialUI.onCustomizeEnd(window);
+ SocialShare._createFrame();
+
+ SocialService.addProvider(manifest, () => {
+ BrowserTestUtils.openNewForegroundTab(gBrowser, activationPage).then(tab => {
+ ensureFrameLoaded(SocialShare.iframe).then(() => {
+ // send keys to the input field. An unexpected failure will happen
+ // if the onbeforeunload handler is fired.
+ EventUtils.sendKey("f");
+ EventUtils.sendKey("a");
+ EventUtils.sendKey("i");
+ EventUtils.sendKey("l");
+
+ SocialShare.panel.addEventListener("popuphidden", function hidden(evt) {
+ SocialShare.panel.removeEventListener("popuphidden", hidden);
+ let topwin = Services.wm.getMostRecentWindow(null);
+ is(topwin, window, "no dialog is open");
+
+ BrowserTestUtils.removeTab(testTab).then(() => {
+ SocialService.disableProvider(manifest.origin, next);
+ });
+ });
+ SocialShare.iframe.messageManager.sendAsyncMessage("closeself", {});
+ });
+
+ let shareButton = SocialShare.shareButton;
+ // verify the attribute for proper css
+ ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
+ // button should be visible
+ is(shareButton.hidden, false, "share button is visible");
+
+ testTab = tab;
+ SocialShare.sharePage();
+ });
+ });
+ }
+}
diff --git a/browser/base/content/test/social/browser_social_activation.js b/browser/base/content/test/social/browser_social_activation.js
new file mode 100644
index 000000000..2af0d8021
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_activation.js
@@ -0,0 +1,270 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: Assert is null");
+
+
+var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+
+var tabsToRemove = [];
+
+function removeProvider(provider) {
+ return new Promise(resolve => {
+ // a full install sets the manifest into a pref, addProvider alone doesn't,
+ // make sure we uninstall if the manifest was added.
+ if (provider.manifest) {
+ SocialService.uninstallProvider(provider.origin, resolve);
+ } else {
+ SocialService.disableProvider(provider.origin, resolve);
+ }
+ });
+}
+
+function postTestCleanup(callback) {
+ Task.spawn(function* () {
+ // any tabs opened by the test.
+ for (let tab of tabsToRemove) {
+ yield BrowserTestUtils.removeTab(tab);
+ }
+ tabsToRemove = [];
+ // all the providers may have been added.
+ while (Social.providers.length > 0) {
+ yield removeProvider(Social.providers[0]);
+ }
+ }).then(callback);
+}
+
+function newTab(url) {
+ return new Promise(resolve => {
+ BrowserTestUtils.openNewForegroundTab(gBrowser, url).then(tab => {
+ tabsToRemove.push(tab);
+ resolve(tab);
+ });
+ });
+}
+
+function sendActivationEvent(tab, callback, nullManifest) {
+ // hack Social.lastEventReceived so we don't hit the "too many events" check.
+ Social.lastEventReceived = 0;
+ BrowserTestUtils.synthesizeMouseAtCenter("#activation", {}, tab.linkedBrowser);
+ executeSoon(callback);
+}
+
+function activateProvider(domain, callback, nullManifest) {
+ let activationURL = domain+"/browser/browser/base/content/test/social/social_activate_basic.html"
+ newTab(activationURL).then(tab => {
+ sendActivationEvent(tab, callback, nullManifest);
+ });
+}
+
+function activateIFrameProvider(domain, callback) {
+ let activationURL = domain+"/browser/browser/base/content/test/social/social_activate_iframe.html"
+ newTab(activationURL).then(tab => {
+ sendActivationEvent(tab, callback, false);
+ });
+}
+
+function waitForProviderLoad(origin) {
+ return Promise.all([
+ ensureFrameLoaded(gBrowser, origin + "/browser/browser/base/content/test/social/social_postActivation.html"),
+ ]);
+}
+
+function getAddonItemInList(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;
+}
+
+function clickAddonRemoveButton(tab, aCallback) {
+ AddonManager.getAddonsByTypes(["service"], function(aAddons) {
+ let addon = aAddons[0];
+
+ let doc = tab.linkedBrowser.contentDocument;
+ let list = doc.getElementById("addon-list");
+
+ let item = getAddonItemInList(addon.id, list);
+ let button = item._removeBtn;
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ // uninstall happens after about:addons tab is closed, so we wait on
+ // disabled
+ promiseObserverNotified("social:provider-disabled").then(() => {
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+ executeSoon(function() { aCallback(addon); });
+ });
+
+ BrowserTestUtils.synthesizeMouseAtCenter(button, {}, tab.linkedBrowser);
+ });
+}
+
+function activateOneProvider(manifest, finishActivation, aCallback) {
+ info("activating provider "+manifest.name);
+ let panel = document.getElementById("servicesInstall-notification");
+ BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+ ok(!panel.hidden, "servicesInstall-notification panel opened");
+ if (finishActivation)
+ panel.button.click();
+ else
+ panel.closebutton.click();
+ });
+ BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden").then(() => {
+ ok(panel.hidden, "servicesInstall-notification panel hidden");
+ if (!finishActivation) {
+ ok(panel.hidden, "activation panel is not showing");
+ executeSoon(aCallback);
+ } else {
+ waitForProviderLoad(manifest.origin).then(() => {
+ checkSocialUI();
+ executeSoon(aCallback);
+ });
+ }
+ });
+
+ // the test will continue as the popup events fire...
+ activateProvider(manifest.origin, function() {
+ info("waiting on activation panel to open/close...");
+ });
+}
+
+var gTestDomains = ["https://example.com", "https://test1.example.com", "https://test2.example.com"];
+var gProviders = [
+ {
+ name: "provider 1",
+ origin: "https://example.com",
+ shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html?provider1",
+ iconURL: "chrome://branding/content/icon48.png"
+ },
+ {
+ name: "provider 2",
+ origin: "https://test1.example.com",
+ shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html?provider2",
+ iconURL: "chrome://branding/content/icon64.png"
+ },
+ {
+ name: "provider 3",
+ origin: "https://test2.example.com",
+ shareURL: "https://test2.example.com/browser/browser/base/content/test/social/social_share.html?provider2",
+ iconURL: "chrome://branding/content/about-logo.png"
+ }
+];
+
+
+function test() {
+ PopupNotifications.panel.setAttribute("animate", "false");
+ registerCleanupFunction(function () {
+ PopupNotifications.panel.removeAttribute("animate");
+ });
+ waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 1]]}, () => {
+ runSocialTests(tests, undefined, postTestCleanup);
+ });
+}
+
+var tests = {
+ testActivationWrongOrigin: function(next) {
+ // At this stage none of our providers exist, so we expect failure.
+ Services.prefs.setBoolPref("social.remote-install.enabled", false);
+ activateProvider(gTestDomains[0], function() {
+ is(SocialUI.enabled, false, "SocialUI is not enabled");
+ let panel = document.getElementById("servicesInstall-notification");
+ ok(panel.hidden, "activation panel still hidden");
+ checkSocialUI();
+ Services.prefs.clearUserPref("social.remote-install.enabled");
+ next();
+ });
+ },
+
+ testIFrameActivation: function(next) {
+ activateIFrameProvider(gTestDomains[0], function() {
+ is(SocialUI.enabled, false, "SocialUI is not enabled");
+ let panel = document.getElementById("servicesInstall-notification");
+ ok(panel.hidden, "activation panel still hidden");
+ checkSocialUI();
+ next();
+ });
+ },
+
+ testActivationFirstProvider: function(next) {
+ // first up we add a manifest entry for a single provider.
+ activateOneProvider(gProviders[0], false, function() {
+ // we deactivated leaving no providers left, so Social is disabled.
+ checkSocialUI();
+ next();
+ });
+ },
+
+ testActivationMultipleProvider: function(next) {
+ // The trick with this test is to make sure that Social.providers[1] is
+ // the current provider when doing the undo - this makes sure that the
+ // Social code doesn't fallback to Social.providers[0], which it will
+ // do in some cases (but those cases do not include what this test does)
+ // first enable the 2 providers
+ SocialService.addProvider(gProviders[0], function() {
+ SocialService.addProvider(gProviders[1], function() {
+ checkSocialUI();
+ // activate the last provider.
+ activateOneProvider(gProviders[2], false, function() {
+ // we deactivated - the first provider should be enabled.
+ checkSocialUI();
+ next();
+ });
+ });
+ });
+ },
+
+ testAddonManagerDoubleInstall: function(next) {
+ // Create a new tab and load about:addons
+ let addonsTab = gBrowser.addTab();
+ gBrowser.selectedTab = addonsTab;
+ BrowserOpenAddonsMgr('addons://list/service');
+ gBrowser.selectedBrowser.addEventListener("load", function tabLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", tabLoad, true);
+ is(addonsTab.linkedBrowser.currentURI.spec, "about:addons", "about:addons should load into blank tab.");
+
+ activateOneProvider(gProviders[0], true, function() {
+ info("first activation completed");
+ is(gBrowser.contentDocument.location.href, gProviders[0].origin + "/browser/browser/base/content/test/social/social_postActivation.html", "postActivationURL loaded");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab).then(() => {
+ is(gBrowser.contentDocument.location.href, gProviders[0].origin + "/browser/browser/base/content/test/social/social_activate_basic.html", "activation page selected");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab).then(() => {
+ tabsToRemove.pop();
+ // uninstall the provider
+ clickAddonRemoveButton(addonsTab, function(addon) {
+ checkSocialUI();
+ activateOneProvider(gProviders[0], true, function() {
+ info("second activation completed");
+ is(gBrowser.contentDocument.location.href, gProviders[0].origin + "/browser/browser/base/content/test/social/social_postActivation.html", "postActivationURL loaded");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab).then(() => {
+
+ // after closing the addons tab, verify provider is still installed
+ AddonManager.getAddonsByTypes(["service"], function(aAddons) {
+ is(aAddons.length, 1, "there can be only one");
+
+ let doc = addonsTab.linkedBrowser.contentDocument;
+ let list = doc.getElementById("addon-list");
+ is(list.childNodes.length, 1, "only one addon is displayed");
+
+ BrowserTestUtils.removeTab(addonsTab).then(next);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }, true);
+ }
+}
diff --git a/browser/base/content/test/social/head.js b/browser/base/content/test/social/head.js
new file mode 100644
index 000000000..ea175c97a
--- /dev/null
+++ b/browser/base/content/test/social/head.js
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+
+function promiseObserverNotified(aTopic) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
+ dump("notification promised "+aTopic);
+ Services.obs.removeObserver(onNotification, aTopic);
+ TestUtils.executeSoon(() => resolve({subject: aSubject, data: aData}));
+ }, aTopic, false);
+ });
+}
+
+// Check that a specified (string) URL hasn't been "remembered" (ie, is not
+// in history, will not appear in about:newtab or auto-complete, etc.)
+function promiseSocialUrlNotRemembered(url) {
+ return new Promise(resolve => {
+ let uri = Services.io.newURI(url, null, null);
+ PlacesUtils.asyncHistory.isURIVisited(uri, function(aURI, aIsVisited) {
+ ok(!aIsVisited, "social URL " + url + " should not be in global history");
+ resolve();
+ });
+ });
+}
+
+var gURLsNotRemembered = [];
+
+
+function checkProviderPrefsEmpty(isError) {
+ let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest.");
+ let prefs = MANIFEST_PREFS.getChildList("", []);
+ let c = 0;
+ for (let pref of prefs) {
+ if (MANIFEST_PREFS.prefHasUserValue(pref)) {
+ info("provider [" + pref + "] manifest left installed from previous test");
+ c++;
+ }
+ }
+ is(c, 0, "all provider prefs uninstalled from previous test");
+ is(Social.providers.length, 0, "all providers uninstalled from previous test " + Social.providers.length);
+}
+
+function defaultFinishChecks() {
+ checkProviderPrefsEmpty(true);
+ finish();
+}
+
+function runSocialTestWithProvider(manifest, callback, finishcallback) {
+
+ let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+
+ let manifests = Array.isArray(manifest) ? manifest : [manifest];
+
+ // Check that none of the provider's content ends up in history.
+ function* finishCleanUp() {
+ for (let i = 0; i < manifests.length; i++) {
+ let m = manifests[i];
+ for (let what of ['iconURL', 'shareURL']) {
+ if (m[what]) {
+ yield promiseSocialUrlNotRemembered(m[what]);
+ }
+ }
+ }
+ for (let i = 0; i < gURLsNotRemembered.length; i++) {
+ yield promiseSocialUrlNotRemembered(gURLsNotRemembered[i]);
+ }
+ gURLsNotRemembered = [];
+ }
+
+ info("runSocialTestWithProvider: " + manifests.toSource());
+
+ let finishCount = 0;
+ function finishIfDone(callFinish) {
+ finishCount++;
+ if (finishCount == manifests.length)
+ Task.spawn(finishCleanUp).then(finishcallback || defaultFinishChecks);
+ }
+ function removeAddedProviders(cleanup) {
+ manifests.forEach(function (m) {
+ // If we're "cleaning up", don't call finish when done.
+ let callback = cleanup ? function () {} : finishIfDone;
+ // Similarly, if we're cleaning up, catch exceptions from removeProvider
+ let removeProvider = SocialService.disableProvider.bind(SocialService);
+ if (cleanup) {
+ removeProvider = function (origin, cb) {
+ try {
+ SocialService.disableProvider(origin, cb);
+ } catch (ex) {
+ // Ignore "provider doesn't exist" errors.
+ if (ex.message.indexOf("SocialService.disableProvider: no provider with origin") == 0)
+ return;
+ info("Failed to clean up provider " + origin + ": " + ex);
+ }
+ }
+ }
+ removeProvider(m.origin, callback);
+ });
+ }
+ function finishSocialTest(cleanup) {
+ removeAddedProviders(cleanup);
+ }
+
+ let providersAdded = 0;
+
+ manifests.forEach(function (m) {
+ SocialService.addProvider(m, function(provider) {
+
+ providersAdded++;
+ info("runSocialTestWithProvider: provider added");
+
+ // we want to set the first specified provider as the UI's provider
+ if (provider.origin == manifests[0].origin) {
+ firstProvider = provider;
+ }
+
+ // If we've added all the providers we need, call the callback to start
+ // the tests (and give it a callback it can call to finish them)
+ if (providersAdded == manifests.length) {
+ registerCleanupFunction(function () {
+ finishSocialTest(true);
+ });
+ BrowserTestUtils.waitForCondition(() => provider.enabled,
+ "providers added and enabled").then(() => {
+ info("provider has been enabled");
+ callback(finishSocialTest);
+ });
+ }
+ });
+ });
+}
+
+function runSocialTests(tests, cbPreTest, cbPostTest, cbFinish) {
+ let testIter = (function*() {
+ for (let name in tests) {
+ if (tests.hasOwnProperty(name)) {
+ yield [name, tests[name]];
+ }
+ }
+ })();
+ let providersAtStart = Social.providers.length;
+ info("runSocialTests: start test run with " + providersAtStart + " providers");
+ window.focus();
+
+
+ if (cbPreTest === undefined) {
+ cbPreTest = function(cb) { cb() };
+ }
+ if (cbPostTest === undefined) {
+ cbPostTest = function(cb) { cb() };
+ }
+
+ function runNextTest() {
+ let result = testIter.next();
+ if (result.done) {
+ // out of items:
+ (cbFinish || defaultFinishChecks)();
+ is(providersAtStart, Social.providers.length,
+ "runSocialTests: finish test run with " + Social.providers.length + " providers");
+ return;
+ }
+ let [name, func] = result.value;
+ // We run on a timeout to help keep the debug messages sane.
+ executeSoon(function() {
+ function cleanupAndRunNextTest() {
+ info("sub-test " + name + " complete");
+ cbPostTest(runNextTest);
+ }
+ cbPreTest(function() {
+ info("pre-test: starting with " + Social.providers.length + " providers");
+ info("sub-test " + name + " starting");
+ try {
+ func.call(tests, cleanupAndRunNextTest);
+ } catch (ex) {
+ ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack);
+ cleanupAndRunNextTest();
+ }
+ })
+ });
+ }
+ runNextTest();
+}
+
+// A fairly large hammer which checks all aspects of the SocialUI for
+// internal consistency.
+function checkSocialUI(win) {
+ let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+ // if we have enabled providers, we should also have instances of those
+ // providers
+ if (SocialService.hasEnabledProviders) {
+ ok(Social.providers.length > 0, "providers are enabled");
+ } else {
+ is(Social.providers.length, 0, "providers are not enabled");
+ }
+}
+
+function setManifestPref(name, manifest) {
+ let string = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ string.data = JSON.stringify(manifest);
+ Services.prefs.setComplexValue(name, Ci.nsISupportsString, string);
+}
+
+function getManifestPrefname(aManifest) {
+ // is same as the generated name in SocialServiceInternal.getManifestPrefname
+ let originUri = Services.io.newURI(aManifest.origin, null, null);
+ return "social.manifest." + originUri.hostPort.replace('.', '-');
+}
+
+function ensureFrameLoaded(frame, uri) {
+ return new Promise(resolve => {
+ if (frame.contentDocument && frame.contentDocument.readyState == "complete" &&
+ (!uri || frame.contentDocument.location.href == uri)) {
+ resolve();
+ } else {
+ frame.addEventListener("load", function handler() {
+ if (uri && frame.contentDocument.location.href != uri)
+ return;
+ frame.removeEventListener("load", handler, true);
+ resolve()
+ }, true);
+ }
+ });
+}
+
+// Support for going on and offline.
+// (via browser/base/content/test/browser_bookmark_titles.js)
+var origProxyType = Services.prefs.getIntPref('network.proxy.type');
+
+function toggleOfflineStatus(goOffline) {
+ // Bug 968887 fix. when going on/offline, wait for notification before continuing
+ return new Promise(resolve => {
+ if (!goOffline) {
+ Services.prefs.setIntPref('network.proxy.type', origProxyType);
+ }
+ if (goOffline != Services.io.offline) {
+ info("initial offline state " + Services.io.offline);
+ let expect = !Services.io.offline;
+ Services.obs.addObserver(function offlineChange(subject, topic, data) {
+ Services.obs.removeObserver(offlineChange, "network:offline-status-changed");
+ info("offline state changed to " + Services.io.offline);
+ is(expect, Services.io.offline, "network:offline-status-changed successful toggle");
+ resolve();
+ }, "network:offline-status-changed", false);
+ BrowserOffline.toggleOfflineStatus();
+ } else {
+ resolve();
+ }
+ if (goOffline) {
+ Services.prefs.setIntPref('network.proxy.type', 0);
+ // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
+ Services.cache2.clear();
+ }
+ });
+}
+
+function goOffline() {
+ // Simulate a network outage with offline mode. (Localhost is still
+ // accessible in offline mode, so disable the test proxy as well.)
+ return toggleOfflineStatus(true);
+}
+
+function goOnline(callback) {
+ return toggleOfflineStatus(false);
+}
diff --git a/browser/base/content/test/social/microformats.html b/browser/base/content/test/social/microformats.html
new file mode 100644
index 000000000..1a0e4436b
--- /dev/null
+++ b/browser/base/content/test/social/microformats.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <head><title>Raspberry Pi Page</title></head>
+ <div class="hproduct">
+ <h2 class="fn">Raspberry Pi</h2>
+ <img class="photo" src="https://example.com/someimage.jpg" />
+ <p class="description">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It's a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>
+ <a class="url" href="https://example.com/">More info about the Raspberry Pi</a>
+ <p class="price">29.95</p>
+ <p class="review hreview"><span id="test-review" class="rating">4.5</span> out of 5</p>
+ <p>Categories:
+ <a rel="tag" href="https://example.com/wiki/computer" class="category">Computer</a>,
+ <a rel="tag" href="https://example.com/wiki/education" class="category">Education</a>
+ </p>
+ </div>
+ </body>
+</html>
diff --git a/browser/base/content/test/social/moz.png b/browser/base/content/test/social/moz.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/browser/base/content/test/social/moz.png
Binary files differ
diff --git a/browser/base/content/test/social/opengraph/og_invalid_url.html b/browser/base/content/test/social/opengraph/og_invalid_url.html
new file mode 100644
index 000000000..ad1dae2be
--- /dev/null
+++ b/browser/base/content/test/social/opengraph/og_invalid_url.html
@@ -0,0 +1,11 @@
+<html xmlns:og="http://ogp.me/ns#">
+<head>
+ <meta property="og:url" content="chrome://browser/content/aboutDialog.xul"/>
+ <meta property="og:site_name" content="Evil chrome delivering website"/>
+ <meta property="og:description"
+ content="A test corpus file for open graph tags passing a bad url"/>
+</head>
+<body>
+ Open Graph Test Page
+</body>
+</html>
diff --git a/browser/base/content/test/social/opengraph/opengraph.html b/browser/base/content/test/social/opengraph/opengraph.html
new file mode 100644
index 000000000..50b7703b8
--- /dev/null
+++ b/browser/base/content/test/social/opengraph/opengraph.html
@@ -0,0 +1,13 @@
+<html xmlns:og="http://ogp.me/ns#">
+<head>
+ <meta property="og:title" content="&gt;This is my title&lt;"/>
+ <meta property="og:url" content="https://www.mozilla.org"/>
+ <meta property="og:image" content="https://www.mozilla.org/favicon.png"/>
+ <meta property="og:site_name" content="&#62;My simple test page&#60;"/>
+ <meta property="og:description"
+ content="A test corpus file for open graph tags we care about"/>
+</head>
+<body>
+ Open Graph Test Page
+</body>
+</html>
diff --git a/browser/base/content/test/social/opengraph/shortlink_linkrel.html b/browser/base/content/test/social/opengraph/shortlink_linkrel.html
new file mode 100644
index 000000000..54c40c376
--- /dev/null
+++ b/browser/base/content/test/social/opengraph/shortlink_linkrel.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <link rel="image_src" href="http://example.com/1234/56789.jpg" id="image-src" />
+ <link id="canonicalurl" rel="canonical" href="http://www.example.com/photos/56789/" />
+ <link rel="shortlink" href="http://imshort/p/abcde" />
+</head>
+<body>
+ link[rel='shortlink']
+</body>
+</html>
diff --git a/browser/base/content/test/social/opengraph/shorturl_link.html b/browser/base/content/test/social/opengraph/shorturl_link.html
new file mode 100644
index 000000000..667122cea
--- /dev/null
+++ b/browser/base/content/test/social/opengraph/shorturl_link.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+ <link rel="image_src" href="http://example.com/1234/56789.jpg" id="image-src" />
+ <link id="canonicalurl" rel="canonical" href="http://www.example.com/photos/56789/" />
+ <link id="shorturl" rev="canonical" type="text/html" href="http://imshort/p/abcde" />
+</head>
+<body>
+ link id="shorturl"
+</body>
+</html>
diff --git a/browser/base/content/test/social/opengraph/shorturl_linkrel.html b/browser/base/content/test/social/opengraph/shorturl_linkrel.html
new file mode 100644
index 000000000..36533528e
--- /dev/null
+++ b/browser/base/content/test/social/opengraph/shorturl_linkrel.html
@@ -0,0 +1,25 @@
+<html>
+<head>
+ <title>Test Image</title>
+
+ <meta name="description" content="Iron man in a tutu" />
+ <meta name="title" content="Test Image" />
+
+ <meta name="medium" content="image" />
+ <link rel="image_src" href="http://example.com/1234/56789.jpg" id="image-src" />
+ <link id="canonicalurl" rel="canonical" href="http://www.example.com/photos/56789/" />
+ <link id="shorturl" href="http://imshort/p/abcde" />
+
+ <meta property="og:title" content="TestImage" />
+ <meta property="og:type" content="photos:photo" />
+ <meta property="og:url" content="http://www.example.com/photos/56789/" />
+ <meta property="og:site_name" content="My Photo Site" />
+ <meta property="og:description" content="Iron man in a tutu" />
+ <meta property="og:image" content="http://example.com/1234/56789.jpg" />
+ <meta property="og:image:width" content="480" />
+ <meta property="og:image:height" content="640" />
+</head>
+<body>
+ link[rel='shorturl']
+</body>
+</html>
diff --git a/browser/base/content/test/social/share.html b/browser/base/content/test/social/share.html
new file mode 100644
index 000000000..55cba9844
--- /dev/null
+++ b/browser/base/content/test/social/share.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body onload="document.getElementById('testclose').focus()">
+ <p>This is a test social share window.</p>
+ <input id="testclose"/>
+ </body>
+</html>
diff --git a/browser/base/content/test/social/share_activate.html b/browser/base/content/test/social/share_activate.html
new file mode 100644
index 000000000..69707e705
--- /dev/null
+++ b/browser/base/content/test/social/share_activate.html
@@ -0,0 +1,35 @@
+<html>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<head>
+ <title>Activation test</title>
+</head>
+<script>
+
+var data = {
+ // currently required
+ "name": "Demo Social Service",
+ // browser_share.js serves this page from "https://example.com"
+ "origin": "https://example.com",
+ "iconURL": "chrome://branding/content/icon16.png",
+ "icon32URL": "chrome://branding/content/favicon32.png",
+ "icon64URL": "chrome://branding/content/icon64.png",
+ "shareURL": "/browser/browser/base/content/test/social/share.html"
+}
+
+function activate(node) {
+ node.setAttribute("data-service", JSON.stringify(data));
+ var event = new CustomEvent("ActivateSocialFeature");
+ node.dispatchEvent(event);
+}
+
+</script>
+<body>
+
+nothing to see here
+
+<button id="activation" onclick="activate(this, true)">Activate the share provider</button>
+
+</body>
+</html>
diff --git a/browser/base/content/test/social/social_activate.html b/browser/base/content/test/social/social_activate.html
new file mode 100644
index 000000000..78da597a1
--- /dev/null
+++ b/browser/base/content/test/social/social_activate.html
@@ -0,0 +1,41 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Activation test</title>
+</head>
+<script>
+// icons from http://findicons.com/icon/158311/firefox?id=356182 by ipapun
+var data = {
+ // currently required
+ "name": "Demo Social Service",
+ "iconURL": "chrome://branding/content/icon16.png",
+ "icon32URL": "chrome://branding/content/favicon32.png",
+ "icon64URL": "chrome://branding/content/icon64.png",
+
+ // at least one of these must be defined
+ "shareURL": "/browser/browser/base/content/test/social/social_share.html",
+ "postActivationURL": "/browser/browser/base/content/test/social/social_postActivation.html",
+
+ // should be available for display purposes
+ "description": "A short paragraph about this provider",
+ "author": "Shane Caraveo, Mozilla",
+
+ // optional
+ "version": "1.0"
+}
+
+function activate(node) {
+ node.setAttribute("data-service", JSON.stringify(data));
+ var event = new CustomEvent("ActivateSocialFeature");
+ node.dispatchEvent(event);
+}
+
+</script>
+<body>
+
+nothing to see here
+
+<button id="activation" onclick="activate(this)">Activate The Demo Provider</button>
+
+</body>
+</html>
diff --git a/browser/base/content/test/social/social_activate_basic.html b/browser/base/content/test/social/social_activate_basic.html
new file mode 100644
index 000000000..78da597a1
--- /dev/null
+++ b/browser/base/content/test/social/social_activate_basic.html
@@ -0,0 +1,41 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Activation test</title>
+</head>
+<script>
+// icons from http://findicons.com/icon/158311/firefox?id=356182 by ipapun
+var data = {
+ // currently required
+ "name": "Demo Social Service",
+ "iconURL": "chrome://branding/content/icon16.png",
+ "icon32URL": "chrome://branding/content/favicon32.png",
+ "icon64URL": "chrome://branding/content/icon64.png",
+
+ // at least one of these must be defined
+ "shareURL": "/browser/browser/base/content/test/social/social_share.html",
+ "postActivationURL": "/browser/browser/base/content/test/social/social_postActivation.html",
+
+ // should be available for display purposes
+ "description": "A short paragraph about this provider",
+ "author": "Shane Caraveo, Mozilla",
+
+ // optional
+ "version": "1.0"
+}
+
+function activate(node) {
+ node.setAttribute("data-service", JSON.stringify(data));
+ var event = new CustomEvent("ActivateSocialFeature");
+ node.dispatchEvent(event);
+}
+
+</script>
+<body>
+
+nothing to see here
+
+<button id="activation" onclick="activate(this)">Activate The Demo Provider</button>
+
+</body>
+</html>
diff --git a/browser/base/content/test/social/social_activate_iframe.html b/browser/base/content/test/social/social_activate_iframe.html
new file mode 100644
index 000000000..bde884c9d
--- /dev/null
+++ b/browser/base/content/test/social/social_activate_iframe.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <title>Activation iframe test</title>
+</head>
+
+<body>
+
+<iframe src="social_activate_basic.html"/>
+
+</body>
+</html>
diff --git a/browser/base/content/test/social/social_crash_content_helper.js b/browser/base/content/test/social/social_crash_content_helper.js
new file mode 100644
index 000000000..4698b6957
--- /dev/null
+++ b/browser/base/content/test/social/social_crash_content_helper.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var Cu = Components.utils;
+
+// Ideally we would use CrashTestUtils.jsm, but that's only available for
+// xpcshell tests - so we just copy a ctypes crasher from it.
+Cu.import("resource://gre/modules/ctypes.jsm");
+var crash = function() { // this will crash when called.
+ let zero = new ctypes.intptr_t(8);
+ let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
+ badptr.contents
+};
+
+
+var TestHelper = {
+ init: function() {
+ addMessageListener("social-test:crash", this);
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "social-test:crash":
+ privateNoteIntentionalCrash();
+ crash();
+ break;
+ }
+ },
+}
+
+TestHelper.init();
diff --git a/browser/base/content/test/social/social_postActivation.html b/browser/base/content/test/social/social_postActivation.html
new file mode 100644
index 000000000..e0a6acfdf
--- /dev/null
+++ b/browser/base/content/test/social/social_postActivation.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Post-Activation test</title>
+</head>
+
+<body>
+
+Post Activation landing page
+
+</body>
+</html>
diff --git a/browser/base/content/test/tabPrompts/.eslintrc.js b/browser/base/content/test/tabPrompts/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/tabPrompts/browser.ini b/browser/base/content/test/tabPrompts/browser.ini
new file mode 100644
index 000000000..9b94f14c5
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser.ini
@@ -0,0 +1,4 @@
+[browser_closeTabSpecificPanels.js]
+[browser_multiplePrompts.js]
+[browser_openPromptInBackgroundTab.js]
+support-files = openPromptOffTimeout.html
diff --git a/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js b/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js
new file mode 100644
index 000000000..30c15a56f
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js
@@ -0,0 +1,41 @@
+"use strict";
+
+/*
+ * This test creates multiple panels, one that has been tagged as specific to its tab's content
+ * and one that isn't. When a tab loses focus, panel specific to that tab should close.
+ * The non-specific panel should remain open.
+ *
+ */
+
+add_task(function*() {
+ let tab1 = gBrowser.addTab("http://mochi.test:8888/#0");
+ let tab2 = gBrowser.addTab("http://mochi.test:8888/#1");
+ let specificPanel = document.createElement("panel");
+ specificPanel.setAttribute("tabspecific", "true");
+ let generalPanel = document.createElement("panel");
+ let anchor = document.getElementById(CustomizableUI.AREA_NAVBAR);
+
+ anchor.appendChild(specificPanel);
+ anchor.appendChild(generalPanel);
+ is(specificPanel.state, "closed", "specificPanel starts as closed");
+ is(generalPanel.state, "closed", "generalPanel starts as closed");
+
+ let specificPanelPromise = BrowserTestUtils.waitForEvent(specificPanel, "popupshown");
+ specificPanel.openPopupAtScreen(210, 210);
+ yield specificPanelPromise;
+ is(specificPanel.state, "open", "specificPanel has been opened");
+
+ let generalPanelPromise = BrowserTestUtils.waitForEvent(generalPanel, "popupshown");
+ generalPanel.openPopupAtScreen(510, 510);
+ yield generalPanelPromise;
+ is(generalPanel.state, "open", "generalPanel has been opened");
+
+ gBrowser.tabContainer.advanceSelectedTab(-1, true);
+ is(specificPanel.state, "closed", "specificPanel panel is closed after its tab loses focus");
+ is(generalPanel.state, "open", "generalPanel is still open after tab switch");
+
+ specificPanel.remove();
+ generalPanel.remove();
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+});
diff --git a/browser/base/content/test/tabPrompts/browser_multiplePrompts.js b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js
new file mode 100644
index 000000000..c548429ea
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js
@@ -0,0 +1,72 @@
+"use strict";
+
+/*
+ * This test triggers multiple alerts on one single tab, because it"s possible
+ * for web content to do so. The behavior is described in bug 1266353.
+ *
+ * We assert the presentation of the multiple alerts, ensuring we show only
+ * the oldest one.
+ */
+add_task(function*() {
+ const PROMPTCOUNT = 5;
+
+ let contentScript = function() {
+ var i = 5; // contentScript has no access to PROMPTCOUNT.
+ window.addEventListener("message", function() {
+ i--;
+ if (i) {
+ window.postMessage("ping", "*");
+ }
+ alert("Alert countdown #" + i);
+ });
+ window.postMessage("ping", "*");
+ };
+ let url = "data:text/html,<script>(" + encodeURIComponent(contentScript.toSource()) + ")();</script>"
+
+ let promptsOpenedPromise = new Promise(function(resolve) {
+ let unopenedPromptCount = PROMPTCOUNT;
+ Services.obs.addObserver(function observer() {
+ unopenedPromptCount--;
+ if (!unopenedPromptCount) {
+ Services.obs.removeObserver(observer, "tabmodal-dialog-loaded");
+ info("Prompts opened.");
+ resolve();
+ }
+ }, "tabmodal-dialog-loaded", false);
+ });
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
+ info("Tab loaded");
+
+ yield promptsOpenedPromise;
+
+ let promptsCount = PROMPTCOUNT;
+ while (promptsCount--) {
+ let prompts = tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+ is(prompts.length, promptsCount + 1, "There should be " + (promptsCount + 1) + " prompt(s).");
+ // The oldest should be the first.
+ let i = 0;
+ for (let prompt of prompts) {
+ is(prompt.Dialog.args.text, "Alert countdown #" + i, "The #" + i + " alert should be labelled as such.");
+ if (i !== promptsCount) {
+ is(prompt.hidden, true, "This prompt should be hidden.");
+ i++;
+ continue;
+ }
+
+ is(prompt.hidden, false, "The last prompt should not be hidden.");
+ prompt.onButtonClick(0);
+
+ // The click is handled async; wait for an event loop turn for that to
+ // happen.
+ yield new Promise(function(resolve) {
+ Services.tm.mainThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+ });
+ }
+ }
+
+ let prompts = tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+ is(prompts.length, 0, "Prompts should all be dismissed.");
+
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
new file mode 100644
index 000000000..d244d157a
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
@@ -0,0 +1,66 @@
+"use strict";
+
+const ROOT = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://example.com/");
+let pageWithAlert = ROOT + "openPromptOffTimeout.html";
+
+registerCleanupFunction(function() {
+ Services.perms.removeAll(makeURI(pageWithAlert));
+});
+
+/*
+ * This test opens a tab that alerts when it is hidden. We then switch away
+ * from the tab, and check that by default the tab is not automatically
+ * re-selected. We also check that a checkbox appears in the alert that allows
+ * the user to enable this automatically re-selecting. We then check that
+ * checking the checkbox does actually enable that behaviour.
+ */
+add_task(function*() {
+ yield SpecialPowers.pushPrefEnv({"set": [["browser.tabs.dontfocusfordialogs", true]]});
+ let firstTab = gBrowser.selectedTab;
+ // load page that opens prompt when page is hidden
+ let openedTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageWithAlert, true);
+ let openedTabGotAttentionPromise = BrowserTestUtils.waitForAttribute("attention", openedTab, "true");
+ // switch away from that tab again - this triggers the alert.
+ yield BrowserTestUtils.switchTab(gBrowser, firstTab);
+ // ... but that's async on e10s...
+ yield openedTabGotAttentionPromise;
+ // check for attention attribute
+ is(openedTab.getAttribute("attention"), "true", "Tab with alert should have 'attention' attribute.");
+ ok(!openedTab.selected, "Tab with alert should not be selected");
+
+ // switch tab back, and check the checkbox is displayed:
+ yield BrowserTestUtils.switchTab(gBrowser, openedTab);
+ // check the prompt is there, and the extra row is present
+ let prompts = openedTab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+ is(prompts.length, 1, "There should be 1 prompt");
+ let ourPrompt = prompts[0];
+ let row = ourPrompt.querySelector("row");
+ ok(row, "Should have found the row with our checkbox");
+ let checkbox = row.querySelector("checkbox[label*='example.com']");
+ ok(checkbox, "The checkbox should be there");
+ ok(!checkbox.checked, "Checkbox shouldn't be checked");
+ // tick box and accept dialog
+ checkbox.checked = true;
+ ourPrompt.onButtonClick(0);
+ // Wait for that click to actually be handled completely.
+ yield new Promise(function(resolve) {
+ Services.tm.mainThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+ });
+ // check permission is set
+ let ps = Services.perms;
+ is(ps.ALLOW_ACTION, ps.testPermission(makeURI(pageWithAlert), "focus-tab-by-prompt"),
+ "Tab switching should now be allowed");
+
+ let openedTabSelectedPromise = BrowserTestUtils.waitForAttribute("selected", openedTab, "true");
+ // switch to other tab again
+ yield BrowserTestUtils.switchTab(gBrowser, firstTab);
+
+ // This is sync in non-e10s, but in e10s we need to wait for this, so yield anyway.
+ // Note that the switchTab promise doesn't actually guarantee anything about *which*
+ // tab ends up as selected when its event fires, so using that here wouldn't work.
+ yield openedTabSelectedPromise;
+ // should be switched back
+ ok(openedTab.selected, "Ta-dah, the other tab should now be selected again!");
+
+ yield BrowserTestUtils.removeTab(openedTab);
+});
diff --git a/browser/base/content/test/tabPrompts/openPromptOffTimeout.html b/browser/base/content/test/tabPrompts/openPromptOffTimeout.html
new file mode 100644
index 000000000..e865c7872
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/openPromptOffTimeout.html
@@ -0,0 +1,10 @@
+<body>
+This page opens an alert box when the page is hidden.
+<script>
+document.addEventListener("visibilitychange", () => {
+ if (document.hidden) {
+ alert("You hid my page!");
+ }
+}, false);
+</script>
+</body>
diff --git a/browser/base/content/test/tabcrashed/browser.ini b/browser/base/content/test/tabcrashed/browser.ini
new file mode 100644
index 000000000..051b40d9f
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files =
+ head.js
+[browser_shown.js]
+skip-if = !e10s || !crashreporter
+[browser_clearEmail.js]
+skip-if = !e10s || !crashreporter
+[browser_showForm.js]
+skip-if = !e10s || !crashreporter
+[browser_withoutDump.js]
+skip-if = !e10s
+[browser_autoSubmitRequest.js]
+skip-if = !e10s || !crashreporter
diff --git a/browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js b/browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js
new file mode 100644
index 000000000..778331814
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_autoSubmitRequest.js
@@ -0,0 +1,152 @@
+"use strict";
+
+const PAGE = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const AUTOSUBMIT_PREF = "browser.crashReports.unsubmittedCheck.autoSubmit2";
+
+const {TabStateFlusher} =
+ Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+
+// On debug builds, crashing tabs results in much thinking, which
+// slows down the test and results in intermittent test timeouts,
+// so we'll pump up the expected timeout for this test.
+requestLongerTimeout(2);
+
+/**
+ * Tests that if the user is not configured to autosubmit
+ * backlogged crash reports, that we offer to do that, and
+ * that the user can accept that offer.
+ */
+add_task(function* test_show_form() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [[AUTOSUBMIT_PREF, false]],
+ })
+
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ // Make sure we've flushed the browser messages so that
+ // we can restore it.
+ yield TabStateFlusher.flush(browser);
+
+ // Now crash the browser.
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ let doc = browser.contentDocument;
+
+ // Ensure the request is visible. We can safely reach into
+ // the content since about:tabcrashed is an in-process URL.
+ let requestAutoSubmit = doc.getElementById("requestAutoSubmit");
+ Assert.ok(!requestAutoSubmit.hidden,
+ "Request for autosubmission is visible.");
+
+ // Since the pref is set to false, the checkbox should be
+ // unchecked.
+ let autoSubmit = doc.getElementById("autoSubmit");
+ Assert.ok(!autoSubmit.checked,
+ "Checkbox for autosubmission is not checked.")
+
+ // Check the checkbox, and then restore the tab.
+ autoSubmit.checked = true;
+ let restoreButton = doc.getElementById("restoreTab");
+ restoreButton.click();
+
+ yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
+
+ // The autosubmission pref should now be set.
+ Assert.ok(Services.prefs.getBoolPref(AUTOSUBMIT_PREF),
+ "Autosubmission pref should have been set.");
+ });
+});
+
+/**
+ * Tests that if the user is autosubmitting backlogged crash reports
+ * that we don't make the offer again.
+ */
+add_task(function* test_show_form() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [[AUTOSUBMIT_PREF, true]],
+ })
+
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ yield TabStateFlusher.flush(browser);
+ // Now crash the browser.
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ let doc = browser.contentDocument;
+
+ // Ensure the request is NOT visible. We can safely reach into
+ // the content since about:tabcrashed is an in-process URL.
+ let requestAutoSubmit = doc.getElementById("requestAutoSubmit");
+ Assert.ok(requestAutoSubmit.hidden,
+ "Request for autosubmission is not visible.");
+
+ // Restore the tab.
+ let restoreButton = doc.getElementById("restoreTab");
+ restoreButton.click();
+
+ yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
+
+ // The autosubmission pref should still be set to true.
+ Assert.ok(Services.prefs.getBoolPref(AUTOSUBMIT_PREF),
+ "Autosubmission pref should have been set.");
+ });
+});
+
+/**
+ * Tests that we properly set the autoSubmit preference if the user is
+ * presented with a tabcrashed page without a crash report.
+ */
+add_task(function* test_no_offer() {
+ // We should default to sending the report.
+ Assert.ok(TabCrashHandler.prefs.getBoolPref("sendReport"));
+
+ yield SpecialPowers.pushPrefEnv({
+ set: [[AUTOSUBMIT_PREF, false]],
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ yield TabStateFlusher.flush(browser);
+
+ // Make it so that it seems like no dump is available for the next crash.
+ prepareNoDump();
+
+ // Now crash the browser.
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ let doc = browser.contentDocument;
+
+ // Ensure the request to autosubmit is invisible, since there's no report.
+ let requestRect = doc.getElementById("requestAutoSubmit")
+ .getBoundingClientRect();
+ Assert.equal(0, requestRect.height,
+ "Request for autosubmission has no height");
+ Assert.equal(0, requestRect.width,
+ "Request for autosubmission has no width");
+
+ // Since the pref is set to false, the checkbox should be
+ // unchecked.
+ let autoSubmit = doc.getElementById("autoSubmit");
+ Assert.ok(!autoSubmit.checked,
+ "Checkbox for autosubmission is not checked.");
+
+ let restoreButton = doc.getElementById("restoreTab");
+ restoreButton.click();
+
+ yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
+
+ // The autosubmission pref should now be set.
+ Assert.ok(!Services.prefs.getBoolPref(AUTOSUBMIT_PREF),
+ "Autosubmission pref should not have changed.");
+ });
+
+ // We should not have changed the default value for sending the report.
+ Assert.ok(TabCrashHandler.prefs.getBoolPref("sendReport"));
+});
diff --git a/browser/base/content/test/tabcrashed/browser_clearEmail.js b/browser/base/content/test/tabcrashed/browser_clearEmail.js
new file mode 100644
index 000000000..9ec04944f
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_clearEmail.js
@@ -0,0 +1,85 @@
+"use strict";
+
+const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
+const PAGE = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const EMAIL = "foo@privacy.com";
+
+/**
+ * Sets up the browser to send crash reports to the local crash report
+ * testing server.
+ */
+add_task(function* setup() {
+ // The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables crash
+ // reports. This test needs them enabled. The test also needs a mock
+ // report server, and fortunately one is already set up by toolkit/
+ // crashreporter/test/Makefile.in. Assign its URL to MOZ_CRASHREPORTER_URL,
+ // which CrashSubmit.jsm uses as a server override.
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
+ let serverUrl = env.get("MOZ_CRASHREPORTER_URL");
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
+ env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
+
+ // By default, requesting the email address of the user is disabled.
+ // For the purposes of this test, we turn it back on.
+ yield SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.crashReporting.requestEmail", true]],
+ });
+
+ registerCleanupFunction(function() {
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
+ env.set("MOZ_CRASHREPORTER_URL", serverUrl);
+ });
+});
+
+/**
+ * Test that if we have an email address stored in prefs, and we decide
+ * not to submit the email address in the next crash report, that we
+ * clear the email address.
+ */
+add_task(function* test_clear_email() {
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ let prefs = TabCrashHandler.prefs;
+ let originalSendReport = prefs.getBoolPref("sendReport");
+ let originalEmailMe = prefs.getBoolPref("emailMe");
+ let originalIncludeURL = prefs.getBoolPref("includeURL");
+ let originalEmail = prefs.getCharPref("email");
+
+ // Pretend that we stored an email address from the previous
+ // crash
+ prefs.setCharPref("email", EMAIL);
+ prefs.setBoolPref("emailMe", true);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ yield BrowserTestUtils.crashBrowser(browser);
+ let doc = browser.contentDocument;
+
+ // Since about:tabcrashed will run in the parent process, we can safely
+ // manipulate its DOM nodes directly
+ let emailMe = doc.getElementById("emailMe");
+ emailMe.checked = false;
+
+ let crashReport = promiseCrashReport({
+ Email: "",
+ });
+
+ let restoreTab = browser.contentDocument.getElementById("restoreTab");
+ restoreTab.click();
+ yield BrowserTestUtils.waitForEvent(tab, "SSTabRestored");
+ yield crashReport;
+
+ is(prefs.getCharPref("email"), "", "No email address should be stored");
+
+ // Submitting the crash report may have set some prefs regarding how to
+ // send tab crash reports. Let's reset them for the next test.
+ prefs.setBoolPref("sendReport", originalSendReport);
+ prefs.setBoolPref("emailMe", originalEmailMe);
+ prefs.setBoolPref("includeURL", originalIncludeURL);
+ prefs.setCharPref("email", originalEmail);
+ });
+});
+
diff --git a/browser/base/content/test/tabcrashed/browser_showForm.js b/browser/base/content/test/tabcrashed/browser_showForm.js
new file mode 100644
index 000000000..780af93fb
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_showForm.js
@@ -0,0 +1,40 @@
+"use strict";
+
+const PAGE = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+// On debug builds, crashing tabs results in much thinking, which
+// slows down the test and results in intermittent test timeouts,
+// so we'll pump up the expected timeout for this test.
+requestLongerTimeout(2);
+
+/**
+ * Tests that we show the about:tabcrashed additional details form
+ * if the "submit a crash report" checkbox was checked by default.
+ */
+add_task(function* test_show_form() {
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ // Flip the pref so that the checkbox should be checked
+ // by default.
+ let pref = TabCrashHandler.prefs.root + "sendReport";
+ yield SpecialPowers.pushPrefEnv({
+ set: [[pref, true]]
+ });
+
+ // Now crash the browser.
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ let doc = browser.contentDocument;
+
+ // Ensure the checkbox is checked. We can safely reach into
+ // the content since about:tabcrashed is an in-process URL.
+ let checkbox = doc.getElementById("sendReport");
+ ok(checkbox.checked, "Send report checkbox is checked.");
+
+ // Ensure the options form is displayed.
+ let options = doc.getElementById("options");
+ ok(!options.hidden, "Showing the crash report options form.");
+ });
+});
diff --git a/browser/base/content/test/tabcrashed/browser_shown.js b/browser/base/content/test/tabcrashed/browser_shown.js
new file mode 100644
index 000000000..d09d9438f
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_shown.js
@@ -0,0 +1,203 @@
+"use strict";
+
+const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
+const PAGE = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const COMMENTS = "Here's my test comment!";
+const EMAIL = "foo@privacy.com";
+
+/**
+ * Sets up the browser to send crash reports to the local crash report
+ * testing server.
+ */
+add_task(function* setup() {
+ // The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables crash
+ // reports. This test needs them enabled. The test also needs a mock
+ // report server, and fortunately one is already set up by toolkit/
+ // crashreporter/test/Makefile.in. Assign its URL to MOZ_CRASHREPORTER_URL,
+ // which CrashSubmit.jsm uses as a server override.
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
+ let serverUrl = env.get("MOZ_CRASHREPORTER_URL");
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
+ env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
+
+ // On debug builds, crashing tabs results in much thinking, which
+ // slows down the test and results in intermittent test timeouts,
+ // so we'll pump up the expected timeout for this test.
+ requestLongerTimeout(2);
+
+ registerCleanupFunction(function() {
+ env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
+ env.set("MOZ_CRASHREPORTER_URL", serverUrl);
+ });
+});
+
+/**
+ * This function returns a Promise that resolves once the following
+ * actions have taken place:
+ *
+ * 1) A new tab is opened up at PAGE
+ * 2) The tab is crashed
+ * 3) The about:tabcrashed page's fields are set in accordance with
+ * fieldValues
+ * 4) The tab is restored
+ * 5) A crash report is received from the testing server
+ * 6) Any tab crash prefs that were overwritten are reset
+ *
+ * @param fieldValues
+ * An Object describing how to set the about:tabcrashed
+ * fields. The following properties are accepted:
+ *
+ * comments (String)
+ * The comments to put in the comment textarea
+ * email (String)
+ * The email address to put in the email address input
+ * emailMe (bool)
+ * The checked value of the "Email me" checkbox
+ * includeURL (bool)
+ * The checked value of the "Include URL" checkbox
+ *
+ * If any of these fields are missing, the defaults from
+ * the user preferences are used.
+ * @param expectedExtra
+ * An Object describing the expected values that the submitted
+ * crash report's extra data should contain.
+ * @returns Promise
+ */
+function crashTabTestHelper(fieldValues, expectedExtra) {
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ let prefs = TabCrashHandler.prefs;
+ let originalSendReport = prefs.getBoolPref("sendReport");
+ let originalEmailMe = prefs.getBoolPref("emailMe");
+ let originalIncludeURL = prefs.getBoolPref("includeURL");
+ let originalEmail = prefs.getCharPref("email");
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ yield BrowserTestUtils.crashBrowser(browser);
+ let doc = browser.contentDocument;
+
+ // Since about:tabcrashed will run in the parent process, we can safely
+ // manipulate its DOM nodes directly
+ let comments = doc.getElementById("comments");
+ let email = doc.getElementById("email");
+ let emailMe = doc.getElementById("emailMe");
+ let includeURL = doc.getElementById("includeURL");
+
+ if (fieldValues.hasOwnProperty("comments")) {
+ comments.value = fieldValues.comments;
+ }
+
+ if (fieldValues.hasOwnProperty("email")) {
+ email.value = fieldValues.email;
+ }
+
+ if (fieldValues.hasOwnProperty("emailMe")) {
+ emailMe.checked = fieldValues.emailMe;
+ }
+
+ if (fieldValues.hasOwnProperty("includeURL")) {
+ includeURL.checked = fieldValues.includeURL;
+ }
+
+ let crashReport = promiseCrashReport(expectedExtra);
+ let restoreTab = browser.contentDocument.getElementById("restoreTab");
+ restoreTab.click();
+ yield BrowserTestUtils.waitForEvent(tab, "SSTabRestored");
+ yield crashReport;
+
+ // Submitting the crash report may have set some prefs regarding how to
+ // send tab crash reports. Let's reset them for the next test.
+ prefs.setBoolPref("sendReport", originalSendReport);
+ prefs.setBoolPref("emailMe", originalEmailMe);
+ prefs.setBoolPref("includeURL", originalIncludeURL);
+ prefs.setCharPref("email", originalEmail);
+ });
+}
+
+/**
+ * Tests what we send with the crash report by default. By default, we do not
+ * send any comments, the URL of the crashing page, or the email address of
+ * the user.
+ */
+add_task(function* test_default() {
+ yield crashTabTestHelper({}, {
+ "Comments": null,
+ "URL": "",
+ "Email": null,
+ });
+});
+
+/**
+ * Test just sending a comment.
+ */
+add_task(function* test_just_a_comment() {
+ yield crashTabTestHelper({
+ comments: COMMENTS,
+ }, {
+ "Comments": COMMENTS,
+ "URL": "",
+ "Email": null,
+ });
+});
+
+/**
+ * Test that we don't send email if emailMe is unchecked
+ */
+add_task(function* test_no_email() {
+ yield crashTabTestHelper({
+ email: EMAIL,
+ emailMe: false,
+ }, {
+ "Comments": null,
+ "URL": "",
+ "Email": null,
+ });
+});
+
+/**
+ * Test that we can send an email address if emailMe is checked
+ */
+add_task(function* test_yes_email() {
+ yield crashTabTestHelper({
+ email: EMAIL,
+ emailMe: true,
+ }, {
+ "Comments": null,
+ "URL": "",
+ "Email": EMAIL,
+ });
+});
+
+/**
+ * Test that we will send the URL of the page if includeURL is checked.
+ */
+add_task(function* test_send_URL() {
+ yield crashTabTestHelper({
+ includeURL: true,
+ }, {
+ "Comments": null,
+ "URL": PAGE,
+ "Email": null,
+ });
+});
+
+/**
+ * Test that we can send comments, the email address, and the URL
+ */
+add_task(function* test_send_all() {
+ yield crashTabTestHelper({
+ includeURL: true,
+ emailMe: true,
+ email: EMAIL,
+ comments: COMMENTS,
+ }, {
+ "Comments": COMMENTS,
+ "URL": PAGE,
+ "Email": EMAIL,
+ });
+});
+
diff --git a/browser/base/content/test/tabcrashed/browser_withoutDump.js b/browser/base/content/test/tabcrashed/browser_withoutDump.js
new file mode 100644
index 000000000..62557f443
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/browser_withoutDump.js
@@ -0,0 +1,36 @@
+"use strict";
+
+const PAGE = "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+
+add_task(function* setup() {
+ prepareNoDump();
+});
+
+/**
+ * Tests tab crash page when a dump is not available.
+ */
+add_task(function* test_without_dump() {
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: PAGE,
+ }, function*(browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ yield BrowserTestUtils.crashBrowser(browser);
+
+ let tabRemovedPromise = BrowserTestUtils.removeTab(tab, { dontRemove: true });
+
+ yield ContentTask.spawn(browser, null, function*() {
+ let doc = content.document;
+ Assert.ok(!doc.documentElement.classList.contains("crashDumpAvailable"),
+ "doesn't have crash dump");
+
+ let options = doc.getElementById("options");
+ Assert.ok(options, "has crash report options");
+ Assert.ok(options.hidden, "crash report options are hidden");
+
+ doc.getElementById("closeTab").click();
+ });
+
+ yield tabRemovedPromise;
+ });
+});
diff --git a/browser/base/content/test/tabcrashed/head.js b/browser/base/content/test/tabcrashed/head.js
new file mode 100644
index 000000000..6eee08f13
--- /dev/null
+++ b/browser/base/content/test/tabcrashed/head.js
@@ -0,0 +1,110 @@
+/**
+ * Returns a Promise that resolves once a crash report has
+ * been submitted. This function will also test the crash
+ * reports extra data to see if it matches expectedExtra.
+ *
+ * @param expectedExtra (object)
+ * An Object whose key-value pairs will be compared
+ * against the key-value pairs in the extra data of the
+ * crash report. A test failure will occur if there is
+ * a mismatch.
+ *
+ * If the value of the key-value pair is "null", this will
+ * be interpreted as "this key should not be included in the
+ * extra data", and will cause a test failure if it is detected
+ * in the crash report.
+ *
+ * Note that this will ignore any keys that are not included
+ * in expectedExtra. It's possible that the crash report
+ * will contain other extra information that is not
+ * compared against.
+ * @returns Promise
+ */
+function promiseCrashReport(expectedExtra={}) {
+ return Task.spawn(function*() {
+ info("Starting wait on crash-report-status");
+ let [subject, ] =
+ yield TestUtils.topicObserved("crash-report-status", (unused, data) => {
+ return data == "success";
+ });
+ info("Topic observed!");
+
+ if (!(subject instanceof Ci.nsIPropertyBag2)) {
+ throw new Error("Subject was not a Ci.nsIPropertyBag2");
+ }
+
+ let remoteID = getPropertyBagValue(subject, "serverCrashID");
+ if (!remoteID) {
+ throw new Error("Report should have a server ID");
+ }
+
+ let file = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
+ file.initWithPath(Services.crashmanager._submittedDumpsDir);
+ file.append(remoteID + ".txt");
+ if (!file.exists()) {
+ throw new Error("Report should have been received by the server");
+ }
+
+ file.remove(false);
+
+ let extra = getPropertyBagValue(subject, "extra");
+ if (!(extra instanceof Ci.nsIPropertyBag2)) {
+ throw new Error("extra was not a Ci.nsIPropertyBag2");
+ }
+
+ info("Iterating crash report extra keys");
+ let enumerator = extra.enumerator;
+ while (enumerator.hasMoreElements()) {
+ let key = enumerator.getNext().QueryInterface(Ci.nsIProperty).name;
+ let value = extra.getPropertyAsAString(key);
+ if (key in expectedExtra) {
+ if (expectedExtra[key] == null) {
+ ok(false, `Got unexpected key ${key} with value ${value}`);
+ } else {
+ is(value, expectedExtra[key],
+ `Crash report had the right extra value for ${key}`);
+ }
+ }
+ }
+ });
+}
+
+
+/**
+ * For an nsIPropertyBag, returns the value for a given
+ * key.
+ *
+ * @param bag
+ * The nsIPropertyBag to retrieve the value from
+ * @param key
+ * The key that we want to get the value for from the
+ * bag
+ * @returns The value corresponding to the key from the bag,
+ * or null if the value could not be retrieved (for
+ * example, if no value is set at that key).
+*/
+function getPropertyBagValue(bag, key) {
+ try {
+ let val = bag.getProperty(key);
+ return val;
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Monkey patches TabCrashHandler.getDumpID to return null in order to test
+ * about:tabcrashed when a dump is not available.
+ */
+function prepareNoDump() {
+ let originalGetDumpID = TabCrashHandler.getDumpID;
+ TabCrashHandler.getDumpID = function(browser) { return null; };
+ registerCleanupFunction(() => {
+ TabCrashHandler.getDumpID = originalGetDumpID;
+ });
+}
diff --git a/browser/base/content/test/tabs/.eslintrc.js b/browser/base/content/test/tabs/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/tabs/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/tabs/browser.ini b/browser/base/content/test/tabs/browser.ini
new file mode 100644
index 000000000..7771e0a6e
--- /dev/null
+++ b/browser/base/content/test/tabs/browser.ini
@@ -0,0 +1,4 @@
+[browser_tabSpinnerProbe.js]
+skip-if = !e10s # Tab spinner is e10s only.
+[browser_tabSwitchPrintPreview.js]
+skip-if = os == 'mac'
diff --git a/browser/base/content/test/tabs/browser_tabSpinnerProbe.js b/browser/base/content/test/tabs/browser_tabSpinnerProbe.js
new file mode 100644
index 000000000..c3569c2b1
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabSpinnerProbe.js
@@ -0,0 +1,93 @@
+"use strict";
+
+/**
+ * Tests the FX_TAB_SWITCH_SPINNER_VISIBLE_MS and
+ * FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS telemetry probes
+ */
+let gMinHangTime = 500; // ms
+let gMaxHangTime = 5 * 1000; // ms
+
+/**
+ * Make a data URI for a generic webpage with a script that hangs for a given
+ * amount of time.
+ * @param {?Number} aHangMs Number of milliseconds that the hang should last.
+ * Defaults to 0.
+ * @return {String} The data URI generated.
+ */
+function makeDataURI(aHangMs = 0) {
+ return `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tab Spinner Test</title>
+ <script>
+ function hang() {
+ let hangDuration = ${aHangMs};
+ if (hangDuration > 0) {
+ let startTime = window.performance.now();
+ while(window.performance.now() - startTime < hangDuration) {}
+ }
+ }
+ </script>
+ </head>
+ <body>
+ <h1 id='header'>Tab Spinner Test</h1>
+ </body>
+ </html>`;
+}
+
+/**
+ * Returns the sum of all values in an array.
+ * @param {Array} aArray An array of integers
+ * @return {Number} The sum of the integers in the array
+ */
+function sum(aArray) {
+ return aArray.reduce(function(previousValue, currentValue) {
+ return previousValue + currentValue;
+ });
+}
+
+/**
+ * A generator intended to be run as a Task. It tests one of the tab spinner
+ * telemetry probes.
+ * @param {String} aProbe The probe to test. Should be one of:
+ * - FX_TAB_SWITCH_SPINNER_VISIBLE_MS
+ * - FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS
+ */
+function* testProbe(aProbe) {
+ info(`Testing probe: ${aProbe}`);
+ let histogram = Services.telemetry.getHistogramById(aProbe);
+ let buckets = histogram.snapshot().ranges.filter(function(value) {
+ return (value > gMinHangTime && value < gMaxHangTime);
+ });
+ let delayTime = buckets[0]; // Pick a bucket arbitrarily
+
+ // The tab spinner does not show up instantly. We need to hang for a little
+ // bit of extra time to account for the tab spinner delay.
+ delayTime += gBrowser.selectedTab.linkedBrowser.getTabBrowser()._getSwitcher().TAB_SWITCH_TIMEOUT;
+ let dataURI1 = makeDataURI(delayTime);
+ let dataURI2 = makeDataURI();
+
+ let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, dataURI1);
+ histogram.clear();
+ // Queue a hang in the content process when the
+ // event loop breathes next.
+ ContentTask.spawn(tab1.linkedBrowser, null, function*() {
+ content.wrappedJSObject.hang();
+ });
+ let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, dataURI2);
+ let snapshot = histogram.snapshot();
+ yield BrowserTestUtils.removeTab(tab2);
+ yield BrowserTestUtils.removeTab(tab1);
+ ok(sum(snapshot.counts) > 0,
+ `Spinner probe should now have a value in some bucket`);
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+add_task(testProbe.bind(null, "FX_TAB_SWITCH_SPINNER_VISIBLE_MS"));
+add_task(testProbe.bind(null, "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS"));
diff --git a/browser/base/content/test/tabs/browser_tabSwitchPrintPreview.js b/browser/base/content/test/tabs/browser_tabSwitchPrintPreview.js
new file mode 100644
index 000000000..4ec36a7cc
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabSwitchPrintPreview.js
@@ -0,0 +1,29 @@
+const kURL1 = "data:text/html,Should I stay or should I go?";
+const kURL2 = "data:text/html,I shouldn't be here!";
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+});
+
+/**
+ * Verify that if we open a new tab and try to make it the selected tab while
+ * print preview is up, that doesn't happen.
+ */
+add_task(function* () {
+ yield BrowserTestUtils.withNewTab(kURL1, function* (browser) {
+ let tab = gBrowser.addTab(kURL2);
+ document.getElementById("cmd_printPreview").doCommand();
+ gBrowser.selectedTab = tab;
+ yield BrowserTestUtils.waitForCondition(() => gInPrintPreviewMode, "should be in print preview mode");
+ isnot(gBrowser.selectedTab, tab, "Selected tab should not be the tab we added");
+ is(gBrowser.selectedTab, PrintPreviewListener._printPreviewTab, "Selected tab should be the print preview tab");
+ gBrowser.selectedTab = tab;
+ isnot(gBrowser.selectedTab, tab, "Selected tab should still not be the tab we added");
+ is(gBrowser.selectedTab, PrintPreviewListener._printPreviewTab, "Selected tab should still be the print preview tab");
+ PrintUtils.exitPrintPreview();
+ yield BrowserTestUtils.waitForCondition(() => !gInPrintPreviewMode, "should be in print preview mode");
+ yield BrowserTestUtils.removeTab(tab);
+ });
+});
diff --git a/browser/base/content/test/urlbar/.eslintrc.js b/browser/base/content/test/urlbar/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/urlbar/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/urlbar/authenticate.sjs b/browser/base/content/test/urlbar/authenticate.sjs
new file mode 100644
index 000000000..58da655cf
--- /dev/null
+++ b/browser/base/content/test/urlbar/authenticate.sjs
@@ -0,0 +1,220 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true, requestProxyAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "", expected_pass = "", realm = "mochitest";
+ var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
+ var huge = false, plugin = false, anonymous = false;
+ var authHeaderCount = 1;
+ // user=xxx
+ match = /[^_]user=([^&]*)/.exec(query);
+ if (match)
+ expected_user = match[1];
+
+ // pass=xxx
+ match = /[^_]pass=([^&]*)/.exec(query);
+ if (match)
+ expected_pass = match[1];
+
+ // realm=xxx
+ match = /[^_]realm=([^&]*)/.exec(query);
+ if (match)
+ realm = match[1];
+
+ // proxy_user=xxx
+ match = /proxy_user=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_user = match[1];
+
+ // proxy_pass=xxx
+ match = /proxy_pass=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_pass = match[1];
+
+ // proxy_realm=xxx
+ match = /proxy_realm=([^&]*)/.exec(query);
+ if (match)
+ proxy_realm = match[1];
+
+ // huge=1
+ match = /huge=1/.exec(query);
+ if (match)
+ huge = true;
+
+ // plugin=1
+ match = /plugin=1/.exec(query);
+ if (match)
+ plugin = true;
+
+ // multiple=1
+ match = /multiple=([^&]*)/.exec(query);
+ if (match)
+ authHeaderCount = match[1]+0;
+
+ // anonymous=1
+ match = /anonymous=1/.exec(query);
+ if (match)
+ anonymous = true;
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw "Couldn't parse auth header: " + authHeader;
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw "Couldn't decode auth header: " + userpass;
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ var proxy_actual_user = "", proxy_actual_pass = "";
+ if (request.hasHeader("Proxy-Authorization")) {
+ authHeader = request.getHeader("Proxy-Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw "Couldn't parse auth header: " + authHeader;
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw "Couldn't decode auth header: " + userpass;
+ proxy_actual_user = match[1];
+ proxy_actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+ if (proxy_expected_user == proxy_actual_user &&
+ proxy_expected_pass == proxy_actual_pass) {
+ requestProxyAuth = false;
+ }
+
+ if (anonymous) {
+ if (authPresent) {
+ response.setStatusLine("1.0", 400, "Unexpected authorization header found");
+ } else {
+ response.setStatusLine("1.0", 200, "Authorization header not found");
+ }
+ } else {
+ if (requestProxyAuth) {
+ response.setStatusLine("1.0", 407, "Proxy authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
+ } else if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+ if (huge) {
+ response.write("<div style='display: none'>");
+ for (i = 0; i < 100000; i++) {
+ response.write("123456789\n");
+ }
+ response.write("</div>");
+ response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
+ }
+
+ if (plugin) {
+ response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " +
+ "type='application/x-test'></embed>\n");
+ }
+
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/browser/base/content/test/urlbar/browser.ini b/browser/base/content/test/urlbar/browser.ini
new file mode 100644
index 000000000..39bc086c9
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -0,0 +1,101 @@
+[DEFAULT]
+support-files =
+ dummy_page.html
+ head.js
+
+[browser_URLBarSetURI.js]
+skip-if = (os == "linux" || os == "mac") && debug # bug 970052, bug 970053
+[browser_action_keyword.js]
+skip-if = os == "linux" # Bug 1188154
+support-files =
+ print_postdata.sjs
+[browser_action_keyword_override.js]
+[browser_action_searchengine.js]
+[browser_action_searchengine_alias.js]
+[browser_autocomplete_a11y_label.js]
+[browser_autocomplete_autoselect.js]
+[browser_autocomplete_cursor.js]
+[browser_autocomplete_edit_completed.js]
+[browser_autocomplete_enter_race.js]
+[browser_autocomplete_no_title.js]
+[browser_autocomplete_tag_star_visibility.js]
+[browser_bug1104165-switchtab-decodeuri.js]
+[browser_bug1003461-switchtab-override.js]
+[browser_bug1024133-switchtab-override-keynav.js]
+[browser_bug1025195_switchToTabHavingURI_aOpenParams.js]
+[browser_bug1070778.js]
+[browser_bug1225194-remotetab.js]
+[browser_bug304198.js]
+[browser_bug556061.js]
+subsuite = clipboard
+[browser_bug562649.js]
+[browser_bug623155.js]
+support-files =
+ redirect_bug623155.sjs
+[browser_bug783614.js]
+[browser_canonizeURL.js]
+[browser_dragdropURL.js]
+[browser_locationBarCommand.js]
+[browser_locationBarExternalLoad.js]
+[browser_moz_action_link.js]
+[browser_removeUnsafeProtocolsFromURLBarPaste.js]
+subsuite = clipboard
+[browser_search_favicon.js]
+[browser_tabMatchesInAwesomebar.js]
+support-files =
+ moz.png
+[browser_tabMatchesInAwesomebar_perwindowpb.js]
+skip-if = os == 'linux' # Bug 1104755
+[browser_urlbarAboutHomeLoading.js]
+[browser_urlbarAutoFillTrimURLs.js]
+[browser_urlbarCopying.js]
+subsuite = clipboard
+support-files =
+ authenticate.sjs
+[browser_urlbarDecode.js]
+[browser_urlbarDelete.js]
+[browser_urlbarEnter.js]
+[browser_urlbarEnterAfterMouseOver.js]
+skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
+[browser_urlbarFocusedCmdK.js]
+[browser_urlbarHashChangeProxyState.js]
+[browser_urlbarKeepStateAcrossTabSwitches.js]
+[browser_urlbarOneOffs.js]
+[browser_urlbarPrivateBrowsingWindowChange.js]
+[browser_urlbarRaceWithTabs.js]
+[browser_urlbarRevert.js]
+[browser_urlbarSearchSingleWordNotification.js]
+[browser_urlbarSearchSuggestions.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_urlbarSearchSuggestionsNotification.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_urlbarSearchTelemetry.js]
+support-files =
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+[browser_urlbarStop.js]
+[browser_urlbarTrimURLs.js]
+subsuite = clipboard
+[browser_urlbarUpdateForDomainCompletion.js]
+[browser_urlbar_autoFill_backspaced.js]
+[browser_urlbar_blanking.js]
+support-files =
+ file_blank_but_not_blank.html
+[browser_urlbar_locationchange_urlbar_edit_dos.js]
+support-files =
+ file_urlbar_edit_dos.html
+[browser_urlbar_searchsettings.js]
+[browser_urlbar_stop_pending.js]
+support-files =
+ slow-page.sjs
+[browser_urlbar_remoteness_switch.js]
+run-if = e10s
+[browser_urlHighlight.js]
+[browser_wyciwyg_urlbarCopying.js]
+subsuite = clipboard
+support-files =
+ test_wyciwyg_copying.html
diff --git a/browser/base/content/test/urlbar/browser_URLBarSetURI.js b/browser/base/content/test/urlbar/browser_URLBarSetURI.js
new file mode 100644
index 000000000..ac8352f1a
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_URLBarSetURI.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/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ // avoid prompting about phishing
+ Services.prefs.setIntPref(phishyUserPassPref, 32);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(phishyUserPassPref);
+ });
+
+ nextTest();
+}
+
+const phishyUserPassPref = "network.http.phishy-userpass-length";
+
+function nextTest() {
+ let test = tests.shift();
+ if (test) {
+ test(function () {
+ executeSoon(nextTest);
+ });
+ } else {
+ executeSoon(finish);
+ }
+}
+
+var tests = [
+ function revert(next) {
+ loadTabInWindow(window, function (tab) {
+ gURLBar.handleRevert();
+ is(gURLBar.textValue, "example.com", "URL bar had user/pass stripped after reverting");
+ gBrowser.removeTab(tab);
+ next();
+ });
+ },
+ function customize(next) {
+ // Need to wait for delayedStartup for the customization part of the test,
+ // since that's where BrowserToolboxCustomizeDone is set.
+ BrowserTestUtils.openNewBrowserWindow().then(function(win) {
+ loadTabInWindow(win, function () {
+ openToolbarCustomizationUI(function () {
+ closeToolbarCustomizationUI(function () {
+ is(win.gURLBar.textValue, "example.com", "URL bar had user/pass stripped after customize");
+ win.close();
+ next();
+ }, win);
+ }, win);
+ });
+ });
+ },
+ function pageloaderror(next) {
+ loadTabInWindow(window, function (tab) {
+ // Load a new URL and then immediately stop it, to simulate a page load
+ // error.
+ tab.linkedBrowser.loadURI("http://test1.example.com");
+ tab.linkedBrowser.stop();
+ is(gURLBar.textValue, "example.com", "URL bar had user/pass stripped after load error");
+ gBrowser.removeTab(tab);
+ next();
+ });
+ }
+];
+
+function loadTabInWindow(win, callback) {
+ info("Loading tab");
+ let url = "http://user:pass@example.com/";
+ let tab = win.gBrowser.selectedTab = win.gBrowser.addTab(url);
+ BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url).then(() => {
+ info("Tab loaded");
+ is(win.gURLBar.textValue, "example.com", "URL bar had user/pass stripped initially");
+ callback(tab);
+ }, true);
+}
+
+function openToolbarCustomizationUI(aCallback, aBrowserWin) {
+ if (!aBrowserWin)
+ aBrowserWin = window;
+
+ aBrowserWin.gCustomizeMode.enter();
+
+ aBrowserWin.gNavToolbox.addEventListener("customizationready", function UI_loaded() {
+ aBrowserWin.gNavToolbox.removeEventListener("customizationready", UI_loaded);
+ executeSoon(function() {
+ aCallback(aBrowserWin)
+ });
+ });
+}
+
+function closeToolbarCustomizationUI(aCallback, aBrowserWin) {
+ aBrowserWin.gNavToolbox.addEventListener("aftercustomization", function unloaded() {
+ aBrowserWin.gNavToolbox.removeEventListener("aftercustomization", unloaded);
+ executeSoon(aCallback);
+ });
+
+ aBrowserWin.gCustomizeMode.exit();
+}
+
diff --git a/browser/base/content/test/urlbar/browser_action_keyword.js b/browser/base/content/test/urlbar/browser_action_keyword.js
new file mode 100644
index 000000000..854a7b82f
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_action_keyword.js
@@ -0,0 +1,119 @@
+function* promise_first_result(inputText) {
+ yield promiseAutocompleteResultPopup(inputText);
+
+ let firstResult = gURLBar.popup.richlistbox.firstChild;
+ return firstResult;
+}
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/base/content/test/urlbar/print_postdata.sjs";
+
+add_task(function* setup() {
+ yield PlacesUtils.keywords.insert({ keyword: "get",
+ url: TEST_URL + "?q=%s" });
+ yield PlacesUtils.keywords.insert({ keyword: "post",
+ url: TEST_URL,
+ postData: "q=%s" });
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.keywords.remove("get");
+ yield PlacesUtils.keywords.remove("post");
+ while (gBrowser.tabs.length > 1) {
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ });
+});
+
+add_task(function* get_keyword() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ let result = yield promise_first_result("get something");
+ isnot(result, null, "Expect a keyword result");
+
+ let types = new Set(result.getAttribute("type").split(/\s+/));
+ Assert.ok(types.has("keyword"));
+ is(result.getAttribute("actiontype"), "keyword", "Expect correct `actiontype` attribute");
+ is(result.getAttribute("title"), "mochi.test:8888", "Expect correct title");
+
+ // We need to make a real URI out of this to ensure it's normalised for
+ // comparison.
+ let uri = NetUtil.newURI(result.getAttribute("url"));
+ is(uri.spec, PlacesUtils.mozActionURI("keyword",
+ { url: TEST_URL + "?q=something",
+ input: "get something"}),
+ "Expect correct url");
+
+ let titleHbox = result._titleText.parentNode.parentNode;
+ ok(titleHbox.classList.contains("ac-title"), "Title hbox element sanity check");
+ is_element_visible(titleHbox, "Title element should be visible");
+ is(result._titleText.textContent, "mochi.test:8888: something",
+ "Node should contain the name of the bookmark and query");
+
+ let urlHbox = result._urlText.parentNode.parentNode;
+ ok(urlHbox.classList.contains("ac-url"), "URL hbox element sanity check");
+ is_element_hidden(urlHbox, "URL element should be hidden");
+
+ let actionHbox = result._actionText.parentNode.parentNode;
+ ok(actionHbox.classList.contains("ac-action"), "Action hbox element sanity check");
+ is_element_visible(actionHbox, "Action element should be visible");
+ is(result._actionText.textContent, "", "Action text should be empty");
+
+ // Click on the result
+ info("Normal click on result");
+ let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeMouseAtCenter(result, {});
+ yield tabPromise;
+ is(tab.linkedBrowser.currentURI.spec, TEST_URL + "?q=something",
+ "Tab should have loaded from clicking on result");
+
+ // Middle-click on the result
+ info("Middle-click on result");
+ result = yield promise_first_result("get somethingmore");
+ isnot(result, null, "Expect a keyword result");
+ // We need to make a real URI out of this to ensure it's normalised for
+ // comparison.
+ uri = NetUtil.newURI(result.getAttribute("url"));
+ is(uri.spec, PlacesUtils.mozActionURI("keyword",
+ { url: TEST_URL + "?q=somethingmore",
+ input: "get somethingmore" }),
+ "Expect correct url");
+
+ tabPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ EventUtils.synthesizeMouseAtCenter(result, {button: 1});
+ let tabOpenEvent = yield tabPromise;
+ let newTab = tabOpenEvent.target;
+ yield BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+ is(newTab.linkedBrowser.currentURI.spec,
+ TEST_URL + "?q=somethingmore",
+ "Tab should have loaded from middle-clicking on result");
+});
+
+
+add_task(function* post_keyword() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ let result = yield promise_first_result("post something");
+ isnot(result, null, "Expect a keyword result");
+
+ let types = new Set(result.getAttribute("type").split(/\s+/));
+ Assert.ok(types.has("keyword"));
+ is(result.getAttribute("actiontype"), "keyword", "Expect correct `actiontype` attribute");
+ is(result.getAttribute("title"), "mochi.test:8888", "Expect correct title");
+
+ is(result.getAttribute("url"),
+ PlacesUtils.mozActionURI("keyword", { url: TEST_URL,
+ input: "post something",
+ "postData": "q=something" }),
+ "Expect correct url");
+
+ // Click on the result
+ info("Normal click on result");
+ let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeMouseAtCenter(result, {});
+ yield tabPromise;
+ is(tab.linkedBrowser.currentURI.spec, TEST_URL,
+ "Tab should have loaded from clicking on result");
+
+ let postData = yield ContentTask.spawn(tab.linkedBrowser, null, function* () {
+ return content.document.body.textContent;
+ });
+ is(postData, "q=something", "post data was submitted correctly");
+});
diff --git a/browser/base/content/test/urlbar/browser_action_keyword_override.js b/browser/base/content/test/urlbar/browser_action_keyword_override.js
new file mode 100644
index 000000000..f5a865678
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_action_keyword_override.js
@@ -0,0 +1,40 @@
+add_task(function*() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test" });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword",
+ url: "http://example.com/?q=%s" })
+
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.bookmarks.remove(bm);
+ });
+
+ yield promiseAutocompleteResultPopup("keyword search");
+ let result = gURLBar.popup.richlistbox.children[0];
+
+ info("Before override");
+ let titleHbox = result._titleText.parentNode.parentNode;
+ ok(titleHbox.classList.contains("ac-title"), "Title hbox element sanity check");
+ is_element_visible(titleHbox, "Title element should be visible");
+
+ let urlHbox = result._urlText.parentNode.parentNode;
+ ok(urlHbox.classList.contains("ac-url"), "URL hbox element sanity check");
+ is_element_hidden(urlHbox, "URL element should be hidden");
+
+ let actionHbox = result._actionText.parentNode.parentNode;
+ ok(actionHbox.classList.contains("ac-action"), "Action hbox element sanity check");
+ is_element_visible(actionHbox, "Action element should be visible");
+ is(result._actionText.textContent, "", "Action text should be empty");
+
+ info("During override");
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown" });
+ is_element_visible(titleHbox, "Title element should be visible");
+ is_element_hidden(urlHbox, "URL element should be hidden");
+ is_element_visible(actionHbox, "Action element should be visible");
+ is(result._actionText.textContent, "", "Action text should be empty");
+
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" });
+
+ gURLBar.popup.hidePopup();
+ yield promisePopupHidden(gURLBar.popup);
+});
diff --git a/browser/base/content/test/urlbar/browser_action_searchengine.js b/browser/base/content/test/urlbar/browser_action_searchengine.js
new file mode 100644
index 000000000..d2115abba
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_action_searchengine.js
@@ -0,0 +1,36 @@
+add_task(function* () {
+ Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+ let engine = Services.search.getEngineByName("MozSearch");
+ let originalEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ registerCleanupFunction(() => {
+ Services.search.currentEngine = originalEngine;
+ let engine = Services.search.getEngineByName("MozSearch");
+ Services.search.removeEngine(engine);
+
+ try {
+ gBrowser.removeTab(tab);
+ } catch (ex) { /* tab may have already been closed in case of failure */ }
+
+ return PlacesTestUtils.clearHistory();
+ });
+
+ yield promiseAutocompleteResultPopup("open a search");
+ let result = gURLBar.popup.richlistbox.firstChild;
+
+ isnot(result, null, "Should have a result");
+ is(result.getAttribute("url"),
+ `moz-action:searchengine,{"engineName":"MozSearch","input":"open%20a%20search","searchQuery":"open%20a%20search"}`,
+ "Result should be a moz-action: for the correct search engine");
+ is(result.hasAttribute("image"), false, "Result shouldn't have an image attribute");
+
+ let tabPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ result.click();
+ yield tabPromise;
+
+ is(gBrowser.selectedBrowser.currentURI.spec, "http://example.com/?q=open+a+search", "Correct URL should be loaded");
+});
diff --git a/browser/base/content/test/urlbar/browser_action_searchengine_alias.js b/browser/base/content/test/urlbar/browser_action_searchengine_alias.js
new file mode 100644
index 000000000..1967d178a
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_action_searchengine_alias.js
@@ -0,0 +1,35 @@
+add_task(function* () {
+ let iconURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC";
+ Services.search.addEngineWithDetails("MozSearch", iconURI, "moz", "", "GET",
+ "http://example.com/?q={searchTerms}");
+ let engine = Services.search.getEngineByName("MozSearch");
+ let originalEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ registerCleanupFunction(() => {
+ Services.search.currentEngine = originalEngine;
+ let engine = Services.search.getEngineByName("MozSearch");
+ Services.search.removeEngine(engine);
+
+ try {
+ gBrowser.removeTab(tab);
+ } catch (ex) { /* tab may have already been closed in case of failure */ }
+
+ return PlacesTestUtils.clearHistory();
+ });
+
+ yield promiseAutocompleteResultPopup("moz open a search");
+
+ let result = gURLBar.popup.richlistbox.children[0];
+ ok(result.hasAttribute("image"), "Result should have an image attribute");
+ ok(result.getAttribute("image") === engine.iconURI.spec,
+ "Image attribute should have the search engine's icon");
+
+ let tabPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", { });
+ yield tabPromise;
+
+ is(gBrowser.selectedBrowser.currentURI.spec, "http://example.com/?q=open+a+search");
+});
diff --git a/browser/base/content/test/urlbar/browser_autocomplete_a11y_label.js b/browser/base/content/test/urlbar/browser_autocomplete_a11y_label.js
new file mode 100644
index 000000000..a27f9672e
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_autocomplete_a11y_label.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+add_task(function* switchToTab() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:about");
+
+ yield promiseAutocompleteResultPopup("% about");
+
+ ok(gURLBar.popup.richlistbox.children.length > 1, "Should get at least 2 results");
+ let result = gURLBar.popup.richlistbox.children[1];
+ is(result.getAttribute("type"), "switchtab", "Expect right type attribute");
+ is(result.label, "about:about about:about Tab", "Result a11y label should be: <title> <url> Tab");
+
+ gURLBar.popup.hidePopup();
+ yield promisePopupHidden(gURLBar.popup);
+ gBrowser.removeTab(tab);
+});
+
+add_task(function* searchSuggestions() {
+ let engine = yield promiseNewSearchEngine(TEST_ENGINE_BASENAME);
+ let oldCurrentEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ registerCleanupFunction(function () {
+ Services.search.currentEngine = oldCurrentEngine;
+ Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
+ Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+ });
+
+ yield promiseAutocompleteResultPopup("foo");
+ // Don't assume that the search doesn't match history or bookmarks left around
+ // by earlier tests.
+ Assert.ok(gURLBar.popup.richlistbox.children.length >= 3,
+ "Should get at least heuristic result + two search suggestions");
+ // The first expected search is the search term itself since the heuristic
+ // result will come before the search suggestions.
+ let expectedSearches = [
+ "foo",
+ "foofoo",
+ "foobar",
+ ];
+ for (let child of gURLBar.popup.richlistbox.children) {
+ if (child.getAttribute("type").split(/\s+/).indexOf("searchengine") >= 0) {
+ Assert.ok(expectedSearches.length > 0);
+ let suggestion = expectedSearches.shift();
+ Assert.equal(child.label, suggestion + " browser_searchSuggestionEngine searchSuggestionEngine.xml Search",
+ "Result label should be: <search term> <engine name> Search");
+ }
+ }
+ Assert.ok(expectedSearches.length == 0);
+ gURLBar.closePopup();
+});
diff --git a/browser/base/content/test/urlbar/browser_autocomplete_autoselect.js b/browser/base/content/test/urlbar/browser_autocomplete_autoselect.js
new file mode 100644
index 000000000..e4e0daa8e
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_autocomplete_autoselect.js
@@ -0,0 +1,92 @@
+const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
+
+function repeat(limit, func) {
+ for (let i = 0; i < limit; i++) {
+ func(i);
+ }
+}
+
+function is_selected(index) {
+ is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ is(gURLBar.popup.oneOffSearchButtons.selectedButton, null,
+ "A result is selected, so the one-offs should not have a selection");
+}
+
+function is_selected_one_off(index) {
+ is(gURLBar.popup.oneOffSearchButtons.selectedButtonIndex, index,
+ "Expected one-off button should be selected");
+
+ // This is true because although both the listbox and the one-offs can have
+ // selections, the test doesn't check that.
+ is(gURLBar.popup.richlistbox.selectedIndex, -1,
+ "A one-off is selected, so the listbox should not have a selection");
+}
+
+add_task(function*() {
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ Services.prefs.setBoolPref(ONEOFF_URLBAR_PREF, true);
+ registerCleanupFunction(function* () {
+ yield PlacesTestUtils.clearHistory();
+ Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF);
+ });
+
+ let visits = [];
+ repeat(maxResults, i => {
+ visits.push({
+ uri: makeURI("http://example.com/autocomplete/?" + i),
+ });
+ });
+ yield PlacesTestUtils.addVisits(visits);
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+ yield promiseAutocompleteResultPopup("example.com/autocomplete");
+
+ let popup = gURLBar.popup;
+ let results = popup.richlistbox.children;
+ is(results.length, maxResults,
+ "Should get maxResults=" + maxResults + " results");
+ is_selected(0);
+
+ info("Key Down to select the next item");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is_selected(1);
+
+ info("Key Down maxResults-1 times should select the first one-off");
+ repeat(maxResults - 1, () => EventUtils.synthesizeKey("VK_DOWN", {}));
+ is_selected_one_off(0);
+
+ info("Key Down numButtons-1 should select the last one-off");
+ let numButtons =
+ gURLBar.popup.oneOffSearchButtons.getSelectableButtons(true).length;
+ repeat(numButtons - 1, () => EventUtils.synthesizeKey("VK_DOWN", {}));
+ is_selected_one_off(numButtons - 1);
+
+ info("Key Down twice more should select the second result");
+ repeat(2, () => EventUtils.synthesizeKey("VK_DOWN", {}));
+ is_selected(1);
+
+ info("Key Down maxResults + numButtons times should wrap around");
+ repeat(maxResults + numButtons,
+ () => EventUtils.synthesizeKey("VK_DOWN", {}));
+ is_selected(1);
+
+ info("Key Up maxResults + numButtons times should wrap around the other way");
+ repeat(maxResults + numButtons, () => EventUtils.synthesizeKey("VK_UP", {}));
+ is_selected(1);
+
+ info("Page Up will go up the list, but not wrap");
+ EventUtils.synthesizeKey("VK_PAGE_UP", {})
+ is_selected(0);
+
+ info("Page Up again will wrap around to the end of the list");
+ EventUtils.synthesizeKey("VK_PAGE_UP", {})
+ is_selected(maxResults - 1);
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promisePopupHidden(gURLBar.popup);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/urlbar/browser_autocomplete_cursor.js b/browser/base/content/test/urlbar/browser_autocomplete_cursor.js
new file mode 100644
index 000000000..9cc2c6eac
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_autocomplete_cursor.js
@@ -0,0 +1,17 @@
+add_task(function*() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+ yield promiseAutocompleteResultPopup("www.mozilla.org");
+
+ gURLBar.selectTextRange(4, 4);
+
+ is(gURLBar.popup.state, "open", "Popup should be open");
+ is(gURLBar.popup.richlistbox.selectedIndex, 0, "Should have selected something");
+
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ yield promisePopupHidden(gURLBar.popup);
+
+ is(gURLBar.selectionStart, 5, "Should have moved the cursor");
+ is(gURLBar.selectionEnd, 5, "And not selected anything");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js b/browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js
new file mode 100644
index 000000000..19db1a368
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js
@@ -0,0 +1,48 @@
+add_task(function*() {
+ yield PlacesTestUtils.clearHistory();
+
+ yield PlacesTestUtils.addVisits([
+ { uri: makeURI("http://example.com/foo") },
+ { uri: makeURI("http://example.com/foo/bar") },
+ ]);
+
+ registerCleanupFunction(function* () {
+ yield PlacesTestUtils.clearHistory();
+ });
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ gURLBar.focus();
+
+ yield promiseAutocompleteResultPopup("http://example.com");
+
+ let popup = gURLBar.popup;
+ let list = popup.richlistbox;
+ let initialIndex = list.selectedIndex;
+
+ info("Key Down to select the next item.");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ let nextIndex = initialIndex + 1;
+ let nextValue = gURLBar.controller.getFinalCompleteValueAt(nextIndex);
+ is(list.selectedIndex, nextIndex, "The next item is selected.");
+ is(gURLBar.value, nextValue, "The selected URL is completed.");
+
+ info("Press backspace");
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+ yield promiseSearchComplete();
+
+ let editedValue = gURLBar.textValue;
+ is(list.selectedIndex, initialIndex, "The initial index is selected again.");
+ isnot(editedValue, nextValue, "The URL has changed.");
+
+ let docLoad = waitForDocLoadAndStopIt("http://" + editedValue);
+
+ info("Press return to load edited URL.");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield Promise.all([
+ promisePopupHidden(gURLBar.popup),
+ docLoad,
+ ]);
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js b/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
new file mode 100644
index 000000000..4e3c8943c
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
@@ -0,0 +1,122 @@
+// The order of these tests matters!
+
+add_task(function* setup () {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test" });
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.bookmarks.remove(bm);
+ yield BrowserTestUtils.removeTab(tab);
+ });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword",
+ url: "http://example.com/?q=%s" });
+ // Needs at least one success.
+ ok(true, "Setup complete");
+});
+
+add_task(function* test_keyword() {
+ yield promiseAutocompleteResultPopup("keyword bear");
+ gURLBar.focus();
+ EventUtils.synthesizeKey("d", {});
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ info("wait for the page to load");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+ false, "http://example.com/?q=beard");
+});
+
+add_task(function* test_sametext() {
+ yield promiseAutocompleteResultPopup("example.com", window, true);
+
+ // Simulate re-entering the same text searched the last time. This may happen
+ // through a copy paste, but clipboard handling is not much reliable, so just
+ // fire an input event.
+ info("synthesize input event");
+ let event = document.createEvent("Events");
+ event.initEvent("input", true, true);
+ gURLBar.dispatchEvent(event);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ info("wait for the page to load");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+ false, "http://example.com/");
+});
+
+add_task(function* test_after_empty_search() {
+ yield promiseAutocompleteResultPopup("");
+ gURLBar.focus();
+ gURLBar.value = "e";
+ EventUtils.synthesizeKey("x", {});
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ info("wait for the page to load");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+ false, "http://example.com/");
+});
+
+add_task(function* test_disabled_ac() {
+ // Disable autocomplete.
+ let suggestHistory = Preferences.get("browser.urlbar.suggest.history");
+ Preferences.set("browser.urlbar.suggest.history", false);
+ let suggestBookmarks = Preferences.get("browser.urlbar.suggest.bookmark");
+ Preferences.set("browser.urlbar.suggest.bookmark", false);
+ let suggestOpenPages = Preferences.get("browser.urlbar.suggest.openpage");
+ Preferences.set("browser.urlbar.suggest.openpages", false);
+
+ Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
+ "http://example.com/?q={searchTerms}");
+ let engine = Services.search.getEngineByName("MozSearch");
+ let originalEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+
+ function* cleanup() {
+ Preferences.set("browser.urlbar.suggest.history", suggestHistory);
+ Preferences.set("browser.urlbar.suggest.bookmark", suggestBookmarks);
+ Preferences.set("browser.urlbar.suggest.openpage", suggestOpenPages);
+
+ Services.search.currentEngine = originalEngine;
+ let engine = Services.search.getEngineByName("MozSearch");
+ if (engine) {
+ Services.search.removeEngine(engine);
+ }
+ }
+ registerCleanupFunction(cleanup);
+
+ gURLBar.focus();
+ gURLBar.value = "e";
+ EventUtils.synthesizeKey("x", {});
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ info("wait for the page to load");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+ false, "http://example.com/?q=ex");
+ yield cleanup();
+});
+
+add_task(function* test_delay() {
+ const TIMEOUT = 10000;
+ // Set a large delay.
+ let delay = Preferences.get("browser.urlbar.delay");
+ Preferences.set("browser.urlbar.delay", TIMEOUT);
+
+ registerCleanupFunction(function* () {
+ Preferences.set("browser.urlbar.delay", delay);
+ });
+
+ // This is needed to clear the current value, otherwise autocomplete may think
+ // the user removed text from the end.
+ let start = Date.now();
+ yield promiseAutocompleteResultPopup("");
+ Assert.ok((Date.now() - start) < TIMEOUT);
+
+ start = Date.now();
+ gURLBar.closePopup();
+ gURLBar.focus();
+ gURLBar.value = "e";
+ EventUtils.synthesizeKey("x", {});
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ info("wait for the page to load");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+ false, "http://example.com/");
+ Assert.ok((Date.now() - start) < TIMEOUT);
+});
diff --git a/browser/base/content/test/urlbar/browser_autocomplete_no_title.js b/browser/base/content/test/urlbar/browser_autocomplete_no_title.js
new file mode 100644
index 000000000..8d608550b
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_autocomplete_no_title.js
@@ -0,0 +1,15 @@
+add_task(function*() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ let uri = NetUtil.newURI("http://bug1060642.example.com/beards/are/pretty/great");
+ yield PlacesTestUtils.addVisits([{uri: uri, title: ""}]);
+
+ yield promiseAutocompleteResultPopup("bug1060642");
+ ok(gURLBar.popup.richlistbox.children.length > 1, "Should get at least 2 results");
+ let result = gURLBar.popup.richlistbox.children[1];
+ is(result._titleText.textContent, "bug1060642.example.com", "Result title should be as expected");
+
+ gURLBar.popup.hidePopup();
+ yield promisePopupHidden(gURLBar.popup);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/urlbar/browser_autocomplete_tag_star_visibility.js b/browser/base/content/test/urlbar/browser_autocomplete_tag_star_visibility.js
new file mode 100644
index 000000000..8a69b4b44
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_autocomplete_tag_star_visibility.js
@@ -0,0 +1,102 @@
+add_task(function*() {
+ registerCleanupFunction(() => {
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ });
+
+ function* addTagItem(tagName) {
+ let uri = NetUtil.newURI(`http://example.com/this/is/tagged/${tagName}`);
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ `test ${tagName}`);
+ PlacesUtils.tagging.tagURI(uri, [tagName]);
+ yield PlacesTestUtils.addVisits([{uri: uri, title: `Test page with tag ${tagName}`}]);
+ }
+
+ // We use different tags for each part of the test, as otherwise the
+ // autocomplete code tries to be smart by using the previously cached element
+ // without updating it (since all parameters it knows about are the same).
+
+ let testcases = [{
+ description: "Test with suggest.bookmark=true",
+ tagName: "tagtest1",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "tagtest1",
+ expected: {
+ type: "bookmark",
+ typeImageVisible: true,
+ },
+ }, {
+ description: "Test with suggest.bookmark=false",
+ tagName: "tagtest2",
+ prefs: {
+ "suggest.bookmark": false,
+ },
+ input: "tagtest2",
+ expected: {
+ type: "tag",
+ typeImageVisible: false,
+ },
+ }, {
+ description: "Test with suggest.bookmark=true (again)",
+ tagName: "tagtest3",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "tagtest3",
+ expected: {
+ type: "bookmark",
+ typeImageVisible: true,
+ },
+ }, {
+ description: "Test with bookmark restriction token",
+ tagName: "tagtest4",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "* tagtest4",
+ expected: {
+ type: "bookmark",
+ typeImageVisible: true,
+ },
+ }, {
+ description: "Test with history restriction token",
+ tagName: "tagtest5",
+ prefs: {
+ "suggest.bookmark": true,
+ },
+ input: "^ tagtest5",
+ expected: {
+ type: "tag",
+ typeImageVisible: false,
+ },
+ }];
+
+ for (let testcase of testcases) {
+ info(`Test case: ${testcase.description}`);
+
+ yield addTagItem(testcase.tagName);
+ for (let prefName of Object.keys(testcase.prefs)) {
+ Services.prefs.setBoolPref(`browser.urlbar.${prefName}`, testcase.prefs[prefName]);
+ }
+
+ yield promiseAutocompleteResultPopup(testcase.input);
+ let result = gURLBar.popup.richlistbox.children[1];
+ ok(result && !result.collasped, "Should have result");
+
+ is(result.getAttribute("type"), testcase.expected.type, "Result should have expected type");
+
+ let typeIconStyle = window.getComputedStyle(result._typeIcon);
+ let imageURL = typeIconStyle.listStyleImage;
+ if (testcase.expected.typeImageVisible) {
+ ok(/^url\(.+\)$/.test(imageURL), "Type image should be visible");
+ } else {
+ is(imageURL, "none", "Type image should be hidden");
+ }
+
+ gURLBar.popup.hidePopup();
+ yield promisePopupHidden(gURLBar.popup);
+ }
+});
diff --git a/browser/base/content/test/urlbar/browser_bug1003461-switchtab-override.js b/browser/base/content/test/urlbar/browser_bug1003461-switchtab-override.js
new file mode 100644
index 000000000..89f604491
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug1003461-switchtab-override.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(function* test_switchtab_override() {
+ let testURL = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+
+ info("Opening first tab");
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, testURL);
+
+ info("Opening and selecting second tab");
+ let secondTab = gBrowser.selectedTab = gBrowser.addTab();
+ registerCleanupFunction(() => {
+ try {
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(secondTab);
+ } catch (ex) { /* tabs may have already been closed in case of failure */ }
+ });
+
+ info("Wait for autocomplete")
+ let deferred = Promise.defer();
+ let onSearchComplete = gURLBar.onSearchComplete;
+ registerCleanupFunction(() => {
+ gURLBar.onSearchComplete = onSearchComplete;
+ });
+ gURLBar.onSearchComplete = function () {
+ ok(gURLBar.popupOpen, "The autocomplete popup is correctly open");
+ onSearchComplete.apply(gURLBar);
+ deferred.resolve();
+ }
+
+ gURLBar.focus();
+ gURLBar.value = "dummy_pag";
+ EventUtils.synthesizeKey("e", {});
+ yield deferred.promise;
+
+ info("Select second autocomplete popup entry");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ ok(/moz-action:switchtab/.test(gURLBar.value), "switch to tab entry found");
+
+ info("Override switch-to-tab");
+ deferred = Promise.defer();
+ // In case of failure this would switch tab.
+ let onTabSelect = event => {
+ deferred.reject(new Error("Should have overridden switch to tab"));
+ };
+ gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect, false);
+ registerCleanupFunction(() => {
+ gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect, false);
+ });
+ // Otherwise it would load the page.
+ BrowserTestUtils.browserLoaded(secondTab.linkedBrowser).then(deferred.resolve);
+
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown" });
+ EventUtils.synthesizeKey("VK_RETURN", { });
+ info(`gURLBar.value = ${gURLBar.value}`);
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" });
+ yield deferred.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/browser/base/content/test/urlbar/browser_bug1024133-switchtab-override-keynav.js b/browser/base/content/test/urlbar/browser_bug1024133-switchtab-override-keynav.js
new file mode 100644
index 000000000..2d97ea07b
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug1024133-switchtab-override-keynav.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/. */
+
+add_task(function* test_switchtab_override_keynav() {
+ let testURL = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+
+ info("Opening first tab");
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, testURL);
+
+ info("Opening and selecting second tab");
+ let secondTab = gBrowser.selectedTab = gBrowser.addTab();
+ registerCleanupFunction(() => {
+ try {
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(secondTab);
+ } catch (ex) { /* tabs may have already been closed in case of failure */ }
+ return PlacesTestUtils.clearHistory();
+ });
+
+ gURLBar.focus();
+ gURLBar.value = "dummy_pag";
+ EventUtils.synthesizeKey("e", {});
+ yield promiseSearchComplete();
+
+ info("Select second autocomplete popup entry");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ ok(/moz-action:switchtab/.test(gURLBar.value), "switch to tab entry found");
+
+ info("Shift+left on switch-to-tab entry");
+
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown" });
+ EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true });
+ EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" });
+
+ ok(!/moz-action:switchtab/.test(gURLBar.inputField.value), "switch to tab should be hidden");
+});
diff --git a/browser/base/content/test/urlbar/browser_bug1025195_switchToTabHavingURI_aOpenParams.js b/browser/base/content/test/urlbar/browser_bug1025195_switchToTabHavingURI_aOpenParams.js
new file mode 100644
index 000000000..9e779ade1
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug1025195_switchToTabHavingURI_aOpenParams.js
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(function* test_ignoreFragment() {
+ let tabRefAboutHome =
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home#1");
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+ let numTabsAtStart = gBrowser.tabs.length;
+
+ switchTab("about:home#1", true);
+ switchTab("about:mozilla", true);
+
+ let hashChangePromise = ContentTask.spawn(tabRefAboutHome.linkedBrowser, null, function* () {
+ yield ContentTaskUtils.waitForEvent(this, "hashchange", false);
+ });
+ switchTab("about:home#2", true, { ignoreFragment: "whenComparingAndReplace" });
+ is(tabRefAboutHome, gBrowser.selectedTab, "The same about:home tab should be switched to");
+ yield hashChangePromise;
+ is(gBrowser.currentURI.ref, "2", "The ref should be updated to the new ref");
+ switchTab("about:mozilla", true);
+ switchTab("about:home#3", true, { ignoreFragment: "whenComparing" });
+ is(tabRefAboutHome, gBrowser.selectedTab, "The same about:home tab should be switched to");
+ is(gBrowser.currentURI.ref, "2", "The ref should be unchanged since the fragment is only ignored when comparing");
+ switchTab("about:mozilla", true);
+ switchTab("about:home#1", false);
+ isnot(tabRefAboutHome, gBrowser.selectedTab, "Selected tab should not be initial about:blank tab");
+ is(gBrowser.tabs.length, numTabsAtStart + 1, "Should have one new tab opened");
+ switchTab("about:mozilla", true);
+ switchTab("about:home", true, {ignoreFragment: "whenComparingAndReplace"});
+ yield BrowserTestUtils.waitForCondition(function() {
+ return tabRefAboutHome.linkedBrowser.currentURI.spec == "about:home";
+ });
+ is(tabRefAboutHome.linkedBrowser.currentURI.spec, "about:home", "about:home shouldn't have hash");
+ switchTab("about:about", false, { ignoreFragment: "whenComparingAndReplace" });
+ cleanupTestTabs();
+});
+
+add_task(function* test_ignoreQueryString() {
+ let tabRefAboutHome =
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home?hello=firefox");
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ switchTab("about:home?hello=firefox", true);
+ switchTab("about:home?hello=firefoxos", false);
+ // Remove the last opened tab to test ignoreQueryString option.
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos", true, { ignoreQueryString: true });
+ is(tabRefAboutHome, gBrowser.selectedTab, "Selected tab should be the initial about:home tab");
+ is(gBrowser.currentURI.spec, "about:home?hello=firefox", "The spec should NOT be updated to the new query string");
+ cleanupTestTabs();
+});
+
+add_task(function* test_replaceQueryString() {
+ let tabRefAboutHome =
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home?hello=firefox");
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ switchTab("about:home", false);
+ switchTab("about:home?hello=firefox", true);
+ switchTab("about:home?hello=firefoxos", false);
+ // Remove the last opened tab to test replaceQueryString option.
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos", true, { replaceQueryString: true });
+ is(tabRefAboutHome, gBrowser.selectedTab, "Selected tab should be the initial about:home tab");
+ // Wait for the tab to load the new URI spec.
+ yield BrowserTestUtils.browserLoaded(tabRefAboutHome.linkedBrowser);
+ is(gBrowser.currentURI.spec, "about:home?hello=firefoxos", "The spec should be updated to the new spec");
+ cleanupTestTabs();
+});
+
+add_task(function* test_replaceQueryStringAndFragment() {
+ let tabRefAboutHome =
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home?hello=firefox#aaa");
+ let tabRefAboutMozilla =
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla?hello=firefoxos#aaa");
+
+ switchTab("about:home", false);
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefox#aaa", true);
+ is(tabRefAboutHome, gBrowser.selectedTab, "Selected tab should be the initial about:home tab");
+ switchTab("about:mozilla?hello=firefox#bbb", true, { replaceQueryString: true, ignoreFragment: "whenComparingAndReplace" });
+ is(tabRefAboutMozilla, gBrowser.selectedTab, "Selected tab should be the initial about:mozilla tab");
+ switchTab("about:home?hello=firefoxos#bbb", true, { ignoreQueryString: true, ignoreFragment: "whenComparingAndReplace" });
+ is(tabRefAboutHome, gBrowser.selectedTab, "Selected tab should be the initial about:home tab");
+ cleanupTestTabs();
+});
+
+add_task(function* test_ignoreQueryStringIgnoresFragment() {
+ let tabRefAboutHome =
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home?hello=firefox#aaa");
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla?hello=firefoxos#aaa");
+
+ switchTab("about:home?hello=firefox#bbb", false, { ignoreQueryString: true });
+ gBrowser.removeCurrentTab();
+ switchTab("about:home?hello=firefoxos#aaa", true, { ignoreQueryString: true });
+ is(tabRefAboutHome, gBrowser.selectedTab, "Selected tab should be the initial about:home tab");
+ cleanupTestTabs();
+});
+
+// Begin helpers
+
+function cleanupTestTabs() {
+ while (gBrowser.tabs.length > 1)
+ gBrowser.removeCurrentTab();
+}
+
+function switchTab(aURI, aShouldFindExistingTab, aOpenParams = {}) {
+ // Build the description before switchToTabHavingURI deletes the object properties.
+ let msg = `Should switch to existing ${aURI} tab if one existed, ` +
+ `${(aOpenParams.ignoreFragment ? "ignoring" : "including")} fragment portion, `;
+ if (aOpenParams.replaceQueryString) {
+ msg += "replacing";
+ } else if (aOpenParams.ignoreQueryString) {
+ msg += "ignoring";
+ } else {
+ msg += "including";
+ }
+ msg += " query string.";
+ let tabFound = switchToTabHavingURI(aURI, true, aOpenParams);
+ is(tabFound, aShouldFindExistingTab, msg);
+}
+
+registerCleanupFunction(cleanupTestTabs);
diff --git a/browser/base/content/test/urlbar/browser_bug1070778.js b/browser/base/content/test/urlbar/browser_bug1070778.js
new file mode 100644
index 000000000..ab88d04d8
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug1070778.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function is_selected(index) {
+ is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
+}
+
+add_task(function*() {
+ let bookmarks = [];
+ bookmarks.push((yield PlacesUtils.bookmarks
+ .insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/?q=%s",
+ title: "test" })));
+ yield PlacesUtils.keywords.insert({ keyword: "keyword",
+ url: "http://example.com/?q=%s" });
+
+ // This item only needed so we can select the keyword item, select something
+ // else, then select the keyword item again.
+ bookmarks.push((yield PlacesUtils.bookmarks
+ .insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/keyword",
+ title: "keyword abc" })));
+
+ registerCleanupFunction(function* () {
+ for (let bm of bookmarks) {
+ yield PlacesUtils.bookmarks.remove(bm);
+ }
+ });
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+ yield promiseAutocompleteResultPopup("keyword a");
+
+ // First item should already be selected
+ is_selected(0);
+ // Select next one (important!)
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is_selected(1);
+ // Re-select keyword item
+ EventUtils.synthesizeKey("VK_UP", {});
+ is_selected(0);
+
+ EventUtils.synthesizeKey("b", {});
+ yield promiseSearchComplete();
+
+ is(gURLBar.textValue, "keyword ab", "urlbar should have expected input");
+
+ let result = gURLBar.popup.richlistbox.firstChild;
+ isnot(result, null, "Should have first item");
+ let uri = NetUtil.newURI(result.getAttribute("url"));
+ is(uri.spec, PlacesUtils.mozActionURI("keyword", {url: "http://example.com/?q=ab", input: "keyword ab"}), "Expect correct url");
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promisePopupHidden(gURLBar.popup);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/urlbar/browser_bug1104165-switchtab-decodeuri.js b/browser/base/content/test/urlbar/browser_bug1104165-switchtab-decodeuri.js
new file mode 100644
index 000000000..d165d7304
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug1104165-switchtab-decodeuri.js
@@ -0,0 +1,29 @@
+add_task(function* test_switchtab_decodeuri() {
+ info("Opening first tab");
+ const TEST_URL = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html#test%7C1";
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ info("Opening and selecting second tab");
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ info("Wait for autocomplete")
+ yield promiseAutocompleteResultPopup("dummy_page");
+
+ info("Select autocomplete popup entry");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ ok(gURLBar.value.startsWith("moz-action:switchtab"), "switch to tab entry found");
+
+ info("switch-to-tab");
+ yield new Promise((resolve, reject) => {
+ // In case of success it should switch tab.
+ gBrowser.tabContainer.addEventListener("TabSelect", function select() {
+ gBrowser.tabContainer.removeEventListener("TabSelect", select, false);
+ is(gBrowser.selectedTab, tab, "Should have switched to the right tab");
+ resolve();
+ }, false);
+ EventUtils.synthesizeKey("VK_RETURN", { });
+ });
+
+ gBrowser.removeCurrentTab();
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/browser/base/content/test/urlbar/browser_bug1225194-remotetab.js b/browser/base/content/test/urlbar/browser_bug1225194-remotetab.js
new file mode 100644
index 000000000..3b4a44e76
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug1225194-remotetab.js
@@ -0,0 +1,16 @@
+add_task(function* test_remotetab_opens() {
+ const url = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+ yield BrowserTestUtils.withNewTab({url: "about:robots", gBrowser}, function* () {
+ // Set the urlbar to include the moz-action
+ gURLBar.value = "moz-action:remotetab," + JSON.stringify({ url });
+ // Focus the urlbar so we can press enter
+ gURLBar.focus();
+
+ // The URL is going to open in the current tab as it is currently about:blank
+ let promiseTabLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield promiseTabLoaded;
+
+ Assert.equal(gBrowser.selectedTab.linkedBrowser.currentURI.spec, url, "correct URL loaded");
+ });
+});
diff --git a/browser/base/content/test/urlbar/browser_bug304198.js b/browser/base/content/test/urlbar/browser_bug304198.js
new file mode 100644
index 000000000..dc8d39fae
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug304198.js
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(function* () {
+ let charsToDelete, deletedURLTab, fullURLTab, partialURLTab, testPartialURL, testURL;
+
+ charsToDelete = 5;
+ deletedURLTab = gBrowser.addTab();
+ fullURLTab = gBrowser.addTab();
+ partialURLTab = gBrowser.addTab();
+ testURL = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+
+ let loaded1 = BrowserTestUtils.browserLoaded(deletedURLTab.linkedBrowser, testURL);
+ let loaded2 = BrowserTestUtils.browserLoaded(fullURLTab.linkedBrowser, testURL);
+ let loaded3 = BrowserTestUtils.browserLoaded(partialURLTab.linkedBrowser, testURL);
+ deletedURLTab.linkedBrowser.loadURI(testURL);
+ fullURLTab.linkedBrowser.loadURI(testURL);
+ partialURLTab.linkedBrowser.loadURI(testURL);
+ yield Promise.all([loaded1, loaded2, loaded3]);
+
+ testURL = gURLBar.trimValue(testURL);
+ testPartialURL = testURL.substr(0, (testURL.length - charsToDelete));
+
+ function cleanUp() {
+ gBrowser.removeTab(fullURLTab);
+ gBrowser.removeTab(partialURLTab);
+ gBrowser.removeTab(deletedURLTab);
+ }
+
+ function* cycleTabs() {
+ yield BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after switching back to fullURLTab');
+
+ yield BrowserTestUtils.switchTab(gBrowser, partialURLTab);
+ is(gURLBar.textValue, testPartialURL, 'gURLBar.textValue should be testPartialURL after switching back to partialURLTab');
+ yield BrowserTestUtils.switchTab(gBrowser, deletedURLTab);
+ is(gURLBar.textValue, '', 'gURLBar.textValue should be "" after switching back to deletedURLTab');
+
+ yield BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after switching back to fullURLTab');
+ }
+
+ function urlbarBackspace() {
+ return new Promise((resolve, reject) => {
+ gBrowser.selectedBrowser.focus();
+ gURLBar.addEventListener("input", function () {
+ gURLBar.removeEventListener("input", arguments.callee, false);
+ resolve();
+ }, false);
+ gURLBar.focus();
+ if (gURLBar.selectionStart == gURLBar.selectionEnd) {
+ gURLBar.selectionStart = gURLBar.selectionEnd = gURLBar.textValue.length;
+ }
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+ });
+ }
+
+ function* prepareDeletedURLTab() {
+ yield BrowserTestUtils.switchTab(gBrowser, deletedURLTab);
+ is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to deletedURLTab');
+
+ // simulate the user removing the whole url from the location bar
+ gPrefService.setBoolPref("browser.urlbar.clickSelectsAll", true);
+
+ yield urlbarBackspace();
+ is(gURLBar.textValue, "", 'gURLBar.textValue should be "" (just set)');
+ if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll")) {
+ gPrefService.clearUserPref("browser.urlbar.clickSelectsAll");
+ }
+ }
+
+ function* prepareFullURLTab() {
+ yield BrowserTestUtils.switchTab(gBrowser, fullURLTab);
+ is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to fullURLTab');
+ }
+
+ function* preparePartialURLTab() {
+ yield BrowserTestUtils.switchTab(gBrowser, partialURLTab);
+ is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to partialURLTab');
+
+ // simulate the user removing part of the url from the location bar
+ gPrefService.setBoolPref("browser.urlbar.clickSelectsAll", false);
+
+ let deleted = 0;
+ while (deleted < charsToDelete) {
+ yield urlbarBackspace(arguments.callee);
+ deleted++;
+ }
+
+ is(gURLBar.textValue, testPartialURL, "gURLBar.textValue should be testPartialURL (just set)");
+ if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll")) {
+ gPrefService.clearUserPref("browser.urlbar.clickSelectsAll");
+ }
+ }
+
+ // prepare the three tabs required by this test
+
+ // First tab
+ yield* prepareFullURLTab();
+ yield* preparePartialURLTab();
+ yield* prepareDeletedURLTab();
+
+ // now cycle the tabs and make sure everything looks good
+ yield* cycleTabs();
+ cleanUp();
+});
+
+
diff --git a/browser/base/content/test/urlbar/browser_bug556061.js b/browser/base/content/test/urlbar/browser_bug556061.js
new file mode 100644
index 000000000..4c6ac5bf5
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug556061.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 testURL = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+var testActionURL = "moz-action:switchtab," + JSON.stringify({url: testURL});
+testURL = gURLBar.trimValue(testURL);
+var testTab;
+
+function runNextTest() {
+ if (tests.length) {
+ let t = tests.shift();
+ waitForClipboard(t.expected, t.setup, function() {
+ t.success();
+ runNextTest();
+ }, cleanup);
+ }
+ else {
+ cleanup();
+ }
+}
+
+function cleanup() {
+ gBrowser.removeTab(testTab);
+ finish();
+}
+
+var tests = [
+ {
+ expected: testURL,
+ setup: function() {
+ gURLBar.value = testActionURL;
+ gURLBar.valueIsTyped = true;
+ is(gURLBar.value, testActionURL, "gURLBar starts with the correct real value");
+ is(gURLBar.textValue, testURL, "gURLBar starts with the correct display value");
+
+ // Focus the urlbar so we can select it all & copy
+ gURLBar.focus();
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ },
+ success: function() {
+ is(gURLBar.value, testActionURL, "gURLBar.value didn't change when copying");
+ }
+ },
+ {
+ expected: testURL.substring(0, 10),
+ setup: function() {
+ // Set selectionStart/End manually and make sure it matches the substring
+ gURLBar.selectionStart = 0;
+ gURLBar.selectionEnd = 10;
+ goDoCommand("cmd_copy");
+ },
+ success: function() {
+ is(gURLBar.value, testActionURL, "gURLBar.value didn't change when copying");
+ }
+ },
+ {
+ expected: testURL,
+ setup: function() {
+ // Setup for cut test...
+ // Select all
+ gURLBar.select();
+ goDoCommand("cmd_cut");
+ },
+ success: function() {
+ is(gURLBar.value, "", "gURLBar.value is now empty");
+ }
+ },
+ {
+ expected: testURL.substring(testURL.length - 10, testURL.length),
+ setup: function() {
+ // Reset urlbar value
+ gURLBar.value = testActionURL;
+ gURLBar.valueIsTyped = true;
+ // Sanity check that we have the right value
+ is(gURLBar.value, testActionURL, "gURLBar starts with the correct real value");
+ is(gURLBar.textValue, testURL, "gURLBar starts with the correct display value");
+
+ // Now just select part of the value & cut that.
+ gURLBar.selectionStart = testURL.length - 10;
+ gURLBar.selectionEnd = testURL.length;
+ goDoCommand("cmd_cut");
+ },
+ success: function() {
+ is(gURLBar.value, testURL.substring(0, testURL.length - 10), "gURLBar.value has the correct value");
+ }
+ }
+];
+
+function test() {
+ waitForExplicitFinish();
+ testTab = gBrowser.addTab();
+ gBrowser.selectedTab = testTab;
+
+ // Kick off the testing
+ runNextTest();
+}
diff --git a/browser/base/content/test/urlbar/browser_bug562649.js b/browser/base/content/test/urlbar/browser_bug562649.js
new file mode 100644
index 000000000..f56e430ee
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug562649.js
@@ -0,0 +1,24 @@
+function test() {
+ const URI = "data:text/plain,bug562649";
+ browserDOMWindow.openURI(makeURI(URI),
+ null,
+ Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+ Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+
+ is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI");
+ is(gURLBar.value, URI, "location bar value matches test URI");
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.removeCurrentTab({ skipPermitUnload: true });
+ is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI after switching tabs");
+ is(gURLBar.value, URI, "location bar value matches test URI after switching tabs");
+
+ waitForExplicitFinish();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ is(gBrowser.userTypedValue, null, "userTypedValue is null as the page has loaded");
+ is(gURLBar.value, URI, "location bar value matches test URI as the page has loaded");
+
+ gBrowser.removeCurrentTab({ skipPermitUnload: true });
+ finish();
+ });
+}
diff --git a/browser/base/content/test/urlbar/browser_bug623155.js b/browser/base/content/test/urlbar/browser_bug623155.js
new file mode 100644
index 000000000..dd6ff8c85
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug623155.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const REDIRECT_FROM = "https://example.com/browser/browser/base/content/test/urlbar/" +
+ "redirect_bug623155.sjs";
+
+const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host.
+
+function isRedirectedURISpec(aURISpec) {
+ return isRedirectedURI(Services.io.newURI(aURISpec, null, null));
+}
+
+function isRedirectedURI(aURI) {
+ // Compare only their before-hash portion.
+ return Services.io.newURI(REDIRECT_TO, null, null)
+ .equalsExceptRef(aURI);
+}
+
+/*
+ Test.
+
+1. Load
+https://example.com/browser/browser/base/content/test/urlbar/redirect_bug623155.sjs#BG
+ in a background tab.
+
+2. The redirected URI is <https://www.bank1.com/#BG>, which displayes a cert
+ error page.
+
+3. Switch the tab to foreground.
+
+4. Check the URLbar's value, expecting <https://www.bank1.com/#BG>
+
+5. Load
+https://example.com/browser/browser/base/content/test/urlbar/redirect_bug623155.sjs#FG
+ in the foreground tab.
+
+6. The redirected URI is <https://www.bank1.com/#FG>. And this is also
+ a cert-error page.
+
+7. Check the URLbar's value, expecting <https://www.bank1.com/#FG>
+
+8. End.
+
+ */
+
+var gNewTab;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Load a URI in the background.
+ gNewTab = gBrowser.addTab(REDIRECT_FROM + "#BG");
+ gBrowser.getBrowserForTab(gNewTab)
+ .webProgress
+ .addProgressListener(gWebProgressListener,
+ Components.interfaces.nsIWebProgress
+ .NOTIFY_LOCATION);
+}
+
+var gWebProgressListener = {
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ // ---------------------------------------------------------------------------
+ // NOTIFY_LOCATION mode should work fine without these methods.
+ //
+ // onStateChange: function() {},
+ // onStatusChange: function() {},
+ // onProgressChange: function() {},
+ // onSecurityChange: function() {},
+ // ----------------------------------------------------------------------------
+
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+ if (!aRequest) {
+ // This is bug 673752, or maybe initial "about:blank".
+ return;
+ }
+
+ ok(gNewTab, "There is a new tab.");
+ ok(isRedirectedURI(aLocation),
+ "onLocationChange catches only redirected URI.");
+
+ if (aLocation.ref == "BG") {
+ // This is background tab's request.
+ isnot(gNewTab, gBrowser.selectedTab, "This is a background tab.");
+ } else if (aLocation.ref == "FG") {
+ // This is foreground tab's request.
+ is(gNewTab, gBrowser.selectedTab, "This is a foreground tab.");
+ }
+ else {
+ // We shonuld not reach here.
+ ok(false, "This URI hash is not expected:" + aLocation.ref);
+ }
+
+ let isSelectedTab = gNewTab.selected;
+ setTimeout(delayed, 0, isSelectedTab);
+ }
+};
+
+function delayed(aIsSelectedTab) {
+ // Switch tab and confirm URL bar.
+ if (!aIsSelectedTab) {
+ gBrowser.selectedTab = gNewTab;
+ }
+
+ let currentURI = gBrowser.selectedBrowser.currentURI.spec;
+ ok(isRedirectedURISpec(currentURI),
+ "The content area is redirected. aIsSelectedTab:" + aIsSelectedTab);
+ is(gURLBar.value, currentURI,
+ "The URL bar shows the content URI. aIsSelectedTab:" + aIsSelectedTab);
+
+ if (!aIsSelectedTab) {
+ // If this was a background request, go on a foreground request.
+ gBrowser.selectedBrowser.loadURI(REDIRECT_FROM + "#FG");
+ }
+ else {
+ // Othrewise, nothing to do remains.
+ finish();
+ }
+}
+
+/* Cleanup */
+registerCleanupFunction(function() {
+ if (gNewTab) {
+ gBrowser.getBrowserForTab(gNewTab)
+ .webProgress
+ .removeProgressListener(gWebProgressListener);
+
+ gBrowser.removeTab(gNewTab);
+ }
+ gNewTab = null;
+});
diff --git a/browser/base/content/test/urlbar/browser_bug783614.js b/browser/base/content/test/urlbar/browser_bug783614.js
new file mode 100644
index 000000000..ebc62e8fa
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_bug783614.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ gURLBar.focus();
+ gURLBar.inputField.value = "https://example.com/";
+ gURLBar.selectionStart = 4;
+ gURLBar.selectionEnd = 5;
+ goDoCommand("cmd_cut");
+ is(gURLBar.inputField.value, "http://example.com/", "location bar value after cutting 's' from https");
+ gURLBar.handleRevert();
+}
diff --git a/browser/base/content/test/urlbar/browser_canonizeURL.js b/browser/base/content/test/urlbar/browser_canonizeURL.js
new file mode 100644
index 000000000..59ab54ca0
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_canonizeURL.js
@@ -0,0 +1,42 @@
+add_task(function*() {
+ let testcases = [
+ ["example", "http://www.example.net/", { shiftKey: true }],
+ // Check that a direct load is not overwritten by a previous canonization.
+ ["http://example.com/test/", "http://example.com/test/", {}],
+ ["ex-ample", "http://www.ex-ample.net/", { shiftKey: true }],
+ [" example ", "http://www.example.net/", { shiftKey: true }],
+ [" example/foo ", "http://www.example.net/foo", { shiftKey: true }],
+ [" example/foo bar ", "http://www.example.net/foo%20bar", { shiftKey: true }],
+ ["example.net", "http://example.net/", { shiftKey: true }],
+ ["http://example", "http://example/", { shiftKey: true }],
+ ["example:8080", "http://example:8080/", { shiftKey: true }],
+ ["ex-ample.foo", "http://ex-ample.foo/", { shiftKey: true }],
+ ["example.foo/bar ", "http://example.foo/bar", { shiftKey: true }],
+ ["1.1.1.1", "http://1.1.1.1/", { shiftKey: true }],
+ ["ftp://example", "ftp://example/", { shiftKey: true }],
+ ["ftp.example.bar", "http://ftp.example.bar/", { shiftKey: true }],
+ ["ex ample", Services.search.defaultEngine.getSubmission("ex ample", null, "keyword").uri.spec, { shiftKey: true }],
+ ];
+
+ // Disable autoFill for this test, since it could mess up the results.
+ let autoFill = Preferences.get("browser.urlbar.autoFill");
+ Preferences.set("browser.urlbar.autoFill", false);
+ registerCleanupFunction(() => {
+ Preferences.set("browser.urlbar.autoFill", autoFill);
+ });
+
+ for (let [inputValue, expectedURL, options] of testcases) {
+ let promiseLoad = waitForDocLoadAndStopIt(expectedURL);
+ gURLBar.focus();
+ if (Object.keys(options).length > 0) {
+ gURLBar.selectionStart = gURLBar.selectionEnd =
+ gURLBar.inputField.value.length;
+ gURLBar.inputField.value = inputValue.slice(0, -1);
+ EventUtils.synthesizeKey(inputValue.slice(-1), {});
+ } else {
+ gURLBar.textValue = inputValue;
+ }
+ EventUtils.synthesizeKey("VK_RETURN", options);
+ yield promiseLoad;
+ }
+});
diff --git a/browser/base/content/test/urlbar/browser_dragdropURL.js b/browser/base/content/test/urlbar/browser_dragdropURL.js
new file mode 100644
index 000000000..ec2906700
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_dragdropURL.js
@@ -0,0 +1,15 @@
+"use strict";
+
+const TEST_URL = "data:text/html,a test page";
+const DRAG_URL = "http://www.example.com/";
+
+add_task(function* checkURLBarUpdateForDrag() {
+ yield BrowserTestUtils.withNewTab(TEST_URL, function* (browser) {
+ // Have to use something other than the URL bar as a source, so picking the
+ // downloads button somewhat arbitrarily:
+ EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
+ [[{type: "text/plain", data: DRAG_URL}]], "copy", window);
+ is(gURLBar.value, TEST_URL, "URL bar value should not have changed");
+ is(gBrowser.selectedBrowser.userTypedValue, null, "Stored URL bar value should not have changed");
+ });
+});
diff --git a/browser/base/content/test/urlbar/browser_locationBarCommand.js b/browser/base/content/test/urlbar/browser_locationBarCommand.js
new file mode 100644
index 000000000..935bdf758
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_locationBarCommand.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_VALUE = "example.com";
+const START_VALUE = "example.org";
+
+add_task(function* setup() {
+ Services.prefs.setBoolPref("browser.altClickSave", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.altClickSave");
+ });
+});
+
+add_task(function* alt_left_click_test() {
+ info("Running test: Alt left click");
+
+ // Monkey patch saveURL() to avoid dealing with file save code paths.
+ let oldSaveURL = saveURL;
+ let saveURLPromise = new Promise(resolve => {
+ saveURL = () => {
+ // Restore old saveURL() value.
+ saveURL = oldSaveURL;
+ resolve();
+ };
+ });
+
+ triggerCommand(true, {altKey: true});
+
+ yield saveURLPromise;
+ ok(true, "SaveURL was called");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+});
+
+add_task(function* shift_left_click_test() {
+ info("Running test: Shift left click");
+
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow();
+ triggerCommand(true, {shiftKey: true});
+ let win = yield newWindowPromise;
+
+ // Wait for the initial browser to load.
+ let browser = win.gBrowser.selectedBrowser;
+ let destinationURL = "http://" + TEST_VALUE + "/";
+ yield Promise.all([
+ BrowserTestUtils.browserLoaded(browser),
+ BrowserTestUtils.waitForLocationChange(win.gBrowser, destinationURL)
+ ]);
+
+ info("URL should be loaded in a new window");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+ yield promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(document.activeElement, gBrowser.selectedBrowser, "Content window should be focused");
+ is(win.gURLBar.textValue, TEST_VALUE, "New URL is loaded in new window");
+
+ // Cleanup.
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+add_task(function* right_click_test() {
+ info("Running test: Right click on go button");
+
+ // Add a new tab.
+ yield* promiseOpenNewTab();
+
+ triggerCommand(true, {button: 2});
+
+ // Right click should do nothing (context menu will be shown).
+ is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* shift_accel_left_click_test() {
+ info("Running test: Shift+Ctrl/Cmd left click on go button");
+
+ // Add a new tab.
+ let tab = yield* promiseOpenNewTab();
+
+ let loadStartedPromise = promiseLoadStarted();
+ triggerCommand(true, {accelKey: true, shiftKey: true});
+ yield loadStartedPromise;
+
+ // Check the load occurred in a new background tab.
+ info("URL should be loaded in a new background tab");
+ is(gURLBar.value, "", "Urlbar reverted to original value");
+ ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command");
+ is(gBrowser.selectedTab, tab, "Focus did not change to the new tab");
+
+ // Select the new background tab
+ gBrowser.selectedTab = gBrowser.selectedTab.nextSibling;
+ is(gURLBar.value, TEST_VALUE, "New URL is loaded in new tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(function* load_in_current_tab_test() {
+ let tests = [
+ {desc: "Simple return keypress"},
+ {desc: "Left click on go button", click: true},
+ {desc: "Ctrl/Cmd+Return keypress", event: {accelKey: true}},
+ {desc: "Alt+Return keypress in a blank tab", event: {altKey: true}}
+ ];
+
+ for (let test of tests) {
+ info(`Running test: ${test.desc}`);
+
+ // Add a new tab.
+ let tab = yield* promiseOpenNewTab();
+
+ // Trigger a load and check it occurs in the current tab.
+ let loadStartedPromise = promiseLoadStarted();
+ triggerCommand(test.click || false, test.event || {});
+ yield loadStartedPromise;
+
+ info("URL should be loaded in the current tab");
+ is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
+ yield promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(document.activeElement, gBrowser.selectedBrowser, "Content window should be focused");
+ is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ }
+});
+
+add_task(function* load_in_new_tab_test() {
+ let tests = [
+ {desc: "Ctrl/Cmd left click on go button", click: true, event: {accelKey: true}},
+ {desc: "Alt+Return keypress in a dirty tab", event: {altKey: true}, url: START_VALUE}
+ ];
+
+ for (let test of tests) {
+ info(`Running test: ${test.desc}`);
+
+ // Add a new tab.
+ let tab = yield* promiseOpenNewTab(test.url || "about:blank");
+
+ // Trigger a load and check it occurs in the current tab.
+ let tabSwitchedPromise = promiseNewTabSwitched();
+ triggerCommand(test.click || false, test.event || {});
+ yield tabSwitchedPromise;
+
+ // Check the load occurred in a new tab.
+ info("URL should be loaded in a new focused tab");
+ is(gURLBar.inputField.value, TEST_VALUE, "Urlbar still has the value we entered");
+ yield promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser);
+ is(document.activeElement, gBrowser.selectedBrowser, "Content window should be focused");
+ isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function triggerCommand(shouldClick, event) {
+ gURLBar.value = TEST_VALUE;
+ gURLBar.focus();
+
+ if (shouldClick) {
+ is(gURLBar.getAttribute("pageproxystate"), "invalid",
+ "page proxy state must be invalid for go button to be visible");
+
+ let goButton = document.getElementById("urlbar-go-button");
+ EventUtils.synthesizeMouseAtCenter(goButton, event);
+ } else {
+ EventUtils.synthesizeKey("VK_RETURN", event);
+ }
+}
+
+function promiseLoadStarted() {
+ return new Promise(resolve => {
+ gBrowser.addTabsProgressListener({
+ onStateChange(browser, webProgress, req, flags, status) {
+ if (flags & Ci.nsIWebProgressListener.STATE_START) {
+ gBrowser.removeTabsProgressListener(this);
+ resolve();
+ }
+ }
+ });
+ });
+}
+
+function* promiseOpenNewTab(url = "about:blank") {
+ let tab = gBrowser.addTab(url);
+ let tabSwitchPromise = promiseNewTabSwitched(tab);
+ gBrowser.selectedTab = tab;
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ yield tabSwitchPromise;
+ return tab;
+}
+
+function promiseNewTabSwitched() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener("TabSwitchDone", function onSwitch() {
+ gBrowser.removeEventListener("TabSwitchDone", onSwitch);
+ executeSoon(resolve);
+ });
+ });
+}
+
+function promiseCheckChildNoFocusedElement(browser)
+{
+ if (!gMultiProcessBrowser) {
+ Assert.equal(Services.focus.focusedElement, null, "There should be no focused element");
+ return null;
+ }
+
+ return ContentTask.spawn(browser, { }, function* () {
+ const fm = Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager);
+ Assert.equal(fm.focusedElement, null, "There should be no focused element");
+ });
+}
diff --git a/browser/base/content/test/urlbar/browser_locationBarExternalLoad.js b/browser/base/content/test/urlbar/browser_locationBarExternalLoad.js
new file mode 100644
index 000000000..31fc84768
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_locationBarExternalLoad.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const url = "data:text/html,<body>hi";
+
+add_task(function*() {
+ yield* testURL(url, urlEnter);
+ yield* testURL(url, urlClick);
+});
+
+function urlEnter(url) {
+ gURLBar.value = url;
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+}
+
+function urlClick(url) {
+ gURLBar.value = url;
+ gURLBar.focus();
+ let goButton = document.getElementById("urlbar-go-button");
+ EventUtils.synthesizeMouseAtCenter(goButton, {});
+}
+
+function promiseNewTabSwitched() {
+ return new Promise(resolve => {
+ gBrowser.addEventListener("TabSwitchDone", function onSwitch() {
+ gBrowser.removeEventListener("TabSwitchDone", onSwitch);
+ executeSoon(resolve);
+ });
+ });
+}
+
+function* testURL(url, loadFunc, endFunc) {
+ let tabSwitchedPromise = promiseNewTabSwitched();
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ let browser = gBrowser.selectedBrowser;
+
+ let pageshowPromise = BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+
+ yield tabSwitchedPromise;
+ yield pageshowPromise;
+
+ let pagePrincipal = gBrowser.contentPrincipal;
+ loadFunc(url);
+
+ yield BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+
+ yield ContentTask.spawn(browser, { isRemote: gMultiProcessBrowser },
+ function* (arg) {
+ const fm = Components.classes["@mozilla.org/focus-manager;1"].
+ getService(Components.interfaces.nsIFocusManager);
+ Assert.equal(fm.focusedElement, null, "focusedElement not null");
+
+ if (arg.isRemote) {
+ Assert.equal(fm.activeWindow, content, "activeWindow not correct");
+ }
+ });
+
+ is(document.activeElement, browser, "content window should be focused");
+
+ ok(!gBrowser.contentPrincipal.equals(pagePrincipal),
+ "load of " + url + " by " + loadFunc.name + " should produce a page with a different principal");
+
+ gBrowser.removeTab(tab);
+}
diff --git a/browser/base/content/test/urlbar/browser_moz_action_link.js b/browser/base/content/test/urlbar/browser_moz_action_link.js
new file mode 100644
index 000000000..ed2d36ee5
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_moz_action_link.js
@@ -0,0 +1,31 @@
+"use strict";
+
+const kURIs = [
+ "moz-action:foo,",
+ "moz-action:foo",
+];
+
+add_task(function*() {
+ for (let uri of kURIs) {
+ let dataURI = `data:text/html,<a id=a href="${uri}" target=_blank>Link</a>`;
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, dataURI);
+
+ let tabSwitchPromise = BrowserTestUtils.switchTab(gBrowser, function() {});
+ yield ContentTask.spawn(tab.linkedBrowser, null, function*() {
+ content.document.getElementById("a").click();
+ });
+ yield tabSwitchPromise;
+ isnot(gBrowser.selectedTab, tab, "Switched to new tab!");
+ is(gURLBar.value, "about:blank", "URL bar should be displaying about:blank");
+ let newTab = gBrowser.selectedTab;
+ yield BrowserTestUtils.switchTab(gBrowser, tab);
+ yield BrowserTestUtils.switchTab(gBrowser, newTab);
+ is(gBrowser.selectedTab, newTab, "Switched to new tab again!");
+ is(gURLBar.value, "about:blank", "URL bar should be displaying about:blank after tab switch");
+ // Finally, check that directly setting it produces the right results, too:
+ URLBarSetURI(makeURI(uri));
+ is(gURLBar.value, "about:blank", "URL bar should still be displaying about:blank");
+ yield BrowserTestUtils.removeTab(newTab);
+ yield BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/base/content/test/urlbar/browser_removeUnsafeProtocolsFromURLBarPaste.js b/browser/base/content/test/urlbar/browser_removeUnsafeProtocolsFromURLBarPaste.js
new file mode 100644
index 000000000..e9ba8d989
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_removeUnsafeProtocolsFromURLBarPaste.js
@@ -0,0 +1,49 @@
+function test() {
+ waitForExplicitFinish();
+ testNext();
+}
+
+var pairs = [
+ ["javascript:", ""],
+ ["javascript:1+1", "1+1"],
+ ["javascript:document.domain", "document.domain"],
+ ["data:text/html,<body>hi</body>", "data:text/html,<body>hi</body>"],
+ // Nested things get confusing because some things don't parse as URIs:
+ ["javascript:javascript:alert('hi!')", "alert('hi!')"],
+ ["data:data:text/html,<body>hi</body>", "data:data:text/html,<body>hi</body>"],
+ ["javascript:data:javascript:alert('hi!')", "data:javascript:alert('hi!')"],
+ ["javascript:data:text/html,javascript:alert('hi!')", "data:text/html,javascript:alert('hi!')"],
+ ["data:data:text/html,javascript:alert('hi!')", "data:data:text/html,javascript:alert('hi!')"],
+];
+
+var clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+
+function paste(input, cb) {
+ waitForClipboard(input, function() {
+ clipboardHelper.copyString(input);
+ }, function() {
+ document.commandDispatcher.getControllerForCommand("cmd_paste").doCommand("cmd_paste");
+ cb();
+ }, function() {
+ ok(false, "Failed to copy string '" + input + "' to clipboard");
+ cb();
+ });
+}
+
+function testNext() {
+ gURLBar.value = '';
+ if (!pairs.length) {
+ finish();
+ return;
+ }
+
+ let [inputValue, expectedURL] = pairs.shift();
+
+ gURLBar.focus();
+ paste(inputValue, function() {
+ is(gURLBar.textValue, expectedURL, "entering '" + inputValue + "' strips relevant bits.");
+
+ setTimeout(testNext, 0);
+ });
+}
+
diff --git a/browser/base/content/test/urlbar/browser_search_favicon.js b/browser/base/content/test/urlbar/browser_search_favicon.js
new file mode 100644
index 000000000..a8e6dbbcd
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_search_favicon.js
@@ -0,0 +1,52 @@
+var gOriginalEngine;
+var gEngine;
+var gRestyleSearchesPref = "browser.urlbar.restyleSearches";
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(gRestyleSearchesPref);
+ Services.search.currentEngine = gOriginalEngine;
+ Services.search.removeEngine(gEngine);
+ return PlacesTestUtils.clearHistory();
+});
+
+add_task(function*() {
+ Services.prefs.setBoolPref(gRestyleSearchesPref, true);
+});
+
+add_task(function*() {
+
+ Services.search.addEngineWithDetails("SearchEngine", "", "", "",
+ "GET", "http://s.example.com/search");
+ gEngine = Services.search.getEngineByName("SearchEngine");
+ gEngine.addParam("q", "{searchTerms}", null);
+ gOriginalEngine = Services.search.currentEngine;
+ Services.search.currentEngine = gEngine;
+
+ let uri = NetUtil.newURI("http://s.example.com/search?q=foo&client=1");
+ yield PlacesTestUtils.addVisits({ uri: uri, title: "Foo - SearchEngine Search" });
+
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+ // The first autocomplete result has the action searchengine, while
+ // the second result is the "search favicon" element.
+ yield promiseAutocompleteResultPopup("foo");
+ let result = gURLBar.popup.richlistbox.children[1];
+
+ isnot(result, null, "Expect a search result");
+ is(result.getAttribute("type"), "searchengine", "Expect correct `type` attribute");
+
+ let titleHbox = result._titleText.parentNode.parentNode;
+ ok(titleHbox.classList.contains("ac-title"), "Title hbox sanity check");
+ is_element_visible(titleHbox, "Title element should be visible");
+
+ let urlHbox = result._urlText.parentNode.parentNode;
+ ok(urlHbox.classList.contains("ac-url"), "URL hbox sanity check");
+ is_element_hidden(urlHbox, "URL element should be hidden");
+
+ let actionHbox = result._actionText.parentNode.parentNode;
+ ok(actionHbox.classList.contains("ac-action"), "Action hbox sanity check");
+ is_element_hidden(actionHbox, "Action element should be hidden because it is not selected");
+ is(result._actionText.textContent, "Search with SearchEngine", "Action text should be as expected");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar.js b/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar.js
new file mode 100644
index 000000000..d207092d4
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar.js
@@ -0,0 +1,216 @@
+/* -*- 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/. */
+
+requestLongerTimeout(2);
+
+const TEST_URL_BASES = [
+ "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html#tabmatch",
+ "http://example.org/browser/browser/base/content/test/urlbar/moz.png#tabmatch"
+];
+
+var gController = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+var gTabCounter = 0;
+
+add_task(function* step_1() {
+ info("Running step 1");
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ let promises = [];
+ for (let i = 0; i < maxResults - 1; i++) {
+ let tab = gBrowser.addTab();
+ promises.push(loadTab(tab, TEST_URL_BASES[0] + (++gTabCounter)));
+ }
+
+ yield Promise.all(promises);
+ yield ensure_opentabs_match_db();
+});
+
+add_task(function* step_2() {
+ info("Running step 2");
+ gBrowser.selectTabAtIndex(1);
+ gBrowser.removeCurrentTab();
+ gBrowser.selectTabAtIndex(1);
+ gBrowser.removeCurrentTab();
+
+ let promises = [];
+ for (let i = 1; i < gBrowser.tabs.length; i++)
+ promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[1] + (++gTabCounter)));
+
+ yield Promise.all(promises);
+ yield ensure_opentabs_match_db();
+});
+
+add_task(function* step_3() {
+ info("Running step 3");
+ let promises = [];
+ for (let i = 1; i < gBrowser.tabs.length; i++)
+ promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[0] + gTabCounter));
+
+ yield Promise.all(promises);
+ yield ensure_opentabs_match_db();
+});
+
+add_task(function* step_4() {
+ info("Running step 4 - ensure we don't register subframes as open pages");
+ let tab = gBrowser.addTab();
+ tab.linkedBrowser.loadURI('data:text/html,<body><iframe src=""></iframe></body>');
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ yield ContentTask.spawn(tab.linkedBrowser, null, function* () {
+ let iframe_loaded = ContentTaskUtils.waitForEvent(content.document, "load", true);
+ content.document.querySelector("iframe").src = "http://test2.example.org/";
+ yield iframe_loaded;
+ });
+
+ yield ensure_opentabs_match_db();
+});
+
+add_task(function* step_5() {
+ info("Running step 5 - remove tab immediately");
+ let tab = gBrowser.addTab("about:logo");
+ yield BrowserTestUtils.removeTab(tab);
+ yield ensure_opentabs_match_db();
+});
+
+add_task(function* step_6() {
+ info("Running step 6 - check swapBrowsersAndCloseOther preserves registered switch-to-tab result");
+ let tabToKeep = gBrowser.addTab();
+ let tab = gBrowser.addTab();
+ tab.linkedBrowser.loadURI("about:mozilla");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ gBrowser.updateBrowserRemoteness(tabToKeep.linkedBrowser, tab.linkedBrowser.isRemoteBrowser);
+ gBrowser.swapBrowsersAndCloseOther(tabToKeep, tab);
+
+ yield ensure_opentabs_match_db()
+
+ yield BrowserTestUtils.removeTab(tabToKeep);
+
+ yield ensure_opentabs_match_db();
+});
+
+add_task(function* step_7() {
+ info("Running step 7 - close all tabs");
+
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+
+ gBrowser.addTab("about:blank", {skipAnimation: true});
+ while (gBrowser.tabs.length > 1) {
+ info("Removing tab: " + gBrowser.tabs[0].linkedBrowser.currentURI.spec);
+ gBrowser.selectTabAtIndex(0);
+ gBrowser.removeCurrentTab();
+ }
+
+ yield ensure_opentabs_match_db();
+});
+
+add_task(function* cleanup() {
+ info("Cleaning up");
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+function loadTab(tab, url) {
+ // Because adding visits is async, we will not be notified immediately.
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let visited = new Promise(resolve => {
+ Services.obs.addObserver(
+ function observer(aSubject, aTopic, aData) {
+ if (url != aSubject.QueryInterface(Ci.nsIURI).spec)
+ return;
+ Services.obs.removeObserver(observer, aTopic);
+ resolve();
+ },
+ "uri-visit-saved",
+ false
+ );
+ });
+
+ info("Loading page: " + url);
+ tab.linkedBrowser.loadURI(url);
+ return Promise.all([ loaded, visited ]);
+}
+
+function ensure_opentabs_match_db() {
+ var tabs = {};
+
+ var winEnum = Services.wm.getEnumerator("navigator:browser");
+ while (winEnum.hasMoreElements()) {
+ let browserWin = winEnum.getNext();
+ // skip closed-but-not-destroyed windows
+ if (browserWin.closed)
+ continue;
+
+ for (let i = 0; i < browserWin.gBrowser.tabContainer.childElementCount; i++) {
+ let browser = browserWin.gBrowser.getBrowserAtIndex(i);
+ let url = browser.currentURI.spec;
+ if (browserWin.isBlankPageURL(url))
+ continue;
+ if (!(url in tabs))
+ tabs[url] = 1;
+ else
+ tabs[url]++;
+ }
+ }
+
+ return new Promise(resolve => {
+ checkAutocompleteResults(tabs, resolve);
+ });
+}
+
+function checkAutocompleteResults(aExpected, aCallback)
+{
+ gController.input = {
+ timeout: 10,
+ textValue: "",
+ searches: ["unifiedcomplete"],
+ searchParam: "enable-actions",
+ popupOpen: false,
+ minResultsForPopup: 0,
+ invalidate: function() {},
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+ get popup() { return this; },
+ onSearchBegin: function() {},
+ onSearchComplete: function ()
+ {
+ info("Found " + gController.matchCount + " matches.");
+ // Check to see the expected uris and titles match up (in any order)
+ for (let i = 0; i < gController.matchCount; i++) {
+ if (gController.getStyleAt(i).includes("heuristic")) {
+ info("Skip heuristic match");
+ continue;
+ }
+ let action = gURLBar.popup.input._parseActionUrl(gController.getValueAt(i));
+ let uri = action.params.url;
+
+ info("Search for '" + uri + "' in open tabs.");
+ let expected = uri in aExpected;
+ ok(expected, uri + " was found in autocomplete, was " + (expected ? "" : "not ") + "expected");
+ // Remove the found entry from expected results.
+ delete aExpected[uri];
+ }
+
+ // Make sure there is no reported open page that is not open.
+ for (let entry in aExpected) {
+ ok(false, "'" + entry + "' should be found in autocomplete");
+ }
+
+ executeSoon(aCallback);
+ },
+ setSelectedIndex: function() {},
+ get searchCount() { return this.searches.length; },
+ getSearchAt: function(aIndex) { return this.searches[aIndex]; },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ ])
+ };
+
+ info("Searching open pages.");
+ gController.startSearch(Services.prefs.getCharPref("browser.urlbar.restrict.openpage"));
+}
diff --git a/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js b/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js
new file mode 100644
index 000000000..08a18b38a
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js
@@ -0,0 +1,84 @@
+let testURL = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+
+add_task(function*() {
+ let normalWindow = yield BrowserTestUtils.openNewBrowserWindow();
+ let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+ yield runTest(normalWindow, privateWindow, false);
+ yield BrowserTestUtils.closeWindow(normalWindow);
+ yield BrowserTestUtils.closeWindow(privateWindow);
+
+ normalWindow = yield BrowserTestUtils.openNewBrowserWindow();
+ privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+ yield runTest(privateWindow, normalWindow, false);
+ yield BrowserTestUtils.closeWindow(normalWindow);
+ yield BrowserTestUtils.closeWindow(privateWindow);
+
+ privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+ yield runTest(privateWindow, privateWindow, false);
+ yield BrowserTestUtils.closeWindow(privateWindow);
+
+ normalWindow = yield BrowserTestUtils.openNewBrowserWindow();
+ yield runTest(normalWindow, normalWindow, true);
+ yield BrowserTestUtils.closeWindow(normalWindow);
+});
+
+function* runTest(aSourceWindow, aDestWindow, aExpectSwitch, aCallback) {
+ yield BrowserTestUtils.openNewForegroundTab(aSourceWindow.gBrowser, testURL);
+ let testTab = yield BrowserTestUtils.openNewForegroundTab(aDestWindow.gBrowser);
+
+ info("waiting for focus on the window");
+ yield SimpleTest.promiseFocus(aDestWindow);
+ info("got focus on the window");
+
+ // Select the testTab
+ aDestWindow.gBrowser.selectedTab = testTab;
+
+ // Ensure that this tab has no history entries
+ let sessionHistoryCount = yield new Promise(resolve => {
+ SessionStore.getSessionHistory(gBrowser.selectedTab, function(sessionHistory) {
+ resolve(sessionHistory.entries.length);
+ });
+ });
+
+ ok(sessionHistoryCount < 2,
+ `The test tab has 1 or fewer history entries. sessionHistoryCount=${sessionHistoryCount}`);
+ // Ensure that this tab is on about:blank
+ is(testTab.linkedBrowser.currentURI.spec, "about:blank",
+ "The test tab is on about:blank");
+ // Ensure that this tab's document has no child nodes
+ yield ContentTask.spawn(testTab.linkedBrowser, null, function*() {
+ ok(!content.document.body.hasChildNodes(),
+ "The test tab has no child nodes");
+ });
+ ok(!testTab.hasAttribute("busy"),
+ "The test tab doesn't have the busy attribute");
+
+ // Wait for the Awesomebar popup to appear.
+ yield promiseAutocompleteResultPopup(testURL, aDestWindow);
+
+ info(`awesomebar popup appeared. aExpectSwitch: ${aExpectSwitch}`);
+ // Make sure the last match is selected.
+ let {controller, popup} = aDestWindow.gURLBar;
+ while (popup.selectedIndex < controller.matchCount - 1) {
+ info("handling key navigation for DOM_VK_DOWN key");
+ controller.handleKeyNavigation(KeyEvent.DOM_VK_DOWN);
+ }
+
+ let awaitTabSwitch;
+ if (aExpectSwitch) {
+ awaitTabSwitch = BrowserTestUtils.removeTab(testTab, {dontRemove: true})
+ }
+
+ // Execute the selected action.
+ controller.handleEnter(true);
+ info("sent Enter command to the controller");
+
+ if (aExpectSwitch) {
+ // If we expect a tab switch then the current tab
+ // will be closed and we switch to the other tab.
+ yield awaitTabSwitch;
+ } else {
+ // If we don't expect a tab switch then wait for the tab to load.
+ yield BrowserTestUtils.browserLoaded(testTab.linkedBrowser);
+ }
+}
diff --git a/browser/base/content/test/urlbar/browser_urlHighlight.js b/browser/base/content/test/urlbar/browser_urlHighlight.js
new file mode 100644
index 000000000..ba1537d91
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlHighlight.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function testVal(aExpected) {
+ gURLBar.value = aExpected.replace(/[<>]/g, "");
+
+ let selectionController = gURLBar.editor.selectionController;
+ let selection = selectionController.getSelection(selectionController.SELECTION_URLSECONDARY);
+ let value = gURLBar.editor.rootElement.textContent;
+ let result = "";
+ for (let i = 0; i < selection.rangeCount; i++) {
+ let range = selection.getRangeAt(i).toString();
+ let pos = value.indexOf(range);
+ result += value.substring(0, pos) + "<" + range + ">";
+ value = value.substring(pos + range.length);
+ }
+ result += value;
+ is(result, aExpected,
+ "Correct part of the urlbar contents is highlighted");
+}
+
+function test() {
+ const prefname = "browser.urlbar.formatting.enabled";
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(prefname);
+ URLBarSetURI();
+ });
+
+ Services.prefs.setBoolPref(prefname, true);
+
+ gURLBar.focus();
+
+ testVal("https://mozilla.org");
+
+ gBrowser.selectedBrowser.focus();
+
+ testVal("<https://>mozilla.org");
+ testVal("<https://>mözilla.org");
+ testVal("<https://>mozilla.imaginatory");
+
+ testVal("<https://www.>mozilla.org");
+ testVal("<https://sub.>mozilla.org");
+ testVal("<https://sub1.sub2.sub3.>mozilla.org");
+ testVal("<www.>mozilla.org");
+ testVal("<sub.>mozilla.org");
+ testVal("<sub1.sub2.sub3.>mozilla.org");
+ testVal("<mozilla.com.>mozilla.com");
+ testVal("<https://mozilla.com:mozilla.com@>mozilla.com");
+ testVal("<mozilla.com:mozilla.com@>mozilla.com");
+
+ testVal("<ftp.>mozilla.org");
+ testVal("<ftp://ftp.>mozilla.org");
+
+ testVal("<https://sub.>mozilla.org");
+ testVal("<https://sub1.sub2.sub3.>mozilla.org");
+ testVal("<https://user:pass@sub1.sub2.sub3.>mozilla.org");
+ testVal("<https://user:pass@>mozilla.org");
+ testVal("<user:pass@sub1.sub2.sub3.>mozilla.org");
+ testVal("<user:pass@>mozilla.org");
+
+ testVal("<https://>mozilla.org< >");
+ testVal("mozilla.org< >");
+
+ testVal("<https://>mozilla.org</file.ext>");
+ testVal("<https://>mozilla.org</sub/file.ext>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>");
+ testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>");
+ testVal("foo.bar<?q=test>");
+ testVal("foo.bar<#mozilla.org>");
+ testVal("foo.bar<?somewhere.mozilla.org>");
+ testVal("foo.bar<?@mozilla.org>");
+ testVal("foo.bar<#x@mozilla.org>");
+ testVal("foo.bar<#@x@mozilla.org>");
+ testVal("foo.bar<?x@mozilla.org>");
+ testVal("foo.bar<?@x@mozilla.org>");
+ testVal("<foo.bar@x@>mozilla.org");
+ testVal("<foo.bar@:baz@>mozilla.org");
+ testVal("<foo.bar:@baz@>mozilla.org");
+ testVal("<foo.bar@:ba:z@>mozilla.org");
+ testVal("<foo.:bar:@baz@>mozilla.org");
+
+ testVal("<https://sub.>mozilla.org<:666/file.ext>");
+ testVal("<sub.>mozilla.org<:666/file.ext>");
+ testVal("localhost<:666/file.ext>");
+
+ let IPs = ["192.168.1.1",
+ "[::]",
+ "[::1]",
+ "[1::]",
+ "[::]",
+ "[::1]",
+ "[1::]",
+ "[1:2:3:4:5:6:7::]",
+ "[::1:2:3:4:5:6:7]",
+ "[1:2:a:B:c:D:e:F]",
+ "[1::8]",
+ "[1:2::8]",
+ "[fe80::222:19ff:fe11:8c76]",
+ "[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]",
+ "[::192.168.1.1]",
+ "[1::0.0.0.0]",
+ "[1:2::255.255.255.255]",
+ "[1:2:3::255.255.255.255]",
+ "[1:2:3:4::255.255.255.255]",
+ "[1:2:3:4:5::255.255.255.255]",
+ "[1:2:3:4:5:6:255.255.255.255]"];
+ IPs.forEach(function (IP) {
+ testVal(IP);
+ testVal(IP + "</file.ext>");
+ testVal(IP + "<:666/file.ext>");
+ testVal("<https://>" + IP);
+ testVal("<https://>" + IP + "</file.ext>");
+ testVal("<https://user:pass@>" + IP + "<:666/file.ext>");
+ testVal("<user:pass@>" + IP + "<:666/file.ext>");
+ });
+
+ testVal("mailto:admin@mozilla.org");
+ testVal("gopher://mozilla.org/");
+ testVal("about:config");
+ testVal("jar:http://mozilla.org/example.jar!/");
+ testVal("view-source:http://mozilla.org/");
+ testVal("foo9://mozilla.org/");
+ testVal("foo+://mozilla.org/");
+ testVal("foo.://mozilla.org/");
+ testVal("foo-://mozilla.org/");
+
+ Services.prefs.setBoolPref(prefname, false);
+
+ testVal("https://mozilla.org");
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarAboutHomeLoading.js b/browser/base/content/test/urlbar/browser_urlbarAboutHomeLoading.js
new file mode 100644
index 000000000..792826eb1
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarAboutHomeLoading.js
@@ -0,0 +1,104 @@
+"use strict";
+
+const {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+
+/**
+ * Test what happens if loading a URL that should clear the
+ * location bar after a parent process URL.
+ */
+add_task(function* clearURLBarAfterParentProcessURL() {
+ let tab = yield new Promise(resolve => {
+ gBrowser.selectedTab = gBrowser.addTab("about:preferences");
+ let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ newTabBrowser.addEventListener("Initialized", function onInit() {
+ newTabBrowser.removeEventListener("Initialized", onInit, true);
+ resolve(gBrowser.selectedTab);
+ }, true);
+ });
+ document.getElementById("home-button").click();
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "The browser should have no recorded userTypedValue");
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Same as above, but open the tab without passing the URL immediately
+ * which changes behaviour in tabbrowser.xml.
+ */
+add_task(function* clearURLBarAfterParentProcessURLInExistingTab() {
+ let tab = yield new Promise(resolve => {
+ gBrowser.selectedTab = gBrowser.addTab();
+ let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ newTabBrowser.addEventListener("Initialized", function onInit() {
+ newTabBrowser.removeEventListener("Initialized", onInit, true);
+ resolve(gBrowser.selectedTab);
+ }, true);
+ newTabBrowser.loadURI("about:preferences");
+ });
+ document.getElementById("home-button").click();
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "The browser should have no recorded userTypedValue");
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Load about:home directly from an about:newtab page. Because it is an
+ * 'initial' page, we need to treat this specially if the user actually
+ * loads a page like this from the URL bar.
+ */
+add_task(function* clearURLBarAfterManuallyLoadingAboutHome() {
+ let promiseTabOpenedAndSwitchedTo = BrowserTestUtils.switchTab(gBrowser, () => {});
+ // This opens about:newtab:
+ BrowserOpenTab();
+ let tab = yield promiseTabOpenedAndSwitchedTo;
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+
+ gURLBar.value = "about:home";
+ gURLBar.select();
+ let aboutHomeLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, "about:home");
+ EventUtils.sendKey("return");
+ yield aboutHomeLoaded;
+
+ is(gURLBar.value, "", "URL bar should be empty");
+ is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null");
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Ensure we don't show 'about:home' in the URL bar temporarily in new tabs
+ * while we're switching remoteness (when the URL we're loading and the
+ * default content principal are different).
+ */
+add_task(function* dontTemporarilyShowAboutHome() {
+ yield SpecialPowers.pushPrefEnv({set: [["browser.startup.page", 1]]});
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow();
+ yield windowOpenedPromise;
+ let promiseTabSwitch = BrowserTestUtils.switchTab(win.gBrowser, () => {});
+ win.BrowserOpenTab();
+ yield promiseTabSwitch;
+ yield TabStateFlusher.flush(win.gBrowser.selectedBrowser);
+ yield BrowserTestUtils.closeWindow(win);
+ ok(SessionStore.getClosedWindowCount(), "Should have a closed window");
+
+ windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ win = SessionStore.undoCloseWindow(0);
+ yield windowOpenedPromise;
+ let wpl = {
+ onLocationChange(wpl, request, location, flags) {
+ is(win.gURLBar.value, "", "URL bar value should stay empty.");
+ },
+ };
+ win.gBrowser.addProgressListener(wpl);
+ let otherTab = win.gBrowser.selectedTab.previousSibling;
+ let tabLoaded = BrowserTestUtils.browserLoaded(otherTab.linkedBrowser, false, "about:home");
+ yield BrowserTestUtils.switchTab(win.gBrowser, otherTab);
+ yield tabLoaded;
+ win.gBrowser.removeProgressListener(wpl);
+ is(win.gURLBar.value, "", "URL bar value should be empty.");
+
+ yield BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js b/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
new file mode 100644
index 000000000..8101c101d
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
@@ -0,0 +1,49 @@
+// This test ensures that autoFilled values are not trimmed, unless the user
+// selects from the autocomplete popup.
+
+add_task(function* setup() {
+ const PREF_TRIMURL = "browser.urlbar.trimURLs";
+ const PREF_AUTOFILL = "browser.urlbar.autoFill";
+
+ registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref(PREF_TRIMURL);
+ Services.prefs.clearUserPref(PREF_AUTOFILL);
+ yield PlacesTestUtils.clearHistory();
+ gURLBar.handleRevert();
+ });
+ Services.prefs.setBoolPref(PREF_TRIMURL, true);
+ Services.prefs.setBoolPref(PREF_AUTOFILL, true);
+
+ // Adding a tab would hit switch-to-tab, so it's safer to just add a visit.
+ yield PlacesTestUtils.addVisits({
+ uri: "http://www.autofilltrimurl.com/whatever",
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ });
+});
+
+function* promiseSearch(searchtext) {
+ gURLBar.focus();
+ gURLBar.inputField.value = searchtext.substr(0, searchtext.length -1);
+ EventUtils.synthesizeKey(searchtext.substr(-1, 1), {});
+ yield promiseSearchComplete();
+}
+
+add_task(function* () {
+ yield promiseSearch("http://");
+ is(gURLBar.inputField.value, "http://", "Autofilled value is as expected");
+});
+
+add_task(function* () {
+ yield promiseSearch("http://au");
+ is(gURLBar.inputField.value, "http://autofilltrimurl.com/", "Autofilled value is as expected");
+});
+
+add_task(function* () {
+ yield promiseSearch("http://www.autofilltrimurl.com");
+ is(gURLBar.inputField.value, "http://www.autofilltrimurl.com/", "Autofilled value is as expected");
+
+ // Now ensure selecting from the popup correctly trims.
+ is(gURLBar.controller.matchCount, 2, "Found the expected number of matches");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is(gURLBar.inputField.value, "www.autofilltrimurl.com/whatever", "trim was applied correctly");
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbarCopying.js b/browser/base/content/test/urlbar/browser_urlbarCopying.js
new file mode 100644
index 000000000..8d5562b61
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarCopying.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const trimPref = "browser.urlbar.trimURLs";
+const phishyUserPassPref = "network.http.phishy-userpass-length";
+
+function toUnicode(input) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+
+ return converter.ConvertToUnicode(input);
+}
+
+function test() {
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ Services.prefs.clearUserPref(trimPref);
+ Services.prefs.clearUserPref(phishyUserPassPref);
+ URLBarSetURI();
+ });
+
+ Services.prefs.setBoolPref(trimPref, true);
+ Services.prefs.setIntPref(phishyUserPassPref, 32); // avoid prompting about phishing
+
+ waitForExplicitFinish();
+
+ nextTest();
+}
+
+var tests = [
+ // pageproxystate="invalid"
+ {
+ setURL: "http://example.com/",
+ expectedURL: "example.com",
+ copyExpected: "example.com"
+ },
+ {
+ copyVal: "<e>xample.com",
+ copyExpected: "e"
+ },
+
+ // pageproxystate="valid" from this point on (due to the load)
+ {
+ loadURL: "http://example.com/",
+ expectedURL: "example.com",
+ copyExpected: "http://example.com/"
+ },
+ {
+ copyVal: "<example.co>m",
+ copyExpected: "example.co"
+ },
+ {
+ copyVal: "e<x>ample.com",
+ copyExpected: "x"
+ },
+ {
+ copyVal: "<e>xample.com",
+ copyExpected: "e"
+ },
+
+ {
+ loadURL: "http://example.com/foo",
+ expectedURL: "example.com/foo",
+ copyExpected: "http://example.com/foo"
+ },
+ {
+ copyVal: "<example.com>/foo",
+ copyExpected: "http://example.com"
+ },
+ {
+ copyVal: "<example>.com/foo",
+ copyExpected: "example"
+ },
+
+ // Test that userPass is stripped out
+ {
+ loadURL: "http://user:pass@mochi.test:8888/browser/browser/base/content/test/urlbar/authenticate.sjs?user=user&pass=pass",
+ expectedURL: "mochi.test:8888/browser/browser/base/content/test/urlbar/authenticate.sjs?user=user&pass=pass",
+ copyExpected: "http://mochi.test:8888/browser/browser/base/content/test/urlbar/authenticate.sjs?user=user&pass=pass"
+ },
+
+ // Test escaping
+ {
+ loadURL: "http://example.com/()%28%29%C3%A9",
+ expectedURL: "example.com/()()\xe9",
+ copyExpected: "http://example.com/()%28%29%C3%A9"
+ },
+ {
+ copyVal: "<example.com/(>)()\xe9",
+ copyExpected: "http://example.com/("
+ },
+ {
+ copyVal: "e<xample.com/(>)()\xe9",
+ copyExpected: "xample.com/("
+ },
+
+ {
+ loadURL: "http://example.com/%C3%A9%C3%A9",
+ expectedURL: "example.com/\xe9\xe9",
+ copyExpected: "http://example.com/%C3%A9%C3%A9"
+ },
+ {
+ copyVal: "e<xample.com/\xe9>\xe9",
+ copyExpected: "xample.com/\xe9"
+ },
+ {
+ copyVal: "<example.com/\xe9>\xe9",
+ copyExpected: "http://example.com/\xe9"
+ },
+
+ {
+ loadURL: "http://example.com/?%C3%B7%C3%B7",
+ expectedURL: "example.com/?\xf7\xf7",
+ copyExpected: "http://example.com/?%C3%B7%C3%B7"
+ },
+ {
+ copyVal: "e<xample.com/?\xf7>\xf7",
+ copyExpected: "xample.com/?\xf7"
+ },
+ {
+ copyVal: "<example.com/?\xf7>\xf7",
+ copyExpected: "http://example.com/?\xf7"
+ },
+ {
+ loadURL: "http://example.com/a%20test",
+ expectedURL: "example.com/a test",
+ copyExpected: "http://example.com/a%20test"
+ },
+ {
+ loadURL: "http://example.com/a%E3%80%80test",
+ expectedURL: toUnicode("example.com/a test"),
+ copyExpected: "http://example.com/a%E3%80%80test"
+ },
+ {
+ loadURL: "http://example.com/a%20%C2%A0test",
+ expectedURL: "example.com/a%20%C2%A0test",
+ copyExpected: "http://example.com/a%20%C2%A0test"
+ },
+ {
+ loadURL: "http://example.com/%20%20%20",
+ expectedURL: "example.com/%20%20%20",
+ copyExpected: "http://example.com/%20%20%20"
+ },
+ {
+ loadURL: "http://example.com/%E3%80%80%E3%80%80",
+ expectedURL: "example.com/%E3%80%80%E3%80%80",
+ copyExpected: "http://example.com/%E3%80%80%E3%80%80"
+ },
+
+ // data: and javsacript: URIs shouldn't be encoded
+ {
+ loadURL: "javascript:('%C3%A9%20%25%50')",
+ expectedURL: "javascript:('%C3%A9 %25P')",
+ copyExpected: "javascript:('%C3%A9 %25P')"
+ },
+ {
+ copyVal: "<javascript:(>'%C3%A9 %25P')",
+ copyExpected: "javascript:("
+ },
+
+ {
+ loadURL: "data:text/html,(%C3%A9%20%25%50)",
+ expectedURL: "data:text/html,(%C3%A9 %25P)",
+ copyExpected: "data:text/html,(%C3%A9 %25P)",
+ },
+ {
+ copyVal: "<data:text/html,(>%C3%A9 %25P)",
+ copyExpected: "data:text/html,("
+ },
+ {
+ copyVal: "<data:text/html,(%C3%A9 %25P>)",
+ copyExpected: "data:text/html,(%C3%A9 %25P",
+ }
+];
+
+function nextTest() {
+ let test = tests.shift();
+ if (tests.length == 0)
+ runTest(test, finish);
+ else
+ runTest(test, nextTest);
+}
+
+function runTest(test, cb) {
+ function doCheck() {
+ if (test.setURL || test.loadURL) {
+ gURLBar.valueIsTyped = !!test.setURL;
+ is(gURLBar.textValue, test.expectedURL, "url bar value set");
+ }
+
+ testCopy(test.copyVal, test.copyExpected, cb);
+ }
+
+ if (test.loadURL) {
+ loadURL(test.loadURL, doCheck);
+ } else {
+ if (test.setURL)
+ gURLBar.value = test.setURL;
+ doCheck();
+ }
+}
+
+function testCopy(copyVal, targetValue, cb) {
+ info("Expecting copy of: " + targetValue);
+ waitForClipboard(targetValue, function () {
+ gURLBar.focus();
+ if (copyVal) {
+ let startBracket = copyVal.indexOf("<");
+ let endBracket = copyVal.indexOf(">");
+ if (startBracket == -1 || endBracket == -1 ||
+ startBracket > endBracket ||
+ copyVal.replace("<", "").replace(">", "") != gURLBar.textValue) {
+ ok(false, "invalid copyVal: " + copyVal);
+ }
+ gURLBar.selectionStart = startBracket;
+ gURLBar.selectionEnd = endBracket - 1;
+ } else {
+ gURLBar.select();
+ }
+
+ goDoCommand("cmd_copy");
+ }, cb, cb);
+}
+
+function loadURL(aURL, aCB) {
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, aURL);
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, aURL).then(aCB);
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarDecode.js b/browser/base/content/test/urlbar/browser_urlbarDecode.js
new file mode 100644
index 000000000..6a2c421ef
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarDecode.js
@@ -0,0 +1,97 @@
+"use strict";
+
+// This test makes sure (1) you can't break the urlbar by typing particular JSON
+// or JS fragments into it, (2) urlbar.textValue shows URLs unescaped, and (3)
+// the urlbar also shows the URLs embedded in action URIs unescaped. See bug
+// 1233672.
+
+add_task(function* injectJSON() {
+ let inputStrs = [
+ 'http://example.com/ ", "url": "bar',
+ 'http://example.com/\\',
+ 'http://example.com/"',
+ 'http://example.com/","url":"evil.com',
+ 'http://mozilla.org/\\u0020',
+ 'http://www.mozilla.org/","url":1e6,"some-key":"foo',
+ 'http://www.mozilla.org/","url":null,"some-key":"foo',
+ 'http://www.mozilla.org/","url":["foo","bar"],"some-key":"foo',
+ ];
+ for (let inputStr of inputStrs) {
+ yield checkInput(inputStr);
+ }
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+add_task(function losslessDecode() {
+ let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
+ let url = "http://" + urlNoScheme;
+ gURLBar.textValue = url;
+ // Since this is directly setting textValue, it is expected to be trimmed.
+ Assert.equal(gURLBar.inputField.value, urlNoScheme,
+ "The string displayed in the textbox should not be escaped");
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+add_task(function* actionURILosslessDecode() {
+ let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
+ let url = "http://" + urlNoScheme;
+ yield promiseAutocompleteResultPopup(url);
+
+ // At this point the heuristic result is selected but the urlbar's value is
+ // simply `url`. Key down and back around until the heuristic result is
+ // selected again, and at that point the urlbar's value should be a visiturl
+ // moz-action.
+
+ do {
+ gURLBar.controller.handleKeyNavigation(KeyEvent.DOM_VK_DOWN);
+ } while (gURLBar.popup.selectedIndex != 0);
+
+ let [, type, ] = gURLBar.value.match(/^moz-action:([^,]+),(.*)$/);
+ Assert.equal(type, "visiturl",
+ "visiturl action URI should be in the urlbar");
+
+ Assert.equal(gURLBar.inputField.value, urlNoScheme,
+ "The string displayed in the textbox should not be escaped");
+
+ gURLBar.value = "";
+ gURLBar.handleRevert();
+ gURLBar.blur();
+});
+
+function* checkInput(inputStr) {
+ yield promiseAutocompleteResultPopup(inputStr);
+
+ let item = gURLBar.popup.richlistbox.firstChild;
+ Assert.ok(item, "Should have a result");
+
+ // visiturl matches have their param.urls fixed up.
+ let fixupInfo = Services.uriFixup.getFixupURIInfo(inputStr,
+ Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
+ );
+ let expectedVisitURL = fixupInfo.fixedURI.spec;
+
+ let type = "visiturl";
+ let params = {
+ url: expectedVisitURL,
+ input: inputStr,
+ };
+ for (let key in params) {
+ params[key] = encodeURIComponent(params[key]);
+ }
+ let expectedURL = "moz-action:" + type + "," + JSON.stringify(params);
+ Assert.equal(item.getAttribute("url"), expectedURL, "url");
+
+ Assert.equal(item.getAttribute("title"), inputStr.replace("\\", "/"), "title");
+ Assert.equal(item.getAttribute("text"), inputStr, "text");
+
+ let itemType = item.getAttribute("type");
+ Assert.equal(itemType, "visiturl");
+
+ Assert.equal(item._titleText.textContent, inputStr.replace("\\", "/"), "Visible title");
+ Assert.equal(item._actionText.textContent, "Visit", "Visible action");
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarDelete.js b/browser/base/content/test/urlbar/browser_urlbarDelete.js
new file mode 100644
index 000000000..d4eb6c856
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarDelete.js
@@ -0,0 +1,39 @@
+add_task(function*() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://bug1105244.example.com/",
+ title: "test" });
+
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.bookmarks.remove(bm);
+ });
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, testDelete);
+});
+
+function sendHome() {
+ // unclear why VK_HOME doesn't work on Mac, but it doesn't...
+ if (Services.appinfo.OS == "Darwin") {
+ EventUtils.synthesizeKey("VK_LEFT", { altKey: true });
+ } else {
+ EventUtils.synthesizeKey("VK_HOME", {});
+ }
+}
+
+function sendDelete() {
+ EventUtils.synthesizeKey("VK_DELETE", {});
+}
+
+function* testDelete() {
+ yield promiseAutocompleteResultPopup("bug1105244");
+
+ // move to the start.
+ sendHome();
+ // delete the first few chars - each delete should operate on the input field.
+ sendDelete();
+ Assert.equal(gURLBar.inputField.value, "ug1105244");
+
+ yield promisePopupShown(gURLBar.popup);
+
+ sendDelete();
+ Assert.equal(gURLBar.inputField.value, "g1105244");
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarEnter.js b/browser/base/content/test/urlbar/browser_urlbarEnter.js
new file mode 100644
index 000000000..32cbaf2be
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarEnter.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_VALUE = "example.com/\xF7?\xF7";
+const START_VALUE = "example.com/%C3%B7?%C3%B7";
+
+add_task(function* () {
+ info("Simple return keypress");
+ let tab = gBrowser.selectedTab = gBrowser.addTab(START_VALUE);
+
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ // Check url bar and selected tab.
+ is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
+ is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
+
+ // Cleanup.
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(function* () {
+ info("Alt+Return keypress");
+ // due to bug 691608, we must wait for the load event, else isTabEmpty() will
+ // return true on e10s for this tab, so it will be reused even with altKey.
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
+
+ let tabOpenPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {altKey: true});
+
+ // wait for the new tab to appear.
+ yield tabOpenPromise;
+
+ // Check url bar and selected tab.
+ is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
+ isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
+
+ // Cleanup.
+ yield BrowserTestUtils.removeTab(tab);
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbarEnterAfterMouseOver.js b/browser/base/content/test/urlbar/browser_urlbarEnterAfterMouseOver.js
new file mode 100644
index 000000000..22e336f91
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarEnterAfterMouseOver.js
@@ -0,0 +1,69 @@
+function repeat(limit, func) {
+ for (let i = 0; i < limit; i++) {
+ func(i);
+ }
+}
+
+function* promiseAutoComplete(inputText) {
+ gURLBar.focus();
+ gURLBar.value = inputText.slice(0, -1);
+ EventUtils.synthesizeKey(inputText.slice(-1), {});
+ yield promiseSearchComplete();
+}
+
+function is_selected(index) {
+ is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
+}
+
+let gMaxResults;
+
+add_task(function*() {
+ registerCleanupFunction(function* () {
+ yield PlacesTestUtils.clearHistory();
+ });
+
+ yield PlacesTestUtils.clearHistory();
+
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ let visits = [];
+ repeat(gMaxResults, i => {
+ visits.push({
+ uri: makeURI("http://example.com/autocomplete/?" + i),
+ });
+ });
+ yield PlacesTestUtils.addVisits(visits);
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield promiseAutoComplete("http://example.com/autocomplete/");
+
+ let popup = gURLBar.popup;
+ let results = popup.richlistbox.children;
+ is(results.length, gMaxResults,
+ "Should get gMaxResults=" + gMaxResults + " results");
+
+ let initiallySelected = gURLBar.popup.richlistbox.selectedIndex;
+
+ info("Key Down to select the next item");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ is_selected(initiallySelected + 1);
+ let expectedURL = gURLBar.controller.getFinalCompleteValueAt(initiallySelected + 1);
+
+ is(gURLBar.value, gURLBar.controller.getValueAt(initiallySelected + 1),
+ "Value in the URL bar should be updated by keyboard selection");
+
+ // Verify that what we're about to do changes the selectedIndex:
+ isnot(initiallySelected + 1, 3, "Shouldn't be changing the selectedIndex to the same index we keyboard-selected.");
+
+ // Would love to use a synthetic mousemove event here, but that doesn't seem to do anything.
+ // EventUtils.synthesizeMouseAtCenter(results[3], {type: "mousemove"});
+ gURLBar.popup.richlistbox.selectedIndex = 3;
+ is_selected(3);
+
+ let autocompletePopupHidden = promisePopupHidden(gURLBar.popup);
+ let openedExpectedPage = waitForDocLoadAndStopIt(expectedURL);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield Promise.all([autocompletePopupHidden, openedExpectedPage]);
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbarFocusedCmdK.js b/browser/base/content/test/urlbar/browser_urlbarFocusedCmdK.js
new file mode 100644
index 000000000..8c9e2c9f2
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarFocusedCmdK.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function*() {
+ // Remove the search bar from toolbar
+ CustomizableUI.removeWidgetFromArea("search-container");
+
+ // Test that Ctrl/Cmd + K will focus the url bar
+ let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
+ EventUtils.synthesizeKey("k", { accelKey: true });
+ yield focusPromise;
+ Assert.equal(document.activeElement, gURLBar.inputField, "URL Bar should be focused");
+
+ // Reset changes made to toolbar
+ CustomizableUI.reset();
+});
+
diff --git a/browser/base/content/test/urlbar/browser_urlbarHashChangeProxyState.js b/browser/base/content/test/urlbar/browser_urlbarHashChangeProxyState.js
new file mode 100644
index 000000000..152106dad
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarHashChangeProxyState.js
@@ -0,0 +1,111 @@
+"use strict";
+
+/**
+ * Check that navigating through both the URL bar and using in-page hash- or ref-
+ * based links and back or forward navigation updates the URL bar and identity block correctly.
+ */
+add_task(function* () {
+ let baseURL = "https://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+ let url = baseURL + "#foo";
+ yield BrowserTestUtils.withNewTab({ gBrowser, url }, function*(browser) {
+ let identityBox = document.getElementById("identity-box");
+ let expectedURL = url;
+
+ let verifyURLBarState = testType => {
+ is(gURLBar.textValue, expectedURL, "URL bar visible value should be correct " + testType);
+ is(gURLBar.value, expectedURL, "URL bar value should be correct " + testType);
+ ok(identityBox.classList.contains("verifiedDomain"), "Identity box should know we're doing SSL " + testType);
+ is(gURLBar.getAttribute("pageproxystate"), "valid", "URL bar is in valid page proxy state");
+ };
+
+ verifyURLBarState("at the beginning");
+
+ let locationChangePromise;
+ let resolveLocationChangePromise;
+ let expectURL = url => {
+ expectedURL = url;
+ locationChangePromise = new Promise(r => resolveLocationChangePromise = r);
+ };
+ let wpl = {
+ onLocationChange(wpl, request, location, flags) {
+ is(location.spec, expectedURL, "Got the expected URL");
+ resolveLocationChangePromise();
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ expectURL(baseURL + "#foo");
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ yield locationChangePromise;
+ verifyURLBarState("after hitting enter on the same URL a second time");
+
+ expectURL(baseURL + "#bar");
+ gURLBar.value = expectedURL;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ yield locationChangePromise;
+ verifyURLBarState("after a URL bar hash navigation");
+
+ expectURL(baseURL + "#foo");
+ yield ContentTask.spawn(browser, null, function() {
+ let a = content.document.createElement("a");
+ a.href = "#foo";
+ a.textContent = "Foo Link";
+ content.document.body.appendChild(a);
+ a.click();
+ });
+
+ yield locationChangePromise;
+ verifyURLBarState("after a page link hash navigation");
+
+ expectURL(baseURL + "#bar");
+ gBrowser.goBack();
+
+ yield locationChangePromise;
+ verifyURLBarState("after going back");
+
+ expectURL(baseURL + "#foo");
+ gBrowser.goForward();
+
+ yield locationChangePromise;
+ verifyURLBarState("after going forward");
+
+ expectURL(baseURL + "#foo");
+ gURLBar.select();
+ EventUtils.sendKey("return");
+
+ yield locationChangePromise;
+ verifyURLBarState("after hitting enter on the same URL");
+
+ gBrowser.removeProgressListener(wpl);
+ });
+});
+
+/**
+ * Check that initial secure loads that swap remoteness
+ * get the correct page icon when finished.
+ */
+add_task(function* () {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+ // NB: CPOW usage because new tab pages can be preloaded, in which case no
+ // load events fire.
+ yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
+ let url = "https://example.org/browser/browser/base/content/test/urlbar/dummy_page.html#foo";
+ gURLBar.value = url;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ is(gURLBar.textValue, url, "URL bar visible value should be correct when the page loads from about:newtab");
+ is(gURLBar.value, url, "URL bar value should be correct when the page loads from about:newtab");
+ let identityBox = document.getElementById("identity-box");
+ ok(identityBox.classList.contains("verifiedDomain"),
+ "Identity box should know we're doing SSL when the page loads from about:newtab");
+ is(gURLBar.getAttribute("pageproxystate"), "valid",
+ "URL bar is in valid page proxy state when SSL page with hash loads from about:newtab");
+ yield BrowserTestUtils.removeTab(tab);
+});
+
diff --git a/browser/base/content/test/urlbar/browser_urlbarKeepStateAcrossTabSwitches.js b/browser/base/content/test/urlbar/browser_urlbarKeepStateAcrossTabSwitches.js
new file mode 100644
index 000000000..9c8996059
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarKeepStateAcrossTabSwitches.js
@@ -0,0 +1,49 @@
+"use strict";
+
+/**
+ * Verify user typed text remains in the URL bar when tab switching, even when
+ * loads fail.
+ */
+add_task(function* () {
+ let input = "i-definitely-dont-exist.example.com";
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+ // NB: CPOW usage because new tab pages can be preloaded, in which case no
+ // load events fire.
+ yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+ gURLBar.value = input;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ yield errorPageLoaded;
+ is(gURLBar.textValue, input, "Text is still in URL bar");
+ yield BrowserTestUtils.switchTab(gBrowser, tab.previousSibling);
+ yield BrowserTestUtils.switchTab(gBrowser, tab);
+ is(gURLBar.textValue, input, "Text is still in URL bar after tab switch");
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Invalid URIs fail differently (that is, immediately, in the loadURI call)
+ * if keyword searches are turned off. Test that this works, too.
+ */
+add_task(function* () {
+ let input = "To be or not to be-that is the question";
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({set: [["keyword.enabled", false]]}, resolve));
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+ // NB: CPOW usage because new tab pages can be preloaded, in which case no
+ // load events fire.
+ yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
+ let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
+ gURLBar.value = input;
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ yield errorPageLoaded;
+ is(gURLBar.textValue, input, "Text is still in URL bar");
+ is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
+ yield BrowserTestUtils.switchTab(gBrowser, tab.previousSibling);
+ yield BrowserTestUtils.switchTab(gBrowser, tab);
+ is(gURLBar.textValue, input, "Text is still in URL bar after tab switch");
+ is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
+ yield BrowserTestUtils.removeTab(tab);
+});
+
diff --git a/browser/base/content/test/urlbar/browser_urlbarOneOffs.js b/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
new file mode 100644
index 000000000..1f58b8edd
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
@@ -0,0 +1,232 @@
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+let gMaxResults;
+
+add_task(function* init() {
+ Services.prefs.setBoolPref("browser.urlbar.oneOffSearches", true);
+ gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+
+ // Add a search suggestion engine and move it to the front so that it appears
+ // as the first one-off.
+ let engine = yield promiseNewSearchEngine(TEST_ENGINE_BASENAME);
+ Services.search.moveEngine(engine, 0);
+
+ registerCleanupFunction(function* () {
+ yield hidePopup();
+ yield PlacesTestUtils.clearHistory();
+ });
+
+ yield PlacesTestUtils.clearHistory();
+
+ let visits = [];
+ for (let i = 0; i < gMaxResults; i++) {
+ visits.push({
+ uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i),
+ // TYPED so that the visit shows up when the urlbar's drop-down arrow is
+ // pressed.
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+});
+
+// Keys up and down through the history panel, i.e., the panel that's shown when
+// there's no text in the textbox.
+add_task(function* history() {
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ yield promisePopupShown(gURLBar.popup);
+
+ assertState(-1, -1, "");
+
+ // Key down through each result.
+ for (let i = 0; i < gMaxResults; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(i, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
+ }
+
+ // Key down through each one-off.
+ let numButtons =
+ gURLBar.popup.oneOffSearchButtons.getSelectableButtons(true).length;
+ for (let i = 0; i < numButtons; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(-1, i, "");
+ }
+
+ // Key down once more. Nothing should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(-1, -1, "");
+
+ // Once more. The first result should be selected.
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(0, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1));
+
+ // Now key up. Nothing should be selected again.
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(-1, -1, "");
+
+ // Key up through each one-off.
+ for (let i = numButtons - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(-1, i, "");
+ }
+
+ // Key up through each result.
+ for (let i = gMaxResults - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(i, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
+ }
+
+ // Key up once more. Nothing should be selected.
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(-1, -1, "");
+
+ yield hidePopup();
+});
+
+// Keys up and down through the non-history panel, i.e., the panel that's shown
+// when you type something in the textbox.
+add_task(function* typedValue() {
+ // Use a typed value that returns the visits added above but that doesn't
+ // trigger autofill since that would complicate the test.
+ let typedValue = "browser_urlbarOneOffs";
+ yield promiseAutocompleteResultPopup(typedValue, window, true);
+
+ assertState(0, -1, typedValue);
+
+ // Key down through each result. The first result is already selected, which
+ // is why gMaxResults - 1 is the correct number of times to do this.
+ for (let i = 0; i < gMaxResults - 1; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ // i starts at zero so that the textValue passed to assertState is correct.
+ // But that means that i + 1 is the expected selected index, since initially
+ // (when this loop starts) the first result is selected.
+ assertState(i + 1, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
+ }
+
+ // Key down through each one-off.
+ let numButtons =
+ gURLBar.popup.oneOffSearchButtons.getSelectableButtons(true).length;
+ for (let i = 0; i < numButtons; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(-1, i, typedValue);
+ }
+
+ // Key down once more. The selection should wrap around to the first result.
+ EventUtils.synthesizeKey("VK_DOWN", {})
+ assertState(0, -1, typedValue);
+
+ // Now key up. The selection should wrap back around to the one-offs. Key
+ // up through all the one-offs.
+ for (let i = numButtons - 1; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(-1, i, typedValue);
+ }
+
+ // Key up through each non-heuristic result.
+ for (let i = gMaxResults - 2; i >= 0; i--) {
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(i + 1, -1,
+ "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1));
+ }
+
+ // Key up once more. The heuristic result should be selected.
+ EventUtils.synthesizeKey("VK_UP", {})
+ assertState(0, -1, typedValue);
+
+ yield hidePopup();
+});
+
+// Checks that "Search with Current Search Engine" items are updated to "Search
+// with One-Off Engine" when a one-off is selected.
+add_task(function* searchWith() {
+ let typedValue = "foo";
+ yield promiseAutocompleteResultPopup(typedValue);
+
+ assertState(0, -1, typedValue);
+
+ let item = gURLBar.popup.richlistbox.firstChild;
+ Assert.equal(item._actionText.textContent,
+ "Search with " + Services.search.currentEngine.name,
+ "Sanity check: first result's action text");
+
+ // Alt+Down to the first one-off. Now the first result and the first one-off
+ // should both be selected.
+ EventUtils.synthesizeKey("VK_DOWN", { altKey: true })
+ assertState(0, 0, typedValue);
+
+ let engineName = gURLBar.popup.oneOffSearchButtons.selectedButton.engine.name;
+ Assert.notEqual(engineName, Services.search.currentEngine.name,
+ "Sanity check: First one-off engine should not be " +
+ "the current engine");
+ Assert.equal(item._actionText.textContent,
+ "Search with " + engineName,
+ "First result's action text should be updated");
+
+ yield hidePopup();
+});
+
+// Clicks a one-off.
+add_task(function* oneOffClick() {
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+ yield promiseAutocompleteResultPopup(typedValue);
+
+ assertState(0, -1, typedValue);
+
+ let oneOffs = gURLBar.popup.oneOffSearchButtons.getSelectableButtons(true);
+ let resultsPromise =
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false,
+ "http://mochi.test:8888/");
+ EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
+ yield resultsPromise;
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
+
+// Presses the Return key when a one-off is selected.
+add_task(function* oneOffReturn() {
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ // We are explicitly using something that looks like a url, to make the test
+ // stricter. Even if it looks like a url, we should search.
+ let typedValue = "foo.bar";
+ yield promiseAutocompleteResultPopup(typedValue, window, true);
+
+ assertState(0, -1, typedValue);
+
+ // Alt+Down to select the first one-off.
+ EventUtils.synthesizeKey("VK_DOWN", { altKey: true })
+ assertState(0, 0, typedValue);
+
+ let resultsPromise =
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false,
+ "http://mochi.test:8888/");
+ EventUtils.synthesizeKey("VK_RETURN", {})
+ yield resultsPromise;
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
+
+
+function assertState(result, oneOff, textValue = undefined) {
+ Assert.equal(gURLBar.popup.selectedIndex, result,
+ "Expected result should be selected");
+ Assert.equal(gURLBar.popup.oneOffSearchButtons.selectedButtonIndex, oneOff,
+ "Expected one-off should be selected");
+ if (textValue !== undefined) {
+ Assert.equal(gURLBar.textValue, textValue, "Expected textValue");
+ }
+}
+
+function* hidePopup() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield promisePopupHidden(gURLBar.popup);
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarPrivateBrowsingWindowChange.js b/browser/base/content/test/urlbar/browser_urlbarPrivateBrowsingWindowChange.js
new file mode 100644
index 000000000..5db0f0ea6
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarPrivateBrowsingWindowChange.js
@@ -0,0 +1,41 @@
+"use strict";
+
+/**
+ * Test that when opening a private browsing window and typing in it before about:privatebrowsing
+ * loads, we don't clear the URL bar.
+ */
+add_task(function*() {
+ let urlbarTestValue = "Mary had a little lamb";
+ let win = OpenBrowserWindow({private: true});
+ yield BrowserTestUtils.waitForEvent(win, "load");
+ let urlbar = win.document.getElementById("urlbar");
+ urlbar.value = urlbarTestValue;
+ // Need this so the autocomplete controller attaches:
+ let focusEv = new FocusEvent("focus", {});
+ urlbar.dispatchEvent(focusEv);
+ // And so we know input happened:
+ let inputEv = new InputEvent("input", {data: "", view: win, bubbles: true});
+ urlbar.onInput(inputEv);
+ // Check it worked:
+ is(urlbar.value, urlbarTestValue, "URL bar value should be there");
+ is(win.gBrowser.selectedBrowser.userTypedValue, urlbarTestValue, "browser object should know the url bar value");
+
+ let continueTest;
+ let continuePromise = new Promise(resolve => continueTest = resolve);
+ let wpl = {
+ onLocationChange(aWebProgress, aRequest, aLocation) {
+ if (aLocation && aLocation.spec == "about:privatebrowsing") {
+ continueTest();
+ }
+ },
+ };
+ win.gBrowser.addProgressListener(wpl);
+
+ yield continuePromise;
+ is(urlbar.value, urlbarTestValue,
+ "URL bar value should be the same once about:privatebrowsing has loaded");
+ is(win.gBrowser.selectedBrowser.userTypedValue, urlbarTestValue,
+ "browser object should still know url bar value once about:privatebrowsing has loaded");
+ win.gBrowser.removeProgressListener(wpl);
+ yield BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbarRaceWithTabs.js b/browser/base/content/test/urlbar/browser_urlbarRaceWithTabs.js
new file mode 100644
index 000000000..d66514c5a
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarRaceWithTabs.js
@@ -0,0 +1,57 @@
+const kURL = "http://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
+
+function* addBookmark(bookmark) {
+ if (bookmark.keyword) {
+ yield PlacesUtils.keywords.insert({
+ keyword: bookmark.keyword,
+ url: bookmark.url,
+ });
+ }
+
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: bookmark.url,
+ title: bookmark.title,
+ });
+
+ registerCleanupFunction(function* () {
+ yield PlacesUtils.bookmarks.remove(bm);
+ if (bookmark.keyword) {
+ yield PlacesUtils.keywords.remove(bookmark.keyword);
+ }
+ });
+}
+
+/**
+ * Check that if the user hits enter and ctrl-t at the same time, we open the URL in the right tab.
+ */
+add_task(function* hitEnterLoadInRightTab() {
+ info("Opening new tab");
+ let oldTabCreatedPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ BrowserOpenTab();
+ let oldTab = (yield oldTabCreatedPromise).target;
+ let oldTabLoadedPromise = BrowserTestUtils.browserLoaded(oldTab.linkedBrowser, false, kURL);
+ oldTabLoadedPromise.then(() => info("Old tab loaded"));
+ let newTabCreatedPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+
+ info("Creating bookmark and keyword");
+ yield addBookmark({title: "Test for keyword bookmark and URL", url: kURL, keyword: "urlbarkeyword"});
+ info("Filling URL bar, sending <return> and opening a tab");
+ gURLBar.value = "urlbarkeyword";
+ gURLBar.select();
+ EventUtils.sendKey("return");
+ BrowserOpenTab();
+ info("Waiting for new tab");
+ let newTab = (yield newTabCreatedPromise).target;
+ info("Created new tab; waiting for either tab to load");
+ let newTabLoadedPromise = BrowserTestUtils.browserLoaded(newTab.linkedBrowser, false, kURL);
+ newTabLoadedPromise.then(() => info("New tab loaded"));
+ yield Promise.race([newTabLoadedPromise, oldTabLoadedPromise]);
+ is(newTab.linkedBrowser.currentURI.spec, "about:newtab", "New tab still has about:newtab");
+ is(oldTab.linkedBrowser.currentURI.spec, kURL, "Old tab loaded URL");
+ info("Closing new tab");
+ yield BrowserTestUtils.removeTab(newTab);
+ info("Closing old tab");
+ yield BrowserTestUtils.removeTab(oldTab);
+ info("Finished");
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbarRevert.js b/browser/base/content/test/urlbar/browser_urlbarRevert.js
new file mode 100644
index 000000000..0ce3c8fac
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarRevert.js
@@ -0,0 +1,37 @@
+var tab = null;
+
+function test() {
+ waitForExplicitFinish();
+
+ let pageLoaded = {
+ onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ gBrowser.removeProgressListener(this);
+ executeSoon(checkURLBarRevert);
+ }
+ }
+ }
+
+ gBrowser.addProgressListener(pageLoaded);
+ tab = gBrowser.addTab("http://example.com");
+ gBrowser.selectedTab = tab;
+}
+
+function checkURLBarRevert() {
+ let originalValue = gURLBar.value;
+
+ gBrowser.userTypedValue = "foobar";
+ gBrowser.selectedTab = gBrowser.tabs[0];
+ gBrowser.selectedTab = tab;
+ is(gURLBar.value, "foobar", "location bar displays typed value");
+
+ gURLBar.focus();
+
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+
+ is(gURLBar.value, originalValue, "ESC reverted the location bar value");
+
+ gBrowser.removeTab(tab);
+ finish();
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js b/browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js
new file mode 100644
index 000000000..ee0342055
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSingleWordNotification.js
@@ -0,0 +1,198 @@
+"use strict";
+
+var notificationObserver;
+registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost");
+ if (notificationObserver) {
+ notificationObserver.disconnect();
+ }
+});
+
+function promiseNotification(aBrowser, value, expected, input) {
+ let deferred = Promise.defer();
+ let notificationBox = aBrowser.getNotificationBox(aBrowser.selectedBrowser);
+ if (expected) {
+ info("Waiting for " + value + " notification");
+ let checkForNotification = function() {
+ if (notificationBox.getNotificationWithValue(value)) {
+ info("Saw the notification");
+ notificationObserver.disconnect();
+ notificationObserver = null;
+ deferred.resolve();
+ }
+ }
+ if (notificationObserver) {
+ notificationObserver.disconnect();
+ }
+ notificationObserver = new MutationObserver(checkForNotification);
+ notificationObserver.observe(notificationBox, {childList: true});
+ } else {
+ setTimeout(() => {
+ is(notificationBox.getNotificationWithValue(value), null,
+ `We are expecting to not get a notification for ${input}`);
+ deferred.resolve();
+ }, 1000);
+ }
+ return deferred.promise;
+}
+
+function* runURLBarSearchTest({valueToOpen, expectSearch, expectNotification, aWindow=window}) {
+ aWindow.gURLBar.value = valueToOpen;
+ let expectedURI;
+ if (!expectSearch) {
+ expectedURI = "http://" + valueToOpen + "/";
+ } else {
+ yield new Promise(resolve => {
+ Services.search.init(resolve);
+ });
+ expectedURI = Services.search.defaultEngine.getSubmission(valueToOpen, null, "keyword").uri.spec;
+ }
+ aWindow.gURLBar.focus();
+ let docLoadPromise = waitForDocLoadAndStopIt(expectedURI, aWindow.gBrowser.selectedBrowser);
+ EventUtils.synthesizeKey("VK_RETURN", {}, aWindow);
+
+ yield Promise.all([
+ docLoadPromise,
+ promiseNotification(aWindow.gBrowser, "keyword-uri-fixup", expectNotification, valueToOpen)
+ ]);
+}
+
+add_task(function* test_navigate_full_domain() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield* runURLBarSearchTest({
+ valueToOpen: "www.mozilla.org",
+ expectSearch: false,
+ expectNotification: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(function* test_navigate_decimal_ip() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield* runURLBarSearchTest({
+ valueToOpen: "1234",
+ expectSearch: true,
+ expectNotification: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(function* test_navigate_decimal_ip_with_path() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield* runURLBarSearchTest({
+ valueToOpen: "1234/12",
+ expectSearch: true,
+ expectNotification: false,
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(function* test_navigate_large_number() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield* runURLBarSearchTest({
+ valueToOpen: "123456789012345",
+ expectSearch: true,
+ expectNotification: false
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(function* test_navigate_small_hex_number() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield* runURLBarSearchTest({
+ valueToOpen: "0x1f00ffff",
+ expectSearch: true,
+ expectNotification: false
+ });
+ gBrowser.removeTab(tab);
+});
+
+add_task(function* test_navigate_large_hex_number() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield* runURLBarSearchTest({
+ valueToOpen: "0x7f0000017f000001",
+ expectSearch: true,
+ expectNotification: false
+ });
+ gBrowser.removeTab(tab);
+});
+
+function get_test_function_for_localhost_with_hostname(hostName, isPrivate) {
+ return function* test_navigate_single_host() {
+ const pref = "browser.fixup.domainwhitelist.localhost";
+ let win;
+ if (isPrivate) {
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ win = OpenBrowserWindow({private: true});
+ yield promiseWin;
+ let deferredOpenFocus = Promise.defer();
+ waitForFocus(deferredOpenFocus.resolve, win);
+ yield deferredOpenFocus.promise;
+ } else {
+ win = window;
+ }
+ let browser = win.gBrowser;
+ let tab = yield BrowserTestUtils.openNewForegroundTab(browser);
+
+ Services.prefs.setBoolPref(pref, false);
+ yield* runURLBarSearchTest({
+ valueToOpen: hostName,
+ expectSearch: true,
+ expectNotification: true,
+ aWindow: win,
+ });
+
+ let notificationBox = browser.getNotificationBox(tab.linkedBrowser);
+ let notification = notificationBox.getNotificationWithValue("keyword-uri-fixup");
+ let docLoadPromise = waitForDocLoadAndStopIt("http://" + hostName + "/", tab.linkedBrowser);
+ notification.querySelector(".notification-button-default").click();
+
+ // check pref value
+ let prefValue = Services.prefs.getBoolPref(pref);
+ is(prefValue, !isPrivate, "Pref should have the correct state.");
+
+ yield docLoadPromise;
+ browser.removeTab(tab);
+
+ // Now try again with the pref set.
+ tab = browser.selectedTab = browser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ // In a private window, the notification should appear again.
+ yield* runURLBarSearchTest({
+ valueToOpen: hostName,
+ expectSearch: isPrivate,
+ expectNotification: isPrivate,
+ aWindow: win,
+ });
+ browser.removeTab(tab);
+ if (isPrivate) {
+ info("Waiting for private window to close");
+ yield BrowserTestUtils.closeWindow(win);
+ let deferredFocus = Promise.defer();
+ info("Waiting for focus");
+ waitForFocus(deferredFocus.resolve, window);
+ yield deferredFocus.promise;
+ }
+ }
+}
+
+add_task(get_test_function_for_localhost_with_hostname("localhost"));
+add_task(get_test_function_for_localhost_with_hostname("localhost."));
+add_task(get_test_function_for_localhost_with_hostname("localhost", true));
+
+add_task(function* test_navigate_invalid_url() {
+ let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ yield* runURLBarSearchTest({
+ valueToOpen: "mozilla is awesome",
+ expectSearch: true,
+ expectNotification: false,
+ });
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
new file mode 100644
index 000000000..5146ba98c
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
@@ -0,0 +1,66 @@
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// Must run first.
+add_task(function* prepare() {
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ let engine = yield promiseNewSearchEngine(TEST_ENGINE_BASENAME);
+ let oldCurrentEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+ registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+ Services.search.currentEngine = oldCurrentEngine;
+
+ // Clicking suggestions causes visits to search results pages, so clear that
+ // history now.
+ yield PlacesTestUtils.clearHistory();
+
+ // Make sure the popup is closed for the next test.
+ gURLBar.blur();
+ Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+ });
+});
+
+add_task(function* clickSuggestion() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("foo");
+ let [idx, suggestion, engineName] = yield promiseFirstSuggestion();
+ Assert.equal(engineName,
+ "browser_searchSuggestionEngine%20searchSuggestionEngine.xml",
+ "Expected suggestion engine");
+ let item = gURLBar.popup.richlistbox.getItemAtIndex(idx);
+
+ let uri = Services.search.currentEngine.getSubmission(suggestion).uri;
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser,
+ false, uri.spec);
+ item.click();
+ yield loadPromise;
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+function getFirstSuggestion() {
+ let controller = gURLBar.popup.input.controller;
+ let matchCount = controller.matchCount;
+ for (let i = 0; i < matchCount; i++) {
+ let url = controller.getValueAt(i);
+ let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
+ if (mozActionMatch) {
+ let [, type, paramStr] = mozActionMatch;
+ let params = JSON.parse(paramStr);
+ if (type == "searchengine" && "searchSuggestion" in params) {
+ return [i, params.searchSuggestion, params.engineName];
+ }
+ }
+ }
+ return [-1, null, null];
+}
+
+function* promiseFirstSuggestion() {
+ let tuple = [-1, null, null];
+ yield BrowserTestUtils.waitForCondition(() => {
+ tuple = getFirstSuggestion();
+ return tuple[0] >= 0;
+ });
+ return tuple;
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarSearchSuggestionsNotification.js b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestionsNotification.js
new file mode 100644
index 000000000..94ae8a3ff
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestionsNotification.js
@@ -0,0 +1,254 @@
+const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const CHOICE_PREF = "browser.urlbar.userMadeSearchSuggestionsChoice";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// Must run first.
+add_task(function* prepare() {
+ let engine = yield promiseNewSearchEngine(TEST_ENGINE_BASENAME);
+ let oldCurrentEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+ registerCleanupFunction(function* () {
+ Services.search.currentEngine = oldCurrentEngine;
+ Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
+ Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+
+ // Disable the notification for future tests so it doesn't interfere with
+ // them. clearUserPref() won't work because by default the pref is false.
+ yield setUserMadeChoicePref(true);
+
+ // Make sure the popup is closed for the next test.
+ gURLBar.blur();
+ Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+ });
+});
+
+add_task(function* focus() {
+ // Focusing the urlbar used to open the popup in order to show the
+ // notification, but it doesn't anymore. Make sure it does not.
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ yield setUserMadeChoicePref(false);
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
+});
+
+add_task(function* dismissWithoutResults() {
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ yield setUserMadeChoicePref(false);
+ gURLBar.blur();
+ gURLBar.focus();
+ let popupPromise = promisePopupShown(gURLBar.popup);
+ gURLBar.openPopup();
+ yield popupPromise;
+ Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+ assertVisible(true);
+ Assert.equal(gURLBar.popup._matchCount, 0, "popup should have no results");
+ let disableButton = document.getAnonymousElementByAttribute(
+ gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+ );
+ let transitionPromise = promiseTransition();
+ disableButton.click();
+ yield transitionPromise;
+ Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
+ yield promiseAutocompleteResultPopup("foo");
+ Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+ assertVisible(false);
+});
+
+add_task(function* dismissWithResults() {
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ yield setUserMadeChoicePref(false);
+ gURLBar.blur();
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("foo");
+ Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+ assertVisible(true);
+ Assert.ok(gURLBar.popup._matchCount > 0, "popup should have results");
+ let disableButton = document.getAnonymousElementByAttribute(
+ gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+ );
+ let transitionPromise = promiseTransition();
+ disableButton.click();
+ yield transitionPromise;
+ Assert.ok(gURLBar.popup.popupOpen, "popup should remain open");
+ gURLBar.blur();
+ gURLBar.focus();
+ Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
+ yield promiseAutocompleteResultPopup("foo");
+ Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+ assertVisible(false);
+});
+
+add_task(function* disable() {
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ yield setUserMadeChoicePref(false);
+ gURLBar.blur();
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("foo");
+ Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+ assertVisible(true);
+ let disableButton = document.getAnonymousElementByAttribute(
+ gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+ );
+ let transitionPromise = promiseTransition();
+ disableButton.click();
+ yield transitionPromise;
+ gURLBar.blur();
+ yield promiseAutocompleteResultPopup("foo");
+ Assert.ok(!suggestionsPresent());
+});
+
+add_task(function* enable() {
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+ yield setUserMadeChoicePref(false);
+ gURLBar.blur();
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("foo");
+ assertVisible(true);
+ Assert.ok(!suggestionsPresent());
+ let enableButton = document.getAnonymousElementByAttribute(
+ gURLBar.popup, "anonid", "search-suggestions-notification-enable"
+ );
+ let searchPromise = BrowserTestUtils.waitForCondition(suggestionsPresent,
+ "waiting for suggestions");
+ enableButton.click();
+ yield searchPromise;
+ // Clicking Yes should trigger a new search so that suggestions appear
+ // immediately.
+ Assert.ok(suggestionsPresent());
+ gURLBar.blur();
+ gURLBar.focus();
+ // Suggestions should still be present in a new search of course.
+ yield promiseAutocompleteResultPopup("bar");
+ Assert.ok(suggestionsPresent());
+});
+
+add_task(function* privateWindow() {
+ // Since suggestions are disabled in private windows, the notification should
+ // not appear even when suggestions are otherwise enabled.
+ let win = yield BrowserTestUtils.openNewBrowserWindow({ private: true });
+ win.gURLBar.blur();
+ win.gURLBar.focus();
+ yield promiseAutocompleteResultPopup("foo", win);
+ assertVisible(false, win);
+ win.gURLBar.blur();
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+add_task(function* multipleWindows() {
+ // Opening multiple windows, using their urlbars, and then dismissing the
+ // notification in one should dismiss the notification in all.
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+ yield setUserMadeChoicePref(false);
+
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("win1");
+ assertVisible(true);
+
+ let win2 = yield BrowserTestUtils.openNewBrowserWindow();
+ win2.gURLBar.focus();
+ yield promiseAutocompleteResultPopup("win2", win2);
+ assertVisible(true, win2);
+
+ let win3 = yield BrowserTestUtils.openNewBrowserWindow();
+ win3.gURLBar.focus();
+ yield promiseAutocompleteResultPopup("win3", win3);
+ assertVisible(true, win3);
+
+ let enableButton = win3.document.getAnonymousElementByAttribute(
+ win3.gURLBar.popup, "anonid", "search-suggestions-notification-enable"
+ );
+ let transitionPromise = promiseTransition(win3);
+ enableButton.click();
+ yield transitionPromise;
+ assertVisible(false, win3);
+
+ win2.gURLBar.focus();
+ yield promiseAutocompleteResultPopup("win2done", win2);
+ assertVisible(false, win2);
+
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("win1done");
+ assertVisible(false);
+
+ yield BrowserTestUtils.closeWindow(win2);
+ yield BrowserTestUtils.closeWindow(win3);
+});
+
+add_task(function* enableOutsideNotification() {
+ // Setting the suggest.searches pref outside the notification (e.g., by
+ // ticking the checkbox in the preferences window) should hide it.
+ Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+ yield setUserMadeChoicePref(false);
+
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("foo");
+ assertVisible(false);
+});
+
+/**
+ * Setting the choice pref triggers a pref observer in the urlbar, which hides
+ * the notification if it's present. This function returns a promise that's
+ * resolved once the observer fires.
+ *
+ * @param userMadeChoice A boolean, the pref's new value.
+ * @return A Promise that's resolved when the observer fires -- or, if the pref
+ * is currently the given value, that's resolved immediately.
+ */
+function setUserMadeChoicePref(userMadeChoice) {
+ return new Promise(resolve => {
+ let currentUserMadeChoice = Services.prefs.getBoolPref(CHOICE_PREF);
+ if (currentUserMadeChoice != userMadeChoice) {
+ Services.prefs.addObserver(CHOICE_PREF, function obs(subj, topic, data) {
+ Services.prefs.removeObserver(CHOICE_PREF, obs);
+ resolve();
+ }, false);
+ }
+ Services.prefs.setBoolPref(CHOICE_PREF, userMadeChoice);
+ if (currentUserMadeChoice == userMadeChoice) {
+ resolve();
+ }
+ });
+}
+
+function suggestionsPresent() {
+ let controller = gURLBar.popup.input.controller;
+ let matchCount = controller.matchCount;
+ for (let i = 0; i < matchCount; i++) {
+ let url = controller.getValueAt(i);
+ let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
+ if (mozActionMatch) {
+ let [, type, paramStr] = mozActionMatch;
+ let params = JSON.parse(paramStr);
+ if (type == "searchengine" && "searchSuggestion" in params) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function assertVisible(visible, win=window) {
+ let style =
+ win.getComputedStyle(win.gURLBar.popup.searchSuggestionsNotification);
+ Assert.equal(style.visibility, visible ? "visible" : "collapse");
+}
+
+function promiseTransition(win=window) {
+ return new Promise(resolve => {
+ win.gURLBar.popup.addEventListener("transitionend", function onEnd() {
+ win.gURLBar.popup.removeEventListener("transitionend", onEnd, true);
+ // The urlbar needs to handle the transitionend first, but that happens
+ // naturally since promises are resolved at the end of the current tick.
+ resolve();
+ }, true);
+ });
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarSearchTelemetry.js b/browser/base/content/test/urlbar/browser_urlbarSearchTelemetry.js
new file mode 100644
index 000000000..8c28401ea
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchTelemetry.js
@@ -0,0 +1,216 @@
+"use strict";
+
+Cu.import("resource:///modules/BrowserUITelemetry.jsm");
+
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// Must run first.
+add_task(function* prepare() {
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ let engine = yield promiseNewSearchEngine(TEST_ENGINE_BASENAME);
+ let oldCurrentEngine = Services.search.currentEngine;
+ Services.search.currentEngine = engine;
+
+ registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+ Services.search.currentEngine = oldCurrentEngine;
+
+ // Clicking urlbar results causes visits to their associated pages, so clear
+ // that history now.
+ yield PlacesTestUtils.clearHistory();
+
+ // Make sure the popup is closed for the next test.
+ gURLBar.blur();
+ Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+ });
+
+ // Move the mouse away from the urlbar one-offs so that a one-off engine is
+ // not inadvertently selected.
+ yield new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(window.document.documentElement, 0, 0,
+ resolve);
+ });
+});
+
+add_task(function* heuristicResultMouse() {
+ yield compareCounts(function* () {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("heuristicResult");
+ let action = getActionAtIndex(0);
+ Assert.ok(!!action, "there should be an action at index 0");
+ Assert.equal(action.type, "searchengine", "type should be searchengine");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gURLBar.popup.richlistbox.getItemAtIndex(0).click();
+ yield loadPromise;
+ yield BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(function* heuristicResultKeyboard() {
+ yield compareCounts(function* () {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("heuristicResult");
+ let action = getActionAtIndex(0);
+ Assert.ok(!!action, "there should be an action at index 0");
+ Assert.equal(action.type, "searchengine", "type should be searchengine");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.sendKey("return");
+ yield loadPromise;
+ yield BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(function* searchSuggestionMouse() {
+ yield compareCounts(function* () {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("searchSuggestion");
+ let idx = getFirstSuggestionIndex();
+ Assert.ok(idx >= 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gURLBar.popup.richlistbox.getItemAtIndex(idx).click();
+ yield loadPromise;
+ yield BrowserTestUtils.removeTab(tab);
+ });
+});
+
+add_task(function* searchSuggestionKeyboard() {
+ yield compareCounts(function* () {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gURLBar.focus();
+ yield promiseAutocompleteResultPopup("searchSuggestion");
+ let idx = getFirstSuggestionIndex();
+ Assert.ok(idx >= 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (idx--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ yield loadPromise;
+ yield BrowserTestUtils.removeTab(tab);
+ });
+});
+
+/**
+ * This does three things: gets current telemetry/FHR counts, calls
+ * clickCallback, gets telemetry/FHR counts again to compare them to the old
+ * counts.
+ *
+ * @param clickCallback Use this to open the urlbar popup and choose and click a
+ * result.
+ */
+function* compareCounts(clickCallback) {
+ // Search events triggered by clicks (not the Return key in the urlbar) are
+ // recorded in three places:
+ // * BrowserUITelemetry
+ // * Telemetry histogram named "SEARCH_COUNTS"
+ // * FHR
+
+ let engine = Services.search.currentEngine;
+ let engineID = "org.mozilla.testsearchsuggestions";
+
+ // First, get the current counts.
+
+ // BrowserUITelemetry
+ let uiTelemCount = 0;
+ let bucket = BrowserUITelemetry.currentBucket;
+ let events = BrowserUITelemetry.getToolbarMeasures().countableEvents;
+ if (events[bucket] &&
+ events[bucket].search &&
+ events[bucket].search.urlbar) {
+ uiTelemCount = events[bucket].search.urlbar;
+ }
+
+ // telemetry histogram SEARCH_COUNTS
+ let histogramCount = 0;
+ let histogramKey = engineID + ".urlbar";
+ let histogram;
+ try {
+ histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+ } catch (ex) {
+ // No searches performed yet, not a problem.
+ }
+ if (histogram) {
+ let snapshot = histogram.snapshot();
+ if (histogramKey in snapshot) {
+ histogramCount = snapshot[histogramKey].sum;
+ }
+ }
+
+ // FHR -- first make sure the engine has an identifier so that FHR is happy.
+ Object.defineProperty(engine.wrappedJSObject, "identifier",
+ { value: engineID });
+
+ gURLBar.focus();
+ yield clickCallback();
+
+ // Now get the new counts and compare them to the old.
+
+ // BrowserUITelemetry
+ events = BrowserUITelemetry.getToolbarMeasures().countableEvents;
+ Assert.ok(bucket in events, "bucket should be recorded");
+ events = events[bucket];
+ Assert.ok("search" in events, "search should be recorded");
+ events = events.search;
+ Assert.ok("urlbar" in events, "urlbar should be recorded");
+ Assert.equal(events.urlbar, uiTelemCount + 1,
+ "clicked suggestion should be recorded");
+
+ // telemetry histogram SEARCH_COUNTS
+ histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+ let snapshot = histogram.snapshot();
+ Assert.ok(histogramKey in snapshot, "histogram with key should be recorded");
+ Assert.equal(snapshot[histogramKey].sum, histogramCount + 1,
+ "histogram sum should be incremented");
+}
+
+/**
+ * Returns the "action" object at the given index in the urlbar results:
+ * { type, params: {}}
+ *
+ * @param index The index in the urlbar results.
+ * @return An action object, or null if index >= number of results.
+ */
+function getActionAtIndex(index) {
+ let controller = gURLBar.popup.input.controller;
+ if (controller.matchCount <= index) {
+ return null;
+ }
+ let url = controller.getValueAt(index);
+ let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
+ if (!mozActionMatch) {
+ let msg = "result at index " + index + " is not a moz-action: " + url;
+ Assert.ok(false, msg);
+ throw new Error(msg);
+ }
+ let [, type, paramStr] = mozActionMatch;
+ return {
+ type: type,
+ params: JSON.parse(paramStr),
+ };
+}
+
+/**
+ * Returns the index of the first search suggestion in the urlbar results.
+ *
+ * @return An index, or -1 if there are no search suggestions.
+ */
+function getFirstSuggestionIndex() {
+ let controller = gURLBar.popup.input.controller;
+ let matchCount = controller.matchCount;
+ for (let i = 0; i < matchCount; i++) {
+ let url = controller.getValueAt(i);
+ let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
+ if (mozActionMatch) {
+ let [, type, paramStr] = mozActionMatch;
+ let params = JSON.parse(paramStr);
+ if (type == "searchengine" && "searchSuggestion" in params) {
+ return i;
+ }
+ }
+ }
+ return -1;
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarStop.js b/browser/base/content/test/urlbar/browser_urlbarStop.js
new file mode 100644
index 000000000..8cf9d8017
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarStop.js
@@ -0,0 +1,30 @@
+"use strict";
+
+const goodURL = "http://mochi.test:8888/";
+const badURL = "http://mochi.test:8888/whatever.html";
+
+add_task(function* () {
+ gBrowser.selectedTab = gBrowser.addTab(goodURL);
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gURLBar.textValue, gURLBar.trimValue(goodURL), "location bar reflects loaded page");
+
+ yield typeAndSubmitAndStop(badURL);
+ is(gURLBar.textValue, gURLBar.trimValue(goodURL), "location bar reflects loaded page after stop()");
+ gBrowser.removeCurrentTab();
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ is(gURLBar.textValue, "", "location bar is empty");
+
+ yield typeAndSubmitAndStop(badURL);
+ is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects stopped page in an empty tab");
+ gBrowser.removeCurrentTab();
+});
+
+function* typeAndSubmitAndStop(url) {
+ yield promiseAutocompleteResultPopup(url, window, true);
+ is(gURLBar.textValue, gURLBar.trimValue(url), "location bar reflects loading page");
+
+ let promise = waitForDocLoadAndStopIt(url, gBrowser.selectedBrowser, false);
+ gURLBar.handleCommand();
+ yield promise;
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarTrimURLs.js b/browser/base/content/test/urlbar/browser_urlbarTrimURLs.js
new file mode 100644
index 000000000..913e99a8e
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarTrimURLs.js
@@ -0,0 +1,98 @@
+add_task(function* () {
+ const PREF_TRIMURLS = "browser.urlbar.trimURLs";
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(function* () {
+ yield BrowserTestUtils.removeTab(tab);
+ Services.prefs.clearUserPref(PREF_TRIMURLS);
+ URLBarSetURI();
+ });
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, true);
+
+ testVal("http://mozilla.org/", "mozilla.org");
+ testVal("https://mozilla.org/", "https://mozilla.org");
+ testVal("http://mözilla.org/", "mözilla.org");
+ testVal("http://mozilla.imaginatory/", "mozilla.imaginatory");
+ testVal("http://www.mozilla.org/", "www.mozilla.org");
+ testVal("http://sub.mozilla.org/", "sub.mozilla.org");
+ testVal("http://sub1.sub2.sub3.mozilla.org/", "sub1.sub2.sub3.mozilla.org");
+ testVal("http://mozilla.org/file.ext", "mozilla.org/file.ext");
+ testVal("http://mozilla.org/sub/", "mozilla.org/sub/");
+
+ testVal("http://ftp.mozilla.org/", "ftp.mozilla.org");
+ testVal("http://ftp1.mozilla.org/", "ftp1.mozilla.org");
+ testVal("http://ftp42.mozilla.org/", "ftp42.mozilla.org");
+ testVal("http://ftpx.mozilla.org/", "ftpx.mozilla.org");
+ testVal("ftp://ftp.mozilla.org/", "ftp://ftp.mozilla.org");
+ testVal("ftp://ftp1.mozilla.org/", "ftp://ftp1.mozilla.org");
+ testVal("ftp://ftp42.mozilla.org/", "ftp://ftp42.mozilla.org");
+ testVal("ftp://ftpx.mozilla.org/", "ftp://ftpx.mozilla.org");
+
+ testVal("https://user:pass@mozilla.org/", "https://user:pass@mozilla.org");
+ testVal("https://user@mozilla.org/", "https://user@mozilla.org");
+ testVal("http://user:pass@mozilla.org/", "user:pass@mozilla.org");
+ testVal("http://user@mozilla.org/", "user@mozilla.org");
+ testVal("http://sub.mozilla.org:666/", "sub.mozilla.org:666");
+
+ testVal("https://[fe80::222:19ff:fe11:8c76]/file.ext");
+ testVal("http://[fe80::222:19ff:fe11:8c76]/", "[fe80::222:19ff:fe11:8c76]");
+ testVal("https://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext");
+ testVal("http://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext", "user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext");
+
+ testVal("mailto:admin@mozilla.org");
+ testVal("gopher://mozilla.org/");
+ testVal("about:config");
+ testVal("jar:http://mozilla.org/example.jar!/");
+ testVal("view-source:http://mozilla.org/");
+
+ // Behaviour for hosts with no dots depends on the whitelist:
+ let fixupWhitelistPref = "browser.fixup.domainwhitelist.localhost";
+ Services.prefs.setBoolPref(fixupWhitelistPref, false);
+ testVal("http://localhost");
+ Services.prefs.setBoolPref(fixupWhitelistPref, true);
+ testVal("http://localhost", "localhost");
+ Services.prefs.clearUserPref(fixupWhitelistPref);
+
+ testVal("http:// invalid url");
+
+ testVal("http://someotherhostwithnodots");
+ testVal("http://localhost/ foo bar baz");
+ testVal("http://localhost.localdomain/ foo bar baz", "localhost.localdomain/ foo bar baz");
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, false);
+
+ testVal("http://mozilla.org/");
+
+ Services.prefs.setBoolPref(PREF_TRIMURLS, true);
+
+ let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser,
+ false, "http://example.com/");
+ gBrowser.loadURI("http://example.com/");
+ yield promiseLoaded;
+
+ yield testCopy("example.com", "http://example.com/")
+
+ SetPageProxyState("invalid");
+ gURLBar.valueIsTyped = true;
+ yield testCopy("example.com", "example.com");
+});
+
+function testVal(originalValue, targetValue) {
+ gURLBar.value = originalValue;
+ gURLBar.valueIsTyped = false;
+ is(gURLBar.textValue, targetValue || originalValue, "url bar value set");
+}
+
+function testCopy(originalValue, targetValue) {
+ return new Promise((resolve, reject) => {
+ waitForClipboard(targetValue, function () {
+ is(gURLBar.textValue, originalValue, "url bar copy value set");
+
+ gURLBar.focus();
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ }, resolve, reject);
+ });
+}
diff --git a/browser/base/content/test/urlbar/browser_urlbarUpdateForDomainCompletion.js b/browser/base/content/test/urlbar/browser_urlbarUpdateForDomainCompletion.js
new file mode 100644
index 000000000..c3cdf507f
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarUpdateForDomainCompletion.js
@@ -0,0 +1,17 @@
+"use strict";
+
+/**
+ * Disable keyword.enabled (so no keyword search), and check that when you type in
+ * "example" and hit enter, the browser loads and the URL bar is updated accordingly.
+ */
+add_task(function* () {
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({set: [["keyword.enabled", false]]}, resolve));
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
+ gURLBar.value = "example";
+ gURLBar.select();
+ let loadPromise = BrowserTestUtils.browserLoaded(browser, false, url => url == "http://www.example.com/");
+ EventUtils.sendKey("return");
+ yield loadPromise;
+ is(gURLBar.textValue, "www.example.com");
+ });
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js b/browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js
new file mode 100644
index 000000000..7fefd3f77
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js
@@ -0,0 +1,146 @@
+/* This test ensures that backspacing autoFilled values still allows to
+ * confirm the remaining value.
+ */
+
+function* test_autocomplete(data) {
+ let {desc, typed, autofilled, modified, keys, action, onAutoFill} = data;
+ info(desc);
+
+ yield promiseAutocompleteResultPopup(typed);
+ is(gURLBar.textValue, autofilled, "autofilled value is as expected");
+ if (onAutoFill)
+ onAutoFill()
+
+ keys.forEach(key => EventUtils.synthesizeKey(key, {}));
+
+ is(gURLBar.textValue, modified, "backspaced value is as expected");
+
+ yield promiseSearchComplete();
+
+ ok(gURLBar.popup.richlistbox.children.length > 0, "Should get at least 1 result");
+ let result = gURLBar.popup.richlistbox.children[0];
+ let type = result.getAttribute("type");
+ let types = type.split(/\s+/);
+ ok(types.indexOf(action) >= 0, `The type attribute "${type}" includes the expected action "${action}"`);
+
+ gURLBar.popup.hidePopup();
+ yield promisePopupHidden(gURLBar.popup);
+ gURLBar.blur();
+}
+
+add_task(function* () {
+ registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ gURLBar.handleRevert();
+ yield PlacesTestUtils.clearHistory();
+ });
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+
+ // Add a typed visit, so it will be autofilled.
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://example.com/"),
+ transition: Ci.nsINavHistoryService.TRANSITION_TYPED
+ });
+
+ yield test_autocomplete({ desc: "DELETE the autofilled part should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exam",
+ keys: ["VK_DELETE"],
+ action: "searchengine"
+ });
+ yield test_autocomplete({ desc: "DELETE the final slash should visit",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["VK_DELETE"],
+ action: "visiturl"
+ });
+
+ yield test_autocomplete({ desc: "BACK_SPACE the autofilled part should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exam",
+ keys: ["VK_BACK_SPACE"],
+ action: "searchengine"
+ });
+ yield test_autocomplete({ desc: "BACK_SPACE the final slash should visit",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.com",
+ keys: ["VK_BACK_SPACE"],
+ action: "visiturl"
+ });
+
+ yield test_autocomplete({ desc: "DELETE the autofilled part, then BACK_SPACE, should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exa",
+ keys: ["VK_DELETE", "VK_BACK_SPACE"],
+ action: "searchengine"
+ });
+ yield test_autocomplete({ desc: "DELETE the final slash, then BACK_SPACE, should search",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.co",
+ keys: ["VK_DELETE", "VK_BACK_SPACE"],
+ action: "visiturl"
+ });
+
+ yield test_autocomplete({ desc: "BACK_SPACE the autofilled part, then BACK_SPACE, should search",
+ typed: "exam",
+ autofilled: "example.com/",
+ modified: "exa",
+ keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+ action: "searchengine"
+ });
+ yield test_autocomplete({ desc: "BACK_SPACE the final slash, then BACK_SPACE, should search",
+ typed: "example.com",
+ autofilled: "example.com/",
+ modified: "example.co",
+ keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+ action: "visiturl"
+ });
+
+ yield test_autocomplete({ desc: "BACK_SPACE after blur should search",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["VK_BACK_SPACE"],
+ action: "searchengine",
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ gURLBar.selectionStart = 1;
+ gURLBar.selectionEnd = 12;
+ }
+ });
+ yield test_autocomplete({ desc: "DELETE after blur should search",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["VK_DELETE"],
+ action: "searchengine",
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ gURLBar.selectionStart = 1;
+ gURLBar.selectionEnd = 12;
+ }
+ });
+ yield test_autocomplete({ desc: "double BACK_SPACE after blur should search",
+ typed: "ex",
+ autofilled: "example.com/",
+ modified: "e",
+ keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+ action: "searchengine",
+ onAutoFill: () => {
+ gURLBar.blur();
+ gURLBar.focus();
+ gURLBar.selectionStart = 2;
+ gURLBar.selectionEnd = 12;
+ }
+ });
+
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbar_blanking.js b/browser/base/content/test/urlbar/browser_urlbar_blanking.js
new file mode 100644
index 000000000..13660edab
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbar_blanking.js
@@ -0,0 +1,35 @@
+"use strict";
+
+add_task(function*() {
+ for (let page of gInitialPages) {
+ if (page == "about:newtab") {
+ // New tab preloading makes this a pain to test, so skip
+ continue;
+ }
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, page);
+ ok(!gURLBar.value, "The URL bar should be empty if we load a plain " + page + " page.");
+ yield BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(function*() {
+ const URI = "http://www.example.com/browser/browser/base/content/test/urlbar/file_blank_but_not_blank.html";
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, URI);
+ is(gURLBar.value, URI, "The URL bar should match the URI");
+ let browserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ ContentTask.spawn(tab.linkedBrowser, null, function() {
+ content.document.querySelector('a').click();
+ });
+ yield browserLoaded;
+ ok(gURLBar.value.startsWith("javascript"), "The URL bar should have the JS URI");
+ // When reloading, the javascript: uri we're using will throw an exception.
+ // That's deliberate, so we need to tell mochitest to ignore it:
+ SimpleTest.expectUncaughtException(true);
+ yield ContentTask.spawn(tab.linkedBrowser, null, function*() {
+ // This is sync, so by the time we return we should have changed the URL bar.
+ content.location.reload();
+ });
+ ok(!!gURLBar.value, "URL bar should not be blank.");
+ yield BrowserTestUtils.removeTab(tab);
+ SimpleTest.expectUncaughtException(false);
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbar_locationchange_urlbar_edit_dos.js b/browser/base/content/test/urlbar/browser_urlbar_locationchange_urlbar_edit_dos.js
new file mode 100644
index 000000000..63ed58a62
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbar_locationchange_urlbar_edit_dos.js
@@ -0,0 +1,41 @@
+"use strict";
+
+function* checkURLBarValueStays(browser) {
+ gURLBar.select();
+ EventUtils.synthesizeKey("a", {});
+ is(gURLBar.value, "a", "URL bar value should match after sending a key");
+ yield new Promise(resolve => {
+ let listener = {
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ ok(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT,
+ "Should only get a same document location change");
+ gBrowser.selectedBrowser.removeProgressListener(filter);
+ filter = null;
+ resolve();
+ },
+ };
+ let filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
+ .createInstance(Ci.nsIWebProgress);
+ filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
+ gBrowser.selectedBrowser.addProgressListener(filter);
+ });
+ is(gURLBar.value, "a", "URL bar should not have been changed by location changes.");
+}
+
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "http://example.com/browser/browser/base/content/test/urlbar/file_urlbar_edit_dos.html"
+ }, function*(browser) {
+ yield ContentTask.spawn(browser, "", function() {
+ content.wrappedJSObject.dos_hash();
+ });
+ yield checkURLBarValueStays(browser);
+ yield ContentTask.spawn(browser, "", function() {
+ content.clearTimeout(content.wrappedJSObject.dos_timeout);
+ content.wrappedJSObject.dos_pushState();
+ });
+ yield checkURLBarValueStays(browser);
+ });
+});
+
diff --git a/browser/base/content/test/urlbar/browser_urlbar_remoteness_switch.js b/browser/base/content/test/urlbar/browser_urlbar_remoteness_switch.js
new file mode 100644
index 000000000..9a1df0505
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbar_remoteness_switch.js
@@ -0,0 +1,39 @@
+"use strict";
+
+/**
+ * Verify that when loading and going back/forward through history between URLs
+ * loaded in the content process, and URLs loaded in the parent process, we
+ * don't set the URL for the tab to about:blank inbetween the loads.
+ */
+add_task(function*() {
+ let url = "http://www.example.com/foo.html";
+ yield BrowserTestUtils.withNewTab({gBrowser, url}, function*(browser) {
+ let wpl = {
+ onLocationChange(wpl, request, location, flags) {
+ if (location.schemeIs("about")) {
+ is(location.spec, "about:config", "Only about: location change should be for about:preferences");
+ } else {
+ is(location.spec, url, "Only non-about: location change should be for the http URL we're dealing with.");
+ }
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ let didLoad = BrowserTestUtils.browserLoaded(browser, null, function(loadedURL) {
+ return loadedURL == "about:config";
+ });
+ yield BrowserTestUtils.loadURI(browser, "about:config");
+ yield didLoad;
+
+ gBrowser.goBack();
+ yield BrowserTestUtils.browserLoaded(browser, null, function(loadedURL) {
+ return url == loadedURL;
+ });
+ gBrowser.goForward();
+ yield BrowserTestUtils.browserLoaded(browser, null, function(loadedURL) {
+ return loadedURL == "about:config";
+ });
+ gBrowser.removeProgressListener(wpl);
+ });
+});
+
diff --git a/browser/base/content/test/urlbar/browser_urlbar_searchsettings.js b/browser/base/content/test/urlbar/browser_urlbar_searchsettings.js
new file mode 100644
index 000000000..04b1c508b
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbar_searchsettings.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function*() {
+ let button = document.getElementById("urlbar-search-settings");
+ if (!button) {
+ ok("Skipping test");
+ return;
+ }
+
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* () {
+ let popupopened = BrowserTestUtils.waitForEvent(gURLBar.popup, "popupshown");
+
+ gURLBar.focus();
+ EventUtils.synthesizeKey("a", {});
+ yield popupopened;
+
+ // Since the current tab is blank the preferences pane will load there
+ let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ let popupclosed = BrowserTestUtils.waitForEvent(gURLBar.popup, "popuphidden");
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ yield loaded;
+ yield popupclosed;
+
+ is(gBrowser.selectedBrowser.currentURI.spec, "about:preferences#search",
+ "Should have loaded the right page");
+ });
+});
diff --git a/browser/base/content/test/urlbar/browser_urlbar_stop_pending.js b/browser/base/content/test/urlbar/browser_urlbar_stop_pending.js
new file mode 100644
index 000000000..6b6a10ea3
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbar_stop_pending.js
@@ -0,0 +1,138 @@
+"use strict";
+
+const SLOW_PAGE = "http://www.example.com/browser/browser/base/content/test/urlbar/slow-page.sjs";
+const SLOW_PAGE2 = "http://mochi.test:8888/browser/browser/base/content/test/urlbar/slow-page.sjs?faster";
+
+/**
+ * Check that if we:
+ * 1) have a loaded page
+ * 2) load a separate URL
+ * 3) before the URL for step 2 has finished loading, load a third URL
+ * we don't revert to the URL from (1).
+ */
+add_task(function*() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com", true, true);
+
+ let expectedURLBarChange = SLOW_PAGE;
+ let sawChange = false;
+ let handler = e => {
+ sawChange = true;
+ is(gURLBar.value, expectedURLBarChange, "Should not change URL bar value!");
+ };
+
+ let obs = new MutationObserver(handler);
+
+ obs.observe(gURLBar, {attributes: true});
+ gURLBar.value = SLOW_PAGE;
+ gURLBar.handleCommand();
+
+ // If this ever starts going intermittent, we've broken this.
+ yield new Promise(resolve => setTimeout(resolve, 200));
+ expectedURLBarChange = SLOW_PAGE2;
+ let pageLoadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gURLBar.value = expectedURLBarChange;
+ gURLBar.handleCommand();
+ is(gURLBar.value, expectedURLBarChange, "Should not have changed URL bar value synchronously.");
+ yield pageLoadPromise;
+ ok(sawChange, "The URL bar change handler should have been called by the time the page was loaded");
+ obs.disconnect();
+ obs = null;
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Check that if we:
+ * 1) middle-click a link to a separate page whose server doesn't respond
+ * 2) we switch to that tab and stop the request
+ *
+ * The URL bar continues to contain the URL of the page we wanted to visit.
+ */
+add_task(function*() {
+ let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
+ socket.init(-1, true, -1);
+ const PORT = socket.port;
+ registerCleanupFunction(() => { socket.close(); });
+
+ const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com");
+ const BASE_PAGE = TEST_PATH + "dummy_page.html";
+ const SLOW_HOST = `https://localhost:${PORT}/`;
+ info("Using URLs: " + SLOW_HOST);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE);
+ info("opened tab");
+ yield ContentTask.spawn(tab.linkedBrowser, SLOW_HOST, URL => {
+ let link = content.document.createElement("a");
+ link.href = URL;
+ link.textContent = "click me to open a slow page";
+ link.id = "clickme"
+ content.document.body.appendChild(link);
+ });
+ info("added link");
+ let newTabPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ // Middle click the link:
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#clickme", { button: 1 }, tab.linkedBrowser);
+ // get new tab, switch to it
+ let newTab = (yield newTabPromise).target;
+ yield BrowserTestUtils.switchTab(gBrowser, newTab);
+ is(gURLBar.value, SLOW_HOST, "Should have slow page in URL bar");
+ let browserStoppedPromise = BrowserTestUtils.browserStopped(newTab.linkedBrowser);
+ BrowserStop();
+ yield browserStoppedPromise;
+
+ is(gURLBar.value, SLOW_HOST, "Should still have slow page in URL bar after stop");
+ yield BrowserTestUtils.removeTab(newTab);
+ yield BrowserTestUtils.removeTab(tab);
+});
+/**
+ * Check that if we:
+ * 1) middle-click a link to a separate page whose server doesn't respond
+ * 2) we alter the URL on that page to some other server that doesn't respond
+ * 3) we stop the request
+ *
+ * The URL bar continues to contain the second URL.
+ */
+add_task(function*() {
+ let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
+ socket.init(-1, true, -1);
+ const PORT1 = socket.port;
+ let socket2 = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
+ socket2.init(-1, true, -1);
+ const PORT2 = socket2.port;
+ registerCleanupFunction(() => { socket.close(); socket2.close(); });
+
+ const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com");
+ const BASE_PAGE = TEST_PATH + "dummy_page.html";
+ const SLOW_HOST1 = `https://localhost:${PORT1}/`;
+ const SLOW_HOST2 = `https://localhost:${PORT2}/`;
+ info("Using URLs: " + SLOW_HOST1 + " and " + SLOW_HOST2);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE);
+ info("opened tab");
+ yield ContentTask.spawn(tab.linkedBrowser, SLOW_HOST1, URL => {
+ let link = content.document.createElement("a");
+ link.href = URL;
+ link.textContent = "click me to open a slow page";
+ link.id = "clickme"
+ content.document.body.appendChild(link);
+ });
+ info("added link");
+ let newTabPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+ // Middle click the link:
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#clickme", { button: 1 }, tab.linkedBrowser);
+ // get new tab, switch to it
+ let newTab = (yield newTabPromise).target;
+ yield BrowserTestUtils.switchTab(gBrowser, newTab);
+ is(gURLBar.value, SLOW_HOST1, "Should have slow page in URL bar");
+ let browserStoppedPromise = BrowserTestUtils.browserStopped(newTab.linkedBrowser);
+ gURLBar.value = SLOW_HOST2;
+ gURLBar.handleCommand();
+ yield browserStoppedPromise;
+
+ is(gURLBar.value, SLOW_HOST2, "Should have second slow page in URL bar");
+ browserStoppedPromise = BrowserTestUtils.browserStopped(newTab.linkedBrowser);
+ BrowserStop();
+ yield browserStoppedPromise;
+
+ is(gURLBar.value, SLOW_HOST2, "Should still have second slow page in URL bar after stop");
+ yield BrowserTestUtils.removeTab(newTab);
+ yield BrowserTestUtils.removeTab(tab);
+});
+
diff --git a/browser/base/content/test/urlbar/browser_wyciwyg_urlbarCopying.js b/browser/base/content/test/urlbar/browser_wyciwyg_urlbarCopying.js
new file mode 100644
index 000000000..54b174aa8
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_wyciwyg_urlbarCopying.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function testURLBarCopy(targetValue) {
+ return new Promise((resolve, reject) => {
+ info("Expecting copy of: " + targetValue);
+ waitForClipboard(targetValue, function () {
+ gURLBar.focus();
+ gURLBar.select();
+
+ goDoCommand("cmd_copy");
+ }, resolve, () => {
+ ok(false, "Clipboard copy failed");
+ reject();
+ });
+ });
+}
+
+add_task(function* () {
+ const url = "http://mochi.test:8888/browser/browser/base/content/test/urlbar/test_wyciwyg_copying.html";
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#btn", {}, tab.linkedBrowser);
+ let currentURL = gBrowser.currentURI.spec;
+ ok(/^wyciwyg:\/\//i.test(currentURL), currentURL + " is a wyciwyg URI");
+
+ yield testURLBarCopy(url);
+
+ while (gBrowser.tabs.length > 1)
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/base/content/test/urlbar/dummy_page.html b/browser/base/content/test/urlbar/dummy_page.html
new file mode 100644
index 000000000..1a87e2840
--- /dev/null
+++ b/browser/base/content/test/urlbar/dummy_page.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/browser/base/content/test/urlbar/file_blank_but_not_blank.html b/browser/base/content/test/urlbar/file_blank_but_not_blank.html
new file mode 100644
index 000000000..1f5fea8dc
--- /dev/null
+++ b/browser/base/content/test/urlbar/file_blank_but_not_blank.html
@@ -0,0 +1,2 @@
+<script>var q = "1";</script>
+<a href="javascript:q">Click me</a>
diff --git a/browser/base/content/test/urlbar/file_urlbar_edit_dos.html b/browser/base/content/test/urlbar/file_urlbar_edit_dos.html
new file mode 100644
index 000000000..5a6e7d109
--- /dev/null
+++ b/browser/base/content/test/urlbar/file_urlbar_edit_dos.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Try editing the URL bar</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<script>
+var dos_timeout = null;
+function dos_hash() {
+ dos_timeout = setTimeout(function() {
+ location.hash = "#";
+ }, 50);
+}
+
+function dos_pushState() {
+ dos_timeout = setTimeout(function() {
+ history.pushState({}, "Some title", "");
+ }, 50);
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/urlbar/head.js b/browser/base/content/test/urlbar/head.js
new file mode 100644
index 000000000..427dba080
--- /dev/null
+++ b/browser/base/content/test/urlbar/head.js
@@ -0,0 +1,205 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+/**
+ * Waits for the next top-level document load in the current browser. The URI
+ * of the document is compared against aExpectedURL. The load is then stopped
+ * before it actually starts.
+ *
+ * @param aExpectedURL
+ * The URL of the document that is expected to load.
+ * @param aStopFromProgressListener
+ * Whether to cancel the load directly from the progress listener. Defaults to true.
+ * If you're using this method to avoid hitting the network, you want the default (true).
+ * However, the browser UI will behave differently for loads stopped directly from
+ * the progress listener (effectively in the middle of a call to loadURI) and so there
+ * are cases where you may want to avoid stopping the load directly from within the
+ * progress listener callback.
+ * @return promise
+ */
+function waitForDocLoadAndStopIt(aExpectedURL, aBrowser=gBrowser.selectedBrowser, aStopFromProgressListener=true) {
+ function content_script(aStopFromProgressListener) {
+ let { interfaces: Ci, utils: Cu } = Components;
+ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+ let wp = docShell.QueryInterface(Ci.nsIWebProgress);
+
+ function stopContent(now, uri) {
+ if (now) {
+ /* Hammer time. */
+ content.stop();
+
+ /* Let the parent know we're done. */
+ sendAsyncMessage("Test:WaitForDocLoadAndStopIt", { uri });
+ } else {
+ setTimeout(stopContent.bind(null, true, uri), 0);
+ }
+ }
+
+ let progressListener = {
+ onStateChange: function (webProgress, req, flags, status) {
+ dump("waitForDocLoadAndStopIt: onStateChange " + flags.toString(16) + ": " + req.name + "\n");
+
+ if (webProgress.isTopLevel &&
+ flags & Ci.nsIWebProgressListener.STATE_START) {
+ wp.removeProgressListener(progressListener);
+
+ let chan = req.QueryInterface(Ci.nsIChannel);
+ dump(`waitForDocLoadAndStopIt: Document start: ${chan.URI.spec}\n`);
+
+ stopContent(aStopFromProgressListener, chan.originalURI.spec);
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"])
+ };
+ wp.addProgressListener(progressListener, wp.NOTIFY_STATE_WINDOW);
+
+ /**
+ * As |this| is undefined and we can't extend |docShell|, adding an unload
+ * event handler is the easiest way to ensure the weakly referenced
+ * progress listener is kept alive as long as necessary.
+ */
+ addEventListener("unload", function () {
+ try {
+ wp.removeProgressListener(progressListener);
+ } catch (e) { /* Will most likely fail. */ }
+ });
+ }
+
+ return new Promise((resolve, reject) => {
+ function complete({ data }) {
+ is(data.uri, aExpectedURL, "waitForDocLoadAndStopIt: The expected URL was loaded");
+ mm.removeMessageListener("Test:WaitForDocLoadAndStopIt", complete);
+ resolve();
+ }
+
+ let mm = aBrowser.messageManager;
+ mm.loadFrameScript("data:,(" + content_script.toString() + ")(" + aStopFromProgressListener + ");", true);
+ mm.addMessageListener("Test:WaitForDocLoadAndStopIt", complete);
+ info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL);
+ });
+}
+
+function is_hidden(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none")
+ return true;
+ if (style.visibility != "visible")
+ return true;
+ if (style.display == "-moz-popup")
+ return ["hiding", "closed"].indexOf(element.state) != -1;
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument)
+ return is_hidden(element.parentNode);
+
+ return false;
+}
+
+function is_visible(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none")
+ return false;
+ if (style.visibility != "visible")
+ return false;
+ if (style.display == "-moz-popup" && element.state != "open")
+ return false;
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument)
+ return is_visible(element.parentNode);
+
+ return true;
+}
+
+function is_element_visible(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_visible(element), msg || "Element should be visible");
+}
+
+function is_element_hidden(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(element), msg || "Element should be hidden");
+}
+
+function promisePopupEvent(popup, eventSuffix) {
+ let endState = {shown: "open", hidden: "closed"}[eventSuffix];
+
+ if (popup.state == endState)
+ return Promise.resolve();
+
+ let eventType = "popup" + eventSuffix;
+ let deferred = Promise.defer();
+ popup.addEventListener(eventType, function onPopupShown(event) {
+ popup.removeEventListener(eventType, onPopupShown);
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function promisePopupShown(popup) {
+ return promisePopupEvent(popup, "shown");
+}
+
+function promisePopupHidden(popup) {
+ return promisePopupEvent(popup, "hidden");
+}
+
+function promiseSearchComplete(win = window) {
+ return promisePopupShown(win.gURLBar.popup).then(() => {
+ function searchIsComplete() {
+ return win.gURLBar.controller.searchStatus >=
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+ }
+
+ // Wait until there are at least two matches.
+ return BrowserTestUtils.waitForCondition(searchIsComplete, "waiting urlbar search to complete");
+ });
+}
+
+function promiseAutocompleteResultPopup(inputText,
+ win = window,
+ fireInputEvent = false) {
+ waitForFocus(() => {
+ win.gURLBar.focus();
+ win.gURLBar.value = inputText;
+ if (fireInputEvent) {
+ // This is necessary to get the urlbar to set gBrowser.userTypedValue.
+ let event = document.createEvent("Events");
+ event.initEvent("input", true, true);
+ win.gURLBar.dispatchEvent(event);
+ }
+ win.gURLBar.controller.startSearch(inputText);
+ }, win);
+
+ return promiseSearchComplete(win);
+}
+
+function promiseNewSearchEngine(basename) {
+ return new Promise((resolve, reject) => {
+ info("Waiting for engine to be added: " + basename);
+ let url = getRootDirectory(gTestPath) + basename;
+ Services.search.addEngine(url, null, "", false, {
+ onSuccess: function (engine) {
+ info("Search engine added: " + basename);
+ registerCleanupFunction(() => Services.search.removeEngine(engine));
+ resolve(engine);
+ },
+ onError: function (errCode) {
+ Assert.ok(false, "addEngine failed with error code " + errCode);
+ reject();
+ },
+ });
+ });
+}
+
diff --git a/browser/base/content/test/urlbar/moz.png b/browser/base/content/test/urlbar/moz.png
new file mode 100644
index 000000000..769c63634
--- /dev/null
+++ b/browser/base/content/test/urlbar/moz.png
Binary files differ
diff --git a/browser/base/content/test/urlbar/print_postdata.sjs b/browser/base/content/test/urlbar/print_postdata.sjs
new file mode 100644
index 000000000..4175a2480
--- /dev/null
+++ b/browser/base/content/test/urlbar/print_postdata.sjs
@@ -0,0 +1,22 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream");
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ if (request.method == "GET") {
+ response.write(request.queryString);
+ } else {
+ var body = new BinaryInputStream(request.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0)
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+ var data = String.fromCharCode.apply(null, bytes);
+ response.bodyOutputStream.write(data, data.length);
+ }
+}
diff --git a/browser/base/content/test/urlbar/redirect_bug623155.sjs b/browser/base/content/test/urlbar/redirect_bug623155.sjs
new file mode 100644
index 000000000..64c6f143b
--- /dev/null
+++ b/browser/base/content/test/urlbar/redirect_bug623155.sjs
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host.
+
+function handleRequest(aRequest, aResponse) {
+ // Set HTTP Status
+ aResponse.setStatusLine(aRequest.httpVersion, 301, "Moved Permanently");
+
+ // Set redirect URI, mirroring the hash value.
+ let hash = (/\#.+/.test(aRequest.path))?
+ "#" + aRequest.path.split("#")[1]:
+ "";
+ aResponse.setHeader("Location", REDIRECT_TO + hash);
+}
diff --git a/browser/base/content/test/urlbar/searchSuggestionEngine.sjs b/browser/base/content/test/urlbar/searchSuggestionEngine.sjs
new file mode 100644
index 000000000..1978b4f66
--- /dev/null
+++ b/browser/base/content/test/urlbar/searchSuggestionEngine.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+ let suffixes = ["foo", "bar"];
+ let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
diff --git a/browser/base/content/test/urlbar/searchSuggestionEngine.xml b/browser/base/content/test/urlbar/searchSuggestionEngine.xml
new file mode 100644
index 000000000..a5659792e
--- /dev/null
+++ b/browser/base/content/test/urlbar/searchSuggestionEngine.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/base/content/test/urlbar/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/base/content/test/urlbar/slow-page.sjs b/browser/base/content/test/urlbar/slow-page.sjs
new file mode 100644
index 000000000..f428d66e4
--- /dev/null
+++ b/browser/base/content/test/urlbar/slow-page.sjs
@@ -0,0 +1,22 @@
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+let timer;
+
+const DELAY_MS = 5000;
+function handleRequest(request, response) {
+ if (request.queryString.endsWith("faster")) {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<body>Not so slow!</body>");
+ return;
+ }
+ response.processAsync();
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(() => {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<body>This was the slow load. You should never see this.</body>");
+ response.finish();
+ }, DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+}
diff --git a/browser/base/content/test/urlbar/test_wyciwyg_copying.html b/browser/base/content/test/urlbar/test_wyciwyg_copying.html
new file mode 100644
index 000000000..3a8c3a150
--- /dev/null
+++ b/browser/base/content/test/urlbar/test_wyciwyg_copying.html
@@ -0,0 +1,13 @@
+<html>
+<body>
+<script>
+ function go() {
+ var w = window.open();
+ w.document.open();
+ w.document.write("<html><body>test document</body></html>");
+ w.document.close();
+ }
+</script>
+<button id="btn" onclick="go();">test</button>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/.eslintrc.js b/browser/base/content/test/webrtc/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/browser/base/content/test/webrtc/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/browser/base/content/test/webrtc/browser.ini b/browser/base/content/test/webrtc/browser.ini
new file mode 100644
index 000000000..8830989ad
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+support-files =
+ get_user_media.html
+ get_user_media_content_script.js
+ head.js
+
+[browser_devices_get_user_media.js]
+skip-if = (os == "linux" && debug) # linux: bug 976544
+[browser_devices_get_user_media_anim.js]
+[browser_devices_get_user_media_in_frame.js]
+[browser_devices_get_user_media_tear_off_tab.js]
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media.js b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
new file mode 100644
index 000000000..3681a810b
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
@@ -0,0 +1,554 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+requestLongerTimeout(2);
+
+registerCleanupFunction(function() {
+ gBrowser.removeCurrentTab();
+});
+
+const permissionError = "error: NotAllowedError: The request is not allowed " +
+ "by the user agent or the platform in the current context.";
+
+var gTests = [
+
+{
+ desc: "getUserMedia audio+video",
+ run: function* checkAudioVideo() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+
+ is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon", "anchored to device icon");
+ checkDeviceSelectors(true, true);
+ let iconclass =
+ PopupNotifications.panel.firstChild.getAttribute("iconclass");
+ ok(iconclass.includes("camera-icon"), "panel using devices icon");
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({audio: true, video: true});
+ yield closeStream();
+ }
+},
+
+{
+ desc: "getUserMedia audio only",
+ run: function* checkAudioOnly() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+
+ is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareMicrophone-notification-icon", "anchored to mic icon");
+ checkDeviceSelectors(true);
+ let iconclass =
+ PopupNotifications.panel.firstChild.getAttribute("iconclass");
+ ok(iconclass.includes("microphone-icon"), "panel using microphone icon");
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "Microphone",
+ "expected microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({audio: true});
+ yield closeStream();
+ }
+},
+
+{
+ desc: "getUserMedia video only",
+ run: function* checkVideoOnly() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(false, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+
+ is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon", "anchored to device icon");
+ checkDeviceSelectors(false, true);
+ let iconclass =
+ PopupNotifications.panel.firstChild.getAttribute("iconclass");
+ ok(iconclass.includes("camera-icon"), "panel using devices icon");
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "Camera", "expected camera to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: true});
+ yield closeStream();
+ }
+},
+
+{
+ desc: "getUserMedia audio+video, user clicks \"Don't Share\"",
+ run: function* checkDontShare() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ yield promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+
+ yield expectObserverCalled("getUserMedia:response:deny");
+ yield expectObserverCalled("recording-window-ended");
+ yield checkNotSharing();
+ }
+},
+
+{
+ desc: "getUserMedia audio+video: stop sharing",
+ run: function* checkStopSharing() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: true, audio: true});
+
+ yield stopSharing();
+
+ // the stream is already closed, but this will do some cleanup anyway
+ yield closeStream(true);
+ }
+},
+
+{
+ desc: "getUserMedia audio+video: reloading the page removes all gUM UI",
+ run: function* checkReloading() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: true, audio: true});
+
+ info("reloading the web page");
+ promise = promiseObserverCalled("recording-device-events");
+ content.location.reload();
+ yield promise;
+
+ yield expectObserverCalled("recording-window-ended");
+ yield expectNoObserverCalled();
+ yield checkNotSharing();
+ }
+},
+
+{
+ desc: "getUserMedia prompt: Always/Never Share",
+ run: function* checkRememberCheckbox() {
+ let elt = id => document.getElementById(id);
+
+ function* checkPerm(aRequestAudio, aRequestVideo,
+ aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(aRequestAudio, aRequestVideo);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+
+ is(elt("webRTC-selectMicrophone").hidden, !aRequestAudio,
+ "microphone selector expected to be " + (aRequestAudio ? "visible" : "hidden"));
+
+ is(elt("webRTC-selectCamera").hidden, !aRequestVideo,
+ "camera selector expected to be " + (aRequestVideo ? "visible" : "hidden"));
+
+ let expectedMessage = aNever ? permissionError : "ok";
+ yield promiseMessage(expectedMessage, () => {
+ activateSecondaryAction(aNever ? kActionNever : kActionAlways);
+ });
+ let expected = [];
+ if (expectedMessage == "ok") {
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ if (aRequestVideo)
+ expected.push("Camera");
+ if (aRequestAudio)
+ expected.push("Microphone");
+ expected = expected.join("And");
+ }
+ else {
+ yield expectObserverCalled("getUserMedia:response:deny");
+ yield expectObserverCalled("recording-window-ended");
+ expected = "none";
+ }
+ is((yield getMediaCaptureState()), expected,
+ "expected " + expected + " to be shared");
+
+ function checkDevicePermissions(aDevice, aExpected) {
+ let Perms = Services.perms;
+ let uri = gBrowser.selectedBrowser.documentURI;
+ let devicePerms = Perms.testExactPermission(uri, aDevice);
+ if (aExpected === undefined)
+ is(devicePerms, Perms.UNKNOWN_ACTION, "no " + aDevice + " persistent permissions");
+ else {
+ is(devicePerms, aExpected ? Perms.ALLOW_ACTION : Perms.DENY_ACTION,
+ aDevice + " persistently " + (aExpected ? "allowed" : "denied"));
+ }
+ Perms.remove(uri, aDevice);
+ }
+ checkDevicePermissions("microphone", aExpectedAudioPerm);
+ checkDevicePermissions("camera", aExpectedVideoPerm);
+
+ if (expectedMessage == "ok")
+ yield closeStream();
+ }
+
+ // 3 cases where the user accepts the device prompt.
+ info("audio+video, user grants, expect both perms set to allow");
+ yield checkPerm(true, true, true, true);
+ info("audio only, user grants, check audio perm set to allow, video perm not set");
+ yield checkPerm(true, false, true, undefined);
+ info("video only, user grants, check video perm set to allow, audio perm not set");
+ yield checkPerm(false, true, undefined, true);
+
+ // 3 cases where the user rejects the device request by using 'Never Share'.
+ info("audio only, user denies, expect audio perm set to deny, video not set");
+ yield checkPerm(true, false, false, undefined, true);
+ info("video only, user denies, expect video perm set to deny, audio perm not set");
+ yield checkPerm(false, true, undefined, false, true);
+ info("audio+video, user denies, expect both perms set to deny");
+ yield checkPerm(true, true, false, false, true);
+ }
+},
+
+{
+ desc: "getUserMedia without prompt: use persistent permissions",
+ run: function* checkUsePersistentPermissions() {
+ function* usePerm(aAllowAudio, aAllowVideo, aRequestAudio, aRequestVideo,
+ aExpectStream) {
+ let Perms = Services.perms;
+ let uri = gBrowser.selectedBrowser.documentURI;
+
+ if (aAllowAudio !== undefined) {
+ Perms.add(uri, "microphone", aAllowAudio ? Perms.ALLOW_ACTION
+ : Perms.DENY_ACTION);
+ }
+ if (aAllowVideo !== undefined) {
+ Perms.add(uri, "camera", aAllowVideo ? Perms.ALLOW_ACTION
+ : Perms.DENY_ACTION);
+ }
+
+ if (aExpectStream === undefined) {
+ // Check that we get a prompt.
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(aRequestAudio, aRequestVideo);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+
+ // Deny the request to cleanup...
+ yield promiseMessage(permissionError, () => {
+ activateSecondaryAction(kActionDeny);
+ });
+ yield expectObserverCalled("getUserMedia:response:deny");
+ yield expectObserverCalled("recording-window-ended");
+ }
+ else {
+ let expectedMessage = aExpectStream ? "ok" : permissionError;
+ let promise = promiseMessage(expectedMessage);
+ yield promiseRequestDevice(aRequestAudio, aRequestVideo);
+ yield promise;
+
+ if (expectedMessage == "ok") {
+ yield expectObserverCalled("getUserMedia:request");
+ yield promiseNoPopupNotification("webRTC-shareDevices");
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+
+ // Check what's actually shared.
+ let expected = [];
+ if (aAllowVideo && aRequestVideo)
+ expected.push("Camera");
+ if (aAllowAudio && aRequestAudio)
+ expected.push("Microphone");
+ expected = expected.join("And");
+ is((yield getMediaCaptureState()), expected,
+ "expected " + expected + " to be shared");
+
+ yield closeStream();
+ }
+ else {
+ yield expectObserverCalled("recording-window-ended");
+ }
+ }
+
+ Perms.remove(uri, "camera");
+ Perms.remove(uri, "microphone");
+ }
+
+ // Set both permissions identically
+ info("allow audio+video, request audio+video, expect ok (audio+video)");
+ yield usePerm(true, true, true, true, true);
+ info("deny audio+video, request audio+video, expect denied");
+ yield usePerm(false, false, true, true, false);
+
+ // Allow audio, deny video.
+ info("allow audio, deny video, request audio+video, expect denied");
+ yield usePerm(true, false, true, true, false);
+ info("allow audio, deny video, request audio, expect ok (audio)");
+ yield usePerm(true, false, true, false, true);
+ info("allow audio, deny video, request video, expect denied");
+ yield usePerm(true, false, false, true, false);
+
+ // Deny audio, allow video.
+ info("deny audio, allow video, request audio+video, expect denied");
+ yield usePerm(false, true, true, true, false);
+ info("deny audio, allow video, request audio, expect denied");
+ yield usePerm(false, true, true, false, false);
+ info("deny audio, allow video, request video, expect ok (video)");
+ yield usePerm(false, true, false, true, true);
+
+ // Allow audio, video not set.
+ info("allow audio, request audio+video, expect prompt");
+ yield usePerm(true, undefined, true, true, undefined);
+ info("allow audio, request audio, expect ok (audio)");
+ yield usePerm(true, undefined, true, false, true);
+ info("allow audio, request video, expect prompt");
+ yield usePerm(true, undefined, false, true, undefined);
+
+ // Deny audio, video not set.
+ info("deny audio, request audio+video, expect denied");
+ yield usePerm(false, undefined, true, true, false);
+ info("deny audio, request audio, expect denied");
+ yield usePerm(false, undefined, true, false, false);
+ info("deny audio, request video, expect prompt");
+ yield usePerm(false, undefined, false, true, undefined);
+
+ // Allow video, audio not set.
+ info("allow video, request audio+video, expect prompt");
+ yield usePerm(undefined, true, true, true, undefined);
+ info("allow video, request audio, expect prompt");
+ yield usePerm(undefined, true, true, false, undefined);
+ info("allow video, request video, expect ok (video)");
+ yield usePerm(undefined, true, false, true, true);
+
+ // Deny video, audio not set.
+ info("deny video, request audio+video, expect denied");
+ yield usePerm(undefined, false, true, true, false);
+ info("deny video, request audio, expect prompt");
+ yield usePerm(undefined, false, true, false, undefined);
+ info("deny video, request video, expect denied");
+ yield usePerm(undefined, false, false, true, false);
+ }
+},
+
+{
+ desc: "Stop Sharing removes persistent permissions",
+ run: function* checkStopSharingRemovesPersistentPermissions() {
+ function* stopAndCheckPerm(aRequestAudio, aRequestVideo) {
+ let Perms = Services.perms;
+ let uri = gBrowser.selectedBrowser.documentURI;
+
+ // Initially set both permissions to 'allow'.
+ Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
+ Perms.add(uri, "camera", Perms.ALLOW_ACTION);
+
+ let indicator = promiseIndicatorWindow();
+ // Start sharing what's been requested.
+ let promise = promiseMessage("ok");
+ yield promiseRequestDevice(aRequestAudio, aRequestVideo);
+ yield promise;
+
+ yield expectObserverCalled("getUserMedia:request");
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ yield indicator;
+ yield checkSharingUI({video: aRequestVideo, audio: aRequestAudio});
+
+ yield stopSharing(aRequestVideo ? "camera" : "microphone");
+
+ // Check that permissions have been removed as expected.
+ let audioPerm = Perms.testExactPermission(uri, "microphone");
+ if (aRequestAudio)
+ is(audioPerm, Perms.UNKNOWN_ACTION, "microphone permissions removed");
+ else
+ is(audioPerm, Perms.ALLOW_ACTION, "microphone permissions untouched");
+
+ let videoPerm = Perms.testExactPermission(uri, "camera");
+ if (aRequestVideo)
+ is(videoPerm, Perms.UNKNOWN_ACTION, "camera permissions removed");
+ else
+ is(videoPerm, Perms.ALLOW_ACTION, "camera permissions untouched");
+
+ // Cleanup.
+ yield closeStream(true);
+
+ Perms.remove(uri, "camera");
+ Perms.remove(uri, "microphone");
+ }
+
+ info("request audio+video, stop sharing resets both");
+ yield stopAndCheckPerm(true, true);
+ info("request audio, stop sharing resets audio only");
+ yield stopAndCheckPerm(true, false);
+ info("request video, stop sharing resets video only");
+ yield stopAndCheckPerm(false, true);
+ }
+},
+
+{
+ desc: "test showControlCenter",
+ run: function* checkShowControlCenter() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(false, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(false, true);
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "Camera", "expected camera to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: true});
+
+ ok(gIdentityHandler._identityPopup.hidden, "control center should be hidden");
+ if ("nsISystemStatusBar" in Ci) {
+ let activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ webrtcUI.showSharingDoorhanger(activeStreams[0], "Devices");
+ }
+ else {
+ let win =
+ Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
+ let elt = win.document.getElementById("audioVideoButton");
+ EventUtils.synthesizeMouseAtCenter(elt, {}, win);
+ yield promiseWaitForCondition(() => !gIdentityHandler._identityPopup.hidden);
+ }
+ ok(!gIdentityHandler._identityPopup.hidden, "control center should be open");
+
+ gIdentityHandler._identityPopup.hidden = true;
+ yield expectNoObserverCalled();
+
+ yield closeStream();
+ }
+},
+
+{
+ desc: "'Always Allow' ignored and not shown on http pages",
+ run: function* checkNoAlwaysOnHttp() {
+ // Load an http page instead of the https version.
+ let browser = gBrowser.selectedBrowser;
+ browser.loadURI(browser.documentURI.spec.replace("https://", "http://"));
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ // Initially set both permissions to 'allow'.
+ let Perms = Services.perms;
+ let uri = browser.documentURI;
+ Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
+ Perms.add(uri, "camera", Perms.ALLOW_ACTION);
+
+ // Request devices and expect a prompt despite the saved 'Allow' permission,
+ // because the connection isn't secure.
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+
+ // Ensure that the 'Always Allow' action isn't shown.
+ let alwaysLabel = gNavigatorBundle.getString("getUserMedia.always.label");
+ ok(!!alwaysLabel, "found the 'Always Allow' localized label");
+ let labels = [];
+ let notification = PopupNotifications.panel.firstChild;
+ for (let node of notification.childNodes) {
+ if (node.localName == "menuitem")
+ labels.push(node.getAttribute("label"));
+ }
+ is(labels.indexOf(alwaysLabel), -1, "The 'Always Allow' item isn't shown");
+
+ // Cleanup.
+ yield closeStream(true);
+ Perms.remove(uri, "camera");
+ Perms.remove(uri, "microphone");
+ }
+}
+
+];
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = gBrowser.addTab();
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+
+ browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
+
+ browser.addEventListener("load", function onload() {
+ browser.removeEventListener("load", onload, true);
+
+ is(PopupNotifications._currentNotifications.length, 0,
+ "should start the test without any prior popup notification");
+ ok(gIdentityHandler._identityPopup.hidden,
+ "should start the test with the control center hidden");
+
+ Task.spawn(function* () {
+ yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]});
+
+ for (let test of gTests) {
+ info(test.desc);
+ yield test.run();
+
+ // Cleanup before the next test
+ yield expectNoObserverCalled();
+ }
+ }).then(finish, ex => {
+ Cu.reportError(ex);
+ ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+ }, true);
+ let rootDir = getRootDirectory(gTestPath);
+ rootDir = rootDir.replace("chrome://mochitests/content/",
+ "https://example.com/");
+ content.location = rootDir + "get_user_media.html";
+}
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
new file mode 100644
index 000000000..f407061a7
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+registerCleanupFunction(function() {
+ gBrowser.removeCurrentTab();
+});
+
+var gTests = [
+
+{
+ desc: "device sharing animation on background tabs",
+ run: function* checkAudioVideo() {
+ function* getStreamAndCheckBackgroundAnim(aAudio, aVideo, aSharing) {
+ // Get a stream
+ let popupPromise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(aAudio, aVideo);
+ yield popupPromise;
+ yield expectObserverCalled("getUserMedia:request");
+
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ let expected = [];
+ if (aVideo)
+ expected.push("Camera");
+ if (aAudio)
+ expected.push("Microphone");
+ is((yield getMediaCaptureState()), expected.join("And"),
+ "expected stream to be shared");
+
+ // Check the attribute on the tab, and check there's no visible
+ // sharing icon on the tab
+ let tab = gBrowser.selectedTab;
+ is(tab.getAttribute("sharing"), aSharing,
+ "the tab has the attribute to show the " + aSharing + " icon");
+ let icon =
+ document.getAnonymousElementByAttribute(tab, "anonid", "sharing-icon");
+ is(window.getComputedStyle(icon).display, "none",
+ "the animated sharing icon of the tab is hidden");
+
+ // After selecting a new tab, check the attribute is still there,
+ // and the icon is now visible.
+ yield BrowserTestUtils.switchTab(gBrowser, gBrowser.addTab());
+ is(gBrowser.selectedTab.getAttribute("sharing"), "",
+ "the new tab doesn't have the 'sharing' attribute");
+ is(tab.getAttribute("sharing"), aSharing,
+ "the tab still has the 'sharing' attribute");
+ isnot(window.getComputedStyle(icon).display, "none",
+ "the animated sharing icon of the tab is now visible");
+
+ // Ensure the icon disappears when selecting the tab.
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ ok(tab.selected, "the tab with ongoing sharing is selected again");
+ is(window.getComputedStyle(icon).display, "none",
+ "the animated sharing icon is gone after selecting the tab again");
+
+ // And finally verify the attribute is removed when closing the stream.
+ yield closeStream();
+
+ // TODO(Bug 1304997): Fix the race in closeStream() and remove this
+ // promiseWaitForCondition().
+ yield promiseWaitForCondition(() => !tab.getAttribute("sharing"));
+ is(tab.getAttribute("sharing"), "",
+ "the tab no longer has the 'sharing' attribute after closing the stream");
+ }
+
+ yield getStreamAndCheckBackgroundAnim(true, true, "camera");
+ yield getStreamAndCheckBackgroundAnim(false, true, "camera");
+ yield getStreamAndCheckBackgroundAnim(true, false, "microphone");
+ }
+}
+
+];
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = gBrowser.addTab();
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+
+ browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
+
+ browser.addEventListener("load", function onload() {
+ browser.removeEventListener("load", onload, true);
+
+ is(PopupNotifications._currentNotifications.length, 0,
+ "should start the test without any prior popup notification");
+
+ Task.spawn(function* () {
+ yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]});
+
+ for (let test of gTests) {
+ info(test.desc);
+ yield test.run();
+ }
+ }).then(finish, ex => {
+ Cu.reportError(ex);
+ ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+ }, true);
+ let rootDir = getRootDirectory(gTestPath);
+ rootDir = rootDir.replace("chrome://mochitests/content/",
+ "https://example.com/");
+ content.location = rootDir + "get_user_media.html";
+}
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
new file mode 100644
index 000000000..01a544aae
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+registerCleanupFunction(function() {
+ gBrowser.removeCurrentTab();
+});
+
+function promiseReloadFrame(aFrameId) {
+ return ContentTask.spawn(gBrowser.selectedBrowser, aFrameId, function*(aFrameId) {
+ content.wrappedJSObject.document.getElementById(aFrameId).contentWindow.location.reload();
+ });
+}
+
+var gTests = [
+
+{
+ desc: "getUserMedia audio+video",
+ run: function* checkAudioVideo() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true, "frame1");
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+
+ is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
+ "webRTC-shareDevices-notification-icon", "anchored to device icon");
+ checkDeviceSelectors(true, true);
+ is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
+ "webRTC-shareDevices", "panel using devices icon");
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({audio: true, video: true});
+ yield closeStream(false, "frame1");
+ }
+},
+
+{
+ desc: "getUserMedia audio+video: stop sharing",
+ run: function* checkStopSharing() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true, "frame1");
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ activateSecondaryAction(kActionAlways);
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: true, audio: true});
+
+ let Perms = Services.perms;
+ let uri = Services.io.newURI("https://example.com/", null, null);
+ is(Perms.testExactPermission(uri, "microphone"), Perms.ALLOW_ACTION,
+ "microphone persistently allowed");
+ is(Perms.testExactPermission(uri, "camera"), Perms.ALLOW_ACTION,
+ "camera persistently allowed");
+
+ yield stopSharing();
+
+ // The persistent permissions for the frame should have been removed.
+ is(Perms.testExactPermission(uri, "microphone"), Perms.UNKNOWN_ACTION,
+ "microphone not persistently allowed");
+ is(Perms.testExactPermission(uri, "camera"), Perms.UNKNOWN_ACTION,
+ "camera not persistently allowed");
+
+ // the stream is already closed, but this will do some cleanup anyway
+ yield closeStream(true, "frame1");
+ }
+},
+
+{
+ desc: "getUserMedia audio+video: reloading the frame removes all sharing UI",
+ run: function* checkReloading() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true, "frame1");
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: true, audio: true});
+
+ info("reloading the frame");
+ promise = promiseObserverCalled("recording-device-events");
+ yield promiseReloadFrame("frame1");
+ yield promise;
+
+ yield expectObserverCalled("recording-window-ended");
+ yield expectNoObserverCalled();
+ yield checkNotSharing();
+ }
+},
+
+{
+ desc: "getUserMedia audio+video: reloading the frame removes prompts",
+ run: function* checkReloadingRemovesPrompts() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true, "frame1");
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ info("reloading the frame");
+ promise = promiseObserverCalled("recording-window-ended");
+ yield promiseReloadFrame("frame1");
+ yield promise;
+ yield promiseNoPopupNotification("webRTC-shareDevices");
+
+ yield expectNoObserverCalled();
+ yield checkNotSharing();
+ }
+},
+
+{
+ desc: "getUserMedia audio+video: reloading a frame updates the sharing UI",
+ run: function* checkUpdateWhenReloading() {
+ // We'll share only the mic in the first frame, then share both in the
+ // second frame, then reload the second frame. After each step, we'll check
+ // the UI is in the correct state.
+
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, false, "frame1");
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, false);
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "Microphone", "microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: false, audio: true});
+ yield expectNoObserverCalled();
+
+ promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true, "frame2");
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield checkSharingUI({video: true, audio: true});
+ yield expectNoObserverCalled();
+
+ info("reloading the second frame");
+ promise = promiseObserverCalled("recording-device-events");
+ yield promiseReloadFrame("frame2");
+ yield promise;
+
+ yield expectObserverCalled("recording-window-ended");
+ yield checkSharingUI({video: false, audio: true});
+ yield expectNoObserverCalled();
+
+ yield closeStream(false, "frame1");
+ yield expectNoObserverCalled();
+ yield checkNotSharing();
+ }
+},
+
+{
+ desc: "getUserMedia audio+video: reloading the top level page removes all sharing UI",
+ run: function* checkReloading() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true, "frame1");
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: true, audio: true});
+
+ info("reloading the web page");
+ promise = promiseObserverCalled("recording-device-events");
+ content.location.reload();
+ yield promise;
+
+ yield expectObserverCalled("recording-window-ended");
+ yield expectNoObserverCalled();
+ yield checkNotSharing();
+ }
+}
+
+];
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = gBrowser.addTab();
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+
+ browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
+
+ browser.addEventListener("load", function onload() {
+ browser.removeEventListener("load", onload, true);
+
+ is(PopupNotifications._currentNotifications.length, 0,
+ "should start the test without any prior popup notification");
+
+ Task.spawn(function* () {
+ yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]});
+
+ for (let test of gTests) {
+ info(test.desc);
+ yield test.run();
+
+ // Cleanup before the next test
+ yield expectNoObserverCalled();
+ }
+ }).then(finish, ex => {
+ Cu.reportError(ex);
+ ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+ }, true);
+ let rootDir = getRootDirectory(gTestPath);
+ rootDir = rootDir.replace("chrome://mochitests/content/",
+ "https://example.com/");
+ let url = rootDir + "get_user_media.html";
+ content.location = 'data:text/html,<iframe id="frame1" src="' + url + '"></iframe><iframe id="frame2" src="' + url + '"></iframe>'
+}
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
new file mode 100644
index 000000000..b19065371
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+registerCleanupFunction(function() {
+ gBrowser.removeCurrentTab();
+});
+
+var gTests = [
+
+{
+ desc: "getUserMedia: tearing-off a tab keeps sharing indicators",
+ run: function* checkTearingOff() {
+ let promise = promisePopupNotificationShown("webRTC-shareDevices");
+ yield promiseRequestDevice(true, true);
+ yield promise;
+ yield expectObserverCalled("getUserMedia:request");
+ checkDeviceSelectors(true, true);
+
+ let indicator = promiseIndicatorWindow();
+ yield promiseMessage("ok", () => {
+ PopupNotifications.panel.firstChild.button.click();
+ });
+ yield expectObserverCalled("getUserMedia:response:allow");
+ yield expectObserverCalled("recording-device-events");
+ is((yield getMediaCaptureState()), "CameraAndMicrophone",
+ "expected camera and microphone to be shared");
+
+ yield indicator;
+ yield checkSharingUI({video: true, audio: true});
+
+ info("tearing off the tab");
+ let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ yield whenDelayedStartupFinished(win);
+ yield checkSharingUI({audio: true, video: true}, win);
+
+ // Clicking the global sharing indicator should open the control center in
+ // the second window.
+ ok(win.gIdentityHandler._identityPopup.hidden, "control center should be hidden");
+ let activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ webrtcUI.showSharingDoorhanger(activeStreams[0], "Devices");
+ ok(!win.gIdentityHandler._identityPopup.hidden,
+ "control center should be open in the second window");
+ ok(gIdentityHandler._identityPopup.hidden,
+ "control center should be hidden in the first window");
+ win.gIdentityHandler._identityPopup.hidden = true;
+
+ // Closing the new window should remove all sharing indicators.
+ // We need to load the content script in the first window so that we can
+ // catch the notifications fired globally when closing the second window.
+ gBrowser.selectedBrowser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
+
+ let promises = [promiseObserverCalled("recording-device-events"),
+ promiseObserverCalled("recording-window-ended")];
+ yield BrowserTestUtils.closeWindow(win);
+ yield Promise.all(promises);
+
+ yield expectNoObserverCalled();
+ yield checkNotSharing();
+ }
+}
+
+];
+
+function test() {
+ waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 1]]}, runTest);
+}
+
+function runTest() {
+ // An empty tab where we can load the content script without leaving it
+ // behind at the end of the test.
+ gBrowser.addTab();
+
+ let tab = gBrowser.addTab();
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+
+ browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
+
+ browser.addEventListener("load", function onload() {
+ browser.removeEventListener("load", onload, true);
+
+ is(PopupNotifications._currentNotifications.length, 0,
+ "should start the test without any prior popup notification");
+ ok(gIdentityHandler._identityPopup.hidden,
+ "should start the test with the control center hidden");
+
+ Task.spawn(function* () {
+ yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]});
+
+ for (let test of gTests) {
+ info(test.desc);
+ yield test.run();
+
+ // Cleanup before the next test
+ yield expectNoObserverCalled();
+ }
+ }).then(finish, ex => {
+ Cu.reportError(ex);
+ ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+ }, true);
+ let rootDir = getRootDirectory(gTestPath);
+ rootDir = rootDir.replace("chrome://mochitests/content/",
+ "https://example.com/");
+ content.location = rootDir + "get_user_media.html";
+}
diff --git a/browser/base/content/test/webrtc/get_user_media.html b/browser/base/content/test/webrtc/get_user_media.html
new file mode 100644
index 000000000..16303c62d
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+// Specifies whether we are using fake streams to run this automation
+var useFakeStreams = true;
+try {
+ var audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev");
+ var videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev");
+ dump("TEST DEVICES: Using media devices:\n");
+ dump("audio: " + audioDevice + "\nvideo: " + videoDevice + "\n");
+ useFakeStreams = false;
+} catch (e) {
+ dump("TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n");
+ useFakeStreams = true;
+}
+
+function message(m) {
+ document.getElementById("message").innerHTML = m;
+ window.parent.postMessage(m, "*");
+}
+
+var gStream;
+
+function requestDevice(aAudio, aVideo, aShare) {
+ var opts = {video: aVideo, audio: aAudio};
+ if (aShare) {
+ opts.video = {
+ mozMediaSource: aShare,
+ mediaSource: aShare
+ }
+ } else if (useFakeStreams) {
+ opts.fake = true;
+ }
+
+ window.navigator.mediaDevices.getUserMedia(opts)
+ .then(stream => {
+ gStream = stream;
+ message("ok");
+ }, err => message("error: " + err));
+}
+message("pending");
+
+function closeStream() {
+ if (!gStream)
+ return;
+ gStream.getTracks().forEach(t => t.stop());
+ gStream = null;
+ message("closed");
+}
+</script>
+</body>
+</html>
diff --git a/browser/base/content/test/webrtc/get_user_media_content_script.js b/browser/base/content/test/webrtc/get_user_media_content_script.js
new file mode 100644
index 000000000..71b68d826
--- /dev/null
+++ b/browser/base/content/test/webrtc/get_user_media_content_script.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
+ "@mozilla.org/mediaManagerService;1",
+ "nsIMediaManagerService");
+
+const kObservedTopics = [
+ "getUserMedia:response:allow",
+ "getUserMedia:revoke",
+ "getUserMedia:response:deny",
+ "getUserMedia:request",
+ "recording-device-events",
+ "recording-window-ended"
+];
+
+var gObservedTopics = {};
+function observer(aSubject, aTopic, aData) {
+ if (!(aTopic in gObservedTopics))
+ gObservedTopics[aTopic] = 1;
+ else
+ ++gObservedTopics[aTopic];
+}
+
+kObservedTopics.forEach(topic => {
+ Services.obs.addObserver(observer, topic, false);
+});
+
+addMessageListener("Test:ExpectObserverCalled", ({data}) => {
+ sendAsyncMessage("Test:ExpectObserverCalled:Reply",
+ {count: gObservedTopics[data]});
+ if (data in gObservedTopics)
+ --gObservedTopics[data];
+});
+
+addMessageListener("Test:ExpectNoObserverCalled", data => {
+ sendAsyncMessage("Test:ExpectNoObserverCalled:Reply", gObservedTopics);
+ gObservedTopics = {};
+});
+
+function _getMediaCaptureState() {
+ let hasVideo = {};
+ let hasAudio = {};
+ let hasScreenShare = {};
+ let hasWindowShare = {};
+ MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio,
+ hasScreenShare, hasWindowShare);
+ if (hasVideo.value && hasAudio.value)
+ return "CameraAndMicrophone";
+ if (hasVideo.value)
+ return "Camera";
+ if (hasAudio.value)
+ return "Microphone";
+ if (hasScreenShare.value)
+ return "Screen";
+ if (hasWindowShare.value)
+ return "Window";
+ return "none";
+}
+
+addMessageListener("Test:GetMediaCaptureState", data => {
+ sendAsyncMessage("Test:MediaCaptureState", _getMediaCaptureState());
+});
+
+addMessageListener("Test:WaitForObserverCall", ({data}) => {
+ let topic = data;
+ Services.obs.addObserver(function observer() {
+ sendAsyncMessage("Test:ObserverCalled", topic);
+ Services.obs.removeObserver(observer, topic);
+
+ if (kObservedTopics.indexOf(topic) != -1) {
+ if (!(topic in gObservedTopics))
+ gObservedTopics[topic] = -1;
+ else
+ --gObservedTopics[topic];
+ }
+ }, topic, false);
+});
+
+addMessageListener("Test:WaitForMessage", () => {
+ content.addEventListener("message", ({data}) => {
+ sendAsyncMessage("Test:MessageReceived", data);
+ }, {once: true});
+});
diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js
new file mode 100644
index 000000000..70b183773
--- /dev/null
+++ b/browser/base/content/test/webrtc/head.js
@@ -0,0 +1,453 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
+const CONTENT_SCRIPT_HELPER = getRootDirectory(gTestPath) + "get_user_media_content_script.js";
+
+function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
+ retryTimes = typeof retryTimes !== 'undefined' ? retryTimes : 30;
+ var tries = 0;
+ var interval = setInterval(function() {
+ if (tries >= retryTimes) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = function() { clearInterval(interval); nextTest(); };
+}
+
+function promiseWaitForCondition(aConditionFn) {
+ let deferred = Promise.defer();
+ waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass.");
+ return deferred.promise;
+}
+
+/**
+ * Waits for a window with the given URL to exist.
+ *
+ * @param url
+ * The url of the window.
+ * @return {Promise} resolved when the window exists.
+ * @resolves to the window
+ */
+function promiseWindow(url) {
+ info("expecting a " + url + " window");
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(win) {
+ win.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function loadHandler() {
+ win.removeEventListener("load", loadHandler);
+
+ if (win.location.href !== url) {
+ info("ignoring a window with this url: " + win.location.href);
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "domwindowopened");
+ resolve(win);
+ });
+ }, "domwindowopened", false);
+ });
+}
+
+function whenDelayedStartupFinished(aWindow) {
+ return new Promise(resolve => {
+ info("Waiting for delayed startup to finish");
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ resolve();
+ }
+ }, "browser-delayed-startup-finished", false);
+ });
+}
+
+function promiseIndicatorWindow() {
+ // We don't show the indicator window on Mac.
+ if ("nsISystemStatusBar" in Ci)
+ return Promise.resolve();
+
+ return promiseWindow("chrome://browser/content/webrtcIndicator.xul");
+}
+
+function* assertWebRTCIndicatorStatus(expected) {
+ let ui = Cu.import("resource:///modules/webrtcUI.jsm", {}).webrtcUI;
+ let expectedState = expected ? "visible" : "hidden";
+ let msg = "WebRTC indicator " + expectedState;
+ if (!expected && ui.showGlobalIndicator) {
+ // It seems the global indicator is not always removed synchronously
+ // in some cases.
+ info("waiting for the global indicator to be hidden");
+ yield promiseWaitForCondition(() => !ui.showGlobalIndicator);
+ }
+ is(ui.showGlobalIndicator, !!expected, msg);
+
+ let expectVideo = false, expectAudio = false, expectScreen = false;
+ if (expected) {
+ if (expected.video)
+ expectVideo = true;
+ if (expected.audio)
+ expectAudio = true;
+ if (expected.screen)
+ expectScreen = true;
+ }
+ is(ui.showCameraIndicator, expectVideo, "camera global indicator as expected");
+ is(ui.showMicrophoneIndicator, expectAudio, "microphone global indicator as expected");
+ is(ui.showScreenSharingIndicator, expectScreen, "screen global indicator as expected");
+
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ let menu = win.document.getElementById("tabSharingMenu");
+ is(menu && !menu.hidden, !!expected, "WebRTC menu should be " + expectedState);
+ }
+
+ if (!("nsISystemStatusBar" in Ci)) {
+ if (!expected) {
+ let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator");
+ if (win) {
+ yield new Promise((resolve, reject) => {
+ win.addEventListener("unload", function listener(e) {
+ if (e.target == win.document) {
+ win.removeEventListener("unload", listener);
+ resolve();
+ }
+ }, false);
+ });
+ }
+ }
+ let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator");
+ let hasWindow = indicator.hasMoreElements();
+ is(hasWindow, !!expected, "popup " + msg);
+ if (hasWindow) {
+ let document = indicator.getNext().document;
+ let docElt = document.documentElement;
+
+ if (document.readyState != "complete") {
+ info("Waiting for the sharing indicator's document to load");
+ let deferred = Promise.defer();
+ document.addEventListener("readystatechange",
+ function onReadyStateChange() {
+ if (document.readyState != "complete")
+ return;
+ document.removeEventListener("readystatechange", onReadyStateChange);
+ deferred.resolve();
+ });
+ yield deferred.promise;
+ }
+
+ for (let item of ["video", "audio", "screen"]) {
+ let expectedValue = (expected && expected[item]) ? "true" : "";
+ is(docElt.getAttribute("sharing" + item), expectedValue,
+ item + " global indicator attribute as expected");
+ }
+
+ ok(!indicator.hasMoreElements(), "only one global indicator window");
+ }
+ }
+}
+
+function promisePopupEvent(popup, eventSuffix) {
+ let endState = {shown: "open", hidden: "closed"}[eventSuffix];
+
+ if (popup.state == endState)
+ return Promise.resolve();
+
+ let eventType = "popup" + eventSuffix;
+ let deferred = Promise.defer();
+ popup.addEventListener(eventType, function onPopupShown(event) {
+ popup.removeEventListener(eventType, onPopupShown);
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function promiseNotificationShown(notification) {
+ let win = notification.browser.ownerGlobal;
+ if (win.PopupNotifications.panel.state == "open") {
+ return Promise.resolve();
+ }
+ let panelPromise = promisePopupEvent(win.PopupNotifications.panel, "shown");
+ notification.reshow();
+ return panelPromise;
+}
+
+function _mm() {
+ return gBrowser.selectedBrowser.messageManager;
+}
+
+function promiseObserverCalled(aTopic) {
+ return new Promise(resolve => {
+ let mm = _mm();
+ mm.addMessageListener("Test:ObserverCalled", function listener({data}) {
+ if (data == aTopic) {
+ ok(true, "got " + aTopic + " notification");
+ mm.removeMessageListener("Test:ObserverCalled", listener);
+ resolve();
+ }
+ });
+ mm.sendAsyncMessage("Test:WaitForObserverCall", aTopic);
+ });
+}
+
+function expectObserverCalled(aTopic) {
+ return new Promise(resolve => {
+ let mm = _mm();
+ mm.addMessageListener("Test:ExpectObserverCalled:Reply",
+ function listener({data}) {
+ is(data.count, 1, "expected notification " + aTopic);
+ mm.removeMessageListener("Test:ExpectObserverCalled:Reply", listener);
+ resolve();
+ });
+ mm.sendAsyncMessage("Test:ExpectObserverCalled", aTopic);
+ });
+}
+
+function expectNoObserverCalled() {
+ return new Promise(resolve => {
+ let mm = _mm();
+ mm.addMessageListener("Test:ExpectNoObserverCalled:Reply",
+ function listener({data}) {
+ mm.removeMessageListener("Test:ExpectNoObserverCalled:Reply", listener);
+ for (let topic in data) {
+ if (data[topic])
+ is(data[topic], 0, topic + " notification unexpected");
+ }
+ resolve();
+ });
+ mm.sendAsyncMessage("Test:ExpectNoObserverCalled");
+ });
+}
+
+function promiseMessage(aMessage, aAction) {
+ let promise = new Promise((resolve, reject) => {
+ let mm = _mm();
+ mm.addMessageListener("Test:MessageReceived", function listener({data}) {
+ is(data, aMessage, "received " + aMessage);
+ if (data == aMessage)
+ resolve();
+ else
+ reject();
+ mm.removeMessageListener("Test:MessageReceived", listener);
+ });
+ mm.sendAsyncMessage("Test:WaitForMessage");
+ });
+
+ if (aAction)
+ aAction();
+
+ return promise;
+}
+
+function promisePopupNotificationShown(aName, aAction) {
+ let deferred = Promise.defer();
+
+ PopupNotifications.panel.addEventListener("popupshown", function popupNotifShown() {
+ PopupNotifications.panel.removeEventListener("popupshown", popupNotifShown);
+
+ ok(!!PopupNotifications.getNotification(aName), aName + " notification shown");
+ ok(PopupNotifications.isPanelOpen, "notification panel open");
+ ok(!!PopupNotifications.panel.firstChild, "notification panel populated");
+
+ deferred.resolve();
+ });
+
+ if (aAction)
+ aAction();
+
+ return deferred.promise;
+}
+
+function promisePopupNotification(aName) {
+ let deferred = Promise.defer();
+
+ waitForCondition(() => PopupNotifications.getNotification(aName),
+ () => {
+ ok(!!PopupNotifications.getNotification(aName),
+ aName + " notification appeared");
+
+ deferred.resolve();
+ }, "timeout waiting for popup notification " + aName);
+
+ return deferred.promise;
+}
+
+function promiseNoPopupNotification(aName) {
+ let deferred = Promise.defer();
+
+ waitForCondition(() => !PopupNotifications.getNotification(aName),
+ () => {
+ ok(!PopupNotifications.getNotification(aName),
+ aName + " notification removed");
+ deferred.resolve();
+ }, "timeout waiting for popup notification " + aName + " to disappear");
+
+ return deferred.promise;
+}
+
+const kActionAlways = 1;
+const kActionDeny = 2;
+const kActionNever = 3;
+
+function activateSecondaryAction(aAction) {
+ let notification = PopupNotifications.panel.firstChild;
+ notification.button.focus();
+ let popup = notification.menupopup;
+ popup.addEventListener("popupshown", function () {
+ popup.removeEventListener("popupshown", arguments.callee, false);
+
+ // Press 'down' as many time as needed to select the requested action.
+ while (aAction--)
+ EventUtils.synthesizeKey("VK_DOWN", {});
+
+ // Activate
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ }, false);
+
+ // One down event to open the popup
+ EventUtils.synthesizeKey("VK_DOWN",
+ { altKey: !navigator.platform.includes("Mac") });
+}
+
+function getMediaCaptureState() {
+ return new Promise(resolve => {
+ let mm = _mm();
+ mm.addMessageListener("Test:MediaCaptureState", ({data}) => {
+ resolve(data);
+ });
+ mm.sendAsyncMessage("Test:GetMediaCaptureState");
+ });
+}
+
+function* stopSharing(aType = "camera") {
+ let promiseRecordingEvent = promiseObserverCalled("recording-device-events");
+ gIdentityHandler._identityBox.click();
+ let permissions = document.getElementById("identity-popup-permission-list");
+ let cancelButton =
+ permissions.querySelector(".identity-popup-permission-icon." + aType + "-icon ~ " +
+ ".identity-popup-permission-remove-button");
+ cancelButton.click();
+ gIdentityHandler._identityPopup.hidden = true;
+ yield promiseRecordingEvent;
+ yield expectObserverCalled("getUserMedia:revoke");
+ yield expectObserverCalled("recording-window-ended");
+ yield expectNoObserverCalled();
+ yield* checkNotSharing();
+}
+
+function promiseRequestDevice(aRequestAudio, aRequestVideo, aFrameId, aType) {
+ info("requesting devices");
+ return ContentTask.spawn(gBrowser.selectedBrowser,
+ {aRequestAudio, aRequestVideo, aFrameId, aType},
+ function*(args) {
+ let global = content.wrappedJSObject;
+ if (args.aFrameId)
+ global = global.document.getElementById(args.aFrameId).contentWindow;
+ global.requestDevice(args.aRequestAudio, args.aRequestVideo, args.aType);
+ });
+}
+
+function* closeStream(aAlreadyClosed, aFrameId) {
+ yield expectNoObserverCalled();
+
+ let promises;
+ if (!aAlreadyClosed) {
+ promises = [promiseObserverCalled("recording-device-events"),
+ promiseObserverCalled("recording-window-ended")];
+ }
+
+ info("closing the stream");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, aFrameId, function*(aFrameId) {
+ let global = content.wrappedJSObject;
+ if (aFrameId)
+ global = global.document.getElementById(aFrameId).contentWindow;
+ global.closeStream();
+ });
+
+ if (promises)
+ yield Promise.all(promises);
+
+ yield* assertWebRTCIndicatorStatus(null);
+}
+
+function checkDeviceSelectors(aAudio, aVideo) {
+ let micSelector = document.getElementById("webRTC-selectMicrophone");
+ if (aAudio)
+ ok(!micSelector.hidden, "microphone selector visible");
+ else
+ ok(micSelector.hidden, "microphone selector hidden");
+
+ let cameraSelector = document.getElementById("webRTC-selectCamera");
+ if (aVideo)
+ ok(!cameraSelector.hidden, "camera selector visible");
+ else
+ ok(cameraSelector.hidden, "camera selector hidden");
+}
+
+function* checkSharingUI(aExpected, aWin = window) {
+ let doc = aWin.document;
+ // First check the icon above the control center (i) icon.
+ let identityBox = doc.getElementById("identity-box");
+ ok(identityBox.hasAttribute("sharing"), "sharing attribute is set");
+ let sharing = identityBox.getAttribute("sharing");
+ if (aExpected.video)
+ is(sharing, "camera", "showing camera icon on the control center icon");
+ else if (aExpected.audio)
+ is(sharing, "microphone", "showing mic icon on the control center icon");
+
+ // Then check the sharing indicators inside the control center panel.
+ identityBox.click();
+ let permissions = doc.getElementById("identity-popup-permission-list");
+ for (let id of ["microphone", "camera", "screen"]) {
+ let convertId = id => {
+ if (id == "camera")
+ return "video";
+ if (id == "microphone")
+ return "audio";
+ return id;
+ };
+ let expected = aExpected[convertId(id)];
+ is(!!aWin.gIdentityHandler._sharingState[id], !!expected,
+ "sharing state for " + id + " as expected");
+ let icon = permissions.querySelectorAll(
+ ".identity-popup-permission-icon." + id + "-icon");
+ if (expected) {
+ is(icon.length, 1, "should show " + id + " icon in control center panel");
+ ok(icon[0].classList.contains("in-use"), "icon should have the in-use class");
+ } else if (!icon.length) {
+ ok(true, "should not show " + id + " icon in the control center panel");
+ } else {
+ // This will happen if there are persistent permissions set.
+ ok(!icon[0].classList.contains("in-use"),
+ "if shown, the " + id + " icon should not have the in-use class");
+ is(icon.length, 1, "should not show more than 1 " + id + " icon");
+ }
+ }
+ aWin.gIdentityHandler._identityPopup.hidden = true;
+
+ // Check the global indicators.
+ yield* assertWebRTCIndicatorStatus(aExpected);
+}
+
+function* checkNotSharing() {
+ is((yield getMediaCaptureState()), "none", "expected nothing to be shared");
+
+ ok(!document.getElementById("identity-box").hasAttribute("sharing"),
+ "no sharing indicator on the control center icon");
+
+ yield* assertWebRTCIndicatorStatus(null);
+}
diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml
new file mode 100644
index 000000000..84ed693ff
--- /dev/null
+++ b/browser/base/content/urlbarBindings.xml
@@ -0,0 +1,2740 @@
+<?xml version="1.0"?>
+
+<!--
+-*- Mode: HTML -*-
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<!DOCTYPE bindings [
+<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
+%notificationDTD;
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+%browserDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+]>
+
+<bindings id="urlbarBindings" xmlns="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"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
+
+ <content sizetopopup="pref">
+ <xul:hbox anonid="textbox-container"
+ class="autocomplete-textbox-container urlbar-textbox-container"
+ flex="1" xbl:inherits="focused">
+ <children includes="image|deck|stack|box">
+ <xul:image class="autocomplete-icon" allowevents="true"/>
+ </children>
+ <xul:hbox anonid="textbox-input-box"
+ class="textbox-input-box urlbar-input-box"
+ flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
+ <children/>
+ <html:input anonid="input"
+ class="autocomplete-textbox urlbar-input textbox-input uri-element-right-align"
+ allowevents="true"
+ inputmode="url"
+ xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/>
+ </xul:hbox>
+ <xul:dropmarker anonid="historydropmarker"
+ class="autocomplete-history-dropmarker urlbar-history-dropmarker"
+ tooltiptext="&urlbar.openHistoryPopup.tooltip;"
+ allowevents="true"
+ xbl:inherits="open,enablehistory,parentfocused=focused"/>
+ <children includes="hbox"/>
+ </xul:hbox>
+ <xul:popupset anonid="popupset"
+ class="autocomplete-result-popupset"/>
+ <children includes="toolbarbutton"/>
+ </content>
+
+ <implementation implements="nsIObserver, nsIDOMEventListener">
+ <field name="AppConstants" readonly="true">
+ (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
+ </field>
+
+ <field name="ExtensionSearchHandler" readonly="true">
+ (Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
+ </field>
+
+ <constructor><![CDATA[
+ this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService)
+ .getBranch("browser.urlbar.");
+
+ this._prefs.addObserver("", this, false);
+ this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll");
+ this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll");
+ this.completeDefaultIndex = this._prefs.getBoolPref("autoFill");
+ this.timeout = this._prefs.getIntPref("delay");
+ this._formattingEnabled = this._prefs.getBoolPref("formatting.enabled");
+ this._mayTrimURLs = this._prefs.getBoolPref("trimURLs");
+ this._cacheUserMadeSearchSuggestionsChoice();
+ this.inputField.controllers.insertControllerAt(0, this._copyCutController);
+ this.inputField.addEventListener("paste", this, false);
+ this.inputField.addEventListener("mousedown", this, false);
+ this.inputField.addEventListener("mousemove", this, false);
+ this.inputField.addEventListener("mouseout", this, false);
+ this.inputField.addEventListener("overflow", this, false);
+ this.inputField.addEventListener("underflow", this, false);
+
+ var textBox = document.getAnonymousElementByAttribute(this,
+ "anonid", "textbox-input-box");
+ var cxmenu = document.getAnonymousElementByAttribute(textBox,
+ "anonid", "input-box-contextmenu");
+ var pasteAndGo;
+ cxmenu.addEventListener("popupshowing", function() {
+ if (!pasteAndGo)
+ return;
+ var controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
+ var enabled = controller.isCommandEnabled("cmd_paste");
+ if (enabled)
+ pasteAndGo.removeAttribute("disabled");
+ else
+ pasteAndGo.setAttribute("disabled", "true");
+ }, false);
+
+ var insertLocation = cxmenu.firstChild;
+ while (insertLocation.nextSibling &&
+ insertLocation.getAttribute("cmd") != "cmd_paste")
+ insertLocation = insertLocation.nextSibling;
+ if (insertLocation) {
+ pasteAndGo = document.createElement("menuitem");
+ let label = Services.strings.createBundle("chrome://browser/locale/browser.properties").
+ GetStringFromName("pasteAndGo.label");
+ pasteAndGo.setAttribute("label", label);
+ pasteAndGo.setAttribute("anonid", "paste-and-go");
+ pasteAndGo.setAttribute("oncommand",
+ "gURLBar.select(); goDoCommand('cmd_paste'); gURLBar.handleCommand();");
+ cxmenu.insertBefore(pasteAndGo, insertLocation.nextSibling);
+ }
+
+ this._enableOrDisableOneOffSearches();
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this._prefs.removeObserver("", this);
+ this._prefs = null;
+ this.inputField.controllers.removeController(this._copyCutController);
+ this.inputField.removeEventListener("paste", this, false);
+ this.inputField.removeEventListener("mousedown", this, false);
+ this.inputField.removeEventListener("mousemove", this, false);
+ this.inputField.removeEventListener("mouseout", this, false);
+ this.inputField.removeEventListener("overflow", this, false);
+ this.inputField.removeEventListener("underflow", this, false);
+ ]]></destructor>
+
+ <field name="_value">""</field>
+ <field name="gotResultForCurrentQuery">false</field>
+
+ <!--
+ This is set around HandleHenter so it can be used in handleCommand.
+ It is also used to track whether we must handle a delayed handleEnter,
+ by checking if it has been cleared.
+ -->
+ <field name="handleEnterInstance">null</field>
+
+ <!--
+ For performance reasons we want to limit the size of the text runs we
+ build and show to the user.
+ -->
+ <field name="textRunsMaxLen">255</field>
+
+ <!--
+ onBeforeValueGet is called by the base-binding's .value getter.
+ It can return an object with a "value" property, to override the
+ return value of the getter.
+ -->
+ <method name="onBeforeValueGet">
+ <body><![CDATA[
+ return { value: this._value };
+ ]]></body>
+ </method>
+
+ <!--
+ onBeforeValueSet is called by the base-binding's .value setter.
+ It should return the value that the setter should use.
+ -->
+ <method name="onBeforeValueSet">
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ this._value = aValue;
+ var returnValue = aValue;
+ var action = this._parseActionUrl(aValue);
+
+ if (action) {
+ switch (action.type) {
+ case "switchtab": // Fall through.
+ case "remotetab": // Fall through.
+ case "visiturl": {
+ returnValue = action.params.displayUrl;
+ break;
+ }
+ case "keyword": // Fall through.
+ case "searchengine": {
+ returnValue = action.params.input;
+ break;
+ }
+ case "extension": {
+ returnValue = action.params.content;
+ break;
+ }
+ }
+ } else {
+ let originalUrl = ReaderMode.getOriginalUrl(aValue);
+ if (originalUrl) {
+ returnValue = originalUrl;
+ }
+ }
+
+ // Set the actiontype only if the user is not overriding actions.
+ if (action && this._pressedNoActionKeys.size == 0) {
+ this.setAttribute("actiontype", action.type);
+ } else {
+ this.removeAttribute("actiontype");
+ }
+ return returnValue;
+ ]]></body>
+ </method>
+
+ <method name="onKeyPress">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_LEFT:
+ case KeyEvent.DOM_VK_RIGHT:
+ case KeyEvent.DOM_VK_HOME:
+ // Reset the selected index so that nsAutoCompleteController
+ // simply closes the popup without trying to fill anything.
+ this.popup.selectedIndex = -1;
+ break;
+ }
+ if (this.popup.popupOpen &&
+ !this.popup.disableKeyNavigation &&
+ this.popup.handleKeyPress(aEvent)) {
+ return true;
+ }
+ return this.handleKeyPress(aEvent);
+ ]]></body>
+ </method>
+
+ <field name="_mayTrimURLs">true</field>
+ <method name="trimValue">
+ <parameter name="aURL"/>
+ <body><![CDATA[
+ // This method must not modify the given URL such that calling
+ // nsIURIFixup::createFixupURI with the result will produce a different URI.
+ return this._mayTrimURLs ? trimURL(aURL) : aURL;
+ ]]></body>
+ </method>
+
+ <field name="_formattingEnabled">true</field>
+ <method name="formatValue">
+ <body><![CDATA[
+ if (!this._formattingEnabled || !this.editor)
+ return;
+
+ let controller = this.editor.selectionController;
+ let strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
+ strikeOut.removeAllRanges();
+
+ let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
+ selection.removeAllRanges();
+
+ if (this.focused)
+ return;
+
+ let textNode = this.editor.rootElement.firstChild;
+ let value = textNode.textContent;
+ if (!value)
+ return;
+
+ // Get the URL from the fixup service:
+ let flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ let uriInfo;
+ try {
+ uriInfo = Services.uriFixup.getFixupURIInfo(value, flags);
+ } catch (ex) {}
+ // Ignore if we couldn't make a URI out of this, the URI resulted in a search,
+ // or the URI has a non-http(s)/ftp protocol.
+ if (!uriInfo ||
+ !uriInfo.fixedURI ||
+ uriInfo.keywordProviderName ||
+ ["http", "https", "ftp"].indexOf(uriInfo.fixedURI.scheme) == -1) {
+ return;
+ }
+
+ // If we trimmed off the http scheme, ensure we stick it back on before
+ // trying to figure out what domain we're accessing, so we don't get
+ // confused by user:pass@host http URLs. We later use
+ // trimmedLength to ensure we don't count the length of a trimmed protocol
+ // when determining which parts of the URL to highlight as "preDomain".
+ let trimmedLength = 0;
+ if (uriInfo.fixedURI.scheme == "http" && !value.startsWith("http://")) {
+ value = "http://" + value;
+ trimmedLength = "http://".length;
+ }
+
+ let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/);
+ if (!matchedURL)
+ return;
+
+ // Strike out the "https" part if mixed active content is loaded.
+ if (this.getAttribute("pageproxystate") == "valid" &&
+ value.startsWith("https:") &&
+ gBrowser.securityUI.state &
+ Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
+ let range = document.createRange();
+ range.setStart(textNode, 0);
+ range.setEnd(textNode, 5);
+ strikeOut.addRange(range);
+ }
+
+ let [, preDomain, domain] = matchedURL;
+ let baseDomain = domain;
+ let subDomain = "";
+ try {
+ baseDomain = Services.eTLD.getBaseDomainFromHost(uriInfo.fixedURI.host);
+ if (!domain.endsWith(baseDomain)) {
+ // getBaseDomainFromHost converts its resultant to ACE.
+ let IDNService = Cc["@mozilla.org/network/idn-service;1"]
+ .getService(Ci.nsIIDNService);
+ baseDomain = IDNService.convertACEtoUTF8(baseDomain);
+ }
+ } catch (e) {}
+ if (baseDomain != domain) {
+ subDomain = domain.slice(0, -baseDomain.length);
+ }
+
+ let rangeLength = preDomain.length + subDomain.length - trimmedLength;
+ if (rangeLength) {
+ let range = document.createRange();
+ range.setStart(textNode, 0);
+ range.setEnd(textNode, rangeLength);
+ selection.addRange(range);
+ }
+
+ let startRest = preDomain.length + domain.length - trimmedLength;
+ if (startRest < value.length - trimmedLength) {
+ let range = document.createRange();
+ range.setStart(textNode, startRest);
+ range.setEnd(textNode, value.length - trimmedLength);
+ selection.addRange(range);
+ }
+ ]]></body>
+ </method>
+
+ <method name="handleRevert">
+ <body><![CDATA[
+ var isScrolling = this.popupOpen;
+
+ gBrowser.userTypedValue = null;
+
+ // don't revert to last valid url unless page is NOT loading
+ // and user is NOT key-scrolling through autocomplete list
+ if (!XULBrowserWindow.isBusy && !isScrolling) {
+ URLBarSetURI();
+
+ // If the value isn't empty and the urlbar has focus, select the value.
+ if (this.value && this.hasAttribute("focused"))
+ this.select();
+ }
+
+ // tell widget to revert to last typed text only if the user
+ // was scrolling when they hit escape
+ return !isScrolling;
+ ]]></body>
+ </method>
+
+ <!--
+ This is ultimately called by the autocomplete controller as the result
+ of handleEnter when the Return key is pressed in the textbox. Since
+ onPopupClick also calls handleEnter, this is also called as a result in
+ that case.
+
+ @param event
+ The event that triggered the command.
+ @param openUILinkWhere
+ Optional. The "where" to pass to openUILinkIn. This method
+ computes the appropriate "where" given the event, but you can
+ use this to override it.
+ @param openUILinkParams
+ Optional. The parameters to pass to openUILinkIn. As with
+ "where", this method computes the appropriate parameters, but
+ any parameters you supply here will override those.
+ -->
+ <method name="handleCommand">
+ <parameter name="event"/>
+ <parameter name="openUILinkWhere"/>
+ <parameter name="openUILinkParams"/>
+ <body><![CDATA[
+ let isMouseEvent = event instanceof MouseEvent;
+ if (isMouseEvent && event.button == 2) {
+ // Do nothing for right clicks.
+ return;
+ }
+
+ // Determine whether to use the selected one-off search button. In
+ // one-off search buttons parlance, "selected" means that the button
+ // has been navigated to via the keyboard. So we want to use it if
+ // the triggering event is not a mouse click -- i.e., it's a Return
+ // key -- or if the one-off was mouse-clicked.
+ let selectedOneOff = this.popup.oneOffSearchButtons.selectedButton;
+ if (selectedOneOff &&
+ isMouseEvent &&
+ event.originalTarget != selectedOneOff) {
+ selectedOneOff = null;
+ }
+
+ // Do the command of the selected one-off if it's not an engine.
+ if (selectedOneOff && !selectedOneOff.engine) {
+ selectedOneOff.doCommand();
+ return;
+ }
+
+ let where = openUILinkWhere;
+ if (!where) {
+ if (isMouseEvent) {
+ where = whereToOpenLink(event, false, false);
+ } else {
+ // If the current tab is empty, ignore Alt+Enter (reuse this tab)
+ let altEnter = !isMouseEvent &&
+ event &&
+ event.altKey &&
+ !isTabEmpty(gBrowser.selectedTab);
+ where = altEnter ? "tab" : "current";
+ }
+ }
+
+ let url = this.value;
+ if (!url) {
+ return;
+ }
+
+ let mayInheritPrincipal = false;
+ let postData = null;
+ let browser = gBrowser.selectedBrowser;
+ let action = this._parseActionUrl(url);
+
+ if (selectedOneOff && selectedOneOff.engine) {
+ // If there's a selected one-off button then load a search using
+ // the one-off's engine.
+ [url, postData] =
+ this._parseAndRecordSearchEngineLoad(selectedOneOff.engine,
+ this.oneOffSearchQuery,
+ event, where,
+ openUILinkParams);
+ } else if (action) {
+ switch (action.type) {
+ case "visiturl":
+ // Unifiedcomplete uses fixupURI to tell if something is a visit
+ // or a search, and passes out the fixedURI as the url param.
+ // By using that uri we would end up passing a different string
+ // to the docshell that may run a different not-found heuristic.
+ // For example, "mozilla/run" would be fixed by unifiedcomplete
+ // to "http://mozilla/run". The docshell, once it can't resolve
+ // mozilla, would note the string has a scheme, and try to load
+ // http://mozilla.com/run instead of searching "mozilla/run".
+ // So, if we have the original input at hand, we pass it through
+ // and let the docshell handle it.
+ if (action.params.input) {
+ url = action.params.input;
+ break;
+ }
+ url = action.params.url;
+ break;
+ case "remotetab":
+ url = action.params.url;
+ break;
+ case "keyword":
+ if (action.params.postData) {
+ postData = getPostDataStream(action.params.postData);
+ }
+ mayInheritPrincipal = true;
+ url = action.params.url;
+ break;
+ case "switchtab":
+ url = action.params.url;
+ if (this.hasAttribute("actiontype")) {
+ this.handleRevert();
+ let prevTab = gBrowser.selectedTab;
+ if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) {
+ gBrowser.removeTab(prevTab);
+ }
+ return;
+ }
+ break;
+ case "searchengine":
+ if (selectedOneOff && selectedOneOff.engine) {
+ // Replace the engine with the selected one-off engine.
+ action.params.engineName = selectedOneOff.engine.name;
+ }
+ const actionDetails = {
+ isSuggestion: !!action.params.searchSuggestion,
+ isAlias: !!action.params.alias
+ };
+ [url, postData] = this._parseAndRecordSearchEngineLoad(
+ action.params.engineName,
+ action.params.searchSuggestion || action.params.searchQuery,
+ event,
+ where,
+ openUILinkParams,
+ actionDetails
+ );
+ break;
+ case "extension":
+ this.handleRevert();
+ // Give the extension control of handling the command.
+ let searchString = action.params.content;
+ let keyword = action.params.keyword;
+ this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
+ return;
+ }
+ } else {
+ // This is a fallback for add-ons and old testing code that directly
+ // set value and try to confirm it. UnifiedComplete should always
+ // resolve to a valid url.
+ try {
+ new URL(url);
+ } catch (ex) {
+ let lastLocationChange = browser.lastLocationChange;
+ getShortcutOrURIAndPostData(url).then(data => {
+ if (where != "current" ||
+ browser.lastLocationChange == lastLocationChange) {
+ this._loadURL(data.url, browser, data.postData, where,
+ openUILinkParams, data.mayInheritPrincipal);
+ }
+ });
+ return;
+ }
+ }
+
+ this._loadURL(url, browser, postData, where, openUILinkParams,
+ mayInheritPrincipal);
+ ]]></body>
+ </method>
+
+ <property name="oneOffSearchQuery">
+ <getter><![CDATA[
+ // this.textValue may be an autofilled string. Search only with the
+ // portion that the user typed, if any, by preferring the autocomplete
+ // controller's searchString (including handleEnterInstance.searchString).
+ return (this.handleEnterInstance && this.handleEnterInstance.searchString) ||
+ this.mController.searchString ||
+ this.textValue;
+ ]]></getter>
+ </property>
+
+ <method name="_loadURL">
+ <parameter name="url"/>
+ <parameter name="browser"/>
+ <parameter name="postData"/>
+ <parameter name="openUILinkWhere"/>
+ <parameter name="openUILinkParams"/>
+ <parameter name="mayInheritPrincipal"/>
+ <body><![CDATA[
+ this.value = url;
+ browser.userTypedValue = url;
+ if (gInitialPages.includes(url)) {
+ browser.initialPageLoadedFromURLBar = url;
+ }
+ try {
+ addToUrlbarHistory(url);
+ } catch (ex) {
+ // Things may go wrong when adding url to session history,
+ // but don't let that interfere with the loading of the url.
+ Cu.reportError(ex);
+ }
+
+ let params = {
+ postData,
+ allowThirdPartyFixup: true,
+ currentBrowser: browser,
+ };
+ if (openUILinkWhere == "current") {
+ params.indicateErrorPageLoad = true;
+ params.allowPinnedTabHostChange = true;
+ params.disallowInheritPrincipal = !mayInheritPrincipal;
+ params.allowPopups = url.startsWith("javascript:");
+ } else {
+ params.initiatingDoc = document;
+ }
+
+ if (openUILinkParams) {
+ for (let key in openUILinkParams) {
+ params[key] = openUILinkParams[key];
+ }
+ }
+
+ // Focus the content area before triggering loads, since if the load
+ // occurs in a new tab, we want focus to be restored to the content
+ // area when the current tab is re-selected.
+ browser.focus();
+
+ if (openUILinkWhere != "current") {
+ this.handleRevert();
+ }
+
+ try {
+ openUILinkIn(url, openUILinkWhere, params);
+ } catch (ex) {
+ // This load can throw an exception in certain cases, which means
+ // we'll want to replace the URL with the loaded URL:
+ if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
+ this.handleRevert();
+ }
+ }
+
+ if (openUILinkWhere == "current") {
+ // Ensure the start of the URL is visible for usability reasons.
+ this.selectionStart = this.selectionEnd = 0;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_parseAndRecordSearchEngineLoad">
+ <parameter name="engineOrEngineName"/>
+ <parameter name="query"/>
+ <parameter name="event"/>
+ <parameter name="openUILinkWhere"/>
+ <parameter name="openUILinkParams"/>
+ <parameter name="searchActionDetails"/>
+ <body><![CDATA[
+ let engine =
+ typeof(engineOrEngineName) == "string" ?
+ Services.search.getEngineByName(engineOrEngineName) :
+ engineOrEngineName;
+ let isOneOff = this.popup.oneOffSearchButtons
+ .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
+ // Infer the type of the event which triggered the search.
+ let eventType = "unknown";
+ if (event instanceof KeyboardEvent) {
+ eventType = "key";
+ } else if (event instanceof MouseEvent) {
+ eventType = "mouse";
+ }
+ // Augment the search action details object.
+ let details = searchActionDetails || {};
+ details.isOneOff = isOneOff;
+ details.type = eventType;
+
+ BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details);
+ let submission = engine.getSubmission(query, null, "keyword");
+ return [submission.uri.spec, submission.postData];
+ ]]></body>
+ </method>
+
+ <method name="maybeCanonizeURL">
+ <parameter name="aTriggeringEvent"/>
+ <parameter name="aUrl"/>
+ <body><![CDATA[
+ // Only add the suffix when the URL bar value isn't already "URL-like",
+ // and only if we get a keyboard event, to match user expectations.
+ if (!/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aUrl) ||
+ !(aTriggeringEvent instanceof KeyEvent)) {
+ return;
+ }
+
+ let url = aUrl;
+ let accel = this.AppConstants.platform == "macosx" ?
+ aTriggeringEvent.metaKey :
+ aTriggeringEvent.ctrlKey;
+ let shift = aTriggeringEvent.shiftKey;
+ let suffix = "";
+
+ switch (true) {
+ case (accel && shift):
+ suffix = ".org/";
+ break;
+ case (shift):
+ suffix = ".net/";
+ break;
+ case (accel):
+ try {
+ suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix");
+ if (suffix.charAt(suffix.length - 1) != "/")
+ suffix += "/";
+ } catch (e) {
+ suffix = ".com/";
+ }
+ break;
+ }
+
+ if (!suffix)
+ return;
+
+ // trim leading/trailing spaces (bug 233205)
+ url = url.trim();
+
+ // Tack www. and suffix on. If user has appended directories, insert
+ // suffix before them (bug 279035). Be careful not to get two slashes.
+ let firstSlash = url.indexOf("/");
+ if (firstSlash >= 0) {
+ url = url.substring(0, firstSlash) + suffix +
+ url.substring(firstSlash + 1);
+ } else {
+ url = url + suffix;
+ }
+
+ this.popup.overrideValue = "http://www." + url;
+ ]]></body>
+ </method>
+
+ <field name="_contentIsCropped">false</field>
+
+ <method name="_initURLTooltip">
+ <body><![CDATA[
+ if (this.focused || !this._contentIsCropped)
+ return;
+ this.inputField.setAttribute("tooltiptext", this.value);
+ ]]></body>
+ </method>
+
+ <method name="_hideURLTooltip">
+ <body><![CDATA[
+ this.inputField.removeAttribute("tooltiptext");
+ ]]></body>
+ </method>
+
+ <method name="onDragOver">
+ <parameter name="aEvent"/>
+ <body>
+ var types = aEvent.dataTransfer.types;
+ if (types.includes("application/x-moz-file") ||
+ types.includes("text/x-moz-url") ||
+ types.includes("text/uri-list") ||
+ types.includes("text/unicode"))
+ aEvent.preventDefault();
+ </body>
+ </method>
+
+ <method name="onDrop">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ let links = browserDragAndDrop.dropLinks(aEvent);
+
+ // The URL bar automatically handles inputs with newline characters,
+ // so we can get away with treating text/x-moz-url flavours as text/plain.
+ if (links.length > 0 && links[0].url) {
+ let url = links[0].url;
+ aEvent.preventDefault();
+ this.value = url;
+ SetPageProxyState("invalid");
+ this.focus();
+ try {
+ urlSecurityCheck(url,
+ gBrowser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+ } catch (ex) {
+ return;
+ }
+ this.handleCommand();
+ // Force not showing the dropped URI immediately.
+ gBrowser.userTypedValue = null;
+ URLBarSetURI();
+ }
+ ]]></body>
+ </method>
+
+ <method name="_getSelectedValueForClipboard">
+ <body><![CDATA[
+ // Grab the actual input field's value, not our value, which could include moz-action:
+ var inputVal = this.inputField.value;
+ var selectedVal = inputVal.substring(this.selectionStart, this.selectionEnd);
+
+ // If the selection doesn't start at the beginning or doesn't span the full domain or
+ // the URL bar is modified or there is no text at all, nothing else to do here.
+ if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "")
+ return selectedVal;
+ // The selection doesn't span the full domain if it doesn't contain a slash and is
+ // followed by some character other than a slash.
+ if (!selectedVal.includes("/")) {
+ let remainder = inputVal.replace(selectedVal, "");
+ if (remainder != "" && remainder[0] != "/")
+ return selectedVal;
+ }
+
+ let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);
+
+ let uri;
+ if (this.getAttribute("pageproxystate") == "valid") {
+ uri = gBrowser.currentURI;
+ } else {
+ // We're dealing with an autocompleted value, create a new URI from that.
+ try {
+ uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
+ } catch (e) {}
+ if (!uri)
+ return selectedVal;
+ }
+
+ // Avoid copying 'about:reader?url=', and always provide the original URI:
+ let readerOriginalURL = ReaderMode.getOriginalUrl(uri.spec);
+ if (readerOriginalURL) {
+ uri = uriFixup.createFixupURI(readerOriginalURL, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
+ }
+
+ // Only copy exposable URIs
+ try {
+ uri = uriFixup.createExposableURI(uri);
+ } catch (ex) {}
+
+ // If the entire URL is selected, just use the actual loaded URI.
+ if (inputVal == selectedVal) {
+ // ... but only if isn't a javascript: or data: URI, since those
+ // are hard to read when encoded
+ if (!uri.schemeIs("javascript") && !uri.schemeIs("data")) {
+ selectedVal = uri.spec;
+ }
+
+ return selectedVal;
+ }
+
+ // Just the beginning of the URL is selected, check for a trimmed
+ // value
+ let spec = uri.spec;
+ let trimmedSpec = this.trimValue(spec);
+ if (spec != trimmedSpec) {
+ // Prepend the portion that trimValue removed from the beginning.
+ // This assumes trimValue will only truncate the URL at
+ // the beginning or end (or both).
+ let trimmedSegments = spec.split(trimmedSpec);
+ selectedVal = trimmedSegments[0] + selectedVal;
+ }
+
+ return selectedVal;
+ ]]></body>
+ </method>
+
+ <field name="_copyCutController"><![CDATA[
+ ({
+ urlbar: this,
+ doCommand: function(aCommand) {
+ var urlbar = this.urlbar;
+ var val = urlbar._getSelectedValueForClipboard();
+ if (!val)
+ return;
+
+ if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) {
+ let start = urlbar.selectionStart;
+ let end = urlbar.selectionEnd;
+ urlbar.inputField.value = urlbar.inputField.value.substring(0, start) +
+ urlbar.inputField.value.substring(end);
+ urlbar.selectionStart = urlbar.selectionEnd = start;
+
+ let event = document.createEvent("UIEvents");
+ event.initUIEvent("input", true, false, window, 0);
+ urlbar.dispatchEvent(event);
+
+ SetPageProxyState("invalid");
+ }
+
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(val);
+ },
+ supportsCommand: function(aCommand) {
+ switch (aCommand) {
+ case "cmd_copy":
+ case "cmd_cut":
+ return true;
+ }
+ return false;
+ },
+ isCommandEnabled: function(aCommand) {
+ return this.supportsCommand(aCommand) &&
+ (aCommand != "cmd_cut" || !this.urlbar.readOnly) &&
+ this.urlbar.selectionStart < this.urlbar.selectionEnd;
+ },
+ onEvent: function(aEventName) {}
+ })
+ ]]></field>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body><![CDATA[
+ if (aTopic == "nsPref:changed") {
+ switch (aData) {
+ case "clickSelectsAll":
+ case "doubleClickSelectsAll":
+ this[aData] = this._prefs.getBoolPref(aData);
+ break;
+ case "autoFill":
+ this.completeDefaultIndex = this._prefs.getBoolPref(aData);
+ break;
+ case "delay":
+ this.timeout = this._prefs.getIntPref(aData);
+ break;
+ case "formatting.enabled":
+ this._formattingEnabled = this._prefs.getBoolPref(aData);
+ break;
+ case "suggest.searches":
+ case "userMadeSearchSuggestionsChoice":
+ // Mirror the value for future use, see the comment in the
+ // binding's constructor.
+ this._prefs.setBoolPref("searchSuggestionsChoice",
+ this._prefs.getBoolPref("suggest.searches"));
+
+ this._cacheUserMadeSearchSuggestionsChoice();
+ if (this._userMadeSearchSuggestionsChoice) {
+ this.popup.searchSuggestionsNotificationWasDismissed(
+ this._prefs.getBoolPref("suggest.searches")
+ );
+ }
+ break;
+ case "trimURLs":
+ this._mayTrimURLs = this._prefs.getBoolPref(aData);
+ break;
+ case "oneOffSearches":
+ this._enableOrDisableOneOffSearches();
+ break;
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_enableOrDisableOneOffSearches">
+ <body><![CDATA[
+ let enable = this._prefs.getBoolPref("oneOffSearches");
+ this.popup.enableOneOffSearches(enable);
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.type) {
+ case "paste":
+ let originalPasteData = aEvent.clipboardData.getData("text/plain");
+ if (!originalPasteData) {
+ return;
+ }
+
+ let oldValue = this.inputField.value;
+ let oldStart = oldValue.substring(0, this.inputField.selectionStart);
+ // If there is already non-whitespace content in the URL bar
+ // preceding the pasted content, it's not necessary to check
+ // protocols used by the pasted content:
+ if (oldStart.trim()) {
+ return;
+ }
+ let oldEnd = oldValue.substring(this.inputField.selectionEnd);
+
+ let pasteData = stripUnsafeProtocolOnPaste(originalPasteData);
+ if (originalPasteData != pasteData) {
+ // Unfortunately we're not allowed to set the bits being pasted
+ // so cancel this event:
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+
+ this.inputField.value = oldStart + pasteData + oldEnd;
+ // Fix up cursor/selection:
+ let newCursorPos = oldStart.length + pasteData.length;
+ this.inputField.selectionStart = newCursorPos;
+ this.inputField.selectionEnd = newCursorPos;
+ }
+ break;
+ case "mousedown":
+ if (this.doubleClickSelectsAll &&
+ aEvent.button == 0 && aEvent.detail == 2) {
+ this.editor.selectAll();
+ aEvent.preventDefault();
+ }
+ break;
+ case "mousemove":
+ this._initURLTooltip();
+ break;
+ case "mouseout":
+ this._hideURLTooltip();
+ break;
+ case "overflow":
+ this._contentIsCropped = true;
+ break;
+ case "underflow":
+ this._contentIsCropped = false;
+ this._hideURLTooltip();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <!--
+ onBeforeTextValueSet is called by the base-binding's .textValue getter.
+ It should return the value that the getter should use.
+ -->
+ <method name="onBeforeTextValueGet">
+ <body><![CDATA[
+ return { value: this.inputField.value };
+ ]]></body>
+ </method>
+
+ <!--
+ onBeforeTextValueSet is called by the base-binding's .textValue setter.
+ It should return the value that the setter should use.
+ -->
+ <method name="onBeforeTextValueSet">
+ <parameter name="aValue"/>
+ <body><![CDATA[
+ let val = aValue;
+ let uri;
+ try {
+ uri = makeURI(val);
+ } catch (ex) {}
+
+ if (uri) {
+ // Do not touch moz-action URIs at all. They depend on being
+ // properly encoded and decoded and will break if decoded
+ // unexpectedly.
+ if (!this._parseActionUrl(val)) {
+ val = losslessDecodeURI(uri);
+ }
+ }
+
+ return val;
+ ]]></body>
+ </method>
+
+ <method name="_parseActionUrl">
+ <parameter name="aUrl"/>
+ <body><![CDATA[
+ const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
+ if (!MOZ_ACTION_REGEX.test(aUrl))
+ return null;
+
+ // URL is in the format moz-action:ACTION,PARAMS
+ // Where PARAMS is a JSON encoded object.
+ let [, type, params] = aUrl.match(MOZ_ACTION_REGEX);
+
+ let action = {
+ type: type,
+ };
+
+ action.params = JSON.parse(params);
+ for (let key in action.params) {
+ action.params[key] = decodeURIComponent(action.params[key]);
+ }
+
+ if ("url" in action.params) {
+ let uri;
+ try {
+ uri = makeURI(action.params.url);
+ action.params.displayUrl = losslessDecodeURI(uri);
+ } catch (e) {
+ action.params.displayUrl = action.params.url;
+ }
+ }
+
+ return action;
+ ]]></body>
+ </method>
+
+ <property name="_noActionKeys" readonly="true">
+ <getter><![CDATA[
+ if (!this.__noActionKeys) {
+ this.__noActionKeys = new Set([
+ KeyEvent.DOM_VK_ALT,
+ KeyEvent.DOM_VK_SHIFT,
+ ]);
+ let modifier = this.AppConstants.platform == "macosx" ?
+ KeyEvent.DOM_VK_META :
+ KeyEvent.DOM_VK_CONTROL;
+ this.__noActionKeys.add(modifier);
+ }
+ return this.__noActionKeys;
+ ]]></getter>
+ </property>
+
+ <field name="_pressedNoActionKeys"><![CDATA[
+ new Set()
+ ]]></field>
+
+ <method name="_clearNoActions">
+ <parameter name="aURL"/>
+ <body><![CDATA[
+ this._pressedNoActionKeys.clear();
+ this.popup.removeAttribute("noactions");
+ let action = this._parseActionUrl(this._value);
+ if (action)
+ this.setAttribute("actiontype", action.type);
+ ]]></body>
+ </method>
+
+ <method name="onInput">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (!this.mIgnoreInput && this.mController.input == this) {
+ this._value = this.inputField.value;
+ gBrowser.userTypedValue = this.value;
+ this.valueIsTyped = true;
+ // Only wait for a result when we are sure to get one. In some
+ // cases, like when pasting the same exact text, we may not fire
+ // a new search and we won't get a result.
+ if (this.mController.handleText()) {
+ this.gotResultForCurrentQuery = false;
+ }
+ }
+ this.resetActionType();
+ ]]></body>
+ </method>
+
+ <method name="handleEnter">
+ <parameter name="event"/>
+ <body><![CDATA[
+ // We need to ensure we're using a selected autocomplete result.
+ // A result should automatically be selected by default,
+ // however autocomplete is async and therefore we may not
+ // have a result set relating to the current input yet. If that
+ // happens, we need to mark that when the first result does get added,
+ // it needs to be handled as if enter was pressed with that first
+ // result selected.
+ // If anything other than the default (first) result is selected, then
+ // it must have been manually selected by the human. We let this
+ // explicit choice be used, even if it may be related to a previous
+ // input.
+ // However, if the default result is automatically selected, we
+ // ensure that it corresponds to the current input.
+
+ // Store the current search string so it can be used in
+ // handleCommand, which will be called as a result of
+ // mController.handleEnter().
+ // Note this is also used to detect if we should perform a delayed
+ // handleEnter, in such a case it won't have been cleared.
+ this.handleEnterInstance = {
+ searchString: this.mController.searchString,
+ event: event
+ };
+
+ if (this.popup.selectedIndex != 0 || this.gotResultForCurrentQuery) {
+ this.maybeCanonizeURL(event, this.value);
+ let rv = this.mController.handleEnter(false, event);
+ this.handleEnterInstance = null;
+ this.popup.overrideValue = null;
+ return rv;
+ }
+
+ return true;
+ ]]></body>
+ </method>
+
+ <method name="handleDelete">
+ <body><![CDATA[
+ // If the heuristic result is selected, then the autocomplete
+ // controller's handleDelete implementation will remove it, which is
+ // not what we want. So in that case, call handleText so it acts as
+ // a backspace on the text value instead of removing the result.
+ if (this.popup.selectedIndex == 0 &&
+ this.popup._isFirstResultHeuristic) {
+ this.mController.handleText();
+ return false;
+ }
+ return this.mController.handleDelete();
+ ]]></body>
+ </method>
+
+ <field name="_userMadeSearchSuggestionsChoice"><![CDATA[
+ false
+ ]]></field>
+
+ <method name="_cacheUserMadeSearchSuggestionsChoice">
+ <body><![CDATA[
+ this._userMadeSearchSuggestionsChoice =
+ this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") ||
+ this._prefs.getBoolPref("suggest.searches");
+ ]]></body>
+ </method>
+
+ <property name="shouldShowSearchSuggestionsNotification" readonly="true">
+ <getter><![CDATA[
+ return !this._userMadeSearchSuggestionsChoice &&
+ !this.inPrivateContext &&
+ // When _urlbarFocused is true, tabbrowser would close the
+ // popup if it's opened here, so don't show the notification.
+ !gBrowser.selectedBrowser._urlbarFocused &&
+ Services.prefs.getBoolPref("browser.search.suggest.enabled") &&
+ this._prefs.getIntPref("daysBeforeHidingSuggestionsPrompt");
+ ]]></getter>
+ </property>
+
+ </implementation>
+
+ <handlers>
+ <handler event="keydown"><![CDATA[
+ if (this._noActionKeys.has(event.keyCode) &&
+ this.popup.selectedIndex >= 0 &&
+ !this._pressedNoActionKeys.has(event.keyCode)) {
+ if (this._pressedNoActionKeys.size == 0) {
+ this.popup.setAttribute("noactions", "true");
+ this.removeAttribute("actiontype");
+ }
+ this._pressedNoActionKeys.add(event.keyCode);
+ }
+ ]]></handler>
+
+ <handler event="keyup"><![CDATA[
+ if (this._noActionKeys.has(event.keyCode) &&
+ this._pressedNoActionKeys.has(event.keyCode)) {
+ this._pressedNoActionKeys.delete(event.keyCode);
+ if (this._pressedNoActionKeys.size == 0)
+ this._clearNoActions();
+ }
+ ]]></handler>
+
+ <handler event="focus"><![CDATA[
+ if (event.originalTarget == this.inputField) {
+ this._hideURLTooltip();
+ this.formatValue();
+ }
+ ]]></handler>
+
+ <handler event="blur"><![CDATA[
+ if (event.originalTarget == this.inputField) {
+ this._clearNoActions();
+ this.formatValue();
+ }
+ if (ExtensionSearchHandler.hasActiveInputSession()) {
+ ExtensionSearchHandler.handleInputCancelled();
+ }
+ ]]></handler>
+
+ <handler event="dragstart" phase="capturing"><![CDATA[
+ // Drag only if the gesture starts from the input field.
+ if (this.inputField != event.originalTarget &&
+ !(this.inputField.compareDocumentPosition(event.originalTarget) &
+ Node.DOCUMENT_POSITION_CONTAINED_BY))
+ return;
+
+ // Drag only if the entire value is selected and it's a valid URI.
+ var isFullSelection = this.selectionStart == 0 &&
+ this.selectionEnd == this.textLength;
+ if (!isFullSelection ||
+ this.getAttribute("pageproxystate") != "valid")
+ return;
+
+ var urlString = gBrowser.selectedBrowser.currentURI.spec;
+ var title = gBrowser.selectedBrowser.contentTitle || urlString;
+ var htmlString = "<a href=\"" + urlString + "\">" + urlString + "</a>";
+
+ var dt = event.dataTransfer;
+ dt.setData("text/x-moz-url", urlString + "\n" + title);
+ dt.setData("text/unicode", urlString);
+ dt.setData("text/html", htmlString);
+
+ dt.effectAllowed = "copyLink";
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragover" phase="capturing" action="this.onDragOver(event, this);"/>
+ <handler event="drop" phase="capturing" action="this.onDrop(event, this);"/>
+ <handler event="select"><![CDATA[
+ if (!Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(Ci.nsIClipboard)
+ .supportsSelectionClipboard())
+ return;
+
+ if (!window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .isHandlingUserInput)
+ return;
+
+ var val = this._getSelectedValueForClipboard();
+ if (!val)
+ return;
+
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyStringToClipboard(val, Ci.nsIClipboard.kSelectionClipboard);
+ ]]></handler>
+ </handlers>
+
+ </binding>
+
+ <!-- Note: this binding is applied to the autocomplete popup used in web page content and extended in search.xml for the searchbar. -->
+ <binding id="browser-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup">
+ <implementation>
+ <field name="AppConstants" readonly="true">
+ (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
+ </field>
+
+ <method name="openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ // initially the panel is hidden
+ // to avoid impacting startup / new window performance
+ aInput.popup.hidden = false;
+
+ // this method is defined on the base binding
+ this._openAutocompletePopup(aInput, aElement);
+ ]]></body>
+ </method>
+
+ <method name="onPopupClick">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ // Ignore all right-clicks
+ if (aEvent.button == 2)
+ return;
+
+ var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
+
+ var searchBar = BrowserSearch.searchBar;
+ var popupForSearchBar = searchBar && searchBar.textbox == this.mInput;
+ if (popupForSearchBar) {
+ searchBar.telemetrySearchDetails = {
+ index: controller.selection.currentIndex,
+ kind: "mouse"
+ };
+ }
+
+ // Check for unmodified left-click, and use default behavior
+ if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
+ !aEvent.altKey && !aEvent.metaKey) {
+ controller.handleEnter(true, aEvent);
+ return;
+ }
+
+ // Check for middle-click or modified clicks on the search bar
+ if (popupForSearchBar) {
+ // Handle search bar popup clicks
+ var search = controller.getValueAt(this.selectedIndex);
+
+ // open the search results according to the clicking subtlety
+ var where = whereToOpenLink(aEvent, false, true);
+ let params = {};
+
+ // But open ctrl/cmd clicks on autocomplete items in a new background tab.
+ let modifier = this.AppConstants.platform == "macosx" ?
+ aEvent.metaKey :
+ aEvent.ctrlKey;
+ if (where == "tab" && (aEvent instanceof MouseEvent) &&
+ (aEvent.button == 1 || modifier))
+ params.inBackground = true;
+
+ // leave the popup open for background tab loads
+ if (!(where == "tab" && params.inBackground)) {
+ // close the autocomplete popup and revert the entered search term
+ this.closePopup();
+ controller.handleEscape();
+ }
+
+ searchBar.doSearch(search, where, null, params);
+ if (where == "tab" && params.inBackground)
+ searchBar.focus();
+ else
+ searchBar.value = search;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
+
+ <resources>
+ <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
+ <stylesheet src="chrome://browser/skin/searchbar.css"/>
+ </resources>
+
+ <content ignorekeys="true" level="top" consumeoutsideclicks="never"
+ aria-owns="richlistbox">
+ <xul:hbox anonid="search-suggestions-notification"
+ align="center"
+ role="alert"
+ aria-describedby="search-suggestions-notification-text">
+ <xul:description flex="1">
+ &urlbar.searchSuggestionsNotification.question;
+ <!-- Several things here are to make the label accessibile via an
+ accesskey so that a11y doesn't suck: the accesskey, using an
+ onclick handler instead of an href attribute, the control
+ attribute, and having the control attribute refer to a valid ID
+ that is the label itself. -->
+ <xul:label id="search-suggestions-notification-learn-more"
+ class="text-link"
+ role="link"
+ value="&urlbar.searchSuggestionsNotification.learnMore;"
+ accesskey="&urlbar.searchSuggestionsNotification.learnMore.accesskey;"
+ onclick="document.getBindingParent(this).openSearchSuggestionsNotificationLearnMoreURL();"
+ control="search-suggestions-notification-learn-more"/>
+ </xul:description>
+ <xul:button anonid="search-suggestions-notification-disable"
+ label="&urlbar.searchSuggestionsNotification.disable;"
+ accesskey="&urlbar.searchSuggestionsNotification.disable.accesskey;"
+ onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/>
+ <xul:button anonid="search-suggestions-notification-enable"
+ label="&urlbar.searchSuggestionsNotification.enable;"
+ accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;"
+ onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
+ </xul:hbox>
+ <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
+ flex="1"/>
+ <xul:hbox anonid="footer">
+ <children/>
+ <xul:vbox anonid="one-off-search-buttons"
+ class="search-one-offs"
+ compact="true"
+ includecurrentengine="true"
+ disabletab="true"
+ flex="1"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <field name="_maxResults">0</field>
+
+ <field name="_bundle" readonly="true">
+ Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle("chrome://browser/locale/places/places.properties");
+ </field>
+
+ <field name="searchSuggestionsNotification" readonly="true">
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "search-suggestions-notification"
+ );
+ </field>
+
+ <field name="footer" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "footer");
+ </field>
+
+ <field name="oneOffSearchButtons" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "one-off-search-buttons");
+ </field>
+
+ <field name="_oneOffSearchesEnabled">false</field>
+
+ <field name="_overrideValue">null</field>
+ <property name="overrideValue"
+ onget="return this._overrideValue;"
+ onset="this._overrideValue = val; return val;"/>
+
+ <method name="onPopupClick">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (aEvent.button == 2) {
+ // Ignore right-clicks.
+ return;
+ }
+ // Otherwise "call super" -- do what autocomplete-base-popup does.
+ let controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
+ controller.handleEnter(true, aEvent);
+ ]]></body>
+ </method>
+
+ <method name="enableOneOffSearches">
+ <parameter name="enable"/>
+ <body><![CDATA[
+ this._oneOffSearchesEnabled = enable;
+ if (enable) {
+ this.oneOffSearchButtons.telemetryOrigin = "urlbar";
+ this.oneOffSearchButtons.style.display = "-moz-box";
+ this.oneOffSearchButtons.popup = this;
+ this.oneOffSearchButtons.textbox = this.input;
+ } else {
+ this.oneOffSearchButtons.telemetryOrigin = null;
+ this.oneOffSearchButtons.style.display = "none";
+ this.oneOffSearchButtons.popup = null;
+ this.oneOffSearchButtons.textbox = null;
+ }
+ ]]></body>
+ </method>
+
+ <method name="openSearchSuggestionsNotificationLearnMoreURL">
+ <body><![CDATA[
+ let url = Services.urlFormatter.formatURL(
+ Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
+ );
+ openUILinkIn(url, "tab");
+ ]]></body>
+ </method>
+
+ <method name="dismissSearchSuggestionsNotification">
+ <parameter name="enableSuggestions"/>
+ <body><![CDATA[
+ // Make sure the urlbar is focused. It won't be, for example, if the
+ // user used an accesskey to make an opt-in choice. mIgnoreFocus
+ // prevents the text from being selected.
+ this.input.mIgnoreFocus = true;
+ this.input.focus();
+ this.input.mIgnoreFocus = false;
+
+ Services.prefs.setBoolPref(
+ "browser.urlbar.suggest.searches", enableSuggestions
+ );
+ Services.prefs.setBoolPref(
+ "browser.urlbar.userMadeSearchSuggestionsChoice", true
+ );
+ // The input's pref observer will now hide the notification.
+ ]]></body>
+ </method>
+
+ <!-- Override this so that navigating between items results in an item
+ always being selected. -->
+ <method name="getNextIndex">
+ <parameter name="reverse"/>
+ <parameter name="amount"/>
+ <parameter name="index"/>
+ <parameter name="maxRow"/>
+ <body><![CDATA[
+ if (maxRow < 0)
+ return -1;
+
+ let newIndex = index + (reverse ? -1 : 1) * amount;
+
+ // We only want to wrap if navigation is in any direction by one item,
+ // otherwise we clamp to one end of the list.
+ // ie, hitting page-down will only cause is to wrap if we're already
+ // at one end of the list.
+
+ // Allow the selection to be removed if the first result is not a
+ // heuristic result.
+ if (!this._isFirstResultHeuristic) {
+ if (reverse && index == -1 || newIndex > maxRow && index != maxRow)
+ newIndex = maxRow;
+ else if (!reverse && index == -1 || newIndex < 0 && index != 0)
+ newIndex = 0;
+
+ if (newIndex < 0 && index == 0 || newIndex > maxRow && index == maxRow)
+ newIndex = -1;
+
+ return newIndex;
+ }
+
+ // Otherwise do not allow the selection to be removed.
+ if (newIndex < 0) {
+ newIndex = index > 0 ? 0 : maxRow;
+ } else if (newIndex > maxRow) {
+ newIndex = index < maxRow ? maxRow : 0;
+ }
+ return newIndex;
+ ]]></body>
+ </method>
+
+ <property name="_isFirstResultHeuristic" readonly="true">
+ <getter>
+ <![CDATA[
+ // The popup usually has a special "heuristic" first result (added
+ // by UnifiedComplete.js) that is automatically selected when the
+ // popup opens.
+ return this.input.mController.matchCount > 0 &&
+ this.input.mController
+ .getStyleAt(0)
+ .split(/\s+/).indexOf("heuristic") > 0;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="maxResults" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._maxResults) {
+ var prefService =
+ Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ this._maxResults = prefService.getIntPref("browser.urlbar.maxRichResults");
+ }
+ return this._maxResults;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body>
+ <![CDATA[
+ // initially the panel is hidden
+ // to avoid impacting startup / new window performance
+ aInput.popup.hidden = false;
+
+ let showNotification = aInput.shouldShowSearchSuggestionsNotification;
+ if (showNotification) {
+ let prefs = aInput._prefs;
+ let now = new Date();
+ let date = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate();
+ let previousDate = prefs.getIntPref("lastSuggestionsPromptDate");
+ if (previousDate < date) {
+ let remainingDays =
+ prefs.getIntPref("daysBeforeHidingSuggestionsPrompt") - 1;
+ prefs.setIntPref("daysBeforeHidingSuggestionsPrompt",
+ remainingDays);
+ prefs.setIntPref("lastSuggestionsPromptDate", date);
+ if (!remainingDays)
+ showNotification = false;
+ }
+ }
+
+ if (showNotification) {
+ this._showSearchSuggestionsNotification();
+ } else if (this.classList.contains("showSearchSuggestionsNotification")) {
+ this._hideSearchSuggestionsNotification();
+ }
+
+ this._openAutocompletePopup(aInput, aElement);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_openAutocompletePopup">
+ <parameter name="aInput"/>
+ <parameter name="aElement"/>
+ <body><![CDATA[
+ if (this.mPopupOpen) {
+ return;
+ }
+
+ this.mInput = aInput;
+ aInput.controller.setInitiallySelectedIndex(this._isFirstResultHeuristic ? 0 : -1);
+ this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView);
+ this._invalidate();
+
+ var rect = window.document.documentElement.getBoundingClientRect();
+ var width = rect.right - rect.left;
+ this.setAttribute("width", width);
+
+ // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
+ var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction;
+ this.style.direction = popupDirection;
+
+ // Make the popup's starting margin negative so that the leading edge
+ // of the popup aligns with the window border.
+ let elementRect = aElement.getBoundingClientRect();
+ if (popupDirection == "rtl") {
+ let offset = elementRect.right - rect.right
+ this.style.marginRight = offset + "px";
+ } else {
+ let offset = rect.left - elementRect.left;
+ this.style.marginLeft = offset + "px";
+ }
+
+ // Keep the popup items' site icons aligned with the urlbar's identity
+ // icon if it's not too far from the edge of the window. If there are
+ // at most two toolbar buttons between the window edge and the urlbar,
+ // then consider that as "not too far." The forward button's
+ // visibility may have changed since the last time the popup was
+ // opened, so this needs to happen now. Do it *before* the popup
+ // opens because otherwise the items will visibly shift.
+ let nodes = [...document.getElementById("nav-bar-customization-target").childNodes];
+ let urlbarPosition = nodes.findIndex(n => n.id == "urlbar-container");
+ let alignSiteIcons = urlbarPosition <= 2 &&
+ nodes.slice(0, urlbarPosition)
+ .every(n => n.localName == "toolbarbutton");
+ if (alignSiteIcons) {
+ let identityRect =
+ document.getElementById("identity-icon").getBoundingClientRect();
+ this.siteIconStart = popupDirection == "rtl" ? identityRect.right
+ : identityRect.left;
+ }
+ else {
+ // Reset the alignment so that the site icons are positioned
+ // according to whatever's in the CSS.
+ this.siteIconStart = undefined;
+ }
+
+ // Position the popup below the navbar. To get the y-coordinate,
+ // which is an offset from the bottom of the input, subtract the
+ // bottom of the navbar from the buttom of the input.
+ let yOffset =
+ document.getElementById("nav-bar").getBoundingClientRect().bottom -
+ aInput.getBoundingClientRect().bottom;
+ this.openPopup(aElement, "after_start", 0, yOffset, false, false);
+ ]]></body>
+ </method>
+
+ <method name="_updateFooterVisibility">
+ <body>
+ <![CDATA[
+ this.footer.collapsed = this._matchCount == 0;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_showSearchSuggestionsNotification">
+ <body>
+ <![CDATA[
+ // With the notification shown, the listbox's height can sometimes be
+ // too small when it's flexed, as it normally is. Also, it can start
+ // out slightly scrolled down. Both problems appear together, most
+ // often when the popup is very narrow and the notification's text
+ // must wrap. Work around them by removing the flex.
+ //
+ // But without flexing the listbox, the listbox's height animation
+ // sometimes fails to complete, leaving the popup too tall. Work
+ // around that problem by disabling the listbox animation.
+ this.richlistbox.flex = 0;
+ this.setAttribute("dontanimate", "true");
+
+ this.classList.add("showSearchSuggestionsNotification");
+ this._updateFooterVisibility();
+
+ // This event allows accessibility APIs to see the notification.
+ if (!this.popupOpen) {
+ let event = document.createEvent("Events");
+ event.initEvent("AlertActive", true, true);
+ this.searchSuggestionsNotification.dispatchEvent(event);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="searchSuggestionsNotificationWasDismissed">
+ <parameter name="enableSuggestions"/>
+ <body>
+ <![CDATA[
+ if (!this.popupOpen) {
+ this._hideSearchSuggestionsNotification();
+ return;
+ }
+ this._hideSearchSuggestionsNotificationWithAnimation().then(() => {
+ if (enableSuggestions && this.input.textValue) {
+ // Start a new search so that suggestions appear immediately.
+ this.input.controller.startSearch(this.input.textValue);
+ }
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="_hideSearchSuggestionsNotification">
+ <body>
+ <![CDATA[
+ this.classList.remove("showSearchSuggestionsNotification");
+ this.richlistbox.flex = 1;
+ this.removeAttribute("dontanimate");
+ if (this._matchCount) {
+ // Update popup height.
+ this._invalidate();
+ } else {
+ this.closePopup();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_hideSearchSuggestionsNotificationWithAnimation">
+ <body>
+ <![CDATA[
+ return new Promise(resolve => {
+ let notificationHeight = this.searchSuggestionsNotification
+ .getBoundingClientRect()
+ .height;
+ this.searchSuggestionsNotification.style.marginTop =
+ "-" + notificationHeight + "px";
+
+ let popupHeightPx =
+ (this.getBoundingClientRect().height - notificationHeight) + "px";
+ this.style.height = popupHeightPx;
+
+ let onTransitionEnd = () => {
+ this.removeEventListener("transitionend", onTransitionEnd, true);
+ this.searchSuggestionsNotification.style.marginTop = "0px";
+ this.style.removeProperty("height");
+ this._hideSearchSuggestionsNotification();
+ resolve();
+ };
+ this.addEventListener("transitionend", onTransitionEnd, true);
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="_selectedOneOffChanged">
+ <body><![CDATA[
+ // Update all searchengine result items to use the newly selected
+ // engine.
+ for (let item of this.richlistbox.childNodes) {
+ if (item.collapsed) {
+ break;
+ }
+ let url = item.getAttribute("url");
+ if (url) {
+ let action = item._parseActionUrl(url);
+ if (action && action.type == "searchengine") {
+ item._adjustAcItem();
+ }
+ }
+ }
+ ]]></body>
+ </method>
+
+ <!-- This handles keypress changes to the selection among the one-off
+ search buttons and between the one-offs and the listbox. It returns
+ true if the keypress was consumed and false if not. -->
+ <method name="handleKeyPress">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ this.oneOffSearchButtons.handleKeyPress(aEvent, this._matchCount,
+ !this._isFirstResultHeuristic,
+ gBrowser.userTypedValue);
+ return aEvent.defaultPrevented;
+ ]]></body>
+ </method>
+
+ <!-- This is called when a one-off is clicked and when "search in new tab"
+ is selected from a one-off context menu. -->
+ <method name="handleOneOffSearch">
+ <parameter name="event"/>
+ <parameter name="engine"/>
+ <parameter name="where"/>
+ <parameter name="params"/>
+ <body><![CDATA[
+ this.input.handleCommand(event, where, params);
+ ]]></body>
+ </method>
+
+ <!-- Result listitems call this to determine which search engine they
+ should show in their labels and include in their url attributes. -->
+ <property name="overrideSearchEngineName" readonly="true">
+ <getter><![CDATA[
+ let button = this.oneOffSearchButtons.selectedButton;
+ return button && button.engine && button.engine.name;
+ ]]></getter>
+ </property>
+
+ <method name="createResultLabel">
+ <parameter name="item"/>
+ <parameter name="proposedLabel"/>
+ <body>
+ <![CDATA[
+ let parts = [proposedLabel];
+
+ let action = this.mInput._parseActionUrl(item.getAttribute("url"));
+ if (action) {
+ switch (action.type) {
+ case "searchengine":
+ parts = [
+ action.params.searchSuggestion || action.params.searchQuery,
+ action.params.engineName,
+ ];
+ break;
+ case "switchtab":
+ case "remotetab":
+ parts = [
+ item.getAttribute("title"),
+ item.getAttribute("displayurl"),
+ ];
+ break;
+ }
+ }
+
+ let types = item.getAttribute("type").split(/\s+/);
+ let type = types.find(type => type != "action" && type != "heuristic");
+ try {
+ // Some types intentionally do not map to strings, which is not
+ // an error.
+ parts.push(this._bundle.GetStringFromName(type + "ResultLabel"));
+ } catch (e) {}
+
+ return parts.filter(str => str).join(" ");
+ ]]>
+ </body>
+ </method>
+
+ <method name="onResultsAdded">
+ <body>
+ <![CDATA[
+ // If nothing is selected yet, select the first result if it is a
+ // pre-selected "heuristic" result. (See UnifiedComplete.js.)
+ if (this.selectedIndex == -1 && this._isFirstResultHeuristic) {
+ // Don't fire DOMMenuItemActive so that screen readers still see
+ // the input as being focused.
+ this.richlistbox.suppressMenuItemEvent = true;
+ this.input.controller.setInitiallySelectedIndex(0);
+ this.richlistbox.suppressMenuItemEvent = false;
+ }
+
+ this.input.gotResultForCurrentQuery = true;
+
+ // Check if we should perform a delayed handleEnter.
+ if (this.input.handleEnterInstance) {
+ let instance = this.input.handleEnterInstance;
+ this.input.handleEnterInstance = null;
+ // Don't handle this immediately or we could cause a recursive
+ // loop where the controller sets popupOpen and re-enters here.
+ setTimeout(() => {
+ // Safety check: handle only if the search string didn't change.
+ let { event, searchString } = instance;
+ if (this.input.mController.searchString == searchString) {
+ this.input.maybeCanonizeURL(event, searchString);
+ this.input.mController.handleEnter(false, event);
+ this.overrideValue = null;
+ }
+ }, 0);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_onSearchBegin">
+ <body><![CDATA[
+ // Set the selected index to 0 (heuristic) until a result comes back
+ // and we can evaluate it better.
+ //
+ // This is required to properly manage delayed handleEnter:
+ // 1. if a search starts we set selectedIndex to 0 here, and it will
+ // be updated by onResultsAdded. Since selectedIndex is 0,
+ // handleEnter will delay the action if a result didn't arrive yet.
+ // 2. if a search doesn't start (for example if autocomplete is
+ // disabled), this won't be called, and the selectedIndex will be
+ // the default -1 value. Then handleEnter will know it should not
+ // delay the action, cause a result wont't ever arrive.
+ this.input.controller.setInitiallySelectedIndex(0);
+ ]]></body>
+ </method>
+
+ </implementation>
+ <handlers>
+
+ <handler event="SelectedOneOffButtonChanged"><![CDATA[
+ this._selectedOneOffChanged();
+ ]]></handler>
+
+ <handler event="mousedown"><![CDATA[
+ // Required to make the xul:label.text-link elements in the search
+ // suggestions notification work correctly when clicked on Linux.
+ // This is copied from the mousedown handler in
+ // browser-search-autocomplete-result-popup, which apparently had a
+ // similar problem.
+ event.preventDefault();
+ ]]></handler>
+
+ </handlers>
+ </binding>
+
+ <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
+ <implementation>
+ <constructor><![CDATA[
+ if (!this.notification)
+ return;
+
+ this.notification.options.installs.forEach(function(aInstall) {
+ aInstall.addListener(this);
+ }, this);
+
+ // Calling updateProgress can sometimes cause this notification to be
+ // removed in the middle of refreshing the notification panel which
+ // makes the panel get refreshed again. Just initialise to the
+ // undetermined state and then schedule a proper check at the next
+ // opportunity
+ this.setProgress(0, -1);
+ this._updateProgressTimeout = setTimeout(this.updateProgress.bind(this), 0);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this.destroy();
+ ]]></destructor>
+
+ <field name="progressmeter" readonly="true">
+ document.getElementById("addon-progress-notification-progressmeter");
+ </field>
+ <field name="progresstext" readonly="true">
+ document.getElementById("addon-progress-notification-progresstext");
+ </field>
+ <property name="DownloadUtils" readonly="true">
+ <getter><![CDATA[
+ let module = {};
+ Components.utils.import("resource://gre/modules/DownloadUtils.jsm", module);
+ Object.defineProperty(this, "DownloadUtils", {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value: module.DownloadUtils
+ });
+ return module.DownloadUtils;
+ ]]></getter>
+ </property>
+
+ <method name="destroy">
+ <body><![CDATA[
+ if (!this.notification)
+ return;
+
+ this.notification.options.installs.forEach(function(aInstall) {
+ aInstall.removeListener(this);
+ }, this);
+ clearTimeout(this._updateProgressTimeout);
+ ]]></body>
+ </method>
+
+ <method name="setProgress">
+ <parameter name="aProgress"/>
+ <parameter name="aMaxProgress"/>
+ <body><![CDATA[
+ if (aMaxProgress == -1) {
+ this.progressmeter.setAttribute("mode", "undetermined");
+ }
+ else {
+ this.progressmeter.setAttribute("mode", "determined");
+ this.progressmeter.setAttribute("value", (aProgress * 100) / aMaxProgress);
+ }
+
+ let now = Date.now();
+
+ if (!this.notification.lastUpdate) {
+ this.notification.lastUpdate = now;
+ this.notification.lastProgress = aProgress;
+ return;
+ }
+
+ let delta = now - this.notification.lastUpdate;
+ if ((delta < 400) && (aProgress < aMaxProgress))
+ return;
+
+ delta /= 1000;
+
+ // This code is taken from nsDownloadManager.cpp
+ let speed = (aProgress - this.notification.lastProgress) / delta;
+ if (this.notification.speed)
+ speed = speed * 0.9 + this.notification.speed * 0.1;
+
+ this.notification.lastUpdate = now;
+ this.notification.lastProgress = aProgress;
+ this.notification.speed = speed;
+
+ let status = null;
+ [status, this.notification.last] = this.DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last);
+ this.progresstext.setAttribute("value", status);
+ this.progresstext.setAttribute("tooltiptext", status);
+ ]]></body>
+ </method>
+
+ <method name="cancel">
+ <body><![CDATA[
+ let installs = this.notification.options.installs;
+ installs.forEach(function(aInstall) {
+ try {
+ aInstall.cancel();
+ }
+ catch (e) {
+ // Cancel will throw if the download has already failed
+ }
+ }, this);
+
+ PopupNotifications.remove(this.notification);
+ ]]></body>
+ </method>
+
+ <method name="updateProgress">
+ <body><![CDATA[
+ if (!this.notification)
+ return;
+
+ let downloadingCount = 0;
+ let progress = 0;
+ let maxProgress = 0;
+
+ this.notification.options.installs.forEach(function(aInstall) {
+ if (aInstall.maxProgress == -1)
+ maxProgress = -1;
+ progress += aInstall.progress;
+ if (maxProgress >= 0)
+ maxProgress += aInstall.maxProgress;
+ if (aInstall.state < AddonManager.STATE_DOWNLOADED)
+ downloadingCount++;
+ });
+
+ if (downloadingCount == 0) {
+ this.destroy();
+ if (Preferences.get("xpinstall.customConfirmationUI", false)) {
+ this.progressmeter.setAttribute("mode", "undetermined");
+ let status = gNavigatorBundle.getString("addonDownloadVerifying");
+ this.progresstext.setAttribute("value", status);
+ this.progresstext.setAttribute("tooltiptext", status);
+ } else {
+ PopupNotifications.remove(this.notification);
+ }
+ }
+ else {
+ this.setProgress(progress, maxProgress);
+ }
+ ]]></body>
+ </method>
+
+ <method name="onDownloadProgress">
+ <body><![CDATA[
+ this.updateProgress();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadFailed">
+ <body><![CDATA[
+ this.updateProgress();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadCancelled">
+ <body><![CDATA[
+ this.updateProgress();
+ ]]></body>
+ </method>
+
+ <method name="onDownloadEnded">
+ <body><![CDATA[
+ this.updateProgress();
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="plugin-popupnotification-center-item">
+ <content align="center">
+ <xul:vbox pack="center" anonid="itemBox" class="itemBox">
+ <xul:description anonid="center-item-label" class="center-item-label" />
+ <xul:hbox flex="1" pack="start" align="center" anonid="center-item-warning">
+ <xul:image anonid="center-item-warning-icon" class="center-item-warning-icon"/>
+ <xul:label anonid="center-item-warning-label"/>
+ <xul:label anonid="center-item-link" value="&checkForUpdates;" class="text-link"/>
+ </xul:hbox>
+ </xul:vbox>
+ <xul:vbox pack="center">
+ <xul:menulist class="center-item-menulist"
+ anonid="center-item-menulist">
+ <xul:menupopup>
+ <xul:menuitem anonid="allownow" value="allownow"
+ label="&pluginActivateNow.label;" />
+ <xul:menuitem anonid="allowalways" value="allowalways"
+ label="&pluginActivateAlways.label;" />
+ <xul:menuitem anonid="block" value="block"
+ label="&pluginBlockNow.label;" />
+ </xul:menupopup>
+ </xul:menulist>
+ </xul:vbox>
+ </content>
+ <resources>
+ <stylesheet src="chrome://global/skin/notification.css"/>
+ </resources>
+ <implementation>
+ <constructor><![CDATA[
+ document.getAnonymousElementByAttribute(this, "anonid", "center-item-label").value = this.action.pluginName;
+
+ let curState = "block";
+ if (this.action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
+ if (this.action.pluginPermissionType == Ci.nsIPermissionManager.EXPIRE_SESSION) {
+ curState = "allownow";
+ }
+ else {
+ curState = "allowalways";
+ }
+ }
+ document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").value = curState;
+
+ let warningString = "";
+ let linkString = "";
+
+ let link = document.getAnonymousElementByAttribute(this, "anonid", "center-item-link");
+
+ let url;
+ let linkHandler;
+
+ if (this.action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) {
+ document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true;
+ warningString = gNavigatorBundle.getString("pluginActivateDisabled.label");
+ linkString = gNavigatorBundle.getString("pluginActivateDisabled.manage");
+ linkHandler = function(event) {
+ event.preventDefault();
+ gPluginHandler.managePlugins();
+ };
+ document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-icon").hidden = true;
+ }
+ else {
+ url = this.action.detailsLink;
+
+ switch (this.action.blocklistState) {
+ case Ci.nsIBlocklistService.STATE_NOT_BLOCKED:
+ document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning").hidden = true;
+ break;
+ case Ci.nsIBlocklistService.STATE_BLOCKED:
+ document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true;
+ warningString = gNavigatorBundle.getString("pluginActivateBlocked.label");
+ linkString = gNavigatorBundle.getString("pluginActivate.learnMore");
+ break;
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
+ warningString = gNavigatorBundle.getString("pluginActivateOutdated.label");
+ linkString = gNavigatorBundle.getString("pluginActivate.updateLabel");
+ break;
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
+ warningString = gNavigatorBundle.getString("pluginActivateVulnerable.label");
+ linkString = gNavigatorBundle.getString("pluginActivate.riskLabel");
+ break;
+ }
+ }
+ document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-label").value = warningString;
+
+ let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
+ let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);
+
+ if (isWindowPrivate) {
+ // TODO: temporary compromise of hiding some privacy leaks, remove once bug 892487 is fixed
+ let allowalways = document.getAnonymousElementByAttribute(this, "anonid", "allowalways");
+ let block = document.getAnonymousElementByAttribute(this, "anonid", "block");
+ let allownow = document.getAnonymousElementByAttribute(this, "anonid", "allownow");
+
+ allowalways.hidden = curState !== "allowalways";
+ block.hidden = curState !== "block";
+ allownow.hidden = curState === "allowalways";
+ }
+
+ if (url || linkHandler) {
+ link.value = linkString;
+ if (url) {
+ link.href = url;
+ }
+ if (linkHandler) {
+ link.addEventListener("click", linkHandler, false);
+ }
+ }
+ else {
+ link.hidden = true;
+ }
+ ]]></constructor>
+ <property name="value">
+ <getter>
+ return document.getAnonymousElementByAttribute(this, "anonid",
+ "center-item-menulist").value;
+ </getter>
+ <setter><!-- This should be used only in automated tests -->
+ document.getAnonymousElementByAttribute(this, "anonid",
+ "center-item-menulist").value = val;
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="click-to-play-plugins-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
+ <content align="start" style="width: &pluginNotification.width;;">
+ <xul:vbox flex="1" align="stretch" class="popup-notification-main-box"
+ xbl:inherits="popupid">
+ <xul:hbox class="click-to-play-plugins-notification-description-box" flex="1" align="start">
+ <xul:description class="click-to-play-plugins-outer-description" flex="1">
+ <html:span anonid="click-to-play-plugins-notification-description" />
+ <xul:label class="text-link click-to-play-plugins-notification-link" anonid="click-to-play-plugins-notification-link" />
+ </xul:description>
+ <xul:toolbarbutton anonid="closebutton"
+ class="messageCloseButton popup-notification-closebutton tabbable close-icon"
+ xbl:inherits="oncommand=closebuttoncommand"
+ tooltiptext="&closeNotification.tooltip;"/>
+ </xul:hbox>
+ <xul:grid anonid="click-to-play-plugins-notification-center-box"
+ class="click-to-play-plugins-notification-center-box">
+ <xul:columns>
+ <xul:column flex="1"/>
+ <xul:column/>
+ </xul:columns>
+ <xul:rows>
+ <children includes="row"/>
+ <xul:hbox pack="start" anonid="plugin-notification-showbox">
+ <xul:button label="&pluginNotification.showAll.label;"
+ accesskey="&pluginNotification.showAll.accesskey;"
+ class="plugin-notification-showbutton"
+ oncommand="document.getBindingParent(this)._setState(2)"/>
+ </xul:hbox>
+ </xul:rows>
+ </xul:grid>
+ <xul:hbox anonid="button-container"
+ class="click-to-play-plugins-notification-button-container"
+ pack="center" align="center">
+ <xul:button anonid="primarybutton"
+ class="click-to-play-popup-button"
+ oncommand="document.getBindingParent(this)._onButton(this)"
+ flex="1"/>
+ <xul:button anonid="secondarybutton"
+ class="click-to-play-popup-button"
+ oncommand="document.getBindingParent(this)._onButton(this);"
+ flex="1"/>
+ </xul:hbox>
+ <xul:box hidden="true">
+ <children/>
+ </xul:box>
+ </xul:vbox>
+ </content>
+ <resources>
+ <stylesheet src="chrome://global/skin/notification.css"/>
+ </resources>
+ <implementation>
+ <field name="_states">
+ ({SINGLE: 0, MULTI_COLLAPSED: 1, MULTI_EXPANDED: 2})
+ </field>
+ <field name="_primaryButton">
+ document.getAnonymousElementByAttribute(this, "anonid", "primarybutton");
+ </field>
+ <field name="_secondaryButton">
+ document.getAnonymousElementByAttribute(this, "anonid", "secondarybutton")
+ </field>
+ <field name="_buttonContainer">
+ document.getAnonymousElementByAttribute(this, "anonid", "button-container")
+ </field>
+ <field name="_brandShortName">
+ document.getElementById("bundle_brand").getString("brandShortName")
+ </field>
+ <field name="_items">[]</field>
+ <constructor><![CDATA[
+ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let sortedActions = [];
+ for (let action of this.notification.options.pluginData.values()) {
+ sortedActions.push(action);
+ }
+ sortedActions.sort((a, b) => a.pluginName.localeCompare(b.pluginName));
+
+ for (let action of sortedActions) {
+ let item = document.createElementNS(XUL_NS, "row");
+ item.setAttribute("class", "plugin-popupnotification-centeritem");
+ item.action = action;
+ this.appendChild(item);
+ this._items.push(item);
+ }
+ switch (this._items.length) {
+ case 0:
+ PopupNotifications._dismiss();
+ break;
+ case 1:
+ this._setState(this._states.SINGLE);
+ break;
+ default:
+ if (this.notification.options.primaryPlugin) {
+ this._setState(this._states.MULTI_COLLAPSED);
+ } else {
+ this._setState(this._states.MULTI_EXPANDED);
+ }
+ }
+ ]]></constructor>
+ <method name="_setState">
+ <parameter name="state" />
+ <body><![CDATA[
+ var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box");
+
+ if (this._states.SINGLE == state) {
+ grid.hidden = true;
+ this._setupSingleState();
+ return;
+ }
+
+ let prePath = this.notification.options.principal.URI.prePath;
+ this._setupDescription("pluginActivateMultiple.message", null, prePath);
+
+ var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox");
+
+ var dialogStrings = Services.strings.createBundle("chrome://global/locale/dialog.properties");
+ this._primaryButton.label = dialogStrings.GetStringFromName("button-accept");
+ this._primaryButton.setAttribute("default", "true");
+
+ this._secondaryButton.label = dialogStrings.GetStringFromName("button-cancel");
+ this._primaryButton.setAttribute("action", "_multiAccept");
+ this._secondaryButton.setAttribute("action", "_cancel");
+
+ grid.hidden = false;
+
+ if (this._states.MULTI_COLLAPSED == state) {
+ for (let child of this.childNodes) {
+ if (child.tagName != "row") {
+ continue;
+ }
+ child.hidden = this.notification.options.primaryPlugin !=
+ child.action.permissionString;
+ }
+ showBox.hidden = false;
+ }
+ else {
+ for (let child of this.childNodes) {
+ if (child.tagName != "row") {
+ continue;
+ }
+ child.hidden = false;
+ }
+ showBox.hidden = true;
+ }
+ this._setupLink(null);
+ ]]></body>
+ </method>
+ <method name="_setupSingleState">
+ <body><![CDATA[
+ var action = this._items[0].action;
+ var prePath = action.pluginPermissionPrePath;
+ let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
+ let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);
+
+ let label, linkLabel, button1, button2;
+
+ if (action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
+ button1 = {
+ label: "pluginBlockNow.label",
+ accesskey: "pluginBlockNow.accesskey",
+ action: "_singleBlock"
+ };
+ button2 = {
+ label: "pluginContinue.label",
+ accesskey: "pluginContinue.accesskey",
+ action: "_singleContinue",
+ default: true
+ };
+ switch (action.blocklistState) {
+ case Ci.nsIBlocklistService.STATE_NOT_BLOCKED:
+ label = "pluginEnabled.message";
+ linkLabel = "pluginActivate.learnMore";
+ break;
+
+ case Ci.nsIBlocklistService.STATE_BLOCKED:
+ Cu.reportError(Error("Cannot happen!"));
+ break;
+
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
+ label = "pluginEnabledOutdated.message";
+ linkLabel = "pluginActivate.updateLabel";
+ break;
+
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
+ label = "pluginEnabledVulnerable.message";
+ linkLabel = "pluginActivate.riskLabel"
+ break;
+
+ default:
+ Cu.reportError(Error("Unexpected blocklist state"));
+ }
+
+ // TODO: temporary compromise, remove this once bug 892487 is fixed
+ if (isWindowPrivate) {
+ this._buttonContainer.hidden = true;
+ }
+ }
+ else if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) {
+ let linkElement =
+ document.getAnonymousElementByAttribute(
+ this, "anonid", "click-to-play-plugins-notification-link");
+ linkElement.textContent = gNavigatorBundle.getString("pluginActivateDisabled.manage");
+ linkElement.setAttribute("onclick", "gPluginHandler.managePlugins()");
+
+ let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description");
+ descElement.textContent = gNavigatorBundle.getFormattedString(
+ "pluginActivateDisabled.message", [action.pluginName, this._brandShortName]) + " ";
+ this._buttonContainer.hidden = true;
+ return;
+ }
+ else if (action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description");
+ descElement.textContent = gNavigatorBundle.getFormattedString(
+ "pluginActivateBlocked.message", [action.pluginName, this._brandShortName]) + " ";
+ this._setupLink("pluginActivate.learnMore", action.detailsLink);
+ this._buttonContainer.hidden = true;
+ return;
+ }
+ else {
+ button1 = {
+ label: "pluginActivateNow.label",
+ accesskey: "pluginActivateNow.accesskey",
+ action: "_singleActivateNow"
+ };
+ button2 = {
+ label: "pluginActivateAlways.label",
+ accesskey: "pluginActivateAlways.accesskey",
+ action: "_singleActivateAlways"
+ };
+ switch (action.blocklistState) {
+ case Ci.nsIBlocklistService.STATE_NOT_BLOCKED:
+ label = "pluginActivateNew.message";
+ linkLabel = "pluginActivate.learnMore";
+ button2.default = true;
+ break;
+
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
+ label = "pluginActivateOutdated.message";
+ linkLabel = "pluginActivate.updateLabel";
+ button1.default = true;
+ break;
+
+ case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
+ label = "pluginActivateVulnerable.message";
+ linkLabel = "pluginActivate.riskLabel"
+ button1.default = true;
+ break;
+
+ default:
+ Cu.reportError(Error("Unexpected blocklist state"));
+ }
+
+ // TODO: temporary compromise, remove this once bug 892487 is fixed
+ if (isWindowPrivate) {
+ button1.default = true;
+ this._secondaryButton.hidden = true;
+ }
+ }
+ this._setupDescription(label, action.pluginName, prePath);
+ this._setupLink(linkLabel, action.detailsLink);
+
+ this._primaryButton.label = gNavigatorBundle.getString(button1.label);
+ this._primaryButton.accessKey = gNavigatorBundle.getString(button1.accesskey);
+ this._primaryButton.setAttribute("action", button1.action);
+
+ this._secondaryButton.label = gNavigatorBundle.getString(button2.label);
+ this._secondaryButton.accessKey = gNavigatorBundle.getString(button2.accesskey);
+ this._secondaryButton.setAttribute("action", button2.action);
+ if (button1.default) {
+ this._primaryButton.setAttribute("default", "true");
+ }
+ else if (button2.default) {
+ this._secondaryButton.setAttribute("default", "true");
+ }
+ ]]></body>
+ </method>
+ <method name="_setupDescription">
+ <parameter name="baseString" />
+ <parameter name="pluginName" /> <!-- null for the multiple-plugin case -->
+ <parameter name="prePath" />
+ <body><![CDATA[
+ var span = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description");
+ while (span.lastChild) {
+ span.removeChild(span.lastChild);
+ }
+
+ var args = ["__prepath__", this._brandShortName];
+ if (pluginName) {
+ args.unshift(pluginName);
+ }
+ var bases = gNavigatorBundle.getFormattedString(baseString, args).
+ split("__prepath__", 2);
+
+ span.appendChild(document.createTextNode(bases[0]));
+ var prePathSpan = document.createElementNS("http://www.w3.org/1999/xhtml", "em");
+ prePathSpan.appendChild(document.createTextNode(prePath));
+ span.appendChild(prePathSpan);
+ span.appendChild(document.createTextNode(bases[1] + " "));
+ ]]></body>
+ </method>
+ <method name="_setupLink">
+ <parameter name="linkString"/>
+ <parameter name="linkUrl" />
+ <body><![CDATA[
+ var link = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-link");
+ if (!linkString || !linkUrl) {
+ link.hidden = true;
+ return;
+ }
+
+ link.hidden = false;
+ link.textContent = gNavigatorBundle.getString(linkString);
+ link.href = linkUrl;
+ ]]></body>
+ </method>
+ <method name="_onButton">
+ <parameter name="aButton" />
+ <body><![CDATA[
+ let methodName = aButton.getAttribute("action");
+ this[methodName]();
+ ]]></body>
+ </method>
+ <method name="_singleActivateNow">
+ <body><![CDATA[
+ gPluginHandler._updatePluginPermission(this.notification,
+ this._items[0].action,
+ "allownow");
+ this._cancel();
+ ]]></body>
+ </method>
+ <method name="_singleBlock">
+ <body><![CDATA[
+ gPluginHandler._updatePluginPermission(this.notification,
+ this._items[0].action,
+ "block");
+ this._cancel();
+ ]]></body>
+ </method>
+ <method name="_singleActivateAlways">
+ <body><![CDATA[
+ gPluginHandler._updatePluginPermission(this.notification,
+ this._items[0].action,
+ "allowalways");
+ this._cancel();
+ ]]></body>
+ </method>
+ <method name="_singleContinue">
+ <body><![CDATA[
+ gPluginHandler._updatePluginPermission(this.notification,
+ this._items[0].action,
+ "continue");
+ this._cancel();
+ ]]></body>
+ </method>
+ <method name="_multiAccept">
+ <body><![CDATA[
+ for (let item of this._items) {
+ let action = item.action;
+ if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED ||
+ action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ continue;
+ }
+ gPluginHandler._updatePluginPermission(this.notification,
+ item.action, item.value);
+ }
+ this._cancel();
+ ]]></body>
+ </method>
+ <method name="_cancel">
+ <body><![CDATA[
+ PopupNotifications._dismiss();
+ ]]></body>
+ </method>
+ <method name="_accept">
+ <parameter name="aEvent" />
+ <body><![CDATA[
+ if (aEvent.defaultPrevented)
+ return;
+ aEvent.preventDefault();
+ if (this._primaryButton.getAttribute("default") == "true") {
+ this._primaryButton.click();
+ }
+ else if (this._secondaryButton.getAttribute("default") == "true") {
+ this._secondaryButton.click();
+ }
+ ]]></body>
+ </method>
+ </implementation>
+ <handlers>
+ <!-- The _accept method checks for .defaultPrevented so that if focus is in a button,
+ enter activates the button and not this default action -->
+ <handler event="keypress" keycode="VK_RETURN" group="system" action="this._accept(event);"/>
+ </handlers>
+ </binding>
+
+ <binding id="splitmenu">
+ <content>
+ <xul:hbox anonid="menuitem" flex="1"
+ class="splitmenu-menuitem"
+ xbl:inherits="iconic,label,disabled,onclick=oncommand,_moz-menuactive=active"/>
+ <xul:menu anonid="menu" class="splitmenu-menu"
+ xbl:inherits="disabled,_moz-menuactive=active"
+ oncommand="event.stopPropagation();">
+ <children includes="menupopup"/>
+ </xul:menu>
+ </content>
+
+ <implementation implements="nsIDOMEventListener">
+ <constructor><![CDATA[
+ this._parentMenupopup.addEventListener("DOMMenuItemActive", this, false);
+ this._parentMenupopup.addEventListener("popuphidden", this, false);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ this._parentMenupopup.removeEventListener("DOMMenuItemActive", this, false);
+ this._parentMenupopup.removeEventListener("popuphidden", this, false);
+ ]]></destructor>
+
+ <field name="menuitem" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "menuitem");
+ </field>
+ <field name="menu" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "menu");
+ </field>
+
+ <field name="_menuDelay">600</field>
+
+ <field name="_parentMenupopup"><![CDATA[
+ this._getParentMenupopup(this);
+ ]]></field>
+
+ <method name="_getParentMenupopup">
+ <parameter name="aNode"/>
+ <body><![CDATA[
+ let node = aNode.parentNode;
+ while (node) {
+ if (node.localName == "menupopup")
+ break;
+ node = node.parentNode;
+ }
+ return node;
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="event"/>
+ <body><![CDATA[
+ switch (event.type) {
+ case "DOMMenuItemActive":
+ if (this.getAttribute("active") == "true" &&
+ event.target != this &&
+ this._getParentMenupopup(event.target) == this._parentMenupopup)
+ this.removeAttribute("active");
+ break;
+ case "popuphidden":
+ if (event.target == this._parentMenupopup)
+ this.removeAttribute("active");
+ break;
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mouseover"><![CDATA[
+ if (this.getAttribute("active") != "true") {
+ this.setAttribute("active", "true");
+
+ let event = document.createEvent("Events");
+ event.initEvent("DOMMenuItemActive", true, false);
+ this.dispatchEvent(event);
+
+ if (this.getAttribute("disabled") != "true") {
+ let self = this;
+ setTimeout(function () {
+ if (self.getAttribute("active") == "true")
+ self.menu.open = true;
+ }, this._menuDelay);
+ }
+ }
+ ]]></handler>
+
+ <handler event="popupshowing"><![CDATA[
+ if (event.target == this.firstChild &&
+ this._parentMenupopup._currentPopup)
+ this._parentMenupopup._currentPopup.hidePopup();
+ ]]></handler>
+
+ <handler event="click" phase="capturing"><![CDATA[
+ if (this.getAttribute("disabled") == "true") {
+ // Prevent the command from being carried out
+ event.stopPropagation();
+ return;
+ }
+
+ let node = event.originalTarget;
+ while (true) {
+ if (node == this.menuitem)
+ break;
+ if (node == this)
+ return;
+ node = node.parentNode;
+ }
+
+ this._parentMenupopup.hidePopup();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="menuitem-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem">
+ <implementation>
+ <constructor><![CDATA[
+ this.setAttribute("tooltiptext", this.getAttribute("acceltext"));
+ // TODO: Simplify this to this.setAttribute("acceltext", "") once bug
+ // 592424 is fixed
+ document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", "");
+ ]]></constructor>
+ </implementation>
+ </binding>
+
+ <binding id="menuitem-iconic-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem-iconic">
+ <implementation>
+ <constructor><![CDATA[
+ this.setAttribute("tooltiptext", this.getAttribute("acceltext"));
+ // TODO: Simplify this to this.setAttribute("acceltext", "") once bug
+ // 592424 is fixed
+ document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", "");
+ ]]></constructor>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/browser/base/content/usercontext.svg b/browser/base/content/usercontext.svg
new file mode 100644
index 000000000..705f80bfd
--- /dev/null
+++ b/browser/base/content/usercontext.svg
@@ -0,0 +1,23 @@
+<?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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="32" height="32" viewBox="0 0 32 32">
+ <style>
+ path, circle {
+ fill: menutext;
+ }
+ path:not(:target),
+ circle:not(:target) {
+ display: none;
+ }
+ </style>
+ <path id="dollar" d="M17.3857868,14.0527919 C14.2304569,13.0862944 13.4913706,12.4609137 13.4913706,11.0964467 C13.4913706,9.61827411 14.7137056,8.85076142 16.4192893,8.85076142 C17.9827411,8.85076142 19.3187817,9.33401015 20.5979695,10.4994924 L22.4456853,8.42436548 C21.1664975,7.20203046 19.3187819,6.26535905 17,6.00952148 L17,2 L15,2 L15,6.00952148 C12.3827412,6.43591742 9.76751269,8.53807107 9.76751269,11.3238579 C9.76751269,14.1664975 11.4730964,15.786802 15.4812183,17.0091371 C18.4375635,17.9187817 19.2335025,18.6294416 19.2335025,20.2213198 C19.2335025,22.0690355 17.7553299,23.035533 15.7370558,23.035533 C13.7756345,23.035533 12.2406091,22.3248731 10.9329949,21.1025381 L9,23.2345178 C10.4213198,24.6274112 12.8659899,25.8324934 15,26.0030518 L15,30 L17,30 L17,26.0030518 C20.7116753,25.4060974 22.9857868,22.893401 22.9857868,20.022335 C22.9857868,16.4690355 20.7116751,15.1045685 17.3857868,14.0527919 Z"/>
+ <path id="briefcase" fill-rule="evenodd" d="M22,9.99887085 L21.635468,10 L29.0034652,10 C29.5538362,10 30,10.4449463 30,10.9933977 L30,27.0066023 C30,27.5552407 29.5601869,28 29.0034652,28 L2.99653482,28 C2.44616384,28 2,27.5550537 2,27.0066023 L2,10.9933977 C2,10.4447593 2.43981314,10 2.99653482,10 L8,10 L8,7.99922997 C8,5.79051625 10.0426627,4 12.5635454,4 L19.4364546,4 C21.9568311,4 24,5.79246765 24,7.99922997 L24,9.99267578 L22,9.99887085 L22,10 L10,10 L10,7.99922997 C10,6.89421235 11.0713286,6 12.3917227,6 L19.6082773,6 C20.9273761,6 22,6.89552665 22,7.99922997 L22,9.99887085 Z"/>
+ <path id="fingerprint" d="M7.17741905,12 C7.10965537,12 7.041327,11.9953181 6.97243393,11.985018 C6.33263187,11.8918489 5.90515601,11.3862071 6.01809547,10.8552833 C7.41798011,4.26321358 12.2613889,2.57493207 15.0238882,2.15590491 C19.6448063,1.45690206 24.3408291,3.21541158 25.8344535,5.29743816 C26.1664955,5.76047488 25.9835336,6.35881757 25.4244832,6.63364321 C24.8654329,6.9098734 24.1437497,6.75583996 23.8122724,6.29327142 C22.8923805,5.01043967 19.1749781,3.51130562 15.4479759,4.07406612 C12.8080159,4.474834 9.43056132,6.03623689 8.33561323,11.1942506 C8.23453242,11.666651 7.73816348,12 7.17741905,12 Z M16.63127,26 C16.1452186,26 15.6509104,25.9658335 15.147795,25.8938767 C10.637921,25.257137 6.71207921,21.8114952 6.01575422,17.8807924 C5.91171832,17.2932317 6.33391695,16.7382846 6.95813239,16.6404441 C7.58454965,16.5343208 8.17298555,16.9406954 8.27757192,17.5272206 C8.80876054,20.5255916 11.9766264,23.26409 15.4885263,23.7610576 C17.3975027,24.02766 20.959494,23.8221432 23.3220449,19.3789425 C24.4625867,17.2331815 23.0049831,11.881462 19.9521622,9.34692739 C18.2380468,7.92384005 16.4573263,7.76905536 14.6628445,8.89499751 C13.26469,9.77142052 11.8070864,12.2857658 11.8665355,14.6287608 C11.9127737,16.4835887 12.8386382,17.9325598 14.6171568,18.9363308 C15.2210054,19.2764429 16.9411759,19.4933486 17.9424527,18.8296898 C18.7257495,18.3104622 18.9591422,17.2761485 18.6365758,15.7583267 C18.3822659,14.5650869 17.2219077,12.4452096 16.6664991,12.3711821 C16.6692513,12.3722175 16.4666841,12.4312324 16.1276041,12.9095636 C15.8545786,13.2936782 15.58981,14.7297074 15.9476054,15.3581643 C16.0142104,15.4761941 16.0725586,15.5465978 16.3202632,15.5465978 C16.9532859,15.5465978 17.46686,16.0290705 17.46686,16.6249139 C17.46686,17.2207573 16.9543868,17.7042653 16.3213641,17.7042653 C15.2644914,17.7042653 14.4140391,17.2336992 13.9268868,16.3774655 C13.1083609,14.9388479 13.5536787,12.6548678 14.2202791,11.7137354 C15.2540327,10.2564816 16.3631986,10.1151564 17.1123672,10.2564816 C19.7066595,10.7389543 20.8763754,15.2908666 20.8857331,15.3359043 C21.5303153,18.3648181 20.3594985,19.8665919 19.264094,20.593407 C17.4151172,21.8192603 14.6920186,21.493643 13.4380832,20.7859819 C10.3280151,19.0310652 9.62013053,16.497566 9.5744428,14.6805283 C9.49022326,11.3643051 11.4779146,8.30018945 13.391845,7.10021984 C16.0417332,5.43848454 18.9877658,5.66781436 21.4714167,7.72919442 C25.1176276,10.7565552 27.0871539,17.1229168 25.3746898,20.3433702 C23.4326862,23.9950465 20.2983981,26 16.63127,26 Z M16.0845157,30 C14.9348455,30 13.9050564,29.8557557 13.0394288,29.6610017 C10.2114238,29.0257442 7.58700058,27.4599412 6.18892823,25.5735955 C5.84440518,25.1078371 5.98426642,24.4803503 6.50105099,24.1700066 C7.01675554,23.8596629 7.71552172,23.986423 8.06112477,24.4507244 C9.89498097,26.9252176 15.9397944,29.9781448 22.2508301,26.1937972 C22.7676147,25.8844249 23.4658409,26.0087566 23.8109039,26.474515 C24.155427,26.9397877 24.0161057,27.5672745 23.4993212,27.8776182 C20.7987573,29.4963593 18.2315746,30 16.0845157,30 Z"/>
+ <path id="cart" fill-rule="evenodd" d="M20.8195396,14 L15.1804604,14 L15.1804604,14 L15.8471271,18 L20.1528729,18 L20.8195396,14 Z M22.8471271,14 L27.6125741,14 L27.6125741,14 L26.2792408,18 L22.1804604,18 L22.8471271,14 Z M21.1528729,12 L14.8471271,12 L14.8471271,12 L14.1804604,8 L21.8195396,8 L21.1528729,12 Z M23.1804604,12 L28.2792408,12 L28.2792408,12 L29.6125741,8 L23.8471271,8 L23.1804604,12 Z M13.1528729,14 L8.47703296,14 L10.077033,18 L10.077033,18 L13.8195396,18 L13.1528729,14 Z M12.8195396,12 L7.67703296,12 L6.07703296,8 L12.1528729,8 L12.8195396,12 L12.8195396,12 Z M31.7207592,8 L32,8 L32,6 L31,6 L5.27703296,6 L5.27703296,6 L4,2.8074176 L4,2 L3,2 L1,2 L0,2 L0,4 L1,4 L2.32296704,4 L9.78931928,22.6658806 L9.78931928,22.6658806 C8.71085924,23.3823847 8,24.6081773 8,26 C8,28.209139 9.790861,30 12,30 C14.209139,30 16,28.209139 16,26 C16,25.2714257 15.8052114,24.5883467 15.4648712,24 L22.5351288,24 C22.1947886,24.5883467 22,25.2714257 22,26 C22,28.209139 23.790861,30 26,30 C28.209139,30 30,28.209139 30,26 C30,23.790861 28.209139,22 26,22 L11.677033,22 L10.877033,20 L27,20 L28,20 L28,19.1622777 L31.7207592,8 L31.7207592,8 Z M26,28 C27.1045695,28 28,27.1045695 28,26 C28,24.8954305 27.1045695,24 26,24 C24.8954305,24 24,24.8954305 24,26 C24,27.1045695 24.8954305,28 26,28 Z M12,28 C13.1045695,28 14,27.1045695 14,26 C14,24.8954305 13.1045695,24 12,24 C10.8954305,24 10,24.8954305 10,26 C10,27.1045695 10.8954305,28 12,28 Z"/>
+ <circle id="circle" r="16" cx="16" cy="16" fill-rule="evenodd" />
+</svg>
+
diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js
new file mode 100644
index 000000000..7da54e064
--- /dev/null
+++ b/browser/base/content/utilityOverlay.js
@@ -0,0 +1,924 @@
+/* -*- 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/. */
+
+// Services = object with smart getters for common XPCOM services
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+Components.utils.import("resource:///modules/RecentWindow.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ShellService",
+ "resource:///modules/ShellService.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+ "@mozilla.org/browser/aboutnewtab-service;1",
+ "nsIAboutNewTabService");
+
+this.__defineGetter__("BROWSER_NEW_TAB_URL", () => {
+ if (PrivateBrowsingUtils.isWindowPrivate(window) &&
+ !PrivateBrowsingUtils.permanentPrivateBrowsing &&
+ !aboutNewTabService.overridden) {
+ return "about:privatebrowsing";
+ }
+ return aboutNewTabService.newTabURL;
+});
+
+var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
+
+var gBidiUI = false;
+
+/**
+ * Determines whether the given url is considered a special URL for new tabs.
+ */
+function isBlankPageURL(aURL) {
+ return aURL == "about:blank" || aURL == BROWSER_NEW_TAB_URL;
+}
+
+function getBrowserURL()
+{
+ return "chrome://browser/content/browser.xul";
+}
+
+function getTopWin(skipPopups) {
+ // If this is called in a browser window, use that window regardless of
+ // whether it's the frontmost window, since commands can be executed in
+ // background windows (bug 626148).
+ if (top.document.documentElement.getAttribute("windowtype") == "navigator:browser" &&
+ (!skipPopups || top.toolbar.visible))
+ return top;
+
+ let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
+ return RecentWindow.getMostRecentBrowserWindow({private: isPrivate,
+ allowPopups: !skipPopups});
+}
+
+function openTopWin(url) {
+ /* deprecated */
+ openUILinkIn(url, "current");
+}
+
+function getBoolPref(prefname, def)
+{
+ try {
+ return Services.prefs.getBoolPref(prefname);
+ }
+ catch (er) {
+ return def;
+ }
+}
+
+/* openUILink handles clicks on UI elements that cause URLs to load.
+ *
+ * As the third argument, you may pass an object with the same properties as
+ * accepted by openUILinkIn, plus "ignoreButton" and "ignoreAlt".
+ */
+function openUILink(url, event, aIgnoreButton, aIgnoreAlt, aAllowThirdPartyFixup,
+ aPostData, aReferrerURI) {
+ let params;
+
+ if (aIgnoreButton && typeof aIgnoreButton == "object") {
+ params = aIgnoreButton;
+
+ // don't forward "ignoreButton" and "ignoreAlt" to openUILinkIn
+ aIgnoreButton = params.ignoreButton;
+ aIgnoreAlt = params.ignoreAlt;
+ delete params.ignoreButton;
+ delete params.ignoreAlt;
+ } else {
+ params = {
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ postData: aPostData,
+ referrerURI: aReferrerURI,
+ referrerPolicy: Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
+ initiatingDoc: event ? event.target.ownerDocument : null,
+ };
+ }
+
+ let where = whereToOpenLink(event, aIgnoreButton, aIgnoreAlt);
+ openUILinkIn(url, where, params);
+}
+
+
+/* whereToOpenLink() looks at an event to decide where to open a link.
+ *
+ * The event may be a mouse event (click, double-click, middle-click) or keypress event (enter).
+ *
+ * On Windows, the modifiers are:
+ * Ctrl new tab, selected
+ * Shift new window
+ * Ctrl+Shift new tab, in background
+ * Alt save
+ *
+ * Middle-clicking is the same as Ctrl+clicking (it opens a new tab).
+ *
+ * Exceptions:
+ * - Alt is ignored for menu items selected using the keyboard so you don't accidentally save stuff.
+ * (Currently, the Alt isn't sent here at all for menu items, but that will change in bug 126189.)
+ * - Alt is hard to use in context menus, because pressing Alt closes the menu.
+ * - Alt can't be used on the bookmarks toolbar because Alt is used for "treat this as something draggable".
+ * - The button is ignored for the middle-click-paste-URL feature, since it's always a middle-click.
+ */
+function whereToOpenLink( e, ignoreButton, ignoreAlt )
+{
+ // This method must treat a null event like a left click without modifier keys (i.e.
+ // e = { shiftKey:false, ctrlKey:false, metaKey:false, altKey:false, button:0 })
+ // for compatibility purposes.
+ if (!e)
+ return "current";
+
+ var shift = e.shiftKey;
+ var ctrl = e.ctrlKey;
+ var meta = e.metaKey;
+ var alt = e.altKey && !ignoreAlt;
+
+ // ignoreButton allows "middle-click paste" to use function without always opening in a new window.
+ var middle = !ignoreButton && e.button == 1;
+ var middleUsesTabs = getBoolPref("browser.tabs.opentabfor.middleclick", true);
+
+ // Don't do anything special with right-mouse clicks. They're probably clicks on context menu items.
+
+ var metaKey = AppConstants.platform == "macosx" ? meta : ctrl;
+ if (metaKey || (middle && middleUsesTabs))
+ return shift ? "tabshifted" : "tab";
+
+ if (alt && getBoolPref("browser.altClickSave", false))
+ return "save";
+
+ if (shift || (middle && !middleUsesTabs))
+ return "window";
+
+ return "current";
+}
+
+/* openUILinkIn opens a URL in a place specified by the parameter |where|.
+ *
+ * |where| can be:
+ * "current" current tab (if there aren't any browser windows, then in a new window instead)
+ * "tab" new tab (if there aren't any browser windows, then in a new window instead)
+ * "tabshifted" same as "tab" but in background if default is to select new tabs, and vice versa
+ * "window" new window
+ * "save" save to disk (with no filename hint!)
+ *
+ * aAllowThirdPartyFixup controls whether third party services such as Google's
+ * I Feel Lucky are allowed to interpret this URL. This parameter may be
+ * undefined, which is treated as false.
+ *
+ * Instead of aAllowThirdPartyFixup, you may also pass an object with any of
+ * these properties:
+ * allowThirdPartyFixup (boolean)
+ * postData (nsIInputStream)
+ * referrerURI (nsIURI)
+ * relatedToCurrent (boolean)
+ * skipTabAnimation (boolean)
+ * allowPinnedTabHostChange (boolean)
+ * allowPopups (boolean)
+ * userContextId (unsigned int)
+ */
+function openUILinkIn(url, where, aAllowThirdPartyFixup, aPostData, aReferrerURI) {
+ var params;
+
+ if (arguments.length == 3 && typeof arguments[2] == "object") {
+ params = aAllowThirdPartyFixup;
+ } else {
+ params = {
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ postData: aPostData,
+ referrerURI: aReferrerURI,
+ referrerPolicy: Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
+ };
+ }
+
+ params.fromChrome = true;
+
+ openLinkIn(url, where, params);
+}
+
+function openLinkIn(url, where, params) {
+ if (!where || !url)
+ return;
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+
+ var aFromChrome = params.fromChrome;
+ var aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ var aPostData = params.postData;
+ var aCharset = params.charset;
+ var aReferrerURI = params.referrerURI;
+ var aReferrerPolicy = ('referrerPolicy' in params ?
+ params.referrerPolicy : Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT);
+ var aRelatedToCurrent = params.relatedToCurrent;
+ var aAllowMixedContent = params.allowMixedContent;
+ var aInBackground = params.inBackground;
+ var aDisallowInheritPrincipal = params.disallowInheritPrincipal;
+ var aInitiatingDoc = params.initiatingDoc;
+ var aIsPrivate = params.private;
+ var aSkipTabAnimation = params.skipTabAnimation;
+ var aAllowPinnedTabHostChange = !!params.allowPinnedTabHostChange;
+ var aNoReferrer = params.noReferrer;
+ var aAllowPopups = !!params.allowPopups;
+ var aUserContextId = params.userContextId;
+ var aIndicateErrorPageLoad = params.indicateErrorPageLoad;
+ var aPrincipal = params.originPrincipal;
+ var aForceAboutBlankViewerInCurrent =
+ params.forceAboutBlankViewerInCurrent;
+
+ // Establish a window in which we're running this code.
+ var w = getTopWin();
+
+ if ((where == "tab" || where == "tabshifted") &&
+ w && !w.toolbar.visible) {
+ w = getTopWin(true);
+ aRelatedToCurrent = false;
+ }
+
+ // Can only do this after we're sure of what |w| will be the rest of this function.
+ // Note that if |w| is null we might have no current browser (we'll open a new window).
+ var aCurrentBrowser = params.currentBrowser || (w && w.gBrowser.selectedBrowser);
+
+ if (where == "save") {
+ // TODO(1073187): propagate referrerPolicy.
+
+ // ContentClick.jsm passes isContentWindowPrivate for saveURL instead of passing a CPOW initiatingDoc
+ if ("isContentWindowPrivate" in params) {
+ saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, null, params.isContentWindowPrivate);
+ }
+ else {
+ if (!aInitiatingDoc) {
+ Components.utils.reportError("openUILink/openLinkIn was called with " +
+ "where == 'save' but without initiatingDoc. See bug 814264.");
+ return;
+ }
+ saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, aInitiatingDoc);
+ }
+ return;
+ }
+
+ if (!w || where == "window") {
+ // This propagates to window.arguments.
+ var sa = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+
+ var wuri = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ wuri.data = url;
+
+ let charset = null;
+ if (aCharset) {
+ charset = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ charset.data = "charset=" + aCharset;
+ }
+
+ var allowThirdPartyFixupSupports = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ allowThirdPartyFixupSupports.data = aAllowThirdPartyFixup;
+
+ var referrerURISupports = null;
+ if (aReferrerURI && !aNoReferrer) {
+ referrerURISupports = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ referrerURISupports.data = aReferrerURI.spec;
+ }
+
+ var referrerPolicySupports = Cc["@mozilla.org/supports-PRUint32;1"].
+ createInstance(Ci.nsISupportsPRUint32);
+ referrerPolicySupports.data = aReferrerPolicy;
+
+ var userContextIdSupports = Cc["@mozilla.org/supports-PRUint32;1"].
+ createInstance(Ci.nsISupportsPRUint32);
+ userContextIdSupports.data = aUserContextId;
+
+ sa.appendElement(wuri, /* weak =*/ false);
+ sa.appendElement(charset, /* weak =*/ false);
+ sa.appendElement(referrerURISupports, /* weak =*/ false);
+ sa.appendElement(aPostData, /* weak =*/ false);
+ sa.appendElement(allowThirdPartyFixupSupports, /* weak =*/ false);
+ sa.appendElement(referrerPolicySupports, /* weak =*/ false);
+ sa.appendElement(userContextIdSupports, /* weak =*/ false);
+ sa.appendElement(aPrincipal, /* weak =*/ false);
+
+ let features = "chrome,dialog=no,all";
+ if (aIsPrivate) {
+ features += ",private";
+ }
+
+ Services.ww.openWindow(w || window, getBrowserURL(), null, features, sa);
+ return;
+ }
+
+ let loadInBackground = where == "current" ? false : aInBackground;
+ if (loadInBackground == null) {
+ loadInBackground = aFromChrome ?
+ false :
+ getBoolPref("browser.tabs.loadInBackground");
+ }
+
+ let uriObj;
+ if (where == "current") {
+ try {
+ uriObj = Services.io.newURI(url, null, null);
+ } catch (e) {}
+ }
+
+ // We avoid using |w| here because in the 'popup window' case,
+ // if we pass a currentBrowser param |w.gBrowser| might not be the
+ // tabbrowser that contains |aCurrentBrowser|. We really only care
+ // about the tab linked to |aCurrentBrowser|.
+ let tab = aCurrentBrowser.getTabBrowser().getTabForBrowser(aCurrentBrowser);
+ if (where == "current" && tab.pinned &&
+ !aAllowPinnedTabHostChange) {
+ try {
+ // nsIURI.host can throw for non-nsStandardURL nsIURIs.
+ if (!uriObj || (!uriObj.schemeIs("javascript") &&
+ aCurrentBrowser.currentURI.host != uriObj.host)) {
+ where = "tab";
+ loadInBackground = false;
+ }
+ } catch (err) {
+ where = "tab";
+ loadInBackground = false;
+ }
+ }
+
+ // Raise the target window before loading the URI, since loading it may
+ // result in a new frontmost window (e.g. "javascript:window.open('');").
+ w.focus();
+
+ let browserUsedForLoad = null;
+ switch (where) {
+ case "current":
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+
+ if (aAllowThirdPartyFixup) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+
+ // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL isn't supported for javascript URIs,
+ // i.e. it causes them not to load at all. Callers should strip
+ // "javascript:" from pasted strings to protect users from malicious URIs
+ // (see stripUnsafeProtocolOnPaste).
+ if (aDisallowInheritPrincipal && !(uriObj && uriObj.schemeIs("javascript"))) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+
+ if (aAllowPopups) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS;
+ }
+ if (aIndicateErrorPageLoad) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV;
+ }
+
+ let {URI_INHERITS_SECURITY_CONTEXT} = Ci.nsIProtocolHandler;
+ if (aForceAboutBlankViewerInCurrent &&
+ (!uriObj ||
+ (Services.io.getProtocolFlags(uriObj.scheme) & URI_INHERITS_SECURITY_CONTEXT))) {
+ // Unless we know for sure we're not inheriting principals,
+ // force the about:blank viewer to have the right principal:
+ aCurrentBrowser.createAboutBlankContentViewer(aPrincipal);
+ }
+
+ aCurrentBrowser.loadURIWithFlags(url, {
+ flags: flags,
+ referrerURI: aNoReferrer ? null : aReferrerURI,
+ referrerPolicy: aReferrerPolicy,
+ postData: aPostData,
+ userContextId: aUserContextId
+ });
+ browserUsedForLoad = aCurrentBrowser;
+ break;
+ case "tabshifted":
+ loadInBackground = !loadInBackground;
+ // fall through
+ case "tab":
+ let tabUsedForLoad = w.gBrowser.loadOneTab(url, {
+ referrerURI: aReferrerURI,
+ referrerPolicy: aReferrerPolicy,
+ charset: aCharset,
+ postData: aPostData,
+ inBackground: loadInBackground,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ relatedToCurrent: aRelatedToCurrent,
+ skipAnimation: aSkipTabAnimation,
+ allowMixedContent: aAllowMixedContent,
+ noReferrer: aNoReferrer,
+ userContextId: aUserContextId,
+ originPrincipal: aPrincipal,
+ });
+ browserUsedForLoad = tabUsedForLoad.linkedBrowser;
+ break;
+ }
+
+ // Focus the content, but only if the browser used for the load is selected.
+ if (browserUsedForLoad &&
+ browserUsedForLoad == browserUsedForLoad.getTabBrowser().selectedBrowser) {
+ browserUsedForLoad.focus();
+ }
+
+ if (!loadInBackground && w.isBlankPageURL(url)) {
+ w.focusAndSelectUrlBar();
+ }
+}
+
+// Used as an onclick handler for UI elements with link-like behavior.
+// e.g. onclick="checkForMiddleClick(this, event);"
+function checkForMiddleClick(node, event) {
+ // We should be using the disabled property here instead of the attribute,
+ // but some elements that this function is used with don't support it (e.g.
+ // menuitem).
+ if (node.getAttribute("disabled") == "true")
+ return; // Do nothing
+
+ if (event.button == 1) {
+ /* Execute the node's oncommand or command.
+ *
+ * XXX: we should use node.oncommand(event) once bug 246720 is fixed.
+ */
+ var target = node.hasAttribute("oncommand") ? node :
+ node.ownerDocument.getElementById(node.getAttribute("command"));
+ var fn = new Function("event", target.getAttribute("oncommand"));
+ fn.call(target, event);
+
+ // If the middle-click was on part of a menu, close the menu.
+ // (Menus close automatically with left-click but not with middle-click.)
+ closeMenus(event.target);
+ }
+}
+
+// Populate a menu with user-context menu items. This method should be called
+// by onpopupshowing passing the event as first argument.
+function createUserContextMenu(event, isContextMenu = false, excludeUserContextId = 0) {
+ while (event.target.hasChildNodes()) {
+ event.target.removeChild(event.target.firstChild);
+ }
+
+ let bundle = document.getElementById("bundle_browser");
+ let docfrag = document.createDocumentFragment();
+
+ // If we are excluding a userContextId, we want to add a 'no-container' item.
+ if (excludeUserContextId) {
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("data-usercontextid", "0");
+ menuitem.setAttribute("label", bundle.getString("userContextNone.label"));
+ menuitem.setAttribute("accesskey", bundle.getString("userContextNone.accesskey"));
+
+ // We don't set an oncommand/command attribute because if we have
+ // to exclude a userContextId we are generating the contextMenu and
+ // isContextMenu will be true.
+
+ docfrag.appendChild(menuitem);
+
+ let menuseparator = document.createElement("menuseparator");
+ docfrag.appendChild(menuseparator);
+ }
+
+ ContextualIdentityService.getIdentities().forEach(identity => {
+ if (identity.userContextId == excludeUserContextId) {
+ return;
+ }
+
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("data-usercontextid", identity.userContextId);
+ menuitem.setAttribute("label", ContextualIdentityService.getUserContextLabel(identity.userContextId));
+
+ if (identity.accessKey) {
+ menuitem.setAttribute("accesskey", bundle.getString(identity.accessKey));
+ }
+
+ menuitem.classList.add("menuitem-iconic");
+ menuitem.setAttribute("data-identity-color", identity.color);
+
+ if (!isContextMenu) {
+ menuitem.setAttribute("command", "Browser:NewUserContextTab");
+ }
+
+ menuitem.setAttribute("data-identity-icon", identity.icon);
+
+ docfrag.appendChild(menuitem);
+ });
+
+ if (!isContextMenu) {
+ docfrag.appendChild(document.createElement("menuseparator"));
+
+ let menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("label",
+ bundle.getString("userContext.aboutPage.label"));
+ menuitem.setAttribute("accesskey",
+ bundle.getString("userContext.aboutPage.accesskey"));
+ menuitem.setAttribute("command", "Browser:OpenAboutContainers");
+ docfrag.appendChild(menuitem);
+ }
+
+ event.target.appendChild(docfrag);
+ return true;
+}
+
+// Closes all popups that are ancestors of the node.
+function closeMenus(node)
+{
+ if ("tagName" in node) {
+ if (node.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ && (node.tagName == "menupopup" || node.tagName == "popup"))
+ node.hidePopup();
+
+ closeMenus(node.parentNode);
+ }
+}
+
+/** This function takes in a key element and compares it to the keys pressed during an event.
+ *
+ * @param aEvent
+ * The KeyboardEvent event you want to compare against your key.
+ *
+ * @param aKey
+ * The <key> element checked to see if it was called in aEvent.
+ * For example, aKey can be a variable set to document.getElementById("key_close")
+ * to check if the close command key was pressed in aEvent.
+*/
+function eventMatchesKey(aEvent, aKey)
+{
+ let keyPressed = aKey.getAttribute("key").toLowerCase();
+ let keyModifiers = aKey.getAttribute("modifiers");
+ let modifiers = ["Alt", "Control", "Meta", "Shift"];
+
+ if (aEvent.key != keyPressed) {
+ return false;
+ }
+ let eventModifiers = modifiers.filter(modifier => aEvent.getModifierState(modifier));
+ // Check if aEvent has a modifier and aKey doesn't
+ if (eventModifiers.length > 0 && keyModifiers.length == 0) {
+ return false;
+ }
+ // Check whether aKey's modifiers match aEvent's modifiers
+ if (keyModifiers) {
+ keyModifiers = keyModifiers.split(/[\s,]+/);
+ // Capitalize first letter of aKey's modifers to compare to aEvent's modifier
+ keyModifiers.forEach(function(modifier, index) {
+ if (modifier == "accel") {
+ keyModifiers[index] = AppConstants.platform == "macosx" ? "Meta" : "Control";
+ } else {
+ keyModifiers[index] = modifier[0].toUpperCase() + modifier.slice(1);
+ }
+ });
+ return modifiers.every(modifier => keyModifiers.includes(modifier) == aEvent.getModifierState(modifier));
+ }
+ return true;
+}
+
+// Gather all descendent text under given document node.
+function gatherTextUnder ( root )
+{
+ var text = "";
+ var node = root.firstChild;
+ var depth = 1;
+ while ( node && depth > 0 ) {
+ // See if this node is text.
+ if ( node.nodeType == Node.TEXT_NODE ) {
+ // Add this text to our collection.
+ text += " " + node.data;
+ } else if ( node instanceof HTMLImageElement) {
+ // If it has an "alt" attribute, add that.
+ var altText = node.getAttribute( "alt" );
+ if ( altText && altText != "" ) {
+ text += " " + altText;
+ }
+ }
+ // Find next node to test.
+ // First, see if this node has children.
+ if ( node.hasChildNodes() ) {
+ // Go to first child.
+ node = node.firstChild;
+ depth++;
+ } else {
+ // No children, try next sibling (or parent next sibling).
+ while ( depth > 0 && !node.nextSibling ) {
+ node = node.parentNode;
+ depth--;
+ }
+ if ( node.nextSibling ) {
+ node = node.nextSibling;
+ }
+ }
+ }
+ // Strip leading and tailing whitespace.
+ text = text.trim();
+ // Compress remaining whitespace.
+ text = text.replace( /\s+/g, " " );
+ return text;
+}
+
+// This function exists for legacy reasons.
+function getShellService()
+{
+ return ShellService;
+}
+
+function isBidiEnabled() {
+ // first check the pref.
+ if (getBoolPref("bidi.browser.ui", false))
+ return true;
+
+ // then check intl.uidirection.<locale>
+ var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Components.interfaces.nsIXULChromeRegistry);
+ if (chromeReg.isLocaleRTL("global"))
+ return true;
+
+ // now see if the system locale is an RTL one.
+ var rv = false;
+
+ try {
+ var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"]
+ .getService(Components.interfaces.nsILocaleService);
+ var systemLocale = localeService.getSystemLocale().getCategory("NSILOCALE_CTYPE").substr(0, 3);
+
+ switch (systemLocale) {
+ case "ar-":
+ case "he-":
+ case "fa-":
+ case "ug-":
+ case "ur-":
+ case "syr":
+ rv = true;
+ Services.prefs.setBoolPref("bidi.browser.ui", true);
+ }
+ } catch (e) {}
+
+ return rv;
+}
+
+function openAboutDialog() {
+ var enumerator = Services.wm.getEnumerator("Browser:About");
+ while (enumerator.hasMoreElements()) {
+ // Only open one about window (Bug 599573)
+ let win = enumerator.getNext();
+ if (win.closed) {
+ continue;
+ }
+ win.focus();
+ return;
+ }
+
+ var features = "chrome,";
+ if (AppConstants.platform == "win") {
+ features += "centerscreen,dependent";
+ } else if (AppConstants.platform == "macosx") {
+ features += "resizable=no,minimizable=no";
+ } else {
+ features += "centerscreen,dependent,dialog=no";
+ }
+
+ window.openDialog("chrome://browser/content/aboutDialog.xul", "", features);
+}
+
+function openPreferences(paneID, extraArgs)
+{
+ function switchToAdvancedSubPane(doc) {
+ if (extraArgs && extraArgs["advancedTab"]) {
+ let advancedPaneTabs = doc.getElementById("advancedPrefs");
+ advancedPaneTabs.selectedTab = doc.getElementById(extraArgs["advancedTab"]);
+ }
+ }
+
+ // This function is duplicated from preferences.js.
+ function internalPrefCategoryNameToFriendlyName(aName) {
+ return (aName || "").replace(/^pane./, function(toReplace) { return toReplace[4].toLowerCase(); });
+ }
+
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let friendlyCategoryName = internalPrefCategoryNameToFriendlyName(paneID);
+ let params;
+ if (extraArgs && extraArgs["urlParams"]) {
+ params = new URLSearchParams();
+ let urlParams = extraArgs["urlParams"];
+ for (let name in urlParams) {
+ if (urlParams[name] !== undefined) {
+ params.set(name, urlParams[name]);
+ }
+ }
+ }
+ let preferencesURL = "about:preferences" + (params ? "?" + params : "") +
+ (friendlyCategoryName ? "#" + friendlyCategoryName : "");
+ let newLoad = true;
+ let browser = null;
+ if (!win) {
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ let windowArguments = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ let supportsStringPrefURL = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ supportsStringPrefURL.data = preferencesURL;
+ windowArguments.appendElement(supportsStringPrefURL, /* weak =*/ false);
+
+ win = Services.ww.openWindow(null, Services.prefs.getCharPref("browser.chromeURL"),
+ "_blank", "chrome,dialog=no,all", windowArguments);
+ } else {
+ let shouldReplaceFragment = friendlyCategoryName ? "whenComparingAndReplace" : "whenComparing";
+ newLoad = !win.switchToTabHavingURI(preferencesURL, true, { ignoreFragment: shouldReplaceFragment, replaceQueryString: true });
+ browser = win.gBrowser.selectedBrowser;
+ }
+
+ if (newLoad) {
+ Services.obs.addObserver(function advancedPaneLoadedObs(prefWin, topic, data) {
+ if (!browser) {
+ browser = win.gBrowser.selectedBrowser;
+ }
+ if (prefWin != browser.contentWindow) {
+ return;
+ }
+ Services.obs.removeObserver(advancedPaneLoadedObs, "advanced-pane-loaded");
+ switchToAdvancedSubPane(browser.contentDocument);
+ }, "advanced-pane-loaded", false);
+ } else {
+ if (paneID) {
+ browser.contentWindow.gotoPref(paneID);
+ }
+ switchToAdvancedSubPane(browser.contentDocument);
+ }
+}
+
+function openAdvancedPreferences(tabID)
+{
+ openPreferences("paneAdvanced", { "advancedTab" : tabID });
+}
+
+/**
+ * Opens the troubleshooting information (about:support) page for this version
+ * of the application.
+ */
+function openTroubleshootingPage()
+{
+ openUILinkIn("about:support", "tab");
+}
+
+/**
+ * Opens the troubleshooting information (about:support) page for this version
+ * of the application.
+ */
+function openHealthReport()
+{
+ openUILinkIn("about:healthreport", "tab");
+}
+
+/**
+ * Opens the feedback page for this version of the application.
+ */
+function openFeedbackPage()
+{
+ var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter)
+ .formatURLPref("app.feedback.baseURL");
+ openUILinkIn(url, "tab");
+}
+
+function openTourPage()
+{
+ let scope = {}
+ Components.utils.import("resource:///modules/UITour.jsm", scope);
+ openUILinkIn(scope.UITour.url, "tab");
+}
+
+function buildHelpMenu()
+{
+ // Enable/disable the "Report Web Forgery" menu item.
+ if (typeof gSafeBrowsing != "undefined") {
+ gSafeBrowsing.setReportPhishingMenu();
+ }
+}
+
+function isElementVisible(aElement)
+{
+ if (!aElement)
+ return false;
+
+ // If aElement or a direct or indirect parent is hidden or collapsed,
+ // height, width or both will be 0.
+ var bo = aElement.boxObject;
+ return (bo.height > 0 && bo.width > 0);
+}
+
+function makeURLAbsolute(aBase, aUrl)
+{
+ // Note: makeURI() will throw if aUri is not a valid URI
+ return makeURI(aUrl, null, makeURI(aBase)).spec;
+}
+
+/**
+ * openNewTabWith: opens a new tab with the given URL.
+ *
+ * @param aURL
+ * The URL to open (as a string).
+ * @param aDocument
+ * Note this parameter is now ignored. There is no security check & no
+ * referrer header derived from aDocument (null case).
+ * @param aPostData
+ * Form POST data, or null.
+ * @param aEvent
+ * The triggering event (for the purpose of determining whether to open
+ * in the background), or null.
+ * @param aAllowThirdPartyFixup
+ * If true, then we allow the URL text to be sent to third party services
+ * (e.g., Google's I Feel Lucky) for interpretation. This parameter may
+ * be undefined in which case it is treated as false.
+ * @param [optional] aReferrer
+ * This will be used as the referrer. There will be no security check.
+ * @param [optional] aReferrerPolicy
+ * Referrer policy - Ci.nsIHttpChannel.REFERRER_POLICY_*.
+ */
+function openNewTabWith(aURL, aDocument, aPostData, aEvent,
+ aAllowThirdPartyFixup, aReferrer, aReferrerPolicy) {
+
+ // As in openNewWindowWith(), we want to pass the charset of the
+ // current document over to a new tab.
+ let originCharset = null;
+ if (document.documentElement.getAttribute("windowtype") == "navigator:browser")
+ originCharset = gBrowser.selectedBrowser.characterSet;
+
+ openLinkIn(aURL, aEvent && aEvent.shiftKey ? "tabshifted" : "tab",
+ { charset: originCharset,
+ postData: aPostData,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ referrerURI: aReferrer,
+ referrerPolicy: aReferrerPolicy,
+ });
+}
+
+/**
+ * @param aDocument
+ * Note this parameter is ignored. See openNewTabWith()
+ */
+function openNewWindowWith(aURL, aDocument, aPostData, aAllowThirdPartyFixup,
+ aReferrer, aReferrerPolicy) {
+ // Extract the current charset menu setting from the current document and
+ // use it to initialize the new browser window...
+ let originCharset = null;
+ if (document.documentElement.getAttribute("windowtype") == "navigator:browser")
+ originCharset = gBrowser.selectedBrowser.characterSet;
+
+ openLinkIn(aURL, "window",
+ { charset: originCharset,
+ postData: aPostData,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ referrerURI: aReferrer,
+ referrerPolicy: aReferrerPolicy,
+ });
+}
+
+function getHelpLinkURL(aHelpTopic) {
+ var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter)
+ .formatURLPref("app.support.baseURL");
+ return url + aHelpTopic;
+}
+
+// aCalledFromModal is optional
+function openHelpLink(aHelpTopic, aCalledFromModal, aWhere) {
+ var url = getHelpLinkURL(aHelpTopic);
+ var where = aWhere;
+ if (!aWhere)
+ where = aCalledFromModal ? "window" : "tab";
+
+ openUILinkIn(url, where);
+}
+
+function openPrefsHelp() {
+ // non-instant apply prefwindows are usually modal, so we can't open in the topmost window,
+ // since its probably behind the window.
+ var instantApply = getBoolPref("browser.preferences.instantApply");
+
+ var helpTopic = document.getElementsByTagName("prefwindow")[0].currentPane.helpTopic;
+ openHelpLink(helpTopic, !instantApply);
+}
+
+function trimURL(aURL) {
+ // This function must not modify the given URL such that calling
+ // nsIURIFixup::createFixupURI with the result will produce a different URI.
+
+ // remove single trailing slash for http/https/ftp URLs
+ let url = aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
+
+ // remove http://
+ if (!url.startsWith("http://")) {
+ return url;
+ }
+ let urlWithoutProtocol = url.substring(7);
+
+ let flags = Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP |
+ Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
+ let fixedUpURL, expectedURLSpec;
+ try {
+ fixedUpURL = Services.uriFixup.createFixupURI(urlWithoutProtocol, flags);
+ expectedURLSpec = makeURI(aURL).spec;
+ } catch (ex) {
+ return url;
+ }
+ if (fixedUpURL.spec == expectedURLSpec) {
+ return urlWithoutProtocol;
+ }
+ return url;
+}
diff --git a/browser/base/content/viewSourceOverlay.xul b/browser/base/content/viewSourceOverlay.xul
new file mode 100644
index 000000000..8b40ddfd2
--- /dev/null
+++ b/browser/base/content/viewSourceOverlay.xul
@@ -0,0 +1,26 @@
+<?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/.
+
+<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?>
+
+<overlay id="viewSourceOverlay"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<window id="viewSource">
+ <commandset id="baseMenuCommandSet"/>
+ <keyset id="baseMenuKeyset"/>
+ <stringbundleset id="stringbundleset"/>
+</window>
+
+<menubar id="viewSource-main-menubar">
+#ifdef XP_MACOSX
+ <menu id="windowMenu"/>
+ <menupopup id="menu_ToolsPopup"/>
+#endif
+ <menu id="helpMenu"/>
+</menubar>
+
+</overlay>
diff --git a/browser/base/content/web-panels.js b/browser/base/content/web-panels.js
new file mode 100644
index 000000000..3a64b92a0
--- /dev/null
+++ b/browser/base/content/web-panels.js
@@ -0,0 +1,104 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const NS_ERROR_MODULE_NETWORK = 2152398848;
+const NS_NET_STATUS_READ_FROM = NS_ERROR_MODULE_NETWORK + 8;
+const NS_NET_STATUS_WROTE_TO = NS_ERROR_MODULE_NETWORK + 9;
+
+function getPanelBrowser()
+{
+ return document.getElementById("web-panels-browser");
+}
+
+var panelProgressListener = {
+ onProgressChange : function (aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ },
+
+ onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (!aRequest)
+ return;
+
+ // ignore local/resource:/chrome: files
+ if (aStatus == NS_NET_STATUS_READ_FROM || aStatus == NS_NET_STATUS_WROTE_TO)
+ return;
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ window.parent.document.getElementById('sidebar-throbber').setAttribute("loading", "true");
+ }
+ else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ window.parent.document.getElementById('sidebar-throbber').removeAttribute("loading");
+ }
+ }
+ ,
+
+ onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) {
+ UpdateBackForwardCommands(getPanelBrowser().webNavigation);
+ },
+
+ onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) {
+ },
+
+ onSecurityChange : function(aWebProgress, aRequest, aState) {
+ },
+
+ QueryInterface : function(aIID)
+ {
+ if (aIID.equals(Ci.nsIWebProgressListener) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ }
+};
+
+var gLoadFired = false;
+function loadWebPanel(aURI) {
+ var panelBrowser = getPanelBrowser();
+ if (gLoadFired) {
+ panelBrowser.webNavigation
+ .loadURI(aURI, nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null);
+ }
+ panelBrowser.setAttribute("cachedurl", aURI);
+}
+
+function load()
+{
+ var panelBrowser = getPanelBrowser();
+ panelBrowser.webProgress.addProgressListener(panelProgressListener,
+ Ci.nsIWebProgress.NOTIFY_ALL);
+ panelBrowser.messageManager.loadFrameScript("chrome://browser/content/content.js", true);
+ var cachedurl = panelBrowser.getAttribute("cachedurl")
+ if (cachedurl) {
+ panelBrowser.webNavigation
+ .loadURI(cachedurl, nsIWebNavigation.LOAD_FLAGS_NONE, null,
+ null, null);
+ }
+
+ gLoadFired = true;
+}
+
+function unload()
+{
+ getPanelBrowser().webProgress.removeProgressListener(panelProgressListener);
+}
+
+function PanelBrowserStop()
+{
+ getPanelBrowser().webNavigation.stop(nsIWebNavigation.STOP_ALL)
+}
+
+function PanelBrowserReload()
+{
+ getPanelBrowser().webNavigation
+ .sessionHistory
+ .QueryInterface(nsIWebNavigation)
+ .reload(nsIWebNavigation.LOAD_FLAGS_NONE);
+}
diff --git a/browser/base/content/web-panels.xul b/browser/base/content/web-panels.xul
new file mode 100644
index 000000000..4693d878b
--- /dev/null
+++ b/browser/base/content/web-panels.xul
@@ -0,0 +1,71 @@
+<?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://browser/skin/" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+<!DOCTYPE page [
+<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+%browserDTD;
+<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
+%textcontextDTD;
+]>
+
+<page id="webpanels-window"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="load()" onunload="unload()">
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
+ <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
+ <script type="application/javascript" src="chrome://browser/content/web-panels.js"/>
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
+ </stringbundleset>
+
+ <broadcasterset id="mainBroadcasterSet">
+ <broadcaster id="isFrameImage"/>
+ </broadcasterset>
+
+ <commandset id="mainCommandset">
+ <command id="Browser:Back"
+ oncommand="getPanelBrowser().webNavigation.goBack();"
+ disabled="true"/>
+ <command id="Browser:Forward"
+ oncommand="getPanelBrowser().webNavigation.goForward();"
+ disabled="true"/>
+ <command id="Browser:Stop" oncommand="PanelBrowserStop();"/>
+ <command id="Browser:Reload" oncommand="PanelBrowserReload();"/>
+ </commandset>
+
+ <popupset id="mainPopupSet">
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <menupopup id="contentAreaContextMenu" pagemenu="start"
+ onpopupshowing="if (event.target != this)
+ return true;
+ gContextMenu = new nsContextMenu(this, event.shiftKey);
+ if (gContextMenu.shouldDisplay)
+ document.popupNode = this.triggerNode;
+ return gContextMenu.shouldDisplay;"
+ onpopuphiding="if (event.target != this)
+ return;
+ gContextMenu.hiding();
+ gContextMenu = null;">
+#include browser-context.inc
+ </menupopup>
+ </popupset>
+
+ <commandset id="editMenuCommands"/>
+ <browser id="web-panels-browser" persist="cachedurl" type="content" flex="1"
+ context="contentAreaContextMenu" tooltip="aHTMLTooltip"
+ onclick="window.parent.contentAreaClick(event, true);"/>
+</page>
diff --git a/browser/base/content/webrtcIndicator.js b/browser/base/content/webrtcIndicator.js
new file mode 100644
index 000000000..301607031
--- /dev/null
+++ b/browser/base/content/webrtcIndicator.js
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/webrtcUI.jsm");
+
+const BUNDLE_URL = "chrome://browser/locale/webrtcIndicator.properties";
+var gStringBundle;
+
+function init(event) {
+ gStringBundle = Services.strings.createBundle(BUNDLE_URL);
+
+ let brand = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+ let brandShortName = brand.GetStringFromName("brandShortName");
+ document.title =
+ gStringBundle.formatStringFromName("webrtcIndicator.windowtitle",
+ [brandShortName], 1);
+
+ for (let id of ["audioVideoButton", "screenSharePopup"]) {
+ let popup = document.getElementById(id);
+ popup.addEventListener("popupshowing", onPopupMenuShowing);
+ popup.addEventListener("popuphiding", onPopupMenuHiding);
+ popup.addEventListener("command", onPopupMenuCommand);
+ }
+
+ let fxButton = document.getElementById("firefoxButton");
+ fxButton.addEventListener("click", onFirefoxButtonClick);
+ fxButton.addEventListener("mousedown", PositionHandler);
+
+ updateIndicatorState();
+
+ // Alert accessibility implementations stuff just changed. We only need to do
+ // this initially, because changes after this will automatically fire alert
+ // events if things change materially.
+ let ev = new CustomEvent("AlertActive", {bubbles: true, cancelable: true});
+ document.documentElement.dispatchEvent(ev);
+}
+
+function updateIndicatorState() {
+ updateWindowAttr("sharingvideo", webrtcUI.showCameraIndicator);
+ updateWindowAttr("sharingaudio", webrtcUI.showMicrophoneIndicator);
+ updateWindowAttr("sharingscreen", webrtcUI.showScreenSharingIndicator);
+
+ // Camera and microphone button tooltip.
+ let shareTypes = [];
+ if (webrtcUI.showCameraIndicator)
+ shareTypes.push("Camera");
+ if (webrtcUI.showMicrophoneIndicator)
+ shareTypes.push("Microphone");
+
+ let audioVideoButton = document.getElementById("audioVideoButton");
+ if (shareTypes.length) {
+ let stringId = "webrtcIndicator.sharing" + shareTypes.join("And") + ".tooltip";
+ audioVideoButton.setAttribute("tooltiptext",
+ gStringBundle.GetStringFromName(stringId));
+ }
+ else {
+ audioVideoButton.removeAttribute("tooltiptext");
+ }
+
+ // Screen sharing button tooltip.
+ let screenShareButton = document.getElementById("screenShareButton");
+ if (webrtcUI.showScreenSharingIndicator) {
+ let stringId = "webrtcIndicator.sharing" +
+ webrtcUI.showScreenSharingIndicator + ".tooltip";
+ screenShareButton.setAttribute("tooltiptext",
+ gStringBundle.GetStringFromName(stringId));
+ }
+ else {
+ screenShareButton.removeAttribute("tooltiptext");
+ }
+
+ // Resize and ensure the window position is correct
+ // (sizeToContent messes with our position).
+ window.sizeToContent();
+ PositionHandler.adjustPosition();
+}
+
+function updateWindowAttr(attr, value) {
+ let docEl = document.documentElement;
+ if (value)
+ docEl.setAttribute(attr, "true");
+ else
+ docEl.removeAttribute(attr);
+}
+
+function onPopupMenuShowing(event) {
+ let popup = event.target;
+ let type = popup.getAttribute("type");
+
+ let activeStreams;
+ if (type == "Devices")
+ activeStreams = webrtcUI.getActiveStreams(true, true, false);
+ else
+ activeStreams = webrtcUI.getActiveStreams(false, false, true);
+
+ if (activeStreams.length == 1) {
+ webrtcUI.showSharingDoorhanger(activeStreams[0], type);
+ event.preventDefault();
+ return;
+ }
+
+ for (let stream of activeStreams) {
+ let item = document.createElement("menuitem");
+ item.setAttribute("label", stream.browser.contentTitle || stream.uri);
+ item.setAttribute("tooltiptext", stream.uri);
+ item.stream = stream;
+ popup.appendChild(item);
+ }
+}
+
+function onPopupMenuHiding(event) {
+ let popup = event.target;
+ while (popup.firstChild)
+ popup.firstChild.remove();
+}
+
+function onPopupMenuCommand(event) {
+ let item = event.target;
+ webrtcUI.showSharingDoorhanger(item.stream,
+ item.parentNode.getAttribute("type"));
+}
+
+function onFirefoxButtonClick(event) {
+ event.target.blur();
+ let activeStreams = webrtcUI.getActiveStreams(true, true, true);
+ activeStreams[0].browser.ownerGlobal.focus();
+}
+
+var PositionHandler = {
+ positionCustomized: false,
+ threshold: 10,
+ adjustPosition: function() {
+ if (!this.positionCustomized) {
+ // Center the window horizontally on the screen (not the available area).
+ // Until we have moved the window to y=0, 'screen.width' may give a value
+ // for a secondary screen, so use values from the screen manager instead.
+ let primaryScreen = Cc["@mozilla.org/gfx/screenmanager;1"]
+ .getService(Ci.nsIScreenManager)
+ .primaryScreen;
+ let widthDevPix = {};
+ primaryScreen.GetRect({}, {}, widthDevPix, {});
+ let availTopDevPix = {};
+ primaryScreen.GetAvailRect({}, availTopDevPix, {}, {});
+ let scaleFactor = primaryScreen.defaultCSSScaleFactor;
+ let widthCss = widthDevPix.value / scaleFactor;
+ window.moveTo((widthCss - document.documentElement.clientWidth) / 2,
+ availTopDevPix.value / scaleFactor);
+ } else {
+ // This will ensure we're at y=0.
+ this.setXPosition(window.screenX);
+ }
+ },
+ setXPosition: function(desiredX) {
+ // Ensure the indicator isn't moved outside the available area of the screen.
+ desiredX = Math.max(desiredX, screen.availLeft);
+ let maxX =
+ screen.availLeft + screen.availWidth - document.documentElement.clientWidth;
+ window.moveTo(Math.min(desiredX, maxX), screen.availTop);
+ },
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "mousedown":
+ if (aEvent.button != 0 || aEvent.defaultPrevented)
+ return;
+
+ this._startMouseX = aEvent.screenX;
+ this._startWindowX = window.screenX;
+ this._deltaX = this._startMouseX - this._startWindowX;
+
+ window.addEventListener("mousemove", this);
+ window.addEventListener("mouseup", this);
+ break;
+
+ case "mousemove":
+ let moveOffset = Math.abs(aEvent.screenX - this._startMouseX);
+ if (this._dragFullyStarted || moveOffset > this.threshold) {
+ this.setXPosition(aEvent.screenX - this._deltaX);
+ this._dragFullyStarted = true;
+ }
+ break;
+
+ case "mouseup":
+ this._dragFullyStarted = false;
+ window.removeEventListener("mousemove", this);
+ window.removeEventListener("mouseup", this);
+ this.positionCustomized =
+ Math.abs(this._startWindowX - window.screenX) >= this.threshold;
+ break;
+ }
+ }
+};
diff --git a/browser/base/content/webrtcIndicator.xul b/browser/base/content/webrtcIndicator.xul
new file mode 100644
index 000000000..9208dc814
--- /dev/null
+++ b/browser/base/content/webrtcIndicator.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://browser/skin/webRTC-indicator.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="webrtcIndicator"
+ role="alert"
+ windowtype="Browser:WebRTCGlobalIndicator"
+ onload="init(event);"
+#ifdef XP_MACOSX
+ inwindowmenu="false"
+#endif
+ sizemode="normal"
+ hidechrome="true"
+ orient="horizontal"
+ >
+ <script type="application/javascript" src="chrome://browser/content/webrtcIndicator.js"/>
+
+ <button id="firefoxButton"/>
+ <button id="audioVideoButton" type="menu">
+ <menupopup id="audioVideoPopup" type="Devices"/>
+ </button>
+ <separator id="shareSeparator"/>
+ <button id="screenShareButton" type="menu">
+ <menupopup id="screenSharePopup" type="Screen"/>
+ </button>
+</window>
diff --git a/browser/base/content/win6BrowserOverlay.xul b/browser/base/content/win6BrowserOverlay.xul
new file mode 100644
index 000000000..a69e3f6bd
--- /dev/null
+++ b/browser/base/content/win6BrowserOverlay.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: HTML -*- -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<overlay id="win6-browser-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <toolbar id="toolbar-menubar"
+ autohide="true"/>
+</overlay>
diff --git a/browser/base/jar.mn b/browser/base/jar.mn
new file mode 100644
index 000000000..4dcd47c95
--- /dev/null
+++ b/browser/base/jar.mn
@@ -0,0 +1,197 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+browser.jar:
+% content browser %content/browser/ contentaccessible=yes
+#ifdef XP_MACOSX
+% overlay chrome://mozapps/content/downloads/downloads.xul chrome://browser/content/downloadManagerOverlay.xul
+% overlay chrome://mozapps/content/update/updates.xul chrome://browser/content/softwareUpdateOverlay.xul
+#endif
+#ifdef XP_WIN
+% overlay chrome://browser/content/browser.xul chrome://browser/content/win6BrowserOverlay.xul os=WINNT osversion>=6
+#endif
+% overlay chrome://global/content/viewSource.xul chrome://browser/content/viewSourceOverlay.xul
+% overlay chrome://global/content/viewPartialSource.xul chrome://browser/content/viewSourceOverlay.xul
+
+ content/browser/aboutDialog-appUpdater.js (content/aboutDialog-appUpdater.js)
+* content/browser/aboutDialog.xul (content/aboutDialog.xul)
+ content/browser/aboutDialog.js (content/aboutDialog.js)
+ content/browser/aboutDialog.css (content/aboutDialog.css)
+ content/browser/aboutRobots.xhtml (content/aboutRobots.xhtml)
+* content/browser/abouthome/aboutHome.xhtml (content/abouthome/aboutHome.xhtml)
+ content/browser/abouthome/aboutHome.js (content/abouthome/aboutHome.js)
+* content/browser/abouthome/aboutHome.css (content/abouthome/aboutHome.css)
+ content/browser/abouthome/snippet1.png (content/abouthome/snippet1.png)
+ content/browser/abouthome/snippet2.png (content/abouthome/snippet2.png)
+ content/browser/abouthome/downloads.png (content/abouthome/downloads.png)
+ content/browser/abouthome/bookmarks.png (content/abouthome/bookmarks.png)
+ content/browser/abouthome/history.png (content/abouthome/history.png)
+ content/browser/abouthome/addons.png (content/abouthome/addons.png)
+ content/browser/abouthome/sync.png (content/abouthome/sync.png)
+ content/browser/abouthome/settings.png (content/abouthome/settings.png)
+ content/browser/abouthome/restore.png (content/abouthome/restore.png)
+ content/browser/abouthome/restore-large.png (content/abouthome/restore-large.png)
+ content/browser/abouthome/mozilla.png (content/abouthome/mozilla.png)
+ content/browser/abouthome/snippet1@2x.png (content/abouthome/snippet1@2x.png)
+ content/browser/abouthome/snippet2@2x.png (content/abouthome/snippet2@2x.png)
+ content/browser/abouthome/downloads@2x.png (content/abouthome/downloads@2x.png)
+ content/browser/abouthome/bookmarks@2x.png (content/abouthome/bookmarks@2x.png)
+ content/browser/abouthome/history@2x.png (content/abouthome/history@2x.png)
+ content/browser/abouthome/addons@2x.png (content/abouthome/addons@2x.png)
+ content/browser/abouthome/sync@2x.png (content/abouthome/sync@2x.png)
+ content/browser/abouthome/settings@2x.png (content/abouthome/settings@2x.png)
+ content/browser/abouthome/restore@2x.png (content/abouthome/restore@2x.png)
+ content/browser/abouthome/restore-large@2x.png (content/abouthome/restore-large@2x.png)
+ content/browser/abouthome/mozilla@2x.png (content/abouthome/mozilla@2x.png)
+
+ content/browser/aboutNetError.xhtml (content/aboutNetError.xhtml)
+
+#ifdef MOZ_SERVICES_HEALTHREPORT
+ content/browser/abouthealthreport/abouthealth.xhtml (content/abouthealthreport/abouthealth.xhtml)
+ content/browser/abouthealthreport/abouthealth.js (content/abouthealthreport/abouthealth.js)
+ content/browser/abouthealthreport/abouthealth.css (content/abouthealthreport/abouthealth.css)
+#endif
+ content/browser/aboutaccounts/aboutaccounts.xhtml (content/aboutaccounts/aboutaccounts.xhtml)
+ content/browser/aboutaccounts/aboutaccounts.js (content/aboutaccounts/aboutaccounts.js)
+ content/browser/aboutaccounts/aboutaccounts.css (content/aboutaccounts/aboutaccounts.css)
+ content/browser/aboutaccounts/main.css (content/aboutaccounts/main.css)
+ content/browser/aboutaccounts/normalize.css (content/aboutaccounts/normalize.css)
+ content/browser/aboutaccounts/images/fox.png (content/aboutaccounts/images/fox.png)
+ content/browser/aboutaccounts/images/graphic_sync_intro.png (content/aboutaccounts/images/graphic_sync_intro.png)
+ content/browser/aboutaccounts/images/graphic_sync_intro@2x.png (content/aboutaccounts/images/graphic_sync_intro@2x.png)
+
+
+ content/browser/aboutRobots-icon.png (content/aboutRobots-icon.png)
+ content/browser/aboutRobots-widget-left.png (content/aboutRobots-widget-left.png)
+ content/browser/aboutSocialError.xhtml (content/aboutSocialError.xhtml)
+ content/browser/aboutProviderDirectory.xhtml (content/aboutProviderDirectory.xhtml)
+ content/browser/aboutTabCrashed.css (content/aboutTabCrashed.css)
+ content/browser/aboutTabCrashed.js (content/aboutTabCrashed.js)
+ content/browser/aboutTabCrashed.xhtml (content/aboutTabCrashed.xhtml)
+* content/browser/browser.css (content/browser.css)
+ content/browser/browser.js (content/browser.js)
+* content/browser/browser.xul (content/browser.xul)
+ content/browser/browser-addons.js (content/browser-addons.js)
+ content/browser/browser-captivePortal.js (content/browser-captivePortal.js)
+ content/browser/browser-ctrlTab.js (content/browser-ctrlTab.js)
+ content/browser/browser-customization.js (content/browser-customization.js)
+ content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js)
+ content/browser/browser-devedition.js (content/browser-devedition.js)
+ content/browser/browser-feeds.js (content/browser-feeds.js)
+ content/browser/browser-fullScreenAndPointerLock.js (content/browser-fullScreenAndPointerLock.js)
+ content/browser/browser-fullZoom.js (content/browser-fullZoom.js)
+ content/browser/browser-fxaccounts.js (content/browser-fxaccounts.js)
+ content/browser/browser-gestureSupport.js (content/browser-gestureSupport.js)
+ content/browser/browser-media.js (content/browser-media.js)
+ content/browser/browser-places.js (content/browser-places.js)
+ content/browser/browser-plugins.js (content/browser-plugins.js)
+ content/browser/browser-refreshblocker.js (content/browser-refreshblocker.js)
+ content/browser/browser-safebrowsing.js (content/browser-safebrowsing.js)
+ content/browser/browser-sidebar.js (content/browser-sidebar.js)
+ content/browser/browser-social.js (content/browser-social.js)
+ content/browser/browser-syncui.js (content/browser-syncui.js)
+* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml)
+#ifdef CAN_DRAW_IN_TITLEBAR
+ content/browser/browser-tabsintitlebar.js (content/browser-tabsintitlebar.js)
+#else
+ content/browser/browser-tabsintitlebar.js (content/browser-tabsintitlebar-stub.js)
+#endif
+ content/browser/browser-thumbnails.js (content/browser-thumbnails.js)
+ content/browser/browser-trackingprotection.js (content/browser-trackingprotection.js)
+ content/browser/tab-content.js (content/tab-content.js)
+ content/browser/content.js (content/content.js)
+ content/browser/social-content.js (content/social-content.js)
+ content/browser/defaultthemes/1.footer.jpg (content/defaultthemes/1.footer.jpg)
+ content/browser/defaultthemes/1.header.jpg (content/defaultthemes/1.header.jpg)
+ content/browser/defaultthemes/1.icon.jpg (content/defaultthemes/1.icon.jpg)
+ content/browser/defaultthemes/1.preview.jpg (content/defaultthemes/1.preview.jpg)
+ content/browser/defaultthemes/2.footer.jpg (content/defaultthemes/2.footer.jpg)
+ content/browser/defaultthemes/2.header.jpg (content/defaultthemes/2.header.jpg)
+ content/browser/defaultthemes/2.icon.jpg (content/defaultthemes/2.icon.jpg)
+ content/browser/defaultthemes/2.preview.jpg (content/defaultthemes/2.preview.jpg)
+ content/browser/defaultthemes/3.footer.png (content/defaultthemes/3.footer.png)
+ content/browser/defaultthemes/3.header.png (content/defaultthemes/3.header.png)
+ content/browser/defaultthemes/3.icon.png (content/defaultthemes/3.icon.png)
+ content/browser/defaultthemes/3.preview.png (content/defaultthemes/3.preview.png)
+ content/browser/defaultthemes/4.footer.png (content/defaultthemes/4.footer.png)
+ content/browser/defaultthemes/4.header.png (content/defaultthemes/4.header.png)
+ content/browser/defaultthemes/4.icon.png (content/defaultthemes/4.icon.png)
+ content/browser/defaultthemes/4.preview.png (content/defaultthemes/4.preview.png)
+ content/browser/defaultthemes/5.footer.png (content/defaultthemes/5.footer.png)
+ content/browser/defaultthemes/5.header.png (content/defaultthemes/5.header.png)
+ content/browser/defaultthemes/5.icon.jpg (content/defaultthemes/5.icon.jpg)
+ content/browser/defaultthemes/5.preview.jpg (content/defaultthemes/5.preview.jpg)
+ content/browser/defaultthemes/devedition.header.png (content/defaultthemes/devedition.header.png)
+ content/browser/defaultthemes/devedition.icon.png (content/defaultthemes/devedition.icon.png)
+ content/browser/gcli_sec_bad.svg (content/gcli_sec_bad.svg)
+ content/browser/gcli_sec_good.svg (content/gcli_sec_good.svg)
+ content/browser/gcli_sec_moderate.svg (content/gcli_sec_moderate.svg)
+ content/browser/newtab/newTab.xhtml (content/newtab/newTab.xhtml)
+* content/browser/newtab/newTab.js (content/newtab/newTab.js)
+ content/browser/newtab/newTab.css (content/newtab/newTab.css)
+ content/browser/newtab/newTab.inadjacent.json (content/newtab/newTab.inadjacent.json)
+ content/browser/newtab/alternativeDefaultSites.json (content/newtab/alternativeDefaultSites.json)
+* content/browser/pageinfo/pageInfo.xul (content/pageinfo/pageInfo.xul)
+ content/browser/pageinfo/pageInfo.js (content/pageinfo/pageInfo.js)
+ content/browser/pageinfo/pageInfo.css (content/pageinfo/pageInfo.css)
+ content/browser/pageinfo/pageInfo.xml (content/pageinfo/pageInfo.xml)
+ content/browser/pageinfo/feeds.js (content/pageinfo/feeds.js)
+ content/browser/pageinfo/feeds.xml (content/pageinfo/feeds.xml)
+ content/browser/pageinfo/permissions.js (content/pageinfo/permissions.js)
+ content/browser/pageinfo/security.js (content/pageinfo/security.js)
+ content/browser/sync/aboutSyncTabs.xul (content/sync/aboutSyncTabs.xul)
+ content/browser/sync/aboutSyncTabs.js (content/sync/aboutSyncTabs.js)
+ content/browser/sync/aboutSyncTabs.css (content/sync/aboutSyncTabs.css)
+ content/browser/sync/aboutSyncTabs-bindings.xml (content/sync/aboutSyncTabs-bindings.xml)
+ content/browser/sync/setup.xul (content/sync/setup.xul)
+ content/browser/sync/addDevice.js (content/sync/addDevice.js)
+ content/browser/sync/addDevice.xul (content/sync/addDevice.xul)
+ content/browser/sync/setup.js (content/sync/setup.js)
+ content/browser/sync/genericChange.xul (content/sync/genericChange.xul)
+ content/browser/sync/genericChange.js (content/sync/genericChange.js)
+ content/browser/sync/key.xhtml (content/sync/key.xhtml)
+ content/browser/sync/utils.js (content/sync/utils.js)
+* content/browser/sync/customize.xul (content/sync/customize.xul)
+ content/browser/sync/customize.js (content/sync/customize.js)
+ content/browser/sync/customize.css (content/sync/customize.css)
+ content/browser/safeMode.css (content/safeMode.css)
+ content/browser/safeMode.js (content/safeMode.js)
+ content/browser/safeMode.xul (content/safeMode.xul)
+ content/browser/sanitize.js (content/sanitize.js)
+* content/browser/sanitize.xul (content/sanitize.xul)
+* content/browser/sanitizeDialog.js (content/sanitizeDialog.js)
+ content/browser/sanitizeDialog.css (content/sanitizeDialog.css)
+ content/browser/contentSearchUI.js (content/contentSearchUI.js)
+ content/browser/contentSearchUI.css (content/contentSearchUI.css)
+ content/browser/tabbrowser.css (content/tabbrowser.css)
+ content/browser/tabbrowser.xml (content/tabbrowser.xml)
+ content/browser/urlbarBindings.xml (content/urlbarBindings.xml)
+ content/browser/utilityOverlay.js (content/utilityOverlay.js)
+ content/browser/usercontext.svg (content/usercontext.svg)
+ content/browser/web-panels.js (content/web-panels.js)
+* content/browser/web-panels.xul (content/web-panels.xul)
+* content/browser/baseMenuOverlay.xul (content/baseMenuOverlay.xul)
+* content/browser/nsContextMenu.js (content/nsContextMenu.js)
+# XXX: We should exclude this one as well (bug 71895)
+* content/browser/hiddenWindow.xul (content/hiddenWindow.xul)
+#ifdef XP_MACOSX
+* content/browser/macBrowserOverlay.xul (content/macBrowserOverlay.xul)
+* content/browser/downloadManagerOverlay.xul (content/downloadManagerOverlay.xul)
+* content/browser/softwareUpdateOverlay.xul (content/softwareUpdateOverlay.xul)
+#endif
+* content/browser/viewSourceOverlay.xul (content/viewSourceOverlay.xul)
+#ifndef XP_MACOSX
+* content/browser/webrtcIndicator.xul (content/webrtcIndicator.xul)
+ content/browser/webrtcIndicator.js (content/webrtcIndicator.js)
+#endif
+#ifdef XP_WIN
+ content/browser/win6BrowserOverlay.xul (content/win6BrowserOverlay.xul)
+#endif
+# the following files are browser-specific overrides
+* content/browser/license.html (/toolkit/content/license.html)
+% override chrome://global/content/license.html chrome://browser/content/license.html
+ content/browser/report-phishing-overlay.xul (content/report-phishing-overlay.xul)
+ content/browser/blockedSite.xhtml (content/blockedSite.xhtml)
+% overlay chrome://browser/content/browser.xul chrome://browser/content/report-phishing-overlay.xul
+
+% override chrome://global/content/netError.xhtml chrome://browser/content/aboutNetError.xhtml
diff --git a/browser/base/moz.build b/browser/base/moz.build
new file mode 100644
index 000000000..466349992
--- /dev/null
+++ b/browser/base/moz.build
@@ -0,0 +1,50 @@
+# -*- 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['sslerrorreport'] = 'content/docs/sslerrorreport'
+
+MOCHITEST_MANIFESTS += [
+ 'content/test/general/mochitest.ini',
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ 'content/test/chrome/chrome.ini',
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ 'content/test/alerts/browser.ini',
+ 'content/test/captivePortal/browser.ini',
+ 'content/test/general/browser.ini',
+ 'content/test/newtab/browser.ini',
+ 'content/test/plugins/browser.ini',
+ 'content/test/popupNotifications/browser.ini',
+ 'content/test/popups/browser.ini',
+ 'content/test/referrer/browser.ini',
+ 'content/test/siteIdentity/browser.ini',
+ 'content/test/social/browser.ini',
+ 'content/test/tabcrashed/browser.ini',
+ 'content/test/tabPrompts/browser.ini',
+ 'content/test/tabs/browser.ini',
+ 'content/test/urlbar/browser.ini',
+ 'content/test/webrtc/browser.ini',
+]
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_VERSION_DISPLAY'] = CONFIG['MOZ_APP_VERSION_DISPLAY']
+
+DEFINES['APP_LICENSE_BLOCK'] = '%s/content/overrides/app-license.html' % SRCDIR
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3', 'cocoa'):
+ DEFINES['HAVE_SHELL_SERVICE'] = 1
+ DEFINES['CONTEXT_COPY_IMAGE_CONTENTS'] = 1
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'):
+ DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'):
+ DEFINES['MENUBAR_CAN_AUTOHIDE'] = 1
+
+JAR_MANIFESTS += ['jar.mn']