From b3a0b467c64f9313040d71da38610351eb6c08ea Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Tue, 12 Feb 2013 14:51:25 -0600 Subject: [PATCH] Bug 750903 - Land browser/metro/* r=mbrubeck --- browser/metro/Makefile.in | 51 + browser/metro/base/Makefile.in | 20 + browser/metro/base/content/AnimatedZoom.js | 124 + .../metro/base/content/BrowserTouchHandler.js | 91 + browser/metro/base/content/ContextCommands.js | 301 +++ .../metro/base/content/LoginManagerChild.js | 818 +++++++ browser/metro/base/content/PageActions.js | 180 ++ browser/metro/base/content/RemoteTabs.js | 144 ++ browser/metro/base/content/TopSites.js | 201 ++ browser/metro/base/content/Util.js | 412 ++++ browser/metro/base/content/WebProgress.js | 209 ++ browser/metro/base/content/appbar.js | 230 ++ .../metro/base/content/bindings/appbar.xml | 89 + .../metro/base/content/bindings/arrowbox.xml | 274 +++ .../base/content/bindings/autocomplete.xml | 445 ++++ .../metro/base/content/bindings/bindings.xml | 407 ++++ .../metro/base/content/bindings/browser.js | 796 +++++++ .../metro/base/content/bindings/browser.xml | 1089 +++++++++ .../metro/base/content/bindings/console.xml | 61 + .../metro/base/content/bindings/dialog.xml | 109 + .../metro/base/content/bindings/downloads.xml | 208 ++ .../base/content/bindings/flyoutpanel.xml | 95 + browser/metro/base/content/bindings/grid.xml | 556 +++++ .../base/content/bindings/pageaction.xml | 22 + .../content/bindings/selectionoverlay.xml | 126 ++ browser/metro/base/content/bindings/tabs.xml | 173 ++ .../base/content/bindings/toggleswitch.xml | 144 ++ browser/metro/base/content/bookmarks.js | 364 +++ browser/metro/base/content/browser-scripts.js | 170 ++ browser/metro/base/content/browser-ui.js | 1638 ++++++++++++++ browser/metro/base/content/browser.css | 221 ++ browser/metro/base/content/browser.js | 1986 +++++++++++++++++ browser/metro/base/content/browser.xul | 691 ++++++ browser/metro/base/content/commandUtil.js | 164 ++ browser/metro/base/content/config.js | 391 ++++ browser/metro/base/content/config.xul | 65 + browser/metro/base/content/console.js | 298 +++ .../contenthandlers/ConsoleAPIObserver.js | 121 + .../base/content/contenthandlers/Content.js | 696 ++++++ .../contenthandlers/ContextMenuHandler.js | 360 +++ .../content/contenthandlers/FindHandler.js | 76 + .../content/contenthandlers/FormHelper.js | 972 ++++++++ .../contenthandlers/PluginCTPHandler.js | 52 + .../contenthandlers/SelectionHandler.js | 1185 ++++++++++ .../contenthandlers/ViewportHandler.js | 136 ++ browser/metro/base/content/cursor.css | 14 + browser/metro/base/content/downloads.js | 469 ++++ browser/metro/base/content/exceptions.js | 115 + .../base/content/helperui/AlertsHelper.js | 82 + .../base/content/helperui/CaptureDialog.js | 145 ++ .../base/content/helperui/CapturePickerUI.js | 26 + .../base/content/helperui/CharsetMenu.js | 89 + .../base/content/helperui/FindHelperUI.js | 173 ++ .../base/content/helperui/FormHelperUI.js | 454 ++++ .../metro/base/content/helperui/IdentityUI.js | 289 +++ .../metro/base/content/helperui/IndexedDB.js | 82 + .../base/content/helperui/MasterPasswordUI.js | 129 ++ browser/metro/base/content/helperui/MenuUI.js | 438 ++++ .../base/content/helperui/OfflineApps.js | 85 + .../base/content/helperui/SelectHelperUI.js | 157 ++ .../content/helperui/SelectionHelperUI.js | 678 ++++++ .../metro/base/content/helperui/SharingUI.js | 83 + browser/metro/base/content/history.js | 90 + browser/metro/base/content/input.js | 1212 ++++++++++ browser/metro/base/content/jsshell/shell.html | 770 +++++++ browser/metro/base/content/jsshell/shell.xul | 13 + .../base/content/pages/aboutCertError.xhtml | 241 ++ .../metro/base/content/pages/aboutCrash.xhtml | 32 + .../base/content/pages/aboutRights.xhtml | 95 + .../base/content/pages/blockedSite.xhtml | 192 ++ .../base/content/pages/fullscreen-video.xhtml | 154 ++ .../metro/base/content/pages/netError.xhtml | 365 +++ browser/metro/base/content/preferences.js | 17 + .../base/content/prompt/CaptureDialog.xul | 40 + browser/metro/base/content/prompt/alert.xul | 40 + browser/metro/base/content/prompt/confirm.xul | 41 + .../base/content/prompt/masterPassword.xul | 45 + browser/metro/base/content/prompt/prompt.js | 72 + browser/metro/base/content/prompt/prompt.xul | 43 + .../base/content/prompt/promptPassword.xul | 63 + .../content/prompt/removeMasterPassword.xul | 43 + browser/metro/base/content/prompt/select.xul | 41 + browser/metro/base/content/prompt/share.xul | 27 + browser/metro/base/content/sanitize.js | 315 +++ browser/metro/base/content/sync.js | 678 ++++++ browser/metro/base/content/video.js | 66 + browser/metro/base/jar.mn | 104 + browser/metro/base/tests/Makefile.in | 29 + .../addons/browser_install1_1/bootstrap.js | 9 + .../addons/browser_install1_1/install.rdf | 24 + .../addons/browser_install1_2/install.rdf | 22 + .../addons/browser_install1_3/install.rdf | 23 + .../tests/addons/browser_locale1/boostrap.js | 9 + .../addons/browser_locale1/chrome.manifest | 4 + .../tests/addons/browser_locale1/install.rdf | 24 + .../metro/base/tests/browser_canonizeURL.js | 21 + .../metro/base/tests/browser_context_ui.js | 93 + browser/metro/base/tests/browser_downloads.js | 450 ++++ .../base/tests/browser_onscreen_keyboard.html | 12 + .../base/tests/browser_onscreen_keyboard.js | 54 + .../base/tests/browser_plugin_input.html | 8 + .../tests/browser_plugin_input_keyboard.js | 53 + .../base/tests/browser_plugin_input_mouse.js | 69 + .../metro/base/tests/browser_remotetabs.js | 47 + browser/metro/base/tests/browser_test.js | 30 + browser/metro/base/tests/head.js | 266 +++ browser/metro/components/AboutRedirector.js | 86 + browser/metro/components/AlertsService.js | 27 + browser/metro/components/BrowserCLH.js | 287 +++ browser/metro/components/BrowserStartup.js | 117 + browser/metro/components/CapturePicker.js | 85 + .../components/ContentDispatchChooser.js | 38 + .../components/ContentPermissionPrompt.js | 129 ++ browser/metro/components/DirectoryProvider.js | 58 + browser/metro/components/DownloadManagerUI.js | 46 + browser/metro/components/FormAutoComplete.js | 95 + browser/metro/components/HelperAppDialog.js | 216 ++ browser/metro/components/LoginManager.js | 686 ++++++ .../metro/components/LoginManagerPrompter.idl | 67 + .../metro/components/LoginManagerPrompter.js | 610 +++++ browser/metro/components/Makefile.in | 53 + browser/metro/components/PromptService.js | 917 ++++++++ browser/metro/components/SafeBrowsing.js | 168 ++ browser/metro/components/SessionStore.idl | 83 + browser/metro/components/SessionStore.js | 768 +++++++ browser/metro/components/Sidebar.js | 149 ++ browser/metro/components/UpdatePrompt.js | 243 ++ browser/metro/components/components.manifest | 100 + browser/metro/defs.mk | 2 + browser/metro/locales/Makefile.in | 33 + .../locales/en-US/chrome/aboutCertError.dtd | 38 + .../metro/locales/en-US/chrome/browser.dtd | 107 + .../locales/en-US/chrome/browser.properties | 140 ++ .../metro/locales/en-US/chrome/checkbox.dtd | 6 + browser/metro/locales/en-US/chrome/config.dtd | 16 + .../locales/en-US/chrome/notification.dtd | 12 + .../metro/locales/en-US/chrome/phishing.dtd | 29 + .../locales/en-US/chrome/preferences.dtd | 19 + browser/metro/locales/en-US/chrome/prompt.dtd | 6 + browser/metro/locales/en-US/chrome/sync.dtd | 31 + .../locales/en-US/chrome/sync.properties | 31 + .../en-US/overrides/passwordmgr.properties | 40 + .../locales/generic/profile/bookmarks.json.in | 17 + browser/metro/locales/import/Makefile.in | 56 + browser/metro/locales/jar.mn | 40 + browser/metro/metroapp.ini.in | 22 + browser/metro/modules/Makefile.in | 17 + browser/metro/modules/video.jsm | 9 + browser/metro/profile/Makefile.in | 15 + browser/metro/profile/metro.js | 583 +++++ browser/metro/shell/Makefile.in | 33 + .../metro/shell/VisualElementsManifest.xml.in | 16 + .../shell/commandexecutehandler/CEHHelper.cpp | 166 ++ .../shell/commandexecutehandler/CEHHelper.h | 29 + .../CommandExecuteHandler.cpp | 707 ++++++ .../CommandExecuteHandler.def | 6 + .../shell/commandexecutehandler/Makefile.in | 42 + browser/metro/shell/linktool/Makefile.in | 33 + browser/metro/shell/linktool/linktool.cpp | 286 +++ browser/metro/shell/priconfig.xml | 44 + browser/metro/shell/resources.pri | Bin 0 -> 6264 bytes browser/metro/shell/testing/Makefile.in | 35 + .../metro/shell/testing/metrotestharness.cpp | 260 +++ .../metro/shell/tileresources/Resources.resw | 64 + .../metro/shell/tileresources/layout.resfiles | 47 + .../shell/tileresources/resources.resfiles | 1 + browser/metro/theme/Makefile.in | 12 + browser/metro/theme/about.css | 50 + browser/metro/theme/aboutPage.css | 72 + browser/metro/theme/browser.css | 1131 ++++++++++ browser/metro/theme/config.css | 97 + browser/metro/theme/content.css | 320 +++ browser/metro/theme/defines.inc | 120 + browser/metro/theme/flyoutpanel.css | 71 + browser/metro/theme/forms.css | 256 +++ browser/metro/theme/header.css | 14 + browser/metro/theme/images/about-footer.png | Bin 0 -> 129698 bytes .../metro/theme/images/aboutBackground.jpg | Bin 0 -> 8436 bytes .../metro/theme/images/alert-downloads-30.png | Bin 0 -> 416 bytes browser/metro/theme/images/appbar-icons.png | Bin 0 -> 8280 bytes browser/metro/theme/images/arrowbox-down.png | Bin 0 -> 701 bytes browser/metro/theme/images/arrowbox-horiz.png | Bin 0 -> 1481 bytes browser/metro/theme/images/arrowbox-up.png | Bin 0 -> 1479 bytes browser/metro/theme/images/arrowdown-16.png | Bin 0 -> 246 bytes .../metro/theme/images/arrowdowndark-16.png | Bin 0 -> 286 bytes browser/metro/theme/images/arrowleft-16.png | Bin 0 -> 280 bytes .../metro/theme/images/arrowleftdark-16.png | Bin 0 -> 210 bytes browser/metro/theme/images/arrowright-16.png | Bin 0 -> 235 bytes .../metro/theme/images/arrowrightdark-16.png | Bin 0 -> 206 bytes browser/metro/theme/images/arrowup-16.png | Bin 0 -> 242 bytes browser/metro/theme/images/arrowupdark-16.png | Bin 0 -> 301 bytes browser/metro/theme/images/back.png | Bin 0 -> 2260 bytes browser/metro/theme/images/button-bg.png | Bin 0 -> 191 bytes browser/metro/theme/images/check-30.png | Bin 0 -> 447 bytes .../metro/theme/images/check-selected-30.png | Bin 0 -> 839 bytes .../theme/images/check-unselected-30.png | Bin 0 -> 675 bytes browser/metro/theme/images/checkmark-hdpi.png | Bin 0 -> 537 bytes .../metro/theme/images/closetab-default.png | Bin 0 -> 1161 bytes browser/metro/theme/images/closetab-tab.png | Bin 0 -> 1193 bytes .../theme/images/closetab-tabselected.png | Bin 0 -> 1363 bytes .../theme/images/errorpage-larry-black.png | Bin 0 -> 850 bytes .../theme/images/errorpage-larry-white.png | Bin 0 -> 886 bytes .../metro/theme/images/errorpage-warning.png | Bin 0 -> 631 bytes .../metro/theme/images/favicon-default-32.png | Bin 0 -> 268 bytes .../metro/theme/images/firefox-watermark.png | Bin 0 -> 8057 bytes .../metro/theme/images/flyout-back-button.png | Bin 0 -> 1863 bytes browser/metro/theme/images/forward.png | Bin 0 -> 1336 bytes .../theme/images/homescreen-blank-hdpi.png | Bin 0 -> 2634 bytes .../theme/images/homescreen-default-hdpi.png | Bin 0 -> 6619 bytes .../theme/images/identity-default-hdpi.png | Bin 0 -> 1291 bytes .../metro/theme/images/identity-ev-hdpi.png | Bin 0 -> 2353 bytes .../theme/images/identity-icons-generic.png | Bin 0 -> 1748 bytes .../theme/images/identity-icons-https-ev.png | Bin 0 -> 520 bytes .../images/identity-icons-https-mixed.png | Bin 0 -> 442 bytes .../theme/images/identity-icons-https.png | Bin 0 -> 500 bytes .../metro/theme/images/identity-ssl-hdpi.png | Bin 0 -> 2440 bytes browser/metro/theme/images/loading.png | Bin 0 -> 10727 bytes browser/metro/theme/images/locked-hdpi.png | Bin 0 -> 1479 bytes browser/metro/theme/images/more-active.png | Bin 0 -> 1598 bytes browser/metro/theme/images/mozilla-32.png | Bin 0 -> 4363 bytes browser/metro/theme/images/mute-hdpi.png | Bin 0 -> 2847 bytes browser/metro/theme/images/newtab-default.png | Bin 0 -> 1105 bytes browser/metro/theme/images/panel-dark.png | Bin 0 -> 6463 bytes browser/metro/theme/images/panel-light.png | Bin 0 -> 484 bytes browser/metro/theme/images/pause-hdpi.png | Bin 0 -> 1359 bytes browser/metro/theme/images/play-hdpi.png | Bin 0 -> 2373 bytes browser/metro/theme/images/popup-bg-hdpi.png | Bin 0 -> 4277 bytes .../theme/images/popup-selected-item-hdpi.png | Bin 0 -> 486 bytes browser/metro/theme/images/reload.png | Bin 0 -> 1686 bytes browser/metro/theme/images/scrubber-hdpi.png | Bin 0 -> 1982 bytes .../metro/theme/images/search-clear-30.png | Bin 0 -> 476 bytes .../metro/theme/images/search-glass-30.png | Bin 0 -> 1133 bytes .../theme/images/section-collapsed-16.png | Bin 0 -> 776 bytes .../theme/images/section-expanded-16.png | Bin 0 -> 767 bytes .../metro/theme/images/selection-monocle.png | Bin 0 -> 3026 bytes browser/metro/theme/images/stop-hdpi.png | Bin 0 -> 737 bytes browser/metro/theme/images/tab-arrows.png | Bin 0 -> 12484 bytes browser/metro/theme/images/tab-crop.png | Bin 0 -> 70702 bytes browser/metro/theme/images/tab-overlay.png | Bin 0 -> 2253 bytes .../metro/theme/images/tab-selection-left.png | Bin 0 -> 9206 bytes .../theme/images/tab-selection-right.png | Bin 0 -> 9306 bytes browser/metro/theme/images/throbber.png | Bin 0 -> 11862 bytes browser/metro/theme/images/unlocked-hdpi.png | Bin 0 -> 1385 bytes browser/metro/theme/images/unmute-hdpi.png | Bin 0 -> 956 bytes browser/metro/theme/jar.mn | 90 + browser/metro/theme/netError.css | 131 ++ browser/metro/theme/platform.css | 522 +++++ browser/metro/theme/touchcontrols.css | 191 ++ 248 files changed, 37658 insertions(+) create mode 100644 browser/metro/Makefile.in create mode 100644 browser/metro/base/Makefile.in create mode 100644 browser/metro/base/content/AnimatedZoom.js create mode 100644 browser/metro/base/content/BrowserTouchHandler.js create mode 100644 browser/metro/base/content/ContextCommands.js create mode 100644 browser/metro/base/content/LoginManagerChild.js create mode 100644 browser/metro/base/content/PageActions.js create mode 100644 browser/metro/base/content/RemoteTabs.js create mode 100644 browser/metro/base/content/TopSites.js create mode 100644 browser/metro/base/content/Util.js create mode 100644 browser/metro/base/content/WebProgress.js create mode 100644 browser/metro/base/content/appbar.js create mode 100644 browser/metro/base/content/bindings/appbar.xml create mode 100644 browser/metro/base/content/bindings/arrowbox.xml create mode 100644 browser/metro/base/content/bindings/autocomplete.xml create mode 100644 browser/metro/base/content/bindings/bindings.xml create mode 100644 browser/metro/base/content/bindings/browser.js create mode 100644 browser/metro/base/content/bindings/browser.xml create mode 100644 browser/metro/base/content/bindings/console.xml create mode 100644 browser/metro/base/content/bindings/dialog.xml create mode 100644 browser/metro/base/content/bindings/downloads.xml create mode 100644 browser/metro/base/content/bindings/flyoutpanel.xml create mode 100644 browser/metro/base/content/bindings/grid.xml create mode 100644 browser/metro/base/content/bindings/pageaction.xml create mode 100644 browser/metro/base/content/bindings/selectionoverlay.xml create mode 100644 browser/metro/base/content/bindings/tabs.xml create mode 100644 browser/metro/base/content/bindings/toggleswitch.xml create mode 100644 browser/metro/base/content/bookmarks.js create mode 100644 browser/metro/base/content/browser-scripts.js create mode 100644 browser/metro/base/content/browser-ui.js create mode 100644 browser/metro/base/content/browser.css create mode 100644 browser/metro/base/content/browser.js create mode 100644 browser/metro/base/content/browser.xul create mode 100644 browser/metro/base/content/commandUtil.js create mode 100644 browser/metro/base/content/config.js create mode 100644 browser/metro/base/content/config.xul create mode 100644 browser/metro/base/content/console.js create mode 100644 browser/metro/base/content/contenthandlers/ConsoleAPIObserver.js create mode 100644 browser/metro/base/content/contenthandlers/Content.js create mode 100644 browser/metro/base/content/contenthandlers/ContextMenuHandler.js create mode 100644 browser/metro/base/content/contenthandlers/FindHandler.js create mode 100644 browser/metro/base/content/contenthandlers/FormHelper.js create mode 100644 browser/metro/base/content/contenthandlers/PluginCTPHandler.js create mode 100644 browser/metro/base/content/contenthandlers/SelectionHandler.js create mode 100644 browser/metro/base/content/contenthandlers/ViewportHandler.js create mode 100644 browser/metro/base/content/cursor.css create mode 100644 browser/metro/base/content/downloads.js create mode 100644 browser/metro/base/content/exceptions.js create mode 100644 browser/metro/base/content/helperui/AlertsHelper.js create mode 100644 browser/metro/base/content/helperui/CaptureDialog.js create mode 100644 browser/metro/base/content/helperui/CapturePickerUI.js create mode 100644 browser/metro/base/content/helperui/CharsetMenu.js create mode 100644 browser/metro/base/content/helperui/FindHelperUI.js create mode 100644 browser/metro/base/content/helperui/FormHelperUI.js create mode 100644 browser/metro/base/content/helperui/IdentityUI.js create mode 100644 browser/metro/base/content/helperui/IndexedDB.js create mode 100644 browser/metro/base/content/helperui/MasterPasswordUI.js create mode 100644 browser/metro/base/content/helperui/MenuUI.js create mode 100644 browser/metro/base/content/helperui/OfflineApps.js create mode 100644 browser/metro/base/content/helperui/SelectHelperUI.js create mode 100644 browser/metro/base/content/helperui/SelectionHelperUI.js create mode 100644 browser/metro/base/content/helperui/SharingUI.js create mode 100644 browser/metro/base/content/history.js create mode 100644 browser/metro/base/content/input.js create mode 100644 browser/metro/base/content/jsshell/shell.html create mode 100644 browser/metro/base/content/jsshell/shell.xul create mode 100644 browser/metro/base/content/pages/aboutCertError.xhtml create mode 100644 browser/metro/base/content/pages/aboutCrash.xhtml create mode 100644 browser/metro/base/content/pages/aboutRights.xhtml create mode 100644 browser/metro/base/content/pages/blockedSite.xhtml create mode 100644 browser/metro/base/content/pages/fullscreen-video.xhtml create mode 100644 browser/metro/base/content/pages/netError.xhtml create mode 100644 browser/metro/base/content/preferences.js create mode 100644 browser/metro/base/content/prompt/CaptureDialog.xul create mode 100644 browser/metro/base/content/prompt/alert.xul create mode 100644 browser/metro/base/content/prompt/confirm.xul create mode 100644 browser/metro/base/content/prompt/masterPassword.xul create mode 100644 browser/metro/base/content/prompt/prompt.js create mode 100644 browser/metro/base/content/prompt/prompt.xul create mode 100644 browser/metro/base/content/prompt/promptPassword.xul create mode 100644 browser/metro/base/content/prompt/removeMasterPassword.xul create mode 100644 browser/metro/base/content/prompt/select.xul create mode 100644 browser/metro/base/content/prompt/share.xul create mode 100644 browser/metro/base/content/sanitize.js create mode 100644 browser/metro/base/content/sync.js create mode 100644 browser/metro/base/content/video.js create mode 100644 browser/metro/base/jar.mn create mode 100644 browser/metro/base/tests/Makefile.in create mode 100644 browser/metro/base/tests/addons/browser_install1_1/bootstrap.js create mode 100644 browser/metro/base/tests/addons/browser_install1_1/install.rdf create mode 100644 browser/metro/base/tests/addons/browser_install1_2/install.rdf create mode 100644 browser/metro/base/tests/addons/browser_install1_3/install.rdf create mode 100644 browser/metro/base/tests/addons/browser_locale1/boostrap.js create mode 100644 browser/metro/base/tests/addons/browser_locale1/chrome.manifest create mode 100644 browser/metro/base/tests/addons/browser_locale1/install.rdf create mode 100644 browser/metro/base/tests/browser_canonizeURL.js create mode 100644 browser/metro/base/tests/browser_context_ui.js create mode 100644 browser/metro/base/tests/browser_downloads.js create mode 100644 browser/metro/base/tests/browser_onscreen_keyboard.html create mode 100644 browser/metro/base/tests/browser_onscreen_keyboard.js create mode 100644 browser/metro/base/tests/browser_plugin_input.html create mode 100644 browser/metro/base/tests/browser_plugin_input_keyboard.js create mode 100644 browser/metro/base/tests/browser_plugin_input_mouse.js create mode 100644 browser/metro/base/tests/browser_remotetabs.js create mode 100644 browser/metro/base/tests/browser_test.js create mode 100644 browser/metro/base/tests/head.js create mode 100644 browser/metro/components/AboutRedirector.js create mode 100644 browser/metro/components/AlertsService.js create mode 100644 browser/metro/components/BrowserCLH.js create mode 100644 browser/metro/components/BrowserStartup.js create mode 100644 browser/metro/components/CapturePicker.js create mode 100644 browser/metro/components/ContentDispatchChooser.js create mode 100644 browser/metro/components/ContentPermissionPrompt.js create mode 100644 browser/metro/components/DirectoryProvider.js create mode 100644 browser/metro/components/DownloadManagerUI.js create mode 100644 browser/metro/components/FormAutoComplete.js create mode 100644 browser/metro/components/HelperAppDialog.js create mode 100644 browser/metro/components/LoginManager.js create mode 100644 browser/metro/components/LoginManagerPrompter.idl create mode 100644 browser/metro/components/LoginManagerPrompter.js create mode 100644 browser/metro/components/Makefile.in create mode 100644 browser/metro/components/PromptService.js create mode 100644 browser/metro/components/SafeBrowsing.js create mode 100644 browser/metro/components/SessionStore.idl create mode 100644 browser/metro/components/SessionStore.js create mode 100644 browser/metro/components/Sidebar.js create mode 100644 browser/metro/components/UpdatePrompt.js create mode 100644 browser/metro/components/components.manifest create mode 100644 browser/metro/defs.mk create mode 100644 browser/metro/locales/Makefile.in create mode 100644 browser/metro/locales/en-US/chrome/aboutCertError.dtd create mode 100644 browser/metro/locales/en-US/chrome/browser.dtd create mode 100644 browser/metro/locales/en-US/chrome/browser.properties create mode 100644 browser/metro/locales/en-US/chrome/checkbox.dtd create mode 100644 browser/metro/locales/en-US/chrome/config.dtd create mode 100644 browser/metro/locales/en-US/chrome/notification.dtd create mode 100644 browser/metro/locales/en-US/chrome/phishing.dtd create mode 100644 browser/metro/locales/en-US/chrome/preferences.dtd create mode 100644 browser/metro/locales/en-US/chrome/prompt.dtd create mode 100644 browser/metro/locales/en-US/chrome/sync.dtd create mode 100644 browser/metro/locales/en-US/chrome/sync.properties create mode 100644 browser/metro/locales/en-US/overrides/passwordmgr.properties create mode 100644 browser/metro/locales/generic/profile/bookmarks.json.in create mode 100644 browser/metro/locales/import/Makefile.in create mode 100644 browser/metro/locales/jar.mn create mode 100644 browser/metro/metroapp.ini.in create mode 100644 browser/metro/modules/Makefile.in create mode 100644 browser/metro/modules/video.jsm create mode 100644 browser/metro/profile/Makefile.in create mode 100644 browser/metro/profile/metro.js create mode 100644 browser/metro/shell/Makefile.in create mode 100644 browser/metro/shell/VisualElementsManifest.xml.in create mode 100644 browser/metro/shell/commandexecutehandler/CEHHelper.cpp create mode 100644 browser/metro/shell/commandexecutehandler/CEHHelper.h create mode 100644 browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp create mode 100644 browser/metro/shell/commandexecutehandler/CommandExecuteHandler.def create mode 100644 browser/metro/shell/commandexecutehandler/Makefile.in create mode 100644 browser/metro/shell/linktool/Makefile.in create mode 100644 browser/metro/shell/linktool/linktool.cpp create mode 100644 browser/metro/shell/priconfig.xml create mode 100644 browser/metro/shell/resources.pri create mode 100644 browser/metro/shell/testing/Makefile.in create mode 100644 browser/metro/shell/testing/metrotestharness.cpp create mode 100644 browser/metro/shell/tileresources/Resources.resw create mode 100644 browser/metro/shell/tileresources/layout.resfiles create mode 100644 browser/metro/shell/tileresources/resources.resfiles create mode 100644 browser/metro/theme/Makefile.in create mode 100644 browser/metro/theme/about.css create mode 100644 browser/metro/theme/aboutPage.css create mode 100644 browser/metro/theme/browser.css create mode 100644 browser/metro/theme/config.css create mode 100644 browser/metro/theme/content.css create mode 100644 browser/metro/theme/defines.inc create mode 100644 browser/metro/theme/flyoutpanel.css create mode 100644 browser/metro/theme/forms.css create mode 100644 browser/metro/theme/header.css create mode 100644 browser/metro/theme/images/about-footer.png create mode 100644 browser/metro/theme/images/aboutBackground.jpg create mode 100644 browser/metro/theme/images/alert-downloads-30.png create mode 100644 browser/metro/theme/images/appbar-icons.png create mode 100644 browser/metro/theme/images/arrowbox-down.png create mode 100644 browser/metro/theme/images/arrowbox-horiz.png create mode 100644 browser/metro/theme/images/arrowbox-up.png create mode 100644 browser/metro/theme/images/arrowdown-16.png create mode 100644 browser/metro/theme/images/arrowdowndark-16.png create mode 100644 browser/metro/theme/images/arrowleft-16.png create mode 100644 browser/metro/theme/images/arrowleftdark-16.png create mode 100644 browser/metro/theme/images/arrowright-16.png create mode 100644 browser/metro/theme/images/arrowrightdark-16.png create mode 100644 browser/metro/theme/images/arrowup-16.png create mode 100644 browser/metro/theme/images/arrowupdark-16.png create mode 100644 browser/metro/theme/images/back.png create mode 100644 browser/metro/theme/images/button-bg.png create mode 100644 browser/metro/theme/images/check-30.png create mode 100644 browser/metro/theme/images/check-selected-30.png create mode 100644 browser/metro/theme/images/check-unselected-30.png create mode 100644 browser/metro/theme/images/checkmark-hdpi.png create mode 100644 browser/metro/theme/images/closetab-default.png create mode 100644 browser/metro/theme/images/closetab-tab.png create mode 100644 browser/metro/theme/images/closetab-tabselected.png create mode 100644 browser/metro/theme/images/errorpage-larry-black.png create mode 100644 browser/metro/theme/images/errorpage-larry-white.png create mode 100644 browser/metro/theme/images/errorpage-warning.png create mode 100644 browser/metro/theme/images/favicon-default-32.png create mode 100644 browser/metro/theme/images/firefox-watermark.png create mode 100644 browser/metro/theme/images/flyout-back-button.png create mode 100644 browser/metro/theme/images/forward.png create mode 100644 browser/metro/theme/images/homescreen-blank-hdpi.png create mode 100644 browser/metro/theme/images/homescreen-default-hdpi.png create mode 100644 browser/metro/theme/images/identity-default-hdpi.png create mode 100644 browser/metro/theme/images/identity-ev-hdpi.png create mode 100644 browser/metro/theme/images/identity-icons-generic.png create mode 100644 browser/metro/theme/images/identity-icons-https-ev.png create mode 100644 browser/metro/theme/images/identity-icons-https-mixed.png create mode 100644 browser/metro/theme/images/identity-icons-https.png create mode 100644 browser/metro/theme/images/identity-ssl-hdpi.png create mode 100644 browser/metro/theme/images/loading.png create mode 100644 browser/metro/theme/images/locked-hdpi.png create mode 100644 browser/metro/theme/images/more-active.png create mode 100644 browser/metro/theme/images/mozilla-32.png create mode 100644 browser/metro/theme/images/mute-hdpi.png create mode 100644 browser/metro/theme/images/newtab-default.png create mode 100644 browser/metro/theme/images/panel-dark.png create mode 100644 browser/metro/theme/images/panel-light.png create mode 100644 browser/metro/theme/images/pause-hdpi.png create mode 100644 browser/metro/theme/images/play-hdpi.png create mode 100644 browser/metro/theme/images/popup-bg-hdpi.png create mode 100644 browser/metro/theme/images/popup-selected-item-hdpi.png create mode 100644 browser/metro/theme/images/reload.png create mode 100644 browser/metro/theme/images/scrubber-hdpi.png create mode 100644 browser/metro/theme/images/search-clear-30.png create mode 100644 browser/metro/theme/images/search-glass-30.png create mode 100644 browser/metro/theme/images/section-collapsed-16.png create mode 100644 browser/metro/theme/images/section-expanded-16.png create mode 100644 browser/metro/theme/images/selection-monocle.png create mode 100644 browser/metro/theme/images/stop-hdpi.png create mode 100644 browser/metro/theme/images/tab-arrows.png create mode 100644 browser/metro/theme/images/tab-crop.png create mode 100644 browser/metro/theme/images/tab-overlay.png create mode 100644 browser/metro/theme/images/tab-selection-left.png create mode 100644 browser/metro/theme/images/tab-selection-right.png create mode 100644 browser/metro/theme/images/throbber.png create mode 100644 browser/metro/theme/images/unlocked-hdpi.png create mode 100644 browser/metro/theme/images/unmute-hdpi.png create mode 100644 browser/metro/theme/jar.mn create mode 100644 browser/metro/theme/netError.css create mode 100644 browser/metro/theme/platform.css create mode 100644 browser/metro/theme/touchcontrols.css diff --git a/browser/metro/Makefile.in b/browser/metro/Makefile.in new file mode 100644 index 000000000000..cd6c34e7f688 --- /dev/null +++ b/browser/metro/Makefile.in @@ -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/. + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/config/config.mk + +DIRS = \ + base \ + components \ + modules \ + theme \ + profile \ + locales \ + $(NULL) + +ifeq ($(OS_ARCH),WINNT) +DIRS += shell +endif + +TEST_DIRS += base/tests + +include $(topsrcdir)/config/rules.mk + +######################################### +# application.ini + +ifdef MOZILLA_OFFICIAL +DEFINES += -DMOZILLA_OFFICIAL +endif + +GRE_MILESTONE := $(shell tail -n 1 $(topsrcdir)/config/milestone.txt 2>/dev/null || tail -1 $(topsrcdir)/config/milestone.txt) +GRE_BUILDID := $(shell cat $(DEPTH)/config/buildid) +DEFINES += -DGRE_MILESTONE=$(GRE_MILESTONE) -DGRE_BUILDID=$(GRE_BUILDID) + +# 'application.ini' breaks firefox build config. So we use something different. +metroapp.ini: metroapp.ini.in $(DEPTH)/config/buildid $(topsrcdir)/config/milestone.txt + $(RM) "metroapp.ini" + $(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $< > $@ + +libs:: metroapp.ini + $(INSTALL) metroapp.ini $(FINAL_TARGET) + +######################################### + +GARBAGE += metroapp.ini diff --git a/browser/metro/base/Makefile.in b/browser/metro/base/Makefile.in new file mode 100644 index 000000000000..6cf5adc48bd5 --- /dev/null +++ b/browser/metro/base/Makefile.in @@ -0,0 +1,20 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/config/config.mk + +DIST_SUBDIR=metro + +DEFINES += -DAB_CD=$(MOZ_UI_LOCALE) \ + -DPACKAGE=browser \ + -DMOZ_APP_VERSION=$(MOZ_APP_VERSION) \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/browser/metro/base/content/AnimatedZoom.js b/browser/metro/base/content/AnimatedZoom.js new file mode 100644 index 000000000000..44108fe3b41d --- /dev/null +++ b/browser/metro/base/content/AnimatedZoom.js @@ -0,0 +1,124 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Responsible for zooming in to a given view rectangle + */ +const AnimatedZoom = { + startScale: null, + + /** Starts an animated zoom to zoomRect. */ + animateTo: function(aZoomRect) { + if (!aZoomRect) + return; + + this.zoomTo = aZoomRect.clone(); + + if (this.animationDuration === undefined) + this.animationDuration = Services.prefs.getIntPref("browser.ui.zoom.animationDuration"); + + Browser.forceChromeReflow(); + + this.start(); + + // Check if zooming animations were occuring before. + if (!this.zoomRect) { + this.updateTo(this.zoomFrom); + + mozRequestAnimationFrame(this); + + let event = document.createEvent("Events"); + event.initEvent("AnimatedZoomBegin", true, true); + window.dispatchEvent(event); + } + }, + + start: function start() { + this.tab = Browser.selectedTab; + this.browser = this.tab.browser; + this.bcr = this.browser.getBoundingClientRect(); + this.zoomFrom = this.zoomRect || this.getStartRect(); + this.startScale = this.browser.scale; + this.beginTime = mozAnimationStartTime; + }, + + /** Get the visible rect, in device pixels relative to the content origin. */ + getStartRect: function getStartRect() { + let browser = this.browser; + let scroll = browser.getRootView().getPosition(); + return new Rect(scroll.x, scroll.y, this.bcr.width, this.bcr.height); + }, + + /** Update the visible rect, in device pixels relative to the content origin. */ + updateTo: function(nextRect) { + // Stop animating if the browser has been destroyed + if (typeof this.browser.fuzzyZoom !== "function") { + this.reset(); + return false; + } + + let zoomRatio = this.bcr.width / nextRect.width; + let scale = this.startScale * zoomRatio; + let scrollX = nextRect.left * zoomRatio; + let scrollY = nextRect.top * zoomRatio; + + this.browser.fuzzyZoom(scale, scrollX, scrollY); + + this.zoomRect = nextRect; + return true; + }, + + /** Stop animation, zoom to point, and clean up. */ + finish: function() { + if (!this.updateTo(this.zoomTo || this.zoomRect)) + return; + + // Check whether the zoom limits have changed since the animation started. + let browser = this.browser; + let finalScale = this.tab.clampZoomLevel(browser.scale); + if (browser.scale != finalScale) + browser.scale = finalScale; // scale= calls finishFuzzyZoom. + else + browser.finishFuzzyZoom(); + + this.reset(); + browser._updateCSSViewport(); + }, + + reset: function reset() { + this.beginTime = null; + this.zoomTo = null; + this.zoomFrom = null; + this.zoomRect = null; + this.startScale = null; + + let event = document.createEvent("Events"); + event.initEvent("AnimatedZoomEnd", true, true); + window.dispatchEvent(event); + }, + + isZooming: function isZooming() { + return this.beginTime != null; + }, + + sample: function(aTimeStamp) { + try { + let tdiff = aTimeStamp - this.beginTime; + let counter = tdiff / this.animationDuration; + if (counter < 1) { + // update browser to interpolated rectangle + let rect = this.zoomFrom.blend(this.zoomTo, counter); + if (this.updateTo(rect)) + mozRequestAnimationFrame(this); + } else { + // last cycle already rendered final scaled image, now clean up + this.finish(); + } + } catch(e) { + this.finish(); + throw e; + } + } +}; diff --git a/browser/metro/base/content/BrowserTouchHandler.js b/browser/metro/base/content/BrowserTouchHandler.js new file mode 100644 index 000000000000..f88e9252a9c5 --- /dev/null +++ b/browser/metro/base/content/BrowserTouchHandler.js @@ -0,0 +1,91 @@ +/* + * BrowserTouchHandler + * + * Receives touch events from our input event capturing in input.js + * and relays appropriate events to content. Also handles requests + * from content for UI. + */ + +const BrowserTouchHandler = { + _debugEvents: false, + + init: function init() { + // Misc. events we catch during the bubbling phase + document.addEventListener("PopupChanged", this, false); + document.addEventListener("CancelTouchSequence", this, false); + + // Messages sent from content.js + messageManager.addMessageListener("Content:ContextMenu", this); + }, + + // Content forwarding the contextmenu command + onContentContextMenu: function onContentContextMenu(aMessage) { + let contextInfo = { name: aMessage.name, + json: aMessage.json, + target: aMessage.target }; + // Touch input selection handling + if (!InputSourceHelper.isPrecise) { + if (SelectionHelperUI.isActive()) { + // Selection handler is active. + if (aMessage.json.types.indexOf("selected-text") != -1) { + // long tap on existing selection. The incoming message has the + // string data, so reset the selection handler and invoke the + // context menu. + SelectionHelperUI.closeEditSession(); + } else { + // Weird, context menu request with no selected text and + // SelectionHelperUI is active? Might be a bug, warn. Fall + // through anyway, the context menu handler will look in the + // incoming message for content types it knows how to handle. + Util.dumpLn("long tap on empty selection with SelectionHelperUI active?"); + SelectionHelperUI.closeEditSession(); + } + } else if (SelectionHelperUI.canHandle(aMessage)) { + SelectionHelperUI.openEditSession(aMessage); + return; + } + } + + // Check to see if we have context menu item(s) that apply to what + // was clicked on. + if (ContextMenuUI.showContextMenu(contextInfo)) { + let event = document.createEvent("Events"); + event.initEvent("CancelTouchSequence", true, false); + document.dispatchEvent(event); + } else { + // Send the MozEdgeUIGesture to input.js to + // toggle the context ui. + let event = document.createEvent("Events"); + event.initEvent("MozEdgeUIGesture", true, false); + window.dispatchEvent(event); + } + }, + + /* + * Events + */ + + handleEvent: function handleEvent(aEvent) { + // ignore content events we generate + if (this._debugEvents) + Util.dumpLn("BrowserTouchHandler:", aEvent.type); + + switch (aEvent.type) { + case "PopupChanged": + case "CancelTouchSequence": + if (!aEvent.detail) + ContextMenuUI.reset(); + break; + } + }, + + receiveMessage: function receiveMessage(aMessage) { + if (this._debugEvents) Util.dumpLn("BrowserTouchHandler:", aMessage.name); + switch (aMessage.name) { + // Content forwarding the contextmenu command + case "Content:ContextMenu": + this.onContentContextMenu(aMessage); + break; + } + }, +}; diff --git a/browser/metro/base/content/ContextCommands.js b/browser/metro/base/content/ContextCommands.js new file mode 100644 index 000000000000..d149421f397f --- /dev/null +++ b/browser/metro/base/content/ContextCommands.js @@ -0,0 +1,301 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /* + * context menu command handlers + */ + +var ContextCommands = { + _picker: null, + + get clipboard() { + return Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper); + }, + + get docRef() { + return Browser.selectedBrowser.contentWindow.document; + }, + + /* + * Context menu handlers + */ + + copy: function cc_copy() { + let target = ContextMenuUI.popupState.target; + if (target.localName == "browser") { + if (ContextMenuUI.popupState.string != "undefined") { + this.clipboard.copyString(ContextMenuUI.popupState.string, + this.docRef); + } else { + let x = ContextMenuUI.popupState.x; + let y = ContextMenuUI.popupState.y; + let json = {x: x, y: y, command: "copy" }; + target.messageManager.sendAsyncMessage("Browser:ContextCommand", json); + } + } else { + target.editor.copy(); + } + + if (target) + target.focus(); + }, + + paste: function cc_paste() { + let target = ContextMenuUI.popupState.target; + if (target.localName == "browser") { + let x = ContextMenuUI.popupState.x; + let y = ContextMenuUI.popupState.y; + let json = {x: x, y: y, command: "paste" }; + target.messageManager.sendAsyncMessage("Browser:ContextCommand", json); + } else { + target.editor.paste(Ci.nsIClipboard.kGlobalClipboard); + target.focus(); + } + }, + + pasteAndGo: function cc_pasteAndGo() { + let target = ContextMenuUI.popupState.target; + target.editor.selectAll(); + target.editor.paste(Ci.nsIClipboard.kGlobalClipboard); + BrowserUI.goToURI(); + }, + + selectAll: function cc_selectAll() { + let target = ContextMenuUI.popupState.target; + if (target.localName == "browser") { + let x = ContextMenuUI.popupState.x; + let y = ContextMenuUI.popupState.y; + let json = {x: x, y: y, command: "select-all" }; + target.messageManager.sendAsyncMessage("Browser:ContextCommand", json); + } else { + target.editor.selectAll(); + target.focus(); + } + }, + + openInNewTab: function cc_openInNewTab() { + Browser.addTab(ContextMenuUI.popupState.linkURL, false, Browser.selectedTab); + ContextUI.peekTabs(); + }, + + saveToWinLibrary: function cc_saveToWinLibrary(aType) { + let popupState = ContextMenuUI.popupState; + let browser = popupState.target; + + // ContentAreaUtils internalSave relies on various desktop related prefs, + // values, and functionality. We want to be more direct by saving the + // image to the users Windows Library. If users want to specify the + // save location they can use the context menu option 'Save (type) To'. + + let dirSvc = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); + let saveLocationPath = dirSvc.get(aType, Components.interfaces.nsIFile); + + let fileInfo = new ContentAreaUtils.FileInfo(); + ContentAreaUtils.initFileInfo(fileInfo, popupState.mediaURL, + browser.documentURI.originCharset, + null, null, null); + let filename = + ContentAreaUtils.getNormalizedLeafName(fileInfo.fileName, fileInfo.fileExt); + saveLocationPath.append(filename); + + // Start the background save process + ContentAreaUtils.internalPersist({ + sourceURI : fileInfo.uri, + sourceDocument : null, + sourceReferrer : browser.documentURI, + targetContentType : popupState.contentType, + targetFile : saveLocationPath, + sourceCacheKey : null, + sourcePostData : null, + bypassCache : false, + initiatingWindow : this.docRef.defaultView + }); + }, + + saveVideo: function cc_saveVideo() { + this.saveToWinLibrary("Vids"); + }, + + saveVideoTo: function cc_saveVideoTo() { + this.saveFileAs(ContextMenuUI.popupState); + }, + + saveImage: function cc_saveImage() { + this.saveToWinLibrary("Pict"); + }, + + saveImageTo: function cc_saveImageTo() { + this.saveFileAs(ContextMenuUI.popupState); + }, + + copyLink: function cc_copyLink() { + this.clipboard.copyString(ContextMenuUI.popupState.linkURL, + this.docRef); + }, + + copyEmail: function cc_copyEmail() { + this.clipboard.copyString(ContextMenuUI.popupState.linkURL.substr(ContextMenuUI.popupState.linkURL.indexOf(':')+1), + this.docRef); + }, + + copyPhone: function cc_copyPhone() { + this.clipboard.copyString(ContextMenuUI.popupState.linkURL.substr(ContextMenuUI.popupState.linkURL.indexOf(':')+1), + this.docRef); + }, + + copyImageLocation: function cc_copyImageLocation() { + this.clipboard.copyString(ContextMenuUI.popupState.mediaURL, + this.docRef); + }, + + bookmarkLink: function cc_bookmarkLink() { + let state = ContextMenuUI.popupState; + let uri = Util.makeURI(state.linkURL); + let title = state.linkTitle || state.linkURL; + + try { + Bookmarks.addForURI(uri, title); + } catch (e) { + return; + } + + let message = Strings.browser.GetStringFromName("alertLinkBookmarked"); + let toaster = Cc["@mozilla.org/toaster-alerts-service;1"].getService(Ci.nsIAlertsService); + toaster.showAlertNotification(null, message, "", false, "", null); + }, + + sendCommand: function cc_playVideo(aCommand) { + let browser = ContextMenuUI.popupState.target; + browser.messageManager.sendAsyncMessage("Browser:ContextCommand", { command: aCommand }); + }, + + editBookmark: function cc_editBookmark() { + let target = ContextMenuUI.popupState.target; + target.startEditing(); + }, + + removeBookmark: function cc_removeBookmark() { + let target = ContextMenuUI.popupState.target; + target.remove(); + }, + + shortcutBookmark: function cc_shortcutBookmark() { + let target = ContextMenuUI.popupState.target; + Util.createShortcut(target.getAttribute("title"), target.getAttribute("uri"), target.getAttribute("src"), "bookmark"); + }, + + findInPage: function cc_findInPage() { + dump('ContextCommand: findInPage'); + FindHelperUI.show(); + }, + + viewOnDesktop: function cc_viewOnDesktop() { + dump('ContextCommand: viewOnDesktop'); + Appbar.onViewOnDesktop(); + }, + + /* + * Utilities + */ + + /* + * isAccessibleDirectory + * + * Test to see if the directory exists and is writable. + */ + isAccessibleDirectory: function isAccessibleDirectory(aDirectory) { + return aDirectory && aDirectory.exists() && aDirectory.isDirectory() && + aDirectory.isWritable(); + }, + + /* + * saveFileAs + * + * Browse for a location and then save a file to the local system using + * standard download prefs. + * + * @param aFileUriString path to file + */ + saveFileAs: function saveFileAs(aPopupState) { + let srcUri = null; + let mediaURL = aPopupState.mediaURL; + try { + srcUri = Util.makeURI(mediaURL, null, null); + } catch (ex) { + Util.dumpLn("could not parse:", mediaURL); + return; + } + + let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let windowTitle = Strings.browser.GetStringFromName("browserForSaveLocation"); + + picker.init(window, windowTitle, Ci.nsIFilePicker.modeSave); + + // prefered filename + let fileName = mediaURL.substring(mediaURL.lastIndexOf("/") + 1); + if (fileName.length) + picker.defaultString = fileName; + + // prefered file extension + let fileExtension = mediaURL.substring(mediaURL.lastIndexOf(".") + 1); + if (fileExtension.length) + picker.defaultExtension = fileExtension; + picker.appendFilters(Ci.nsIFilePicker.filterImages); + + // prefered save location + var dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); + picker.displayDirectory = dnldMgr.userDownloadsDirectory; + try { + let lastDir = Services.prefs.getComplexValue("browser.download.lastDir", Ci.nsILocalFile); + if (this.isAccessibleDirectory(lastDir)) + picker.displayDirectory = lastDir; + } + catch (e) { } + + this._picker = picker; + this._pickerUrl = mediaURL; + this._pickerContentDisp = aPopupState.contentDisposition; + this._contentType = aPopupState.contentType; + picker.open(ContextCommands); + }, + + /* + * Filepicker callback + */ + done: function done(aSuccess) { + let picker = this._picker; + this._picker = null; + + if (aSuccess == Ci.nsIFilePicker.returnCancel) + return; + + let file = picker.file; + if (file == null) + return; + + try { + if (file.exists()) + file.remove(false); + } catch (e) {} + + ContentAreaUtils.internalSave(this._pickerUrl, null, null, + this._pickerContentDisp, + this._contentType, false, "SaveImageTitle", + new AutoChosen(file, Util.makeURI(this._pickerUrl, null, null)), + getBrowser().documentURI, this.docRef, true, null); + + var newDir = file.parent.QueryInterface(Ci.nsILocalFile); + Services.prefs.setComplexValue("browser.download.lastDir", Ci.nsILocalFile, newDir); + }, + +}; + +function AutoChosen(aFileAutoChosen, aUriAutoChosen) { + this.file = aFileAutoChosen; + this.uri = aUriAutoChosen; +} + diff --git a/browser/metro/base/content/LoginManagerChild.js b/browser/metro/base/content/LoginManagerChild.js new file mode 100644 index 000000000000..0e601800f86b --- /dev/null +++ b/browser/metro/base/content/LoginManagerChild.js @@ -0,0 +1,818 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +var loginManager = { + + /* ---------- private members ---------- */ + + + get _formFillService() { + return this._formFillService = + Cc["@mozilla.org/satchel/form-fill-controller;1"]. + getService(Ci.nsIFormFillController); + }, + + _nsLoginInfo : null, // Constructor for nsILoginInfo implementation + _debug : false, // mirrors signon.debug + _remember : true, // mirrors signon.rememberSignons preference + + init : function () { + // Cache references to current |this| in utility objects + this._webProgressListener._domEventListener = this._domEventListener; + this._webProgressListener._pwmgr = this; + this._domEventListener._pwmgr = this; + this._observer._pwmgr = this; + + // Form submit observer checks forms for new logins and pw changes. + Services.obs.addObserver(this._observer, "earlyformsubmit", false); + + // Get constructor for nsILoginInfo + this._nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo); + + // Preferences. Add observer so we get notified of changes. + Services.prefs.addObserver("signon.", this._observer, false); + + // Get current preference values. + this._debug = Services.prefs.getBoolPref("signon.debug"); + this._remember = Services.prefs.getBoolPref("signon.rememberSignons"); + + // WebProgressListener for getting notification of new doc loads. + var progress = Cc["@mozilla.org/docloaderservice;1"]. + getService(Ci.nsIWebProgress); + progress.addProgressListener(this._webProgressListener, + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); + }, + + + /* + * log + * + * Internal function for logging debug messages to the Error Console window + */ + log : function (message) { + if (!this._debug) + return; + dump("PasswordUtils: " + message + "\n"); + Services.console.logStringMessage("PasswordUtils: " + message); + }, + + + /* fillForm + * + * Fill the form with login information if we can find it. + */ + fillForm: function (form) { + this._fillForm(form, true, true, false, null); + }, + + + /* + * _fillform + * + * Fill the form with login information if we can find it. + * autofillForm denotes if we should fill the form in automatically + * ignoreAutocomplete denotes if we should ignore autocomplete=off attributes + * foundLogins is an array of nsILoginInfo + */ + _fillForm : function (form, autofillForm, ignoreAutocomplete, + clobberPassword, foundLogins) { + // Heuristically determine what the user/pass fields are + // We do this before checking to see if logins are stored, + // so that the user isn't prompted for a master password + // without need. + var [usernameField, passwordField, ignored] = + this._getFormFields(form, false); + + // Need a valid password field to do anything. + if (passwordField == null) + return false; + + // If the fields are disabled or read-only, there's nothing to do. + if (passwordField.disabled || passwordField.readOnly || + usernameField && (usernameField.disabled || + usernameField.readOnly)) { + this.log("not filling form, login fields disabled"); + return false; + } + + // Discard logins which have username/password values that don't + // fit into the fields (as specified by the maxlength attribute). + // The user couldn't enter these values anyway, and it helps + // with sites that have an extra PIN to be entered (bug 391514) + var maxUsernameLen = Number.MAX_VALUE; + var maxPasswordLen = Number.MAX_VALUE; + + // If attribute wasn't set, default is -1. + if (usernameField && usernameField.maxLength >= 0) + maxUsernameLen = usernameField.maxLength; + if (passwordField.maxLength >= 0) + maxPasswordLen = passwordField.maxLength; + + var logins = foundLogins.filter(function (l) { + var fit = (l.username.length <= maxUsernameLen && + l.password.length <= maxPasswordLen); + if (!fit) + this.log("Ignored " + l.username + " login: won't fit"); + + return fit; + }, this); + + + // Nothing to do if we have no matching logins available. + if (logins.length == 0) + return false; + + + // The reason we didn't end up filling the form, if any. We include + // this in the formInfo object we send with the passwordmgr-found-logins + // notification. See the _notifyFoundLogins docs for possible values. + var didntFillReason = null; + + // Attach autocomplete stuff to the username field, if we have + // one. This is normally used to select from multiple accounts, + // but even with one account we should refill if the user edits. + if (usernameField) + this._attachToInput(usernameField); + + // Don't clobber an existing password. + if (passwordField.value && !clobberPassword) { + didntFillReason = "existingPassword"; + this._notifyFoundLogins(didntFillReason, usernameField, + passwordField, foundLogins, null); + return false; + } + + // If the form has an autocomplete=off attribute in play, don't + // fill in the login automatically. We check this after attaching + // the autocomplete stuff to the username field, so the user can + // still manually select a login to be filled in. + var isFormDisabled = false; + if (!ignoreAutocomplete && + (this._isAutocompleteDisabled(form) || + this._isAutocompleteDisabled(usernameField) || + this._isAutocompleteDisabled(passwordField))) { + + isFormDisabled = true; + this.log("form not filled, has autocomplete=off"); + } + + // Variable such that we reduce code duplication and can be sure we + // should be firing notifications if and only if we can fill the form. + var selectedLogin = null; + + if (usernameField && usernameField.value) { + // If username was specified in the form, only fill in the + // password if we find a matching login. + var username = usernameField.value.toLowerCase(); + + let matchingLogins = logins.filter(function(l) + l.username.toLowerCase() == username); + if (matchingLogins.length) { + selectedLogin = matchingLogins[0]; + } else { + didntFillReason = "existingUsername"; + this.log("Password not filled. None of the stored " + + "logins match the username already present."); + } + } else if (logins.length == 1) { + selectedLogin = logins[0]; + } else { + // We have multiple logins. Handle a special case here, for sites + // which have a normal user+pass login *and* a password-only login + // (eg, a PIN). Prefer the login that matches the type of the form + // (user+pass or pass-only) when there's exactly one that matches. + let matchingLogins; + if (usernameField) + matchingLogins = logins.filter(function(l) l.username); + else + matchingLogins = logins.filter(function(l) !l.username); + if (matchingLogins.length == 1) { + selectedLogin = matchingLogins[0]; + } else { + didntFillReason = "multipleLogins"; + this.log("Multiple logins for form, so not filling any."); + } + } + + var didFillForm = false; + if (selectedLogin && autofillForm && !isFormDisabled) { + // Fill the form + if (usernameField) + usernameField.value = selectedLogin.username; + passwordField.value = selectedLogin.password; + didFillForm = true; + } else if (selectedLogin && !autofillForm) { + // For when autofillForm is false, but we still have the information + // to fill a form, we notify observers. + didntFillReason = "noAutofillForms"; + Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); + this.log("autofillForms=false but form can be filled; notified observers"); + } else if (selectedLogin && isFormDisabled) { + // For when autocomplete is off, but we still have the information + // to fill a form, we notify observers. + didntFillReason = "autocompleteOff"; + Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); + this.log("autocomplete=off but form can be filled; notified observers"); + } + + this._notifyFoundLogins(didntFillReason, usernameField, passwordField, + foundLogins, selectedLogin); + + return didFillForm; + }, + + + /* + * _getPasswordOrigin + * + * Get the parts of the URL we want for identification. + */ + _getPasswordOrigin : function (uriString, allowJS) { + var realm = ""; + try { + var uri = Services.io.newURI(uriString, null, null); + + if (allowJS && uri.scheme == "javascript") + return "javascript:" + + realm = uri.scheme + "://" + uri.host; + + // If the URI explicitly specified a port, only include it when + // it's not the default. (We never want "http://foo.com:80") + var port = uri.port; + if (port != -1) { + var handler = Services.io.getProtocolHandler(uri.scheme); + if (port != handler.defaultPort) + realm += ":" + port; + } + + } catch (e) { + // bug 159484 - disallow url types that don't support a hostPort. + // (although we handle "javascript:..." as a special case above.) + this.log("Couldn't parse origin for " + uriString); + realm = null; + } + + return realm; + }, + + + _getActionOrigin : function (form) { + var uriString = form.action; + + // A blank or mission action submits to where it came from. + if (uriString == "") + uriString = form.baseURI; // ala bug 297761 + + return this._getPasswordOrigin(uriString, true); + }, + + + /* + * _isAutoCompleteDisabled + * + * Returns true if the page requests autocomplete be disabled for the + * specified form input. + */ + _isAutocompleteDisabled : function (element) { + if (element && element.hasAttribute("autocomplete") && + element.getAttribute("autocomplete").toLowerCase() == "off") + return true; + + return false; + }, + + + /* + * _getFormFields + * + * Returns the username and password fields found in the form. + * Can handle complex forms by trying to figure out what the + * relevant fields are. + * + * Returns: [usernameField, newPasswordField, oldPasswordField] + * + * usernameField may be null. + * newPasswordField will always be non-null. + * oldPasswordField may be null. If null, newPasswordField is just + * "theLoginField". If not null, the form is apparently a + * change-password field, with oldPasswordField containing the password + * that is being changed. + */ + _getFormFields : function (form, isSubmission) { + var usernameField = null; + + // Locate the password field(s) in the form. Up to 3 supported. + // If there's no password field, there's nothing for us to do. + var pwFields = this._getPasswordFields(form, isSubmission); + if (!pwFields) + return [null, null, null]; + + + // Locate the username field in the form by searching backwards + // from the first passwordfield, assume the first text field is the + // username. We might not find a username field if the user is + // already logged in to the site. + for (var i = pwFields[0].index - 1; i >= 0; i--) { + var element = form.elements[i]; + var fieldType = (element.hasAttribute("type") ? + element.getAttribute("type").toLowerCase() : + element.type); + if (fieldType == "text" || + fieldType == "email" || + fieldType == "url" || + fieldType == "tel" || + fieldType == "number") { + usernameField = element; + break; + } + } + + if (!usernameField) + this.log("(form -- no username field found)"); + + + // If we're not submitting a form (it's a page load), there are no + // password field values for us to use for identifying fields. So, + // just assume the first password field is the one to be filled in. + if (!isSubmission || pwFields.length == 1) + return [usernameField, pwFields[0].element, null]; + + + // Try to figure out WTF is in the form based on the password values. + var oldPasswordField, newPasswordField; + var pw1 = pwFields[0].element.value; + var pw2 = pwFields[1].element.value; + var pw3 = (pwFields[2] ? pwFields[2].element.value : null); + + if (pwFields.length == 3) { + // Look for two identical passwords, that's the new password + + if (pw1 == pw2 && pw2 == pw3) { + // All 3 passwords the same? Weird! Treat as if 1 pw field. + newPasswordField = pwFields[0].element; + oldPasswordField = null; + } else if (pw1 == pw2) { + newPasswordField = pwFields[0].element; + oldPasswordField = pwFields[2].element; + } else if (pw2 == pw3) { + oldPasswordField = pwFields[0].element; + newPasswordField = pwFields[2].element; + } else if (pw1 == pw3) { + // A bit odd, but could make sense with the right page layout. + newPasswordField = pwFields[0].element; + oldPasswordField = pwFields[1].element; + } else { + // We can't tell which of the 3 passwords should be saved. + this.log("(form ignored -- all 3 pw fields differ)"); + return [null, null, null]; + } + } else { // pwFields.length == 2 + if (pw1 == pw2) { + // Treat as if 1 pw field + newPasswordField = pwFields[0].element; + oldPasswordField = null; + } else { + // Just assume that the 2nd password is the new password + oldPasswordField = pwFields[0].element; + newPasswordField = pwFields[1].element; + } + } + + return [usernameField, newPasswordField, oldPasswordField]; + }, + + + /* ---------- Private methods ---------- */ + + + /* + * _getPasswordFields + * + * Returns an array of password field elements for the specified form. + * If no pw fields are found, or if more than 3 are found, then null + * is returned. + * + * skipEmptyFields can be set to ignore password fields with no value. + */ + _getPasswordFields : function (form, skipEmptyFields) { + // Locate the password fields in the form. + var pwFields = []; + for (var i = 0; i < form.elements.length; i++) { + var element = form.elements[i]; + if (!(element instanceof Ci.nsIDOMHTMLInputElement) || + element.type != "password") + continue; + + if (skipEmptyFields && !element.value) + continue; + + pwFields[pwFields.length] = { + index : i, + element : element + }; + } + + // If too few or too many fields, bail out. + if (pwFields.length == 0) { + this.log("(form ignored -- no password fields.)"); + return null; + } else if (pwFields.length > 3) { + this.log("(form ignored -- too many password fields. [got " + + pwFields.length + "])"); + return null; + } + + return pwFields; + }, + + + /** + * Notify observers about an attempt to fill a form that resulted in some + * saved logins being found for the form. + * + * This does not get called if the login manager attempts to fill a form + * but does not find any saved logins. It does, however, get called when + * the login manager does find saved logins whether or not it actually + * fills the form with one of them. + * + * @param didntFillReason {String} + * the reason the login manager didn't fill the form, if any; + * if the value of this parameter is null, then the form was filled; + * otherwise, this parameter will be one of these values: + * existingUsername: the username field already contains a username + * that doesn't match any stored usernames + * existingPassword: the password field already contains a password + * autocompleteOff: autocomplete has been disabled for the form + * or its username or password fields + * multipleLogins: we have multiple logins for the form + * noAutofillForms: the autofillForms pref is set to false + * + * @param usernameField {HTMLInputElement} + * the username field detected by the login manager, if any; + * otherwise null + * + * @param passwordField {HTMLInputElement} + * the password field detected by the login manager + * + * @param foundLogins {Array} + * an array of nsILoginInfos that can be used to fill the form + * + * @param selectedLogin {nsILoginInfo} + * the nsILoginInfo that was/would be used to fill the form, if any; + * otherwise null; whether or not it was actually used depends on + * the value of the didntFillReason parameter + */ + _notifyFoundLogins : function (didntFillReason, usernameField, + passwordField, foundLogins, selectedLogin) { + // We need .setProperty(), which is a method on the original + // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2 + // doesn't inherit from that, so the additional QI is needed. + let formInfo = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag2). + QueryInterface(Ci.nsIWritablePropertyBag); + + formInfo.setPropertyAsACString("didntFillReason", didntFillReason); + formInfo.setPropertyAsInterface("usernameField", usernameField); + formInfo.setPropertyAsInterface("passwordField", passwordField); + formInfo.setProperty("foundLogins", foundLogins.concat()); + formInfo.setPropertyAsInterface("selectedLogin", selectedLogin); + + Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null); + }, + + /* + * _attachToInput + * + * Hooks up autocomplete support to a username field, to allow + * a user editing the field to select an existing login and have + * the password field filled in. + */ + _attachToInput : function (element) { + this.log("attaching autocomplete stuff"); + element.addEventListener("blur", + this._domEventListener, false); + element.addEventListener("DOMAutoComplete", + this._domEventListener, false); + this._formFillService.markAsLoginManagerField(element); + }, + + /* + * _fillDocument + * + * Called when a page has loaded. For each form in the document, + * we ask the parent process to see if it can be filled with a stored login + * and fill them in with the results + */ + _fillDocument : function (doc) { + var forms = doc.forms; + if (!forms || forms.length == 0) + return; + + this.log("_fillDocument processing " + forms.length + + " forms on " + doc.documentURI); + + var autofillForm = !PrivateBrowsingUtils.isWindowPrivate(doc.defaultView) && + Services.prefs.getBoolPref("signon.autofillForms"); + + // actionOrigins is a list of each form's action origins for this + // document. The parent process needs this to find the passwords + // for each action origin. + var actionOrigins = []; + + for (var i = 0; i < forms.length; i++) { + var form = forms[i]; + let [, passwordField, ] = this._getFormFields(form, false); + if (passwordField) { + var actionOrigin = this._getActionOrigin(form); + actionOrigins.push(actionOrigin); + } + } // foreach form + + if (!actionOrigins.length) + return; + + var formOrigin = this._getPasswordOrigin(doc.documentURI); + var foundLogins = this._getPasswords(actionOrigins, formOrigin); + + for (var i = 0; i < forms.length; i++) { + var form = forms[i]; + var actionOrigin = this._getActionOrigin(form); + if (foundLogins[actionOrigin]) { + this.log("_fillDocument processing form[" + i + "]"); + this._fillForm(form, autofillForm, false, false, + foundLogins[actionOrigin]); + } + } // foreach form + }, + + /* + * _getPasswords + * + * Retrieve passwords from parent process and prepare logins to be passed to + * _fillForm. Returns map from action origins to passwords. + */ + _getPasswords: function(actionOrigins, formOrigin) { + // foundLogins will be a map from action origins to passwords. + var message = sendSyncMessage("PasswordMgr:GetPasswords", { + actionOrigins: actionOrigins, + formOrigin: formOrigin + })[0]; + + // XXX need to somehow respond to the UI being busy + // not needed for Fennec yet + + var foundLogins = message.foundLogins; + + // Each password will be a JSON-unserialized object, but they need to be + // nsILoginInfo's. + for (var key in foundLogins) { + var logins = foundLogins[key]; + for (var i = 0; i < logins.length; i++) { + var obj = logins[i]; + logins[i] = new this._nsLoginInfo(); + logins[i].init(obj.hostname, obj.formSubmitURL, obj.httpRealm, + obj.username, obj.password, + obj.usernameField, obj.passwordField); + } + } + + return foundLogins; + }, + + /* + * _onFormSubmit + * + * Called by the our observer when notified of a form submission. + * [Note that this happens before any DOM onsubmit handlers are invoked.] + * Looks for a password change in the submitted form, so we can update + * our stored password. + */ + _onFormSubmit : function (form) { + var doc = form.ownerDocument; + var win = doc.defaultView; + + if (PrivateBrowsingUtils.isWindowPrivate(win)) { + // We won't do anything in private browsing mode anyway, + // so there's no need to perform further checks. + this.log("(form submission ignored in private browsing mode)"); + return; + } + + // If password saving is disabled (globally or for host), bail out now. + if (!this._remember) + return; + + var hostname = this._getPasswordOrigin(doc.documentURI); + var formSubmitURL = this._getActionOrigin(form); + + + // Get the appropriate fields from the form. + var [usernameField, newPasswordField, oldPasswordField] = + this._getFormFields(form, true); + + // Need at least 1 valid password field to do anything. + if (newPasswordField == null) + return; + + // Check for autocomplete=off attribute. We don't use it to prevent + // autofilling (for existing logins), but won't save logins when it's + // present. + // XXX spin out a bug that we don't update timeLastUsed in this case? + if (this._isAutocompleteDisabled(form) || + this._isAutocompleteDisabled(usernameField) || + this._isAutocompleteDisabled(newPasswordField) || + this._isAutocompleteDisabled(oldPasswordField)) { + this.log("(form submission ignored -- autocomplete=off found)"); + return; + } + + sendSyncMessage("PasswordMgr:FormSubmitted", { + hostname: hostname, + formSubmitURL: formSubmitURL, + usernameField: usernameField ? usernameField.name : "", + usernameValue: usernameField ? usernameField.value : "", + passwordField: newPasswordField.name, + passwordValue: newPasswordField.value, + hasOldPasswordField: !!oldPasswordField + }); + }, + + /* ---------- Utility objects ---------- */ + + /* + * _observer object + * + * Internal utility object, implements the nsIObserver interface. + * Used to receive notification for: form submission, preference changes. + */ + _observer : { + _pwmgr : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsIFormSubmitObserver, + Ci.nsISupportsWeakReference]), + + + // nsFormSubmitObserver + notify : function (formElement, aWindow, actionURI) { + // Counterintuitively, form submit observers fire for content that + // may not be the content in this context. + if (aWindow.top != content) + return true; + + this._pwmgr.log("observer notified for form submission."); + + // We're invoked before the content's |onsubmit| handlers, so we + // can grab form data before it might be modified (see bug 257781). + + try { + this._pwmgr._onFormSubmit(formElement); + } catch (e) { + this._pwmgr.log("Caught error in onFormSubmit: " + e); + } + + return true; // Always return true, or form submit will be canceled. + }, + + observe : function (aSubject, aTopic, aData) { + this._pwmgr._debug = Services.prefs.getBoolPref("signon.debug"); + this._pwmgr._remember = Services.prefs.getBoolPref("signon.rememberSignons"); + } + }, + + + /* + * _webProgressListener object + * + * Internal utility object, implements nsIWebProgressListener interface. + * This is attached to the document loader service, so we get + * notifications about all page loads. + */ + _webProgressListener : { + _pwmgr : null, + _domEventListener : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + + onStateChange : function (aWebProgress, aRequest, + aStateFlags, aStatus) { + + // STATE_START is too early, doc is still the old page. + if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING)) + return; + + if (!this._pwmgr._remember) + return; + + var domWin = aWebProgress.DOMWindow; + var domDoc = domWin.document; + + // Only process things which might have HTML forms. + if (!(domDoc instanceof Ci.nsIDOMHTMLDocument)) + return; + if (this._pwmgr._debug) { + let requestName = "(null)"; + if (aRequest) { + try { + requestName = aRequest.name; + } catch (ex if ex.result == Components.results.NS_ERROR_NOT_IMPLEMENTED) { + // do nothing - leave requestName = "(null)" + } + } + this._pwmgr.log("onStateChange accepted: req = " + requestName + + ", flags = 0x" + aStateFlags.toString(16)); + } + + // Fastback doesn't fire DOMContentLoaded, so process forms now. + if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) { + this._pwmgr.log("onStateChange: restoring document"); + return this._pwmgr._fillDocument(domDoc); + } + + // Add event listener to process page when DOM is complete. + domDoc.addEventListener("DOMContentLoaded", + this._domEventListener, false); + return; + }, + + // stubs for the nsIWebProgressListener interfaces which we don't use. + onProgressChange : function() { throw "Unexpected onProgressChange"; }, + onLocationChange : function() { throw "Unexpected onLocationChange"; }, + onStatusChange : function() { throw "Unexpected onStatusChange"; }, + onSecurityChange : function() { throw "Unexpected onSecurityChange"; } + }, + + /* + * _domEventListener object + * + * Internal utility object, implements nsIDOMEventListener + * Used to catch certain DOM events needed to properly implement form fill. + */ + _domEventListener : { + _pwmgr : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener, + Ci.nsISupportsWeakReference]), + + + handleEvent : function (event) { + if (!event.isTrusted) + return; + + this._pwmgr.log("domEventListener: got event " + event.type); + + switch (event.type) { + case "DOMAutoComplete": + case "blur": + var acInputField = event.target; + var acForm = acInputField.form; + + // If the username is blank, bail out now -- we don't want + // _fillForm() to try filling in a login without a username + // to filter on (bug 471906). + if (!acInputField.value) + return; + + // Make sure the username field _fillForm will use is the + // same field as the autocomplete was activated on. If + // not, the DOM has been altered and we'll just give up. + var [usernameField, passwordField, ignored] = + this._pwmgr._getFormFields(acForm, false); + if (usernameField == acInputField && passwordField) { + var actionOrigin = this._pwmgr._getActionOrigin(acForm); + var formOrigin = this._pwmgr._getPasswordOrigin(acForm.ownerDocument.documentURI); + var foundLogins = this._pwmgr._getPasswords([actionOrigin], formOrigin); + this._pwmgr._fillForm(acForm, true, true, true, foundLogins[actionOrigin]); + } else { + this._pwmgr.log("Oops, form changed before AC invoked"); + } + return; + + case "DOMContentLoaded": + // Only process when we need to + event.currentTarget.removeEventListener(event.type, this); + if (this._pwmgr._remember && event.target instanceof Ci.nsIDOMHTMLDocument) + this._pwmgr._fillDocument(event.target); + break; + + default: + this._pwmgr.log("Oops! This event unexpected."); + return; + } + } + } +}; + +loginManager.init(); diff --git a/browser/metro/base/content/PageActions.js b/browser/metro/base/content/PageActions.js new file mode 100644 index 000000000000..7a14df29112e --- /dev/null +++ b/browser/metro/base/content/PageActions.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/. */ + +var PageActions = { + _handlers: null, + + init: function init() { + if (this._handlers) + return; + + this._handlers = []; + document.getElementById("pageactions-container").addEventListener("click", this, true); + + this.register("pageaction-reset", this.updatePagePermissions, this); + this.register("pageaction-password", this.updateForgetPassword, this); +#ifdef NS_PRINTING + this.register("pageaction-saveas", this.updatePageSaveAs, this); +#endif + this.register("pageaction-share", this.updateShare, this); + + CharsetMenu.init(); + }, + + handleEvent: function handleEvent(aEvent) { + switch (aEvent.type) { + case "click": + IdentityUI.hide(); + break; + } + }, + + /** + * @param aId id of a pageaction element + * @param aCallback function that takes an element and returns true if it should be visible + * @param aThisObj (optional) scope object for aCallback + */ + register: function register(aId, aCallback, aThisObj) { + this.init(); + this._handlers.push({id: aId, callback: aCallback, obj: aThisObj}); + }, + + updateSiteMenu: function updateSiteMenu() { + this.init(); + this._handlers.forEach(function(action) { + let node = document.getElementById(action.id); + if (node) + node.hidden = !action.callback.call(action.obj, node); + }); + this._updateAttributes(); + }, + + get _loginManager() { + delete this._loginManager; + return this._loginManager = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + }, + + // Permissions we track in Page Actions + _permissions: ["popup", "offline-app", "geolocation", "desktop-notification", "openWebappsManage"], + + _forEachPermissions: function _forEachPermissions(aHost, aCallback) { + let pm = Services.perms; + for (let i = 0; i < this._permissions.length; i++) { + let type = this._permissions[i]; + if (!pm.testPermission(aHost, type)) + continue; + + let perms = pm.enumerator; + while (perms.hasMoreElements()) { + let permission = perms.getNext().QueryInterface(Ci.nsIPermission); + if (permission.host == aHost.asciiHost && permission.type == type) + aCallback(type); + } + } + }, + + updatePagePermissions: function updatePagePermissions(aNode) { + let host = Browser.selectedBrowser.currentURI; + let permissions = []; + + this._forEachPermissions(host, function(aType) { + permissions.push("pageactions." + aType); + }); + + if (!this._loginManager.getLoginSavingEnabled(host.prePath)) { + // If rememberSignons is false, then getLoginSavingEnabled returns false + // for all pages, so we should just ignore it (Bug 601163). + if (Services.prefs.getBoolPref("signon.rememberSignons")) + permissions.push("pageactions.password"); + } + + let descriptions = permissions.map(function(s) Strings.browser.GetStringFromName(s)); + aNode.setAttribute("description", descriptions.join(", ")); + + return (permissions.length > 0); + }, + + updateForgetPassword: function updateForgetPassword(aNode) { + let host = Browser.selectedBrowser.currentURI; + let logins = this._loginManager.findLogins({}, host.prePath, "", ""); + + return logins.some(function(login) login.hostname == host.prePath); + }, + + forgetPassword: function forgetPassword(aEvent) { + let host = Browser.selectedBrowser.currentURI; + let lm = this._loginManager; + + lm.findLogins({}, host.prePath, "", "").forEach(function(login) { + if (login.hostname == host.prePath) + lm.removeLogin(login); + }); + + this.hideItem(aEvent.target); + aEvent.stopPropagation(); // Don't hide the site menu. + }, + + clearPagePermissions: function clearPagePermissions(aEvent) { + let pm = Services.perms; + let host = Browser.selectedBrowser.currentURI; + this._forEachPermissions(host, function(aType) { + pm.remove(host.asciiHost, aType); + + // reset the 'remember' counter for permissions that support it + if (["geolocation", "desktop-notification"].indexOf(aType) != -1) + Services.contentPrefs.setPref(host.asciiHost, aType + ".request.remember", 0); + }); + + let lm = this._loginManager; + if (!lm.getLoginSavingEnabled(host.prePath)) + lm.setLoginSavingEnabled(host.prePath, true); + + this.hideItem(aEvent.target); + aEvent.stopPropagation(); // Don't hide the site menu. + }, + + pinSite : function pinCurrentSite() { + if (Browser.selectedBrowser.currentURI.spec.length == 0) { + return; + } + Browser.pinSite(); + }, + + unpinSite : function unpinCurrentSite() { + if (Browser.selectedBrowser.currentURI.spec.length == 0) { + return; + } + Browser.unpinSite(); + }, + + updatePageSaveAs: function updatePageSaveAs(aNode) { + // Check for local XUL content + let contentWindow = Browser.selectedBrowser.contentWindow; + return !(contentWindow && contentWindow.document instanceof XULDocument); + }, + + updateShare: function updateShare(aNode) { + return Util.isShareableScheme(Browser.selectedBrowser.currentURI.scheme); + }, + + hideItem: function hideItem(aNode) { + aNode.hidden = true; + this._updateAttributes(); + }, + + _updateAttributes: function _updateAttributes() { + let container = document.getElementById("pageactions-container"); + let visibleNodes = container.querySelectorAll("pageaction:not([hidden=true])"); + let visibleCount = visibleNodes.length; + if (visibleCount == 0) + return; + + for (let i = 0; i < visibleCount; i++) + visibleNodes[i].classList.remove("odd-last-child"); + + visibleNodes[visibleCount - 1].classList.add("last-child"); + if (visibleCount % 2) + visibleNodes[visibleCount - 1].classList.add("odd"); + } +}; diff --git a/browser/metro/base/content/RemoteTabs.js b/browser/metro/base/content/RemoteTabs.js new file mode 100644 index 000000000000..45935c633333 --- /dev/null +++ b/browser/metro/base/content/RemoteTabs.js @@ -0,0 +1,144 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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://services-sync/main.js"); + +/** + * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and + * fills it with the user's synced tabs. + * + * @param aSet Control implementing nsIDOMXULSelectControlElement. + * @param aSetUIAccess The UI element that should be hidden when Sync is + * disabled. Must sanely support 'hidden' attribute. + * You may only have one UI access point at this time. + */ +function RemoteTabsView(aSet, aSetUIAccess) { + this._set = aSet; + this._set.controller = this; + this._uiAccessElement = aSetUIAccess; + + // Sync uses special voodoo observers. + // If you want to change this code, talk to the fx-si team + Weave.Svc.Obs.add("weave:service:setup-complete", this); + Weave.Svc.Obs.add("weave:service:sync:finish", this); + Weave.Svc.Obs.add("weave:service:start-over", this); + if (this.isSyncEnabled() ) { + this.populateTabs(); + this.populateGrid(); + this.setUIAccessVisible(true); + } + else { + this.setUIAccessVisible(false); + } +} + +RemoteTabsView.prototype = { + _set: null, + _uiAccessElement: null, + + handleItemClick: function tabview_handleItemClick(aItem) { + let url = aItem.getAttribute("value"); + BrowserUI.goToURI(url); + }, + + observe: function(subject, topic, data) { + switch (topic) { + case "weave:service:setup-complete": + this.populateTabs(); + this.setUIAccessVisible(true); + break; + case "weave:service:sync:finish": + this.populateGrid(); + break; + case "weave:service:start-over": + this.setUIAccessVisible(false); + break; + } + }, + + setUIAccessVisible: function setUIAccessVisible(aVisible) { + this._uiAccessElement.hidden = !aVisible; + }, + + populateGrid: function populateGrid() { + + let tabsEngine = Weave.Service.engineManager.get("tabs"); + let list = this._set; + let seenURLs = new Set(); + + for (let [guid, client] in Iterator(tabsEngine.getAllClients())) { + client.tabs.forEach(function({title, urlHistory, icon}) { + let url = urlHistory[0]; + if (tabsEngine.locallyOpenTabMatchesURL(url) || seenURLs.has(url)) { + return; + } + seenURLs.add(url); + + // If we wish to group tabs by client, we should be looking for records + // of {type:client, clientName, class:{mobile, desktop}} and will + // need to readd logic to reset seenURLs for each client. + + let item = this._set.appendItem((title || url), url); + item.setAttribute("iconURI", Weave.Utils.getIcon(icon)); + + }, this); + } + }, + + populateTabs: function populateTabs() { + Weave.Service.scheduler.scheduleNextSync(0); + }, + + destruct: function destruct() { + Weave.Svc.Obs.remove("weave:service:setup-complete", this); + Weave.Svc.Obs.remove("weave:engine:sync:finish", this); + Weave.Svc.Obs.remove("weave:service:logout:start-over", this); + }, + + isSyncEnabled: function isSyncEnabled() { + return (Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED); + } + +}; + +let RemoteTabsStartView = { + _view: null, + get _grid() { return document.getElementById("start-remotetabs-grid"); }, + + init: function init() { + let vbox = document.getElementById("start-remotetabs"); + this._view = new RemoteTabsView(this._grid, vbox); + }, + + uninit: function uninit() { + this._view.destruct(); + }, + + show: function show() { + this._grid.arrangeItems(); + } +}; + +let RemoteTabsPanelView = { + _view: null, + + get _grid() { return document.getElementById("remotetabs-list"); }, + get visible() { return PanelUI.isPaneVisible("remotetabs-container"); }, + + init: function init() { + //decks are fragile, don't hide the tab panel(bad things happen), hide link. + let menuEntry = document.getElementById("menuitem-remotetabs"); + this._view = new RemoteTabsView(this._grid, menuEntry); + }, + + show: function show() { + this._grid.arrangeItems(); + }, + + uninit: function uninit() { + this._view.destruct(); + } +}; diff --git a/browser/metro/base/content/TopSites.js b/browser/metro/base/content/TopSites.js new file mode 100644 index 000000000000..0de08d98f54d --- /dev/null +++ b/browser/metro/base/content/TopSites.js @@ -0,0 +1,201 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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'; + let prefs = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch); + +// singleton to provide data-level functionality to the views +let TopSites = { + pinSite: function(aId, aSlotIndex) { + Util.dumpLn("TopSites.pinSite: " + aId + ", (TODO)"); + // FIXME: implementation needed + return true; // operation was successful + }, + unpinSite: function(aId) { + Util.dumpLn("TopSites.unpinSite: " + aId + ", (TODO)"); + // FIXME: implementation needed + return true; // operation was successful + }, + hideSite: function(aId) { + Util.dumpLn("TopSites.hideSite: " + aId + ", (TODO)"); + // FIXME: implementation needed + return true; // operation was successful + }, + restoreSite: function(aId) { + Util.dumpLn("TopSites.restoreSite: " + aId + ", (TODO)"); + // FIXME: implementation needed + return true; // operation was successful + } +}; + +function TopSitesView(aGrid, maxSites) { + this._set = aGrid; + this._set.controller = this; + this._topSitesMax = maxSites; + + // handle selectionchange DOM events from the grid/tile group + this._set.addEventListener("context-action", this, false); +} + +TopSitesView.prototype = { + _set:null, + _topSitesMax: null, + + handleItemClick: function tabview_handleItemClick(aItem) { + let url = aItem.getAttribute("value"); + BrowserUI.goToURI(url); + }, + + doActionOnSelectedTiles: function(aActionName) { + let tileGroup = this._set; + let selectedTiles = tileGroup.selectedItems; + + switch (aActionName){ + case "delete": + Array.forEach(selectedTiles, function(aNode) { + let id = aNode.getAttribute("data-itemid"); + // add some class to transition element before deletion? + if (TopSites.hideSite(id)) { + // success + aNode.contextActions.delete('delete'); + aNode.contextActions.add('restore'); + } + // TODO: we'll use some callback/event to remove the item or re-draw the grid + }); + break; + case "pin": + Array.forEach(selectedTiles, function(aNode) { + let id = aNode.getAttribute("data-itemid"); + if (TopSites.pinSite(id)) { + // success + aNode.contextActions.delete('pin'); + aNode.contextActions.add('unpin'); + } + // TODO: we'll use some callback/event to add some class to + // indicate element is pinned? + }); + break; + case "unpin": + Array.forEach(selectedTiles, function(aNode) { + let id = aNode.getAttribute("data-itemid"); + if (TopSites.unpinSite(id)) { + // success + aNode.contextActions.delete('unpin'); + aNode.contextActions.add('pin'); + } + // TODO: we'll use some callback/event to add some class to + // indicate element is pinned (or just redraw grid) + }); + break; + // default: no action + } + }, + + handleEvent: function(aEvent) { + switch (aEvent.type){ + case "context-action": + this.doActionOnSelectedTiles(aEvent.action); + break; + } + }, + + populateGrid: function populateGrid() { + let query = gHistSvc.getNewQuery(); + let options = gHistSvc.getNewQueryOptions(); + options.excludeQueries = true; + options.queryType = options.QUERY_TYPE_HISTORY; + options.maxResults = this._topSitesMax; + options.resultType = options.RESULTS_AS_URI; + options.sortingMode = options.SORT_BY_FRECENCY_DESCENDING; + + let result = gHistSvc.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let childCount = rootNode.childCount; + + // use this property as the data-itemid attribute on the tiles + // which identifies the site + let identifier = 'uri'; + + function isPinned(aNode) { + // placeholder condition, + // FIXME: do the actual lookup/check + return (aNode.uri.indexOf('google') > -1); + } + + for (let i = 0; i < childCount; i++) { + let node = rootNode.getChild(i); + let uri = node.uri; + let title = node.title || uri; + + let supportedActions = ['delete']; + // placeholder condition - check field/property for this site + if (isPinned(node)) { + supportedActions.push('unpin'); + } else { + supportedActions.push('pin'); + } + let item = this._set.appendItem(title, uri); + item.setAttribute("iconURI", node.icon); + item.setAttribute("data-itemid", node[identifier]); + // here is where we could add verbs based on pinned etc. state + item.setAttribute("data-contextactions", supportedActions.join(',')); + } + rootNode.containerOpen = false; + }, + + isFirstRun: function isFirstRun() { + return prefs.getBoolPref("browser.firstrun.show.localepicker"); + }, + + destruct: function destruct() { + // remove the observers here + } + +}; + +let TopSitesStartView = { + _view: null, + get _grid() { return document.getElementById("start-topsites-grid"); }, + + init: function init() { + this._view = new TopSitesView(this._grid, 9); + if (this._view.isFirstRun()) { + let topsitesVbox = document.getElementById("start-topsites"); + topsitesVbox.setAttribute("hidden", "true"); + } + this._view.populateGrid(); + }, + + uninit: function uninit() { + this._view.destruct(); + }, + + show: function show() { + this._grid.arrangeItems(3, 3); + }, +}; + +let TopSitesSnappedView = { + get _grid() { return document.getElementById("snapped-topsite-grid"); }, + + show: function show() { + this._grid.arrangeItems(1, 9); + }, + + init: function() { + this._view = new TopSitesView(this._grid, 9); + if (this._view.isFirstRun()) { + let topsitesVbox = document.getElementById("snapped-topsites"); + topsitesVbox.setAttribute("hidden", "true"); + } + this._view.populateGrid(); + }, + + uninit: function uninit() { + this._view.destruct(); + }, +}; diff --git a/browser/metro/base/content/Util.js b/browser/metro/base/content/Util.js new file mode 100644 index 000000000000..7cd14250bd7b --- /dev/null +++ b/browser/metro/base/content/Util.js @@ -0,0 +1,412 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let Util = { + /* + * General purpose utilities + */ + + getWindowUtils: function getWindowUtils(aWindow) { + return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + }, + + // Recursively find all documents, including root document. + getAllDocuments: function getAllDocuments(doc, resultSoFar) { + resultSoFar = resultSoFar || [doc]; + if (!doc.defaultView) + return resultSoFar; + let frames = doc.defaultView.frames; + if (!frames) + return resultSoFar; + + let i; + let currentDoc; + for (i = 0; i < frames.length; i++) { + currentDoc = frames[i].document; + resultSoFar.push(currentDoc); + this.getAllDocuments(currentDoc, resultSoFar); + } + + return resultSoFar; + }, + + // Put the Mozilla networking code into a state that will kick the + // auto-connection process. + forceOnline: function forceOnline() { + Services.io.offline = false; + }, + + /* + * Timing utilties + */ + + // Executes aFunc after other events have been processed. + executeSoon: function executeSoon(aFunc) { + Services.tm.mainThread.dispatch({ + run: function() { + aFunc(); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /* + * Console printing utilities + */ + + dumpf: function dumpf(str) { + let args = arguments; + let i = 1; + dump(str.replace(/%s/g, function() { + if (i >= args.length) { + throw "dumps received too many placeholders and not enough arguments"; + } + return args[i++].toString(); + })); + }, + + // Like dump, but each arg is handled and there's an automatic newline + dumpLn: function dumpLn() { + for (let i = 0; i < arguments.length; i++) + dump(arguments[i] + " "); + dump("\n"); + }, + + dumpElement: function dumpElement(aElement) { + this.dumpLn(aElement.id); + }, + + dumpElementTree: function dumpElementTree(aElement) { + let node = aElement; + while (node) { + this.dumpLn("node:", node, "id:", node.id, "class:", node.classList); + node = node.parentNode; + } + }, + + /* + * Element utilities + */ + + highlightElement: function highlightElement(aElement) { + if (aElement == null) { + this.dumpLn("aElement is null"); + return; + } + aElement.style.border = "2px solid red"; + }, + + getHrefForElement: function getHrefForElement(target) { + let link = null; + while (target) { + if (target instanceof Ci.nsIDOMHTMLAnchorElement || + target instanceof Ci.nsIDOMHTMLAreaElement || + target instanceof Ci.nsIDOMHTMLLinkElement) { + if (target.hasAttribute("href")) + link = target; + } + target = target.parentNode; + } + + if (link && link.hasAttribute("href")) + return link.href; + else + return null; + }, + + /* + * Rect and nsIDOMRect utilities + */ + + pointWithinRect: function pointWithinRect(aX, aY, aRect) { + return (aRect.left < aX && aRect.top < aY && + aRect.right > aX && aRect.bottom > aY); + }, + + pointWithinDOMRect: function pointWithinDOMRect(aX, aY, aRect) { + if (!aRect.width || !aRect.height) + return false; + return this.pointWithinRect(aX, aY, aRect); + }, + + isEmptyDOMRect: function isEmptyDOMRect(aRect) { + if ((aRect.bottom - aRect.top) <= 0 && + (aRect.right - aRect.left) <= 0) + return true; + return false; + }, + + // Dumps the details of a dom rect to the console + dumpDOMRect: function dumpDOMRect(aMsg, aRect) { + try { + Util.dumpLn(aMsg, + "left:" + Math.round(aRect.left) + ",", + "top:" + Math.round(aRect.top) + ",", + "right:" + Math.round(aRect.right) + ",", + "bottom:" + Math.round(aRect.bottom) + ",", + "width:" + Math.round(aRect.right - aRect.left) + ",", + "height:" + Math.round(aRect.bottom - aRect.top) ); + } catch (ex) { + Util.dumpLn("dumpDOMRect:", ex.message); + } + }, + + /* + * URIs and schemes + */ + + makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) { + return Services.io.newURI(aURL, aOriginCharset, aBaseURI); + }, + + makeURLAbsolute: function makeURLAbsolute(base, url) { + // Note: makeURI() will throw if url is not a valid URI + return this.makeURI(url, null, this.makeURI(base)).spec; + }, + + isLocalScheme: function isLocalScheme(aURL) { + return ((aURL.indexOf("about:") == 0 && + aURL != "about:blank" && + aURL != "about:empty" && + aURL != "about:start") || + aURL.indexOf("chrome:") == 0); + }, + + isOpenableScheme: function isShareableScheme(aProtocol) { + let dontOpen = /^(mailto|javascript|news|snews)$/; + return (aProtocol && !dontOpen.test(aProtocol)); + }, + + isShareableScheme: function isShareableScheme(aProtocol) { + let dontShare = /^(chrome|about|file|javascript|resource)$/; + return (aProtocol && !dontShare.test(aProtocol)); + }, + + // Don't display anything in the urlbar for these special URIs. + isURLEmpty: function isURLEmpty(aURL) { + return (!aURL || + aURL == "about:blank" || + aURL == "about:empty" || + aURL == "about:home" || + aURL == "about:start"); + }, + + // Don't remember these pages in the session store. + isURLMemorable: function isURLMemorable(aURL) { + return !(aURL == "about:blank" || + aURL == "about:empty" || + aURL == "about:start"); + }, + + /* + * Math utilities + */ + + clamp: function(num, min, max) { + return Math.max(min, Math.min(max, num)); + }, + + /* + * Screen and layout utilities + */ + + get displayDPI() { + delete this.displayDPI; + return this.displayDPI = this.getWindowUtils(window).displayDPI; + }, + + isPortrait: function isPortrait() { + return (window.innerWidth <= window.innerHeight); + }, + + LOCALE_DIR_RTL: -1, + LOCALE_DIR_LTR: 1, + get localeDir() { + // determine browser dir first to know which direction to snap to + let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry); + return chromeReg.isLocaleRTL("global") ? this.LOCALE_DIR_RTL : this.LOCALE_DIR_LTR; + }, + + /* + * Process utilities + */ + + isParentProcess: function isInParentProcess() { + let appInfo = Cc["@mozilla.org/xre/app-info;1"]; + return (!appInfo || appInfo.getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT); + }, + + /* + * Event utilities + */ + + modifierMaskFromEvent: function modifierMaskFromEvent(aEvent) { + return (aEvent.altKey ? Ci.nsIDOMEvent.ALT_MASK : 0) | + (aEvent.ctrlKey ? Ci.nsIDOMEvent.CONTROL_MASK : 0) | + (aEvent.shiftKey ? Ci.nsIDOMEvent.SHIFT_MASK : 0) | + (aEvent.metaKey ? Ci.nsIDOMEvent.META_MASK : 0); + }, + + /* + * Download utilities + */ + + insertDownload: function insertDownload(aSrcUri, aFile) { + let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); + let db = dm.DBConnection; + + let stmt = db.createStatement( + "INSERT INTO moz_downloads (name, source, target, startTime, endTime, state, referrer) " + + "VALUES (:name, :source, :target, :startTime, :endTime, :state, :referrer)" + ); + + stmt.params.name = aFile.leafName; + stmt.params.source = aSrcUri.spec; + stmt.params.target = aFile.path; + stmt.params.startTime = Date.now() * 1000; + stmt.params.endTime = Date.now() * 1000; + stmt.params.state = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED; + stmt.params.referrer = aSrcUri.spec; + + stmt.execute(); + stmt.finalize(); + + let newItemId = db.lastInsertRowID; + let download = dm.getDownload(newItemId); + //dm.resumeDownload(download); + //Services.obs.notifyObservers(download, "dl-start", null); + }, + + /* + * Local system utilities + */ + + createShortcut: function Util_createShortcut(aTitle, aURL, aIconURL, aType) { + // The background images are 72px, but Android will resize as needed. + // Bigger is better than too small. + const kIconSize = 72; + const kOverlaySize = 32; + const kOffset = 20; + + // We have to fallback to something + aTitle = aTitle || aURL; + + let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.setAttribute("style", "display: none"); + + function _createShortcut() { + let icon = canvas.toDataURL("image/png", ""); + canvas = null; + try { + let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService); + shell.createShortcut(aTitle, aURL, icon, aType); + } catch(e) { + Cu.reportError(e); + } + } + + // Load the main background image first + let image = new Image(); + image.onload = function() { + canvas.width = canvas.height = kIconSize; + let ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0, kIconSize, kIconSize); + + // If we have a favicon, lets draw it next + if (aIconURL) { + let favicon = new Image(); + favicon.onload = function() { + // Center the favicon and overlay it on the background + ctx.drawImage(favicon, kOffset, kOffset, kOverlaySize, kOverlaySize); + _createShortcut(); + } + + favicon.onerror = function() { + Cu.reportError("CreateShortcut: favicon image load error"); + } + + favicon.src = aIconURL; + } else { + _createShortcut(); + } + } + + image.onerror = function() { + Cu.reportError("CreateShortcut: background image load error"); + } + + // Pick the right background + image.src = aIconURL ? "chrome://browser/skin/images/homescreen-blank-hdpi.png" + : "chrome://browser/skin/images/homescreen-default-hdpi.png"; + }, +}; + + +/* + * Timeout + * + * Helper class to nsITimer that adds a little more pizazz. Callback can be an + * object with a notify method or a function. + */ +Util.Timeout = function(aCallback) { + this._callback = aCallback; + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._type = null; +}; + +Util.Timeout.prototype = { + // Timer callback. Don't call this manually. + notify: function notify() { + if (this._type == this._timer.TYPE_ONE_SHOT) + this._type = null; + + if (this._callback.notify) + this._callback.notify(); + else + this._callback.apply(null); + }, + + // Helper function for once and interval. + _start: function _start(aDelay, aType, aCallback) { + if (aCallback) + this._callback = aCallback; + this.clear(); + this._timer.initWithCallback(this, aDelay, aType); + this._type = aType; + return this; + }, + + // Do the callback once. Cancels other timeouts on this object. + once: function once(aDelay, aCallback) { + return this._start(aDelay, this._timer.TYPE_ONE_SHOT, aCallback); + }, + + // Do the callback every aDelay msecs. Cancels other timeouts on this object. + interval: function interval(aDelay, aCallback) { + return this._start(aDelay, this._timer.TYPE_REPEATING_SLACK, aCallback); + }, + + // Clear any pending timeouts. + clear: function clear() { + if (this.isPending()) { + this._timer.cancel(); + this._type = null; + } + return this; + }, + + // If there is a pending timeout, call it and cancel the timeout. + flush: function flush() { + if (this.isPending()) { + this.notify(); + this.clear(); + } + return this; + }, + + // Return true if we are waiting for a callback. + isPending: function isPending() { + return this._type !== null; + } +}; + diff --git a/browser/metro/base/content/WebProgress.js b/browser/metro/base/content/WebProgress.js new file mode 100644 index 000000000000..5ad86bae344c --- /dev/null +++ b/browser/metro/base/content/WebProgress.js @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Progress heartbeat timer duration (ms) +const kHeartbeatDuration = 1000; +// Start and end progress screen css margins as percentages +const kProgressMarginStart = 30; +const kProgressMarginEnd = 70; + +const WebProgress = { + _progressActive: false, + + init: function init() { + messageManager.addMessageListener("Content:StateChange", this); + messageManager.addMessageListener("Content:LocationChange", this); + messageManager.addMessageListener("Content:SecurityChange", this); + Elements.progress.addEventListener("transitionend", this._progressTransEnd, true); + return this; + }, + + receiveMessage: function receiveMessage(aMessage) { + let json = aMessage.json; + let tab = Browser.getTabForBrowser(aMessage.target); + + switch (aMessage.name) { + case "Content:StateChange": { + if (json.stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { + if (json.stateFlags & Ci.nsIWebProgressListener.STATE_START) + this._windowStart(json, tab); + else if (json.stateFlags & Ci.nsIWebProgressListener.STATE_STOP) + this._windowStop(json, tab); + } + + if (json.stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + if (json.stateFlags & Ci.nsIWebProgressListener.STATE_START) + this._networkStart(json, tab); + else if (json.stateFlags & Ci.nsIWebProgressListener.STATE_STOP) + this._networkStop(json, tab); + } + + this._progressStep(); + break; + } + + case "Content:LocationChange": { + this._locationChange(json, tab); + this._progressStep(); + break; + } + + case "Content:SecurityChange": { + this._securityChange(json, tab); + this._progressStep(); + break; + } + } + }, + + _securityChange: function _securityChange(aJson, aTab) { + // Don't need to do anything if the data we use to update the UI hasn't changed + if (aTab.state == aJson.state && !aTab.hostChanged) + return; + + aTab.hostChanged = false; + aTab.state = aJson.state; + + if (aTab == Browser.selectedTab) { + IdentityUI.checkIdentity(); + } + }, + + _locationChange: function _locationChange(aJson, aTab) { + let spec = aJson.location; + let location = spec.split("#")[0]; // Ignore fragment identifier changes. + + if (aTab == Browser.selectedTab) + BrowserUI.updateURI(); + + let locationHasChanged = (location != aTab.browser.lastLocation); + if (locationHasChanged) { + Browser.getNotificationBox(aTab.browser).removeTransientNotifications(); + aTab.resetZoomLevel(); + aTab.hostChanged = true; + aTab.browser.lastLocation = location; + aTab.browser.userTypedValue = ""; + aTab.browser.appIcon = { href: null, size:-1 }; + +#ifdef MOZ_CRASHREPORTER + if (CrashReporter.enabled) + CrashReporter.annotateCrashReport("URL", spec); +#endif + this._waitForLoad(aTab); + } + + let event = document.createEvent("UIEvents"); + event.initUIEvent("URLChanged", true, false, window, locationHasChanged); + aTab.browser.dispatchEvent(event); + }, + + _waitForLoad: function _waitForLoad(aTab) { + let browser = aTab.browser; + + aTab._firstPaint = false; + + browser.messageManager.addMessageListener("Browser:FirstPaint", function firstPaintListener(aMessage) { + browser.messageManager.removeMessageListener(aMessage.name, arguments.callee); + aTab._firstPaint = true; + aTab.scrolledAreaChanged(true); + aTab.updateThumbnailSource(); + }); + }, + + _networkStart: function _networkStart(aJson, aTab) { + aTab.startLoading(); + + if (aTab == Browser.selectedTab) { + BrowserUI.update(TOOLBARSTATE_LOADING); + + // We should at least show something in the URLBar until + // the load has progressed further along + if (aTab.browser.currentURI.spec == "about:blank") + BrowserUI.updateURI({ captionOnly: true }); + } + }, + + _networkStop: function _networkStop(aJson, aTab) { + aTab.endLoading(); + + if (aTab == Browser.selectedTab) { + BrowserUI.update(TOOLBARSTATE_LOADED); + } + }, + + _windowStart: function _windowStart(aJson, aTab) { + this._progressStart(aJson, aTab); + }, + + _windowStop: function _windowStop(aJson, aTab) { + this._progressStop(aJson, aTab); + }, + + _progressStart: function _progressStart(aJson, aTab) { + // We will get multiple calls from _windowStart, so + // only process once. + if (this._progressActive) + return; + + this._progressActive = true; + + // 'Whoosh' in + this._progressCount = kProgressMarginStart; + Elements.progress.style.width = this._progressCount + "%"; + Elements.progress.removeAttribute("fade"); + + // Create a pulse timer to keep things moving even if we don't + // collect any state changes. + setTimeout(function() { + WebProgress._progressStepTimer(); + }, kHeartbeatDuration, this); + }, + + _stepProgressCount: function _stepProgressCount() { + // Step toward the end margin in smaller slices as we get closer + let left = kProgressMarginEnd - this._progressCount; + let step = left * .05; + this._progressCount += Math.ceil(step); + + // Don't go past the 'whoosh out' margin. + if (this._progressCount > kProgressMarginEnd) { + this._progressCount = kProgressMarginEnd; + } + }, + + _progressStep: function _progressStep() { + if (!this._progressActive) + return; + this._stepProgressCount(); + Elements.progress.style.width = this._progressCount + "%"; + }, + + _progressStepTimer: function _progressStepTimer() { + if (!this._progressActive) + return; + this._progressStep(); + + setTimeout(function() { + WebProgress._progressStepTimer(); + }, kHeartbeatDuration, this); + }, + + _progressStop: function _progressStop(aJson, aTab) { + this._progressActive = false; + // 'Whoosh out' and fade + Elements.progress.style.width = "100%"; + Elements.progress.setAttribute("fade", true); + }, + + _progressTransEnd: function _progressTransEnd(data) { + if (!Elements.progress.hasAttribute("fade")) + return; + // Close out fade finished, reset + if (data.propertyName == "opacity") { + Elements.progress.style.width = "0px"; + } + }, +}; + + diff --git a/browser/metro/base/content/appbar.js b/browser/metro/base/content/appbar.js new file mode 100644 index 000000000000..ca948a615aae --- /dev/null +++ b/browser/metro/base/content/appbar.js @@ -0,0 +1,230 @@ +var Appbar = { + get appbar() { return document.getElementById('appbar'); }, + get consoleButton() { return document.getElementById('console-button'); }, + get jsShellButton() { return document.getElementById('jsshell-button'); }, + get zoomInButton() { return document.getElementById('zoomin-button'); }, + get zoomOutButton() { return document.getElementById('zoomout-button'); }, + get starButton() { return document.getElementById('star-button'); }, + get pinButton() { return document.getElementById('pin-button'); }, + get moreButton() { return document.getElementById('more-button'); }, + + // track selected/active richgrid/tilegroup - the context for contextual action buttons + activeTileset: null, + + init: function Appbar_init() { + window.addEventListener('MozContextUIShow', this, false); + window.addEventListener('MozPrecisePointer', this, false); + window.addEventListener('MozImprecisePointer', this, false); + + this._updateDebugButtons(); + this._updateZoomButtons(); + + // tilegroup selection events for all modules get bubbled up + window.addEventListener("selectionchange", this, false); + }, + + handleEvent: function Appbar_handleEvent(aEvent) { + switch (aEvent.type) { + case 'MozContextUIShow': + this._updatePinButton(); + this._updateStarButton(); + break; + case 'MozPrecisePointer': + case 'MozImprecisePointer': + this._updateZoomButtons(); + break; + case "selectionchange": + let nodeName = aEvent.target.nodeName; + if ('richgrid' === nodeName) { + this._onTileSelectionChanged(aEvent); + } + break; + } + }, + + onDownloadButton: function() { + PanelUI.show("downloads-container"); + ContextUI.dismiss(); + }, + + onZoomOutButton: function() { + Browser.zoom(1); + }, + + onZoomInButton: function() { + Browser.zoom(-1); + }, + + onPinButton: function() { + if (this.pinButton.checked) { + Browser.pinSite(); + } else { + Browser.unpinSite(); + } + }, + + onStarButton: function(aValue) { + if (aValue === undefined) { + aValue = this.starButton.checked; + } + + if (aValue) { + Browser.starSite(function () { + Appbar._updateStarButton(); + }); + } else { + Browser.unstarSite(function () { + Appbar._updateStarButton(); + }); + } + }, + + onMoreButton: function(aEvent) { + var typesArray = ["find-in-page"]; + try { + // If we have a valid http or https URI then show the view on desktop + // menu item. + var uri = Services.io.newURI(Browser.selectedBrowser.currentURI.spec, + null, null); + if (uri.schemeIs('http') || uri.schemeIs('https')) { + typesArray.push("view-on-desktop"); + } + } catch(ex) { + } + + var x = this.moreButton.getBoundingClientRect().left; + var y = this.appbar.getBoundingClientRect().top; + ContextMenuUI.showContextMenu({ + json: { + types: typesArray, + string: '', + xPos: x, + yPos: y, + forcePosition: true, + leftAligned: true, + bottomAligned: true + } + + }); + }, + + onViewOnDesktop: function() { + try { + // Make sure we have a valid URI so Windows doesn't prompt + // with an unrecognized command, select default program window + var uri = Services.io.newURI(Browser.selectedBrowser.currentURI.spec, + null, null); + if (uri.schemeIs('http') || uri.schemeIs('https')) { + MetroUtils.launchInDesktop(Browser.selectedBrowser.currentURI.spec, ""); + } + } catch(ex) { + } + }, + + onConsoleButton: function() { + PanelUI.show("console-container"); + }, + + onJSShellButton: function() { + // XXX for debugging, this only works when running on the desktop. + if (!MetroUtils.immersive) + window.openDialog("chrome://browser/content/shell.xul", "_blank", + "all=no,scrollbars=yes,resizable=yes,dialog=no"); + }, + + dispatchContextualAction: function(aActionName){ + let activeTileset = this.activeTileset; + if (activeTileset) { + // fire event on the richgrid, others can listen + // but we keep coupling loose so grid doesn't need to know about appbar + let event = document.createEvent("Events"); + event.action = aActionName; + event.initEvent("context-action", true, false); + activeTileset.dispatchEvent(event); + + // done with this selection, explicitly clear it + activeTileset.clearSelection(); + } + this.appbar.dismiss(); + }, + + showContextualActions: function(aVerbs){ + let doc = document; + + // button element id to action verb lookup + let buttonsMap = new Map(); + for (let verb of aVerbs) { + let id = verb + "-selected-button"; + let buttonNode = doc.getElementById(id); + if (buttonNode) { + buttonsMap.set(id, verb); + } else { + Util.dumpLn("Appbar.showContextualActions: no button for " + verb); + } + } + + // hide/show buttons as appropriate + let buttons = doc.querySelectorAll("#contextualactions-tray > toolbarbutton"); + + for (let btnNode of buttons) { + if (buttonsMap.has(btnNode.id)){ + btnNode.hidden = false; + } else { + btnNode.hidden = true; + } + }; + + if (buttonsMap.size) { + // there are buttons to show + // TODO: show the contextual actions tray? + } else { + // 0 actions to show; + // TODO: hide the contextual actions tray entirely? + } + }, + + _onTileSelectionChanged: function _onTileSelectionChanged(aEvent){ + let activeTileset = aEvent.target; + + // deselect tiles in other tile groups + if (this.activeTileset && this.activeTileset !== activeTileset) { + this.activeTileset.clearSelection(); + } + // keep track of which view is the target/scope for the contextual actions + this.activeTileset = activeTileset; + + // ask the view for the list verbs/action-names it thinks are + // appropriate for the tiles selected + let contextActions = activeTileset.contextActions; + let verbs = [v for (v of contextActions)]; + + // could transition in old, new buttons? + this.showContextualActions(verbs); + + if (verbs.length) { + this.appbar.show(); + } else { + this.appbar.dismiss(); + } + }, + + _updatePinButton: function() { + this.pinButton.checked = Browser.isSitePinned(); + }, + + _updateStarButton: function() { + Browser.isSiteStarredAsync(function (isStarred) { + this.starButton.checked = isStarred; + }.bind(this)); + }, + + _updateDebugButtons: function() { + this.consoleButton.disabled = !ConsolePanelView.enabled; + this.jsShellButton.disabled = MetroUtils.immersive; + }, + + _updateZoomButtons: function() { + let zoomDisabled = !InputSourceHelper.isPrecise; + this.zoomOutButton.disabled = this.zoomInButton.disabled = zoomDisabled; + } + }; diff --git a/browser/metro/base/content/bindings/appbar.xml b/browser/metro/base/content/bindings/appbar.xml new file mode 100644 index 000000000000..8f8df361d99d --- /dev/null +++ b/browser/metro/base/content/bindings/appbar.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + false + document.getAnonymousElementByAttribute(this, "anonid", "toolbar"); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false; + + + diff --git a/browser/metro/base/content/bindings/arrowbox.xml b/browser/metro/base/content/bindings/arrowbox.xml new file mode 100644 index 000000000000..1f1f0e6262ee --- /dev/null +++ b/browser/metro/base/content/bindings/arrowbox.xml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + window.innerHeight) { + content.style.overflow = "hidden"; + this.style.minHeight = (window.innerHeight - parseInt(this.top) - kBottomMargin) + "px"; + } + + let HALF_ARROW_WIDTH = 16; + + let anchorClass = ""; + let hideArrow = false; + if (horizPos == 0) { + container.orient = "vertical"; + arrowbox.orient = ""; + if (vertPos == 0) { + hideArrow = true; + } else { + let anchorPosX = 0.5; + // check for hasAttribute because, in some cases, anchorNode is actually a rect + if (this.anchorNode && this.anchorNode.hasAttribute && this.anchorNode.hasAttribute("anchorPosX")) + anchorPosX = parseFloat(this.anchorNode.getAttribute("anchorPosX")) || 0.5; + arrowbox.style.marginLeft = ((targetRect.left - popupRect.left) + (targetRect.width * anchorPosX) - HALF_ARROW_WIDTH) + "px"; + if (vertPos == 1) { + container.dir = "normal"; + anchorClass = "top"; + } else if (vertPos == -1) { + container.dir = "reverse"; + anchorClass = "bottom"; + } + } + } else if (vertPos == 0) { + container.orient = ""; + arrowbox.orient = "vertical"; + let anchorPosY = 0.5; + // check for hasAttribute because, in some cases, anchorNode is actually a rect + if (this.anchorNode && this.anchorNode.hasAttribute && this.anchorNode.hasAttribute("anchorPosY")) + anchorPosY = parseFloat(this.anchorNode.getAttribute("anchorPosY")) || 0.5; + arrowbox.style.marginTop = ((targetRect.top - popupRect.top) + (targetRect.height * anchorPosY) - HALF_ARROW_WIDTH) + "px"; + if (horizPos == 1) { + container.dir = "ltr"; + anchorClass = "left"; + } else if (horizPos == -1) { + container.dir = "rtl"; + anchorClass = "right"; + } + } else { + hideArrow = true; + } + arrow.hidden = hideArrow; + arrow.setAttribute("side", anchorClass); + ]]> + + + null + + + + + window.innerWidth) + left = window.innerWidth - popupRect.width; + else if (left < 0) + left = 1; + + popupRect.left = left; + this.setAttribute("left", left); + popupRect.top = top; + this.setAttribute("top", top); + } else { + horizPos = (Math.round(popupRect.right) <= Math.round(anchorRect.left + offset)) ? -1 : + (Math.round(popupRect.left) >= Math.round(anchorRect.right - offset)) ? 1 : 0; + vertPos = (Math.round(popupRect.bottom) <= Math.round(anchorRect.top + offset)) ? -1 : + (Math.round(popupRect.top) >= Math.round(anchorRect.bottom - offset)) ? 1 : 0; + } + this._updateArrow(popupRect, anchorRect, horizPos, vertPos); + ]]> + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/autocomplete.xml b/browser/metro/base/content/bindings/autocomplete.xml new file mode 100644 index 000000000000..dbe2dfe6e7a8 --- /dev/null +++ b/browser/metro/base/content/bindings/autocomplete.xml @@ -0,0 +1,445 @@ + + + + + +%browserDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + null + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + : Pressing page up/down should jump between rows, + // not just items in the grid + + // Move between grids if we're at the edge of one + if ((this._grid.isSelectionAtEnd && !aReverse) || + (this._grid.isSelectionAtStart && aReverse)) { + let index = aReverse ? this._otherGrid.itemCount - 1 : 0; + this._otherGrid.selectedIndex = index; + } else { + this._grid.offsetSelection(aReverse ? -1 : 1); + } + + this._handleOnSelect = handleOnSelect; + ]]> + + + + + + + + + + + + + + + + + + + lastMatch) { + let lastItem = this._results.itemCount - 1; + this._results.removeItemAt(lastItem); + continue; + } + + let value = controller.getValueAt(i); + let label = controller.getCommentAt(i) || value; + let iconURI = controller.getImageAt(i); + + let item = this._results.getItemAtIndex(i); + if (item == null) { + item = this._results.appendItem(label, value); + item.setAttribute("autocomplete", "true"); + } else { + item.setAttribute("label", label); + item.setAttribute("value", value); + } + + item.setAttribute("iconURI", iconURI); + } + + this._results.arrangeItems(); + ]]> + + + + + + + + + + 0) + this._searches.removeItemAt(0); + + this._engines.forEach(function (anEngine) { + let item = this._searches.appendItem(anEngine.name, anEngine.name); + item.setAttribute("autocomplete", "true"); + item.setAttribute("search", "true"); + + let iconURI = anEngine.iconURI ? anEngine.iconURI.spec : ""; + item.setAttribute("iconURI", iconURI); + }.bind(this)); + + this._searches.arrangeItems(); + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + [] + true + null + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/bindings.xml b/browser/metro/base/content/bindings/bindings.xml new file mode 100644 index 000000000000..a2ba5420243e --- /dev/null +++ b/browser/metro/base/content/bindings/bindings.xml @@ -0,0 +1,407 @@ + + + + + +%browserDTD; +]> + + + + + + + + + 0.8) + this._insertItems(); + ]]> + + + + + + + this.scrollBoxObject.height; + [] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + document.getAnonymousElementByAttribute(this, "anonid", "previous-button"); + + + + document.getAnonymousElementByAttribute(this, "anonid", "next-button"); + + + + document.getAnonymousElementByAttribute(this, "anonid", "close-button"); + + + + + + + + + null + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 || selectionEnd < aTextbox.textLength) + json.types.push("select-all"); + + let clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]. + getService(Ci.nsIClipboard); + let flavors = ["text/unicode"]; + let hasData = clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard); + + if (hasData && (!aTextbox.readOnly || aIgnoreReadOnly)) { + json.types.push("paste"); + if (aTextbox.type == "url") { + json.types.push("paste-url"); + } + } + json.xPos = aEvent.clientX; + json.yPos = aEvent.clientY; + json.source = aEvent.mozInputSource; + ContextMenuUI.showContextMenu({ target: aTextbox, json: json }); + ]]> + + + + diff --git a/browser/metro/base/content/bindings/browser.js b/browser/metro/base/content/bindings/browser.js new file mode 100644 index 000000000000..e011b6f40841 --- /dev/null +++ b/browser/metro/base/content/bindings/browser.js @@ -0,0 +1,796 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +let WebProgressListener = { + _lastLocation: null, + _firstPaint: false, + + init: function() { + let flags = Ci.nsIWebProgress.NOTIFY_LOCATION | + Ci.nsIWebProgress.NOTIFY_SECURITY | + Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | + Ci.nsIWebProgress.NOTIFY_STATE_NETWORK | + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT; + + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(this, flags); + }, + + onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (content != aWebProgress.DOMWindow) + return; + + sendAsyncMessage("Content:StateChange", { + contentWindowId: this.contentWindowId, + stateFlags: aStateFlags, + status: aStatus + }); + }, + + onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { + if (content != aWebProgress.DOMWindow) + return; + + let spec = aLocationURI ? aLocationURI.spec : ""; + let location = spec.split("#")[0]; + + let charset = content.document.characterSet; + + sendAsyncMessage("Content:LocationChange", { + contentWindowId: this.contentWindowId, + documentURI: aWebProgress.DOMWindow.document.documentURIObject.spec, + location: spec, + canGoBack: docShell.canGoBack, + canGoForward: docShell.canGoForward, + charset: charset.toString() + }); + + this._firstPaint = false; + let self = this; + + // Keep track of hash changes + this.hashChanged = (location == this._lastLocation); + this._lastLocation = location; + + // When a new page is loaded fire a message for the first paint + addEventListener("MozAfterPaint", function(aEvent) { + removeEventListener("MozAfterPaint", arguments.callee, true); + + self._firstPaint = true; + let scrollOffset = ContentScroll.getScrollOffset(content); + sendAsyncMessage("Browser:FirstPaint", scrollOffset); + }, true); + }, + + onStatusChange: function onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + }, + + onSecurityChange: function onSecurityChange(aWebProgress, aRequest, aState) { + if (content != aWebProgress.DOMWindow) + return; + + let serialization = SecurityUI.getSSLStatusAsString(); + + sendAsyncMessage("Content:SecurityChange", { + contentWindowId: this.contentWindowId, + SSLStatusAsString: serialization, + state: aState + }); + }, + + get contentWindowId() { + return content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + }, + + QueryInterface: function QueryInterface(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) { + return this; + } + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +}; + +WebProgressListener.init(); + + +let SecurityUI = { + getSSLStatusAsString: function() { + let status = docShell.securityUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus; + + if (status) { + let serhelper = Cc["@mozilla.org/network/serialization-helper;1"] + .getService(Ci.nsISerializationHelper); + + status.QueryInterface(Ci.nsISerializable); + return serhelper.serializeToString(status); + } + + return null; + } +}; + +let WebNavigation = { + _webNavigation: docShell.QueryInterface(Ci.nsIWebNavigation), + _timer: null, + + init: function() { + addMessageListener("WebNavigation:GoBack", this); + addMessageListener("WebNavigation:GoForward", this); + addMessageListener("WebNavigation:GotoIndex", this); + addMessageListener("WebNavigation:LoadURI", this); + addMessageListener("WebNavigation:Reload", this); + addMessageListener("WebNavigation:Stop", this); + }, + + receiveMessage: function(message) { + switch (message.name) { + case "WebNavigation:GoBack": + this.goBack(); + break; + case "WebNavigation:GoForward": + this.goForward(); + break; + case "WebNavigation:GotoIndex": + this.gotoIndex(message); + break; + case "WebNavigation:LoadURI": + this.loadURI(message); + break; + case "WebNavigation:Reload": + this.reload(message); + break; + case "WebNavigation:Stop": + this.stop(message); + break; + } + }, + + goBack: function() { + if (this._webNavigation.canGoBack) + this._webNavigation.goBack(); + }, + + goForward: function() { + if (this._webNavigation.canGoForward) + this._webNavigation.goForward(); + }, + + gotoIndex: function(message) { + this._webNavigation.gotoIndex(message.index); + }, + + loadURI: function(message) { + let flags = message.json.flags || this._webNavigation.LOAD_FLAGS_NONE; + this._webNavigation.loadURI(message.json.uri, flags, null, null, null); + + let tabData = message.json; + if (tabData.entries) { + // We are going to load from history so kill the current load. We do not + // want the load added to the history anyway. We reload after resetting history + this._webNavigation.stop(this._webNavigation.STOP_ALL); + this._restoreHistory(tabData, 0); + } + }, + + reload: function(message) { + let flags = message.json.flags || this._webNavigation.LOAD_FLAGS_NONE; + this._webNavigation.reload(flags); + }, + + stop: function(message) { + let flags = message.json.flags || this._webNavigation.STOP_ALL; + this._webNavigation.stop(flags); + }, + + _restoreHistory: function _restoreHistory(aTabData, aCount) { + // We need to wait for the sessionHistory to be initialized and there + // is no good way to do this. We'll try a wait loop like desktop + try { + if (!this._webNavigation.sessionHistory) + throw new Error(); + } catch (ex) { + if (aCount < 10) { + let self = this; + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback(function(aTimer) { + self._timer = null; + self._restoreHistory(aTabData, aCount + 1); + }, 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + } + + let history = this._webNavigation.sessionHistory; + if (history.count > 0) + history.PurgeHistory(history.count); + history.QueryInterface(Ci.nsISHistoryInternal); + + // helper hashes for ensuring unique frame IDs and unique document + // identifiers. + let idMap = { used: {} }; + let docIdentMap = {}; + + for (let i = 0; i < aTabData.entries.length; i++) { + if (!aTabData.entries[i].url) + continue; + history.addEntry(this._deserializeHistoryEntry(aTabData.entries[i], idMap, docIdentMap), true); + } + + // We need to force set the active history item and cause it to reload since + // we stop the load above + let activeIndex = (aTabData.index || aTabData.entries.length) - 1; + history.getEntryAtIndex(activeIndex, true); + history.QueryInterface(Ci.nsISHistory).reloadCurrentEntry(); + }, + + _deserializeHistoryEntry: function _deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) { + let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry); + + shEntry.setURI(Services.io.newURI(aEntry.url, null, null)); + shEntry.setTitle(aEntry.title || aEntry.url); + if (aEntry.subframe) + shEntry.setIsSubFrame(aEntry.subframe || false); + shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; + if (aEntry.contentType) + shEntry.contentType = aEntry.contentType; + if (aEntry.referrer) + shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null); + + if (aEntry.cacheKey) { + let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(Ci.nsISupportsPRUint32); + cacheKey.data = aEntry.cacheKey; + shEntry.cacheKey = cacheKey; + } + + if (aEntry.ID) { + // get a new unique ID for this frame (since the one from the last + // start might already be in use) + let id = aIdMap[aEntry.ID] || 0; + if (!id) { + for (id = Date.now(); id in aIdMap.used; id++); + aIdMap[aEntry.ID] = id; + aIdMap.used[id] = true; + } + shEntry.ID = id; + } + + if (aEntry.docshellID) + shEntry.docshellID = aEntry.docshellID; + + if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) { + shEntry.stateData = + Cc["@mozilla.org/docshell/structured-clone-container;1"]. + createInstance(Ci.nsIStructuredCloneContainer); + + shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion); + } + + if (aEntry.scroll) { + let scrollPos = aEntry.scroll.split(","); + scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; + shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); + } + + let childDocIdents = {}; + if (aEntry.docIdentifier) { + // If we have a serialized document identifier, try to find an SHEntry + // which matches that doc identifier and adopt that SHEntry's + // BFCacheEntry. If we don't find a match, insert shEntry as the match + // for the document identifier. + let matchingEntry = aDocIdentMap[aEntry.docIdentifier]; + if (!matchingEntry) { + matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; + aDocIdentMap[aEntry.docIdentifier] = matchingEntry; + } else { + shEntry.adoptBFCacheEntry(matchingEntry); + childDocIdents = matchingEntry.childDocIdents; + } + } + + if (aEntry.owner_b64) { + let ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); + let binaryData = atob(aEntry.owner_b64); + ownerInput.setData(binaryData, binaryData.length); + let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIObjectInputStream); + binaryStream.setInputStream(ownerInput); + try { // Catch possible deserialization exceptions + shEntry.owner = binaryStream.readObject(true); + } catch (ex) { dump(ex); } + } + + if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { + for (let i = 0; i < aEntry.children.length; i++) { + if (!aEntry.children[i].url) + continue; + + // We're getting sessionrestore.js files with a cycle in the + // doc-identifier graph, likely due to bug 698656. (That is, we have + // an entry where doc identifier A is an ancestor of doc identifier B, + // and another entry where doc identifier B is an ancestor of A.) + // + // If we were to respect these doc identifiers, we'd create a cycle in + // the SHEntries themselves, which causes the docshell to loop forever + // when it looks for the root SHEntry. + // + // So as a hack to fix this, we restrict the scope of a doc identifier + // to be a node's siblings and cousins, and pass childDocIdents, not + // aDocIdents, to _deserializeHistoryEntry. That is, we say that two + // SHEntries with the same doc identifier have the same document iff + // they have the same parent or their parents have the same document. + + shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i); + } + } + + return shEntry; + }, + + sendHistory: function sendHistory() { + // We need to package up the session history and send it to the sessionstore + let entries = []; + let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; + for (let i = 0; i < history.count; i++) { + let entry = this._serializeHistoryEntry(history.getEntryAtIndex(i, false)); + entries.push(entry); + } + let index = history.index + 1; + sendAsyncMessage("Content:SessionHistory", { entries: entries, index: index }); + }, + + _serializeHistoryEntry: function _serializeHistoryEntry(aEntry) { + let entry = { url: aEntry.URI.spec }; + + if (aEntry.title && aEntry.title != entry.url) + entry.title = aEntry.title; + + if (!(aEntry instanceof Ci.nsISHEntry)) + return entry; + + let cacheKey = aEntry.cacheKey; + if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0) + entry.cacheKey = cacheKey.data; + + entry.ID = aEntry.ID; + entry.docshellID = aEntry.docshellID; + + if (aEntry.referrerURI) + entry.referrer = aEntry.referrerURI.spec; + + if (aEntry.contentType) + entry.contentType = aEntry.contentType; + + let x = {}, y = {}; + aEntry.getScrollPosition(x, y); + if (x.value != 0 || y.value != 0) + entry.scroll = x.value + "," + y.value; + + if (aEntry.owner) { + try { + let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIObjectOutputStream); + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + binaryStream.setOutputStream(pipe.outputStream); + binaryStream.writeCompoundObject(aEntry.owner, Ci.nsISupports, true); + binaryStream.close(); + + // Now we want to read the data from the pipe's input end and encode it. + let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + scriptableStream.setInputStream(pipe.inputStream); + let ownerBytes = scriptableStream.readByteArray(scriptableStream.available()); + // We can stop doing base64 encoding once our serialization into JSON + // is guaranteed to handle all chars in strings, including embedded + // nulls. + entry.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes)); + } catch (e) { dump(e); } + } + + entry.docIdentifier = aEntry.BFCacheEntry.ID; + + if (aEntry.stateData != null) { + entry.structuredCloneState = aEntry.stateData.getDataAsBase64(); + entry.structuredCloneVersion = aEntry.stateData.formatVersion; + } + + if (!(aEntry instanceof Ci.nsISHContainer)) + return entry; + + if (aEntry.childCount > 0) { + entry.children = []; + for (let i = 0; i < aEntry.childCount; i++) { + let child = aEntry.GetChildAt(i); + if (child) + entry.children.push(this._serializeHistoryEntry(child)); + else // to maintain the correct frame order, insert a dummy entry + entry.children.push({ url: "about:blank" }); + + // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) + if (/^wyciwyg:\/\//.test(entry.children[i].url)) { + delete entry.children; + break; + } + } + } + + return entry; + } +}; + +WebNavigation.init(); + + +let DOMEvents = { + init: function() { + addEventListener("DOMContentLoaded", this, false); + addEventListener("DOMTitleChanged", this, false); + addEventListener("DOMLinkAdded", this, false); + addEventListener("DOMWillOpenModalDialog", this, false); + addEventListener("DOMModalDialogClosed", this, true); + addEventListener("DOMWindowClose", this, false); + addEventListener("DOMPopupBlocked", this, false); + addEventListener("pageshow", this, false); + addEventListener("pagehide", this, false); + }, + + handleEvent: function(aEvent) { + let document = content.document; + switch (aEvent.type) { + case "DOMContentLoaded": + if (document.documentURIObject.spec == "about:blank") + return; + + sendAsyncMessage("DOMContentLoaded", { }); + + // Send the session history now too + WebNavigation.sendHistory(); + break; + + case "pageshow": + case "pagehide": { + if (aEvent.target.defaultView != content) + break; + + let util = aEvent.target.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let json = { + contentWindowWidth: content.innerWidth, + contentWindowHeight: content.innerHeight, + windowId: util.outerWindowID, + persisted: aEvent.persisted + }; + + // Clear onload focus to prevent the VKB to be shown unexpectingly + // but only if the location has really changed and not only the + // fragment identifier + let contentWindowID = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; + if (!WebProgressListener.hashChanged && contentWindowID == util.currentInnerWindowID) { + let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + focusManager.clearFocus(content); + } + + sendAsyncMessage(aEvent.type, json); + break; + } + + case "DOMPopupBlocked": { + let util = aEvent.requestingWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let json = { + windowId: util.outerWindowID, + popupWindowURI: { + spec: aEvent.popupWindowURI.spec, + charset: aEvent.popupWindowURI.originCharset + }, + popupWindowFeatures: aEvent.popupWindowFeatures, + popupWindowName: aEvent.popupWindowName + }; + + sendAsyncMessage("DOMPopupBlocked", json); + break; + } + + case "DOMTitleChanged": + sendAsyncMessage("DOMTitleChanged", { title: document.title }); + break; + + case "DOMLinkAdded": + let target = aEvent.originalTarget; + if (!target.href || target.disabled) + return; + + let json = { + windowId: target.ownerDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID, + href: target.href, + charset: document.characterSet, + title: target.title, + rel: target.rel, + type: target.type + }; + + // rel=icon can also have a sizes attribute + if (target.hasAttribute("sizes")) + json.sizes = target.getAttribute("sizes"); + + sendAsyncMessage("DOMLinkAdded", json); + break; + + case "DOMWillOpenModalDialog": + case "DOMModalDialogClosed": + case "DOMWindowClose": + let retvals = sendSyncMessage(aEvent.type, { }); + for (let i in retvals) { + if (retvals[i].preventDefault) { + aEvent.preventDefault(); + break; + } + } + break; + } + } +}; + +DOMEvents.init(); + +let ContentScroll = { + _scrollOffset: { x: 0, y: 0 }, + + init: function() { + addMessageListener("Content:SetCacheViewport", this); + addMessageListener("Content:SetWindowSize", this); + + addEventListener("scroll", this, false); + addEventListener("pagehide", this, false); + addEventListener("MozScrolledAreaChanged", this, false); + }, + + getScrollOffset: function(aWindow) { + let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + let scrollX = {}, scrollY = {}; + cwu.getScrollXY(false, scrollX, scrollY); + return { x: scrollX.value, y: scrollY.value }; + }, + + getScrollOffsetForElement: function(aElement) { + if (aElement.parentNode == aElement.ownerDocument) + return this.getScrollOffset(aElement.ownerDocument.defaultView); + return { x: aElement.scrollLeft, y: aElement.scrollTop }; + }, + + setScrollOffsetForElement: function(aElement, aLeft, aTop) { + if (aElement.parentNode == aElement.ownerDocument) { + aElement.ownerDocument.defaultView.scrollTo(aLeft, aTop); + } else { + aElement.scrollLeft = aLeft; + aElement.scrollTop = aTop; + } + }, + + receiveMessage: function(aMessage) { + let json = aMessage.json; + switch (aMessage.name) { + case "Content:SetCacheViewport": { + // Set resolution for root view + let rootCwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + if (json.id == 1) { + rootCwu.setResolution(json.scale, json.scale); + if (!WebProgressListener._firstPaint) + break; + } + + let displayport = new Rect(json.x, json.y, json.w, json.h); + if (displayport.isEmpty()) + break; + + // Map ID to element + let element = null; + try { + element = rootCwu.findElementWithViewId(json.id); + } catch(e) { + // This could give NS_ERROR_NOT_AVAILABLE. In that case, the + // presshell is not available because the page is reloading. + } + + if (!element) + break; + + let binding = element.ownerDocument.getBindingParent(element); + if (binding instanceof Ci.nsIDOMHTMLInputElement && binding.mozIsTextField(false)) + break; + + // Set the scroll offset for this element if specified + if (json.scrollX >= 0 && json.scrollY >= 0) { + this.setScrollOffsetForElement(element, json.scrollX, json.scrollY) + if (json.id == 1) + this._scrollOffset = this.getScrollOffset(content); + } + + // Set displayport. We want to set this after setting the scroll offset, because + // it is calculated based on the scroll offset. + let scrollOffset = this.getScrollOffsetForElement(element); + let x = displayport.x - scrollOffset.x; + let y = displayport.y - scrollOffset.y; + + if (json.id == 1) { + x = Math.round(x * json.scale) / json.scale; + y = Math.round(y * json.scale) / json.scale; + } + + let win = element.ownerDocument.defaultView; + let winCwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + winCwu.setDisplayPortForElement(x, y, displayport.width, displayport.height, element); + break; + } + + case "Content:SetWindowSize": { + let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + cwu.setCSSViewport(json.width, json.height); + break; + } + } + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "pagehide": + this._scrollOffset = { x: 0, y: 0 }; + break; + + case "scroll": { + let doc = aEvent.target; + if (doc != content.document) + break; + + this.sendScroll(); + break; + } + + case "MozScrolledAreaChanged": { + let doc = aEvent.originalTarget; + if (content != doc.defaultView) // We are only interested in root scroll pane changes + return; + + sendAsyncMessage("MozScrolledAreaChanged", { + width: aEvent.width, + height: aEvent.height, + left: aEvent.x + content.scrollX + }); + + // Send event only after painting to make sure content views in the parent process have + // been updated. + addEventListener("MozAfterPaint", function afterPaint() { + removeEventListener("MozAfterPaint", afterPaint, false); + sendAsyncMessage("Content:UpdateDisplayPort"); + }, false); + + break; + } + } + }, + + sendScroll: function sendScroll() { + let scrollOffset = this.getScrollOffset(content); + if (this._scrollOffset.x == scrollOffset.x && this._scrollOffset.y == scrollOffset.y) + return; + + this._scrollOffset = scrollOffset; + sendAsyncMessage("scroll", scrollOffset); + } +}; + +ContentScroll.init(); + +let ContentActive = { + init: function() { + addMessageListener("Content:Activate", this); + addMessageListener("Content:Deactivate", this); + }, + + receiveMessage: function(aMessage) { + let json = aMessage.json; + switch (aMessage.name) { + case "Content:Deactivate": + docShell.isActive = false; + let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + if (json.keepviewport) + break; + cwu.setDisplayPortForElement(0, 0, 0, 0, content.document.documentElement); + break; + + case "Content:Activate": + docShell.isActive = true; + break; + } + } +}; + +ContentActive.init(); + +/** + * Helper class for IndexedDB, child part. Listens using + * the observer service for events regarding IndexedDB + * prompts, and sends messages to the parent to actually + * show the prompts. + */ +let IndexedDB = { + _permissionsPrompt: "indexedDB-permissions-prompt", + _permissionsResponse: "indexedDB-permissions-response", + + _quotaPrompt: "indexedDB-quota-prompt", + _quotaResponse: "indexedDB-quota-response", + _quotaCancel: "indexedDB-quota-cancel", + + waitingObservers: [], + + init: function IndexedDBPromptHelper_init() { + let os = Services.obs; + os.addObserver(this, this._permissionsPrompt, false); + os.addObserver(this, this._quotaPrompt, false); + os.addObserver(this, this._quotaCancel, false); + addMessageListener("IndexedDB:Response", this); + }, + + observe: function IndexedDBPromptHelper_observe(aSubject, aTopic, aData) { + if (aTopic != this._permissionsPrompt && aTopic != this._quotaPrompt && aTopic != this._quotaCancel) { + throw new Error("Unexpected topic!"); + } + + let requestor = aSubject.QueryInterface(Ci.nsIInterfaceRequestor); + let observer = requestor.getInterface(Ci.nsIObserver); + + let contentWindow = requestor.getInterface(Ci.nsIDOMWindow); + let contentDocument = contentWindow.document; + + if (aTopic == this._quotaCancel) { + observer.observe(null, this._quotaResponse, Ci.nsIPermissionManager.UNKNOWN_ACTION); + return; + } + + // Remote to parent + sendAsyncMessage("IndexedDB:Prompt", { + topic: aTopic, + host: contentDocument.documentURIObject.asciiHost, + location: contentDocument.location.toString(), + data: aData, + observerId: this.addWaitingObserver(observer) + }); + }, + + receiveMessage: function(aMessage) { + let payload = aMessage.json; + switch (aMessage.name) { + case "IndexedDB:Response": + let observer = this.getAndRemoveWaitingObserver(payload.observerId); + observer.observe(null, payload.responseTopic, payload.permission); + } + }, + + addWaitingObserver: function(aObserver) { + let observerId = 0; + while (observerId in this.waitingObservers) + observerId++; + this.waitingObservers[observerId] = aObserver; + return observerId; + }, + + getAndRemoveWaitingObserver: function(aObserverId) { + let observer = this.waitingObservers[aObserverId]; + delete this.waitingObservers[aObserverId]; + return observer; + } +}; + +IndexedDB.init(); + diff --git a/browser/metro/base/content/bindings/browser.xml b/browser/metro/base/content/bindings/browser.xml new file mode 100644 index 000000000000..146c732aaf7a --- /dev/null +++ b/browser/metro/base/content/bindings/browser.xml @@ -0,0 +1,1089 @@ + + + + + + %findBarDTD; +]> + + + + + + null + + + + + + [] + + + null + + + null + + + + null + + + Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + null + null + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + + + + + + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + binding. + this.removeEventListener("pageshow", this.onPageShow, true); + this.removeEventListener("pagehide", this.onPageHide, true); + this.removeEventListener("DOMPopupBlocked", this.onPopupBlocked, true); + ]]> + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + null + + + + + + + + + + + + + + + + + + + + + + true + + + + + ({}) + + + + + = this.kDieTime) + this._die(); + else + // This doesn't need to be exact, just be sure to clean up at some point. + this._timeout = setTimeout(this._dieIfOld.bind(this), this.kDieTime); + }, + + /** Cleanup after ourselves. */ + _die: function() { + let timeout = this._timeout; + if (timeout) { + clearTimeout(timeout); + this._timeout = null; + } + + if (this._contentView && Math.abs(this._pixelsPannedSinceRefresh) > 0) + this._updateCacheViewport(); + + // We expect contentViews to contain our ID. If not, something bad + // happened. + delete this.self._contentViews[this._id]; + }, + + /** + * Given the cache size and the viewport size, this determines where the cache + * should start relative to the scroll position. This adjusts the position based + * on which direction the user is panning, so that we use our cache as + * effectively as possible. + * + * @param aDirection Negative means user is panning to the left or above + * Zero means user did not pan + * Positive means user is panning to the right or below + * @param aViewportSize The width or height of the viewport + * @param aCacheSize The width or height of the displayport + */ + _getRelativeCacheStart: function(aDirection, aViewportSize, aCacheSize) { + // Remember that this is relative to the viewport scroll position. + // Let's assume we are thinking about the y-axis. + // The extreme cases: + // |0| would mean that there is no content available above + // |aViewportSize - aCacheSize| would mean no content available below + // + // Taking the average of the extremes puts equal amounts of cache on the + // top and bottom of the viewport. If we think of this like a weighted + // average, .5 is the sweet spot where equals amounts of content are + // above and below the visible area. + // + // This weight is therefore how much of the cache is above (or to the + // left) the visible area. + let cachedAbove = .5; + + // If panning down, leave only 25% of the non-visible cache above. + if (aDirection > 0) + cachedAbove = .25; + + // If panning up, Leave 75% of the non-visible cache above. + if (aDirection < 0) + cachedAbove = .75; + + return (aViewportSize - aCacheSize) * cachedAbove; + }, + + /** Determine size of the pixel cache. */ + _getCacheSize: function(viewportSize) { + let self = this.self; + let contentView = this._contentView; + + let cacheWidth = self._cacheRatioWidth * viewportSize.width; + let cacheHeight = self._cacheRatioHeight * viewportSize.height; + let contentSize = this._getContentSize(); + let contentWidth = contentSize.width; + let contentHeight = contentSize.height; + + // There are common cases, such as long skinny pages, where our cache size is + // bigger than our content size. In those cases, we take that sliver of leftover + // space and apply it to the other dimension. + if (contentWidth < cacheWidth) { + cacheHeight += (cacheWidth - contentWidth) * cacheHeight / cacheWidth; + cacheWidth = contentWidth; + } else if (contentHeight < cacheHeight) { + cacheWidth += (cacheHeight - contentHeight) * cacheWidth / cacheHeight; + cacheHeight = contentHeight; + } + + return { width: cacheWidth, height: cacheHeight }; + }, + + _sendDisplayportUpdate: function(scrollX, scrollY) { + let self = this.self; + if (!self.active) + return; + + let contentView = this._contentView; + let viewportSize = this._getViewportSize(); + let cacheSize = this._getCacheSize(viewportSize); + let cacheX = this._getRelativeCacheStart(this._pixelsPannedSinceRefresh.x, viewportSize.width, cacheSize.width) + contentView.scrollX; + let cacheY = this._getRelativeCacheStart(this._pixelsPannedSinceRefresh.y, viewportSize.height, cacheSize.height) + contentView.scrollY; + let contentSize = this._getContentSize(); + + // Use our pixels efficiently and don't try to cache things outside of content + // boundaries (The left bound can be negative because of RTL). + + let rootScale = self.scale; + let leftBound = self._contentDocumentLeft * rootScale; + let bounds = new Rect(leftBound, 0, contentSize.width, contentSize.height); + let displayport = new Rect(cacheX, cacheY, cacheSize.width, cacheSize.height); + displayport.translateInside(bounds); + + self.messageManager.sendAsyncMessage("Content:SetCacheViewport", { + scrollX: Math.round(scrollX) / rootScale, + scrollY: Math.round(scrollY) / rootScale, + x: Math.round(displayport.x) / rootScale, + y: Math.round(displayport.y) / rootScale, + w: Math.round(displayport.width) / rootScale, + h: Math.round(displayport.height) / rootScale, + scale: rootScale, + id: contentView.id + }); + + this._pixelsPannedSinceRefresh.x = 0; + this._pixelsPannedSinceRefresh.y = 0; + }, + + _updateCSSViewport: function() { + let contentView = this._contentView; + this._sendDisplayportUpdate(contentView.scrollX, + contentView.scrollY); + }, + + /** + * The cache viewport is what parts of content is cached in the parent process for + * fast scrolling. This syncs that up with the current projection viewport. + */ + _updateCacheViewport: function() { + // Do not update scroll values for content. + if (this.isRoot()) + this._sendDisplayportUpdate(-1, -1); + else { + let contentView = this._contentView; + this._sendDisplayportUpdate(contentView.scrollX, + contentView.scrollY); + } + }, + + _getContentSize: function() { + let self = this.self; + return { width: this._contentView.contentWidth, + height: this._contentView.contentHeight }; + }, + + _getViewportSize: function() { + let self = this.self; + if (this.isRoot()) { + let bcr = self.getBoundingClientRect(); + return { width: bcr.width, height: bcr.height }; + } else { + return { width: this._contentView.viewportWidth, + height: this._contentView.viewportHeight }; + } + }, + + init: function(contentView) { + let self = this.self; + + this._contentView = contentView; + this._id = contentView.id; + this._scale = 1; + self._contentViews[this._id] = this; + + if (!this.isRoot()) { + // Non-root content views are short lived. + this._timeout = setTimeout(this._dieIfOld.bind(this), this.kDieTime); + // This iframe may not have a display port yet, so build up a cache + // immediately. + this._updateCacheViewport(); + } + }, + + isRoot: function() { + return this.self._contentViewManager.rootContentView == this._contentView; + }, + + scrollBy: function(x, y) { + let self = this.self; + + // Bounding content rectangle is in device pixels + let contentView = this._contentView; + let viewportSize = this._getViewportSize(); + let contentSize = this._getContentSize(); + // Calculate document dimensions in device pixels + let scrollRangeX = contentSize.width - viewportSize.width; + let scrollRangeY = contentSize.height - viewportSize.height; + + let leftOffset = self._contentDocumentLeft * this._scale; + x = Math.floor(Math.max(leftOffset, Math.min(scrollRangeX + leftOffset, contentView.scrollX + x))) - contentView.scrollX; + y = Math.floor(Math.max(0, Math.min(scrollRangeY, contentView.scrollY + y))) - contentView.scrollY; + + if (x == 0 && y == 0) + return; + + contentView.scrollBy(x, y); + + this._lastPanTime = Date.now(); + + this._pixelsPannedSinceRefresh.x += x; + this._pixelsPannedSinceRefresh.y += y; + if (Math.abs(this._pixelsPannedSinceRefresh.x) > 20 || + Math.abs(this._pixelsPannedSinceRefresh.y) > 20) + this._updateCacheViewport(); + }, + + scrollTo: function(x, y) { + let contentView = this._contentView; + this.scrollBy(x - contentView.scrollX, y - contentView.scrollY); + }, + + _setScale: function _setScale(scale) { + this._scale = scale; + this._contentView.setScale(scale, scale); + }, + + getPosition: function() { + let contentView = this._contentView; + return { x: contentView.scrollX, y: contentView.scrollY }; + } + }) + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/console.xml b/browser/metro/base/content/bindings/console.xml new file mode 100644 index 000000000000..627da33a1ed0 --- /dev/null +++ b/browser/metro/base/content/bindings/console.xml @@ -0,0 +1,61 @@ + + + + + +%browserDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/dialog.xml b/browser/metro/base/content/bindings/dialog.xml new file mode 100644 index 000000000000..dd1152744b52 --- /dev/null +++ b/browser/metro/base/content/bindings/dialog.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/downloads.xml b/browser/metro/base/content/bindings/downloads.xml new file mode 100644 index 000000000000..44ffed6075cb --- /dev/null +++ b/browser/metro/base/content/bindings/downloads.xml @@ -0,0 +1,208 @@ + + + + + +%browserDTD; +]> + + + + + + + + + + + + + + + + Components.interfaces.nsIDownloadManager + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/flyoutpanel.xml b/browser/metro/base/content/bindings/flyoutpanel.xml new file mode 100644 index 000000000000..b6770dda30dc --- /dev/null +++ b/browser/metro/base/content/bindings/flyoutpanel.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false; + + + diff --git a/browser/metro/base/content/bindings/grid.xml b/browser/metro/base/content/bindings/grid.xml new file mode 100644 index 000000000000..89b6a583096b --- /dev/null +++ b/browser/metro/base/content/bindings/grid.xml @@ -0,0 +1,556 @@ + + + + + + + + + + + + + + + + + null + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + null + + + + + + + + + + + null + + + + + + + + + + + + + + + + + + + = 0) { + let selected = this.getItemAtIndex(val); + this.selectItem(selected); + } else { + this.clearSelection(); + } + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + = lowerBound && + this.selectedIndex < higherBound; + ]]> + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 10 + + + + + + this._scheduledArrangeItemsTries) { + // schedule re-try of arrangeItems at the next tick + this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), 0); + return; + } + } + + // items ready to arrange (or retries max exceeded) + // reset the flags + if (this._scheduledArrangeItemsTimerId) { + clearTimeout(this._scheduledArrangeItemsTimerId); + delete this._scheduledArrangeItemsTimerId; + } + if (this._scheduledArrangeItemsTries) { + this._scheduledArrangeItemsTries = 0; + } + + // Autocomplete is a binding within a binding, so we have to step + // up an additional parentNode. + let container = null; + if (this.parentNode.id == "results-vbox" || + this.parentNode.id == "searches-vbox") + container = this.parentNode.parentNode.getBoundingClientRect(); + else + container = this.parentNode.getBoundingClientRect(); + + // If we don't have valid dimensions we can't arrange yet + if (!container.height || !gridItemRect.height) + return; + + // We favor overflowing horizontally, not vertically + let maxRowCount = Math.floor(container.height / gridItemRect.height) - 1; + + if (aNumRows) { + this._rowCount = aNumRows; + } else { + this._rowCount = Math.min(this.itemCount, maxRowCount); + } + if (aNumColumns) { + this._columnCount = aNumColumns; + } else { + this._columnCount = Math.ceil(this.itemCount / this._rowCount); + } + + this._grid.style.width = (this._columnCount * gridItemRect.width) + "px"; + ]]> + + + + + + + + + + + + + + + + + + = 0 && anIndex < this.itemCount; + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + null + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/pageaction.xml b/browser/metro/base/content/bindings/pageaction.xml new file mode 100644 index 000000000000..9492d595d743 --- /dev/null +++ b/browser/metro/base/content/bindings/pageaction.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/selectionoverlay.xml b/browser/metro/base/content/bindings/selectionoverlay.xml new file mode 100644 index 000000000000..0ed93b15b029 --- /dev/null +++ b/browser/metro/base/content/bindings/selectionoverlay.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + document.getAnonymousElementByAttribute(this, "anonid", "selection-overlay-inner").parentNode; + document.getAnonymousElementByAttribute(this, "anonid", "selection-overlay-debug"); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/tabs.xml b/browser/metro/base/content/bindings/tabs.xml new file mode 100644 index 000000000000..d3ad1b113170 --- /dev/null +++ b/browser/metro/base/content/bindings/tabs.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + document.getAnonymousElementByAttribute(this, "anonid", "thumbnail"); + document.getAnonymousElementByAttribute(this, "anonid", "close"); + document.getAnonymousElementByAttribute(this, "anonid", "title"); + document.getAnonymousElementByAttribute(this, "anonid", "favicon"); + this.parentNode; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + document.getAnonymousElementByAttribute(this, "anonid", "tabs-scrollbox"); + null + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bindings/toggleswitch.xml b/browser/metro/base/content/bindings/toggleswitch.xml new file mode 100644 index 000000000000..8dcc83355918 --- /dev/null +++ b/browser/metro/base/content/bindings/toggleswitch.xml @@ -0,0 +1,144 @@ + + + + + + %checkboxDTD; +]> + + + + + + + + + + + + + + + + + + + document.getAnonymousElementByAttribute(this, "anonid", "group"); + + + + document.getAnonymousElementByAttribute(this, "anonid", "on"); + + + + document.getAnonymousElementByAttribute(this, "anonid", "onlabel"); + + + + document.getAnonymousElementByAttribute(this, "anonid", "off"); + + + + document.getAnonymousElementByAttribute(this, "anonid", "offlabel"); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/bookmarks.js b/browser/metro/base/content/bookmarks.js new file mode 100644 index 000000000000..8e5dc4ca7aa5 --- /dev/null +++ b/browser/metro/base/content/bookmarks.js @@ -0,0 +1,364 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Utility singleton for manipulating bookmarks. + */ +var Bookmarks = { + get metroRoot() { + return PlacesUtils.annotations.getItemsWithAnnotation('metro/bookmarksRoot', {})[0]; + }, + + logging: false, + log: function(msg) { + if (this.logging) { + Services.console.logStringMessage(msg); + } + }, + + /* + * fixupColorFormat - convert a decimal color value to a valid + * css color string of the format '#123456' + */ + fixupColorFormat: function bv_fixupColorFormat(aColor) { + let color = aColor.toString(16); + while (color.length < 6) + color = "0" + color; + return "#" + color; + }, + + getFaveIconPrimaryColor: function bh_getFaveIconPrimaryColor(aBookmarkId) { + if (PlacesUtils.annotations.itemHasAnnotation(aBookmarkId, 'metro/faveIconColor')) + return PlacesUtils.annotations.getItemAnnotation(aBookmarkId, 'metro/faveIconColor'); + return ""; + }, + + setFaveIconPrimaryColor: function bh_setFaveIconPrimaryColor(aBookmarkId, aColorStr) { + var anno = [{ name: "metro/faveIconColor", + type: Ci.nsIAnnotationService.TYPE_STRING, + flags: 0, + value: aColorStr, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }]; + PlacesUtils.setAnnotationsForItem(aBookmarkId, anno); + }, + + addForURI: function bh_addForURI(aURI, aTitle, callback) { + this.isURIBookmarked(aURI, function (isBookmarked) { + if (isBookmarked) + return; + + let bookmarkTitle = aTitle || aURI.spec; + let bookmarkService = PlacesUtils.bookmarks; + let bookmarkId = bookmarkService.insertBookmark(Bookmarks.metroRoot, + aURI, + bookmarkService.DEFAULT_INDEX, + bookmarkTitle); + + // XXX Used for browser-chrome tests + let event = document.createEvent("Events"); + event.initEvent("BookmarkCreated", true, false); + window.dispatchEvent(event); + + if (callback) + callback(bookmarkId); + }); + }, + + isURIBookmarked: function bh_isURIBookmarked(aURI, callback) { + if (!callback) + return; + PlacesUtils.asyncGetBookmarkIds(aURI, function(aItemIds) { + callback(aItemIds && aItemIds.length > 0); + }, this); + }, + + removeForURI: function bh_removeForURI(aURI, callback) { + // XXX blargle xpconnect! might not matter, but a method on + // nsINavBookmarksService that takes an array of items to + // delete would be faster. better yet, a method that takes a URI! + PlacesUtils.asyncGetBookmarkIds(aURI, function(aItemIds) { + aItemIds.forEach(PlacesUtils.bookmarks.removeItem); + if (callback) + callback(aURI, aItemIds); + + // XXX Used for browser-chrome tests + let event = document.createEvent("Events"); + event.initEvent("BookmarkRemoved", true, false); + window.dispatchEvent(event); + }); + } +}; + +/** + * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and + * fills it with the user's bookmarks. + * + * @param aSet Control implementing nsIDOMXULSelectControlElement. + * @param {Number} aLimit Maximum number of items to show in the view. + * @param aRoot Bookmark root to show in the view. + */ +function BookmarksView(aSet, aLimit, aRoot) { + this._set = aSet; + this._set.controller = this; + + this._limit = aLimit; + + this._changes = new BookmarkChangeListener(this); + PlacesUtils.bookmarks.addObserver(this._changes, false); + + // This also implicitly calls `getBookmarks` + this.root = aRoot; +} + +BookmarksView.prototype = { + _limit: null, + _set: null, + _changes: null, + _root: null, + _sort: 0, // Natural bookmark order. + + get sort() { + return this._sort; + }, + + set sort(aSort) { + this._sort = aSort; + this.getBookmarks(); + }, + + get root() { + return this._root; + }, + + set root(aRoot) { + this._root = aRoot; + this.getBookmarks(); + }, + + handleItemClick: function bv_handleItemClick(aItem) { + let url = aItem.getAttribute("value"); + BrowserUI.goToURI(url); + }, + + _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) { + return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']"); + }, + + _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) { + return aItem.getAttribute("bookmarkId"); + }, + + _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) { + for (let name in aAttrs) + anItem.setAttribute(name, aAttrs[name]); + }, + + getBookmarks: function bv_getBookmarks() { + let options = gHistSvc.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + options.excludeQueries = true; // Don't include "smart folders" + options.maxResults = this._limit; + options.sortingMode = this._sort; + + let query = gHistSvc.getNewQuery(); + query.setFolders([Bookmarks.metroRoot], 1); + + let result = gHistSvc.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let childCount = rootNode.childCount; + + for (let i = 0; i < childCount; i++) { + let node = rootNode.getChild(i); + + // Ignore folders, separators, undefined item types, etc. + if (node.type != node.RESULT_TYPE_URI && + node.type != node.RESULT_TYPE_VISIT && + node.type != node.RESULT_TYPE_FULL_VISIT) + continue; + + this.addBookmark(node.itemId); + } + + rootNode.containerOpen = false; + }, + + inCurrentView: function bv_inCurrentView(aParentId, aIndex, aItemType) { + if (this._root && aParentId != this._root) + return false; + + if (this._limit && aIndex >= this._limit) + return false; + + if (aItemType != PlacesUtils.bookmarks.TYPE_BOOKMARK) + return false; + + return true; + }, + + clearBookmarks: function bv_clearBookmarks() { + while (this._set.itemCount > 0) + this._set.removeItemAt(0); + }, + + addBookmark: function bv_addBookmark(aBookmarkId) { + let bookmarks = PlacesUtils.bookmarks; + + let index = bookmarks.getItemIndex(aBookmarkId); + let uri = bookmarks.getBookmarkURI(aBookmarkId); + let title = bookmarks.getItemTitle(aBookmarkId) || uri.spec; + let item = this._set.insertItemAt(index, title, uri.spec); + item.setAttribute("bookmarkId", aBookmarkId); + this._updateFavicon(aBookmarkId, item, uri); + }, + + _updateFavicon: function _updateFavicon(aBookmarkId, aItem, aUri) { + PlacesUtils.favicons.getFaviconURLForPage(aUri, this._gotIcon.bind(this, aBookmarkId, aItem)); + }, + + _gotIcon: function _gotIcon(aBookmarkId, aItem, aIconUri) { + aItem.setAttribute("iconURI", aIconUri ? aIconUri.spec : ""); + + let color = Bookmarks.getFaveIconPrimaryColor(aBookmarkId); + if (color) { + aItem.color = color; + return; + } + let url = Services.io.newURI(aIconUri.spec.replace("moz-anno:favicon:",""), "", null) + let ca = Components.classes["@mozilla.org/places/colorAnalyzer;1"] + .getService(Components.interfaces.mozIColorAnalyzer); + ca.findRepresentativeColor(url, function (success, color) { + let colorStr = Bookmarks.fixupColorFormat(color); + Bookmarks.setFaveIconPrimaryColor(aBookmarkId, colorStr); + aItem.color = colorStr; + }, this); + }, + + updateBookmark: function bv_updateBookmark(aBookmarkId) { + let item = this._getItemForBookmarkId(aBookmarkId); + + if (!item) + return; + + let bookmarks = PlacesUtils.bookmarks; + let oldIndex = this._set.getIndexOfItem(item); + let index = bookmarks.getItemIndex(aBookmarkId); + + if (oldIndex != index) { + this.removeBookmark(aBookmarkId); + this.addBookmark(aBookmarkId); + return; + } + + let uri = bookmarks.getBookmarkURI(aBookmarkId); + let title = bookmarks.getItemTitle(aBookmarkId) || uri.spec; + + item.setAttribute("value", uri.spec); + item.setAttribute("label", title); + + this._updateFavicon(aBookmarkId, item, uri); + }, + + removeBookmark: function bv_removeBookmark(aBookmarkId) { + let item = this._getItemForBookmarkId(aBookmarkId); + let index = this._set.getIndexOfItem(item); + this._set.removeItemAt(index); + }, + + destruct: function bv_destruct() { + PlacesUtils.bookmarks.removeObserver(this._changes); + } +}; + +var BookmarksStartView = { + _view: null, + get _grid() { return document.getElementById("start-bookmarks-grid"); }, + + init: function init() { + this._view = new BookmarksView(this._grid, StartUI.maxResultsPerSection, Bookmarks.metroRoot); + }, + + uninit: function uninit() { + this._view.destruct(); + }, + + show: function show() { + this._grid.arrangeItems(); + } +}; + +var BookmarksPanelView = { + _view: null, + + get _grid() { return document.getElementById("bookmarks-list"); }, + get visible() { return PanelUI.isPaneVisible("bookmarks-container"); }, + + init: function init() { + this._view = new BookmarksView(this._grid, null, Bookmarks.metroRoot); + }, + + show: function show() { + this._grid.arrangeItems(); + }, + + uninit: function uninit() { + this._view.destruct(); + } +}; + +/** + * Observes bookmark changes and keeps a linked BookmarksView updated. + * + * @param aView An instance of BookmarksView. + */ +function BookmarkChangeListener(aView) { + this._view = aView; +}; + +BookmarkChangeListener.prototype = { + ////////////////////////////////////////////////////////////////////////////// + //// nsINavBookmarkObserver + onBeginUpdateBatch: function () { }, + onEndUpdateBatch: function () { }, + + onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) { + if (!this._view.inCurrentView(aParentId, aIndex, aItemType)) + return; + + this._view.addBookmark(aItemId); + }, + + onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) { + let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId); + if (!this._view.inCurrentView(aParentId, itemIndex, aItemType)) + return; + + this._view.updateBookmark(aItemId); + }, + + onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) { + let wasInView = this._view.inCurrentView(aOldParentId, aOldIndex, aItemType); + let nowInView = this._view.inCurrentView(aNewParentId, aNewIndex, aItemType); + + if (!wasInView && nowInView) + this._view.addBookmark(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded); + + if (wasInView && !nowInView) + this._view.removeBookmark(aItemId); + }, + + onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { }, + onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) { + if (!this._view.inCurrentView(aParentId, aIndex, aItemType)) + return; + + this._view.removeBookmark(aItemId); + }, + + onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsISupports + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) +}; diff --git a/browser/metro/base/content/browser-scripts.js b/browser/metro/base/content/browser-scripts.js new file mode 100644 index 000000000000..97f9ae2afbc1 --- /dev/null +++ b/browser/metro/base/content/browser-scripts.js @@ -0,0 +1,170 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"); +Cu.import("resource://gre/modules/Services.jsm"); + +/* + * JS modules + */ + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PdfJs", + "resource://pdf.js/PdfJs.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm"); + +/* + * Services + */ + +#ifdef XP_WIN +XPCOMUtils.defineLazyServiceGetter(this, "MetroUtils", + "@mozilla.org/windows-metroutils;1", + "nsIWinMetroUtils"); +#else +// Stub nsIWinMetroUtils implementation for testing on non-Windows platforms: +var MetroUtils = { + snappedState: Ci.nsIWinMetroUtils.fullScreenLandscape, + immersive: false, + handPreference: Ci.nsIWinMetroUtils.handPreferenceLeft, + unsnap: function() {}, + launchInDesktop: function() {}, + pinTileAsync: function() {}, + unpinTileAsync: function() {}, + isTilePinned: function() { return false; }, + keyboardVisible: false, + keyboardX: 0, + keyboardY: 0, + keyboardWidth: 0, + keyboardHeight: 0 +}; +#endif +XPCOMUtils.defineLazyServiceGetter(this, "StyleSheetSvc", + "@mozilla.org/content/style-sheet-service;1", + "nsIStyleSheetService"); +XPCOMUtils.defineLazyServiceGetter(window, "gHistSvc", + "@mozilla.org/browser/nav-history-service;1", + "nsINavHistoryService", + "nsIBrowserHistory"); +XPCOMUtils.defineLazyServiceGetter(window, "gURIFixup", + "@mozilla.org/docshell/urifixup;1", + "nsIURIFixup"); +XPCOMUtils.defineLazyServiceGetter(window, "gFaviconService", + "@mozilla.org/browser/favicon-service;1", + "nsIFaviconService"); +XPCOMUtils.defineLazyServiceGetter(window, "gFocusManager", + "@mozilla.org/focus-manager;1", + "nsIFocusManager"); +#ifdef MOZ_CRASHREPORTER +XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", + "@mozilla.org/xre/app-info;1", + "nsICrashReporter"); +#endif + +/* + * window.Rect is used by + * http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-Rect + * so it is not possible to set a lazy getter for Geometry.jsm. + */ +Cu.import("resource://gre/modules/Geometry.jsm"); + +/* + * Browser scripts + */ +[ + ["WebProgress", "chrome://browser/content/WebProgress.js"], + ["FindHelperUI", "chrome://browser/content/helperui/FindHelperUI.js"], + ["FormHelperUI", "chrome://browser/content/helperui/FormHelperUI.js"], + ["BrowserTouchHandler", "chrome://browser/content/BrowserTouchHandler.js"], + ["AlertsHelper", "chrome://browser/content/helperui/AlertsHelper.js"], + ["CapturePickerUI", "chrome://browser/content/helperui/CapturePickerUI.js"], + ["CharsetMenu", "chrome://browser/content/helperui/CharsetMenu.js"], + ["AutofillMenuUI", "chrome://browser/content/helperui/MenuUI.js"], + ["ContextMenuUI", "chrome://browser/content/helperui/MenuUI.js"], + ["MenuControlUI", "chrome://browser/content/helperui/MenuUI.js"], + ["MenuPopup", "chrome://browser/content/helperui/MenuUI.js"], + ["IdentityUI", "chrome://browser/content/helperui/IdentityUI.js"], + ["IndexedDB", "chrome://browser/content/helperui/IndexedDB.js"], + ["MasterPasswordUI", "chrome://browser/content/helperui/MasterPasswordUI.js"], + ["OfflineApps", "chrome://browser/content/helperui/OfflineApps.js"], + ["SelectHelperUI", "chrome://browser/content/helperui/SelectHelperUI.js"], + ["SelectionHelperUI", "chrome://browser/content/helperui/SelectionHelperUI.js"], + ["SharingUI", "chrome://browser/content/helperui/SharingUI.js"], + ["FullScreenVideo", "chrome://browser/content/video.js"], + ["AnimatedZoom", "chrome://browser/content/AnimatedZoom.js"], + ["CommandUpdater", "chrome://browser/content/commandUtil.js"], + ["ContextCommands", "chrome://browser/content/ContextCommands.js"], + ["Bookmarks", "chrome://browser/content/bookmarks.js"], + ["Downloads", "chrome://browser/content/downloads.js"], + ["BookmarksPanelView", "chrome://browser/content/bookmarks.js"], + ["ConsolePanelView", "chrome://browser/content/console.js"], + ["DownloadsPanelView", "chrome://browser/content/downloads.js"], + ["DownloadsView", "chrome://browser/content/downloads.js"], + ["Downloads", "chrome://browser/content/downloads.js"], + ["PreferencesPanelView", "chrome://browser/content/preferences.js"], + ["BookmarksStartView", "chrome://browser/content/bookmarks.js"], + ["HistoryView", "chrome://browser/content/history.js"], + ["HistoryStartView", "chrome://browser/content/history.js"], + ["HistoryPanelView", "chrome://browser/content/history.js"], + ["TopSitesView", "chrome://browser/content/TopSites.js"], + ["TopSitesSnappedView", "chrome://browser/content/TopSites.js"], + ["TopSitesStartView", "chrome://browser/content/TopSites.js"], + ["InputSourceHelper", "chrome://browser/content/input.js"], + ["PageActions", "chrome://browser/content/PageActions.js"], + ["Sanitizer", "chrome://browser/content/sanitize.js"], + ["SSLExceptions", "chrome://browser/content/exceptions.js"], +#ifdef MOZ_SERVICES_SYNC + ["WeaveGlue", "chrome://browser/content/sync.js"], + ["SyncPairDevice", "chrome://browser/content/sync.js"], + ["RemoteTabsView", "chrome://browser/content/RemoteTabs.js"], + ["RemoteTabsPanelView", "chrome://browser/content/RemoteTabs.js"], + ["RemoteTabsStartView", "chrome://browser/content/RemoteTabs.js"], +#endif +].forEach(function (aScript) { + let [name, script] = aScript; + XPCOMUtils.defineLazyGetter(window, name, function() { + let sandbox = {}; + Services.scriptloader.loadSubScript(script, sandbox); + return sandbox[name]; + }); +}); + +#ifdef MOZ_SERVICES_SYNC +XPCOMUtils.defineLazyGetter(this, "Weave", function() { + Components.utils.import("resource://services-sync/main.js"); + return Weave; +}); +#endif + +/* + * Delay load some global scripts using a custom namespace + */ +XPCOMUtils.defineLazyGetter(this, "GlobalOverlay", function() { + let GlobalOverlay = {}; + Services.scriptloader.loadSubScript("chrome://global/content/globalOverlay.js", GlobalOverlay); + return GlobalOverlay; +}); + +XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() { + let ContentAreaUtils = {}; + Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils); + return ContentAreaUtils; +}); + +XPCOMUtils.defineLazyGetter(this, "ZoomManager", function() { + let sandbox = {}; + Services.scriptloader.loadSubScript("chrome://global/content/viewZoomOverlay.js", sandbox); + return sandbox.ZoomManager; +}); diff --git a/browser/metro/base/content/browser-ui.js b/browser/metro/base/content/browser-ui.js new file mode 100644 index 000000000000..ee47f131ee37 --- /dev/null +++ b/browser/metro/base/content/browser-ui.js @@ -0,0 +1,1638 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Constants + */ + +// BrowserUI.update(state) constants. Currently passed in +// but update doesn't pay attention to them. Can we remove? +const TOOLBARSTATE_LOADING = 1; +const TOOLBARSTATE_LOADED = 2; + +// delay for ContextUI's dismissWithDelay +const kHideContextAndTrayDelayMsec = 3000; + +// delay when showing the tab bar briefly as a new tab opens +const kNewTabAnimationDelayMsec = 500; + + +// Page for which the start UI is shown +const kStartOverlayURI = "about:start"; + +/** + * Cache of commonly used elements. + */ + +let Elements = {}; +[ + ["contentShowing", "bcast_contentShowing"], + ["urlbarState", "bcast_urlbarState"], + ["windowState", "bcast_windowState"], + ["mainKeyset", "mainKeyset"], + ["stack", "stack"], + ["tabList", "tabs"], + ["tabs", "tabs-container"], + ["controls", "browser-controls"], + ["panelUI", "panel-container"], + ["startUI", "start-container"], + ["tray", "tray"], + ["toolbar", "toolbar"], + ["browsers", "browsers"], + ["appbar", "appbar"], + ["contentViewport", "content-viewport"], + ["progress", "progress-control"], + ["contentNavigator", "content-navigator"], + ["aboutFlyout", "about-flyoutpanel"], + ["prefsFlyout", "prefs-flyoutpanel"] +].forEach(function (aElementGlobal) { + let [name, id] = aElementGlobal; + XPCOMUtils.defineLazyGetter(Elements, name, function() { + return document.getElementById(id); + }); +}); + +/** + * Cache of commonly used string bundles. + */ + +var Strings = {}; +[ + ["browser", "chrome://browser/locale/browser.properties"], + ["brand", "chrome://branding/locale/brand.properties"] +].forEach(function (aStringBundle) { + let [name, bundle] = aStringBundle; + XPCOMUtils.defineLazyGetter(Strings, name, function() { + return Services.strings.createBundle(bundle); + }); +}); + +var BrowserUI = { + get _edit() { return document.getElementById("urlbar-edit"); }, + get _back() { return document.getElementById("cmd_back"); }, + get _forward() { return document.getElementById("cmd_forward"); }, + + init: function() { + // listen content messages + messageManager.addMessageListener("DOMTitleChanged", this); + messageManager.addMessageListener("DOMWillOpenModalDialog", this); + messageManager.addMessageListener("DOMWindowClose", this); + + messageManager.addMessageListener("Browser:OpenURI", this); + messageManager.addMessageListener("Browser:SaveAs:Return", this); + + // listening escape to dismiss dialog on VK_ESCAPE + window.addEventListener("keypress", this, true); + + window.addEventListener("MozPrecisePointer", this, true); + window.addEventListener("MozImprecisePointer", this, true); + + Services.prefs.addObserver("browser.tabs.tabsOnly", this, false); + Services.obs.addObserver(this, "metro_viewstate_changed", false); + + // Init core UI modules + ContextUI.init(); + StartUI.init(); + PanelUI.init(); + IdentityUI.init(); + if (Browser.getHomePage() === "about:start") { + StartUI.show(); + } + FlyoutPanelsUI.init(); + + // show the right toolbars, awesomescreen, etc for the os viewstate + BrowserUI._adjustDOMforViewState(); + + // We can delay some initialization until after startup. We wait until + // the first page is shown, then dispatch a UIReadyDelayed event. + messageManager.addMessageListener("pageshow", function() { + if (getBrowser().currentURI.spec == "about:blank") + return; + + messageManager.removeMessageListener("pageshow", arguments.callee, true); + + setTimeout(function() { + let event = document.createEvent("Events"); + event.initEvent("UIReadyDelayed", true, false); + window.dispatchEvent(event); + }, 0); + }); + + // Only load IndexedDB.js when we actually need it. A general fix will happen in bug 647079. + messageManager.addMessageListener("IndexedDB:Prompt", function(aMessage) { + return IndexedDB.receiveMessage(aMessage); + }); + + // Delay the panel UI and Sync initialization + window.addEventListener("UIReadyDelayed", function(aEvent) { + Util.dumpLn("* delay load started..."); + window.removeEventListener(aEvent.type, arguments.callee, false); + + // Login Manager and Form History initialization + Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2); + + messageManager.addMessageListener("Browser:MozApplicationManifest", OfflineApps); + + try { + BrowserUI._updateTabsOnly(); + Downloads.init(); + DialogUI.init(); + FormHelperUI.init(); + FindHelperUI.init(); + FullScreenVideo.init(); + PdfJs.init(); +#ifdef MOZ_SERVICES_SYNC + WeaveGlue.init(); +#endif + } catch(ex) { + Util.dumpLn("Exception in delay load module:", ex.message); + } + + try { + SettingsCharm.init(); + } catch (ex) { + } + + try { + // XXX This is currently failing + CapturePickerUI.init(); + } catch(ex) { + Util.dumpLn("Exception in CapturePickerUI:", ex.message); + } + +#ifdef MOZ_UPDATER + // Check for updates in progress + let updatePrompt = Cc["@mozilla.org/updates/update-prompt;1"].createInstance(Ci.nsIUpdatePrompt); + updatePrompt.checkForUpdates(); +#endif + + // check for left over crash reports and submit them if found. + if (BrowserUI.startupCrashCheck()) { + Browser.selectedTab = BrowserUI.newOrSelectTab("about:crash"); + } + Util.dumpLn("* delay load complete."); + }, false); + +#ifndef MOZ_OFFICIAL_BRANDING + setTimeout(function() { + let startup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup).getStartupInfo(); + for (let name in startup) { + if (name != "process") + Services.console.logStringMessage("[timing] " + name + ": " + (startup[name] - startup.process) + "ms"); + } + }, 3000); +#endif + }, + + uninit: function() { + messageManager.removeMessageListener("Browser:MozApplicationManifest", OfflineApps); + + PanelUI.uninit(); + StartUI.uninit(); + Downloads.uninit(); + SettingsCharm.uninit(); + }, + + + /********************************* + * Content visibility + */ + + get isContentShowing() { + return Elements.contentShowing.getAttribute("disabled") != true; + }, + + showContent: function showContent(aURI) { + DialogUI.closeAllDialogs(); + StartUI.update(aURI); + ContextUI.dismissTabs(); + ContextUI.dismissAppbar(); + FlyoutPanelsUI.hide(); + PanelUI.hide(); + }, + + /********************************* + * Crash reporting + */ + + get CrashSubmit() { + delete this.CrashSubmit; + Cu.import("resource://gre/modules/CrashSubmit.jsm", this); + return this.CrashSubmit; + }, + + startupCrashCheck: function startupCrashCheck() { +#ifdef MOZ_CRASHREPORTER + if (!Services.prefs.getBoolPref("app.reportCrashes")) + return false; + if (CrashReporter.enabled) { + var lastCrashID = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).lastRunCrashID; + if (lastCrashID.length) { + this.CrashSubmit.submit(lastCrashID); + return true; + } + } +#endif + return false; + }, + + + /********************************* + * Navigation + */ + + update: function(aState) { + this._updateToolbar(); + }, + + getDisplayURI: function(browser) { + let uri = browser.currentURI; + try { + uri = gURIFixup.createExposableURI(uri); + } catch (ex) {} + + return uri.spec; + }, + + /* Set the location to the current content */ + updateURI: function(aOptions) { + aOptions = aOptions || {}; + + let uri = this.getDisplayURI(Browser.selectedBrowser); + let cleanURI = Util.isURLEmpty(uri) ? "" : uri; + this._setURI(cleanURI); + + if ("captionOnly" in aOptions && aOptions.captionOnly) + return; + + StartUI.update(uri); + this._updateButtons(); + this._updateToolbar(); + }, + + goToURI: function(aURI) { + aURI = aURI || this._edit.value; + if (!aURI) + return; + + // Make sure we're online before attempting to load + Util.forceOnline(); + + BrowserUI.showContent(aURI); + content.focus(); + this._setURI(aURI); + + let postData = {}; + aURI = Browser.getShortcutOrURI(aURI, postData); + Browser.loadURI(aURI, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, postData: postData }); + + // Delay doing the fixup so the raw URI is passed to loadURIWithFlags + // and the proper third-party fixup can be done + let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + let uri = gURIFixup.createFixupURI(aURI, fixupFlags); + gHistSvc.markPageAsTyped(uri); + + this._titleChanged(Browser.selectedBrowser); + }, + + handleUrlbarEnter: function handleUrlbarEnter(aEvent) { + let url = this._edit.value; + if (aEvent instanceof KeyEvent) + url = this._canonizeURL(url, aEvent); + this.goToURI(url); + }, + + _canonizeURL: function _canonizeURL(aUrl, aTriggeringEvent) { + if (!aUrl) + return ""; + + // 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)) { + let accel = 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) { + // trim leading/trailing spaces (bug 233205) + aUrl = aUrl.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 = aUrl.indexOf("/"); + if (firstSlash >= 0) { + aUrl = aUrl.substring(0, firstSlash) + suffix + aUrl.substring(firstSlash + 1); + } else { + aUrl = aUrl + suffix; + } + aUrl = "http://www." + aUrl; + } + } + return aUrl; + }, + + doOpenSearch: function doOpenSearch(aName) { + // save the current value of the urlbar + let searchValue = this._edit.value; + + // Make sure we're online before attempting to load + Util.forceOnline(); + BrowserUI.showContent(); + + let engine = Services.search.getEngineByName(aName); + let submission = engine.getSubmission(searchValue, null); + Browser.loadURI(submission.uri.spec, { postData: submission.postData }); + + // loadURI may open a new tab, so get the selectedBrowser afterward. + Browser.selectedBrowser.userTypedValue = submission.uri.spec; + this._titleChanged(Browser.selectedBrowser); + }, + + /********************************* + * Tab management + */ + + newTab: function newTab(aURI, aOwner) { + aURI = aURI || kStartOverlayURI; + let tab = Browser.addTab(aURI, true, aOwner); + ContextUI.peekTabs(); + return tab; + }, + + newOrSelectTab: function newOrSelectTab(aURI, aOwner) { + let tabs = Browser.tabs; + for (let i = 0; i < tabs.length; i++) { + if (tabs[i].browser.currentURI.spec == aURI) { + Browser.selectedTab = tabs[i]; + return; + } + } + this.newTab(aURI, aOwner); + }, + + closeTab: function closeTab(aTab) { + // If we only have one tab, open a new one + if (Browser.tabs.length == 1) + Browser.addTab(Browser.getHomePage()); + + // If no tab is passed in, assume the current tab + Browser.closeTab(aTab || Browser.selectedTab); + }, + + /** + * Re-open a closed tab. + * @param aIndex + * The index of the tab (via nsSessionStore.getClosedTabData) + * @returns a reference to the reopened tab. + */ + undoCloseTab: function undoCloseTab(aIndex) { + var tab = null; + aIndex = aIndex || 0; + var ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + if (ss.getClosedTabCount(window) > (aIndex)) { + tab = ss.undoCloseTab(window, aIndex); + } + return tab; + }, + + // Useful for when we've received an event to close a particular DOM window. + // Since we don't have windows, we want to close the corresponding tab. + closeTabForBrowser: function closeTabForBrowser(aBrowser) { + // Find the relevant tab, and close it. + let browsers = Browser.browsers; + for (let i = 0; i < browsers.length; i++) { + if (browsers[i] == aBrowser) { + Browser.closeTab(Browser.getTabAtIndex(i)); + return { preventDefault: true }; + } + } + + return {}; + }, + + selectTab: function selectTab(aTab) { + Browser.selectedTab = aTab; + }, + + selectTabAndDismiss: function selectTabAndDismiss(aTab) { + this.selectTab(aTab); + ContextUI.dismiss(); + }, + + selectTabAtIndex: function selectTabAtIndex(aIndex) { + // count backwards for aIndex < 0 + if (aIndex < 0) + aIndex += Browser._tabs.length; + + if (aIndex >= 0 && aIndex < Browser._tabs.length) + Browser.selectedTab = Browser._tabs[aIndex]; + }, + + selectNextTab: function selectNextTab() { + if (Browser._tabs.length == 1 || !Browser.selectedTab) { + return; + } + + let tabIndex = Browser._tabs.indexOf(Browser.selectedTab) + 1; + if (tabIndex >= Browser._tabs.length) { + tabIndex = 0; + } + + Browser.selectedTab = Browser._tabs[tabIndex]; + }, + + selectPreviousTab: function selectPreviousTab() { + if (Browser._tabs.length == 1 || !Browser.selectedTab) { + return; + } + + let tabIndex = Browser._tabs.indexOf(Browser.selectedTab) - 1; + if (tabIndex < 0) { + tabIndex = Browser._tabs.length - 1; + } + + Browser.selectedTab = Browser._tabs[tabIndex]; + }, + + // Used for when we're about to open a modal dialog, + // and want to ensure the opening tab is in front. + selectTabForBrowser: function selectTabForBrowser(aBrowser) { + for (let i = 0; i < Browser.tabs.length; i++) { + if (Browser._tabs[i].browser == aBrowser) { + Browser.selectedTab = Browser.tabs[i]; + break; + } + } + }, + + updateUIFocus: function _updateUIFocus() { + if (Elements.contentShowing.getAttribute("disabled") == "true" && Browser.selectedBrowser) + Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:Blur", { }); + }, + + blurFocusedElement: function blurFocusedElement() { + let focusedElement = document.commandDispatcher.focusedElement; + if (focusedElement) + focusedElement.blur(); + }, + + // If the user types in the address bar, cancel pending + // navbar autohide if set. + navEditKeyPress: function navEditKeyPress() { + ContextUI.cancelDismiss(); + }, + + + /********************************* + * Conventional tabs + */ + + // Tabsonly displays the url bar with conventional tabs. Also + // the tray does not auto hide. + get isTabsOnly() { + return Services.prefs.getBoolPref("browser.tabs.tabsOnly"); + }, + + _updateTabsOnly: function _updateTabsOnly() { + if (this.isTabsOnly) { + Elements.windowState.setAttribute("tabsonly", "true"); + } else { + Elements.windowState.removeAttribute("tabsonly"); + } + }, + + observe: function BrowserUI_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "nsPref:changed": + if (aData == "browser.tabs.tabsOnly") + this._updateTabsOnly(); + break; + case "metro_viewstate_changed": + this._adjustDOMforViewState(); + break; + } + }, + + /********************************* + * Internal utils + */ + + _adjustDOMforViewState: function() { + if (MetroUtils.immersive) { + let currViewState = ""; + switch (MetroUtils.snappedState) { + case Ci.nsIWinMetroUtils.fullScreenLandscape: + currViewState = "landscape"; + break; + case Ci.nsIWinMetroUtils.fullScreenPortrait: + currViewState = "portrait"; + break; + case Ci.nsIWinMetroUtils.filled: + currViewState = "filled"; + break; + case Ci.nsIWinMetroUtils.snapped: + currViewState = "snapped"; + break; + } + Elements.windowState.setAttribute("viewstate", currViewState); + } + // content navigator helper + document.getElementById("content-navigator").contentHasChanged(); + }, + + _titleChanged: function(aBrowser) { + let url = this.getDisplayURI(aBrowser); + + let tabCaption; + if (aBrowser.contentTitle) { + tabCaption = aBrowser.contentTitle; + } else if (!Util.isURLEmpty(aBrowser.userTypedValue)) { + tabCaption = aBrowser.userTypedValue; + } else if (!Util.isURLEmpty(url)) { + tabCaption = url; + } else { + tabCaption = Strings.browser.GetStringFromName("tabs.emptyTabTitle"); + } + + let tab = Browser.getTabForBrowser(aBrowser); + if (tab) + tab.chromeTab.updateTitle(tabCaption); + }, + + _updateButtons: function _updateButtons() { + let browser = Browser.selectedBrowser; + this._back.setAttribute("disabled", !browser.canGoBack); + this._forward.setAttribute("disabled", !browser.canGoForward); + }, + + _updateToolbar: function _updateToolbar() { + let mode = Elements.urlbarState.getAttribute("mode"); + let isLoading = Browser.selectedTab.isLoading(); + + if (isLoading && mode != "loading") + Elements.urlbarState.setAttribute("mode", "loading"); + else if (!isLoading && mode != "edit") + Elements.urlbarState.setAttribute("mode", "view"); + }, + + _setURI: function _setURI(aURL) { + this._edit.value = aURL; + }, + + _urlbarClicked: function _urlbarClicked() { + // If the urlbar is not already focused, focus it and select the contents. + if (Elements.urlbarState.getAttribute("mode") != "edit") + this._editURI(); + }, + + _editURI: function _editURI() { + this._edit.focus(); + this._edit.select(); + + Elements.urlbarState.setAttribute("mode", "edit"); + StartUI.show(); + ContextUI.dismissTabs(); + }, + + _urlbarBlurred: function _urlbarBlurred() { + let state = Elements.urlbarState; + if (state.getAttribute("mode") == "edit") + state.removeAttribute("mode"); + this._updateToolbar(); + }, + + _closeOrQuit: function _closeOrQuit() { + // Close active dialog, if we have one. If not then close the application. + if (!BrowserUI.isContentShowing()) { + BrowserUI.showContent(); + } else { + // Check to see if we should really close the window + if (Browser.closing()) { + window.close(); + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); + } + } + }, + + _onPreciseInput: function _onPreciseInput() { + document.getElementById("bcast_preciseInput").setAttribute("input", "precise"); + let uri = Util.makeURI("chrome://browser/content/cursor.css"); + if (StyleSheetSvc.sheetRegistered(uri, Ci.nsIStyleSheetService.AGENT_SHEET)) { + StyleSheetSvc.unregisterSheet(uri, + Ci.nsIStyleSheetService.AGENT_SHEET); + } + }, + + _onImpreciseInput: function _onImpreciseInput() { + document.getElementById("bcast_preciseInput").setAttribute("input", "imprecise"); + let uri = Util.makeURI("chrome://browser/content/cursor.css"); + if (!StyleSheetSvc.sheetRegistered(uri, Ci.nsIStyleSheetService.AGENT_SHEET)) { + StyleSheetSvc.loadAndRegisterSheet(uri, + Ci.nsIStyleSheetService.AGENT_SHEET); + } + }, + + /********************************* + * Event handling + */ + + handleEvent: function handleEvent(aEvent) { + var target = aEvent.target; + switch (aEvent.type) { + // Window events + case "keypress": + if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) + this.handleEscape(aEvent); + break; + case "MozPrecisePointer": + this._onPreciseInput(); + break; + case "MozImprecisePointer": + this._onImpreciseInput(); + break; + } + }, + + // Checks if various different parts of the UI is visible and closes + // them one at a time. + handleEscape: function (aEvent) { + aEvent.stopPropagation(); + aEvent.preventDefault(); + + if (this._edit.popupOpen) { + this._edit.closePopup(); + StartUI.hide(); + ContextUI.dismiss(); + return; + } + + // Check open popups + if (DialogUI._popup) { + DialogUI._hidePopup(); + return; + } + + // Check open dialogs + let dialog = DialogUI.activeDialog; + if (dialog) { + dialog.close(); + return; + } + + // Check open modal elements + if (DialogUI.modals.length > 0) + return; + + // Check open panel + if (PanelUI.isVisible) { + PanelUI.hide(); + return; + } + + // Check content helper + let contentHelper = document.getElementById("content-navigator"); + if (contentHelper.isActive) { + contentHelper.model.hide(); + return; + } + + if (StartUI.hide()) { + // When escaping from the start screen, hide the toolbar too. + ContextUI.dismiss(); + return; + } + + if (ContextUI.dismiss()) { + return; + } + + if (Browser.selectedTab.isLoading()) { + Browser.selectedBrowser.stop(); + return; + } + }, + + handleBackspace: function handleBackspace() { + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + CommandUpdater.doCommand("cmd_back"); + break; + case 1: + CommandUpdater.doCommand("cmd_scrollPageUp"); + break; + } + }, + + handleShiftBackspace: function handleShiftBackspace() { + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + CommandUpdater.doCommand("cmd_forward"); + break; + case 1: + CommandUpdater.doCommand("cmd_scrollPageDown"); + break; + } + }, + + receiveMessage: function receiveMessage(aMessage) { + let browser = aMessage.target; + let json = aMessage.json; + switch (aMessage.name) { + case "DOMTitleChanged": + this._titleChanged(browser); + break; + case "DOMWillOpenModalDialog": + this.selectTabForBrowser(browser); + break; + case "DOMWindowClose": + return this.closeTabForBrowser(browser); + break; + // XXX this and content's sender are a little warped + case "Browser:OpenURI": + let referrerURI = null; + if (json.referrer) + referrerURI = Services.io.newURI(json.referrer, null, null); + //Browser.addTab(json.uri, json.bringFront, Browser.selectedTab, { referrerURI: referrerURI }); + this.goToURI(json.uri); + break; + } + + return {}; + }, + + supportsCommand : function(cmd) { + var isSupported = false; + switch (cmd) { + case "cmd_back": + case "cmd_forward": + case "cmd_reload": + case "cmd_forceReload": + case "cmd_stop": + case "cmd_go": + case "cmd_home": + case "cmd_openLocation": + case "cmd_addBookmark": + case "cmd_bookmarks": + case "cmd_history": + case "cmd_remoteTabs": + case "cmd_quit": + case "cmd_close": + case "cmd_newTab": + case "cmd_closeTab": + case "cmd_undoCloseTab": + case "cmd_actions": + case "cmd_panel": + case "cmd_flyout_back": + case "cmd_sanitize": + case "cmd_zoomin": + case "cmd_zoomout": + case "cmd_volumeLeft": + case "cmd_volumeRight": + isSupported = true; + break; + default: + isSupported = false; + break; + } + return isSupported; + }, + + isCommandEnabled : function(cmd) { + let elem = document.getElementById(cmd); + if (elem && elem.getAttribute("disabled") == "true") + return false; + return true; + }, + + doCommand : function(cmd) { + if (!this.isCommandEnabled(cmd)) + return; + let browser = getBrowser(); + switch (cmd) { + case "cmd_back": + browser.goBack(); + break; + case "cmd_forward": + browser.goForward(); + break; + case "cmd_reload": + browser.reload(); + break; + case "cmd_forceReload": + { + // Simulate a new page + browser.lastLocation = null; + + const reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + browser.reloadWithFlags(reloadFlags); + break; + } + case "cmd_stop": + browser.stop(); + break; + case "cmd_go": + this.goToURI(); + break; + case "cmd_home": + this.goToURI(Browser.getHomePage()); + break; + case "cmd_openLocation": + ContextUI.displayNavbar(); + this._editURI(); + break; + case "cmd_addBookmark": + Elements.appbar.show(); + Appbar.onStarButton(true); + break; + case "cmd_bookmarks": + PanelUI.show("bookmarks-container"); + break; + case "cmd_history": + PanelUI.show("history-container"); + break; + case "cmd_remoteTabs": + if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED) { + WeaveGlue.open(); + } else { + PanelUI.show("remotetabs-container"); + } + break; + case "cmd_quit": + // Only close one window + this._closeOrQuit(); + break; + case "cmd_close": + this._closeOrQuit(); + break; + case "cmd_newTab": + this.newTab(); + this._editURI(); + break; + case "cmd_closeTab": + this.closeTab(); + break; + case "cmd_undoCloseTab": + this.undoCloseTab(); + break; + case "cmd_sanitize": + { + let title = Strings.browser.GetStringFromName("clearPrivateData.title"); + let message = Strings.browser.GetStringFromName("clearPrivateData.message"); + let clear = Services.prompt.confirm(window, title, message); + if (clear) { + // disable the button temporarily to indicate something happened + let button = document.getElementById("prefs-clear-data"); + button.disabled = true; + setTimeout(function() { button.disabled = false; }, 5000); + + Sanitizer.sanitize(); + } + break; + } + case "cmd_flyout_back": + FlyoutPanelsUI.hide(); + MetroUtils.showSettingsFlyout(); + break; + case "cmd_panel": + PanelUI.toggle(); + break; + case "cmd_zoomin": + Browser.zoom(-1); + break; + case "cmd_zoomout": + Browser.zoom(1); + break; + case "cmd_volumeLeft": + // Zoom in (portrait) or out (landscape) + Browser.zoom(Util.isPortrait() ? -1 : 1); + break; + case "cmd_volumeRight": + // Zoom out (portrait) or in (landscape) + Browser.zoom(Util.isPortrait() ? 1 : -1); + break; + } + } +}; + +/** + * Tracks whether context UI (app bar, tab bar, url bar) is shown or hidden. + * Manages events to summon and hide the context UI. + */ +var ContextUI = { + _expandable: true, + _hidingId: 0, + + /******************************************* + * init + */ + + init: function init() { + Elements.browsers.addEventListener("mousedown", this, true); + Elements.browsers.addEventListener("touchstart", this, true); + window.addEventListener("MozEdgeUIGesture", this, true); + window.addEventListener("keypress", this, true); + window.addEventListener("KeyboardChanged", this, false); + + Elements.tray.addEventListener("transitionend", this, true); + + Appbar.init(); + }, + + /******************************************* + * Context UI state getters & setters + */ + + get isVisible() { return Elements.tray.hasAttribute("visible"); }, + get isExpanded() { return Elements.tray.hasAttribute("expanded"); }, + get isExpandable() { return this._expandable; }, + + set isExpandable(aFlag) { + this._expandable = aFlag; + if (!this._expandable) + this.dismiss(); + }, + + /******************************************* + * Context UI state control + */ + + toggle: function toggle() { + if (!this._expandable) { + // exandable setter takes care of resetting state + // so if we're not expandable, there's nothing to do here + return; + } + // if we're not showing, show + if (!this.dismiss()) { + dump("* ContextUI is hidden, show it\n"); + this.show(); + } + }, + + // show all context UI + // returns true if any non-visible UI was shown + show: function() { + let shown = false; + if (!this.isExpanded) { + // show the tab tray + this._setIsExpanded(true); + shown = true; + } + if (!this.isVisible) { + // show the navbar + this._setIsVisible(true); + shown = true; + } + if (!Elements.appbar.isShowing) { + // show the appbar + Elements.appbar.show(); + shown = true; + } + + this._clearDelayedTimeout(); + if (shown) { + ContentAreaObserver.update(window.innerWidth, window.innerHeight); + } + return shown; + }, + + // Display the nav bar + displayNavbar: function displayNavbar() { + this._clearDelayedTimeout(); + this._setIsVisible(true, true); + }, + + // Display the toolbar and tabs + displayTabs: function displayTabs() { + this._clearDelayedTimeout(); + this._setIsVisible(true, true); + this._setIsExpanded(true, true); + }, + + /** Briefly show the tab bar and then hide it */ + peekTabs: function peekTabs() { + if (this.isExpanded) + return; + + Elements.tabs.addEventListener("animationend", function onAnimationEnd() { + Elements.tabs.removeEventListener("animationend", onAnimationEnd); + ContextUI.dismissWithDelay(kNewTabAnimationDelayMsec); + }); + this.displayTabs(); + }, + + // Dismiss all context UI. + // Returns true if any visible UI was dismissed. + dismiss: function dismiss() { + let dismissed = false; + if (this.isExpanded) { + this._setIsExpanded(false); + dismissed = true; + } + if (this.isVisible && !StartUI.isStartURI()) { + this._setIsVisible(false); + dismissed = true; + } + if (Elements.appbar.isShowing) { + this.dismissAppbar(); + dismissed = true; + } + this._clearDelayedTimeout(); + if (dismissed) { + ContentAreaObserver.update(window.innerWidth, window.innerHeight); + } + return dismissed; + }, + + // Dismiss all context ui after a delay + dismissWithDelay: function dismissWithDelay(aDelay) { + aDelay = aDelay || kHideContextAndTrayDelayMsec; + this._clearDelayedTimeout(); + this._hidingId = setTimeout(function () { + ContextUI.dismiss(); + }, aDelay); + }, + + // Cancel any pending delayed dismiss + cancelDismiss: function cancelDismiss() { + this._clearDelayedTimeout(); + }, + + dismissTabs: function dimissTabs() { + this._clearDelayedTimeout(); + this._setIsExpanded(false, true); + }, + + dismissAppbar: function dismissAppbar() { + this._fire("MozAppbarDismiss"); + }, + + /******************************************* + * Internal tray state setters + */ + + // url bar state + _setIsVisible: function _setIsVisible(aFlag, setSilently) { + if (this.isVisible == aFlag) + return; + + if (aFlag) + Elements.tray.setAttribute("visible", "true"); + else + Elements.tray.removeAttribute("visible"); + + if (!aFlag) { + content.focus(); + } + + if (!setSilently) + this._fire(aFlag ? "MozContextUIShow" : "MozContextUIDismiss"); + }, + + // tab tray state + _setIsExpanded: function _setIsExpanded(aFlag, setSilently) { + // if the tray can't be expanded because we're in + // tabsonly mode, don't expand it. + if (!this.isExpandable || this.isExpanded == aFlag) + return; + + if (aFlag) + Elements.tray.setAttribute("expanded", "true"); + else + Elements.tray.removeAttribute("expanded"); + + if (!setSilently) + this._fire("MozContextUIExpand"); + }, + + /******************************************* + * Internal utils + */ + + _clearDelayedTimeout: function _clearDelayedTimeout() { + if (this._hidingId) { + clearTimeout(this._hidingId); + this._hidingId = 0; + } + }, + + /******************************************* + * Events + */ + + _onEdgeUIEvent: function _onEdgeUIEvent(aEvent) { + this._clearDelayedTimeout(); + if (StartUI.hide()) { + this.dismiss(); + return; + } + this.toggle(); + }, + + handleEvent: function handleEvent(aEvent) { + switch (aEvent.type) { + case "MozEdgeUIGesture": + this._onEdgeUIEvent(aEvent); + break; + case "mousedown": + if (aEvent.button == 0 && this.isVisible) + this.dismiss(); + break; + case "touchstart": + this.dismiss(); + break; + case "keypress": + if (String.fromCharCode(aEvent.which) == "z" && + aEvent.getModifierState("Win")) + this.toggle(); + break; + case "transitionend": + setTimeout(function () { + ContentAreaObserver.updateContentArea(); + }, 0); + break; + case "KeyboardChanged": + this.dismissTabs(); + break; + } + }, + + _fire: function (name) { + let event = document.createEvent("Events"); + event.initEvent(name, true, true); + window.dispatchEvent(event); + } +}; + +var StartUI = { + get isVisible() { return this.isStartPageVisible || this.isFiltering; }, + get isStartPageVisible() { return Elements.windowState.hasAttribute("startpage"); }, + get isFiltering() { return Elements.windowState.hasAttribute("filtering"); }, + + get maxResultsPerSection() { + return Services.prefs.getIntPref("browser.display.startUI.maxresults"); + }, + + sections: [ + "TopSitesStartView", + "BookmarksStartView", + "HistoryStartView", + "RemoteTabsStartView" + ], + + init: function init() { + Elements.startUI.addEventListener("autocompletestart", this, false); + Elements.startUI.addEventListener("autocompleteend", this, false); + Elements.startUI.addEventListener("contextmenu", this, false); + + this.sections.forEach(function (sectionName) { + let section = window[sectionName]; + if (section.init) + section.init(); + }); + }, + + uninit: function() { + this.sections.forEach(function (sectionName) { + let section = window[sectionName]; + if (section.uninit) + section.uninit(); + }); + }, + + /** Show the Firefox start page / "new tab" page */ + show: function show() { + if (this.isStartPageVisible) + return false; + + ContextUI.displayNavbar(); + + Elements.contentShowing.setAttribute("disabled", "true"); + Elements.windowState.setAttribute("startpage", "true"); + + this.sections.forEach(function (sectionName) { + let section = window[sectionName]; + if (section.show) + section.show(); + }); + return true; + }, + + /** Show the autocomplete popup */ + filter: function filter() { + if (this.isFiltering) + return; + + BrowserUI._edit.openPopup(); + Elements.windowState.setAttribute("filtering", "true"); + }, + + /** Hide the autocomplete popup */ + unfilter: function unfilter() { + if (!this.isFiltering) + return; + + BrowserUI._edit.closePopup(); + Elements.windowState.removeAttribute("filtering"); + }, + + /** Hide the Firefox start page */ + hide: function hide(aURI) { + aURI = aURI || Browser.selectedBrowser.currentURI.spec; + if (!this.isStartPageVisible || this.isStartURI(aURI)) + return false; + + Elements.contentShowing.removeAttribute("disabled"); + Elements.windowState.removeAttribute("startpage"); + + this.unfilter(); + return true; + }, + + /** Is the current tab supposed to show the Firefox start page? */ + isStartURI: function isStartURI(aURI) { + aURI = aURI || Browser.selectedBrowser.currentURI.spec; + return aURI == kStartOverlayURI || aURI == "about:home"; + }, + + /** Call this to show or hide the start page when switching tabs or pages */ + update: function update(aURI) { + aURI = aURI || Browser.selectedBrowser.currentURI.spec; + if (this.isStartURI(aURI)) { + this.show(); + } else if (aURI != "about:blank") { // about:blank is loaded briefly for new tabs; ignore it + this.hide(aURI); + } + }, + + handleEvent: function handleEvent(aEvent) { + switch (aEvent.type) { + case "autocompletestart": + this.filter(); + break; + case "autocompleteend": + this.unfilter(); + break; + case "contextmenu": + let event = document.createEvent("Events"); + event.initEvent("MozEdgeUIGesture", true, false); + window.dispatchEvent(event); + break; + } + } +}; + +var FlyoutPanelsUI = { + get _aboutVersionLabel() { + return document.getElementById('about-version-label'); + }, + + _initAboutPanel: function() { + // Include the build ID if this is an "a#" (nightly or aurora) build + let version = Services.appinfo.version; + if (/a\d+$/.test(version)) { + let buildID = Services.appinfo.appBuildID; + let buildDate = buildID.slice(0,4) + "-" + buildID.slice(4,6) + + "-" + buildID.slice(6,8); + this._aboutVersionLabel.textContent +=" (" + buildDate + ")"; + } + }, + + init: function() { + this._initAboutPanel(); + PreferencesPanelView.init(); + }, + + hide: function() { + Elements.aboutFlyout.hide(); + Elements.prefsFlyout.hide(); + } +}; + +var PanelUI = { + get _panels() { return document.getElementById("panel-items"); }, + get _switcher() { return document.getElementById("panel-view-switcher"); }, + + get isVisible() { + return !Elements.panelUI.hidden; + }, + + views: { + "bookmarks-container": "BookmarksPanelView", + "downloads-container": "DownloadsPanelView", + "console-container": "ConsolePanelView", + "remotetabs-container": "RemoteTabsPanelView", + "history-container" : "HistoryPanelView" + }, + + init: function() { + // Perform core init soon + setTimeout(function () { + for each (let viewName in this.views) { + let view = window[viewName]; + if (view.init) + view.init(); + } + }.bind(this), 0); + + // Lazily run other initialization tasks when the views are shown + this._panels.addEventListener("ToolPanelShown", function(aEvent) { + let viewName = this.views[this._panels.selectedPanel.id]; + let view = window[viewName]; + if (view.show) + view.show(); + }.bind(this), true); + }, + + uninit: function() { + for each (let viewName in this.views) { + let view = window[viewName]; + if (view.uninit) + view.uninit(); + } + }, + + switchPane: function switchPane(aPanelId) { + BrowserUI.blurFocusedElement(); + + let panel = aPanelId ? document.getElementById(aPanelId) : this._panels.selectedPanel; + let oldPanel = this._panels.selectedPanel; + + if (oldPanel != panel) { + this._panels.selectedPanel = panel; + this._switcher.value = panel.id; + + this._fire("ToolPanelHidden", oldPanel); + } + + this._fire("ToolPanelShown", panel); + }, + + isPaneVisible: function isPaneVisible(aPanelId) { + return this.isVisible && this._panels.selectedPanel.id == aPanelId; + }, + + show: function show(aPanelId) { + Elements.panelUI.hidden = false; + Elements.contentShowing.setAttribute("disabled", "true"); + + this.switchPane(aPanelId); + }, + + hide: function hide() { + if (!this.isVisible) + return; + + Elements.panelUI.hidden = true; + Elements.contentShowing.removeAttribute("disabled"); + BrowserUI.blurFocusedElement(); + + this._fire("ToolPanelHidden", this._panels); + }, + + toggle: function toggle() { + if (this.isVisible) { + this.hide(); + } else { + this.show(); + } + }, + + _fire: function _fire(aName, anElement) { + let event = document.createEvent("Events"); + event.initEvent(aName, true, true); + anElement.dispatchEvent(event); + } +}; + +var DialogUI = { + _dialogs: [], + _popup: null, + + init: function() { + window.addEventListener("mousedown", this, true); + }, + + /******************************************* + * Modal popups + */ + + get modals() { + return document.getElementsByClassName("modal-block"); + }, + + importModal: function importModal(aParent, aSrc, aArguments) { + // load the dialog with a synchronous XHR + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); + xhr.open("GET", aSrc, false); + xhr.overrideMimeType("text/xml"); + xhr.send(null); + if (!xhr.responseXML) + return null; + + let currentNode; + let nodeIterator = xhr.responseXML.createNodeIterator(xhr.responseXML, NodeFilter.SHOW_TEXT, null, false); + while (currentNode = nodeIterator.nextNode()) { + let trimmed = currentNode.nodeValue.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); + if (!trimmed.length) + currentNode.parentNode.removeChild(currentNode); + } + + let doc = xhr.responseXML.documentElement; + + let dialog = null; + + // we need to insert before context-container if we want allow pasting (using + // the context menu) into dialogs + let contentMenuContainer = document.getElementById("context-container"); + let parentNode = contentMenuContainer.parentNode; + + // emit DOMWillOpenModalDialog event + let event = document.createEvent("Events"); + event.initEvent("DOMWillOpenModalDialog", true, false); + let dispatcher = aParent || getBrowser(); + dispatcher.dispatchEvent(event); + + // create a full-screen semi-opaque box as a background + let back = document.createElement("box"); + back.setAttribute("class", "modal-block"); + dialog = back.appendChild(document.importNode(doc, true)); + parentNode.insertBefore(back, contentMenuContainer); + + dialog.arguments = aArguments; + dialog.parent = aParent; + return dialog; + }, + + /******************************************* + * Dialogs + */ + + get activeDialog() { + // Return the topmost dialog + if (this._dialogs.length) + return this._dialogs[this._dialogs.length - 1]; + return null; + }, + + closeAllDialogs: function closeAllDialogs() { + while (this.activeDialog) + this.activeDialog.close(); + }, + + pushDialog: function pushDialog(aDialog) { + // If we have a dialog push it on the stack and set the attr for CSS + if (aDialog) { + this._dialogs.push(aDialog); + Elements.contentShowing.setAttribute("disabled", "true"); + } + }, + + popDialog: function popDialog() { + if (this._dialogs.length) + this._dialogs.pop(); + + // If no more dialogs are being displayed, remove the attr for CSS + if (!this._dialogs.length) + Elements.contentShowing.removeAttribute("disabled"); + }, + + /******************************************* + * Popups + */ + + pushPopup: function pushPopup(aPanel, aElements, aParent) { + this._hidePopup(); + this._popup = { "panel": aPanel, + "elements": (aElements instanceof Array) ? aElements : [aElements] }; + this._dispatchPopupChanged(true); + }, + + popPopup: function popPopup(aPanel) { + if (!this._popup || aPanel != this._popup.panel) + return; + this._popup = null; + this._dispatchPopupChanged(false); + }, + + _hidePopup: function _hidePopup() { + if (!this._popup) + return; + let panel = this._popup.panel; + if (panel.hide) + panel.hide(); + }, + + /******************************************* + * Events + */ + + handleEvent: function (aEvent) { + switch (aEvent.type) { + case "mousedown": + if (!this._isEventInsidePopup(aEvent)) + this._hidePopup(); + break; + default: + break; + } + }, + + _dispatchPopupChanged: function _dispatchPopupChanged(aVisible) { + let event = document.createEvent("UIEvents"); + event.initUIEvent("PopupChanged", true, true, window, aVisible); + event.popup = this._popup; + Elements.stack.dispatchEvent(event); + }, + + _isEventInsidePopup: function _isEventInsidePopup(aEvent) { + if (!this._popup) + return false; + let elements = this._popup.elements; + let targetNode = aEvent.target; + while (targetNode && elements.indexOf(targetNode) == -1) { + if (targetNode instanceof Element && targetNode.hasAttribute("for")) + targetNode = document.getElementById(targetNode.getAttribute("for")); + else + targetNode = targetNode.parentNode; + } + return targetNode ? true : false; + } +}; + +/** + * Manage the contents of the Windows 8 "Settings" charm. + */ +var SettingsCharm = { + _entries: new Map(), + _nextId: 0, + + /** + * Add a new item to the "Settings" menu in the Windows 8 charms. + * @param aEntry Object with a "label" property (string that will appear in the UI) + * and an "onselected" property (function to be called when the user chooses this entry) + */ + addEntry: function addEntry(aEntry) { + let id = MetroUtils.addSettingsPanelEntry(aEntry.label); + this._entries.set(id, aEntry); + }, + + init: function SettingsCharm_init() { + Services.obs.addObserver(this, "metro-settings-entry-selected", false); + + // Options + this.addEntry({ + label: Strings.browser.GetStringFromName("optionsCharm"), + onselected: function() Elements.prefsFlyout.show() + }); + // About + this.addEntry({ + label: Strings.browser.GetStringFromName("aboutCharm1"), + onselected: function() Elements.aboutFlyout.show() + }); + // Help + this.addEntry({ + label: Strings.browser.GetStringFromName("helpOnlineCharm"), + onselected: function() { + let url = Services.urlFormatter.formatURLPref("app.support.baseURL"); + BrowserUI.newTab(url, Browser.selectedTab); + } + }); + }, + + observe: function SettingsCharm_observe(aSubject, aTopic, aData) { + if (aTopic == "metro-settings-entry-selected") { + let entry = this._entries.get(parseInt(aData, 10)); + if (entry) + entry.onselected(); + } + }, + + uninit: function SettingsCharm_uninit() { + Services.obs.removeObserver(this, "metro-settings-entry-selected"); + } +}; diff --git a/browser/metro/base/content/browser.css b/browser/metro/base/content/browser.css new file mode 100644 index 000000000000..7632141cb3a6 --- /dev/null +++ b/browser/metro/base/content/browser.css @@ -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/. */ + +browser[remote="false"] { + -moz-binding: url("chrome://browser/content/bindings/browser.xml#local-browser"); +} + +browser[remote="true"] { + -moz-binding: url("chrome://browser/content/bindings/browser.xml#remote-browser"); +} + +#content-navigator { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#content-navigator"); +} + +#tabs { + -moz-binding: url("chrome://browser/content/bindings/tabs.xml#tablist"); +} + +documenttab { + -moz-binding: url("chrome://browser/content/bindings/tabs.xml#documenttab"); +} + +appbar { + -moz-binding: url('chrome://browser/content/bindings/appbar.xml#appbarBinding'); +} + +flyoutpanel { + -moz-binding: url('chrome://browser/content/bindings/flyoutpanel.xml#flyoutpanelBinding'); +} + +settings { + -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#settings"); +} + +setting { + display: none; +} + +setting[type="bool"] { + display: -moz-box; + -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-bool"); +} + +setting[type="bool"][localized="true"] { + display: -moz-box; + -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-localized-bool"); +} + +setting[type="boolint"] { + display: -moz-box; + -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-boolint"); +} + +setting[type="integer"] { + display: -moz-box; + -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-integer"); +} + +setting[type="control"] { + display: -moz-box; + -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-control"); +} + +setting[type="string"] { + display: -moz-box; + -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-string"); +} + +setting[type="color"] { + display: -moz-box; + -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-color"); +} + +setting[type="file"], +setting[type="directory"] { + display: -moz-box; + -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-path"); +} + +setting[type="radio"], +setting[type="menulist"] { + display: -moz-box; + -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-multi"); +} + +#selection-overlay { + -moz-binding: url("chrome://browser/content/bindings/selectionoverlay.xml#selection-binding"); +} + +#urlbar-edit { + -moz-binding: url("chrome://browser/content/bindings/autocomplete.xml#autocomplete"); +} + +#start-autocomplete { + -moz-binding: url("chrome://browser/content/bindings/autocomplete.xml#autocomplete-popup"); +} + +richgrid { + -moz-binding: url("chrome://browser/content/bindings/grid.xml#richgrid"); +} + +richgriditem { + -moz-binding: url("chrome://browser/content/bindings/grid.xml#richgrid-item"); +} + +placeitem { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#place-item"); + background-color: transparent; +} + +placeitem[type="folder"] { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#place-folder"); +} + +placelabel { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#place-label"); +} + +radio { + -moz-binding: url("chrome://global/content/bindings/radio.xml#radio"); +} + +checkbox.toggleswitch { + -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#checkbox-toggleswitch"); +} + +menulist { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#menulist"); +} + +.chrome-select-option { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#chrome-select-option"); +} + +/* richlist defaults ------------------------------------------------------- */ +richlistbox[batch] { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#richlistbox-batch"); +} + +richlistbox[bindingType="contextmenu"] { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#richlistbox-contextmenu"); +} + +richlistitem { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#richlistitem"); +} + +richgriditem[typeName="download"][state="-1"] { + -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-not-started"); +} + +richgriditem[typeName="download"] { + -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-downloading"); +} + +richgriditem[typeName="download"][state="1"] { + -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-done"); +} + +richgriditem[typeName="download"][state="2"], +richgriditem[typeName="download"][state="3"] { + -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-retry"); +} + +richgriditem[typeName="download"][state="4"] { + -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-paused"); +} + +richlistitem[type="error"], +richlistitem[type="warning"] { + -moz-binding: url("chrome://browser/content/bindings/console.xml#error"); +} + +richlistitem[type="message"]{ + -moz-binding: url("chrome://browser/content/bindings/console.xml#message"); +} + +dialog { + -moz-binding: url("chrome://browser/content/bindings/dialog.xml#dialog"); +} + +/* Do not allow these to inherit from the toolkit binding */ +dialog.content-dialog { + -moz-binding: none; +} + +pageaction { + -moz-binding: url("chrome://browser/content/bindings/pageaction.xml#pageaction"); +} + +arrowbox { + -moz-binding: url("chrome://browser/content/bindings/arrowbox.xml#arrowbox"); +} + +/* Disable context menus in textboxes */ +.textbox-input-box, +.textbox-input-box[spellcheck="true"] { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#input-box"); +} + +textbox { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#textbox"); +} + +textbox[multiline="true"] { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#textarea"); +} + +textbox[type="timed"] { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#timed-textbox"); +} + +textbox[type="search"] { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#search-textbox"); +} + +textbox[type="number"] { + -moz-binding: url("chrome://browser/content/bindings/bindings.xml#numberbox"); +} diff --git a/browser/metro/base/content/browser.js b/browser/metro/base/content/browser.js new file mode 100644 index 000000000000..e2b9fe2b2ff9 --- /dev/null +++ b/browser/metro/base/content/browser.js @@ -0,0 +1,1986 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; +let Cr = Components.results; + +const kBrowserViewZoomLevelPrecision = 10000; + +// allow panning after this timeout on pages with registered touch listeners +const kTouchTimeout = 300; +const kSetInactiveStateTimeout = 100; + +const kDefaultMetadata = { autoSize: false, allowZoom: true, autoScale: true }; + +// Override sizeToContent in the main window. It breaks things (bug 565887) +window.sizeToContent = function() { + Cu.reportError("window.sizeToContent is not allowed in this window"); +} + +function getBrowser() { + return Browser.selectedBrowser; +} + +var Browser = { + _debugEvents: false, + _tabs: [], + _selectedTab: null, + _tabId: 0, + windowUtils: window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils), + + get defaultBrowserWidth() { + return window.innerWidth; + }, + + startup: function startup() { + var self = this; + + try { + messageManager.loadFrameScript("chrome://browser/content/Util.js", true); + messageManager.loadFrameScript("chrome://browser/content/contenthandlers/Content.js", true); + messageManager.loadFrameScript("chrome://browser/content/contenthandlers/FormHelper.js", true); + messageManager.loadFrameScript("chrome://browser/content/contenthandlers/SelectionHandler.js", true); + messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ContextMenuHandler.js", true); + messageManager.loadFrameScript("chrome://browser/content/contenthandlers/FindHandler.js", true); + // XXX Viewport resizing disabled because of bug 766142 + //messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ViewportHandler.js", true); + messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ConsoleAPIObserver.js", true); + //messageManager.loadFrameScript("chrome://browser/content/contenthandlers/PluginCTPHandler.js", true); + } catch (e) { + // XXX whatever is calling startup needs to dump errors! + dump("###########" + e + "\n"); + } + + /* handles dispatching clicks on browser into clicks in content or zooms */ + Elements.browsers.customDragger = new Browser.MainDragger(); + + /* handles web progress management for open browsers */ + Elements.browsers.webProgress = WebProgress.init(); + + // Call InputSourceHelper first so global listeners get called before + // we start processing input in TouchModule. + InputSourceHelper.init(); + + TouchModule.init(); + ScrollwheelModule.init(Elements.browsers); + GestureModule.init(); + BrowserTouchHandler.init(); + + // Warning, total hack ahead. All of the real-browser related scrolling code + // lies in a pretend scrollbox here. Let's not land this as-is. Maybe it's time + // to redo all the dragging code. + this.contentScrollbox = Elements.browsers; + this.contentScrollboxScroller = { + scrollBy: function(aDx, aDy) { + let view = getBrowser().getRootView(); + view.scrollBy(aDx, aDy); + }, + + scrollTo: function(aX, aY) { + let view = getBrowser().getRootView(); + view.scrollTo(aX, aY); + }, + + getPosition: function(aScrollX, aScrollY) { + let view = getBrowser().getRootView(); + let scroll = view.getPosition(); + aScrollX.value = scroll.x; + aScrollY.value = scroll.y; + } + }; + + ContentAreaObserver.init(); + + function fullscreenHandler() { + if (!window.fullScreen) + Elements.toolbar.setAttribute("fullscreen", "true"); + else + Elements.toolbar.removeAttribute("fullscreen"); + } + window.addEventListener("fullscreen", fullscreenHandler, false); + + BrowserUI.init(); + + window.controllers.appendController(this); + window.controllers.appendController(BrowserUI); + + let os = Services.obs; + os.addObserver(SessionHistoryObserver, "browser:purge-session-history", false); + os.addObserver(ActivityObserver, "application-background", false); + os.addObserver(ActivityObserver, "application-foreground", false); + os.addObserver(ActivityObserver, "system-active", false); + os.addObserver(ActivityObserver, "system-idle", false); + os.addObserver(ActivityObserver, "system-display-on", false); + os.addObserver(ActivityObserver, "system-display-off", false); + + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess(); + + Elements.browsers.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false); + + // Make sure we're online before attempting to load + Util.forceOnline(); + + // If this is an intial window launch the commandline handler passes us the default + // page as an argument. commandURL _should_ never be empty, but we protect against it + // below. However, we delay trying to get the fallback homepage until we really need it. + let commandURL = null; + if (window.arguments && window.arguments[0]) + commandURL = window.arguments[0]; + + // Should we restore the previous session (crash or some other event) + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + if (ss.shouldRestore() || Services.prefs.getBoolPref("browser.startup.sessionRestore")) { + let bringFront = false; + // First open any commandline URLs, except the homepage + if (commandURL && commandURL != this.getHomePage()) { + this.addTab(commandURL, true); + } else { + bringFront = true; + // Initial window resizes call functions that assume a tab is in the tab list + // and restored tabs are added too late. We add a dummy to to satisfy the resize + // code and then remove the dummy after the session has been restored. + let dummy = this.addTab("about:blank", true); + let dummyCleanup = { + observe: function(aSubject, aTopic, aData) { + Services.obs.removeObserver(dummyCleanup, "sessionstore-windows-restored"); + if (aData == "fail") + Browser.addTab(commandURL || Browser.getHomePage(), true); + dummy.chromeTab.ignoreUndo = true; + Browser.closeTab(dummy, { forceClose: true }); + } + }; + Services.obs.addObserver(dummyCleanup, "sessionstore-windows-restored", false); + } + ss.restoreLastSession(bringFront); + } else { + this.addTab(commandURL || this.getHomePage(), true); + } + + messageManager.addMessageListener("DOMLinkAdded", this); + messageManager.addMessageListener("MozScrolledAreaChanged", this); + messageManager.addMessageListener("Browser:ViewportMetadata", this); + messageManager.addMessageListener("Browser:FormSubmit", this); + messageManager.addMessageListener("Browser:ZoomToPoint:Return", this); + messageManager.addMessageListener("Browser:CanUnload:Return", this); + messageManager.addMessageListener("scroll", this); + messageManager.addMessageListener("Browser:CertException", this); + messageManager.addMessageListener("Browser:BlockedSite", this); + messageManager.addMessageListener("Browser:ErrorPage", this); + messageManager.addMessageListener("Browser:TapOnSelection", this); + messageManager.addMessageListener("Browser:PluginClickToPlayClicked", this); + + // Let everyone know what kind of mouse input we are + // starting with: + InputSourceHelper.fireUpdate(); + + // Broadcast a UIReady message so add-ons know we are finished with startup + let event = document.createEvent("Events"); + event.initEvent("UIReady", true, false); + window.dispatchEvent(event); + }, + + quit: function quit() { + // NOTE: onclose seems to be called only when using OS chrome to close a window, + // so we need to handle the Browser.closing check ourselves. + if (this.closing()) { + window.QueryInterface(Ci.nsIDOMChromeWindow).minimize(); + window.close(); + } + }, + + _waitingToClose: false, + closing: function closing() { + // If we are already waiting for the close prompt, don't show another + if (this._waitingToClose) + return false; + + // Prompt if we have multiple tabs before closing window + let numTabs = this._tabs.length; + if (numTabs > 1) { + let shouldPrompt = Services.prefs.getBoolPref("browser.tabs.warnOnClose"); + if (shouldPrompt) { + let prompt = Services.prompt; + + // Default to true: if it were false, we wouldn't get this far + let warnOnClose = { value: true }; + + let messageBase = Strings.browser.GetStringFromName("tabs.closeWarning"); + let message = PluralForm.get(numTabs, messageBase).replace("#1", numTabs); + + let title = Strings.browser.GetStringFromName("tabs.closeWarningTitle"); + let closeText = Strings.browser.GetStringFromName("tabs.closeButton"); + let checkText = Strings.browser.GetStringFromName("tabs.closeWarningPromptMe"); + let buttons = (prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_0) + + (prompt.BUTTON_TITLE_CANCEL * prompt.BUTTON_POS_1); + + this._waitingToClose = true; + let pressed = prompt.confirmEx(window, title, message, buttons, closeText, null, null, checkText, warnOnClose); + this._waitingToClose = false; + + // Don't set the pref unless they press OK and it's false + let reallyClose = (pressed == 0); + if (reallyClose && !warnOnClose.value) + Services.prefs.setBoolPref("browser.tabs.warnOnClose", false); + + // If we don't want to close, return now. If we are closing, continue with other housekeeping. + if (!reallyClose) + return false; + } + } + + // Figure out if there's at least one other browser window around. + let lastBrowser = true; + let e = Services.wm.getEnumerator("navigator:browser"); + while (e.hasMoreElements() && lastBrowser) { + let win = e.getNext(); + if (win != window) + lastBrowser = false; + } + if (!lastBrowser) + return true; + + // Let everyone know we are closing the last browser window + let closingCancelled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(closingCancelled, "browser-lastwindow-close-requested", null); + if (closingCancelled.data) + return false; + + Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null); + return true; + }, + + shutdown: function shutdown() { + BrowserUI.uninit(); + ContentAreaObserver.uninit(); + + messageManager.removeMessageListener("MozScrolledAreaChanged", this); + messageManager.removeMessageListener("Browser:ViewportMetadata", this); + messageManager.removeMessageListener("Browser:FormSubmit", this); + messageManager.removeMessageListener("Browser:ZoomToPoint:Return", this); + messageManager.removeMessageListener("scroll", this); + messageManager.removeMessageListener("Browser:CertException", this); + messageManager.removeMessageListener("Browser:BlockedSite", this); + messageManager.removeMessageListener("Browser:ErrorPage", this); + messageManager.removeMessageListener("Browser:PluginClickToPlayClicked", this); + messageManager.removeMessageListener("Browser:TapOnSelection", this); + + var os = Services.obs; + os.removeObserver(SessionHistoryObserver, "browser:purge-session-history"); + os.removeObserver(ActivityObserver, "application-background", false); + os.removeObserver(ActivityObserver, "application-foreground", false); + os.removeObserver(ActivityObserver, "system-active", false); + os.removeObserver(ActivityObserver, "system-idle", false); + os.removeObserver(ActivityObserver, "system-display-on", false); + os.removeObserver(ActivityObserver, "system-display-off", false); + + window.controllers.removeController(this); + window.controllers.removeController(BrowserUI); + }, + + getHomePage: function getHomePage(aOptions) { + aOptions = aOptions || { useDefault: false }; + + let url = "about:start"; + try { + let prefs = aOptions.useDefault ? Services.prefs.getDefaultBranch(null) : Services.prefs; + url = prefs.getComplexValue("browser.startup.homepage", Ci.nsIPrefLocalizedString).data; + } + catch(e) { } + + return url; + }, + + get browsers() { + return this._tabs.map(function(tab) { return tab.browser; }); + }, + + /** + * Load a URI in the current tab, or a new tab if necessary. + * @param aURI String + * @param aParams Object with optional properties that will be passed to loadURIWithFlags: + * flags, referrerURI, charset, postData. + */ + loadURI: function loadURI(aURI, aParams) { + let browser = this.selectedBrowser; + + // We need to keep about: pages opening in new "local" tabs. We also want to spawn + // new "remote" tabs if opening web pages from a "local" about: page. + dump("loadURI=" + aURI + "\ncurrentURI=" + browser.currentURI.spec + "\n"); + + let params = aParams || {}; + try { + let flags = params.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + let postData = ("postData" in params && params.postData) ? params.postData.value : null; + let referrerURI = "referrerURI" in params ? params.referrerURI : null; + let charset = "charset" in params ? params.charset : null; + dump("loading tab: " + aURI + "\n"); + browser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData); + } catch(e) { + dump("Error: " + e + "\n"); + } + }, + + /** + * Determine if the given URL is a shortcut/keyword and, if so, expand it + * @param aURL String + * @param aPostDataRef Out param contains any required post data for a search + * @returns the expanded shortcut, or the original URL if not a shortcut + */ + getShortcutOrURI: function getShortcutOrURI(aURL, aPostDataRef) { + let shortcutURL = null; + let keyword = aURL; + let param = ""; + + let offset = aURL.indexOf(" "); + if (offset > 0) { + keyword = aURL.substr(0, offset); + param = aURL.substr(offset + 1); + } + + if (!aPostDataRef) + aPostDataRef = {}; + + let engine = Services.search.getEngineByAlias(keyword); + if (engine) { + let submission = engine.getSubmission(param); + aPostDataRef.value = submission.postData; + return submission.uri.spec; + } + + try { + [shortcutURL, aPostDataRef.value] = PlacesUtils.getURLAndPostDataForKeyword(keyword); + } catch (e) {} + + if (!shortcutURL) + return aURL; + + let postData = ""; + if (aPostDataRef.value) + postData = unescape(aPostDataRef.value); + + if (/%s/i.test(shortcutURL) || /%s/i.test(postData)) { + let charset = ""; + const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/; + let matches = shortcutURL.match(re); + if (matches) + [, shortcutURL, charset] = matches; + else { + // Try to get the saved character-set. + try { + // makeURI throws if URI is invalid. + // Will return an empty string if character-set is not found. + charset = PlacesUtils.history.getCharsetForURI(Util.makeURI(shortcutURL)); + } catch (e) { dump("--- error " + e + "\n"); } + } + + let encodedParam = ""; + if (charset) + encodedParam = escape(convertFromUnicode(charset, param)); + else // Default charset is UTF-8 + encodedParam = encodeURIComponent(param); + + shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param); + + if (/%s/i.test(postData)) // POST keyword + aPostDataRef.value = getPostDataStream(postData, param, encodedParam, "application/x-www-form-urlencoded"); + } else if (param) { + // This keyword doesn't take a parameter, but one was provided. Just return + // the original URL. + aPostDataRef.value = null; + + return aURL; + } + + return shortcutURL; + }, + + /** + * Return the currently active object + */ + get selectedBrowser() { + return (this._selectedTab && this._selectedTab.browser); + }, + + get tabs() { + return this._tabs; + }, + + getTabForBrowser: function getTabForBrowser(aBrowser) { + let tabs = this._tabs; + for (let i = 0; i < tabs.length; i++) { + if (tabs[i].browser == aBrowser) + return tabs[i]; + } + return null; + }, + + getBrowserForWindowId: function getBrowserForWindowId(aWindowId) { + for (let i = 0; i < this.browsers.length; i++) { + if (this.browsers[i].contentWindowId == aWindowId) + return this.browsers[i]; + } + return null; + }, + + getTabAtIndex: function getTabAtIndex(index) { + if (index >= this._tabs.length || index < 0) + return null; + return this._tabs[index]; + }, + + getTabFromChrome: function getTabFromChrome(chromeTab) { + for (var t = 0; t < this._tabs.length; t++) { + if (this._tabs[t].chromeTab == chromeTab) + return this._tabs[t]; + } + return null; + }, + + createTabId: function createTabId() { + return this._tabId++; + }, + + addTab: function browser_addTab(aURI, aBringFront, aOwner, aParams) { + let params = aParams || {}; + let newTab = new Tab(aURI, params); + newTab.owner = aOwner || null; + this._tabs.push(newTab); + + if (aBringFront) + this.selectedTab = newTab; + + let getAttention = ("getAttention" in params ? params.getAttention : !aBringFront); + let event = document.createEvent("UIEvents"); + event.initUIEvent("TabOpen", true, false, window, getAttention); + newTab.chromeTab.dispatchEvent(event); + newTab.browser.messageManager.sendAsyncMessage("Browser:TabOpen"); + + return newTab; + }, + + closeTab: function closeTab(aTab, aOptions) { + let tab = aTab instanceof XULElement ? this.getTabFromChrome(aTab) : aTab; + if (!tab || !this._getNextTab(tab)) + return; + + if (aOptions && "forceClose" in aOptions && aOptions.forceClose) { + this._doCloseTab(aTab); + return; + } + + tab.browser.messageManager.sendAsyncMessage("Browser:CanUnload", {}); + }, + + _doCloseTab: function _doCloseTab(aTab) { + let nextTab = this._getNextTab(aTab); + if (!nextTab) + return; + + // Tabs owned by the closed tab are now orphaned. + this._tabs.forEach(function(item, index, array) { + if (item.owner == aTab) + item.owner = null; + }); + + let event = document.createEvent("Events"); + event.initEvent("TabClose", true, false); + aTab.chromeTab.dispatchEvent(event); + aTab.browser.messageManager.sendAsyncMessage("Browser:TabClose"); + + let container = aTab.chromeTab.parentNode; + aTab.destroy(); + this._tabs.splice(this._tabs.indexOf(aTab), 1); + + this.selectedTab = nextTab; + + event = document.createEvent("Events"); + event.initEvent("TabRemove", true, false); + container.dispatchEvent(event); + }, + + _getNextTab: function _getNextTab(aTab) { + let tabIndex = this._tabs.indexOf(aTab); + if (tabIndex == -1) + return null; + + let nextTab = this._selectedTab; + if (nextTab == aTab) { + nextTab = this.getTabAtIndex(tabIndex + 1) || this.getTabAtIndex(tabIndex - 1); + + // If the next tab is not a sibling, switch back to the parent. + if (aTab.owner && nextTab.owner != aTab.owner) + nextTab = aTab.owner; + + if (!nextTab) + return null; + } + + return nextTab; + }, + + get selectedTab() { + return this._selectedTab; + }, + + set selectedTab(tab) { + if (tab instanceof XULElement) + tab = this.getTabFromChrome(tab); + + if (!tab) + return; + + if (this._selectedTab == tab) { + // Deck does not update its selectedIndex when children + // are removed. See bug 602708 + Elements.browsers.selectedPanel = tab.notification; + return; + } + + let isFirstTab = this._selectedTab == null; + let lastTab = this._selectedTab; + let oldBrowser = lastTab ? lastTab._browser : null; + let browser = tab.browser; + + this._selectedTab = tab; + + if (lastTab) + lastTab.active = false; + + if (tab) + tab.active = true; + + if (isFirstTab) { + // Don't waste time at startup updating the whole UI; just display the URL. + BrowserUI._titleChanged(browser); + } else { + // Update all of our UI to reflect the new tab's location + BrowserUI.updateURI(); + IdentityUI.checkIdentity(); + + let event = document.createEvent("Events"); + event.initEvent("TabSelect", true, false); + event.lastTab = lastTab; + tab.chromeTab.dispatchEvent(event); + } + + tab.lastSelected = Date.now(); + }, + + supportsCommand: function(cmd) { + return false; + }, + + isCommandEnabled: function(cmd) { + return false; + }, + + doCommand: function(cmd) { + }, + + getNotificationBox: function getNotificationBox(aBrowser) { + let browser = aBrowser || this.selectedBrowser; + return browser.parentNode; + }, + + /** + * Handle cert exception message from content. + */ + _handleCertException: function _handleCertException(aMessage) { + let json = aMessage.json; + if (json.action == "leave") { + // Get the start page from the *default* pref branch, not the user's + let url = Browser.getHomePage({ useDefault: true }); + this.loadURI(url); + } else { + // Handle setting an cert exception and reloading the page + try { + // Add a new SSL exception for this URL + let uri = Services.io.newURI(json.url, null, null); + let sslExceptions = new SSLExceptions(); + + if (json.action == "permanent") + sslExceptions.addPermanentException(uri, errorDoc.defaultView); + else + sslExceptions.addTemporaryException(uri, errorDoc.defaultView); + } catch (e) { + dump("EXCEPTION handle content command: " + e + "\n" ); + } + + // Automatically reload after the exception was added + aMessage.target.reload(); + } + }, + + /** + * Handle blocked site message from content. + */ + _handleBlockedSite: function _handleBlockedSite(aMessage) { + let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter); + let json = aMessage.json; + switch (json.action) { + case "leave": { + // Get the start page from the *default* pref branch, not the user's + let url = Browser.getHomePage({ useDefault: true }); + this.loadURI(url); + break; + } + case "report-malware": { + // Get the stop badware "why is this blocked" report url, append the current url, and go there. + try { + let reportURL = formatter.formatURLPref("browser.safebrowsing.malware.reportURL"); + reportURL += json.url; + this.loadURI(reportURL); + } catch (e) { + Cu.reportError("Couldn't get malware report URL: " + e); + } + break; + } + case "report-phishing": { + // It's a phishing site, not malware + try { + let reportURL = formatter.formatURLPref("browser.safebrowsing.warning.infoURL"); + this.loadURI(reportURL); + } catch (e) { + Cu.reportError("Couldn't get phishing info URL: " + e); + } + break; + } + } + }, + + pinSite: function browser_pinSite() { + // We use a unique ID per URL, so just use an MD5 hash of the URL as the ID. + let hasher = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + hasher.init(Ci.nsICryptoHash.MD5); + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + stringStream.data = Browser.selectedBrowser.currentURI.spec; + hasher.updateFromStream(stringStream, -1); + let hashASCII = hasher.finish(true); + + // Get a path to our app tile + var file = Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("CurProcD", Components.interfaces.nsIFile); + // Get rid of the current working directory's metro subidr + file = file.parent; + file.append("tileresources"); + file.append("VisualElements_logo.png"); + var ios = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + var uriSpec = ios.newFileURI(file).spec; + MetroUtils.pinTileAsync("FFTileID_" + hashASCII, + Browser.selectedBrowser.contentTitle, // short name + Browser.selectedBrowser.contentTitle, // display name + "metrobrowser -url " + Browser.selectedBrowser.currentURI.spec, + uriSpec, + uriSpec); + }, + + unpinSite: function browser_unpinSite() { + if (!MetroUtils.immersive) + return; + + // We use a unique ID per URL, so just use an MD5 hash of the URL as the ID. + let hasher = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + hasher.init(Ci.nsICryptoHash.MD5); + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + stringStream.data = Browser.selectedBrowser.currentURI.spec; + hasher.updateFromStream(stringStream, -1); + let hashASCII = hasher.finish(true); + + MetroUtils.unpinTileAsync("FFTileID_" + hashASCII); + }, + + isSitePinned: function browser_isSitePinned() { + if (!MetroUtils.immersive) + return false; + + // We use a unique ID per URL, so just use an MD5 hash of the URL as the ID. + let hasher = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + hasher.init(Ci.nsICryptoHash.MD5); + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + stringStream.data = Browser.selectedBrowser.currentURI.spec; + hasher.updateFromStream(stringStream, -1); + let hashASCII = hasher.finish(true); + + return MetroUtils.isTilePinned("FFTileID_" + hashASCII); + }, + + starSite: function browser_starSite(callback) { + let uri = this.selectedBrowser.currentURI; + let title = this.selectedBrowser.contentTitle; + + Bookmarks.addForURI(uri, title, callback); + }, + + unstarSite: function browser_unstarSite(callback) { + let uri = this.selectedBrowser.currentURI; + Bookmarks.removeForURI(uri, callback); + }, + + isSiteStarredAsync: function browser_isSiteStarredAsync(callback) { + let uri = this.selectedBrowser.currentURI; + Bookmarks.isURIBookmarked(uri, callback); + }, + + /** Zoom one step in (negative) or out (positive). */ + zoom: function zoom(aDirection) { + let tab = this.selectedTab; + if (!tab.allowZoom) + return; + + let browser = tab.browser; + let oldZoomLevel = browser.scale; + let zoomLevel = oldZoomLevel; + + let zoomValues = ZoomManager.zoomValues; + let i = zoomValues.indexOf(ZoomManager.snap(zoomLevel)) + (aDirection < 0 ? 1 : -1); + if (i >= 0 && i < zoomValues.length) + zoomLevel = zoomValues[i]; + + zoomLevel = tab.clampZoomLevel(zoomLevel); + + let browserRect = browser.getBoundingClientRect(); + let center = browser.transformClientToBrowser(browserRect.width / 2, + browserRect.height / 2); + let rect = this._getZoomRectForPoint(center.x, center.y, zoomLevel); + AnimatedZoom.animateTo(rect); + }, + + /** Rect should be in browser coordinates. */ + _getZoomLevelForRect: function _getZoomLevelForRect(rect) { + const margin = 15; + return this.selectedTab.clampZoomLevel(ContentAreaObserver.width / (rect.width + margin * 2)); + }, + + /** + * Find an appropriate zoom rect for an element bounding rect, if it exists. + * @return Rect in viewport coordinates, or null + */ + _getZoomRectForRect: function _getZoomRectForRect(rect, y) { + let zoomLevel = this._getZoomLevelForRect(rect); + return this._getZoomRectForPoint(rect.center().x, y, zoomLevel); + }, + + /** + * Find a good zoom rectangle for point that is specified in browser coordinates. + * @return Rect in viewport coordinates + */ + _getZoomRectForPoint: function _getZoomRectForPoint(x, y, zoomLevel) { + let browser = getBrowser(); + x = x * browser.scale; + y = y * browser.scale; + + zoomLevel = Math.min(ZoomManager.MAX, zoomLevel); + let oldScale = browser.scale; + let zoomRatio = zoomLevel / oldScale; + let browserRect = browser.getBoundingClientRect(); + let newVisW = browserRect.width / zoomRatio, newVisH = browserRect.height / zoomRatio; + let result = new Rect(x - newVisW / 2, y - newVisH / 2, newVisW, newVisH); + + // Make sure rectangle doesn't poke out of viewport + return result.translateInside(new Rect(0, 0, browser.contentDocumentWidth * oldScale, + browser.contentDocumentHeight * oldScale)); + }, + + zoomToPoint: function zoomToPoint(cX, cY, aRect) { + let tab = this.selectedTab; + if (!tab.allowZoom) + return null; + + let zoomRect = null; + if (aRect) + zoomRect = this._getZoomRectForRect(aRect, cY); + + if (!zoomRect && tab.isDefaultZoomLevel()) { + let scale = tab.clampZoomLevel(tab.browser.scale * 2); + zoomRect = this._getZoomRectForPoint(cX, cY, scale); + } + + if (zoomRect) + AnimatedZoom.animateTo(zoomRect); + + return zoomRect; + }, + + zoomFromPoint: function zoomFromPoint(cX, cY) { + let tab = this.selectedTab; + if (tab.allowZoom && !tab.isDefaultZoomLevel()) { + let zoomLevel = tab.getDefaultZoomLevel(); + let zoomRect = this._getZoomRectForPoint(cX, cY, zoomLevel); + AnimatedZoom.animateTo(zoomRect); + } + }, + + // The device-pixel-to-CSS-px ratio used to adjust meta viewport values. + // This is higher on higher-dpi displays, so pages stay about the same physical size. + getScaleRatio: function getScaleRatio() { + let prefValue = Services.prefs.getIntPref("browser.viewport.scaleRatio"); + if (prefValue > 0) + return prefValue / 100; + + let dpi = Util.displayDPI; + if (dpi < 200) // Includes desktop displays, and LDPI and MDPI Android devices + return 1; + else if (dpi < 300) // Includes Nokia N900, and HDPI Android devices + return 1.5; + + // For very high-density displays like the iPhone 4, calculate an integer ratio. + return Math.floor(dpi / 150); + }, + + /** + * Convenience function for getting the scrollbox position off of a + * scrollBoxObject interface. Returns the actual values instead of the + * wrapping objects. + * + * @param scroller a scrollBoxObject on which to call scroller.getPosition() + */ + getScrollboxPosition: function getScrollboxPosition(scroller) { + let x = {}; + let y = {}; + scroller.getPosition(x, y); + return new Point(x.value, y.value); + }, + + forceChromeReflow: function forceChromeReflow() { + let dummy = getComputedStyle(document.documentElement, "").width; + }, + + receiveMessage: function receiveMessage(aMessage) { + let json = aMessage.json; + let browser = aMessage.target; + + switch (aMessage.name) { + case "DOMLinkAdded": { + // checks for an icon to use for a web app + // apple-touch-icon size is 57px and default size is 16px + let rel = json.rel.toLowerCase().split(" "); + if (rel.indexOf("icon") != -1) { + // We use the sizes attribute if available + // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon + let size = 16; + if (json.sizes) { + let sizes = json.sizes.toLowerCase().split(" "); + sizes.forEach(function(item) { + if (item != "any") { + let [w, h] = item.split("x"); + size = Math.max(Math.min(w, h), size); + } + }); + } + if (size > browser.appIcon.size) { + browser.appIcon.href = json.href; + browser.appIcon.size = size; + } + } + else if ((rel.indexOf("apple-touch-icon") != -1) && (browser.appIcon.size < 57)) { + // XXX should we support apple-touch-icon-precomposed ? + // see http://developer.apple.com/safari/library/documentation/appleapplications/reference/safariwebcontent/configuringwebapplications/configuringwebapplications.html + browser.appIcon.href = json.href; + browser.appIcon.size = 57; + } + break; + } + case "MozScrolledAreaChanged": { + let tab = this.getTabForBrowser(browser); + if (tab) + tab.scrolledAreaChanged(); + break; + } + case "Browser:ViewportMetadata": { + let tab = this.getTabForBrowser(browser); + // Some browser such as iframes loaded dynamically into the chrome UI + // does not have any assigned tab + if (tab) + tab.updateViewportMetadata(json); + break; + } + case "Browser:FormSubmit": + browser.lastLocation = null; + break; + + case "Browser:CanUnload:Return": { + if (!json.permit) + return; + + // Allow a little delay to not close the target tab while processing + // a message for this particular tab + setTimeout(function(self) { + let tab = self.getTabForBrowser(browser); + self._doCloseTab(tab); + }, 0, this); + break; + } + case "Browser:ZoomToPoint:Return": + if (json.zoomTo) { + let rect = Rect.fromRect(json.zoomTo); + this.zoomToPoint(json.x, json.y, rect); + } else { + this.zoomFromPoint(json.x, json.y); + } + break; + case "Browser:CertException": + this._handleCertException(aMessage); + break; + case "Browser:BlockedSite": + this._handleBlockedSite(aMessage); + break; + case "Browser:ErrorPage": + break; + case "Browser:PluginClickToPlayClicked": { + // Save off session history + let parent = browser.parentNode; + let data = browser.__SS_data; + if (data.entries.length == 0) + return; + + // Remove the browser from the DOM, effectively killing it's content + parent.removeChild(browser); + + // Re-create the browser as non-remote, so plugins work + browser.setAttribute("remote", "false"); + parent.appendChild(browser); + + // Reload the content using session history + browser.__SS_data = data; + let json = { + uri: data.entries[data.index - 1].url, + flags: null, + entries: data.entries, + index: data.index + }; + browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json); + break; + } + + case "Browser:TapOnSelection": + if (!InputSourceHelper.isPrecise) { + if (SelectionHelperUI.isActive()) { + SelectionHelperUI.shutdown(); + } + if (SelectionHelperUI.canHandle(aMessage)) { + SelectionHelperUI.openEditSession(aMessage); + } + } + break; + } + }, + + onAboutPolicyClick: function() { + FlyoutPanelsUI.hide(); + BrowserUI.newTab(Services.prefs.getCharPref("app.privacyURL"), + Browser.selectedTab); + } + +}; + +Browser.MainDragger = function MainDragger() { + this._horizontalScrollbar = document.getElementById("horizontal-scroller"); + this._verticalScrollbar = document.getElementById("vertical-scroller"); + this._scrollScales = { x: 0, y: 0 }; + + Elements.browsers.addEventListener("PanBegin", this, false); + Elements.browsers.addEventListener("PanFinished", this, false); +}; + +Browser.MainDragger.prototype = { + isDraggable: function isDraggable(target, scroller) { + return { x: true, y: true }; + }, + + dragStart: function dragStart(clientX, clientY, target, scroller) { + let browser = getBrowser(); + let bcr = browser.getBoundingClientRect(); + this._contentView = browser.getViewAt(clientX - bcr.left, clientY - bcr.top); + }, + + dragStop: function dragStop(dx, dy, scroller) { + if (this._contentView && this._contentView._updateCacheViewport) + this._contentView._updateCacheViewport(); + this._contentView = null; + }, + + dragMove: function dragMove(dx, dy, scroller, aIsKinetic) { + let doffset = new Point(dx, dy); + + this._panContent(doffset); + + if (aIsKinetic && doffset.x != 0) + return false; + + this._updateScrollbars(); + + return !doffset.equals(dx, dy); + }, + + handleEvent: function handleEvent(aEvent) { + let browser = getBrowser(); + switch (aEvent.type) { + case "PanBegin": { + let width = ContentAreaObserver.width, height = ContentAreaObserver.height; + let contentWidth = browser.contentDocumentWidth * browser.scale; + let contentHeight = browser.contentDocumentHeight * browser.scale; + + // Allow a small margin on both sides to prevent adding scrollbars + // on small viewport approximation + const ALLOWED_MARGIN = 5; + const SCROLL_CORNER_SIZE = 8; + this._scrollScales = { + x: (width + ALLOWED_MARGIN) < contentWidth ? (width - SCROLL_CORNER_SIZE) / contentWidth : 0, + y: (height + ALLOWED_MARGIN) < contentHeight ? (height - SCROLL_CORNER_SIZE) / contentHeight : 0 + } + + this._showScrollbars(); + break; + } + case "PanFinished": + this._hideScrollbars(); + + // Update the scroll position of the content + browser._updateCSSViewport(); + break; + } + }, + + _panContent: function md_panContent(aOffset) { + if (this._contentView && !this._contentView.isRoot()) { + this._panContentView(this._contentView, aOffset); + // XXX we may need to have "escape borders" for iframe panning + // XXX does not deal with scrollables within scrollables + } + // Do content panning + this._panContentView(getBrowser().getRootView(), aOffset); + }, + + /** Pan scroller by the given amount. Updates doffset with leftovers. */ + _panContentView: function _panContentView(contentView, doffset) { + let pos0 = contentView.getPosition(); + contentView.scrollBy(doffset.x, doffset.y); + let pos1 = contentView.getPosition(); + doffset.subtract(pos1.x - pos0.x, pos1.y - pos0.y); + }, + + _updateScrollbars: function _updateScrollbars() { + let scaleX = this._scrollScales.x, scaleY = this._scrollScales.y; + let contentScroll = Browser.getScrollboxPosition(Browser.contentScrollboxScroller); + + if (scaleX) + this._horizontalScrollbar.style.MozTransform = + "translateX(" + Math.round(contentScroll.x * scaleX) + "px)"; + + if (scaleY) { + let y = Math.round(contentScroll.y * scaleY); + let x = 0; + + this._verticalScrollbar.style.MozTransform = + "translate(" + x + "px," + y + "px)"; + } + }, + + _showScrollbars: function _showScrollbars() { + this._updateScrollbars(); + let scaleX = this._scrollScales.x, scaleY = this._scrollScales.y; + if (scaleX) { + this._horizontalScrollbar.width = ContentAreaObserver.width * scaleX; + this._horizontalScrollbar.setAttribute("panning", "true"); + } + + if (scaleY) { + this._verticalScrollbar.height = ContentAreaObserver.height * scaleY; + this._verticalScrollbar.setAttribute("panning", "true"); + } + }, + + _hideScrollbars: function _hideScrollbars() { + this._scrollScales.x = 0, this._scrollScales.y = 0; + this._horizontalScrollbar.removeAttribute("panning"); + this._verticalScrollbar.removeAttribute("panning"); + this._horizontalScrollbar.style.MozTransform = ""; + this._verticalScrollbar.style.MozTransform = ""; + } +}; + + + +const OPEN_APPTAB = 100; // Hack until we get a real API + +function nsBrowserAccess() { } + +nsBrowserAccess.prototype = { + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIBrowserDOMWindow) || aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + }, + + _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) { + let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + if (isExternal && aURI && aURI.schemeIs("chrome")) + return null; + + let loadflags = isExternal ? + Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : + Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + let location; + if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { + switch (aContext) { + case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL : + aWhere = Services.prefs.getIntPref("browser.link.open_external"); + break; + default : // OPEN_NEW or an illegal value + aWhere = Services.prefs.getIntPref("browser.link.open_newwindow"); + } + } + + let browser; + if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW) { + let url = aURI ? aURI.spec : "about:blank"; + let newWindow = openDialog("chrome://browser/content/browser.xul", "_blank", + "all,dialog=no", url, null, null, null); + // since newWindow.Browser doesn't exist yet, just return null + return null; + } else if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) { + let owner = isExternal ? null : Browser.selectedTab; + let tab = Browser.addTab("about:blank", true, owner, { getAttention: true }); + if (isExternal) + tab.closeOnExit = true; + browser = tab.browser; + } else if (aWhere == OPEN_APPTAB) { + Browser.tabs.forEach(function(aTab) { + if ("appURI" in aTab.browser && aTab.browser.appURI.spec == aURI.spec) { + Browser.selectedTab = aTab; + browser = aTab.browser; + } + }); + + if (!browser) { + // Make a new tab to hold the app + let tab = Browser.addTab("about:blank", true, null, { getAttention: true }); + browser = tab.browser; + browser.appURI = aURI; + } else { + // Just use the existing browser, but return null to keep the system from trying to load the URI again + browser = null; + } + } else { // OPEN_CURRENTWINDOW and illegal values + browser = Browser.selectedBrowser; + } + + try { + let referrer; + if (aURI && browser) { + if (aOpener) { + location = aOpener.location; + referrer = Services.io.newURI(location, null, null); + } + browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null); + } + browser.focus(); + } catch(e) { } + + // We are loading web content into this window, so make sure content is visible + // XXX Can we remove this? It seems to be reproduced in BrowserUI already. + BrowserUI.showContent(); + + return browser; + }, + + openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) { + let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); + return browser ? browser.contentWindow : null; + }, + + openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) { + let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); + return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null; + }, + + zoom: function browser_zoom(aAmount) { + Browser.zoom(aAmount); + }, + + isTabContentWindow: function(aWindow) { + return Browser.browsers.some(function (browser) browser.contentWindow == aWindow); + } +}; + +/** + * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml + */ +var PopupBlockerObserver = { + onUpdatePageReport: function onUpdatePageReport(aEvent) + { + var cBrowser = Browser.selectedBrowser; + if (aEvent.originalTarget != cBrowser) + return; + + if (!cBrowser.pageReport) + return; + + let result = Services.perms.testExactPermission(Browser.selectedBrowser.currentURI, "popup"); + if (result == Ci.nsIPermissionManager.DENY_ACTION) + return; + + // 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 (!cBrowser.pageReport.reported) { + if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) { + var brandShortName = Strings.brand.GetStringFromName("brandShortName"); + var message; + var popupCount = cBrowser.pageReport.length; + + let strings = Strings.browser; + if (popupCount > 1) + message = strings.formatStringFromName("popupWarningMultiple", [brandShortName, popupCount], 2); + else + message = strings.formatStringFromName("popupWarning", [brandShortName], 1); + + var notificationBox = Browser.getNotificationBox(); + var notification = notificationBox.getNotificationWithValue("popup-blocked"); + if (notification) { + notification.label = message; + } + else { + var buttons = [ + { + label: strings.GetStringFromName("popupButtonAllowOnce"), + accessKey: null, + callback: function() { PopupBlockerObserver.showPopupsForSite(); } + }, + { + label: strings.GetStringFromName("popupButtonAlwaysAllow2"), + accessKey: null, + callback: function() { PopupBlockerObserver.allowPopupsForSite(true); } + }, + { + label: strings.GetStringFromName("popupButtonNeverWarn2"), + accessKey: null, + callback: function() { PopupBlockerObserver.allowPopupsForSite(false); } + } + ]; + + const priority = notificationBox.PRIORITY_WARNING_MEDIUM; + notificationBox.appendNotification(message, "popup-blocked", + "", + priority, buttons); + } + } + // Record the fact that we've reported this blocked popup, so we don't + // show it again. + cBrowser.pageReport.reported = true; + } + }, + + allowPopupsForSite: function allowPopupsForSite(aAllow) { + var currentURI = Browser.selectedBrowser.currentURI; + Services.perms.add(currentURI, "popup", aAllow + ? Ci.nsIPermissionManager.ALLOW_ACTION + : Ci.nsIPermissionManager.DENY_ACTION); + + Browser.getNotificationBox().removeCurrentNotification(); + }, + + showPopupsForSite: function showPopupsForSite() { + let uri = Browser.selectedBrowser.currentURI; + let pageReport = Browser.selectedBrowser.pageReport; + if (pageReport) { + for (let i = 0; i < pageReport.length; ++i) { + var popupURIspec = pageReport[i].popupWindowURI.spec; + + // Sometimes the popup URI that we get back from the pageReport + // 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 == "" || !Util.isURLMemorable(popupURIspec) || popupURIspec == uri.spec) + continue; + + let popupFeatures = pageReport[i].popupWindowFeatures; + let popupName = pageReport[i].popupWindowName; + + Browser.addTab(popupURIspec, false, Browser.selectedTab); + } + } + } +}; + +var SessionHistoryObserver = { + observe: function sho_observe(aSubject, aTopic, aData) { + if (aTopic != "browser:purge-session-history") + return; + + let back = document.getElementById("cmd_back"); + back.setAttribute("disabled", "true"); + let forward = document.getElementById("cmd_forward"); + forward.setAttribute("disabled", "true"); + + let urlbar = document.getElementById("urlbar-edit"); + if (urlbar) { + // Clear undo history of the URL bar + urlbar.editor.transactionManager.clear(); + } + } +}; + +var ActivityObserver = { + _inBackground : false, + _notActive : false, + _isDisplayOff : false, + _timeoutID: 0, + observe: function ao_observe(aSubject, aTopic, aData) { + if (aTopic == "application-background") { + this._inBackground = true; + } else if (aTopic == "application-foreground") { + this._inBackground = false; + } else if (aTopic == "system-idle") { + this._notActive = true; + } else if (aTopic == "system-active") { + this._notActive = false; + } else if (aTopic == "system-display-on") { + this._isDisplayOff = false; + } else if (aTopic == "system-display-off") { + this._isDisplayOff = true; + } + let activeTabState = !this._inBackground && !this._notActive && !this._isDisplayOff; + if (this._timeoutID) + clearTimeout(this._timeoutID); + if (Browser.selectedTab.active != activeTabState) { + // On Maemo all backgrounded applications getting portrait orientation + // so if browser had landscape mode then we need timeout in order + // to finish last rotate/paint operation and have nice lookine browser in TS + this._timeoutID = setTimeout(function() { Browser.selectedTab.active = activeTabState; }, activeTabState ? 0 : kSetInactiveStateTimeout); + } + } +}; + +function getNotificationBox(aBrowser) { + return Browser.getNotificationBox(aBrowser); +} + +function showDownloadManager(aWindowContext, aID, aReason) { + PanelUI.show("downloads-container"); + // TODO: select the download with aID +} + +function Tab(aURI, aParams) { + this._id = null; + this._browser = null; + this._notification = null; + this._loading = false; + this._chromeTab = null; + this._metadata = null; + + this.owner = null; + + this.hostChanged = false; + this.state = null; + + // Set to 0 since new tabs that have not been viewed yet are good tabs to + // toss if app needs more memory. + this.lastSelected = 0; + + // aParams is an object that contains some properties for the initial tab + // loading like flags, a referrerURI, a charset or even a postData. + this.create(aURI, aParams || {}); + + // default tabs to inactive (i.e. no display port) + this.active = false; +} + +Tab.prototype = { + get browser() { + return this._browser; + }, + + get notification() { + return this._notification; + }, + + get chromeTab() { + return this._chromeTab; + }, + + get metadata() { + return this._metadata || kDefaultMetadata; + }, + + /** Update browser styles when the viewport metadata changes. */ + updateViewportMetadata: function updateViewportMetadata(aMetadata) { + if (aMetadata && aMetadata.autoScale) { + let scaleRatio = aMetadata.scaleRatio = Browser.getScaleRatio(); + + if ("defaultZoom" in aMetadata && aMetadata.defaultZoom > 0) + aMetadata.defaultZoom *= scaleRatio; + if ("minZoom" in aMetadata && aMetadata.minZoom > 0) + aMetadata.minZoom *= scaleRatio; + if ("maxZoom" in aMetadata && aMetadata.maxZoom > 0) + aMetadata.maxZoom *= scaleRatio; + } + this._metadata = aMetadata; + this.updateViewportSize(); + }, + + /** + * Update browser size when the metadata or the window size changes. + */ + updateViewportSize: function updateViewportSize(width, height) { + /* XXX Viewport resizing disabled because of bug 766142 + + let browser = this._browser; + if (!browser) + return; + + let screenW = width || ContentAreaObserver.width; + let screenH = height || ContentAreaObserver.height; + let viewportW, viewportH; + + let metadata = this.metadata; + if (metadata.autoSize) { + if ("scaleRatio" in metadata) { + viewportW = screenW / metadata.scaleRatio; + viewportH = screenH / metadata.scaleRatio; + } else { + viewportW = screenW; + viewportH = screenH; + } + } else { + viewportW = metadata.width; + viewportH = metadata.height; + + // If (scale * width) < device-width, increase the width (bug 561413). + let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom; + if (maxInitialZoom && viewportW) + viewportW = Math.max(viewportW, screenW / maxInitialZoom); + + let validW = viewportW > 0; + let validH = viewportH > 0; + + if (!validW) + viewportW = validH ? (viewportH * (screenW / screenH)) : Browser.defaultBrowserWidth; + if (!validH) + viewportH = viewportW * (screenH / screenW); + } + + // Make sure the viewport height is not shorter than the window when + // the page is zoomed out to show its full width. + let pageZoomLevel = this.getPageZoomLevel(screenW); + let minScale = this.clampZoomLevel(pageZoomLevel, pageZoomLevel); + viewportH = Math.max(viewportH, screenH / minScale); + + if (browser.contentWindowWidth != viewportW || browser.contentWindowHeight != viewportH) + browser.setWindowSize(viewportW, viewportH); + */ + }, + + restoreViewportPosition: function restoreViewportPosition(aOldWidth, aNewWidth) { + let browser = this._browser; + + // zoom to keep the same portion of the document visible + let oldScale = browser.scale; + let newScale = this.clampZoomLevel(oldScale * aNewWidth / aOldWidth); + let scaleRatio = newScale / oldScale; + + let view = browser.getRootView(); + let pos = view.getPosition(); + browser.fuzzyZoom(newScale, pos.x * scaleRatio, pos.y * scaleRatio); + browser.finishFuzzyZoom(); + }, + + startLoading: function startLoading() { + if (this._loading) throw "Already Loading!"; + this._loading = true; + }, + + endLoading: function endLoading() { + if (!this._loading) throw "Not Loading!"; + this._loading = false; + this.updateFavicon(); + }, + + isLoading: function isLoading() { + return this._loading; + }, + + create: function create(aURI, aParams) { + this._chromeTab = Elements.tabList.addTab(); + this._id = Browser.createTabId(); + let browser = this._createBrowser(aURI, null); + + // Should we fully load the new browser, or wait until later + if ("delayLoad" in aParams && aParams.delayLoad) + return; + + try { + let flags = aParams.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null; + let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; + let charset = "charset" in aParams ? aParams.charset : null; + browser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData); + } catch(e) { + dump("Error: " + e + "\n"); + } + }, + + destroy: function destroy() { + Elements.tabList.removeTab(this._chromeTab); + this._chromeTab = null; + this._destroyBrowser(); + }, + + resurrect: function resurrect() { + let dead = this._browser; + let active = this.active; + + // Hold onto the session store data + let session = { data: dead.__SS_data, extra: dead.__SS_extdata }; + + // We need this data to correctly create and position the new browser + // If this browser is already a zombie, fallback to the session data + let currentURL = dead.__SS_restore ? session.data.entries[0].url : dead.currentURI.spec; + let sibling = dead.nextSibling; + + // Destory and re-create the browser + this._destroyBrowser(); + let browser = this._createBrowser(currentURL, sibling); + if (active) + this.active = true; + + // Reattach session store data and flag this browser so it is restored on select + browser.__SS_data = session.data; + browser.__SS_extdata = session.extra; + browser.__SS_restore = true; + }, + + _createBrowser: function _createBrowser(aURI, aInsertBefore) { + if (this._browser) + throw "Browser already exists"; + + // Create a notification box around the browser. Note this includes + // the input overlay we use to shade content from input events when + // we're intercepting touch input. + let notification = this._notification = document.createElement("notificationbox"); + + let browser = this._browser = document.createElement("browser"); + browser.id = "browser-" + this._id; + browser.setAttribute("class", "viewable-width viewable-height"); + this._chromeTab.linkedBrowser = browser; + + browser.setAttribute("type", "content"); + + let useRemote = Services.prefs.getBoolPref("browser.tabs.remote"); + let useLocal = Util.isLocalScheme(aURI); + browser.setAttribute("remote", (!useLocal && useRemote) ? "true" : "false"); + + // Append the browser to the document, which should start the page load + notification.appendChild(browser); + Elements.browsers.insertBefore(notification, aInsertBefore); + + // stop about:blank from loading + browser.stop(); + + let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; + fl.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL; + + return browser; + }, + + _destroyBrowser: function _destroyBrowser() { + if (this._browser) { + let notification = this._notification; + let browser = this._browser; + browser.active = false; + + this._notification = null; + this._browser = null; + this._loading = false; + + Elements.browsers.removeChild(notification); + } + }, + + /** + * Takes a scale and restricts it based on this tab's zoom limits. + * @param aScale The original scale. + * @param aPageZoomLevel (optional) The zoom-to-fit scale, if known. + * This is a performance optimization to avoid extra calls. + */ + clampZoomLevel: function clampZoomLevel(aScale, aPageZoomLevel) { + let md = this.metadata; + if (!this.allowZoom) { + return (md && md.defaultZoom) + ? md.defaultZoom + : (aPageZoomLevel || this.getPageZoomLevel()); + } + + let browser = this._browser; + let bounded = Util.clamp(aScale, ZoomManager.MIN, ZoomManager.MAX); + + if (md && md.minZoom) + bounded = Math.max(bounded, md.minZoom); + if (md && md.maxZoom) + bounded = Math.min(bounded, md.maxZoom); + + bounded = Math.max(bounded, this.getPageZoomLevel()); + + let rounded = Math.round(bounded * kBrowserViewZoomLevelPrecision) / kBrowserViewZoomLevelPrecision; + return rounded || 1.0; + }, + + /** Record the initial zoom level when a page first loads. */ + resetZoomLevel: function resetZoomLevel() { + this._defaultZoomLevel = this._browser.scale; + }, + + scrolledAreaChanged: function scrolledAreaChanged(firstPaint) { + if (!this._browser) + return; + + if (firstPaint) { + // You only get one shot, do not miss your chance to reflow. + this.updateViewportSize(); + } + + this.updateDefaultZoomLevel(); + }, + + /** + * Recalculate default zoom level when page size changes, and update zoom + * level if we are at default. + */ + updateDefaultZoomLevel: function updateDefaultZoomLevel() { + let browser = this._browser; + if (!browser || !this._firstPaint) + return; + + let isDefault = this.isDefaultZoomLevel(); + this._defaultZoomLevel = this.getDefaultZoomLevel(); + if (isDefault) { + if (browser.scale != this._defaultZoomLevel) { + browser.scale = this._defaultZoomLevel; + } else { + // If the scale level has not changed we want to be sure the content + // render correctly since the page refresh process could have been + // stalled during page load. In this case if the page has the exact + // same width (like the same page, so by doing 'refresh') and the + // page was scrolled the content is just checkerboard at this point + // and this call ensure we render it correctly. + browser.getRootView()._updateCacheViewport(); + } + } else { + // if we are reloading, the page will retain its scale. if it is zoomed + // we need to refresh the viewport so that we do not show checkerboard + browser.getRootView()._updateCacheViewport(); + } + }, + + isDefaultZoomLevel: function isDefaultZoomLevel() { + return this._browser.scale == this._defaultZoomLevel; + }, + + getDefaultZoomLevel: function getDefaultZoomLevel() { + let md = this.metadata; + if (md && md.defaultZoom) + return this.clampZoomLevel(md.defaultZoom); + + let browserWidth = this._browser.getBoundingClientRect().width; + let defaultZoom = browserWidth / this._browser.contentWindowWidth; + return this.clampZoomLevel(defaultZoom); + }, + + /** + * @param aScreenWidth (optional) The width of the browser widget, if known. + * This is a performance optimization to save extra calls to getBoundingClientRect. + * @return The scale at which the browser will be zoomed out to fit the document width. + */ + getPageZoomLevel: function getPageZoomLevel(aScreenWidth) { + let browserW = this._browser.contentDocumentWidth; + if (browserW == 0) + return 1.0; + + let screenW = aScreenWidth || this._browser.getBoundingClientRect().width; + return screenW / browserW; + }, + + get allowZoom() { + return this.metadata.allowZoom && !Util.isURLEmpty(this.browser.currentURI.spec); + }, + + updateThumbnailSource: function updateThumbnailSource() { + this._chromeTab.updateThumbnailSource(this._browser); + }, + + updateFavicon: function updateFavicon() { + this._chromeTab.updateFavicon(this._browser.mIconURL); + }, + + set active(aActive) { + if (!this._browser) + return; + + let notification = this._notification; + let browser = this._browser; + + if (aActive) { + browser.setAttribute("type", "content-primary"); + Elements.browsers.selectedPanel = notification; + browser.active = true; + Elements.tabList.selectedTab = this._chromeTab; + browser.focus(); + } else { + browser.messageManager.sendAsyncMessage("Browser:Blur", { }); + browser.setAttribute("type", "content"); + browser.active = false; + } + }, + + get active() { + if (!this._browser) + return false; + return this._browser.getAttribute("type") == "content-primary"; + }, + + toString: function() { + return "[Tab " + (this._browser ? this._browser.currentURI.spec : "(no browser)") + "]"; + } +}; + +// Helper used to hide IPC / non-IPC differences for rendering to a canvas +function rendererFactory(aBrowser, aCanvas) { + let wrapper = {}; + + if (aBrowser.contentWindow) { + let ctx = aCanvas.getContext("2d"); + let draw = function(browser, aLeft, aTop, aWidth, aHeight, aColor, aFlags) { + ctx.drawWindow(browser.contentWindow, aLeft, aTop, aWidth, aHeight, aColor, aFlags); + let e = document.createEvent("HTMLEvents"); + e.initEvent("MozAsyncCanvasRender", true, true); + aCanvas.dispatchEvent(e); + }; + wrapper.checkBrowser = function(browser) { + return browser.contentWindow; + }; + wrapper.drawContent = function(callback) { + callback(ctx, draw); + }; + } + else { + let ctx = aCanvas.MozGetIPCContext("2d"); + let draw = function(browser, aLeft, aTop, aWidth, aHeight, aColor, aFlags) { + ctx.asyncDrawXULElement(browser, aLeft, aTop, aWidth, aHeight, aColor, aFlags); + }; + wrapper.checkBrowser = function(browser) { + return !browser.contentWindow; + }; + wrapper.drawContent = function(callback) { + callback(ctx, draw); + }; + } + + return wrapper; +}; + +var ContentAreaObserver = { + styles: {}, + + // In desktop mode avoids breaking window dims before + // the desktop window is displayed. + get width() { return window.innerWidth || 1366; }, + get height() { return window.innerHeight || 768; }, + + get contentHeight() { + return this._getContentHeightForWindow(this.height); + }, + + get contentTop () { + return Elements.toolbar.getBoundingClientRect().bottom; + }, + + get viewableHeight() { + return this._getViewableHeightForContent(this.contentHeight); + }, + + get isKeyboardOpened() { return MetroUtils.keyboardVisible; }, + get hasVirtualKeyboard() { return true; }, + + init: function cao_init() { + window.addEventListener("resize", this, false); + + let os = Services.obs; + os.addObserver(this, "metro_softkeyboard_shown", false); + os.addObserver(this, "metro_softkeyboard_hidden", false); + + // Create styles for the following class names. The 'width' and 'height' + // properties of these styles are updated whenever various parts of the UI + // are resized. + // + // * window-width, window-height: The innerWidth/innerHeight of the main + // chrome window. + // * content-width, content-height: The area of the window dedicated to web + // content; this is equal to the innerWidth/Height minus any toolbars + // or similar chrome. + // * viewable-width, viewable-height: The portion of the content area that + // is not obscured by the on-screen keyboard. + let stylesheet = document.styleSheets[0]; + for (let style of ["window-width", "window-height", + "content-height", "content-width", + "viewable-height", "viewable-width"]) { + let index = stylesheet.insertRule("." + style + " {}", stylesheet.cssRules.length); + this.styles[style] = stylesheet.cssRules[index].style; + } + this.update(); + }, + + uninit: function cao_uninit() { + let os = Services.obs; + os.removeObserver(this, "metro_softkeyboard_shown"); + os.removeObserver(this, "metro_softkeyboard_hidden"); + }, + + update: function cao_update (width, height) { + let oldWidth = parseInt(this.styles["window-width"].width); + let oldHeight = parseInt(this.styles["window-height"].height); + + let newWidth = width || this.width; + let newHeight = height || this.height; + + if (newHeight == oldHeight && newWidth == oldWidth) + return; + + this.styles["window-width"].width = newWidth + "px"; + this.styles["window-width"].maxWidth = newWidth + "px"; + this.styles["window-height"].height = newHeight + "px"; + this.styles["window-height"].maxHeight = newHeight + "px"; + + let isStartup = !oldHeight && !oldWidth; + for (let i = Browser.tabs.length - 1; i >=0; i--) { + let tab = Browser.tabs[i]; + let oldContentWindowWidth = tab.browser.contentWindowWidth; + tab.updateViewportSize(newWidth, newHeight); // contentWindowWidth may change here. + + // Don't bother updating the zoom level on startup + if (!isStartup) { + // If the viewport width is still the same, the page layout has not + // changed, so we can keep keep the same content on-screen. + if (tab.browser.contentWindowWidth == oldContentWindowWidth) + tab.restoreViewportPosition(oldWidth, newWidth); + + tab.updateDefaultZoomLevel(); + } + } + + // We want to keep the current focused element into view if possible + if (!isStartup) { + let currentElement = document.activeElement; + let [scrollbox, scrollInterface] = ScrollUtils.getScrollboxFromElement(currentElement); + if (scrollbox && scrollInterface && currentElement && currentElement != scrollbox) { + // retrieve the direct child of the scrollbox + while (currentElement.parentNode != scrollbox) + currentElement = currentElement.parentNode; + + setTimeout(function() { scrollInterface.ensureElementIsVisible(currentElement) }, 0); + } + } + + this.updateContentArea(newWidth, this._getContentHeightForWindow(newHeight)); + this._fire("SizeChanged"); + }, + + updateContentArea: function cao_updateContentArea (width, height) { + let oldHeight = parseInt(this.styles["content-height"].height); + let oldWidth = parseInt(this.styles["content-width"].width); + + let newWidth = width || this.width; + let newHeight = height || this.contentHeight; + + if (newHeight == oldHeight && newWidth == oldWidth) + return; + + this.styles["content-height"].height = newHeight + "px"; + this.styles["content-height"].maxHeight = newHeight + "px"; + this.styles["content-width"].width = newWidth + "px"; + this.styles["content-width"].maxWidth = newWidth + "px"; + + this.updateViewableArea(newWidth, this._getViewableHeightForContent(newHeight)); + this._fire("ContentSizeChanged"); + }, + + updateViewableArea: function cao_updateViewableArea (width, height) { + let oldHeight = parseInt(this.styles["viewable-height"].height); + let oldWidth = parseInt(this.styles["viewable-width"].width); + + let newWidth = width || this.width; + let newHeight = height || this.viewableHeight; + + if (newHeight == oldHeight && newWidth == oldWidth) + return; + + this.styles["viewable-height"].height = newHeight + "px"; + this.styles["viewable-height"].maxHeight = newHeight + "px"; + this.styles["viewable-width"].width = newWidth + "px"; + this.styles["viewable-width"].maxWidth = newWidth + "px"; + + this._fire("ViewableSizeChanged"); + }, + + observe: function cao_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "metro_softkeyboard_shown": + case "metro_softkeyboard_hidden": { + let event = document.createEvent("UIEvents"); + let eventDetails = { + opened: aTopic == "metro_softkeyboard_shown", + details: aData + }; + + event.initUIEvent("KeyboardChanged", true, false, window, eventDetails); + window.dispatchEvent(event); + + this.updateViewableArea(); + break; + } + }; + }, + + handleEvent: function cao_handleEvent(anEvent) { + switch (anEvent.type) { + case 'resize': + if (anEvent.target != window) + return; + + ContentAreaObserver.update(); + break; + } + }, + + _fire: function (aName) { + // setTimeout(callback, 0) to ensure the resize event handler dispatch is finished + setTimeout(function() { + let event = document.createEvent("Events"); + event.initEvent(aName, true, false); + Elements.browsers.dispatchEvent(event); + }, 0); + }, + + _getContentHeightForWindow: function (windowHeight) { + let contextUIHeight = BrowserUI.isTabsOnly ? Elements.toolbar.getBoundingClientRect().bottom : 0; + return windowHeight - contextUIHeight; + }, + + _getViewableHeightForContent: function (contentHeight) { + let keyboardHeight = MetroUtils.keyboardHeight; + return contentHeight - keyboardHeight; + } +}; diff --git a/browser/metro/base/content/browser.xul b/browser/metro/base/content/browser.xul new file mode 100644 index 000000000000..345b70b4634c --- /dev/null +++ b/browser/metro/base/content/browser.xul @@ -0,0 +1,691 @@ + + + + + + + + + + +%globalDTD; + +%browserDTD; + +%brandDTD; + +%prefsDTD; +#ifdef MOZ_SERVICES_SYNC + +%syncDTD; +#endif +]> + + + + + + + + + + + + + +

JavaScript Shell 1.4

Features: autocompletion of property names with Tab, multiline input with Shift+Enter, input history with (Ctrl+) Up/Down, Math, help
Values and functions: ans, print(string), props(object), blink(node), clear(), load(scriptURL), scope(object)
+ +
+ + + + diff --git a/browser/metro/base/content/jsshell/shell.xul b/browser/metro/base/content/jsshell/shell.xul new file mode 100644 index 000000000000..5bb0cd2108b2 --- /dev/null +++ b/browser/metro/base/content/jsshell/shell.xul @@ -0,0 +1,13 @@ + + + + + + diff --git a/browser/metro/base/content/pages/aboutCertError.xhtml b/browser/metro/base/content/pages/aboutCertError.xhtml new file mode 100644 index 000000000000..9e0ab5291fd4 --- /dev/null +++ b/browser/metro/base/content/pages/aboutCertError.xhtml @@ -0,0 +1,241 @@ + + + + %htmlDTD; + + %globalDTD; + + %certerrorDTD; +]> + + + + + &certerror.pagetitle; + + + + + + + + + + + +
+

&certerror.longpagetitle;

+
+ + +
+ + +
+
+

&certerror.introPara1;

+
+ +
+

&certerror.whatShouldIDo.heading;

+
+

&certerror.whatShouldIDo.content;

+ +
+
+ + +
+

&certerror.technical.heading;

+

+

+ +
+

&certerror.expert.heading;

+
+

&certerror.expert.content;

+

&certerror.expert.contentPara2;

+ + +
+
+
+
+ + + + + + diff --git a/browser/metro/base/content/pages/aboutCrash.xhtml b/browser/metro/base/content/pages/aboutCrash.xhtml new file mode 100644 index 000000000000..24a538da36d1 --- /dev/null +++ b/browser/metro/base/content/pages/aboutCrash.xhtml @@ -0,0 +1,32 @@ + + + %htmlDTD; + + %brandDTD; + + %aboutRightsDTD; +]> + + + + + + Oh noooz! You crashed! + + + + + +

Oh noooz! You crashed!

+ +

A crash report is being submitted as you read this, we hope! Check about:crashes for crash reports.

+

(I'm know I'm boring to look at, hopefully someday I'll be pretty and useful and stuff!)

+ + + + + diff --git a/browser/metro/base/content/pages/aboutRights.xhtml b/browser/metro/base/content/pages/aboutRights.xhtml new file mode 100644 index 000000000000..47e41b209253 --- /dev/null +++ b/browser/metro/base/content/pages/aboutRights.xhtml @@ -0,0 +1,95 @@ + + + %htmlDTD; + + %brandDTD; + + %aboutRightsDTD; +]> +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + + + + &rights.pagetitle; + + + + + +

&rights.intro-header;

+ +

&rights.intro;

+ +
    +
  • &rights.intro-point1a;&rights.intro-point1b;&rights.intro-point1c;
  • +# Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded. +# Point 3 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace) +# Point 4 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace) +
  • &rights.intro-point2-a;&rights.intro-point2-b;&rights.intro-point2-c;
  • +
  • &rights.intro-point2.5;
  • +
  • &rights2.intro-point3a;&rights2.intro-point3b;&rights.intro-point3c;
  • +
  • &rights2.intro-point4a;&rights.intro-point4b;&rights.intro-point4c;
  • +
+ + + + + + + diff --git a/browser/metro/base/content/pages/blockedSite.xhtml b/browser/metro/base/content/pages/blockedSite.xhtml new file mode 100644 index 000000000000..c0979ef08f6a --- /dev/null +++ b/browser/metro/base/content/pages/blockedSite.xhtml @@ -0,0 +1,192 @@ + + + + %htmlDTD; + + %globalDTD; + + %brandDTD; + + %blockedSiteDTD; +]> + + + + + + + + + + + + + + + + +
+

&safeb.blocked.phishingPage.title2;

+

&safeb.blocked.malwarePage.title;

+
+ +
+ +
+ + +
+

&safeb.blocked.phishingPage.shortDesc2;

+

&safeb.blocked.malwarePage.shortDesc;

+
+ + +
+

&safeb.blocked.phishingPage.longDesc2;

+

&safeb.blocked.malwarePage.longDesc;

+
+ + +
+ + + +
+
+
+ + + + diff --git a/browser/metro/base/content/pages/fullscreen-video.xhtml b/browser/metro/base/content/pages/fullscreen-video.xhtml new file mode 100644 index 000000000000..48694b16496e --- /dev/null +++ b/browser/metro/base/content/pages/fullscreen-video.xhtml @@ -0,0 +1,154 @@ + + + + + + + + + + + + +
+ + +
+ + +
+

+

+ + +
+ + +
+ &securityOverride.linkText; + +
+
+ + + + +
+ + + + + + diff --git a/browser/metro/base/content/preferences.js b/browser/metro/base/content/preferences.js new file mode 100644 index 000000000000..b6890d194373 --- /dev/null +++ b/browser/metro/base/content/preferences.js @@ -0,0 +1,17 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 PreferencesPanelView = { + init: function pv_init() { + // Run some setup code the first time the panel is shown. + Elements.prefsFlyout.addEventListener("PopupChanged", function onShow(aEvent) { + if (aEvent.detail && aEvent.popup === Elements.prefsFlyout) { + Elements.prefsFlyout.removeEventListener("PopupChanged", onShow, false); + MasterPasswordUI.updatePreference(); + WeaveGlue.init(); + } + }, false); + } +}; diff --git a/browser/metro/base/content/prompt/CaptureDialog.xul b/browser/metro/base/content/prompt/CaptureDialog.xul new file mode 100644 index 000000000000..210c6aef66c9 --- /dev/null +++ b/browser/metro/base/content/prompt/CaptureDialog.xul @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/prompt/alert.xul b/browser/metro/base/content/prompt/alert.xul new file mode 100644 index 000000000000..177615a0c3fb --- /dev/null +++ b/browser/metro/base/content/prompt/alert.xul @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/prompt/confirm.xul b/browser/metro/base/content/prompt/confirm.xul new file mode 100644 index 000000000000..c43ee24e6db0 --- /dev/null +++ b/browser/metro/base/content/prompt/confirm.xul @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/prompt/masterPassword.xul b/browser/metro/base/content/prompt/masterPassword.xul new file mode 100644 index 000000000000..0a2a68d1ac1d --- /dev/null +++ b/browser/metro/base/content/prompt/masterPassword.xul @@ -0,0 +1,45 @@ + + + + + + +%dialog; +%changempDTD; +]> + + + + + + + + + + + + + + + + &setPassword.title; + + + + + + + diff --git a/browser/metro/base/content/prompt/prompt.js b/browser/metro/base/content/prompt/prompt.js new file mode 100644 index 000000000000..eae208634220 --- /dev/null +++ b/browser/metro/base/content/prompt/prompt.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 PromptHelper = { + closeDialog: function(confirm, id) { + let dialog = document.getElementById(id); + if (typeof confirm == "boolean" && dialog.arguments && "defaultButton" in dialog.arguments) + // confirmEx always returns 1 when dismissed with "escape" (bug 345067). + dialog.arguments.result = confirm ? dialog.arguments.defaultButton : 1; + else + dialog.arguments.result = confirm; + dialog.close(); + }, + + // Alert dialog + onCloseAlert: function(dialog) { + if (dialog.arguments) + dialog.arguments.value = document.getElementById("prompt-alert-checkbox").checked; + }, + + // Confirm dialog + closeConfirm: function(confirm) { + this.closeDialog(confirm, "prompt-confirm-dialog"); + }, + + onCloseConfirm: function(dialog) { + if (dialog.arguments && ("checkbox" in dialog.arguments)) + dialog.arguments.checkbox.value = document.getElementById("prompt-confirm-checkbox").checked; + }, + + // Prompt dialog + closePrompt: function(confirm) { + this.closeDialog(confirm, "prompt-prompt-dialog"); + }, + + onClosePrompt: function(dialog) { + if (dialog.arguments) { + dialog.arguments.checkbox.value = document.getElementById("prompt-prompt-checkbox").checked; + dialog.arguments.value.value = document.getElementById("prompt-prompt-textbox").value; + } + }, + + // User / Password dialog + onLoadPassword: function onLoadPassword(dialog) { + let user = document.getElementById('prompt-password-user'); + if (!user.value) + user.focus(); + }, + + closePassword: function(confirm) { + this.closeDialog(confirm, "prompt-password-dialog"); + }, + + onClosePassword: function(dialog) { + if (dialog.arguments) { + dialog.arguments.checkbox.value = document.getElementById("prompt-password-checkbox").checked; + dialog.arguments.user.value = document.getElementById("prompt-password-user").value; + dialog.arguments.password.value = document.getElementById("prompt-password-password").value; + } + }, + + // Select dialog + closeSelect: function(confirm) { + this.closeDialog(confirm, "prompt-select-dialog"); + }, + + onCloseSelect: function(dialog) { + if (dialog.arguments) + dialog.arguments.selection.value = document.getElementById("prompt-select-list").selectedIndex; + } +}; diff --git a/browser/metro/base/content/prompt/prompt.xul b/browser/metro/base/content/prompt/prompt.xul new file mode 100644 index 000000000000..6979fdb74975 --- /dev/null +++ b/browser/metro/base/content/prompt/prompt.xul @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/prompt/promptPassword.xul b/browser/metro/base/content/prompt/promptPassword.xul new file mode 100644 index 000000000000..97c3aeb5fc80 --- /dev/null +++ b/browser/metro/base/content/prompt/promptPassword.xul @@ -0,0 +1,63 @@ + + + + + + + %promptDTD; + %commonDialogDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/prompt/removeMasterPassword.xul b/browser/metro/base/content/prompt/removeMasterPassword.xul new file mode 100644 index 000000000000..95e07b3ba116 --- /dev/null +++ b/browser/metro/base/content/prompt/removeMasterPassword.xul @@ -0,0 +1,43 @@ + + + + + + +%dialog; +%removempDTD; +]> + + + + + + + + + + + + + + + + &removePassword.title; + + + + + + + diff --git a/browser/metro/base/content/prompt/select.xul b/browser/metro/base/content/prompt/select.xul new file mode 100644 index 000000000000..3dd7202cc5e6 --- /dev/null +++ b/browser/metro/base/content/prompt/select.xul @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/prompt/share.xul b/browser/metro/base/content/prompt/share.xul new file mode 100644 index 000000000000..8e5359d64cc4 --- /dev/null +++ b/browser/metro/base/content/prompt/share.xul @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/base/content/sanitize.js b/browser/metro/base/content/sanitize.js new file mode 100644 index 000000000000..56e715977ac5 --- /dev/null +++ b/browser/metro/base/content/sanitize.js @@ -0,0 +1,315 @@ +// -*- 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/. */ + +function Sanitizer() {} +Sanitizer.prototype = { + // warning to the caller: this one may raise an exception (e.g. bug #265028) + clearItem: function (aItemName) + { + if (this.items[aItemName].canClear) + this.items[aItemName].clear(); + }, + + canClearItem: function (aItemName) + { + return this.items[aItemName].canClear; + }, + + _prefDomain: "privacy.item.", + getNameFromPreference: function (aPreferenceName) + { + return aPreferenceName.substr(this._prefDomain.length); + }, + + /** + * Deletes privacy sensitive data in a batch, according to user preferences + * + * @returns null if everything's fine; an object in the form + * { itemName: error, ... } on (partial) failure + */ + sanitize: function () + { + var branch = Services.prefs.getBranch(this._prefDomain); + var errors = null; + for (var itemName in this.items) { + var item = this.items[itemName]; + if ("clear" in item && item.canClear && branch.getBoolPref(itemName)) { + // Some of these clear() may raise exceptions (see bug #265028) + // to sanitize as much as possible, we catch and store them, + // rather than fail fast. + // Callers should check returned errors and give user feedback + // about items that could not be sanitized + try { + item.clear(); + } catch(er) { + if (!errors) + errors = {}; + errors[itemName] = er; + dump("Error sanitizing " + itemName + ": " + er + "\n"); + } + } + } + return errors; + }, + + items: { + // Clear Sync account before passwords so that Sync still has access to the + // credentials to clean up device-specific records on the server. Also + // disable it before wiping history so we don't accidentally sync that. + syncAccount: { + clear: function () + { + WeaveGlue.disconnect(); + }, + + get canClear() + { + return (Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED); + } + }, + + cache: { + clear: function () + { + var cacheService = Cc["@mozilla.org/network/cache-service;1"].getService(Ci.nsICacheService); + try { + cacheService.evictEntries(Ci.nsICache.STORE_ANYWHERE); + } catch(er) {} + + let imageCache = Cc["@mozilla.org/image/cache;1"].getService(Ci.imgICache); + try { + imageCache.clearCache(false); // true=chrome, false=content + } catch(er) {} + }, + + get canClear() + { + return true; + } + }, + + cookies: { + clear: function () + { + var cookieMgr = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + cookieMgr.removeAll(); + }, + + get canClear() + { + return true; + } + }, + + geolocation: { + clear: function () + { + // clear any network geolocation provider sessions + try { + var branch = Services.prefs.getBranch("geo.wifi.access_token."); + branch.deleteBranch(""); + + branch = Services.prefs.getBranch("geo.request.remember."); + branch.deleteBranch(""); + } catch (e) {dump(e);} + }, + + get canClear() + { + return true; + } + }, + + siteSettings: { + clear: function () + { + // Clear site-specific permissions like "Allow this site to open popups" + Services.perms.removeAll(); + + // Clear site-specific settings like page-zoom level + var cps = Cc["@mozilla.org/content-pref/service;1"].getService(Ci.nsIContentPrefService); + cps.removeGroupedPrefs(); + + // Clear "Never remember passwords for this site", which is not handled by + // the permission manager + var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + var hosts = pwmgr.getAllDisabledHosts({}) + for each (var host in hosts) { + pwmgr.setLoginSavingEnabled(host, true); + } + }, + + get canClear() + { + return true; + } + }, + + offlineApps: { + clear: function () + { + var cacheService = Cc["@mozilla.org/network/cache-service;1"].getService(Ci.nsICacheService); + try { + cacheService.evictEntries(Ci.nsICache.STORE_OFFLINE); + } catch(er) {} + }, + + get canClear() + { + return true; + } + }, + + history: { + clear: function () + { + var globalHistory = Cc["@mozilla.org/browser/global-history;2"].getService(Ci.nsIBrowserHistory); + globalHistory.removeAllPages(); + + try { + Services.obs.notifyObservers(null, "browser:purge-session-history", ""); + } + catch (e) { } + + // Clear last URL of the Open Web Location dialog + try { + Services.prefs.clearUserPref("general.open_location.last_url"); + } + catch (e) { } + }, + + get canClear() + { + // bug 347231: Always allow clearing history due to dependencies on + // the browser:purge-session-history notification. (like error console) + return true; + } + }, + + formdata: { + clear: function () + { + //Clear undo history of all searchBars + var windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + var searchBar = windows.getNext().document.getElementById("searchbar"); + if (searchBar) { + searchBar.value = ""; + searchBar.textbox.editor.transactionManager.clear(); + } + } + + var formHistory = Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2); + formHistory.removeAllEntries(); + }, + + get canClear() + { + var formHistory = Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2); + return formHistory.hasEntries; + } + }, + + downloads: { + clear: function () + { + var dlMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); + dlMgr.cleanUp(); + }, + + get canClear() + { + var dlMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); + return dlMgr.canCleanUp; + } + }, + + passwords: { + clear: function () + { + var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + pwmgr.removeAllLogins(); + }, + + get canClear() + { + var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + var count = pwmgr.countLogins("", "", ""); // count all logins + return (count > 0); + } + }, + + sessions: { + clear: function () + { + // clear all auth tokens + var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing); + sdr.logoutAndTeardown(); + + // clear plain HTTP auth sessions + var authMgr = Cc['@mozilla.org/network/http-auth-manager;1'].getService(Ci.nsIHttpAuthManager); + authMgr.clearAll(); + }, + + get canClear() + { + return true; + } + } + } +}; + + +// "Static" members +Sanitizer.prefDomain = "privacy.sanitize."; +Sanitizer.prefShutdown = "sanitizeOnShutdown"; +Sanitizer.prefDidShutdown = "didShutdownSanitize"; + +Sanitizer._prefs = null; +Sanitizer.__defineGetter__("prefs", function() +{ + return Sanitizer._prefs ? Sanitizer._prefs + : Sanitizer._prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService) + .getBranch(Sanitizer.prefDomain); +}); + +/** + * Deletes privacy sensitive data in a batch, optionally showing the + * sanitize UI, according to user preferences + * + * @returns null if everything's fine + * an object in the form { itemName: error, ... } on (partial) failure + */ +Sanitizer.sanitize = function() +{ + return new Sanitizer().sanitize(); +}; + +Sanitizer.onStartup = function() +{ + // we check for unclean exit with pending sanitization + Sanitizer._checkAndSanitize(); +}; + +Sanitizer.onShutdown = function() +{ + // we check if sanitization is needed and perform it + Sanitizer._checkAndSanitize(); +}; + +// this is called on startup and shutdown, to perform pending sanitizations +Sanitizer._checkAndSanitize = function() +{ + const prefs = Sanitizer.prefs; + if (prefs.getBoolPref(Sanitizer.prefShutdown) && + !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) { + // this is a shutdown or a startup after an unclean exit + Sanitizer.sanitize() || // sanitize() returns null on full success + prefs.setBoolPref(Sanitizer.prefDidShutdown, true); + } +}; + + diff --git a/browser/metro/base/content/sync.js b/browser/metro/base/content/sync.js new file mode 100644 index 000000000000..c19178f7d79b --- /dev/null +++ b/browser/metro/base/content/sync.js @@ -0,0 +1,678 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let WeaveGlue = { + setupData: null, + _boundOnEngineSync: null, // Needed to unhook the observers in close(). + _boundOnServiceSync: null, + jpake: null, + _bundle: null, + _loginError: false, + _progressBar: null, + _progressValue: 0, + _progressMax: null, + + init: function init() { + if (this._bundle) { + return; + } + + let service = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + + if (service.ready) { + this._init(); + return; + } + + Services.obs.addObserver(function onReady() { + Services.obs.removeObserver(onReady, "weave:service:ready"); + this._init(); + }.bind(this), "weave:service:ready", false); + + service.ensureLoaded(); + }, + + _init: function () { + this._bundle = Services.strings.createBundle("chrome://browser/locale/sync.properties"); + this._msg = document.getElementById("prefs-messages"); + + this._addListeners(); + + this.setupData = { account: "", password: "" , synckey: "", serverURL: "" }; + + if (Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED) { + // Put the settings UI into a state of "connecting..." if we are going to auto-connect + this._elements.connect.firstChild.disabled = true; + this._elements.connect.setAttribute("title", this._bundle.GetStringFromName("connecting.label")); + + try { + this._elements.device.value = Services.prefs.getCharPref("services.sync.client.name"); + } catch(e) {} + } else if (Weave.Status.login != Weave.LOGIN_FAILED_NO_USERNAME) { + this.loadSetupData(); + } + this._boundOnEngineSync = this.onEngineSync.bind(this); + this._boundOnServiceSync = this.onServiceSync.bind(this); + this._progressBar = document.getElementById("syncsetup-progressbar"); + }, + + abortEasySetup: function abortEasySetup() { + document.getElementById("syncsetup-code1").value = "...."; + document.getElementById("syncsetup-code2").value = "...."; + document.getElementById("syncsetup-code3").value = "...."; + if (!this.jpake) + return; + + this.jpake.abort(); + this.jpake = null; + }, + + _resetScrollPosition: function _resetScrollPosition() { + let scrollboxes = document.getElementsByClassName("syncsetup-scrollbox"); + for (let i = 0; i < scrollboxes.length; i++) { + let sbo = scrollboxes[i].boxObject.QueryInterface(Ci.nsIScrollBoxObject); + try { + sbo.scrollTo(0, 0); + } catch(e) {} + } + }, + + open: function open() { + let container = document.getElementById("syncsetup-container"); + if (!container.hidden) + return; + + // Services.io.offline is lying to us, so we use the NetworkLinkService instead + let nls = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService); + if (!nls.isLinkUp) { + Services.obs.notifyObservers(null, "browser:sync:setup:networkerror", ""); + Services.prompt.alert(window, + this._bundle.GetStringFromName("sync.setup.error.title"), + this._bundle.GetStringFromName("sync.setup.error.network")); + return; + } + + // Clear up any previous JPAKE codes + this.abortEasySetup(); + + // Show the connect UI + container.hidden = false; + document.getElementById("syncsetup-simple").hidden = false; + document.getElementById("syncsetup-waiting").hidden = true; + document.getElementById("syncsetup-fallback").hidden = true; + + DialogUI.pushDialog(this); + + let self = this; + this.jpake = new Weave.JPAKEClient({ + displayPIN: function displayPIN(aPin) { + document.getElementById("syncsetup-code1").value = aPin.slice(0, 4); + document.getElementById("syncsetup-code2").value = aPin.slice(4, 8); + document.getElementById("syncsetup-code3").value = aPin.slice(8); + }, + + onPairingStart: function onPairingStart() { + document.getElementById("syncsetup-simple").hidden = true; + document.getElementById("syncsetup-waiting").hidden = false; + }, + + onComplete: function onComplete(aCredentials) { + self.jpake = null; + + self._progressBar.mode = "determined"; + document.getElementById("syncsetup-waiting-desc").hidden = true; + document.getElementById("syncsetup-waiting-cancel").hidden = true; + document.getElementById("syncsetup-waitingdownload-desc").hidden = false; + document.getElementById("syncsetup-waiting-close").hidden = false; + Services.obs.addObserver(self._boundOnEngineSync, "weave:engine:sync:finish", false); + Services.obs.addObserver(self._boundOnEngineSync, "weave:engine:sync:error", false); + Services.obs.addObserver(self._boundOnServiceSync, "weave:service:sync:finish", false); + Services.obs.addObserver(self._boundOnServiceSync, "weave:service:sync:error", false); + self.setupData = aCredentials; + self.connect(); + }, + + onAbort: function onAbort(aError) { + self.jpake = null; + + if (aError == "jpake.error.userabort" || container.hidden) { + Services.obs.notifyObservers(null, "browser:sync:setup:userabort", ""); + return; + } + + // Automatically go to manual setup if we couldn't acquire a channel. + let brandShortName = Strings.brand.GetStringFromName("brandShortName"); + let tryAgain = self._bundle.GetStringFromName("sync.setup.tryagain"); + let manualSetup = self._bundle.GetStringFromName("sync.setup.manual"); + let buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) + + (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_2); + + let button = Services.prompt.confirmEx(window, + self._bundle.GetStringFromName("sync.setup.error.title"), + self._bundle.formatStringFromName("sync.setup.error.nodata", [brandShortName], 1), + buttonFlags, tryAgain, manualSetup, null, "", {}); + switch (button) { + case 0: + // we have to build a new JPAKEClient here rather than reuse the old one + container.hidden = true; + self.open(); + break; + case 1: + self.openManual(); + break; + case 2: + default: + self.close(); + break; + } + } + }); + this.jpake.receiveNoPIN(); + }, + + openManual: function openManual() { + this.abortEasySetup(); + + // Reset the scroll since the previous page might have been scrolled + this._resetScrollPosition(); + + document.getElementById("syncsetup-simple").hidden = true; + document.getElementById("syncsetup-waiting").hidden = true; + document.getElementById("syncsetup-fallback").hidden = false; + + // Push the current setup data into the UI + if (this.setupData && "account" in this.setupData) { + this._elements.account.value = this.setupData.account; + this._elements.password.value = this.setupData.password; + let pp = this.setupData.synckey; + if (Weave.Utils.isPassphrase(pp)) + pp = Weave.Utils.hyphenatePassphrase(pp); + this._elements.synckey.value = pp; + if (this.setupData.serverURL && this.setupData.serverURL.length) { + this._elements.usecustomserver.checked = true; + this._elements.customserver.disabled = false; + this._elements.customserver.value = this.setupData.serverURL; + } else { + this._elements.usecustomserver.checked = false; + this._elements.customserver.disabled = true; + this._elements.customserver.value = ""; + } + } + + this.canConnect(); + }, + + onEngineSync: function onEngineSync(subject, topic, data) { + // The Clients engine syncs first. At this point we don't necessarily know + // yet how many engines will be enabled, so we'll ignore the Clients engine + // and evaluate how many engines are enabled when the first "real" engine + // syncs. + if (data == 'clients') { + return; + } + if (this._progressMax == null) { + this._progressMax = Weave.Service.engineManager.getEnabled().length; + this._progressBar.max = this._progressMax; + } + this._progressValue += 1; + this._progressBar.setAttribute("value", this._progressValue); + }, + + onServiceSync: function onServiceSync() { + this.close(); + }, + + close: function close() { + try { + Services.obs.removeObserver(this._boundOnEngineSync, "weave:engine:sync:finish"); + Services.obs.removeObserver(this._boundOnEngineSync, "weave:engine:sync:error"); + Services.obs.removeObserver(this._boundOnServiceSync, "weave:service:sync:finish"); + Services.obs.removeObserver(this._boundOnServiceSync, "weave:service:sync:error"); + } + catch(e) { + // Observers weren't registered because we never got as far as onComplete. + } + + if (this.jpake) + this.abortEasySetup(); + + // Reset the scroll since the previous page might have been scrolled + this._resetScrollPosition(); + + // Save current setup data + this.setupData = { + account: this._elements.account.value.trim(), + password: this._elements.password.value.trim(), + synckey: Weave.Utils.normalizePassphrase(this._elements.synckey.value.trim()), + serverURL: this._validateServer(this._elements.customserver.value.trim()) + }; + + // Clear the UI so it's ready for next time + this._elements.account.value = ""; + this._elements.password.value = ""; + this._elements.synckey.value = ""; + this._elements.usecustomserver.checked = false; + this._elements.customserver.disabled = true; + this._elements.customserver.value = ""; + document.getElementById("syncsetup-waiting-desc").hidden = false; + document.getElementById("syncsetup-waiting-cancel").hidden = false; + document.getElementById("syncsetup-waitingdownload-desc").hidden = true; + document.getElementById("syncsetup-waiting-close").hidden = true; + this._progressMax = null; + this._progressValue = 0; + this._progressBar.max = 0; + this._progressBar.value = 0; + this._progressBar.mode = "undetermined"; + + // Close the connect UI + document.getElementById("syncsetup-container").hidden = true; + DialogUI.popDialog(); + }, + + toggleCustomServer: function toggleCustomServer() { + let useCustomServer = this._elements.usecustomserver.checked; + this._elements.customserver.disabled = !useCustomServer; + if (!useCustomServer) + this._elements.customserver.value = ""; + }, + + canConnect: function canConnect() { + let account = this._elements.account.value; + let password = this._elements.password.value; + let synckey = this._elements.synckey.value; + + let disabled = !(account && password && synckey); + document.getElementById("syncsetup-button-connect").disabled = disabled; + }, + + showDetails: function showDetails() { + // Show the connect UI detail settings + let show = this._elements.sync.collapsed; + this._elements.details.checked = show; + this._elements.sync.collapsed = !show; + this._elements.device.collapsed = !show; + this._elements.disconnect.collapsed = !show; + }, + + tryConnect: function login() { + // If Sync is not configured, simply show the setup dialog + if (this._loginError || Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED) { + this.open(); + return; + } + + // No setup data, do nothing + if (!this.setupData) + return; + + if (this.setupData.serverURL && this.setupData.serverURL.length) + Weave.Service.serverURL = this.setupData.serverURL; + + // We might still be in the middle of a sync from before Sync was disabled, so + // let's force the UI into a state that the Sync code feels comfortable + this.observe(null, "", ""); + + // Now try to re-connect. If successful, this will reset the UI into the + // correct state automatically. + Weave.Service.login(Weave.Service.identity.username, this.setupData.password, this.setupData.synckey); + }, + + connect: function connect(aSetupData) { + // Use setup data to pre-configure manual fields + if (aSetupData) + this.setupData = aSetupData; + + // Cause the Sync system to reset internals if we seem to be switching accounts + if (this.setupData.account != Weave.Service.identity.account) + Weave.Service.startOver(); + + // Remove any leftover connection error string + this._elements.connect.removeAttribute("desc"); + + // Reset the custom server URL, if we have one + if (this.setupData.serverURL && this.setupData.serverURL.length) + Weave.Service.serverURL = this.setupData.serverURL; + + // Sync will use the account value and munge it into a username, as needed + Weave.Service.identity.account = this.setupData.account; + Weave.Service.identity.basicPassword = this.setupData.password; + Weave.Service.identity.syncKey = this.setupData.synckey; + Weave.Service.persistLogin(); + Weave.Svc.Obs.notify("weave:service:setup-complete"); + setTimeout(function () { Weave.Service.sync(); }, 0); + }, + + disconnect: function disconnect() { + // Save credentials for undo + let undoData = this.setupData; + + // Remove all credentials + this.setupData = null; + Weave.Service.startOver(); + + let message = this._bundle.GetStringFromName("notificationDisconnect.label"); + let button = this._bundle.GetStringFromName("notificationDisconnect.button"); + let buttons = [ { + label: button, + accessKey: "", + callback: function() { WeaveGlue.connect(undoData); } + } ]; + this.showMessage(message, "undo-disconnect", buttons); + + // Hide the notification when the panel is changed or closed. + let panel = document.getElementById("prefs-container"); + panel.addEventListener("ToolPanelHidden", function onHide(aEvent) { + panel.removeEventListener(aEvent.type, onHide, false); + let notification = WeaveGlue._msg.getNotificationWithValue("undo-disconnect"); + if (notification) + notification.close(); + }, false); + + Weave.Service.logout(); + }, + + sync: function sync() { + Weave.Service.sync(); + }, + + _addListeners: function _addListeners() { + let topics = ["weave:service:setup-complete", + "weave:service:sync:start", "weave:service:sync:finish", + "weave:service:sync:error", "weave:service:login:start", + "weave:service:login:finish", "weave:service:login:error", + "weave:ui:login:error", + "weave:service:logout:finish"]; + + // For each topic, add WeaveGlue the observer + topics.forEach(function(topic) { + Services.obs.addObserver(WeaveGlue, topic, false); + }); + + // Remove them on unload + addEventListener("unload", function() { + topics.forEach(function(topic) { + Services.obs.removeObserver(WeaveGlue, topic, false); + }); + }, false); + }, + + get _elements() { + // Do a quick test to see if the options exist yet + let syncButton = document.getElementById("sync-syncButton"); + if (syncButton == null) + return null; + + // Get all the setting nodes from the add-ons display + let elements = {}; + let setupids = ["account", "password", "synckey", "usecustomserver", "customserver"]; + setupids.forEach(function(id) { + elements[id] = document.getElementById("syncsetup-" + id); + }); + + let settingids = ["device", "connect", "connected", "disconnect", "sync", "details", "pairdevice"]; + settingids.forEach(function(id) { + elements[id] = document.getElementById("sync-" + id); + }); + + // Replace the getter with the collection of settings + delete this._elements; + return this._elements = elements; + }, + + observe: function observe(aSubject, aTopic, aData) { + // Make sure we're online when connecting/syncing + Util.forceOnline(); + + // Can't do anything before settings are loaded + if (this._elements == null) + return; + + // Make some aliases + let connect = this._elements.connect; + let connected = this._elements.connected; + let details = this._elements.details; + let device = this._elements.device; + let disconnect = this._elements.disconnect; + let sync = this._elements.sync; + let pairdevice = this._elements.pairdevice; + + // Show what went wrong with login if necessary + if (aTopic == "weave:ui:login:error") { + this._loginError = true; + connect.setAttribute("desc", Weave.Utils.getErrorString(Weave.Status.login)); + } else { + connect.removeAttribute("desc"); + } + + if (aTopic == "weave:service:login:finish") { + this._loginError = false; + // Init the setup data if we just logged in + if (!this.setupData) + this.loadSetupData(); + } + + let isConfigured = (!this._loginError && Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED); + + connect.collapsed = isConfigured; + connected.collapsed = !isConfigured; + + if (!isConfigured) { + connect.setAttribute("title", this._bundle.GetStringFromName("notconnected.label")); + connect.firstChild.disabled = false; + details.checked = false; + sync.collapsed = true; + device.collapsed = true; + disconnect.collapsed = true; + } + + // Check the lock on a timeout because it's set just after notifying + setTimeout(function(self) { + // Prevent certain actions when the service is locked + if (Weave.Service.locked) { + connect.firstChild.disabled = true; + sync.firstChild.disabled = true; + + if (aTopic == "weave:service:login:start") + connect.setAttribute("title", self._bundle.GetStringFromName("connecting.label")); + + if (aTopic == "weave:service:sync:start") + sync.setAttribute("title", self._bundle.GetStringFromName("lastSyncInProgress2.label")); + } else { + connect.firstChild.disabled = false; + sync.firstChild.disabled = false; + } + }, 0, this); + + // Dynamically generate some strings + let accountStr = this._bundle.formatStringFromName("account.label", [Weave.Service.identity.account], 1); + disconnect.setAttribute("title", accountStr); + + // Show the day-of-week and time (HH:MM) of last sync + let lastSync = Weave.Svc.Prefs.get("lastSync"); + if (lastSync != null) { + let syncDate = new Date(lastSync).toLocaleFormat("%a %H:%M"); + let dateStr = this._bundle.formatStringFromName("lastSync2.label", [syncDate], 1); + sync.setAttribute("title", dateStr); + } + + // Check for a storage format update, update the user and load the Sync update page + if (aTopic =="weave:service:sync:error") { + let clientOutdated = false, remoteOutdated = false; + if (Weave.Status.sync == Weave.VERSION_OUT_OF_DATE) { + clientOutdated = true; + } else if (Weave.Status.sync == Weave.DESKTOP_VERSION_OUT_OF_DATE) { + remoteOutdated = true; + } else if (Weave.Status.service == Weave.SYNC_FAILED_PARTIAL) { + // Some engines failed, check for per-engine compat + for (let [engine, reason] in Iterator(Weave.Status.engines)) { + clientOutdated = clientOutdated || reason == Weave.VERSION_OUT_OF_DATE; + remoteOutdated = remoteOutdated || reason == Weave.DESKTOP_VERSION_OUT_OF_DATE; + } + } + + if (clientOutdated || remoteOutdated) { + let brand = Services.strings.createBundle("chrome://branding/locale/brand.properties"); + let brandName = brand.GetStringFromName("brandShortName"); + + let type = clientOutdated ? "client" : "remote"; + let message = this._bundle.GetStringFromName("sync.update." + type); + message = message.replace("#1", brandName); + message = message.replace("#2", Services.appinfo.version); + let title = this._bundle.GetStringFromName("sync.update.title") + let button = this._bundle.GetStringFromName("sync.update.button") + let close = this._bundle.GetStringFromName("sync.update.close") + + let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING; + let choice = Services.prompt.confirmEx(window, title, message, flags, button, close, null, null, {}); + if (choice == 0) + Browser.addTab("https://services.mozilla.com/update/", true, Browser.selectedTab); + } + } + + device.value = Weave.Service.clientsEngine.localName || ""; + }, + + changeName: function changeName(aInput) { + // Make sure to update to a modified name, e.g., empty-string -> default + Weave.Service.clientsEngine.localName = aInput.value; + aInput.value = Weave.Service.clientsEngine.localName; + }, + + showMessage: function showMessage(aMsg, aValue, aButtons) { + let notification = this._msg.getNotificationWithValue(aValue); + if (notification) + return; + + this._msg.appendNotification(aMsg, aValue, "", this._msg.PRIORITY_WARNING_LOW, aButtons); + }, + + _validateServer: function _validateServer(aURL) { + let uri = Weave.Utils.makeURI(aURL); + + if (!uri && aURL) + uri = Weave.Utils.makeURI("https://" + aURL); + + if (!uri) + return ""; + return uri.spec; + }, + + openTutorial: function _openTutorial() { + WeaveGlue.close(); + + let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter); + let url = formatter.formatURLPref("app.sync.tutorialURL"); + BrowserUI.newTab(url, Browser.selectedTab); + }, + + loadSetupData: function _loadSetupData() { + this.setupData = {}; + this.setupData.account = Weave.Service.identity.account || ""; + this.setupData.password = Weave.Service.identity.basicPassword || ""; + this.setupData.synckey = Weave.Service.identity.syncKey || ""; + + let serverURL = Weave.Service.serverURL; + let defaultPrefs = Services.prefs.getDefaultBranch(null); + if (serverURL == defaultPrefs.getCharPref("services.sync.serverURL")) + serverURL = ""; + this.setupData.serverURL = serverURL; + } +}; + + +const PIN_PART_LENGTH = 4; + +let SyncPairDevice = { + jpake: null, + + open: function open() { + this.code1.setAttribute("maxlength", PIN_PART_LENGTH); + this.code2.setAttribute("maxlength", PIN_PART_LENGTH); + this.code3.setAttribute("maxlength", PIN_PART_LENGTH); + this.nextFocusEl = {code1: this.code2, + code2: this.code3, + code3: this.connectbutton}; + + document.getElementById("syncpair-container").hidden = false; + DialogUI.pushDialog(this); + this.code1.focus(); + + // 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.SyncScheduler.scheduleNextSync(0); + }, + + close: function close() { + this.code1.value = this.code2.value = this.code3.value = ""; + this.code1.disabled = this.code2.disabled = this.code3.disabled = false; + this.connectbutton.disabled = true; + if (this.jpake) { + this.jpake.abort(); + this.jpake = null; + } + document.getElementById("syncpair-container").hidden = true; + DialogUI.popDialog(); + }, + + onTextBoxInput: function onTextBoxInput(textbox) { + if (textbox && textbox.value.length == PIN_PART_LENGTH) { + let name = textbox.id.split("-")[1]; + this.nextFocusEl[name].focus(); + } + + this.connectbutton.disabled = + !(this.code1.value.length == PIN_PART_LENGTH && + this.code2.value.length == PIN_PART_LENGTH && + this.code3.value.length == PIN_PART_LENGTH); + }, + + connect: function connect() { + let self = this; + let jpake = this.jpake = 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}; + jpake.sendAndComplete(credentials); + }, + onComplete: function onComplete() { + self.jpake = null; + self.close(); + + // Schedule a Sync for soonish to fetch the data uploaded by the + // device with which we just paired. + Weave.SyncScheduler.scheduleNextSync(Weave.SyncScheduler.activeInterval); + }, + onAbort: function onAbort(error) { + self.jpake = null; + + // Aborted by user, ignore. + if (error == Weave.JPAKE_ERROR_USERABORT) { + return; + } + + self.code1.value = self.code2.value = self.code3.value = ""; + self.code1.disabled = self.code2.disabled = self.code3.disabled = false; + self.code1.focus(); + } + }); + this.code1.disabled = this.code2.disabled = this.code3.disabled = true; + this.connectbutton.disabled = true; + + let pin = this.code1.value + this.code2.value + this.code3.value; + let expectDelay = false; + jpake.pairWithPIN(pin, expectDelay); + } +}; +["code1", "code2", "code3", "connectbutton"].forEach(function (id) { + XPCOMUtils.defineLazyGetter(SyncPairDevice, id, function() { + return document.getElementById("syncpair-" + id); + }); +}); diff --git a/browser/metro/base/content/video.js b/browser/metro/base/content/video.js new file mode 100644 index 000000000000..7f2aecee1ab0 --- /dev/null +++ b/browser/metro/base/content/video.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/. */ + +var FullScreenVideo = { + _tab: null, + + init: function fsv_init() { + // These come in from content.js, currently we only use Start. + messageManager.addMessageListener("Browser:FullScreenVideo:Start", this.show.bind(this)); + messageManager.addMessageListener("Browser:FullScreenVideo:Close", this.hide.bind(this)); + messageManager.addMessageListener("Browser:FullScreenVideo:Play", this.play.bind(this)); + messageManager.addMessageListener("Browser:FullScreenVideo:Pause", this.pause.bind(this)); + + // If the screen supports brightness locks, we will utilize that, see checkBrightnessLocking() + try { + this.screen = null; + let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(Ci.nsIScreenManager); + this.screen = screenManager.primaryScreen; + } + catch (e) {} // The screen does not support brightness locks + }, + + play: function() { + this.playing = true; + this.checkBrightnessLocking(); + }, + + pause: function() { + this.playing = false; + this.checkBrightnessLocking(); + }, + + checkBrightnessLocking: function() { + // screen manager support for metro: bug 776113 + var shouldLock = !!this.screen && !!window.fullScreen && !!this.playing; + var locking = !!this.brightnessLocked; + if (shouldLock == locking) + return; + + if (shouldLock) + this.screen.lockMinimumBrightness(this.screen.BRIGHTNESS_FULL); + else + this.screen.unlockMinimumBrightness(this.screen.BRIGHTNESS_FULL); + this.brightnessLocked = shouldLock; + }, + + show: function fsv_show() { + this.createTab(); + this.checkBrightnessLocking(); + }, + + hide: function fsv_hide() { + this.checkBrightnessLocking(); + this.destroyTab(); + }, + + createTab: function fsv_createBrowser() { + this._tab = BrowserUI.newTab("chrome://browser/content/fullscreen-video.xhtml"); + }, + + destroyTab: function fsv_destroyBrowser() { + Browser.closeTab(this._tab); + this._tab = null; + } +}; diff --git a/browser/metro/base/jar.mn b/browser/metro/base/jar.mn new file mode 100644 index 000000000000..28da25150485 --- /dev/null +++ b/browser/metro/base/jar.mn @@ -0,0 +1,104 @@ +#filter substitution +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +chrome.jar: +% content browser %content/ + + content/aboutCertError.xhtml (content/pages/aboutCertError.xhtml) +* content/aboutRights.xhtml (content/pages/aboutRights.xhtml) + content/aboutCrash.xhtml (content/pages/aboutCrash.xhtml) + content/blockedSite.xhtml (content/pages/blockedSite.xhtml) + content/fullscreen-video.xhtml (content/pages/fullscreen-video.xhtml) + content/netError.xhtml (content/pages/netError.xhtml) + +* content/bindings/bindings.xml (content/bindings/bindings.xml) + content/bindings/tabs.xml (content/bindings/tabs.xml) + content/bindings/toggleswitch.xml (content/bindings/toggleswitch.xml) +* content/bindings/browser.xml (content/bindings/browser.xml) + content/bindings/browser.js (content/bindings/browser.js) + content/bindings/downloads.xml (content/bindings/downloads.xml) + content/bindings/console.xml (content/bindings/console.xml) + content/bindings/dialog.xml (content/bindings/dialog.xml) + content/bindings/pageaction.xml (content/bindings/pageaction.xml) + content/bindings/arrowbox.xml (content/bindings/arrowbox.xml) + content/bindings/grid.xml (content/bindings/grid.xml) + content/bindings/autocomplete.xml (content/bindings/autocomplete.xml) + content/bindings/appbar.xml (content/bindings/appbar.xml) + content/bindings/flyoutpanel.xml (content/bindings/flyoutpanel.xml) + content/bindings/selectionoverlay.xml (content/bindings/selectionoverlay.xml) + + content/prompt/CaptureDialog.xul (content/prompt/CaptureDialog.xul) + content/prompt/alert.xul (content/prompt/alert.xul) + content/prompt/confirm.xul (content/prompt/confirm.xul) + content/prompt/prompt.xul (content/prompt/prompt.xul) + content/prompt/promptPassword.xul (content/prompt/promptPassword.xul) + content/prompt/select.xul (content/prompt/select.xul) + content/prompt/prompt.js (content/prompt/prompt.js) + content/prompt/share.xul (content/prompt/share.xul) + content/prompt/masterPassword.xul (content/prompt/masterPassword.xul) + content/prompt/removeMasterPassword.xul (content/prompt/removeMasterPassword.xul) + +* content/helperui/AlertsHelper.js (content/helperui/AlertsHelper.js) + content/helperui/CaptureDialog.js (content/helperui/CaptureDialog.js) + content/helperui/CapturePickerUI.js (content/helperui/CapturePickerUI.js) + content/helperui/CharsetMenu.js (content/helperui/CharsetMenu.js) + content/helperui/IdentityUI.js (content/helperui/IdentityUI.js) + content/helperui/IndexedDB.js (content/helperui/IndexedDB.js) + content/helperui/MasterPasswordUI.js (content/helperui/MasterPasswordUI.js) + content/helperui/MenuUI.js (content/helperui/MenuUI.js) + content/helperui/OfflineApps.js (content/helperui/OfflineApps.js) + content/helperui/SelectHelperUI.js (content/helperui/SelectHelperUI.js) + content/helperui/SelectionHelperUI.js (content/helperui/SelectionHelperUI.js) + content/helperui/SharingUI.js (content/helperui/SharingUI.js) + content/helperui/FormHelperUI.js (content/helperui/FormHelperUI.js) + content/helperui/FindHelperUI.js (content/helperui/FindHelperUI.js) + + content/contenthandlers/ContextMenuHandler.js (content/contenthandlers/ContextMenuHandler.js) + content/contenthandlers/PluginCTPHandler.js (content/contenthandlers/PluginCTPHandler.js) + content/contenthandlers/SelectionHandler.js (content/contenthandlers/SelectionHandler.js) + content/contenthandlers/FormHelper.js (content/contenthandlers/FormHelper.js) + content/contenthandlers/FindHandler.js (content/contenthandlers/FindHandler.js) + content/contenthandlers/ViewportHandler.js (content/contenthandlers/ViewportHandler.js) + content/contenthandlers/ConsoleAPIObserver.js (content/contenthandlers/ConsoleAPIObserver.js) +* content/contenthandlers/Content.js (content/contenthandlers/Content.js) + + content/BrowserTouchHandler.js (content/BrowserTouchHandler.js) +* content/WebProgress.js (content/WebProgress.js) + content/config.xul (content/config.xul) + content/config.js (content/config.js) +* content/browser.xul (content/browser.xul) +* content/browser.js (content/browser.js) +* content/browser-ui.js (content/browser-ui.js) +* content/browser-scripts.js (content/browser-scripts.js) +* content/ContextCommands.js (content/ContextCommands.js) +* content/PageActions.js (content/PageActions.js) + content/commandUtil.js (content/commandUtil.js) + content/appbar.js (content/appbar.js) + content/shell.xul (content/jsshell/shell.xul) + content/shell.html (content/jsshell/shell.html) + content/browser.css (content/browser.css) + content/cursor.css (content/cursor.css) +% content branding %content/branding/ + content/sanitize.js (content/sanitize.js) +* content/input.js (content/input.js) +* content/Util.js (content/Util.js) + content/bookmarks.js (content/bookmarks.js) +* content/preferences.js (content/preferences.js) + content/exceptions.js (content/exceptions.js) +* content/downloads.js (content/downloads.js) + content/history.js (content/history.js) + content/TopSites.js (content/TopSites.js) + content/console.js (content/console.js) + content/AnimatedZoom.js (content/AnimatedZoom.js) + content/LoginManagerChild.js (content/LoginManagerChild.js) + content/video.js (content/video.js) +#ifdef MOZ_SERVICES_SYNC + content/sync.js (content/sync.js) + content/RemoteTabs.js (content/RemoteTabs.js) +#endif + +% override chrome://global/content/config.xul chrome://browser/content/config.xul +% override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml diff --git a/browser/metro/base/tests/Makefile.in b/browser/metro/base/tests/Makefile.in new file mode 100644 index 000000000000..ee15013618ab --- /dev/null +++ b/browser/metro/base/tests/Makefile.in @@ -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/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = @relativesrcdir@ + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/config/rules.mk + +_BROWSER_FILES = \ + head.js \ + browser_test.js \ + browser_canonizeURL.js \ + browser_context_ui.js \ + browser_onscreen_keyboard.js \ + browser_onscreen_keyboard.html \ + browser_remotetabs.js \ + browser_downloads.js \ + browser_plugin_input.html \ + browser_plugin_input_mouse.js \ + browser_plugin_input_keyboard.js \ + $(NULL) + +libs:: $(_BROWSER_FILES) + $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/metro/ diff --git a/browser/metro/base/tests/addons/browser_install1_1/bootstrap.js b/browser/metro/base/tests/addons/browser_install1_1/bootstrap.js new file mode 100644 index 000000000000..7b86e419a3b3 --- /dev/null +++ b/browser/metro/base/tests/addons/browser_install1_1/bootstrap.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function install(data, reason) {} +function startup(data, reason) {} +function shutdown(data, reason) {} +function uninstall(data, reason) {} + diff --git a/browser/metro/base/tests/addons/browser_install1_1/install.rdf b/browser/metro/base/tests/addons/browser_install1_1/install.rdf new file mode 100644 index 000000000000..0825d11aa0cb --- /dev/null +++ b/browser/metro/base/tests/addons/browser_install1_1/install.rdf @@ -0,0 +1,24 @@ + + + + + + addon1@tests.mozilla.org + 1.0 + http://example.com/browser/mobile/chrome/tests/browser_upgrade.rdf + true + + + + toolkit@mozilla.org + 0 + * + + + + + Install Tests + + + diff --git a/browser/metro/base/tests/addons/browser_install1_2/install.rdf b/browser/metro/base/tests/addons/browser_install1_2/install.rdf new file mode 100644 index 000000000000..945afb22b014 --- /dev/null +++ b/browser/metro/base/tests/addons/browser_install1_2/install.rdf @@ -0,0 +1,22 @@ + + + + + + addon2@tests.mozilla.org + 2.0 + + + + toolkit@mozilla.org + 0 + * + + + + + Install Tests 2 + + + diff --git a/browser/metro/base/tests/addons/browser_install1_3/install.rdf b/browser/metro/base/tests/addons/browser_install1_3/install.rdf new file mode 100644 index 000000000000..2c10f4921628 --- /dev/null +++ b/browser/metro/base/tests/addons/browser_install1_3/install.rdf @@ -0,0 +1,23 @@ + + + + + + addon1@tests.mozilla.org + 3.0 + http://example.com/browser/mobile/chrome/tests/browser_upgrade.rdf + + + + toolkit@mozilla.org + 0 + * + + + + + Install Tests + + + diff --git a/browser/metro/base/tests/addons/browser_locale1/boostrap.js b/browser/metro/base/tests/addons/browser_locale1/boostrap.js new file mode 100644 index 000000000000..7b86e419a3b3 --- /dev/null +++ b/browser/metro/base/tests/addons/browser_locale1/boostrap.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function install(data, reason) {} +function startup(data, reason) {} +function shutdown(data, reason) {} +function uninstall(data, reason) {} + diff --git a/browser/metro/base/tests/addons/browser_locale1/chrome.manifest b/browser/metro/base/tests/addons/browser_locale1/chrome.manifest new file mode 100644 index 000000000000..a909200d5b02 --- /dev/null +++ b/browser/metro/base/tests/addons/browser_locale1/chrome.manifest @@ -0,0 +1,4 @@ +locale mozapps te-st chrome # locale +locale browser te-st chrome # duplicate locale +locale browser te-st-a chrome # second locale +locale branding te-st-3 chrome # wrong component diff --git a/browser/metro/base/tests/addons/browser_locale1/install.rdf b/browser/metro/base/tests/addons/browser_locale1/install.rdf new file mode 100644 index 000000000000..ba92a4581869 --- /dev/null +++ b/browser/metro/base/tests/addons/browser_locale1/install.rdf @@ -0,0 +1,24 @@ + + + + + + locale1@tests.mozilla.org + 1.0 + 8 + true + + + + toolkit@mozilla.org + 0 + * + + + + + Test Locale + + + diff --git a/browser/metro/base/tests/browser_canonizeURL.js b/browser/metro/base/tests/browser_canonizeURL.js new file mode 100644 index 000000000000..1528a78e8c03 --- /dev/null +++ b/browser/metro/base/tests/browser_canonizeURL.js @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function test() { + let testcases = [ + ["example", {}, "example"], + ["example", {ctrlKey: true}, "http://www.example.com/"], + ["example.org", {ctrlKey: true}, "example.org"], + ["example", {shiftKey: true}, "http://www.example.net/"], + ["example", {shiftKey: true, ctrlKey: true}, "http://www.example.org/"], + [" example ", {ctrlKey: true}, "http://www.example.com/"], + [" example/a ", {ctrlKey: true}, "http://www.example.com/a"] + ]; + for (let [input, modifiers, result] of testcases) { + is(BrowserUI._canonizeURL(input, modifiers), result, input + " -> " + result); + } +} diff --git a/browser/metro/base/tests/browser_context_ui.js b/browser/metro/base/tests/browser_context_ui.js new file mode 100644 index 000000000000..6b3700c075a3 --- /dev/null +++ b/browser/metro/base/tests/browser_context_ui.js @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function test() { + runTests(); +} + +gTests.push({ + desc: "Context UI on about:start", + run: function testAboutStart() { + yield addTab("about:start"); + is(StartUI.isVisible, true, "Start UI is displayed on about:start"); + is(ContextUI.isVisible, true, "Toolbar is displayed on about:start"); + is(ContextUI.isExpanded, false, "Tab bar is not displayed initially"); + is(Elements.appbar.isShowing, false, "Appbar is not displayed initially"); + + // toggle on + doEdgeUIGesture(); + is(ContextUI.isVisible, true, "Toolbar is still visible after one swipe"); + is(ContextUI.isExpanded, true, "Tab bar is visible after one swipe"); + is(Elements.appbar.isShowing, true, "Appbar is visible after one swipe"); + + // toggle off + doEdgeUIGesture(); + is(ContextUI.isVisible, true, "Toolbar is still visible after second swipe"); + is(ContextUI.isExpanded, false, "Tab bar is hidden after second swipe"); + is(Elements.appbar.isShowing, false, "Appbar is hidden after second swipe"); + + // sanity check - toggle on again + doEdgeUIGesture(); + is(ContextUI.isVisible, true, "Toolbar is still visible after third swipe"); + is(ContextUI.isExpanded, true, "Tab bar is visible after third swipe"); + is(Elements.appbar.isShowing, true, "Appbar is visible after third swipe"); + + is(StartUI.isVisible, true, "Start UI is still visible"); + } +}); + +gTests.push({ + desc: "Context UI on a web page (about:)", + run: function testAbout() { + yield addTab("about:"); + ContextUI.dismiss(); + is(StartUI.isVisible, false, "Start UI is not visible on about:"); + is(ContextUI.isVisible, false, "Toolbar is not initially visible on about:"); + is(ContextUI.isExpanded, false, "Tab bar is not initially visible on about:"); + is(Elements.appbar.isShowing, false, "Appbar is not initially visible on about on about::"); + + doEdgeUIGesture(); + is(ContextUI.isVisible, true, "Toolbar is visible after one swipe"); + is(ContextUI.isExpanded, true, "Tab bar is visble after one swipe"); + is(Elements.appbar.isShowing, true, "Appbar is visible after one swipe"); + + doEdgeUIGesture(); + is(ContextUI.isVisible, false, "Toolbar is not visible after second swipe"); + is(ContextUI.isExpanded, false, "Tab bar is not visible after second swipe"); + is(Elements.appbar.isShowing, false, "Appbar is hidden after second swipe"); + + is(StartUI.isVisible, false, "Start UI is still not visible"); + } +}); + +gTests.push({ + desc: "Control-L keyboard shortcut", + run: function testAbout() { + let tab = yield addTab("about:"); + ContextUI.dismiss(); + is(ContextUI.isVisible, false, "Navbar is not initially visible"); + is(ContextUI.isExpanded, false, "Tab bar is not initially visible"); + + EventUtils.synthesizeKey('l', { accelKey: true }); + is(ContextUI.isVisible, true, "Navbar is visible"); + is(ContextUI.isExpanded, false, "Tab bar is not visible"); + + let edit = document.getElementById("urlbar-edit"); + is(edit.value, "about:", "Location field contains the page URL"); + ok(document.commandDispatcher.focusedElement, edit.inputField, "Location field is focused"); + is(edit.selectionStart, 0, "Location field is selected"); + is(edit.selectionEnd, edit.value.length, "Location field is selected"); + + Browser.closeTab(tab); + } +}); + +function doEdgeUIGesture() { + let event = document.createEvent("Events"); + event.initEvent("MozEdgeUIGesture", true, false); + window.dispatchEvent(event); +} diff --git a/browser/metro/base/tests/browser_downloads.js b/browser/metro/base/tests/browser_downloads.js new file mode 100644 index 000000000000..2c0777040bf5 --- /dev/null +++ b/browser/metro/base/tests/browser_downloads.js @@ -0,0 +1,450 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Provides infrastructure for automated download components tests. + * (adapted from browser/component/downloads test's head.js) + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); + +const nsIDM = Ci.nsIDownloadManager; + +//////////////////////////////////////////////////////////////////////////////// +// Test Helpers + +var { spawn } = Task; + +function equalStrings(){ + let ref = ""+arguments[0]; + for (let i=1; i= 0; i--) { + let dataRow = aDataRows[i]; + let download = yield addDownloadRow(dataRow); + + // At each iteration, ensure that the start and end time in the global + // template is distinct, as these column are used to sort each download + // in its category. + gDownloadRowTemplate.startTime++; + gDownloadRowTemplate.endTime++; + } + } finally { + info("gen_addDownloadRows, finally"); + } +} + +///////////////////////////////////// +// Test implementations + +/** + * Test that: + * view can represent all possible download states + * clearDownloads does in fact empty the view + * addDownload adds an item, to the right place + * removeDownload removes an item, leaving the view in the correct state + */ +gTests.push({ + desc: "UI sanity check", + run: function(){ + + ok(document.getElementById('downloads-list'), "Downloads panel grid is present"); + ok(DownloadsPanelView, "Downloads panel object is present"); + + PanelUI.show('downloads-container'); + ok(DownloadsPanelView.visible, "Downloads panel is visible after being shown"); + } +}); + +gTests.push({ + desc: "zero downloads", + run: function () { + + yield resetDownloads(); + + let downloadslist = document.getElementById("downloads-list"); + + // wait for the richgrid to announce its readiness + // .. fail a test if the timeout is exceeded + let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000); + // tickle the view to cause it to refresh itself + DownloadsPanelView._view.getDownloads(); + + yield isReady; + + if (!isReady || isReady instanceof Error){ + ok(false, "DownloadsReady event never fired"); + } + + let count = downloadslist.children.length; + is(count, 0, "Zero items in grid view with empty downloads db"); + } +}); + +/** + * Make sure the downloads panel can display items in the right order and + * contains the expected data. + */ +gTests.push({ + desc: "Show downloads", + run: function(){ + // Display one of each download state. + let DownloadData = [ + { endTime: 1180493839859239, state: nsIDM.DOWNLOAD_NOTSTARTED }, + { endTime: 1180493839859238, state: nsIDM.DOWNLOAD_DOWNLOADING }, + { endTime: 1180493839859237, state: nsIDM.DOWNLOAD_PAUSED }, + { endTime: 1180493839859236, state: nsIDM.DOWNLOAD_SCANNING }, + { endTime: 1180493839859235, state: nsIDM.DOWNLOAD_QUEUED }, + { endTime: 1180493839859234, state: nsIDM.DOWNLOAD_FINISHED }, + { endTime: 1180493839859233, state: nsIDM.DOWNLOAD_FAILED }, + { endTime: 1180493839859232, state: nsIDM.DOWNLOAD_CANCELED }, + { endTime: 1180493839859231, state: nsIDM.DOWNLOAD_BLOCKED_PARENTAL }, + { endTime: 1180493839859230, state: nsIDM.DOWNLOAD_DIRTY }, + { endTime: 1180493839859229, state: nsIDM.DOWNLOAD_BLOCKED_POLICY }, + ]; + + yield resetDownloads(); + DownloadsPanelView._view.getDownloads(); + + // NB: beware display limits which might cause mismatch btw. rendered item and db rows + + try { + // Populate the downloads database with the data required by this test. + // we're going to add stuff to the downloads db. + yield spawn( gen_addDownloadRows( DownloadData ) ); + + // Test item data and count. This also tests the ordering of the display. + let downloadslist = document.getElementById("downloads-list"); + // wait for the richgrid to announce its readiness + // .. fail a test if the timeout is exceeded + let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000); + + // tickle the view to cause it to refresh itself + DownloadsPanelView._view.getDownloads(); + + yield isReady; + + if (!isReady || isReady instanceof Error){ + ok(false, "DownloadsReady event never fired"); + } + + is(downloadslist.children.length, DownloadData.length, + "There is the correct number of richlistitems"); + + for (let i = 0; i < downloadslist.children.length; i++) { + let element = downloadslist.children[i]; + let id = element.getAttribute("downloadId"); + let dataItem = Downloads.manager.getDownload(id); // nsIDownload object + + ok( equalStrings( + element.getAttribute("name"), + dataItem.displayName, + DownloadData[i].name + ), "Download names match up"); + + ok( equalNumbers( + element.getAttribute("state"), + dataItem.state, + DownloadData[i].state + ), "Download states match up"); + + ok( equalStrings( + element.getAttribute("target"), + dataItem.target.spec, + DownloadData[i].target + ), "Download targets match up"); + + if (DownloadData[i].source && dataItem.referrer){ + ok( equalStrings( + dataItem.referrer.spec, + DownloadData[i].source + ), "Download sources match up"); + } + } + } catch(e) { + info("Show downloads, some error: " + e); + } + finally { + // Clean up when the test finishes. + DownloadsPanelView._view.clearDownloads(); + yield resetDownloads(); + } + } +}); + +/** + * Make sure the downloads can be removed with the expected result on the panel and its listing + */ +gTests.push({ + desc: "Remove downloads", + run: function(){ + // Push a few items into the downloads db. + let DownloadData = [ + { endTime: 1180493839859239, state: nsIDM.DOWNLOAD_FINISHED }, + { endTime: 1180493839859238, state: nsIDM.DOWNLOAD_FINISHED }, + { endTime: 1180493839859237, state: nsIDM.DOWNLOAD_FINISHED } + ]; + + yield resetDownloads(); + DownloadsPanelView._view.getDownloads(); + + try { + // Populate the downloads database with the data required by this test. + yield spawn( gen_addDownloadRows( DownloadData ) ); + + // Test item data and count. This also tests the ordering of the display. + let downloadslist = document.getElementById("downloads-list"); + // wait for the richgrid to announce its readiness + // .. fail a test if the timeout is exceeded + let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000); + // tickle the view to cause it to refresh itself + DownloadsPanelView._view.getDownloads(); + + yield isReady; + + if (!isReady || isReady instanceof Error){ + is(false, "DownloadsReady event never fired"); + } + + let downloadRows = null, + promisedDownloads; + // get all the downloads from the db + promisedDownloads = getPromisedDbResult( + "SELECT guid " + + "FROM moz_downloads " + + "ORDER BY startTime DESC" + ).then(function(aRows){ + downloadRows = aRows; + }, function(aError){ + throw aError; + }); + yield promisedDownloads; + + is(downloadRows.length, 3, "Correct number of downloads in the db before removal"); + + // remove the first one + let itemNode = downloadslist.children[0]; + let id = itemNode.getAttribute("downloadId"); + // check the file exists + let download = Downloads.manager.getDownload( id ); + let file = download.targetFile; + ok(file && file.exists()); + + Downloads.manager.removeDownload( id ); + // remove is async(?), wait a bit + yield waitForMs(0); + + // get all the downloads from the db + downloadRows = null; + promisedDownloads = getPromisedDbResult( + "SELECT guid " + + "FROM moz_downloads " + + "ORDER BY startTime DESC" + ).then(function(aRows){ + downloadRows = aRows; + }, function(aError){ + throw aError; + }); + yield promisedDownloads; + + is(downloadRows.length, 2, "Correct number of downloads in the db after removal"); + + is(2, downloadslist.children.length, + "Removing a download updates the items list"); + ok(file && file.exists(), "File still exists after download removal"); + + } catch(e) { + info("Remove downloads, some error: " + e); + } + finally { + // Clean up when the test finishes. + DownloadsPanelView._view.clearDownloads(); + yield resetDownloads(); + } + } +}); + + diff --git a/browser/metro/base/tests/browser_onscreen_keyboard.html b/browser/metro/base/tests/browser_onscreen_keyboard.html new file mode 100644 index 000000000000..26e7b4f4b8d9 --- /dev/null +++ b/browser/metro/base/tests/browser_onscreen_keyboard.html @@ -0,0 +1,12 @@ + + + + On-Screen Keyboard Test + + + + + + diff --git a/browser/metro/base/tests/browser_onscreen_keyboard.js b/browser/metro/base/tests/browser_onscreen_keyboard.js new file mode 100644 index 000000000000..4f3a94434501 --- /dev/null +++ b/browser/metro/base/tests/browser_onscreen_keyboard.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + runTests(); +} + +gTests.push({ + desc: "Onscreen keyboard tests", + run: function() { + // By design, Metro apps can't show the keyboard programmatically, so we + // can't use the real keyboard in this test: + // http://msdn.microsoft.com/en-us/library/windows/apps/hh465404.aspx#user-driven_invocation + // + // Instead, we will use this mock object to simulate keyboard changes. + let originalUtils = window.MetroUtils; + window.MetroUtils = { + keyboardHeight: 0, + keyboardVisible: false + }; + registerCleanupFunction(function() { + window.MetroUtils = originalUtils; + }); + + let tab = yield addTab(chromeRoot + "browser_onscreen_keyboard.html"); + // Explicitly dismiss the toolbar at the start, because it messes with the + // keyboard and sizing if it's dismissed later. + ContextUI.dismiss(); + + let doc = tab.browser.contentDocument; + let text = doc.getElementById("text") + let rect0 = text.getBoundingClientRect(); + text.focus(); + + // "Show" the keyboard. + MetroUtils.keyboardHeight = 100; + MetroUtils.keyboardVisible = true; + Services.obs.notifyObservers(null, "metro_softkeyboard_shown", null); + + let rect1 = text.getBoundingClientRect(); + is(rect1.top, rect0.top - 100, "text field moves up by 100px"); + + // "Hide" the keyboard. + MetroUtils.keyboardHeight = 0; + MetroUtils.keyboardVisible = false; + Services.obs.notifyObservers(null, "metro_softkeyboard_hidden", null); + + let rect2 = text.getBoundingClientRect(); + is(rect2.top, rect0.top, "text field moves back to the original position"); + + finish(); + } +}); diff --git a/browser/metro/base/tests/browser_plugin_input.html b/browser/metro/base/tests/browser_plugin_input.html new file mode 100644 index 000000000000..80b5cd109179 --- /dev/null +++ b/browser/metro/base/tests/browser_plugin_input.html @@ -0,0 +1,8 @@ + + + Test Plugin Input + + + + + diff --git a/browser/metro/base/tests/browser_plugin_input_keyboard.js b/browser/metro/base/tests/browser_plugin_input_keyboard.js new file mode 100644 index 000000000000..023cdefb959e --- /dev/null +++ b/browser/metro/base/tests/browser_plugin_input_keyboard.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + runTests(); +} + +gTests.push({ + desc: "Plugin keyboard input", + run: function() { + Services.prefs.setBoolPref("plugin.disable", false); + Services.prefs.setBoolPref("plugins.click_to_play", false); + registerCleanupFunction(Services.prefs.clearUserPref.bind(null, "plugin.disable")); + registerCleanupFunction(Services.prefs.clearUserPref.bind(null, "plugins.click_to_play")); + + let tab = yield addTab(chromeRoot + "browser_plugin_input.html"); + + let doc = tab.browser.contentDocument; + let plugin = doc.getElementById("plugin1"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Plugin activated"); + plugin.focus(); + + try { + is(plugin.getLastKeyText(), "", "Plugin should not have received " + + "any character events yet."); + } catch(e) { + ok(false, "plugin.getLastKeyText should not throw: " + e); + } + + let keys = [{ kbLayout: arSpanish, + keyCode: 65, + modifiers: 0, + expectedChar: 'a' }]; + + /* XXX: Re-enable this once bug 837293 is fixed + { kbLayout: arSpanish, + keyCode: 65, + modifiers: rightAlt, + expectedChar: "á" }]; + */ + + while (keys.length > 0) { + let key = keys.shift(); + info("Sending keypress: " + key.expectedChar); + synthesizeNativeKey(key.kbLayout, key.keyCode, key.modifiers); + let success = yield waitForCondition(function() plugin.getLastKeyText() == key.expectedChar); + ok(success && !(success instanceof Error), + "Plugin received char: " + key.expectedChar); + } + } +}); diff --git a/browser/metro/base/tests/browser_plugin_input_mouse.js b/browser/metro/base/tests/browser_plugin_input_mouse.js new file mode 100644 index 000000000000..388b084e2747 --- /dev/null +++ b/browser/metro/base/tests/browser_plugin_input_mouse.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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() { + runTests(); +} + +gTests.push({ + desc: "Plugin mouse input", + run: function() { + // This test needs "Switch primary and secondary buttons" disabled. + let origValue = MetroUtils.swapMouseButton(false); + registerCleanupFunction(function() MetroUtils.swapMouseButton(origValue)); + + Services.prefs.setBoolPref("plugin.disable", false); + Services.prefs.setBoolPref("plugins.click_to_play", false); + registerCleanupFunction(Services.prefs.clearUserPref.bind(null, "plugin.disable")); + registerCleanupFunction(Services.prefs.clearUserPref.bind(null, "plugins.click_to_play")); + + let tab = yield addTab(chromeRoot + "browser_plugin_input.html"); + + let doc = tab.browser.contentDocument; + let plugin = doc.getElementById("plugin1"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Plugin activated"); + + // XXX: This shouldn't be necessary, but removing it causes the first click to + // sometimes not register + let wait = yield waitForMs(0); + ok(wait, "Initial wait"); + + try { + is(plugin.getMouseUpEventCount(), 0, "Plugin should not have received " + + "any mouse up events yet."); + } catch(e) { + ok(false, "plugin.getMouseUpEventCount should not throw: " + e); + } + + let bottom = plugin.getBoundingClientRect().height - 1; + let right = plugin.getBoundingClientRect().width - 1; + let middleX = right / 2; + let middleY = bottom / 2; + let left = 1; + let top = 1; + + let clicks = [{ x: left, y: top}, // left top corner + { x: left, y: middleY}, // left middle + { x: left, y: bottom}, // left bottom corner + { x: middleX, y: bottom}, // bottom middle + { x: right, y: bottom}, // right bottom corner + { x: right, y: middleY}, // right middle + { x: right, y: top}, // right top corner + { x: middleX, y: top}, // top middle + { x: middleX, y: middleY}]; // middle + + let curClicks = 0; + while (clicks.length > 0) { + let click = clicks.shift(); + curClicks++; + info("Sending click " + curClicks + " { x: " + click.x + ", y: " + click.y + "}"); + synthesizeNativeMouseLDown(plugin, click.x, click.y); + synthesizeNativeMouseLUp(plugin, click.x, click.y); + let success = yield waitForCondition(function() plugin.getMouseUpEventCount() == curClicks); + ok(success && !(success instanceof Error), + "Plugin received click " + curClicks); + } + } +}); diff --git a/browser/metro/base/tests/browser_remotetabs.js b/browser/metro/base/tests/browser_remotetabs.js new file mode 100644 index 000000000000..ca9c8d85a6c5 --- /dev/null +++ b/browser/metro/base/tests/browser_remotetabs.js @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +Components.utils.import("resource://services-sync/main.js"); +//////////////////////////////////////////////////////////////////////////////// +//// Test(s) + +function test() { + is(Weave.Status.checkSetup(), Weave.CLIENT_NOT_CONFIGURED, "Sync should be disabled on start"); + // check start page is hidden + + let vbox = document.getElementById("start-remotetabs"); + ok(vbox.hidden, "remote tabs in the start page should be hidden when sync is not enabled"); + // check container link is hidden + let menulink = document.getElementById("menuitem-remotetabs"); + ok(menulink.hidden, "link to container should be hidden when sync is not enabled"); + + // hacky-fake sync setup and enabled. Note the Sync Tracker will spit + // a number of warnings about undefined ids + Weave.Status._authManager.username = "jane doe"; // must set username before key + Weave.Status._authManager.basicPassword = "goatcheesesalad"; + Weave.Status._authManager.syncKey = "a-bcdef-abcde-acbde-acbde-acbde"; + // check that it worked + isnot(Weave.Status.checkSetup(), Weave.CLIENT_NOT_CONFIGURED, "Sync is enabled"); + Weave.Svc.Obs.notify("weave:service:setup-complete"); + + // start page grid should be visible + ok(vbox, "remote tabs grid is present on start page"); + //PanelUI.show("remotetabs-container"); + is(vbox.hidden, false, "remote tabs should be visible in start page when sync is enabled"); + // container link should be visible + is(menulink.hidden, false, "link to container should be visible when sync is enabled"); + + // hacky-fake sync disable + Weave.Status._authManager.deleteSyncCredentials(); + Weave.Svc.Obs.notify("weave:service:start-over"); + is(Weave.Status.checkSetup(), Weave.CLIENT_NOT_CONFIGURED, "Sync has been disabled"); + ok(vbox.hidden, "remote tabs in the start page should be hidden when sync is not enabled"); + ok(menulink.hidden, "link to container should be hidden when sync is not enabled"); + +} diff --git a/browser/metro/base/tests/browser_test.js b/browser/metro/base/tests/browser_test.js new file mode 100644 index 000000000000..f1d11b7faf48 --- /dev/null +++ b/browser/metro/base/tests/browser_test.js @@ -0,0 +1,30 @@ +// Tests for the test functions in head.js + +function test() { + waitForExplicitFinish(); + runTests(); +} + +gTests.push({ + desc: "task sanity check", + run: function() { + let sum2plus2 = yield asyncSum(2, 2); + ok(sum2plus2 == 4, "asyncSum responded 2+2=4"); + + function asyncSum(a, b) { + var defd = Promise.defer(); + setTimeout(function(){ + defd.resolve(a+b); + }, 25); + return defd.promise; + } + } +}); + +gTests.push({ + desc: "addTab", + run: function testAddTab() { + let tab = yield addTab("http://example.com/"); + is(tab, Browser.selectedTab, "The new tab is selected"); + } +}); diff --git a/browser/metro/base/tests/head.js b/browser/metro/base/tests/head.js new file mode 100644 index 000000000000..8a052fbeb4a9 --- /dev/null +++ b/browser/metro/base/tests/head.js @@ -0,0 +1,266 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/*============================================================================= + Globals +=============================================================================*/ +XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/commonjs/sdk/core/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); + +/*============================================================================= + Useful constants +=============================================================================*/ +const serverRoot = "http://example.com/browser/metro/"; +const baseURI = "http://mochi.test:8888/browser/metro/"; +const chromeRoot = getRootDirectory(gTestPath); +const kDefaultWait = 10000; +const kDefaultInterval = 50; + +/*============================================================================= + Asynchronous test helpers +=============================================================================*/ +/** + * Loads a URL in a new tab asynchronously. + * + * Usage: + * Task.spawn(function() { + * let tab = yield addTab("http://example.com/"); + * ok(Browser.selectedTab == tab, "the new tab is selected"); + * }); + * + * @param aUrl the URL to load + * @returns a task that resolves to the new tab object after the URL is loaded. + */ +function addTab(aUrl) { + return Task.spawn(function() { + info("Opening "+aUrl+" in a new tab"); + let tab = Browser.addTab(aUrl, true); + yield waitForEvent(tab.browser, "pageshow"); + + is(tab.browser.currentURI.spec, aUrl, aUrl + " is loaded"); + registerCleanupFunction(function() Browser.closeTab(tab)); + throw new Task.Result(tab); + }); +} + +/** + * Waits a specified number of miliseconds for a specified event to be + * fired on a specified element. + * + * Usage: + * let receivedEvent = waitForEvent(element, "eventName"); + * // Do some processing here that will cause the event to be fired + * // ... + * // Now yield until the Promise is fulfilled + * yield receivedEvent; + * if (receivedEvent && !(receivedEvent instanceof Error)) { + * receivedEvent.msg == "eventName"; + * // ... + * } + * + * @param aSubject the element that should receive the event + * @param aEventName the event to wait for + * @param aTimeoutMs the number of miliseconds to wait before giving up + * @returns a Promise that resolves to the received event, or to an Error + */ +function waitForEvent(aSubject, aEventName, aTimeoutMs) { + let eventDeferred = Promise.defer(); + let timeoutMs = aTimeoutMs || kDefaultWait; + let timerID = setTimeout(function wfe_canceller() { + aSubject.removeEventListener(aEventName, onEvent); + eventDeferred.reject( new Error(aEventName+" event timeout") ); + }, timeoutMs); + + function onEvent(aEvent) { + // stop the timeout clock and resume + clearTimeout(timerID); + eventDeferred.resolve(aEvent); + } + + function cleanup() { + // unhook listener in case of success or failure + aSubject.removeEventListener(aEventName, onEvent); + } + eventDeferred.promise.then(cleanup, cleanup); + + aSubject.addEventListener(aEventName, onEvent, false); + return eventDeferred.promise; +} + +/** + * 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) { + info("Wating for " + aMs + "ms"); + let deferred = Promise.defer(); + let startTime = Date.now(); + setTimeout(done, aMs); + + function done() { + deferred.resolve(true); + info("waitForMs finished waiting, waited for " + + (Date.now() - startTime) + + "ms"); + } + + return deferred.promise; +} + +/** + * Waits a specified number of miliseconds for a supplied callback to + * return a truthy value. + * + * Usage: + * let success = yield waitForCondition(myTestFunction); + * if (success && !(success instanceof Error)) { + * // ... + * } + * + * @param aCondition the callback that must return a truthy value + * @param aTimeoutMs the number of miliseconds to wait before giving up + * @param aIntervalMs the number of miliseconds between calls to aCondition + * @returns a Promise that resolves to true, or to an Error + */ +function waitForCondition(aCondition, aTimeoutMs, aIntervalMs) { + let deferred = Promise.defer(); + let timeoutMs = aTimeoutMs || kDefaultWait; + let intervalMs = aIntervalMs || kDefaultInterval; + let startTime = Date.now(); + + function testCondition() { + let now = Date.now(); + if((now - startTime) > timeoutMs) { + deferred.reject( new Error("Timed out waiting for condition to be true") ); + return; + } + + let condition; + try { + condition = aCondition(); + } catch (e) { + deferred.reject( new Error("Got exception while attempting to test conditino: " + e) ); + return; + } + + if (condition) { + deferred.resolve(true); + } else { + setTimeout(testCondition, intervalMs); + } + } + + setTimeout(testCondition, 0); + return deferred.promise; +} + +/*============================================================================= + Native input synthesis helpers +=============================================================================*/ +// Keyboard layouts for use with synthesizeNativeKey +const usEnglish = 0x409; +const arSpanish = 0x2C0A; + +// Modifiers for use with synthesizeNativeKey +const leftShift = 0x100; +const rightShift = 0x200; +const leftControl = 0x400; +const rightControl = 0x800; +const leftAlt = 0x1000; +const rightAlt = 0x2000; + +function synthesizeNativeKey(aKbLayout, aVKey, aModifiers) { + Browser.windowUtils.sendNativeKeyEvent(aKbLayout, aVKey, aModifiers, '', ''); +} + +function synthesizeNativeMouse(aElement, aOffsetX, aOffsetY, aMsg) { + let x = aOffsetX; + let y = aOffsetY; + if (aElement) { + if (aElement.getBoundingClientRect) { + let rect = aElement.getBoundingClientRect(); + x += rect.left; + y += rect.top; + } else if(aElement.left && aElement.top) { + x += aElement.left; + y += aElement.top; + } + } + Browser.windowUtils.sendNativeMouseEvent(x, y, aMsg, 0, null); +} + +function synthesizeNativeMouseMove(aElement, aOffsetX, aOffsetY) { + synthesizeNativeMouse(aElement, + aOffsetX, + aOffsetY, + 0x0001); // MOUSEEVENTF_MOVE +} + +function synthesizeNativeMouseLDown(aElement, aOffsetX, aOffsetY) { + synthesizeNativeMouse(aElement, + aOffsetX, + aOffsetY, + 0x0002); // MOUSEEVENTF_LEFTDOWN +} + +function synthesizeNativeMouseLUp(aElement, aOffsetX, aOffsetY) { + synthesizeNativeMouse(aElement, + aOffsetX, + aOffsetY, + 0x0004); // MOUSEEVENTF_LEFTUP +} + +function synthesizeNativeMouseRDown(aElement, aOffsetX, aOffsetY) { + synthesizeNativeMouse(aElement, + aOffsetX, + aOffsetY, + 0x0008); // MOUSEEVENTF_RIGHTDOWN +} + +function synthesizeNativeMouseRUp(aElement, aOffsetX, aOffsetY) { + synthesizeNativeMouse(aElement, + aOffsetX, + aOffsetY, + 0x0010); // MOUSEEVENTF_RIGHTUP +} + +function synthesizeNativeMouseMDown(aElement, aOffsetX, aOffsetY) { + synthesizeNativeMouse(aElement, + aOffsetX, + aOffsetY, + 0x0020); // MOUSEEVENTF_MIDDLEDOWN +} + +function synthesizeNativeMouseMUp(aElement, aOffsetX, aOffsetY) { + synthesizeNativeMouse(aElement, + aOffsetX, + aOffsetY, + 0x0040); // MOUSEEVENTF_MIDDLEUP +} + +/*============================================================================= + Test-running helpers +=============================================================================*/ +let gCurrentTest = null; +let gTests = []; + +function runTests() { + waitForExplicitFinish(); + Task.spawn(function() { + while((gCurrentTest = gTests.shift())){ + info(gCurrentTest.desc); + yield Task.spawn(gCurrentTest.run); + info("END "+gCurrentTest.desc); + } + info("done with gTests while loop, calling finish"); + finish(); + }); +} diff --git a/browser/metro/components/AboutRedirector.js b/browser/metro/components/AboutRedirector.js new file mode 100644 index 000000000000..3b872a7c377e --- /dev/null +++ b/browser/metro/components/AboutRedirector.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +let modules = { + start: { + uri: "about:blank", + privileged: false + }, + // about:blank has some bad loading behavior we can avoid, if we use an alias + empty: { + uri: "about:blank", + privileged: false + }, + firstrun: { + uri: "chrome://browser/content/firstrun/firstrun.xhtml", + privileged: true + }, + rights: { +#ifdef MOZ_OFFICIAL_BRANDING + uri: "chrome://browser/content/aboutRights.xhtml", +#else + uri: "chrome://global/content/aboutRights-unbranded.xhtml", +#endif + privileged: false + }, + blocked: { + uri: "chrome://browser/content/blockedSite.xhtml", + privileged: true + }, + certerror: { + uri: "chrome://browser/content/aboutCertError.xhtml", + privileged: true + }, + // an alias for about:start + home: { + uri: "about:blank", + privileged: true + }, + crash: { + uri: "chrome://browser/content/aboutCrash.xhtml", + privileged: true + } +} + +function AboutGeneric() {} + +AboutGeneric.prototype = { + classID: Components.ID("{433d2d75-5923-49b0-854d-f37267b03dc7}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]), + + _getModuleInfo: function (aURI) { + let moduleName = aURI.path.replace(/[?#].*/, "").toLowerCase(); + return modules[moduleName]; + }, + + getURIFlags: function(aURI) { + return Ci.nsIAboutModule.ALLOW_SCRIPT; + }, + + newChannel: function(aURI) { + let moduleInfo = this._getModuleInfo(aURI); + + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + var channel = ios.newChannel(moduleInfo.uri, null, null); + + if (!moduleInfo.privileged) { + // Setting the owner to null means that we'll go through the normal + // path in GetChannelPrincipal and create a codebase principal based + // on the channel's originalURI + channel.owner = null; + } + + channel.originalURI = aURI; + return channel; + } +}; + +const components = [AboutGeneric]; +const NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/metro/components/AlertsService.js b/browser/metro/components/AlertsService.js new file mode 100644 index 000000000000..e07e38f5f6ac --- /dev/null +++ b/browser/metro/components/AlertsService.js @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// ----------------------------------------------------------------------- +// Alerts Service +// ----------------------------------------------------------------------- + +function AlertsService() { } + +AlertsService.prototype = { + classID: Components.ID("{fe33c107-82a4-41d6-8c64-5353267e04c9}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService]), + + showAlertNotification: function(aImageUrl, aTitle, aText, aTextClickable, aCookie, aAlertListener, aName) { + let browser = Services.wm.getMostRecentWindow("navigator:browser"); + browser.AlertsHelper.showAlertNotification(aImageUrl, aTitle, aText, aTextClickable, aCookie, aAlertListener); + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AlertsService]); diff --git a/browser/metro/components/BrowserCLH.js b/browser/metro/components/BrowserCLH.js new file mode 100644 index 000000000000..ff845132c6e4 --- /dev/null +++ b/browser/metro/components/BrowserCLH.js @@ -0,0 +1,287 @@ +/* -*- Mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const nsIBrowserSearchService = Components.interfaces.nsIBrowserSearchService; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function openWindow(aParent, aURL, aTarget, aFeatures, aArgs) { + let argString = null; + if (aArgs && !(aArgs instanceof Ci.nsISupportsArray)) { + argString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + argString.data = aArgs; + } + + return Services.ww.openWindow(aParent, aURL, aTarget, aFeatures, argString || aArgs); +} + +function resolveURIInternal(aCmdLine, aArgument) { + let uri = aCmdLine.resolveURI(aArgument); + + if (!(uri instanceof Ci.nsIFileURL)) + return uri; + + try { + if (uri.file.exists()) + return uri; + } catch (e) { + Cu.reportError(e); + } + + try { + let urifixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); + uri = urifixup.createFixupURI(aArgument, 0); + } catch (e) { + Cu.reportError(e); + } + + return uri; +} + +/** + * Determines whether a home page override is needed. + * Returns: + * "new profile" if this is the first run with a new profile. + * "new version" if this is the first run with a build with a different + * Gecko milestone (i.e. right after an upgrade). + * "none" otherwise. + */ +function needHomepageOverride() { + let savedmstone = null; + try { + savedmstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone"); + } catch (e) {} + + if (savedmstone == "ignore") + return "none"; + +#expand let ourmstone = "__MOZ_APP_VERSION__"; + + if (ourmstone != savedmstone) { + Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourmstone); + + return (savedmstone ? "new version" : "new profile"); + } + + return "none"; +} + +function getHomePage() { + let url = "about:start"; + try { + url = Services.prefs.getComplexValue("browser.startup.homepage", Ci.nsIPrefLocalizedString).data; + } catch (e) { } + + return url; +} + +function showPanelWhenReady(aWindow, aPage) { + aWindow.addEventListener("UIReadyDelayed", function(aEvent) { + aWindow.PanelUI.show(aPage); + }, false); +} + +function haveSystemLocale() { + let localeService = Cc["@mozilla.org/intl/nslocaleservice;1"].getService(Ci.nsILocaleService); + let systemLocale = localeService.getSystemLocale().getCategory("NSILOCALE_CTYPE"); + return isLocaleAvailable(systemLocale); +} + +function checkCurrentLocale() { + if (Services.prefs.prefHasUserValue("general.useragent.locale")) { + // if the user has a compatible locale from a different buildid, we need to update + var buildID = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).platformBuildID; + let localeBuildID = Services.prefs.getCharPref("extensions.compatability.locales.buildid"); + if (buildID != localeBuildID) + return false; + + let currentLocale = Services.prefs.getCharPref("general.useragent.locale"); + return isLocaleAvailable(currentLocale); + } + return true; +} + +function isLocaleAvailable(aLocale) { + let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry); + chrome.QueryInterface(Ci.nsIToolkitChromeRegistry); + let availableLocales = chrome.getLocalesForPackage("browser"); + + let locale = aLocale.split("-")[0]; + let localeRegEx = new RegExp("^" + locale); + + while (availableLocales.hasMore()) { + let locale = availableLocales.getNext(); + if (localeRegEx.test(locale)) + return true; + } + return false; +} + +function BrowserCLH() { } + +BrowserCLH.prototype = { + // + // nsICommandLineHandler + // + handle: function fs_handle(aCmdLine) { +#ifdef DEBUG + for (var idx = 0; idx < aCmdLine.length; idx++) { + dump(aCmdLine.getArgument(idx) + "\n"); + } +#endif + // Instantiate the search service so the search engine cache is created now + // instead when the application is running. The install process will register + // this component by using the -silent command line flag, thereby creating + // the cache during install, not runtime. + // NOTE: This code assumes this CLH is run before the nsDefaultCLH, which + // consumes the "-silent" flag. + if (aCmdLine.findFlag("silent", false) > -1) { + let searchService = Services.search; + let autoComplete = Cc["@mozilla.org/autocomplete/search;1?name=history"]. + getService(Ci.nsIAutoCompleteSearch); + return; + } + + // Handle chrome windows loaded via commandline + let chromeParam = aCmdLine.handleFlagWithParam("chrome", false); + if (chromeParam) { + try { + // Only load URIs which do not inherit chrome privs + let features = "chrome,dialog=no,all"; + let uri = resolveURIInternal(aCmdLine, chromeParam); + let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil); + if (!netutil.URIChainHasFlags(uri, Ci.nsIHttpProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) { + openWindow(null, uri.spec, "_blank", features, null); + + // Stop the normal commandline processing from continuing + aCmdLine.preventDefault = true; + } + } catch (e) { + Cu.reportError(e); + } + return; + } + + // Check and remove the alert flag here, but we'll handle it a bit later - see below + let alertFlag = aCmdLine.handleFlagWithParam("alert", false); + + // Check and remove the webapp param + let appFlag = aCmdLine.handleFlagWithParam("webapp", false); + let appURI; + if (appFlag) + appURI = resolveURIInternal(aCmdLine, appFlag); + + // Keep an array of possible URL arguments + let uris = []; + + // Check for the "url" flag + let uriFlag = aCmdLine.handleFlagWithParam("url", false); + if (uriFlag) { + let uri = resolveURIInternal(aCmdLine, uriFlag); + if (uri) + uris.push(uri); + } + + // Check for the "search" flag + let searchParam = aCmdLine.handleFlagWithParam("search", false); + if (searchParam) { + var ss = Components.classes["@mozilla.org/browser/search-service;1"] + .getService(nsIBrowserSearchService); + var submission = ss.defaultEngine.getSubmission(searchParam.replace("\"", "", "g")); + uris.push(submission.uri); + } + + for (let i = 0; i < aCmdLine.length; i++) { + let arg = aCmdLine.getArgument(i); + if (!arg || arg[0] == '-') + continue; + + let uri = resolveURIInternal(aCmdLine, arg); + if (uri) + uris.push(uri); + } + + // Open the main browser window, if we don't already have one + let browserWin; + try { + let localeWin = Services.wm.getMostRecentWindow("navigator:localepicker"); + if (localeWin) { + localeWin.focus(); + aCmdLine.preventDefault = true; + return; + } + + browserWin = Services.wm.getMostRecentWindow("navigator:browser"); + if (!browserWin) { + // Default to the saved homepage + let defaultURL = getHomePage(); + + // Override the default if we have a URL passed on command line + if (uris.length > 0) { + defaultURL = uris[0].spec; + uris = uris.slice(1); + } + + // Show the locale selector if we have a new profile, or if the selected locale is no longer compatible + let showLocalePicker = Services.prefs.getBoolPref("browser.firstrun.show.localepicker"); + if ((needHomepageOverride() == "new profile" && showLocalePicker && !haveSystemLocale())) { // || !checkCurrentLocale()) { + browserWin = openWindow(null, "chrome://browser/content/localePicker.xul", "_blank", "chrome,dialog=no,all", defaultURL); + aCmdLine.preventDefault = true; + return; + } + + browserWin = openWindow(null, "chrome://browser/content/browser.xul", "_blank", "chrome,dialog=no,all", defaultURL); + } + + browserWin.focus(); + + // Stop the normal commandline processing from continuing. We just opened the main browser window + aCmdLine.preventDefault = true; + } catch (e) { + Cu.reportError(e); + } + + // Assumption: All remaining command line arguments have been sent remotely (browser is already running) + // Action: Open any URLs we find into an existing browser window + + // First, get a browserDOMWindow object + while (!browserWin.browserDOMWindow) + Services.tm.currentThread.processNextEvent(true); + + // Open any URIs into new tabs + for (let i = 0; i < uris.length; i++) + browserWin.browserDOMWindow.openURI(uris[i], null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + + if (appURI) + browserWin.browserDOMWindow.openURI(appURI, null, browserWin.OPEN_APPTAB, Ci.nsIBrowserDOMWindow.OPEN_NEW); + + // Handle the notification, if called from it + if (alertFlag) { + if (alertFlag == "update-app") { + // Notification was already displayed and clicked, skip it next time + Services.prefs.setBoolPref("app.update.skipNotification", true); + + var updateService = Cc["@mozilla.org/updates/update-service;1"].getService(Ci.nsIApplicationUpdateService); + var updateTimerCallback = updateService.QueryInterface(Ci.nsITimerCallback); + updateTimerCallback.notify(null); + } else if (alertFlag.length >= 9 && alertFlag.substr(0, 9) == "download:") { + showPanelWhenReady(browserWin, "downloads-container"); + } + } + }, + + // QI + QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]), + + // XPCOMUtils factory + classID: Components.ID("{be623d20-d305-11de-8a39-0800200c9a66}"), +}; + +var components = [ BrowserCLH ]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/metro/components/BrowserStartup.js b/browser/metro/components/BrowserStartup.js new file mode 100644 index 000000000000..37b57cde4bc7 --- /dev/null +++ b/browser/metro/components/BrowserStartup.js @@ -0,0 +1,117 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// Custom factory object to ensure that we're a singleton +const BrowserStartupServiceFactory = { + _instance: null, + createInstance: function (outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return this._instance || (this._instance = new BrowserStartup()); + } +}; + +function BrowserStartup() { + this._init(); +} + +BrowserStartup.prototype = { + // for XPCOM + classID: Components.ID("{1d542abc-c88b-4636-a4ef-075b49806317}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + _xpcom_factory: BrowserStartupServiceFactory, + + _init: function() { + Services.obs.addObserver(this, "places-init-complete", false); + Services.obs.addObserver(this, "final-ui-startup", false); + }, + + _initDefaultBookmarks: function() { + // We must instantiate the history service since it will tell us if we + // need to import or restore bookmarks due to first-run, corruption or + // forced migration (due to a major schema change). + let histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + + // If the database is corrupt or has been newly created we should + // import bookmarks. + let databaseStatus = histsvc.databaseStatus; + let importBookmarks = databaseStatus == histsvc.DATABASE_STATUS_CREATE || + databaseStatus == histsvc.DATABASE_STATUS_CORRUPT; + + if (!importBookmarks) { + // Check to see whether "mobile" root already exists. This is to handle + // existing profiles created with pre-1.0 builds (which won't have mobile + // bookmarks root). We can remove this eventually when we stop + // caring about users migrating to current builds with pre-1.0 profiles. + let annos = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + let metroRootItems = annos.getItemsWithAnnotation("metro/bookmarksRoot", {}); + if (metroRootItems.length > 0) + return; // no need to do initial import + } + + Cu.import("resource://gre/modules/PlacesUtils.jsm"); + + try { + let observer = { + onStreamComplete : function(aLoader, aContext, aStatus, aLength, aResult) { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + let jsonStr = ""; + try { + converter.charset = "UTF-8"; + jsonStr = converter.convertFromByteArray(aResult, aResult.length); + + // aReplace=false since this may be called when there are existing + // bookmarks that we don't want to overwrite ("no mobile root" + // case from above) + PlacesUtils.restoreBookmarksFromJSONString(jsonStr, false); + } catch (err) { + Cu.reportError("Failed to parse default bookmarks from bookmarks.json: " + err); + } + } + }; + + let ioSvc = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let uri = ioSvc.newURI("chrome://browser/locale/bookmarks.json", null, null); + let channel = ioSvc.newChannelFromURI(uri); + let sl = Cc["@mozilla.org/network/stream-loader;1"]. + createInstance(Ci.nsIStreamLoader); + sl.init(observer); + channel.asyncOpen(sl, channel); + } catch (err) { + // Report the error, but ignore it. + Cu.reportError("Failed to load default bookmarks from bookmarks.json: " + err); + } + }, + + _startupActions: function() { + }, + + // nsIObserver + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "places-init-complete": + Services.obs.removeObserver(this, "places-init-complete"); + this._initDefaultBookmarks(); + break; + case "final-ui-startup": + Services.obs.removeObserver(this, "final-ui-startup"); + this._startupActions(); + break; + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([BrowserStartup]); diff --git a/browser/metro/components/CapturePicker.js b/browser/metro/components/CapturePicker.js new file mode 100644 index 000000000000..8458dfcf4543 --- /dev/null +++ b/browser/metro/components/CapturePicker.js @@ -0,0 +1,85 @@ +/* -*- Mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function CapturePicker() { + this.messageManager = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender); +} + +CapturePicker.prototype = { + _file: null, + _mode: -1, + _result: -1, + _shown: false, + _title: "", + _type: "", + _window: null, + + // + // nsICapturePicker + // + init: function(aWindow, aTitle, aMode) { + this._window = aWindow; + this._title = aTitle; + this._mode = aMode; + }, + + show: function() { + if (this._shown) + throw Cr.NS_ERROR_UNEXPECTED; + + this._shown = true; + + let res = this.messageManager.sendSyncMessage("CapturePicker:Show", { title: this._title, mode: this._mode, type: this._type })[0]; + if (res.value) + this._file = res.path; + + return (res.value ? Ci.nsICapturePicker.RETURN_OK : Ci.nsICapturePicker.RETURN_CANCEL); + }, + + modeMayBeAvailable: function(aMode) { + if (aMode != Ci.nsICapturePicker.MODE_STILL) + return false; + return true; + }, + + get file() { + if (this._file) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(this._file); + let utils = this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + return utils.wrapDOMFile(file); + } else { + throw Cr.NS_ERROR_FAILURE; + } + }, + + get type() { + return this._type; + }, + + set type(aNewType) { + if (this._shown) + throw Cr.NS_ERROR_UNEXPECTED; + else + this._type = aNewType; + }, + + // QI + QueryInterface: XPCOMUtils.generateQI([Ci.nsICapturePicker]), + + // XPCOMUtils factory + classID: Components.ID("{cb5a47f0-b58c-4fc3-b61a-358ee95f8238}"), +}; + +var components = [ CapturePicker ]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/metro/components/ContentDispatchChooser.js b/browser/metro/components/ContentDispatchChooser.js new file mode 100644 index 000000000000..03e7e28c0a42 --- /dev/null +++ b/browser/metro/components/ContentDispatchChooser.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/. */ + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function ContentDispatchChooser() {} + +ContentDispatchChooser.prototype = +{ + classID: Components.ID("5a072a22-1e66-4100-afc1-07aed8b62fc5"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentDispatchChooser]), + + ask: function ask(aHandler, aWindowContext, aURI, aReason) { + let window = null; + try { + if (aWindowContext) + window = aWindowContext.getInterface(Ci.nsIDOMWindow); + } catch (e) { /* it's OK to not have a window */ } + + let bundle = Services.strings.createBundle("chrome://mozapps/locale/handling/handling.properties"); + + let title = bundle.GetStringFromName("protocol.title"); + let message = bundle.GetStringFromName("protocol.description"); + + let open = Services.prompt.confirm(window, title, message); + if (open) + aHandler.launchWithURI(aURI, aWindowContext); + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentDispatchChooser]); + diff --git a/browser/metro/components/ContentPermissionPrompt.js b/browser/metro/components/ContentPermissionPrompt.js new file mode 100644 index 000000000000..0c66d7bf493d --- /dev/null +++ b/browser/metro/components/ContentPermissionPrompt.js @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const kCountBeforeWeRemember = 5; + +function setPagePermission(type, principal, allow) { + let pm = Services.perms; + let contentPrefs = Services.contentPrefs; + let contentPrefName = type + ".request.remember"; + + if (!contentPrefs.hasPref(principal.URI, contentPrefName)) + contentPrefs.setPref(principal.URI, contentPrefName, 0); + + let count = contentPrefs.getPref(principal.URI, contentPrefName); + + if (allow == false) + count--; + else + count++; + + contentPrefs.setPref(principal.URI, contentPrefName, count); + if (count == kCountBeforeWeRemember) + pm.addFromPrincipal(principal, type, Ci.nsIPermissionManager.ALLOW_ACTION); + else if (count == -kCountBeforeWeRemember) + pm.addFromPrincipal(principal, type, Ci.nsIPermissionManager.DENY_ACTION); +} + +const kEntities = { "geolocation": "geolocation", "desktop-notification": "desktopNotification", + "indexedDB": "offlineApps", "indexedDBQuota": "indexedDBQuota", + "openWebappsManage": "openWebappsManage" }; + +function ContentPermissionPrompt() {} + +ContentPermissionPrompt.prototype = { + classID: Components.ID("{C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]), + + getChromeWindow: function getChromeWindow(aWindow) { + let chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + return chromeWin; + }, + + getNotificationBoxForRequest: function getNotificationBoxForRequest(request) { + let notificationBox = null; + if (request.window) { + let requestingWindow = request.window.top; + let chromeWin = this.getChromeWindow(requestingWindow).wrappedJSObject; + let windowID = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; + let browser = chromeWin.Browser.getBrowserForWindowId(windowID); + return chromeWin.getNotificationBox(browser); + } + + let chromeWin = request.element.ownerDocument.defaultView; + return chromeWin.Browser.getNotificationBox(request.element); + }, + + handleExistingPermission: function handleExistingPermission(request) { + let result = Services.perms.testExactPermissionFromPrincipal(request.principal, request.type); + if (result == Ci.nsIPermissionManager.ALLOW_ACTION) { + request.allow(); + return true; + } + if (result == Ci.nsIPermissionManager.DENY_ACTION) { + request.cancel(); + return true; + } + return false; + }, + + prompt: function(request) { + // returns true if the request was handled + if (this.handleExistingPermission(request)) + return; + + let pm = Services.perms; + let notificationBox = this.getNotificationBoxForRequest(request); + let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); + + let notification = notificationBox.getNotificationWithValue(request.type); + if (notification) + return; + + let entityName = kEntities[request.type]; + + let buttons = [{ + label: browserBundle.GetStringFromName(entityName + ".allow"), + accessKey: null, + callback: function(notification) { + setPagePermission(request.type, request.principal, true); + request.allow(); + } + }, + { + label: browserBundle.GetStringFromName(entityName + ".dontAllow"), + accessKey: null, + callback: function(notification) { + setPagePermission(request.type, request.principal, false); + request.cancel(); + } + }]; + + let message = browserBundle.formatStringFromName(entityName + ".wantsTo", + [request.principal.URI.host], 1); + let newBar = notificationBox.appendNotification(message, + request.type, + "", // Notifications in Fennec do not display images. + notificationBox.PRIORITY_WARNING_MEDIUM, + buttons); + } +}; + + +//module initialization +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]); diff --git a/browser/metro/components/DirectoryProvider.js b/browser/metro/components/DirectoryProvider.js new file mode 100644 index 000000000000..08bacdc6968a --- /dev/null +++ b/browser/metro/components/DirectoryProvider.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +// ----------------------------------------------------------------------- +// Directory Provider for special browser folders and files +// ----------------------------------------------------------------------- + +const NS_APP_CACHE_PARENT_DIR = "cachePDir"; +const XRE_UPDATE_ROOT_DIR = "UpdRootD"; +const ENVVAR_UPDATE_DIR = "UPDATES_DIRECTORY"; + +function DirectoryProvider() {} + +DirectoryProvider.prototype = { + classID: Components.ID("{ef0f7a87-c1ee-45a8-8d67-26f586e46a4b}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]), + + getFile: function(prop, persistent) { + if (prop == NS_APP_CACHE_PARENT_DIR) { + let dirsvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); + let profile = dirsvc.get("ProfD", Ci.nsIFile); + + let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); + let device = sysInfo.get("device"); + switch (device) { + default: + return profile; + } + } else if (prop == XRE_UPDATE_ROOT_DIR) { + let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); + if (env.exists(ENVVAR_UPDATE_DIR)) { + let path = env.get(ENVVAR_UPDATE_DIR); + if (path) { + let localFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + localFile.initWithPath(path); + return localFile; + } + } + let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); + return dm.defaultDownloadsDirectory; + } + + // We are retuning null to show failure instead for throwing an error. The + // interface is called quite a bit and throwing an error is noisy. Returning + // null works with the way the interface is called [see bug 529077] + return null; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DirectoryProvider]); + diff --git a/browser/metro/components/DownloadManagerUI.js b/browser/metro/components/DownloadManagerUI.js new file mode 100644 index 000000000000..19023d9e19ac --- /dev/null +++ b/browser/metro/components/DownloadManagerUI.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/. */ + +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +// ----------------------------------------------------------------------- +// Download Manager UI +// ----------------------------------------------------------------------- + +function DownloadManagerUI() { } + +DownloadManagerUI.prototype = { + classID: Components.ID("{93db15b1-b408-453e-9a2b-6619e168324a}"), + + show: function show(aWindowContext, aID, aReason, aUsePrivateUI) { + if (!aReason) + aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED; + + let browser = Services.wm.getMostRecentWindow("navigator:browser"); + if (browser) + browser.showDownloadManager(aWindowContext, aID, aReason); + }, + + get visible() { + let browser = Services.wm.getMostRecentWindow("navigator:browser"); + if (browser) { + return browser.DownloadsView.visible; + } + return false; + }, + + getAttention: function getAttention() { + if (this.visible) + this.show(null, null, null); + else + throw Cr.NS_ERROR_UNEXPECTED; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadManagerUI]); diff --git a/browser/metro/components/FormAutoComplete.js b/browser/metro/components/FormAutoComplete.js new file mode 100644 index 000000000000..1f2df9021983 --- /dev/null +++ b/browser/metro/components/FormAutoComplete.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/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function LOG() { + return; // comment out for verbose debugging + let msg = Array.join(arguments, " "); + dump(msg + "\n"); + Cu.reportError(msg); +} + +// Lazily get the base Form AutoComplete Search +XPCOMUtils.defineLazyGetter(this, "FAC", function() { + return Components.classesByID["{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"] + .getService(Ci.nsIFormAutoComplete); +}); + +function FormAutoComplete() { + LOG("new FAC"); +} + +FormAutoComplete.prototype = { + classDescription: "Form AutoComplete Plus", + classID: Components.ID("{cccd414c-3ec2-4cc5-9dc4-36c87cc3c4fe}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormAutoComplete]), + + // Specify the html5 types that we want and some values to guess + contactTypes: { + email: /^(?:.*(?:e-?mail|recipients?).*|(send_)?to(_b?cc)?)$/i, + tel: /^(?:tel(?:ephone)?|.*phone.*)$/i + }, + + checkQueryType: function checkQueryType(aName, aField) { + // If we have an input field with the desired html5 type, take it! + if (aField && "type" in aField) { + let type = aField.type; + if (type && type in this.contactTypes) + return type; + } + + // Grab properties to check for contact inputs + let props = [aName]; + if (aField) { + let specialProps = [aField["className"], aField["id"]]; + props = props.concat(specialProps.filter(function(aValue) { + return aValue; + })); + } + + // Check the gathered properties for contact-like values + for (let [type, regex] in Iterator(this.contactTypes)) { + if (props.some(function(prop) prop.search(regex) != -1)) + return type; + } + return null; + }, + + autoCompleteSearch: function autoCompleteSearch(aName, aQuery, aField, aPrev) { + if (!Services.prefs.getBoolPref("browser.formfill.enable")) + return null; + + LOG("autocomplete search", Array.slice(arguments)); + let result = Cc["@mozilla.org/autocomplete/simple-result;1"].createInstance(Ci.nsIAutoCompleteSimpleResult); + result.setSearchString(aQuery); + + // Don't allow duplicates get merged into the final results + let dupCheck = {}; + + // Use the base form autocomplete for non-contact searches + let normal = FAC.autoCompleteSearch(aName, aQuery, aField, aPrev); + if (normal.matchCount > 0) { + for (let i = 0; i < normal.matchCount; i++) { + dupCheck[normal.getValueAt(i)] = true; + result.appendMatch(normal.getValueAt(i), normal.getCommentAt(i), normal.getImageAt(i), normal.getStyleAt(i)); + } + } + + // Do searches for certain input fields + let type = this.checkQueryType(aName, aField); + + let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH"; + result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]); + return result; + } +}; + +let components = [FormAutoComplete]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/metro/components/HelperAppDialog.js b/browser/metro/components/HelperAppDialog.js new file mode 100644 index 000000000000..92091a985dd3 --- /dev/null +++ b/browser/metro/components/HelperAppDialog.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/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir"; +const URI_GENERIC_ICON_DOWNLOAD = "chrome://browser/skin/images/alert-downloads-30.png"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// ----------------------------------------------------------------------- +// HelperApp Launcher Dialog +// ----------------------------------------------------------------------- + +function HelperAppLauncherDialog() { } + +HelperAppLauncherDialog.prototype = { + classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), + + show: function hald_show(aLauncher, aContext, aReason) { + // Check to see if we can open this file or not + if (aLauncher.MIMEInfo.hasDefaultHandler) { + aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault; + aLauncher.launchWithApplication(null, false); + } else { + let wasClicked = false; + let listener = { + observe: function(aSubject, aTopic, aData) { + if (aTopic == "alertclickcallback") { + wasClicked = true; + let win = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator).getMostRecentWindow("navigator:browser"); + if (win) + win.PanelUI.show("downloads-container"); + + aLauncher.saveToDisk(null, false); + } else { + if (!wasClicked) + aLauncher.cancel(Cr.NS_BINDING_ABORTED); + } + } + }; + this._notify(aLauncher, listener); + } + }, + + promptForSaveToFile: function hald_promptForSaveToFile(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) { + let file = null; + let prefs = Services.prefs; + + if (!aForcePrompt) { + // Check to see if the user wishes to auto save to the default download + // folder without prompting. Note that preference might not be set. + let autodownload = true; + try { + autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR); + } catch (e) { } + + if (autodownload) { + // Retrieve the user's default download directory + let dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); + let defaultFolder = dnldMgr.userDownloadsDirectory; + + try { + file = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExt); + } + catch (e) { + } + + // Check to make sure we have a valid directory, otherwise, prompt + if (file) + return file; + } + } + + // Use file picker to show dialog. + let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let windowTitle = ""; + let parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + picker.init(parent, windowTitle, Ci.nsIFilePicker.modeSave); + picker.defaultString = aDefaultFile; + + if (aSuggestedFileExt) { + // aSuggestedFileExtension includes the period, so strip it + picker.defaultExtension = aSuggestedFileExt.substring(1); + } + else { + try { + picker.defaultExtension = aLauncher.MIMEInfo.primaryExtension; + } + catch (e) { } + } + + var wildCardExtension = "*"; + if (aSuggestedFileExt) { + wildCardExtension += aSuggestedFileExt; + picker.appendFilter(aLauncher.MIMEInfo.description, wildCardExtension); + } + + picker.appendFilters(Ci.nsIFilePicker.filterAll); + + // Default to lastDir if it is valid, otherwise use the user's default + // downloads directory. userDownloadsDirectory should always return a + // valid directory, so we can safely default to it. + var dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); + picker.displayDirectory = dnldMgr.userDownloadsDirectory; + + // The last directory preference may not exist, which will throw. + try { + let lastDir = prefs.getComplexValue("browser.download.lastDir", Ci.nsILocalFile); + if (isUsableDirectory(lastDir)) + picker.displayDirectory = lastDir; + } + catch (e) { } + + if (picker.show() == Ci.nsIFilePicker.returnCancel) { + // null result means user cancelled. + return null; + } + + // Be sure to save the directory the user chose through the Save As... + // dialog as the new browser.download.dir since the old one + // didn't exist. + file = picker.file; + + if (file) { + try { + // Remove the file so that it's not there when we ensure non-existence later; + // this is safe because for the file to exist, the user would have had to + // confirm that he wanted the file overwritten. + if (file.exists()) + file.remove(false); + } + catch (e) { } + var newDir = file.parent.QueryInterface(Ci.nsILocalFile); + prefs.setComplexValue("browser.download.lastDir", Ci.nsILocalFile, newDir); + file = this.validateLeafName(newDir, file.leafName, null); + } + return file; + }, + + validateLeafName: function hald_validateLeafName(aLocalFile, aLeafName, aFileExt) { + if (!(aLocalFile && this.isUsableDirectory(aLocalFile))) + return null; + + // Remove any leading periods, since we don't want to save hidden files + // automatically. + aLeafName = aLeafName.replace(/^\.+/, ""); + + if (aLeafName == "") + aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : ""); + aLocalFile.append(aLeafName); + + this.makeFileUnique(aLocalFile); + return aLocalFile; + }, + + makeFileUnique: function hald_makeFileUnique(aLocalFile) { + try { + // Note - this code is identical to that in + // toolkit/content/contentAreaUtils.js. + // If you are updating this code, update that code too! We can't share code + // here since this is called in a js component. + var collisionCount = 0; + while (aLocalFile.exists()) { + collisionCount++; + if (collisionCount == 1) { + // Append "(2)" before the last dot in (or at the end of) the filename + // special case .ext.gz etc files so we don't wind up with .tar(2).gz + if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) + aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); + else + aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); + } + else { + // replace the last (n) in the filename with (n+1) + aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")"); + } + } + aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); + } + catch (e) { + dump("*** exception in validateLeafName: " + e + "\n"); + + if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED) + throw e; + + if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) { + aLocalFile.append("unnamed"); + if (aLocalFile.exists()) + aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); + } + } + }, + + isUsableDirectory: function hald_isUsableDirectory(aDirectory) { + return aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable(); + }, + + _notify: function hald_notify(aLauncher, aCallback) { + let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); + + let notifier = Cc[aCallback ? "@mozilla.org/alerts-service;1" : "@mozilla.org/toaster-alerts-service;1"].getService(Ci.nsIAlertsService); + notifier.showAlertNotification(URI_GENERIC_ICON_DOWNLOAD, + bundle.GetStringFromName("alertDownloads"), + bundle.GetStringFromName("alertCantOpenDownload"), + true, "", aCallback, "downloadopen-fail"); + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]); diff --git a/browser/metro/components/LoginManager.js b/browser/metro/components/LoginManager.js new file mode 100644 index 000000000000..d9ed14d80205 --- /dev/null +++ b/browser/metro/components/LoginManager.js @@ -0,0 +1,686 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function LoginManager() { + this.init(); +} + +LoginManager.prototype = { + + classID: Components.ID("{f9a0edde-2a8d-4bfd-a08c-3f9333213a85}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager, + Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + + /* ---------- private members ---------- */ + + + __storage : null, // Storage component which contains the saved logins + get _storage() { + if (!this.__storage) { + + var contractID = "@mozilla.org/login-manager/storage/mozStorage;1"; + try { + var catMan = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + contractID = catMan.getCategoryEntry("login-manager-storage", + "nsILoginManagerStorage"); + this.log("Found alternate nsILoginManagerStorage with " + + "contract ID: " + contractID); + } catch (e) { + this.log("No alternate nsILoginManagerStorage registered"); + } + + this.__storage = Cc[contractID]. + createInstance(Ci.nsILoginManagerStorage); + try { + this.__storage.init(); + } catch (e) { + this.log("Initialization of storage component failed: " + e); + this.__storage = null; + } + } + + return this.__storage; + }, + + + _nsLoginInfo : null, // Constructor for nsILoginInfo implementation + _debug : false, // mirrors signon.debug + _remember : true, // mirrors signon.rememberSignons preference + + + /* + * init + * + * Initialize the Login Manager. Automatically called when service + * is created. + * + * Note: Service created in /browser/base/content/browser.js, + * delayedStartup() + */ + init : function () { + // Add content listener. + var messageManager = Cc["@mozilla.org/globalmessagemanager;1"]. + getService(Ci.nsIMessageListenerManager); + messageManager.loadFrameScript("chrome://browser/content/LoginManagerChild.js", true); + messageManager.addMessageListener("PasswordMgr:FormSubmitted", this); + messageManager.addMessageListener("PasswordMgr:GetPasswords", this); + + // Get constructor for nsILoginInfo + this._nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo); + + // Preferences. Add observer so we get notified of changes. + Services.prefs.addObserver("signon.", this, false); + + // Get current preference values. + this._debug = Services.prefs.getBoolPref("signon.debug"); + this._remember = Services.prefs.getBoolPref("signon.rememberSignons"); + + // Listen for shutdown to clean up + Services.obs.addObserver(this, "xpcom-shutdown", false); + }, + + /* + * log + * + * Internal function for logging debug messages to the Error Console window + */ + log : function (message) { + if (!this._debug) + return; + dump("PasswordUtils: " + message + "\n"); + Services.console.logStringMessage("PasswordUtils: " + message); + }, + + /* + * observe + * + * Implements nsIObserver for preferences and shutdown. + */ + observe : function (subject, topic, data) { + if (topic == "nsPref:changed") { + this._pwmgr._debug = Services.prefs.getBoolPref("signon.debug"); + this._pwmgr._remember = Services.prefs.getBoolPref("signon.rememberSignons"); + } else if (topic == "xpcom-shutdown") { + // Circular reference forms when we mark an input field as managed + // by the password manager + this._formFillService = null; + } else { + this._pwmgr.log("Oops! Unexpected notification: " + topic); + } + }, + + /* + * receiveMessage + * + * Receives messages from content process. + */ + receiveMessage: function (message) { + // local helper function + function getPrompter(aBrowser) { + var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"]. + createInstance(Ci.nsILoginManagerPrompter); + prompterSvc.init(aBrowser); + return prompterSvc; + } + + switch (message.name) { + case "PasswordMgr:GetPasswords": + // If there are no logins for this site, bail out now. + if (!this.countLogins(message.json.formOrigin, "", null)) + return { foundLogins: {} }; + + var foundLogins = {}; + + if (!this.uiBusy) { + for (var i = 0; i < message.json.actionOrigins.length; i++) { + var actionOrigin = message.json.actionOrigins[i]; + var logins = this.findLogins({}, message.json.formOrigin, actionOrigin, null); + if (logins.length) { + foundLogins[actionOrigin] = logins; + } + } + } + + return { + uiBusy: this.uiBusy, + foundLogins: foundLogins + }; + + case "PasswordMgr:FormSubmitted": + var json = message.json; + var hostname = json.hostname; + var formSubmitURL = json.formSubmitURL; + + if (!this.getLoginSavingEnabled(hostname)) { + this.log("(form submission ignored -- saving is " + + "disabled for: " + hostname + ")"); + return {}; + } + + var browser = message.target; + + var formLogin = new this._nsLoginInfo(); + + formLogin.init(hostname, formSubmitURL, null, + json.usernameValue, + json.passwordValue, + json.usernameField, + json.passwordField); + + // If we didn't find a username field, but seem to be changing a + // password, allow the user to select from a list of applicable + // logins to update the password for. + if (!json.usernameField && json.hasOldPasswordField) { + + var logins = this.findLogins({}, hostname, formSubmitURL, null); + + if (logins.length == 0) { + // Could prompt to save this as a new password-only login. + // This seems uncommon, and might be wrong, so ignore. + this.log("(no logins for this host -- pwchange ignored)"); + return {}; + } + + var prompter = getPrompter(browser); + + if (logins.length == 1) { + var oldLogin = logins[0]; + formLogin.username = oldLogin.username; + formLogin.usernameField = oldLogin.usernameField; + + prompter.promptToChangePassword(oldLogin, formLogin); + } else { + prompter.promptToChangePasswordWithUsernames( + logins, logins.length, formLogin); + } + + } else { + + // Look for an existing login that matches the form login. + var existingLogin = null; + var logins = this.findLogins({}, hostname, formSubmitURL, null); + + for (var i = 0; i < logins.length; i++) { + var same, login = logins[i]; + + // If one login has a username but the other doesn't, ignore + // the username when comparing and only match if they have the + // same password. Otherwise, compare the logins and match even + // if the passwords differ. + if (!login.username && formLogin.username) { + var restoreMe = formLogin.username; + formLogin.username = ""; + same = formLogin.matches(login, false); + formLogin.username = restoreMe; + } else if (!formLogin.username && login.username) { + formLogin.username = login.username; + same = formLogin.matches(login, false); + formLogin.username = ""; // we know it's always blank. + } else { + same = formLogin.matches(login, true); + } + + if (same) { + existingLogin = login; + break; + } + } + + if (existingLogin) { + this.log("Found an existing login matching this form submission"); + + // Change password if needed. + if (existingLogin.password != formLogin.password) { + this.log("...passwords differ, prompting to change."); + prompter = getPrompter(browser); + prompter.promptToChangePassword(existingLogin, formLogin); + } else { + // Update the lastUsed timestamp. + var propBag = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + propBag.setProperty("timeLastUsed", Date.now()); + propBag.setProperty("timesUsedIncrement", 1); + this.modifyLogin(existingLogin, propBag); + } + + return {}; + } + + + // Prompt user to save login (via dialog or notification bar) + prompter = getPrompter(browser); + prompter.promptToSavePassword(formLogin); + } + return {}; + + default: + throw "Unexpected message " + message.name; + } + }, + + + /* + * _getPasswordOrigin + * + * Get the parts of the URL we want for identification. + */ + _getPasswordOrigin : function (uriString, allowJS) { + var realm = ""; + try { + var uri = Services.io.newURI(uriString, null, null); + + if (allowJS && uri.scheme == "javascript") + return "javascript:" + + realm = uri.scheme + "://" + uri.host; + + // If the URI explicitly specified a port, only include it when + // it's not the default. (We never want "http://foo.com:80") + var port = uri.port; + if (port != -1) { + var handler = Services.io.getProtocolHandler(uri.scheme); + if (port != handler.defaultPort) + realm += ":" + port; + } + + } catch (e) { + // bug 159484 - disallow url types that don't support a hostPort. + // (although we handle "javascript:..." as a special case above.) + this.log("Couldn't parse origin for " + uriString); + realm = null; + } + + return realm; + }, + + + _getActionOrigin : function (form) { + var uriString = form.action; + + // A blank or mission action submits to where it came from. + if (uriString == "") + uriString = form.baseURI; // ala bug 297761 + + return this._getPasswordOrigin(uriString, true); + }, + + + /* ---------- Primary Public interfaces ---------- */ + + + /* + * fillForm + * + * Fill the form with login information if we can find it. + */ + fillForm : function (form) { + // XXX figure out what to do about fillForm + return false; + }, + + + /* + * addLogin + * + * Add a new login to login storage. + */ + addLogin : function (login) { + // Sanity check the login + if (login.hostname == null || login.hostname.length == 0) + throw "Can't add a login with a null or empty hostname."; + + // For logins w/o a username, set to "", not null. + if (login.username == null) + throw "Can't add a login with a null username."; + + if (login.password == null || login.password.length == 0) + throw "Can't add a login with a null or empty password."; + + if (login.formSubmitURL || login.formSubmitURL == "") { + // We have a form submit URL. Can't have a HTTP realm. + if (login.httpRealm != null) + throw "Can't add a login with both a httpRealm and formSubmitURL."; + } else if (login.httpRealm) { + // We have a HTTP realm. Can't have a form submit URL. + if (login.formSubmitURL != null) + throw "Can't add a login with both a httpRealm and formSubmitURL."; + } else { + // Need one or the other! + throw "Can't add a login without a httpRealm or formSubmitURL."; + } + + + // Look for an existing entry. + var logins = this.findLogins({}, login.hostname, login.formSubmitURL, + login.httpRealm); + + if (logins.some(function(l) login.matches(l, true))) + throw "This login already exists."; + + this.log("Adding login: " + login); + return this._storage.addLogin(login); + }, + + + /* + * removeLogin + * + * Remove the specified login from the stored logins. + */ + removeLogin : function (login) { + this.log("Removing login: " + login); + return this._storage.removeLogin(login); + }, + + + /* + * modifyLogin + * + * Change the specified login to match the new login. + */ + modifyLogin : function (oldLogin, newLogin) { + this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin); + return this._storage.modifyLogin(oldLogin, newLogin); + }, + + + /* + * getAllLogins + * + * Get a dump of all stored logins. Used by the login manager UI. + * + * |count| is only needed for XPCOM. + * + * Returns an array of logins. If there are no logins, the array is empty. + */ + getAllLogins : function (count) { + this.log("Getting a list of all logins"); + return this._storage.getAllLogins(count); + }, + + + /* + * removeAllLogins + * + * Remove all stored logins. + */ + removeAllLogins : function () { + this.log("Removing all logins"); + this._storage.removeAllLogins(); + }, + + /* + * getAllDisabledHosts + * + * Get a list of all hosts for which logins are disabled. + * + * |count| is only needed for XPCOM. + * + * Returns an array of disabled logins. If there are no disabled logins, + * the array is empty. + */ + getAllDisabledHosts : function (count) { + this.log("Getting a list of all disabled hosts"); + return this._storage.getAllDisabledHosts(count); + }, + + + /* + * findLogins + * + * Search for the known logins for entries matching the specified criteria. + */ + findLogins : function (count, hostname, formSubmitURL, httpRealm) { + this.log("Searching for logins matching host: " + hostname + + ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); + + return this._storage.findLogins(count, hostname, formSubmitURL, + httpRealm); + }, + + + /* + * searchLogins + * + * Public wrapper around _searchLogins to convert the nsIPropertyBag to a + * JavaScript object and decrypt the results. + * + * Returns an array of decrypted nsILoginInfo. + */ + searchLogins : function(count, matchData) { + this.log("Searching for logins"); + + return this._storage.searchLogins(count, matchData); + }, + + + /* + * countLogins + * + * Search for the known logins for entries matching the specified criteria, + * returns only the count. + */ + countLogins : function (hostname, formSubmitURL, httpRealm) { + this.log("Counting logins matching host: " + hostname + + ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); + + return this._storage.countLogins(hostname, formSubmitURL, httpRealm); + }, + + + /* + * uiBusy + */ + get uiBusy() { + return this._storage.uiBusy; + }, + + + /* + * getLoginSavingEnabled + * + * Check to see if user has disabled saving logins for the host. + */ + getLoginSavingEnabled : function (host) { + this.log("Checking if logins to " + host + " can be saved."); + if (!this._remember) + return false; + + return this._storage.getLoginSavingEnabled(host); + }, + + + /* + * setLoginSavingEnabled + * + * Enable or disable storing logins for the specified host. + */ + setLoginSavingEnabled : function (hostname, enabled) { + // Nulls won't round-trip with getAllDisabledHosts(). + if (hostname.indexOf("\0") != -1) + throw "Invalid hostname"; + + this.log("Saving logins for " + hostname + " enabled? " + enabled); + return this._storage.setLoginSavingEnabled(hostname, enabled); + }, + + + /* + * autoCompleteSearch + * + * Yuck. This is called directly by satchel: + * nsFormFillController::StartSearch() + * [toolkit/components/satchel/src/nsFormFillController.cpp] + * + * We really ought to have a simple way for code to register an + * auto-complete provider, and not have satchel calling pwmgr directly. + */ + autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) { + // aPreviousResult & aResult are nsIAutoCompleteResult, + // aElement is nsIDOMHTMLInputElement + + if (!this._remember) + return null; + + this.log("AutoCompleteSearch invoked. Search is: " + aSearchString); + + var result = null; + + if (aPreviousResult && + aSearchString.substr(0, aPreviousResult.searchString.length) == aPreviousResult.searchString) { + this.log("Using previous autocomplete result"); + result = aPreviousResult; + result.wrappedJSObject.searchString = aSearchString; + + // We have a list of results for a shorter search string, so just + // filter them further based on the new search string. + // Count backwards, because result.matchCount is decremented + // when we remove an entry. + for (var i = result.matchCount - 1; i >= 0; i--) { + var match = result.getValueAt(i); + + // Remove results that are too short, or have different prefix. + if (aSearchString.length > match.length || + aSearchString.toLowerCase() != + match.substr(0, aSearchString.length).toLowerCase()) + { + this.log("Removing autocomplete entry '" + match + "'"); + result.removeValueAt(i, false); + } + } + } else { + this.log("Creating new autocomplete search result."); + + var doc = aElement.ownerDocument; + var origin = this._getPasswordOrigin(doc.documentURI); + var actionOrigin = this._getActionOrigin(aElement.form); + + // This shouldn't trigger a master password prompt, because we + // don't attach to the input until after we successfully obtain + // logins for the form. + var logins = this.findLogins({}, origin, actionOrigin, null); + var matchingLogins = []; + + // Filter out logins that don't match the search prefix. Also + // filter logins without a username, since that's confusing to see + // in the dropdown and we can't autocomplete them anyway. + for (i = 0; i < logins.length; i++) { + var username = logins[i].username.toLowerCase(); + if (username && + aSearchString.length <= username.length && + aSearchString.toLowerCase() == + username.substr(0, aSearchString.length)) + { + matchingLogins.push(logins[i]); + } + } + this.log(matchingLogins.length + " autocomplete logins avail."); + result = new UserAutoCompleteResult(aSearchString, matchingLogins); + } + + return result; + } +}; // end of LoginManager implementation + + + + +// nsIAutoCompleteResult implementation +function UserAutoCompleteResult (aSearchString, matchingLogins) { + function loginSort(a,b) { + var userA = a.username.toLowerCase(); + var userB = b.username.toLowerCase(); + + if (userA < userB) + return -1; + + if (userB > userA) + return 1; + + return 0; + }; + + this.searchString = aSearchString; + this.logins = matchingLogins.sort(loginSort); + this.matchCount = matchingLogins.length; + + if (this.matchCount > 0) { + this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; + this.defaultIndex = 0; + } +} + +UserAutoCompleteResult.prototype = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, + Ci.nsISupportsWeakReference]), + + // private + logins : null, + + // Allow autoCompleteSearch to get at the JS object so it can + // modify some readonly properties for internal use. + get wrappedJSObject() { + return this; + }, + + // Interfaces from idl... + searchString : null, + searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH, + defaultIndex : -1, + errorDescription : "", + matchCount : 0, + + getValueAt : function (index) { + if (index < 0 || index >= this.logins.length) + throw "Index out of range."; + + return this.logins[index].username; + }, + + getLabelAt : function (index) { + return this.getValueAt(index); + }, + + getCommentAt : function (index) { + return ""; + }, + + getStyleAt : function (index) { + return ""; + }, + + getImageAt : function (index) { + return ""; + }, + + removeValueAt : function (index, removeFromDB) { + if (index < 0 || index >= this.logins.length) + throw "Index out of range."; + + var [removedLogin] = this.logins.splice(index, 1); + + this.matchCount--; + if (this.defaultIndex > this.logins.length) + this.defaultIndex--; + + if (removeFromDB) { + var pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeLogin(removedLogin); + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]); diff --git a/browser/metro/components/LoginManagerPrompter.idl b/browser/metro/components/LoginManagerPrompter.idl new file mode 100644 index 000000000000..fe1961842360 --- /dev/null +++ b/browser/metro/components/LoginManagerPrompter.idl @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#include "nsISupports.idl" + +interface nsILoginInfo; +interface nsIFrameLoaderOwner; + +[scriptable, uuid(3f385080-aef5-11df-94e2-0800200c9a66)] + +interface nsILoginManagerPrompter : nsISupports { + /** + * Initialize the prompter. Must be called before using other interfaces. + * + * @param aBrowser + * A browser element in which the user is doing some + * login-related action in need to prompt them for something. + * The prompt will be associated with browser. + */ + void init(in nsIFrameLoaderOwner aBrowser); + + /** + * Ask the user if they want to save a login (Yes, Never, Not Now) + * + * @param aLogin + * The login to be saved. + */ + void promptToSavePassword(in nsILoginInfo aLogin); + + /** + * Ask the user if they want to change a login's password. If the + * user consents, modifyLogin() will be called. + * + * @param aOldLogin + * The existing login (with the old password). + * @param aNewLogin + * The new login. + */ + void promptToChangePassword(in nsILoginInfo aOldLogin, + in nsILoginInfo aNewLogin); + + /** + * Ask the user if they want to change the password for one of + * multiple logins, when the caller can't determine exactly which + * login should be changed. If the user consents, modifyLogin() will + * be called. + * + * @param logins + * An array of existing logins. + * @param count + * (length of the array) + * @param aNewLogin + * The new login. + * + * Note: Because the caller does not know the username of the login + * to be changed, aNewLogin.username and aNewLogin.usernameField + * will be set (using the user's selection) before modifyLogin() + * is called. + */ + void promptToChangePasswordWithUsernames( + [array, size_is(count)] in nsILoginInfo logins, + in uint32_t count, + in nsILoginInfo aNewLogin); +}; + diff --git a/browser/metro/components/LoginManagerPrompter.js b/browser/metro/components/LoginManagerPrompter.js new file mode 100644 index 000000000000..35cebc46c806 --- /dev/null +++ b/browser/metro/components/LoginManagerPrompter.js @@ -0,0 +1,610 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +/* ==================== LoginManagerPrompter ==================== */ +/* + * LoginManagerPrompter + * + * Implements interfaces for prompting the user to enter/save/change auth info. + * + * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins + * found in HTML forms. + */ +function LoginManagerPrompter() { +} + +LoginManagerPrompter.prototype = { + + classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerPrompter]), + + _factory : null, + _browser : null, + _debug : false, // mirrors signon.debug + + __pwmgr : null, // Password Manager service + get _pwmgr() { + if (!this.__pwmgr) + this.__pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + return this.__pwmgr; + }, + + __promptService : null, // Prompt service for user interaction + get _promptService() { + if (!this.__promptService) + this.__promptService = + Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService2); + return this.__promptService; + }, + + __strBundle : null, // String bundle for L10N + get _strBundle() { + if (!this.__strBundle) { + var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + this.__strBundle = bunService.createBundle( + "chrome://passwordmgr/locale/passwordmgr.properties"); + if (!this.__strBundle) + throw "String bundle for Login Manager not present!"; + } + + return this.__strBundle; + }, + + __brandBundle : null, // String bundle for L10N + get _brandBundle() { + if (!this.__brandBundle) { + var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + this.__brandBundle = bunService.createBundle( + "chrome://branding/locale/brand.properties"); + if (!this.__brandBundle) + throw "Branding string bundle not present!"; + } + + return this.__brandBundle; + }, + + + __ellipsis : null, + get _ellipsis() { + if (!this.__ellipsis) { + this.__ellipsis = "\u2026"; + try { + this.__ellipsis = Services.prefs.getComplexValue( + "intl.ellipsis", Ci.nsIPrefLocalizedString).data; + } catch (e) { } + } + return this.__ellipsis; + }, + + + /* + * log + * + * Internal function for logging debug messages to the Error Console window. + */ + log : function (message) { + if (!this._debug) + return; + + dump("Pwmgr Prompter: " + message + "\n"); + Services.console.logStringMessage("Pwmgr Prompter: " + message); + }, + + + /* ---------- nsILoginManagerPrompter prompts ---------- */ + + + + + /* + * init + * + */ + init : function (aBrowser, aFactory) { + this._browser = aBrowser; + this._factory = aFactory || null; + + var prefBranch = Services.prefs.getBranch("signon."); + this._debug = prefBranch.getBoolPref("debug"); + this.log("===== initialized ====="); + }, + + + /* + * promptToSavePassword + * + */ + promptToSavePassword : function (aLogin) { + var notifyBox = this._getNotifyBox(); + + if (notifyBox) + this._showSaveLoginNotification(notifyBox, aLogin); + else + this._showSaveLoginDialog(aLogin); + }, + + + /* + * _showLoginNotification + * + * Displays a notification bar. + * + */ + _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) { + var oldBar = aNotifyBox.getNotificationWithValue(aName); + const priority = aNotifyBox.PRIORITY_INFO_MEDIUM; + + this.log("Adding new " + aName + " notification bar"); + var newBar = aNotifyBox.appendNotification( + aText, aName, + "chrome://mozapps/skin/passwordmgr/key.png", + priority, aButtons); + + // The page we're going to hasn't loaded yet, so we want to persist + // across the first location change. + newBar.persistence++; + + // Sites like Gmail perform a funky redirect dance before you end up + // at the post-authentication page. I don't see a good way to + // heuristically determine when to ignore such location changes, so + // we'll try ignoring location changes based on a time interval. + newBar.timeout = Date.now() + 20000; // 20 seconds + + if (oldBar) { + this.log("(...and removing old " + aName + " notification bar)"); + aNotifyBox.removeNotification(oldBar); + } + }, + + + /* + * _showSaveLoginNotification + * + * Displays a notification bar (rather than a popup), to allow the user to + * save the specified login. This allows the user to see the results of + * their login, and only save a login which they know worked. + * + */ + _showSaveLoginNotification : function (aNotifyBox, aLogin) { + + // Ugh. We can't use the strings from the popup window, because they + // have the access key marked in the string (eg "Mo&zilla"), along + // with some weird rules for handling access keys that do not occur + // in the string, for L10N. See commonDialog.js's setLabelForNode(). + var neverButtonText = + this._getLocalizedString("notifyBarNeverForSiteButtonText"); + var neverButtonAccessKey = + this._getLocalizedString("notifyBarNeverForSiteButtonAccessKey"); + var rememberButtonText = + this._getLocalizedString("notifyBarRememberButtonText"); + var rememberButtonAccessKey = + this._getLocalizedString("notifyBarRememberButtonAccessKey"); + var notNowButtonText = + this._getLocalizedString("notifyBarNotNowButtonText"); + var notNowButtonAccessKey = + this._getLocalizedString("notifyBarNotNowButtonAccessKey"); + + var brandShortName = + this._brandBundle.GetStringFromName("brandShortName"); + var displayHost = this._getShortDisplayHost(aLogin.hostname); + var notificationText; + if (aLogin.username) { + var displayUser = this._sanitizeUsername(aLogin.username); + notificationText = this._getLocalizedString( + "saveLoginText", + [brandShortName, displayUser, displayHost]); + } else { + notificationText = this._getLocalizedString( + "saveLoginTextNoUsername", + [brandShortName, displayHost]); + } + + // The callbacks in |buttons| have a closure to access the variables + // in scope here; set one to |this._pwmgr| so we can get back to pwmgr + // without a getService() call. + var pwmgr = this._pwmgr; + + + var buttons = [ + // "Remember" button + { + label: rememberButtonText, + accessKey: rememberButtonAccessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + pwmgr.addLogin(aLogin); + } + }, + + // "Never for this site" button + { + label: neverButtonText, + accessKey: neverButtonAccessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + pwmgr.setLoginSavingEnabled(aLogin.hostname, false); + } + }, + + // "Not now" button + { + label: notNowButtonText, + accessKey: notNowButtonAccessKey, + popup: null, + callback: function() { /* NOP */ } + } + ]; + + this._showLoginNotification(aNotifyBox, "password-save", + notificationText, buttons); + }, + + + /* + * _showSaveLoginDialog + * + * Called when we detect a new login in a form submission, + * asks the user what to do. + * + */ + _showSaveLoginDialog : function (aLogin) { + const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2); + + var brandShortName = + this._brandBundle.GetStringFromName("brandShortName"); + var displayHost = this._getShortDisplayHost(aLogin.hostname); + + var dialogText; + if (aLogin.username) { + var displayUser = this._sanitizeUsername(aLogin.username); + dialogText = this._getLocalizedString( + "saveLoginText", + [brandShortName, displayUser, displayHost]); + } else { + dialogText = this._getLocalizedString( + "saveLoginTextNoUsername", + [brandShortName, displayHost]); + } + var dialogTitle = this._getLocalizedString( + "savePasswordTitle"); + var neverButtonText = this._getLocalizedString( + "neverForSiteButtonText"); + var rememberButtonText = this._getLocalizedString( + "rememberButtonText"); + var notNowButtonText = this._getLocalizedString( + "notNowButtonText"); + + this.log("Prompting user to save/ignore login"); + var userChoice = this._promptService.confirmEx(null, + dialogTitle, dialogText, + buttonFlags, rememberButtonText, + notNowButtonText, neverButtonText, + null, {}); + // Returns: + // 0 - Save the login + // 1 - Ignore the login this time + // 2 - Never save logins for this site + if (userChoice == 2) { + this.log("Disabling " + aLogin.hostname + " logins by request."); + this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false); + } else if (userChoice == 0) { + this.log("Saving login for " + aLogin.hostname); + this._pwmgr.addLogin(aLogin); + } else { + // userChoice == 1 --> just ignore the login. + this.log("Ignoring login."); + } + }, + + /* + * promptToChangePassword + * + * Called when we think we detect a password change for an existing + * login, when the form being submitted contains multiple password + * fields. + * + */ + promptToChangePassword : function (aOldLogin, aNewLogin) { + var notifyBox = this._getNotifyBox(); + + if (notifyBox) + this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin.password); + else + this._showChangeLoginDialog(aOldLogin, aNewLogin.password); + }, + + /* + * _showChangeLoginNotification + * + * Shows the Change Password notification bar. + * + */ + _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewPassword) { + var notificationText; + if (aOldLogin.username) + notificationText = this._getLocalizedString( + "passwordChangeText", + [aOldLogin.username]); + else + notificationText = this._getLocalizedString( + "passwordChangeTextNoUser"); + + var changeButtonText = + this._getLocalizedString("notifyBarChangeButtonText"); + var changeButtonAccessKey = + this._getLocalizedString("notifyBarChangeButtonAccessKey"); + var dontChangeButtonText = + this._getLocalizedString("notifyBarDontChangeButtonText"); + var dontChangeButtonAccessKey = + this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); + + // The callbacks in |buttons| have a closure to access the variables + // in scope here; set one to |this._pwmgr| so we can get back to pwmgr + // without a getService() call. + var self = this; + + var buttons = [ + // "Yes" button + { + label: changeButtonText, + accessKey: changeButtonAccessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + self._updateLogin(aOldLogin, aNewPassword); + } + }, + + // "No" button + { + label: dontChangeButtonText, + accessKey: dontChangeButtonAccessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + // do nothing + } + } + ]; + + this._showLoginNotification(aNotifyBox, "password-change", + notificationText, buttons); + }, + + /* + * _showChangeLoginDialog + * + * Shows the Change Password dialog. + * + */ + _showChangeLoginDialog : function (aOldLogin, aNewPassword) { + const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; + + var dialogText; + if (aOldLogin.username) + dialogText = this._getLocalizedString( + "passwordChangeText", + [aOldLogin.username]); + else + dialogText = this._getLocalizedString( + "passwordChangeTextNoUser"); + + var dialogTitle = this._getLocalizedString( + "passwordChangeTitle"); + + // returns 0 for yes, 1 for no. + var ok = !this._promptService.confirmEx(null, + dialogTitle, dialogText, buttonFlags, + null, null, null, + null, {}); + if (ok) { + this.log("Updating password for user " + aOldLogin.username); + this._updateLogin(aOldLogin, aNewPassword); + } + }, + + + /* + * promptToChangePasswordWithUsernames + * + * Called when we detect a password change in a form submission, but we + * don't know which existing login (username) it's for. Asks the user + * to select a username and confirm the password change. + * + * Note: The caller doesn't know the username for aNewLogin, so this + * function fills in .username and .usernameField with the values + * from the login selected by the user. + * + * Note; XPCOM stupidity: |count| is just |logins.length|. + */ + promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) { + const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; + + var usernames = logins.map(function (l) l.username); + var dialogText = this._getLocalizedString("userSelectText"); + var dialogTitle = this._getLocalizedString("passwordChangeTitle"); + var selectedIndex = { value: null }; + + // If user selects ok, outparam.value is set to the index + // of the selected username. + var ok = this._promptService.select(null, + dialogTitle, dialogText, + usernames.length, usernames, + selectedIndex); + if (ok) { + // Now that we know which login to use, modify its password. + var selectedLogin = logins[selectedIndex.value]; + this.log("Updating password for user " + selectedLogin.username); + this._updateLogin(selectedLogin, aNewLogin.password); + } + }, + + + + + /* ---------- Internal Methods ---------- */ + + + + + /* + * _updateLogin + */ + _updateLogin : function (login, newPassword) { + var now = Date.now(); + var propBag = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + if (newPassword) { + propBag.setProperty("password", newPassword); + // Explicitly set the password change time here (even though it would + // be changed automatically), to ensure that it's exactly the same + // value as timeLastUsed. + propBag.setProperty("timePasswordChanged", now); + } + propBag.setProperty("timeLastUsed", now); + propBag.setProperty("timesUsedIncrement", 1); + this._pwmgr.modifyLogin(login, propBag); + }, + + /* + * _getNotifyBox + * + * Returns the notification box to this prompter, or null if there isn't + * a notification box available. + */ + _getNotifyBox : function () { + let notifyBox = null; + try { + let chromeWin = this._browser.ownerDocument.defaultView; + if (chromeWin.getNotificationBox) { + notifyBox = chromeWin.getNotificationBox(this._browser); + } else { + this.log("getNotificationBox() not available on window"); + } + + } catch (e) { + // If any errors happen, just assume no notification box. + this.log("No notification box available: " + e) + } + return notifyBox; + }, + + + /* + * _getLocalizedString + * + * Can be called as: + * _getLocalizedString("key1"); + * _getLocalizedString("key2", ["arg1"]); + * _getLocalizedString("key3", ["arg1", "arg2"]); + * (etc) + * + * Returns the localized string for the specified key, + * formatted if required. + * + */ + _getLocalizedString : function (key, formatArgs) { + if (formatArgs) + return this._strBundle.formatStringFromName( + key, formatArgs, formatArgs.length); + else + return this._strBundle.GetStringFromName(key); + }, + + + /* + * _sanitizeUsername + * + * Sanitizes the specified username, by stripping quotes and truncating if + * it's too long. This helps prevent an evil site from messing with the + * "save password?" prompt too much. + */ + _sanitizeUsername : function (username) { + if (username.length > 30) { + username = username.substring(0, 30); + username += this._ellipsis; + } + return username.replace(/['"]/g, ""); + }, + + + /* + * _getFormattedHostname + * + * The aURI parameter may either be a string uri, or an nsIURI instance. + * + * Returns the hostname to use in a nsILoginInfo object (for example, + * "http://example.com"). + */ + _getFormattedHostname : function (aURI) { + var uri; + if (aURI instanceof Ci.nsIURI) { + uri = aURI; + } else { + uri = Services.io.newURI(aURI, null, null); + } + var scheme = uri.scheme; + + var hostname = scheme + "://" + uri.host; + + // If the URI explicitly specified a port, only include it when + // it's not the default. (We never want "http://foo.com:80") + port = uri.port; + if (port != -1) { + var handler = Services.io.getProtocolHandler(scheme); + if (port != handler.defaultPort) + hostname += ":" + port; + } + + return hostname; + }, + + + /* + * _getShortDisplayHost + * + * Converts a login's hostname field (a URL) to a short string for + * prompting purposes. Eg, "http://foo.com" --> "foo.com", or + * "ftp://www.site.co.uk" --> "site.co.uk". + */ + _getShortDisplayHost: function (aURIString) { + var displayHost; + + var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. + getService(Ci.nsIEffectiveTLDService); + var idnService = Cc["@mozilla.org/network/idn-service;1"]. + getService(Ci.nsIIDNService); + try { + var uri = Services.io.newURI(aURIString, null, null); + var baseDomain = eTLDService.getBaseDomain(uri); + displayHost = idnService.convertToDisplayIDN(baseDomain, {}); + } catch (e) { + this.log("_getShortDisplayHost couldn't process " + aURIString); + } + + if (!displayHost) + displayHost = aURIString; + + return displayHost; + }, + +}; // end of LoginManagerPrompter implementation + + +var component = [LoginManagerPrompter]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); + diff --git a/browser/metro/components/Makefile.in b/browser/metro/components/Makefile.in new file mode 100644 index 000000000000..1ebcbb650cd0 --- /dev/null +++ b/browser/metro/components/Makefile.in @@ -0,0 +1,53 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/config/config.mk + +# metro/components.manifest +MODULE = components +XPIDL_MODULE = components + +XPIDLSRCS = \ + SessionStore.idl \ + LoginManagerPrompter.idl \ + $(NULL) + +EXTRA_PP_COMPONENTS = \ + components.manifest \ + AboutRedirector.js \ + BrowserCLH.js \ + BrowserStartup.js \ + DirectoryProvider.js\ + HelperAppDialog.js \ + Sidebar.js \ + SessionStore.js \ + $(NULL) + +EXTRA_COMPONENTS = \ + AlertsService.js \ + ContentPermissionPrompt.js \ + DownloadManagerUI.js \ + PromptService.js \ + ContentDispatchChooser.js \ + FormAutoComplete.js \ + LoginManager.js \ + LoginManagerPrompter.js \ + CapturePicker.js \ + $(NULL) + +ifdef MOZ_SAFE_BROWSING +EXTRA_COMPONENTS += SafeBrowsing.js +endif + +ifdef MOZ_UPDATER +EXTRA_COMPONENTS += UpdatePrompt.js +endif + +include $(topsrcdir)/config/rules.mk diff --git a/browser/metro/components/PromptService.js b/browser/metro/components/PromptService.js new file mode 100644 index 000000000000..66d21c0c196c --- /dev/null +++ b/browser/metro/components/PromptService.js @@ -0,0 +1,917 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// Whitelist of methods we remote - to check against malicious data. +// For example, it would be dangerous to allow content to show auth prompts. +const REMOTABLE_METHODS = { + alert: { outParams: [] }, + alertCheck: { outParams: [4] }, + confirm: { outParams: [] }, + prompt: { outParams: [3, 5] }, + confirmEx: { outParams: [8] }, + confirmCheck: { outParams: [4] }, + select: { outParams: [5] } +}; + +var gPromptService = null; + +function PromptService() { + // Depending on if we are in the parent or child, prepare to remote + // certain calls + var appInfo = Cc["@mozilla.org/xre/app-info;1"]; + if (!appInfo || appInfo.getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + // Parent process + this.inContentProcess = false; + + // Used for wakeups service. FIXME: clean up with bug 593407 + this.wrappedJSObject = this; + + // Setup listener for child messages. We don't need to call + // addMessageListener as the wakeup service will do that for us. + this.receiveMessage = function(aMessage) { + var json = aMessage.json; + switch (aMessage.name) { + case "Prompt:Call": + var method = aMessage.json.method; + if (!REMOTABLE_METHODS.hasOwnProperty(method)) + throw "PromptServiceRemoter received an invalid method "+method; + + var arguments = aMessage.json.arguments; + var ret = this[method].apply(this, arguments); + // Return multiple return values in objects of form { value: ... }, + // and also with the actual return value at the end + arguments.push(ret); + return arguments; + } + }; + } else { + // Child process + this.inContentProcess = true; + } + + gPromptService = this; +} + +PromptService.prototype = { + classID: Components.ID("{9a61149b-2276-4a0a-b79c-be994ad106cf}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIPromptService, Ci.nsIPromptService2]), + + /* ---------- nsIPromptFactory ---------- */ + + // XXX Copied from nsPrompter.js. + getPrompt: function getPrompt(domWin, iid) { + if (this.inContentProcess) + return ContentPrompt.QueryInterface(iid); + + let doc = this.getDocument(); + if (!doc) { + let fallback = this._getFallbackService(); + return fallback.getPrompt(domWin, iid); + } + + let p = new Prompt(domWin, doc); + p.QueryInterface(iid); + return p; + }, + + /* ---------- private memebers ---------- */ + + _getFallbackService: function _getFallbackService() { + return Components.classesByID["{7ad1b327-6dfa-46ec-9234-f2a620ea7e00}"] + .getService(Ci.nsIPromptService); + }, + + getDocument: function getDocument() { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + return win ? win.document : null; + }, + + // nsIPromptService and nsIPromptService2 methods proxy to our Prompt class + // if we can show in-document popups, or to the fallback service otherwise. + callProxy: function(aMethod, aArguments) { + let prompt; + if (this.inContentProcess) { + // Bring this tab to the front, so prompt appears on the right tab + var window = aArguments[0]; + if (window && window.document) { + var event = window.document.createEvent("Events"); + event.initEvent("DOMWillOpenModalDialog", true, true); + let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + winUtils.dispatchEventToChromeOnly(window, event); + } + prompt = ContentPrompt; + } else { + let doc = this.getDocument(); + if (!doc) { + let fallback = this._getFallbackService(); + return fallback[aMethod].apply(fallback, aArguments); + } + let domWin = aArguments[0]; + prompt = new Prompt(domWin, doc); + } + return prompt[aMethod].apply(prompt, Array.prototype.slice.call(aArguments, 1)); + }, + + /* ---------- nsIPromptService ---------- */ + + alert: function() { + return this.callProxy("alert", arguments); + }, + alertCheck: function() { + return this.callProxy("alertCheck", arguments); + }, + confirm: function() { + return this.callProxy("confirm", arguments); + }, + confirmCheck: function() { + return this.callProxy("confirmCheck", arguments); + }, + confirmEx: function() { + return this.callProxy("confirmEx", arguments); + }, + prompt: function() { + return this.callProxy("prompt", arguments); + }, + promptUsernameAndPassword: function() { + return this.callProxy("promptUsernameAndPassword", arguments); + }, + promptPassword: function() { + return this.callProxy("promptPassword", arguments); + }, + select: function() { + return this.callProxy("select", arguments); + }, + + /* ---------- nsIPromptService2 ---------- */ + + promptAuth: function() { + return this.callProxy("promptAuth", arguments); + }, + asyncPromptAuth: function() { + return this.callProxy("asyncPromptAuth", arguments); + } +}; + +// Implementation of nsIPrompt that just forwards to the parent process. +let ContentPrompt = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + + sendMessage: function sendMessage(aMethod) { + let args = Array.prototype.slice.call(arguments); + args[0] = null; // No need to pass "window" argument to the prompt service. + + // We send all prompts as sync, even alert (which has no important + // return value), as otherwise program flow will continue, and the + // script can theoretically show several alerts at once. In particular + // this can lead to a bug where you cannot click the earlier one, which + // is now hidden by a new one (and Fennec is helplessly frozen). + var json = { method: aMethod, arguments: args }; + var response = this.messageManager.sendSyncMessage("Prompt:Call", json)[0]; + + // Args copying - for methods that have out values + REMOTABLE_METHODS[aMethod].outParams.forEach(function(i) { + args[i].value = response[i].value; + }); + return response.pop(); // final return value was given at the end + } +}; + +XPCOMUtils.defineLazyServiceGetter(ContentPrompt, "messageManager", + "@mozilla.org/childprocessmessagemanager;1", Ci.nsISyncMessageSender); + +// Add remotable methods to ContentPrompt. +for (let [method, _] in Iterator(REMOTABLE_METHODS)) { + ContentPrompt[method] = ContentPrompt.sendMessage.bind(ContentPrompt, method); +} + +function Prompt(aDomWin, aDocument) { + this._domWin = aDomWin; + this._doc = aDocument; +} + +Prompt.prototype = { + _domWin: null, + _doc: null, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt, Ci.nsIAuthPrompt, Ci.nsIAuthPrompt2]), + + /* ---------- internal methods ---------- */ + + openDialog: function openDialog(aSrc, aParams) { + let browser = Services.wm.getMostRecentWindow("navigator:browser"); + return browser.DialogUI.importModal(this._domWin, aSrc, aParams); + }, + + _setupPrompt: function setupPrompt(aDoc, aType, aTitle, aText, aCheck) { + aDoc.getElementById("prompt-" + aType + "-title").appendChild(aDoc.createTextNode(aTitle)); + aDoc.getElementById("prompt-" + aType + "-message").appendChild(aDoc.createTextNode(aText)); + + if (aCheck && aCheck.msg) { + aDoc.getElementById("prompt-" + aType + "-checkbox").checked = aCheck.value; + this.setLabelForNode(aDoc.getElementById("prompt-" + aType + "-checkbox-label"), aCheck.msg); + aDoc.getElementById("prompt-" + aType + "-checkbox").removeAttribute("collapsed"); + } + }, + + commonPrompt: function commonPrompt(aTitle, aText, aValue, aCheckMsg, aCheckState, isPassword) { + var params = new Object(); + params.result = false; + params.checkbox = aCheckState; + params.value = aValue; + + let dialog = this.openDialog("chrome://browser/content/prompt/prompt.xul", params); + let doc = this._doc; + this._setupPrompt(doc, "prompt", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg}); + doc.getElementById("prompt-prompt-textbox").value = aValue.value; + if (isPassword) + doc.getElementById("prompt-prompt-textbox").type = "password"; + + dialog.waitForClose(); + return params.result; + }, + + // + // Copied from chrome://global/content/commonDialog.js + // + setLabelForNode: function setLabelForNode(aNode, aLabel) { + // This is for labels which may contain embedded access keys. + // If we end in (&X) where X represents the access key, optionally preceded + // by spaces and/or followed by the ':' character, store the access key and + // remove the access key placeholder + leading spaces from the label. + // Otherwise a character preceded by one but not two &s is the access key. + // Store it and remove the &. + + // Note that if you change the following code, see the comment of + // nsTextBoxFrame::UpdateAccessTitle. + + if (!aLabel) + return; + + var accessKey = null; + if (/ *\(\&([^&])\)(:)?$/.test(aLabel)) { + aLabel = RegExp.leftContext + RegExp.$2; + accessKey = RegExp.$1; + } else if (/^(.*[^&])?\&(([^&]).*$)/.test(aLabel)) { + aLabel = RegExp.$1 + RegExp.$2; + accessKey = RegExp.$3; + } + + // && is the magic sequence to embed an & in your label. + aLabel = aLabel.replace(/\&\&/g, "&"); + if (aNode instanceof Ci.nsIDOMXULLabelElement) { + aNode.setAttribute("value", aLabel); + } else if (aNode instanceof Ci.nsIDOMXULDescriptionElement) { + let text = aNode.ownerDocument.createTextNode(aLabel); + aNode.appendChild(text); + } else { // Set text for other xul elements + aNode.setAttribute("label", aLabel); + } + + // XXXjag bug 325251 + // Need to set this after aNode.setAttribute("value", aLabel); + if (accessKey) + aNode.setAttribute("accesskey", accessKey); + }, + + /* + * ---------- interface disambiguation ---------- + * + * XXX Copied from nsPrompter.js. + * + * nsIPrompt and nsIAuthPrompt share 3 method names with slightly + * different arguments. All but prompt() have the same number of + * arguments, so look at the arg types to figure out how we're being + * called. :-( + */ + prompt: function prompt() { + if (gPromptService.inContentProcess) + return gPromptService.callProxy("prompt", [null].concat(Array.prototype.slice.call(arguments))); + + // also, the nsIPrompt flavor has 5 args instead of 6. + if (typeof arguments[2] == "object") + return this.nsIPrompt_prompt.apply(this, arguments); + else + return this.nsIAuthPrompt_prompt.apply(this, arguments); + }, + + promptUsernameAndPassword: function promptUsernameAndPassword() { + // Both have 6 args, so use types. + if (typeof arguments[2] == "object") + return this.nsIPrompt_promptUsernameAndPassword.apply(this, arguments); + else + return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments); + }, + + promptPassword: function promptPassword() { + // Both have 5 args, so use types. + if (typeof arguments[2] == "object") + return this.nsIPrompt_promptPassword.apply(this, arguments); + else + return this.nsIAuthPrompt_promptPassword.apply(this, arguments); + }, + + /* ---------- nsIPrompt ---------- */ + + alert: function alert(aTitle, aText) { + let dialog = this.openDialog("chrome://browser/content/prompt/alert.xul", null); + let doc = this._doc; + this._setupPrompt(doc, "alert", aTitle, aText); + + dialog.waitForClose(); + }, + + alertCheck: function alertCheck(aTitle, aText, aCheckMsg, aCheckState) { + let dialog = this.openDialog("chrome://browser/content/prompt/alert.xul", aCheckState); + let doc = this._doc; + this._setupPrompt(doc, "alert", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg}); + dialog.waitForClose(); + }, + + confirm: function confirm(aTitle, aText) { + var params = new Object(); + params.result = false; + + let dialog = this.openDialog("chrome://browser/content/prompt/confirm.xul", params); + let doc = this._doc; + this._setupPrompt(doc, "confirm", aTitle, aText); + + dialog.waitForClose(); + return params.result; + }, + + confirmCheck: function confirmCheck(aTitle, aText, aCheckMsg, aCheckState) { + var params = new Object(); + params.result = false; + params.checkbox = aCheckState; + + let dialog = this.openDialog("chrome://browser/content/prompt/confirm.xul", params); + let doc = this._doc; + this._setupPrompt(doc, "confirm", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg}); + + dialog.waitForClose(); + return params.result; + }, + + confirmEx: function confirmEx(aTitle, aText, aButtonFlags, aButton0, + aButton1, aButton2, aCheckMsg, aCheckState) { + + let numButtons = 0; + let titles = [aButton0, aButton1, aButton2]; + + let defaultButton = 0; + if (aButtonFlags & Ci.nsIPromptService.BUTTON_POS_1_DEFAULT) + defaultButton = 1; + if (aButtonFlags & Ci.nsIPromptService.BUTTON_POS_2_DEFAULT) + defaultButton = 2; + + var params = { + result: false, + checkbox: aCheckState, + defaultButton: defaultButton + } + + let dialog = this.openDialog("chrome://browser/content/prompt/confirm.xul", params); + let doc = this._doc; + this._setupPrompt(doc, "confirm", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg}); + + let bbox = doc.getElementById("prompt-confirm-buttons-box"); + while (bbox.lastChild) + bbox.removeChild(bbox.lastChild); + + for (let i = 0; i < 3; i++) { + let bTitle = null; + switch (aButtonFlags & 0xff) { + case Ci.nsIPromptService.BUTTON_TITLE_OK : + bTitle = PromptUtils.getLocaleString("OK"); + break; + case Ci.nsIPromptService.BUTTON_TITLE_CANCEL : + bTitle = PromptUtils.getLocaleString("Cancel"); + break; + case Ci.nsIPromptService.BUTTON_TITLE_YES : + bTitle = PromptUtils.getLocaleString("Yes"); + break; + case Ci.nsIPromptService.BUTTON_TITLE_NO : + bTitle = PromptUtils.getLocaleString("No"); + break; + case Ci.nsIPromptService.BUTTON_TITLE_SAVE : + bTitle = PromptUtils.getLocaleString("Save"); + break; + case Ci.nsIPromptService.BUTTON_TITLE_DONT_SAVE : + bTitle = PromptUtils.getLocaleString("DontSave"); + break; + case Ci.nsIPromptService.BUTTON_TITLE_REVERT : + bTitle = PromptUtils.getLocaleString("Revert"); + break; + case Ci.nsIPromptService.BUTTON_TITLE_IS_STRING : + bTitle = titles[i]; + break; + } + + if (bTitle) { + let button = doc.createElement("button"); + button.className = "prompt-button"; + this.setLabelForNode(button, bTitle); + if (i == defaultButton) { + button.setAttribute("command", "cmd_ok"); + } + else { + button.setAttribute("oncommand", + "document.getElementById('prompt-confirm-dialog').PromptHelper.closeConfirm(" + i + ")"); + } + bbox.appendChild(button); + } + + aButtonFlags >>= 8; + } + + dialog.waitForClose(); + return params.result; + }, + + nsIPrompt_prompt: function nsIPrompt_prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) { + return this.commonPrompt(aTitle, aText, aValue, aCheckMsg, aCheckState, false); + }, + + nsIPrompt_promptPassword: function nsIPrompt_promptPassword( + aTitle, aText, aPassword, aCheckMsg, aCheckState) { + return this.commonPrompt(aTitle, aText, aPassword, aCheckMsg, aCheckState, true); + }, + + nsIPrompt_promptUsernameAndPassword: function nsIPrompt_promptUsernameAndPassword( + aTitle, aText, aUsername, aPassword, aCheckMsg, aCheckState) { + var params = new Object(); + params.result = false; + params.checkbox = aCheckState; + params.user = aUsername; + params.password = aPassword; + + let dialog = this.openDialog("chrome://browser/content/prompt/promptPassword.xul", params); + let doc = this._doc; + this._setupPrompt(doc, "password", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg}); + + doc.getElementById("prompt-password-user").value = aUsername.value; + doc.getElementById("prompt-password-password").value = aPassword.value; + + dialog.waitForClose(); + return params.result; + }, + + select: function select(aTitle, aText, aCount, aSelectList, aOutSelection) { + var params = new Object(); + params.result = false; + params.selection = aOutSelection; + + let dialog = this.openDialog("chrome://browser/content/prompt/select.xul", params); + let doc = this._doc; + this._setupPrompt(doc, "select", aTitle, aText); + + let list = doc.getElementById("prompt-select-list"); + for (let i = 0; i < aCount; i++) + list.appendItem(aSelectList[i], null, null); + + // select the first one + list.selectedIndex = 0; + + dialog.waitForClose(); + return params.result; + }, + + /* ---------- nsIAuthPrompt ---------- */ + + nsIAuthPrompt_prompt : function (title, text, passwordRealm, savePassword, defaultText, result) { + // TODO: Port functions from nsLoginManagerPrompter.js to here + if (defaultText) + result.value = defaultText; + return this.nsIPrompt_prompt(title, text, result, null, {}); + }, + + nsIAuthPrompt_promptUsernameAndPassword : function (aTitle, aText, aPasswordRealm, aSavePassword, aUser, aPass) { + return nsIAuthPrompt_loginPrompt(aTitle, aText, aPasswordRealm, aSavePassword, aUser, aPass); + }, + + nsIAuthPrompt_promptPassword : function (aTitle, aText, aPasswordRealm, aSavePassword, aPass) { + return nsIAuthPrompt_loginPrompt(aTitle, aText, aPasswordRealm, aSavePassword, null, aPass); + }, + + nsIAuthPrompt_loginPrompt: function(aTitle, aPasswordRealm, aSavePassword, aUser, aPass) { + let checkMsg = null; + let check = { value: false }; + let [hostname, realm, aUser] = PromptUtils.getHostnameAndRealm(aPasswordRealm); + + let canSave = PromptUtils.canSaveLogin(hostname, aSavePassword); + if (canSave) { + // Look for existing logins. + let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, realm); + [checkMsg, check] = PromptUtils.getUsernameAndPassword(foundLogins, aUser, aPass); + } + + let ok = false; + if (aUser) + ok = this.nsIPrompt_promptUsernameAndPassword(aTitle, aText, aUser, aPass, checkMsg, check); + else + ok = this.nsIPrompt_promptPassword(aTitle, aText, aPass, checkMsg, check); + + if (ok && canSave && check.value) + PromptUtils.savePassword(hostname, realm, aUser, aPass); + + return ok; }, + + /* ---------- nsIAuthPrompt2 ---------- */ + + promptAuth: function promptAuth(aChannel, aLevel, aAuthInfo) { + let checkMsg = null; + let check = { value: false }; + let message = PromptUtils.makeDialogText(aChannel, aAuthInfo); + let [username, password] = PromptUtils.getAuthInfo(aAuthInfo); + let [hostname, httpRealm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo); + let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, httpRealm); + + let canSave = PromptUtils.canSaveLogin(hostname, null); + if (canSave) + [checkMsg, check] = PromptUtils.getUsernameAndPassword(foundLogins, username, password); + + if (username.value && password.value) { + PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value); + } + + let canAutologin = false; + if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY && + !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) && + Services.prefs.getBoolPref("signon.autologin.proxy")) + canAutologin = true; + + let ok = canAutologin; + if (!ok && aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) + ok = this.nsIPrompt_promptPassword(null, message, password, checkMsg, check); + else if (!ok) + ok = this.nsIPrompt_promptUsernameAndPassword(null, message, username, password, checkMsg, check); + + PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value); + + if (ok && canSave && check.value) + PromptUtils.savePassword(foundLogins, username, password, hostname, httpRealm); + + return ok; + }, + + _asyncPrompts: {}, + _asyncPromptInProgress: false, + + _doAsyncPrompt : function() { + if (this._asyncPromptInProgress) + return; + + // Find the first prompt key we have in the queue + let hashKey = null; + for (hashKey in this._asyncPrompts) + break; + + if (!hashKey) + return; + + // If login manger has logins for this host, defer prompting if we're + // already waiting on a master password entry. + let prompt = this._asyncPrompts[hashKey]; + let prompter = prompt.prompter; + let [hostname, httpRealm] = PromptUtils.getAuthTarget(prompt.channel, prompt.authInfo); + let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, httpRealm); + if (foundLogins.length > 0 && PromptUtils.pwmgr.uiBusy) + return; + + this._asyncPromptInProgress = true; + prompt.inProgress = true; + + let self = this; + + let runnable = { + run: function() { + let ok = false; + try { + ok = prompter.promptAuth(prompt.channel, prompt.level, prompt.authInfo); + } catch (e) { + Cu.reportError("_doAsyncPrompt:run: " + e + "\n"); + } + + delete self._asyncPrompts[hashKey]; + prompt.inProgress = false; + self._asyncPromptInProgress = false; + + for each (let consumer in prompt.consumers) { + if (!consumer.callback) + // Not having a callback means that consumer didn't provide it + // or canceled the notification + continue; + + try { + if (ok) + consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo); + else + consumer.callback.onAuthCancelled(consumer.context, true); + } catch (e) { /* Throw away exceptions caused by callback */ } + } + self._doAsyncPrompt(); + } + } + + Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL); + }, + + asyncPromptAuth: function asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) { + let cancelable = null; + try { + // If the user submits a login but it fails, we need to remove the + // notification bar that was displayed. Conveniently, the user will + // be prompted for authentication again, which brings us here. + //this._removeLoginNotifications(); + + cancelable = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]), + callback: aCallback, + context: aContext, + cancel: function() { + this.callback.onAuthCancelled(this.context, false); + this.callback = null; + this.context = null; + } + }; + let [hostname, httpRealm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo); + let hashKey = aLevel + "|" + hostname + "|" + httpRealm; + let asyncPrompt = this._asyncPrompts[hashKey]; + if (asyncPrompt) { + asyncPrompt.consumers.push(cancelable); + return cancelable; + } + + asyncPrompt = { + consumers: [cancelable], + channel: aChannel, + authInfo: aAuthInfo, + level: aLevel, + inProgress : false, + prompter: this + } + + this._asyncPrompts[hashKey] = asyncPrompt; + this._doAsyncPrompt(); + } catch (e) { + Cu.reportError("PromptService: " + e + "\n"); + throw e; + } + return cancelable; + } +}; + +let PromptUtils = { + getLocaleString: function pu_getLocaleString(aKey, aService) { + if (aService == "passwdmgr") + return this.passwdBundle.GetStringFromName(aKey); + + return this.bundle.GetStringFromName(aKey); + }, + + get pwmgr() { + delete this.pwmgr; + return this.pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + }, + + getHostnameAndRealm: function pu_getHostnameAndRealm(aRealmString) { + let httpRealm = /^.+ \(.+\)$/; + if (httpRealm.test(aRealmString)) + return [null, null, null]; + + let uri = Services.io.newURI(aRealmString, null, null); + let pathname = ""; + + if (uri.path != "/") + pathname = uri.path; + + let formattedHostname = this._getFormattedHostname(uri); + return [formattedHostname, formattedHostname + pathname, uri.username]; + }, + + canSaveLogin: function pu_canSaveLogin(aHostname, aSavePassword) { + let canSave = !this._inPrivateBrowsing && this.pwmgr.getLoginSavingEnabled(aHostname) + if (aSavePassword) + canSave = canSave && (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) + return canSave; + }, + + getUsernameAndPassword: function pu_getUsernameAndPassword(aFoundLogins, aUser, aPass) { + let checkLabel = null; + let check = { value: false }; + let selectedLogin; + + checkLabel = this.getLocaleString("rememberPassword", "passwdmgr"); + + // XXX Like the original code, we can't deal with multiple + // account selection. (bug 227632) + if (aFoundLogins.length > 0) { + selectedLogin = aFoundLogins[0]; + + // If the caller provided a username, try to use it. If they + // provided only a password, this will try to find a password-only + // login (or return null if none exists). + if (aUser.value) + selectedLogin = this.findLogin(aFoundLogins, "username", aUser.value); + + if (selectedLogin) { + check.value = true; + aUser.value = selectedLogin.username; + // If the caller provided a password, prefer it. + if (!aPass.value) + aPass.value = selectedLogin.password; + } + } + + return [checkLabel, check]; + }, + + findLogin: function pu_findLogin(aLogins, aName, aValue) { + for (let i = 0; i < aLogins.length; i++) + if (aLogins[i][aName] == aValue) + return aLogins[i]; + return null; + }, + + savePassword: function pu_savePassword(aLogins, aUser, aPass, aHostname, aRealm) { + let selectedLogin = this.findLogin(aLogins, "username", aUser.value); + + // If we didn't find an existing login, or if the username + // changed, save as a new login. + if (!selectedLogin) { + // add as new + var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + newLogin.init(aHostname, null, aRealm, aUser.value, aPass.value, "", ""); + this.pwmgr.addLogin(newLogin); + } else if (aPass.value != selectedLogin.password) { + // update password + this.updateLogin(selectedLogin, aPass.value); + } else { + this.updateLogin(selectedLogin); + } + }, + + updateLogin: function pu_updateLogin(aLogin, aPassword) { + let now = Date.now(); + let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag); + if (aPassword) { + propBag.setProperty("password", aPassword); + // Explicitly set the password change time here (even though it would + // be changed automatically), to ensure that it's exactly the same + // value as timeLastUsed. + propBag.setProperty("timePasswordChanged", now); + } + propBag.setProperty("timeLastUsed", now); + propBag.setProperty("timesUsedIncrement", 1); + + this.pwmgr.modifyLogin(aLogin, propBag); + }, + + // JS port of http://mxr.mozilla.org/mozilla-central/source/embedding/components/windowwatcher/src/nsPrompt.cpp#388 + makeDialogText: function pu_makeDialogText(aChannel, aAuthInfo) { + let isProxy = (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY); + let isPassOnly = (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD); + + let username = aAuthInfo.username; + let [displayHost, realm] = this.getAuthTarget(aChannel, aAuthInfo); + + // Suppress "the site says: $realm" when we synthesized a missing realm. + if (!aAuthInfo.realm && !isProxy) + realm = ""; + + // Trim obnoxiously long realms. + if (realm.length > 150) { + realm = realm.substring(0, 150); + // Append "..." (or localized equivalent). + realm += this.ellipsis; + } + + let text; + if (isProxy) + text = this.bundle.formatStringFromName("EnterLoginForProxy", [realm, displayHost], 2); + else if (isPassOnly) + text = this.bundle.formatStringFromName("EnterPasswordFor", [username, displayHost], 2); + else if (!realm) + text = this.bundle.formatStringFromName("EnterUserPasswordFor", [displayHost], 1); + else + text = this.bundle.formatStringFromName("EnterLoginForRealm", [realm, displayHost], 2); + + return text; + }, + + // JS port of http://mxr.mozilla.org/mozilla-central/source/embedding/components/windowwatcher/public/nsPromptUtils.h#89 + getAuthHostPort: function pu_getAuthHostPort(aChannel, aAuthInfo) { + let uri = aChannel.URI; + let res = { host: null, port: -1 }; + if (aAuthInfo.flags & aAuthInfo.AUTH_PROXY) { + let proxy = aChannel.QueryInterface(Ci.nsIProxiedChannel); + res.host = proxy.proxyInfo.host; + res.port = proxy.proxyInfo.port; + } else { + res.host = uri.host; + res.port = uri.port; + } + return res; + }, + + getAuthTarget : function pu_getAuthTarget(aChannel, aAuthInfo) { + let hostname, realm; + // If our proxy is demanding authentication, don't use the + // channel's actual destination. + if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { + if (!(aChannel instanceof Ci.nsIProxiedChannel)) + throw "proxy auth needs nsIProxiedChannel"; + + let info = aChannel.proxyInfo; + if (!info) + throw "proxy auth needs nsIProxyInfo"; + + // Proxies don't have a scheme, but we'll use "moz-proxy://" + // so that it's more obvious what the login is for. + let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); + hostname = "moz-proxy://" + idnService.convertUTF8toACE(info.host) + ":" + info.port; + realm = aAuthInfo.realm; + if (!realm) + realm = hostname; + + return [hostname, realm]; + } + hostname = this.getFormattedHostname(aChannel.URI); + + // If a HTTP WWW-Authenticate header specified a realm, that value + // will be available here. If it wasn't set or wasn't HTTP, we'll use + // the formatted hostname instead. + realm = aAuthInfo.realm; + if (!realm) + realm = hostname; + + return [hostname, realm]; + }, + + getAuthInfo : function pu_getAuthInfo(aAuthInfo) { + let flags = aAuthInfo.flags; + let username = {value: ""}; + let password = {value: ""}; + + if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain) + username.value = aAuthInfo.domain + "\\" + aAuthInfo.username; + else + username.value = aAuthInfo.username; + + password.value = aAuthInfo.password + + return [username, password]; + }, + + setAuthInfo : function (aAuthInfo, username, password) { + var flags = aAuthInfo.flags; + if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) { + // Domain is separated from username by a backslash + var idx = username.indexOf("\\"); + if (idx == -1) { + aAuthInfo.username = username; + } else { + aAuthInfo.domain = username.substring(0, idx); + aAuthInfo.username = username.substring(idx+1); + } + } else { + aAuthInfo.username = username; + } + aAuthInfo.password = password; + }, + + getFormattedHostname : function pu_getFormattedHostname(uri) { + let scheme = uri.scheme; + let hostname = scheme + "://" + uri.host; + + // If the URI explicitly specified a port, only include it when + // it's not the default. (We never want "http://foo.com:80") + port = uri.port; + if (port != -1) { + let handler = Services.io.getProtocolHandler(scheme); + if (port != handler.defaultPort) + hostname += ":" + port; + } + return hostname; + }, +}; + +XPCOMUtils.defineLazyGetter(PromptUtils, "passwdBundle", function () { + return Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties"); +}); + +XPCOMUtils.defineLazyGetter(PromptUtils, "bundle", function () { + return Services.strings.createBundle("chrome://global/locale/commonDialogs.properties"); +}); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PromptService]); diff --git a/browser/metro/components/SafeBrowsing.js b/browser/metro/components/SafeBrowsing.js new file mode 100644 index 000000000000..d1d31bcf5d59 --- /dev/null +++ b/browser/metro/components/SafeBrowsing.js @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const kPhishWardenEnabledPref = "browser.safebrowsing.enabled"; +const kMalwareWardenEnabledPref = "browser.safebrowsing.malware.enabled"; + +// This XPCOM object doesn't have a public interface. It just works quietly in the background +function SafeBrowsing() { + this.listManager = null; + + // Once we register tables, their respective names will be listed here. + this.phishing = { + pref: kPhishWardenEnabledPref, + blackTables: [], + whiteTables: [] + }; + this.malware = { + pref: kMalwareWardenEnabledPref, + blackTables: [], + whiteTables: [] + }; + + // Get notifications when the phishing or malware warden enabled pref changes + Services.prefs.addObserver(kPhishWardenEnabledPref, this, true); + Services.prefs.addObserver(kMalwareWardenEnabledPref, this, true); +} + +SafeBrowsing.prototype = { + classID: Components.ID("{aadaed90-6c03-42d0-924a-fc61198ff283}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore, + Ci.nsIDOMEventListener, + Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + observe: function sb_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "app-startup": + Services.obs.addObserver(this, "final-ui-startup", true); + Services.obs.addObserver(this, "xpcom-shutdown", true); + break; + case "final-ui-startup": + Services.obs.removeObserver(this, "final-ui-startup"); + this._startup(); + break; + case "xpcom-shutdown": + Services.obs.removeObserver(this, "xpcom-shutdown"); + this._shutdown(); + break; + case "nsPref:changed": + if (aData == kPhishWardenEnabledPref) + this.maybeToggleUpdateChecking(this.phishing); + else if (aData == kMalwareWardenEnabledPref) + this.maybeToggleUpdateChecking(this.malware); + break; + } + }, + + _startup: function sb_startup() { + this.listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].getService(Ci.nsIUrlListManager); + + // Add a test chunk to the database + let testData = "mozilla.org/firefox/its-an-attack.html"; + let testUpdate = + "n:1000\ni:test-malware-simple\nad:1\n" + + "a:1:32:" + testData.length + "\n" + + testData; + + testData = "mozilla.org/firefox/its-a-trap.html"; + testUpdate += + "n:1000\ni:test-phish-simple\nad:1\n" + + "a:1:32:" + testData.length + "\n" + + testData; + + let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(Ci.nsIUrlClassifierDBService); + + let listener = { + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsISupports) || aIID.equals(Ci.nsIUrlClassifierUpdateObserver)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + updateUrlRequested: function(aURL) { }, + streamFinished: function(aStatus) { }, + updateError: function(aErrorCode) { }, + updateSuccess: function(aRequestedTimeout) { } + }; + + try { + dbService.beginUpdate(listener, "test-malware-simple,test-phish-simple", ""); + dbService.beginStream("", ""); + dbService.updateStream(testUpdate); + dbService.finishStream(); + dbService.finishUpdate(); + } catch(ex) {} + + this.registerBlackTable("goog-malware-shavar", this.malware); + this.maybeToggleUpdateChecking(this.malware); + + this.registerBlackTable("goog-phish-shavar", this.phishing); + this.maybeToggleUpdateChecking(this.phishing); + }, + + _shutdown: function sb_shutdown() { + Services.prefs.removeObserver(kPhishWardenEnabledPref, this); + Services.prefs.removeObserver(kMalwareWardenEnabledPref, this); + + this.listManager = null; + }, + + enableBlacklistTableUpdates: function sb_enableBlacklistTableUpdates(aWarden) { + for (let i = 0; i < aWarden.blackTables.length; ++i) { + this.listManager.enableUpdate(aWarden.blackTables[i]); + } + }, + + disableBlacklistTableUpdates: function sb_disableBlacklistTableUpdates(aWarden) { + for (let i = 0; i < aWarden.blackTables.length; ++i) { + this.listManager.disableUpdate(aWarden.blackTables[i]); + } + }, + + enableWhitelistTableUpdates: function sb_enableWhitelistTableUpdates(aWarden) { + for (let i = 0; i < this.whiteTables.length; ++i) { + this.listManager.enableUpdate(this.whiteTables[i]); + } + }, + + disableWhitelistTableUpdates: function sb_disableWhitelistTableUpdates(aWarden) { + for (let i = 0; i < aWarden.whiteTables.length; ++i) { + this.listManager.disableUpdate(aWarden.whiteTables[i]); + } + }, + + registerBlackTable: function sb_registerBlackTable(aTableName, aWarden) { + let result = this.listManager.registerTable(aTableName, false); + if (result) + aWarden.blackTables.push(aTableName); + return result; + }, + + registerWhiteTable: function sb_registerWhiteTable(aTableName, aWarden) { + let result = this.listManager.registerTable(aTableName, false); + if (result) + aWarden.whiteTables.push(aTableName); + return result; + }, + + maybeToggleUpdateChecking: function sb_maybeToggleUpdateChecking(aWarden) { + let enabled = Services.prefs.getBoolPref(aWarden.pref); + if (enabled) + this.enableBlacklistTableUpdates(aWarden); + else + this.disableBlacklistTableUpdates(aWarden); + } +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SafeBrowsing]); diff --git a/browser/metro/components/SessionStore.idl b/browser/metro/components/SessionStore.idl new file mode 100644 index 000000000000..a5bf20fafaf4 --- /dev/null +++ b/browser/metro/components/SessionStore.idl @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIDOMWindow; +interface nsIDOMNode; + +/** + * nsISessionStore keeps track of the current browsing state. + * + * The nsISessionStore API operates mostly on browser windows and the browser + * tabs contained in them. + */ + +[scriptable, uuid(766a09c1-d21b-4bf8-9fe3-8b34b716251a)] +interface nsISessionStore : nsISupports +{ + /** + * Get the current browsing state. + * @returns a JSON string representing the session state. + */ + AString getBrowserState(); + + /** + * Get the number of restore-able tabs for a browser window + */ + unsigned long getClosedTabCount(in nsIDOMWindow aWindow); + + /** + * Get closed tab data + * + * @param aWindow is the browser window for which to get closed tab data + * @returns a JSON string representing the list of closed tabs. + */ + AString getClosedTabData(in nsIDOMWindow aWindow); + + /** + * @param aWindow is the browser window to reopen a closed tab in. + * @param aIndex is the index of the tab to be restored (FIFO ordered). + * @returns a reference to the reopened tab. + */ + nsIDOMNode undoCloseTab(in nsIDOMWindow aWindow, in unsigned long aIndex); + + /** + * @param aWindow is the browser window associated with the closed tab. + * @param aIndex is the index of the closed tab to be removed (FIFO ordered). + */ + nsIDOMNode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex); + + /** + * @param aTab is the browser tab to get the value for. + * @param aKey is the value's name. + * + * @returns A string value or an empty string if none is set. + */ + AString getTabValue(in nsIDOMNode aTab, in AString aKey); + + /** + * @param aTab is the browser tab to set the value for. + * @param aKey is the value's name. + * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects). + */ + void setTabValue(in nsIDOMNode aTab, in AString aKey, in AString aStringValue); + + /** + * @param aTab is the browser tab to get the value for. + * @param aKey is the value's name. + */ + void deleteTabValue(in nsIDOMNode aTab, in AString aKey); + + /** + * @returns A boolean indicating we should restore previous browser session + */ + boolean shouldRestore(); + + /** + * Restores the previous browser session using a fast, lightweight strategy + * @param aBringToFront should a restored tab be brought to the foreground? + */ + void restoreLastSession(in boolean aBringToFront); +}; diff --git a/browser/metro/components/SessionStore.js b/browser/metro/components/SessionStore.js new file mode 100644 index 000000000000..71dedcb2d3e2 --- /dev/null +++ b/browser/metro/components/SessionStore.js @@ -0,0 +1,768 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +#ifdef MOZ_CRASHREPORTER +XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", + "@mozilla.org/xre/app-info;1", "nsICrashReporter"); +#endif + +XPCOMUtils.defineLazyGetter(this, "NetUtil", function() { + Cu.import("resource://gre/modules/NetUtil.jsm"); + return NetUtil; +}); + +// ----------------------------------------------------------------------- +// Session Store +// ----------------------------------------------------------------------- + +const STATE_STOPPED = 0; +const STATE_RUNNING = 1; +const STATE_QUITTING = -1; + +function SessionStore() { } + +SessionStore.prototype = { + classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore, + Ci.nsIDOMEventListener, + Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + _windows: {}, + _lastSaveTime: 0, + _lastSessionTime: 0, + _interval: 10000, + _maxTabsUndo: 1, + _shouldRestore: false, + + init: function ss_init() { + // Get file references + this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + this._sessionFileBackup = this._sessionFile.clone(); + this._sessionCache = this._sessionFile.clone(); + this._sessionFile.append("sessionstore.js"); + this._sessionFileBackup.append("sessionstore.bak"); + this._sessionCache.append("sessionstoreCache"); + + this._loadState = STATE_STOPPED; + + try { + if (this._sessionFileBackup.exists()) { + this._shouldRestore = true; + this._sessionFileBackup.remove(false); + } + + if (this._sessionFile.exists()) { + // Disable crash recovery if we have exceeded the timeout + this._lastSessionTime = this._sessionFile.lastModifiedTime; + let delta = Date.now() - this._lastSessionTime; + let timeout = Services.prefs.getIntPref("browser.sessionstore.resume_from_crash_timeout"); + if (delta > (timeout * 60000)) + this._shouldRestore = false; + + this._sessionFile.copyTo(null, this._sessionFileBackup.leafName); + } + + if (!this._sessionCache.exists() || !this._sessionCache.isDirectory()) + this._sessionCache.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); + } catch (ex) { + Cu.reportError(ex); // file was write-locked? + } + + this._interval = Services.prefs.getIntPref("browser.sessionstore.interval"); + this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo"); + + // Disable crash recovery if it has been turned off + if (!Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash")) + this._shouldRestore = false; + + // Do we need to restore session just this once, in case of a restart? + if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) { + Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", false); + this._shouldRestore = true; + } + }, + + _clearDisk: function ss_clearDisk() { + if (this._sessionFile.exists()) { + try { + this._sessionFile.remove(false); + } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now? + } + if (this._sessionFileBackup.exists()) { + try { + this._sessionFileBackup.remove(false); + } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now? + } + + this._clearCache(); + }, + + _clearCache: function ss_clearCache() { + // First, let's get a list of files we think should be active + let activeFiles = []; + this._forEachBrowserWindow(function(aWindow) { + let tabs = aWindow.Browser.tabs; + for (let i = 0; i < tabs.length; i++) { + let browser = tabs[i].browser; + if (browser.__SS_extdata && "thumbnail" in browser.__SS_extdata) + activeFiles.push(browser.__SS_extdata.thumbnail); + } + }); + + // Now, let's find the stale files in the cache folder + let staleFiles = []; + let cacheFiles = this._sessionCache.directoryEntries; + while (cacheFiles.hasMoreElements()) { + let file = cacheFiles.getNext().QueryInterface(Ci.nsILocalFile); + let fileURI = Services.io.newFileURI(file); + if (activeFiles.indexOf(fileURI) == -1) + staleFiles.push(file); + } + + // Remove the stale files in a separate step to keep the enumerator from + // messing up if we remove the files as we collect them. + staleFiles.forEach(function(aFile) { + aFile.remove(false); + }) + }, + + observe: function ss_observe(aSubject, aTopic, aData) { + let self = this; + let observerService = Services.obs; + switch (aTopic) { + case "app-startup": + observerService.addObserver(this, "final-ui-startup", true); + observerService.addObserver(this, "domwindowopened", true); + observerService.addObserver(this, "domwindowclosed", true); + observerService.addObserver(this, "browser-lastwindow-close-granted", true); + observerService.addObserver(this, "browser:purge-session-history", true); + observerService.addObserver(this, "quit-application-requested", true); + observerService.addObserver(this, "quit-application-granted", true); + observerService.addObserver(this, "quit-application", true); + break; + case "final-ui-startup": + observerService.removeObserver(this, "final-ui-startup"); + this.init(); + break; + case "domwindowopened": + let window = aSubject; + window.addEventListener("load", function() { + self.onWindowOpen(window); + window.removeEventListener("load", arguments.callee, false); + }, false); + break; + case "domwindowclosed": // catch closed windows + this.onWindowClose(aSubject); + break; + case "browser-lastwindow-close-granted": + // If a save has been queued, kill the timer and save state now + if (this._saveTimer) { + this._saveTimer.cancel(); + this._saveTimer = null; + this.saveState(); + } + + // Freeze the data at what we've got (ignoring closing windows) + this._loadState = STATE_QUITTING; + break; + case "quit-application-requested": + // Get a current snapshot of all windows + this._forEachBrowserWindow(function(aWindow) { + self._collectWindowData(aWindow); + }); + break; + case "quit-application-granted": + // Get a current snapshot of all windows + this._forEachBrowserWindow(function(aWindow) { + self._collectWindowData(aWindow); + }); + + // Freeze the data at what we've got (ignoring closing windows) + this._loadState = STATE_QUITTING; + break; + case "quit-application": + // If we are restarting, lets restore the tabs + if (aData == "restart") { + Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true); + + // Ignore purges when restarting. The notification is fired after "quit-application". + Services.obs.removeObserver(this, "browser:purge-session-history"); + } + + // Freeze the data at what we've got (ignoring closing windows) + this._loadState = STATE_QUITTING; + + // No need for this back up, we are shutting down just fine + if (this._sessionFileBackup.exists()) + this._sessionFileBackup.remove(false); + + observerService.removeObserver(this, "domwindowopened"); + observerService.removeObserver(this, "domwindowclosed"); + observerService.removeObserver(this, "browser-lastwindow-close-granted"); + observerService.removeObserver(this, "quit-application-requested"); + observerService.removeObserver(this, "quit-application-granted"); + observerService.removeObserver(this, "quit-application"); + + // If a save has been queued, kill the timer and save state now + if (this._saveTimer) { + this._saveTimer.cancel(); + this._saveTimer = null; + this.saveState(); + } + break; + case "browser:purge-session-history": // catch sanitization + this._clearDisk(); + + // If the browser is shutting down, simply return after clearing the + // session data on disk as this notification fires after the + // quit-application notification so the browser is about to exit. + if (this._loadState == STATE_QUITTING) + return; + + // Clear all data about closed tabs + for (let [ssid, win] in Iterator(this._windows)) + win.closedTabs = []; + + if (this._loadState == STATE_RUNNING) { + // Save the purged state immediately + this.saveStateNow(); + } + break; + case "timer-callback": + // Timer call back for delayed saving + this._saveTimer = null; + this.saveState(); + break; + } + }, + + handleEvent: function ss_handleEvent(aEvent) { + let window = aEvent.currentTarget.ownerDocument.defaultView; + switch (aEvent.type) { + case "TabOpen": + case "TabClose": { + let browser = aEvent.originalTarget.linkedBrowser; + if (aEvent.type == "TabOpen") { + this.onTabAdd(window, browser); + } + else { + this.onTabClose(window, browser); + this.onTabRemove(window, browser); + } + break; + } + case "TabSelect": { + let browser = aEvent.originalTarget.linkedBrowser; + this.onTabSelect(window, browser); + break; + } + } + }, + + receiveMessage: function ss_receiveMessage(aMessage) { + let window = aMessage.target.ownerDocument.defaultView; + this.onTabLoad(window, aMessage.target, aMessage); + }, + + onWindowOpen: function ss_onWindowOpen(aWindow) { + // Return if window has already been initialized + if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) + return; + + // Ignore non-browser windows and windows opened while shutting down + if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState == STATE_QUITTING) + return; + + // Assign it a unique identifier (timestamp) and create its data object + aWindow.__SSID = "window" + Date.now(); + this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] }; + + // Perform additional initialization when the first window is loading + if (this._loadState == STATE_STOPPED) { + this._loadState = STATE_RUNNING; + this._lastSaveTime = Date.now(); + + // Nothing to restore, notify observers things are complete + if (!this._shouldRestore) { + this._clearCache(); + Services.obs.notifyObservers(null, "sessionstore-windows-restored", ""); + } + } + + // Add tab change listeners to all already existing tabs + let tabs = aWindow.Browser.tabs; + for (let i = 0; i < tabs.length; i++) + this.onTabAdd(aWindow, tabs[i].browser, true); + + // Notification of tab add/remove/selection + let tabContainer = aWindow.document.getElementById("tabs"); + tabContainer.addEventListener("TabOpen", this, true); + tabContainer.addEventListener("TabClose", this, true); + tabContainer.addEventListener("TabSelect", this, true); + }, + + onWindowClose: function ss_onWindowClose(aWindow) { + // Ignore windows not tracked by SessionStore + if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) + return; + + let tabContainer = aWindow.document.getElementById("tabs"); + tabContainer.removeEventListener("TabOpen", this, true); + tabContainer.removeEventListener("TabClose", this, true); + tabContainer.removeEventListener("TabSelect", this, true); + + if (this._loadState == STATE_RUNNING) { + // Update all window data for a last time + this._collectWindowData(aWindow); + + // Clear this window from the list + delete this._windows[aWindow.__SSID]; + + // Save the state without this window to disk + this.saveStateDelayed(); + } + + let tabs = aWindow.Browser.tabs; + for (let i = 0; i < tabs.length; i++) + this.onTabRemove(aWindow, tabs[i].browser, true); + + delete aWindow.__SSID; + }, + + onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) { + aBrowser.messageManager.addMessageListener("pageshow", this); + aBrowser.messageManager.addMessageListener("Content:SessionHistory", this); + + if (!aNoNotification) + this.saveStateDelayed(); + this._updateCrashReportURL(aWindow); + }, + + onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) { + aBrowser.messageManager.removeMessageListener("pageshow", this); + aBrowser.messageManager.removeMessageListener("Content:SessionHistory", this); + + // If this browser is being restored, skip any session save activity + if (aBrowser.__SS_restore) + return; + + delete aBrowser.__SS_data; + + if (!aNoNotification) + this.saveStateDelayed(); + }, + + onTabClose: function ss_onTabClose(aWindow, aBrowser) { + if (this._maxTabsUndo == 0) + return; + + if (aWindow.Browser.tabs.length > 0) { + // Bundle this browser's data and extra data and save in the closedTabs + // window property + let data = aBrowser.__SS_data; + data.extData = aBrowser.__SS_extdata; + + this._windows[aWindow.__SSID].closedTabs.unshift(data); + let length = this._windows[aWindow.__SSID].closedTabs.length; + if (length > this._maxTabsUndo) + this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo); + } + }, + + onTabLoad: function ss_onTabLoad(aWindow, aBrowser, aMessage) { + // If this browser is being restored, skip any session save activity + if (aBrowser.__SS_restore) + return; + + // Ignore a transient "about:blank" + if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank") + return; + + if (aMessage.name == "Content:SessionHistory") { + delete aBrowser.__SS_data; + this._collectTabData(aBrowser, aMessage.json); + } + + // Save out the state as quickly as possible + if (aMessage.name == "pageshow") + this.saveStateNow(); + + this._updateCrashReportURL(aWindow); + }, + + onTabSelect: function ss_onTabSelect(aWindow, aBrowser) { + if (this._loadState != STATE_RUNNING) + return; + + let index = aWindow.Elements.browsers.selectedIndex; + this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based + + // Restore the resurrected browser + if (aBrowser.__SS_restore) { + let data = aBrowser.__SS_data; + if (data.entries.length > 0) { + let json = { + uri: data.entries[data.index - 1].url, + flags: null, + entries: data.entries, + index: data.index + }; + aBrowser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json); + } + + delete aBrowser.__SS_restore; + } + + this._updateCrashReportURL(aWindow); + }, + + saveStateDelayed: function ss_saveStateDelayed() { + if (!this._saveTimer) { + // Interval until the next disk operation is allowed + let minimalDelay = this._lastSaveTime + this._interval - Date.now(); + + // If we have to wait, set a timer, otherwise saveState directly + let delay = Math.max(minimalDelay, 2000); + if (delay > 0) { + this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT); + } else { + this.saveState(); + } + } + }, + + saveStateNow: function ss_saveStateNow() { + // Kill any queued timer and save immediately + if (this._saveTimer) { + this._saveTimer.cancel(); + this._saveTimer = null; + } + this.saveState(); + }, + + saveState: function ss_saveState() { + let data = this._getCurrentState(); + this._writeFile(this._sessionFile, JSON.stringify(data)); + + this._lastSaveTime = Date.now(); + }, + + _getCurrentState: function ss_getCurrentState() { + let self = this; + this._forEachBrowserWindow(function(aWindow) { + self._collectWindowData(aWindow); + }); + + let data = { windows: [] }; + let index; + for (index in this._windows) + data.windows.push(this._windows[index]); + return data; + }, + + _collectTabData: function ss__collectTabData(aBrowser, aHistory) { + // If this browser is being restored, skip any session save activity + if (aBrowser.__SS_restore) + return; + + let aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 }; + + let tabData = {}; + tabData.entries = aHistory.entries; + tabData.index = aHistory.index; + tabData.attributes = { image: aBrowser.mIconURL }; + + aBrowser.__SS_data = tabData; + }, + + _collectWindowData: function ss__collectWindowData(aWindow) { + // Ignore windows not tracked by SessionStore + if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) + return; + + let winData = this._windows[aWindow.__SSID]; + winData.tabs = []; + + let index = aWindow.Elements.browsers.selectedIndex; + winData.selected = parseInt(index) + 1; // 1-based + + let tabs = aWindow.Browser.tabs; + for (let i = 0; i < tabs.length; i++) { + let browser = tabs[i].browser; + if (browser.__SS_data) { + let tabData = browser.__SS_data; + if (browser.__SS_extdata) + tabData.extData = browser.__SS_extdata; + winData.tabs.push(tabData); + } + } + }, + + _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) { + let windowsEnum = Services.wm.getEnumerator("navigator:browser"); + while (windowsEnum.hasMoreElements()) { + let window = windowsEnum.getNext(); + if (window.__SSID && !window.closed) + aFunc.call(this, window); + } + }, + + _writeFile: function ss_writeFile(aFile, aData) { + let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + stateString.data = aData; + Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); + + // Don't touch the file if an observer has deleted all state data + if (!stateString.data) + return; + + // Initialize the file output stream. + let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + ostream.init(aFile, 0x02 | 0x08 | 0x20, 0600, ostream.DEFER_OPEN); + + // Obtain a converter to convert our data to a UTF-8 encoded input stream. + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + // Asynchronously copy the data to the file. + let istream = converter.convertToInputStream(aData); + NetUtil.asyncCopy(istream, ostream, function(rc) { + if (Components.isSuccessCode(rc)) { + Services.obs.notifyObservers(null, "sessionstore-state-write-complete", ""); + } + }); + }, + + _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) { +#ifdef MOZ_CRASHREPORTER + try { + let currentURI = aWindow.Browser.selectedBrowser.currentURI.clone(); + // if the current URI contains a username/password, remove it + try { + currentURI.userPass = ""; + } + catch (ex) { } // ignore failures on about: URIs + + CrashReporter.annotateCrashReport("URL", currentURI.spec); + } + catch (ex) { + // don't make noise when crashreporter is built but not enabled + if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) + Components.utils.reportError("SessionStore:" + ex); + } +#endif + }, + + getBrowserState: function ss_getBrowserState() { + let data = this._getCurrentState(); + return JSON.stringify(data); + }, + + getClosedTabCount: function ss_getClosedTabCount(aWindow) { + if (!aWindow || !aWindow.__SSID) + return 0; // not a browser window, or not otherwise tracked by SS. + + return this._windows[aWindow.__SSID].closedTabs.length; + }, + + getClosedTabData: function ss_getClosedTabData(aWindow) { + if (!aWindow.__SSID) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + return JSON.stringify(this._windows[aWindow.__SSID].closedTabs); + }, + + undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) { + if (!aWindow.__SSID) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + let closedTabs = this._windows[aWindow.__SSID].closedTabs; + if (!closedTabs) + return null; + + // default to the most-recently closed tab + aIndex = aIndex || 0; + if (!(aIndex in closedTabs)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // fetch the data of closed tab, while removing it from the array + let closedTab = closedTabs.splice(aIndex, 1).shift(); + + // create a new tab and bring to front + let tab = aWindow.Browser.addTab(closedTab.entries[closedTab.index - 1].url, true); + + tab.browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", { + uri: closedTab.entries[closedTab.index - 1].url, + flags: null, + entries: closedTab.entries, + index: closedTab.index + }); + + // Put back the extra data + tab.browser.__SS_extdata = closedTab.extData; + + return tab.chromeTab; + }, + + forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) { + if (!aWindow.__SSID) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + let closedTabs = this._windows[aWindow.__SSID].closedTabs; + + // default to the most-recently closed tab + aIndex = aIndex || 0; + if (!(aIndex in closedTabs)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // remove closed tab from the array + closedTabs.splice(aIndex, 1); + }, + + getTabValue: function ss_getTabValue(aTab, aKey) { + let browser = aTab.linkedBrowser; + let data = browser.__SS_extdata || {}; + return data[aKey] || ""; + }, + + setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) { + let browser = aTab.linkedBrowser; + + // Thumbnails are actually stored in the cache, so do the save and update the URI + if (aKey == "thumbnail") { + let file = this._sessionCache.clone(); + file.append("thumbnail-" + browser.contentWindowId); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); + + let source = Services.io.newURI(aStringValue, "UTF8", null); + let target = Services.io.newFileURI(file) + + let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist); + persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; + persist.saveURI(source, null, null, null, null, file); + + aStringValue = target.spec; + } + + if (!browser.__SS_extdata) + browser.__SS_extdata = {}; + browser.__SS_extdata[aKey] = aStringValue; + this.saveStateDelayed(); + }, + + deleteTabValue: function ss_deleteTabValue(aTab, aKey) { + let browser = aTab.linkedBrowser; + if (browser.__SS_extdata && browser.__SS_extdata[aKey]) + delete browser.__SS_extdata[aKey]; + else + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + shouldRestore: function ss_shouldRestore() { + return this._shouldRestore; + }, + + restoreLastSession: function ss_restoreLastSession(aBringToFront) { + let self = this; + function notifyObservers(aMessage) { + self._clearCache(); + Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || ""); + } + + // The previous session data has already been renamed to the backup file + if (!this._sessionFileBackup.exists()) { + notifyObservers("fail") + return; + } + + try { + let channel = NetUtil.newChannel(this._sessionFileBackup); + channel.contentType = "application/json"; + NetUtil.asyncFetch(channel, function(aStream, aResult) { + if (!Components.isSuccessCode(aResult)) { + Cu.reportError("SessionStore: Could not read from sessionstore.bak file"); + notifyObservers("fail"); + return; + } + + // Read session state file into a string and let observers modify the state before it's being used + let state = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + state.data = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || ""; + aStream.close(); + + Services.obs.notifyObservers(state, "sessionstore-state-read", ""); + + let data = null; + try { + data = JSON.parse(state.data); + } catch (ex) { + Cu.reportError("SessionStore: Could not parse JSON: " + ex); + } + + if (!data || data.windows.length == 0) { + notifyObservers("fail"); + return; + } + + let window = Services.wm.getMostRecentWindow("navigator:browser"); + + let tabs = data.windows[0].tabs; + let selected = data.windows[0].selected; + if (selected > tabs.length) // Clamp the selected index if it's bogus + selected = 1; + + for (let i=0; i 'nightly' +BRANDFOLDER = $(notdir $(MOZ_BRANDING_DIRECTORY)) + +branding-$(BRANDFOLDER): + $(MAKE) -C $(DEPTH)/browser/branding/$(BRANDFOLDER) \ + DIST_SUBDIR=$(DIST_SUBDIR) XPI_ROOT_APPID="$(XPI_ROOT_APPID)" + +libs:: branding-$(BRANDFOLDER) diff --git a/browser/metro/locales/en-US/chrome/aboutCertError.dtd b/browser/metro/locales/en-US/chrome/aboutCertError.dtd new file mode 100644 index 000000000000..cd6c6ba632f4 --- /dev/null +++ b/browser/metro/locales/en-US/chrome/aboutCertError.dtd @@ -0,0 +1,38 @@ + + + + %brandDTD; + + + + + + + +#1, but we can't confirm that your connection is secure."> + + + + + + +Even if you trust the site, this error could mean that someone is +tampering with your connection."> + + + + + diff --git a/browser/metro/locales/en-US/chrome/browser.dtd b/browser/metro/locales/en-US/chrome/browser.dtd new file mode 100644 index 000000000000..71dac3782f1d --- /dev/null +++ b/browser/metro/locales/en-US/chrome/browser.dtd @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/locales/en-US/chrome/browser.properties b/browser/metro/locales/en-US/chrome/browser.properties new file mode 100644 index 000000000000..6d403c184fce --- /dev/null +++ b/browser/metro/locales/en-US/chrome/browser.properties @@ -0,0 +1,140 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Default search engine +browser.search.defaultenginename=Bing + +# Search engine order (order displayed in the search bar dropdown)s +browser.search.order.1=Bing +browser.search.order.2=Google +browser.search.order.3=Yahoo + +# Settings Charms +aboutCharm1=About +optionsCharm=Options +helpOnlineCharm=Help (online) + +# General +browserForSaveLocation=Save Location + +# Download Manager +downloadsUnknownSize=Unknown size + +# Alerts +alertLinkBookmarked=Bookmark added +alertDownloads=Downloads +alertDownloadsStart=Downloading: %S +alertDownloadsDone=%S has finished downloading +alertCantOpenDownload=Can't open file. Tap to save it. +alertDownloadsSize=Download too big +alertDownloadsNoSpace=Not enough storage space + +# Popup Blocker +popupWarning=%S prevented this site from opening a pop-up window. +popupWarningMultiple=%S prevented this site from opening %S pop-up windows. +popupButtonAllowOnce=Show +popupButtonAlwaysAllow2=Always Show +popupButtonNeverWarn2=Never Show + +# Site Identity +identity.identified.verifier=Verified by: %S +identity.identified.verified_by_you=You have added a security exception for this site +identity.identified.state_and_country=%S, %S +identity.identified.title_with_country=%S (%S) +identity.encrypted2=Encrypted +identity.unencrypted2=Not encrypted +identity.unknown.tooltip=This website does not supply identity information. +identity.ownerUnknown2=(unknown) + +# Geolocation UI +geolocation.allow=Share +geolocation.dontAllow=Don't share +geolocation.wantsTo=%S wants your location. + +# Desktop notification UI +desktopNotification.allow=Allow +desktopNotification.dontAllow=Don't allow +desktopNotification.wantsTo=%S wants to use notifications. + +# Error Console +typeError=Error: +typeWarning=Warning: + +# Offline web applications +offlineApps.available2=%S wants to store data on your device for offline use. +offlineApps.allow=Allow +offlineApps.never=Don't Allow +offlineApps.notNow=Not Now + +# New-style ContentPermissionPrompt values +offlineApps.dontAllow=Don't Allow +offlineApps.wantsTo=%S wants to store data on your device for offline use. + +# IndexedDB Quota increases +indexedDBQuota.allow=Allow +indexedDBQuota.dontAllow=Don't Allow +indexedDBQuota.wantsTo=%S wants to store a lot of data on your device for offline use. + +# Open Web Apps management API +openWebappsManage.allow=Allow +openWebappsManage.dontAllow=Don't Allow +openWebappsManage.wantsTo=%S wants to manage applications on your device. + +# Bookmark List +bookmarkList.desktop=Desktop Bookmarks + +# Closing Tabs +tabs.closeWarningTitle=Confirm close + +# LOCALIZATION NOTE (tabs.closeWarning): Semi-colon list of plural forms. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +# #1 number of tabs (must be greater than 1) +tabs.closeWarning=NOT USED;You are about to close #1 tabs. Continue? + +tabs.closeButton=Close tabs +tabs.closeWarningPromptMe=Warn me when I attempt to close multiple tabs +tabs.emptyTabTitle=New Tab + +# Homepage +# LOCALIZATION NOTE: homepage.custom2 is the text displayed on the selector button if +# the user selects a webpage to be the startpage. We can't display the entire URL +# or webpage title on the menulist +homepage.custom2=Custom Page + +# Page Actions +pageactions.pin.site=Pin Site +pageactions.geolocation=Location +pageactions.popup=Popups +pageactions.offline-app=Offline Storage +pageactions.password=Password +pageactions.desktop-notification=Web Notifications +pageactions.openWebappsManage=Manage Web Apps + +# Open Search +opensearch.search=Search: %S + +# Open in Another App +# LOCALIZATION NOTE: openinapp.specific is the text displayed if there is a single external app +# %S is the name of the app, like "YouTube" or "Picassa" +openinapp.specific=Open in %S App +openinapp.general=Open in Another App + +# Clear Private Data +clearPrivateData.title=Clear Private Data +clearPrivateData.message=Delete your browsing history and settings, including passwords and cookies? + +# LOCALIZATION NOTE (browser.menu.showCharacterEncoding): Set to the string +# "true" (spelled and capitalized exactly that way) to show the "Character +# Encoding" menu in the site menu. Any other value will hide it. Without this +# setting, the "Character Encoding" menu must be enabled via Preferences. +# This is not a string to translate. If users frequently use the "Character Encoding" +# menu, set this to "true". Otherwise, you can leave it as "false". +browser.menu.showCharacterEncoding=false + +# LOCALIZATION NOTE (intl.charsetmenu.browser.static): Set to a series of comma separated +# values for charsets that the user can select from in the Character Encoding menu. +intl.charsetmenu.browser.static=iso-8859-1,utf-8,x-gbk,big5,iso-2022-jp,shift_jis,euc-jp + +#Text Selection +selectionHelper.textCopied=Text copied to clipboard diff --git a/browser/metro/locales/en-US/chrome/checkbox.dtd b/browser/metro/locales/en-US/chrome/checkbox.dtd new file mode 100644 index 000000000000..523375f7c59f --- /dev/null +++ b/browser/metro/locales/en-US/chrome/checkbox.dtd @@ -0,0 +1,6 @@ + + + + diff --git a/browser/metro/locales/en-US/chrome/config.dtd b/browser/metro/locales/en-US/chrome/config.dtd new file mode 100644 index 000000000000..ac49b2d29bfb --- /dev/null +++ b/browser/metro/locales/en-US/chrome/config.dtd @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/browser/metro/locales/en-US/chrome/notification.dtd b/browser/metro/locales/en-US/chrome/notification.dtd new file mode 100644 index 000000000000..d45e71b67234 --- /dev/null +++ b/browser/metro/locales/en-US/chrome/notification.dtd @@ -0,0 +1,12 @@ + + + + + diff --git a/browser/metro/locales/en-US/chrome/phishing.dtd b/browser/metro/locales/en-US/chrome/phishing.dtd new file mode 100644 index 000000000000..b3fd118e12f5 --- /dev/null +++ b/browser/metro/locales/en-US/chrome/phishing.dtd @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + has been reported as an attack page and has been blocked based on your security preferences."> +Attack pages try to install programs that steal private information, use your computer to attack others, or damage your system.

Some attack pages intentionally distribute harmful software, but many are compromised without the knowledge or permission of their owners.

"> + + + +These types of web forgeries are used in scams known as phishing attacks, in which fraudulent web pages and emails are used to imitate sources you may trust.

"> diff --git a/browser/metro/locales/en-US/chrome/preferences.dtd b/browser/metro/locales/en-US/chrome/preferences.dtd new file mode 100644 index 000000000000..71c7834e7f41 --- /dev/null +++ b/browser/metro/locales/en-US/chrome/preferences.dtd @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/browser/metro/locales/en-US/chrome/prompt.dtd b/browser/metro/locales/en-US/chrome/prompt.dtd new file mode 100644 index 000000000000..dd747db6aace --- /dev/null +++ b/browser/metro/locales/en-US/chrome/prompt.dtd @@ -0,0 +1,6 @@ + + + + diff --git a/browser/metro/locales/en-US/chrome/sync.dtd b/browser/metro/locales/en-US/chrome/sync.dtd new file mode 100644 index 000000000000..2d7e6e62f6cf --- /dev/null +++ b/browser/metro/locales/en-US/chrome/sync.dtd @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/locales/en-US/chrome/sync.properties b/browser/metro/locales/en-US/chrome/sync.properties new file mode 100644 index 000000000000..3cc69529fe9c --- /dev/null +++ b/browser/metro/locales/en-US/chrome/sync.properties @@ -0,0 +1,31 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Mobile Sync + +# %S is the date and time at which the last sync successfully completed +lastSync2.label=Last sync: %S +lastSyncInProgress2.label=Last sync: in progress… + +# %S is the username logged in +account.label=Account: %S +notconnected.label=Not connected +connecting.label=Connecting… + +notificationDisconnect.label=Your Firefox Sync account has been removed +notificationDisconnect.button=Undo + +# LOCALIZATION NOTE (sync.clientUpdate, sync.remoteUpdate): +# #1 is the "application name" +# #2 is the "version" +sync.update.client=#1 #2 is not compatible with the latest version of Firefox Sync. Please update to the latest version. +sync.update.remote=#1 #2 is not compatible with older versions of Firefox Sync. Please update Firefox on your other computer(s). +sync.update.title=Firefox Sync +sync.update.button=Learn More +sync.update.close=Close +sync.setup.error.title=Cannot Setup Sync +sync.setup.error.network=No internet connection available +sync.setup.error.nodata=%S could not connect to Sync. Would you like to try again? +sync.setup.tryagain=Try again +sync.setup.manual=Manual setup diff --git a/browser/metro/locales/en-US/overrides/passwordmgr.properties b/browser/metro/locales/en-US/overrides/passwordmgr.properties new file mode 100644 index 000000000000..933de16a7754 --- /dev/null +++ b/browser/metro/locales/en-US/overrides/passwordmgr.properties @@ -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/. + +rememberValue = Use Password Manager to remember this value. +rememberPassword = Use Password Manager to remember this password. +savePasswordTitle = Confirm +# 1st string is product name, 2nd is the username for the login, 3rd is the +# login's hostname. Note that long usernames may be truncated. +saveLoginText = Do you want %1$S to remember the password for "%2$S" on %3$S? +# 1st string is product name, 2nd is the login's hostname +saveLoginTextNoUsername = Do you want %1$S to remember this password on %2$S? +promptNotNowButtonText = Not Now +notifyBarNotNowButtonText = Not Now +notifyBarNotNowButtonAccessKey = +promptNeverForSiteButtonText = Never +notifyBarNeverForSiteButtonText = Never +notifyBarNeverForSiteButtonAccessKey = +promptRememberButtonText = Remember +notifyBarRememberButtonText = Remember +notifyBarRememberButtonAccessKey = +passwordChangeTitle = Confirm Password Change +passwordChangeText = Would you like to change the stored password for %S? +passwordChangeTextNoUser = Would you like to change the stored password for this login? +notifyBarChangeButtonText = Change +notifyBarChangeButtonAccessKey = +notifyBarDontChangeButtonText = Don't Change +notifyBarDontChangeButtonAccessKey = +userSelectText = Please confirm which user you are changing the password for +hidePasswords=Hide Passwords +hidePasswordsAccessKey=P +showPasswords=Show Passwords +showPasswordsAccessKey=P +noMasterPasswordPrompt=Are you sure you wish to show your passwords? +removeAllPasswordsPrompt=Are you sure you wish to remove all passwords? +removeAllPasswordsTitle=Remove all passwords +loginsSpielAll=Passwords for the following sites are stored on your computer: +loginsSpielFiltered=The following passwords match your search: +username=Username +password=Password diff --git a/browser/metro/locales/generic/profile/bookmarks.json.in b/browser/metro/locales/generic/profile/bookmarks.json.in new file mode 100644 index 000000000000..95a5628e050a --- /dev/null +++ b/browser/metro/locales/generic/profile/bookmarks.json.in @@ -0,0 +1,17 @@ +#filter substitution +{"type":"text/x-moz-place-container","root":"placesRoot","children": + [{"type":"text/x-moz-place-container","title":"@bookmarks_title@","annos":[{"name":"metro/bookmarksRoot","expires":4,"type":1,"value":1}], + "children": + [ + {"index":1,"title":"@firefox_about@", "type":"text/x-moz-place", "uri":"http://www.mozilla.com/@AB_CD@/about/", + "iconUri":"chrome://branding/content/favicon32.png" + }, + {"index":2,"title":"@getting_started@", "type":"text/x-moz-place", "uri":"http://www.mozilla.com/@AB_CD@/firefox/central/", + "iconUri":"chrome://branding/content/favicon32.png" + }, + {"index":3,"title":"@firefox_community@", "type":"text/x-moz-place", "uri":"http://www.mozilla.com/@AB_CD@/firefox/community/", + "iconUri":"chrome://branding/content/favicon32.png" + }, + ] + }] +} diff --git a/browser/metro/locales/import/Makefile.in b/browser/metro/locales/import/Makefile.in new file mode 100644 index 000000000000..76fc1bbbb156 --- /dev/null +++ b/browser/metro/locales/import/Makefile.in @@ -0,0 +1,56 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +# desktop specific resources - this defines LOCALE_SRCDIR +relativesrcdir = browser/locales + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/config/config.mk + +######################################### +# l10s prefs file + +DEFINES += -DAB_CD=$(AB_CD) + +# copying firefox-l10n.js over from LOCALE_SRCDIR or browser +PREF_JS_EXPORTS = $(firstword $(wildcard $(LOCALE_SRCDIR)/firefox-l10n.js) \ + $(topsrcdir)/$(relativesrcdir)/en-US/firefox-l10n.js ) + +include $(topsrcdir)/config/rules.mk + +######################################### +# Search plugins + +# Metro reuses desktop search plugins +libs:: + $(MAKE) -C $(DEPTH)/browser/locales searchplugins \ + DIST_SUBDIR=$(DIST_SUBDIR) XPI_ROOT_APPID="$(XPI_ROOT_APPID)" + +######################################### +# Bookmarks + +# Pick up desktop's bookmarks.inc file +vpath book%.inc $(LOCALE_SRCDIR)/profile +ifdef LOCALE_MERGEDIR +vpath book%.inc $(LOCALE_SRCDIR)/profile +vpath book%.inc @top_srcdir@/$(relativesrcdir)/en-US/profile +endif + +bookmarks-src = $(srcdir)/../generic/profile/bookmarks.json.in + +# The resulting bookmarks.json will get picked up and packaged by the +# processing of the jar file in the parent directory. +bookmarks: bookmarks.inc + @echo "Generating: $@" + $(PYTHON) $(topsrcdir)/config/Preprocessor.py \ + -I $^ \ + -DAB_CD=$(AB_CD) \ + $(bookmarks-src) > ../bookmarks.json + +libs:: bookmarks diff --git a/browser/metro/locales/jar.mn b/browser/metro/locales/jar.mn new file mode 100644 index 000000000000..4a3415db9db4 --- /dev/null +++ b/browser/metro/locales/jar.mn @@ -0,0 +1,40 @@ +#filter substitution +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# +# Metro jar resources +# + +@AB_CD@.jar: +% locale browser @AB_CD@ %locale/browser/ + locale/browser/aboutCertError.dtd (%chrome/aboutCertError.dtd) + locale/browser/browser.dtd (%chrome/browser.dtd) + locale/browser/browser.properties (%chrome/browser.properties) + locale/browser/config.dtd (%chrome/config.dtd) + locale/browser/preferences.dtd (%chrome/preferences.dtd) + locale/browser/checkbox.dtd (%chrome/checkbox.dtd) + locale/browser/notification.dtd (%chrome/notification.dtd) + locale/browser/sync.dtd (%chrome/sync.dtd) + locale/browser/sync.properties (%chrome/sync.properties) + locale/browser/prompt.dtd (%chrome/prompt.dtd) + locale/browser/phishing.dtd (%chrome/phishing.dtd) + +@AB_CD@.jar: +% locale browser @AB_CD@ %locale/browser/ + locale/browser/bookmarks.json (bookmarks.json) +* locale/browser/passwordmgr.properties (%overrides/passwordmgr.properties) +% override chrome://passwordmgr/locale/passwordmgr.properties chrome://browser/locale/passwordmgr.properties + +# +# Browser jar resources +# + +@AB_CD@.jar: +relativesrcdir browser/locales: + locale/browser/region.properties (%chrome/browser-region/region.properties) +* locale/browser/netError.dtd (%chrome/overrides/netError.dtd) +% override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd +* locale/browser/appstrings.properties (%chrome/overrides/appstrings.properties) +% override chrome://global/locale/appstrings.properties chrome://browser/locale/appstrings.properties diff --git a/browser/metro/metroapp.ini.in b/browser/metro/metroapp.ini.in new file mode 100644 index 000000000000..2b7867ba1f71 --- /dev/null +++ b/browser/metro/metroapp.ini.in @@ -0,0 +1,22 @@ +#filter substitution +[App] +Vendor=Mozilla +Name=MetroFirefox +UAName=Firefox +Version=@GRE_MILESTONE@ +BuildID=@GRE_BUILDID@ +ID={99bceaaa-e3c6-48c1-b981-ef9b46b67d60} + +[Gecko] +MinVersion=@GRE_MILESTONE@ +MaxVersion=@GRE_MILESTONE@ + +[XRE] +EnableExtensionManager=0 +EnableProfileMigrator=0 + +[Crash Reporter] +#if MOZILLA_OFFICIAL +Enabled=1 +#endif +ServerURL=https://crash-reports.mozilla.com/submit?id={99bceaaa-e3c6-48c1-b981-ef9b46b67d60}&version=@GRE_MILESTONE@&buildid=@GRE_BUILDID@ diff --git a/browser/metro/modules/Makefile.in b/browser/metro/modules/Makefile.in new file mode 100644 index 000000000000..52584df83967 --- /dev/null +++ b/browser/metro/modules/Makefile.in @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/config/config.mk + +EXTRA_JS_MODULES = \ + video.jsm \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/browser/metro/modules/video.jsm b/browser/metro/modules/video.jsm new file mode 100644 index 000000000000..10e142b49175 --- /dev/null +++ b/browser/metro/modules/video.jsm @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["Video"]; + +this.Video = { + fullScreenSourceElement: null +}; diff --git a/browser/metro/profile/Makefile.in b/browser/metro/profile/Makefile.in new file mode 100644 index 000000000000..834a723802f5 --- /dev/null +++ b/browser/metro/profile/Makefile.in @@ -0,0 +1,15 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/config/config.mk + +PREF_JS_EXPORTS = $(srcdir)/metro.js + +include $(topsrcdir)/config/rules.mk diff --git a/browser/metro/profile/metro.js b/browser/metro/profile/metro.js new file mode 100644 index 000000000000..a756ea802420 --- /dev/null +++ b/browser/metro/profile/metro.js @@ -0,0 +1,583 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#filter substitution + +#ifdef DEBUG +// disable content and content script caching +pref("nglayout.debug.disable_xul_cache", true); +pref("nglayout.debug.disable_xul_fastload", true); +pref("devtools.errorconsole.enabled", true); +#endif + +// Enable headless crash reporting by default +pref("app.reportCrashes", true); + +// Debug prefs, see input.js +pref("metro.debug.treatmouseastouch", false); +pref("metro.debug.colorizeInputOverlay", false); +pref("metro.debug.selection.displayRanges", false); +pref("metro.debug.selection.dumpRanges", false); +pref("metro.debug.selection.dumpEvents", false); + +// Form helper options: 0 = disabled, 1 = enabled, 2 = dynamic depending on screen size +pref("formhelper.mode", 1); +// Auto zoom to form elements when they take focus +pref("formhelper.autozoom", true); +// Auto zoom to the caret +pref("formhelper.autozoom.caret", false); + +// form autocomplete service +pref("browser.formfill.enable", true); + +// Enable Microsoft TSF support by default for imes. +pref("intl.enable_tsf_support", true); + +pref("general.autoScroll", true); +pref("general.smoothScroll", true); +pref("general.smoothScroll.durationToIntervalRatio", 200); +pref("mousewheel.enable_pixel_scrolling", true); + +// For browser.xml binding +// +// cacheRatio* is a ratio that determines the amount of pixels to cache. The +// ratio is multiplied by the viewport width or height to get the displayport's +// width or height, respectively. +// +// (divide integer value by 1000 to get the ratio) +// +// For instance: cachePercentageWidth is 1500 +// viewport height is 500 +// => display port height will be 500 * 1.5 = 750 +// +pref("toolkit.browser.cacheRatioWidth", 2000); +pref("toolkit.browser.cacheRatioHeight", 3000); + +// How long before a content view (a handle to a remote scrollable object) +// expires. +pref("toolkit.browser.contentViewExpire", 3000); + +pref("toolkit.defaultChromeURI", "chrome://browser/content/browser.xul"); +pref("browser.chromeURL", "chrome://browser/content/"); + +// When true, always show the tab strip and use desktop-style tabs (no thumbnails) +pref("browser.tabs.tabsOnly", false); + +pref("browser.tabs.warnOnClose", true); +pref("browser.tabs.remote", false); + +// Telemetry +pref("toolkit.telemetry.enabled", true); +pref("toolkit.telemetry.prompted", 2); + +pref("toolkit.screen.lock", false); + +// From libpref/src/init/all.js, extended to allow a slightly wider zoom range. +pref("zoom.minPercent", 20); +pref("zoom.maxPercent", 400); +pref("toolkit.zoomManager.zoomValues", ".2,.3,.5,.67,.8,.9,1,1.1,1.2,1.33,1.5,1.7,2,2.4,3,4"); + +// Device pixel to CSS px ratio, in percent. Set to -1 to calculate based on display density. +pref("browser.viewport.scaleRatio", -1); + +/* use long press to display a context menu */ +pref("ui.click_hold_context_menus", false); + +/* offline cache prefs */ +pref("browser.offline-apps.notify", true); + +/* protocol warning prefs */ +pref("network.protocol-handler.warn-external.tel", false); +pref("network.protocol-handler.warn-external.mailto", false); +pref("network.protocol-handler.warn-external.vnd.youtube", false); + +/* history max results display */ +pref("browser.display.history.maxresults", 100); + +/* max items per section of the startui */ +pref("browser.display.startUI.maxresults", 16); + +// Backspace and Shift+Backspace behavior +// 0 goes Back/Forward +// 1 act like PgUp/PgDown +// 2 and other values, nothing +pref("browser.backspace_action", 0); + +/* session history */ +pref("browser.sessionhistory.max_entries", 50); + +// On startup, automatically restore tabs from last time? +pref("browser.startup.sessionRestore", false); + +/* session store */ +pref("browser.sessionstore.resume_from_crash", true); +pref("browser.sessionstore.resume_session_once", false); +pref("browser.sessionstore.resume_from_crash_timeout", 60); // minutes +// minimal interval between two save operations in milliseconds +pref("browser.sessionstore.interval", 15000); // milliseconds +// maximum amount of POSTDATA to be saved in bytes per history entry (-1 = all of it) +// (NB: POSTDATA will be saved either entirely or not at all) +pref("browser.sessionstore.postdata", 0); +// on which sites to save text data, POSTDATA and cookies +// 0 = everywhere, 1 = unencrypted sites, 2 = nowhere +pref("browser.sessionstore.privacy_level", 0); +// the same as browser.sessionstore.privacy_level, but for saving deferred session data +pref("browser.sessionstore.privacy_level_deferred", 1); +// how many tabs can be reopened (per window) +pref("browser.sessionstore.max_tabs_undo", 10); +// number of crashes that can occur before the about:sessionrestore page is displayed +// (this pref has no effect if more than 6 hours have passed since the last crash) +pref("browser.sessionstore.max_resumed_crashes", 1); +// restore_on_demand overrides MAX_CONCURRENT_TAB_RESTORES (sessionstore constant) +// and restore_hidden_tabs. When true, tabs will not be restored until they are +// focused (also applies to tabs that aren't visible). When false, the values +// for MAX_CONCURRENT_TAB_RESTORES and restore_hidden_tabs are respected. +// Selected tabs are always restored regardless of this pref. +pref("browser.sessionstore.restore_on_demand", true); + +/* these should help performance */ +pref("mozilla.widget.force-24bpp", true); +pref("mozilla.widget.use-buffer-pixmap", true); +pref("mozilla.widget.disable-native-theme", false); +pref("layout.reflow.synthMouseMove", false); + +/* "Preview" of framerate increase for animations, discussed in 710563. */ +pref("layout.frame_rate.precise", true); + +/* download manager (don't show the window or alert) */ +pref("browser.download.useDownloadDir", true); +pref("browser.download.folderList", 1); // Default to ~/Downloads +pref("browser.download.manager.showAlertOnComplete", false); +pref("browser.download.manager.showAlertInterval", 2000); +pref("browser.download.manager.retention", 2); +pref("browser.download.manager.showWhenStarting", false); +pref("browser.download.manager.closeWhenDone", true); +pref("browser.download.manager.openDelay", 0); +pref("browser.download.manager.focusWhenStarting", false); +pref("browser.download.manager.flashCount", 2); +pref("browser.download.manager.addToRecentDocs", true); +pref("browser.download.manager.displayedHistoryDays", 7); +pref("browser.download.manager.resumeOnWakeDelay", 10000); +pref("browser.download.manager.quitBehavior", 0); + +/* download alerts (disabled above) */ +pref("alerts.totalOpenTime", 6000); + +/* download helper */ +pref("browser.helperApps.deleteTempFileOnExit", false); + +/* password manager */ +pref("signon.rememberSignons", true); +pref("signon.expireMasterPassword", false); +pref("signon.SignonFileName", "signons.txt"); + +/* find helper */ +pref("findhelper.autozoom", true); + +// this will automatically enable inline spellchecking (if it is available) for +// editable elements in HTML +// 0 = spellcheck nothing +// 1 = check multi-line controls [default] +// 2 = check multi/single line controls +pref("layout.spellcheckDefault", 1); + +/* extension manager and xpinstall */ +// Disable all add-on locations other than the profile +pref("extensions.enabledScopes", 1); +// Auto-disable any add-ons that are "dropped in" to the profile +pref("extensions.autoDisableScopes", 1); +// Disable add-on installation via the web-exposed APIs +pref("xpinstall.enabled", false); +pref("xpinstall.whitelist.add", "addons.mozilla.org"); +pref("extensions.autoupdate.enabled", false); +pref("extensions.update.enabled", false); + +/* blocklist preferences */ +pref("extensions.blocklist.enabled", true); +pref("extensions.blocklist.interval", 86400); +pref("extensions.blocklist.url", "https://addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/"); +pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/"); + +/* block popups by default, and notify the user about blocked popups */ +pref("dom.disable_open_during_load", true); +pref("privacy.popups.showBrowserMessage", true); + +/* disable opening windows with the dialog feature */ +pref("dom.disable_window_open_dialog_feature", true); + +pref("keyword.enabled", true); +pref("keyword.URL", "http://www.bing.com/search?q="); + +pref("accessibility.typeaheadfind", false); +pref("accessibility.typeaheadfind.timeout", 5000); +pref("accessibility.typeaheadfind.flashBar", 1); +pref("accessibility.typeaheadfind.linksonly", false); +pref("accessibility.typeaheadfind.casesensitive", 0); + +// Trun on F7 caret browsing hot key +pref("accessibility.browsewithcaret_shortcut.enabled", true); +pref("accessibility.browsewithcaret", false); + +// Whether or not we show a dialog box informing the user that the update was +// successfully applied. +pref("app.update.showInstalledUI", false); + +// Whether the character encoding menu is under the main Firefox button. This +// preference is a string so that localizers can alter it. +pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties"); +pref("intl.charsetmenu.browser.static", "chrome://browser/locale/browser.properties"); + +// pointer to the default engine name +pref("browser.search.defaultenginename", "chrome://browser/locale/browser.properties"); + +// SSL error page behaviour +pref("browser.ssl_override_behavior", 2); +pref("browser.xul.error_pages.expert_bad_cert", false); + +// disable logging for the search service by default +pref("browser.search.log", false); + +// ordering of search engines in the engine list. +pref("browser.search.order.1", "chrome://browser/locale/browser.properties"); +pref("browser.search.order.2", "chrome://browser/locale/browser.properties"); +pref("browser.search.order.3", "chrome://browser/locale/browser.properties"); + +// send ping to the server to update +pref("browser.search.update", true); + +// disable logging for the search service update system by default +pref("browser.search.update.log", false); + +// Check whether we need to perform engine updates every 6 hours +pref("browser.search.update.interval", 21600); + +// enable search suggestions by default +pref("browser.search.suggest.enabled", true); + +// tell the search service that we don't really expose the "current engine" +pref("browser.search.noCurrentEngine", true); + +#ifdef MOZ_OFFICIAL_BRANDING +// {moz:official} expands to "official" +pref("browser.search.official", true); +#endif + +// enable xul error pages +pref("browser.xul.error_pages.enabled", true); + +// Specify emptyRestriction = 0 so that bookmarks appear in the list by default +pref("browser.urlbar.default.behavior", 0); +pref("browser.urlbar.default.behavior.emptyRestriction", 0); + +// Let the faviconservice know that we display favicons as 25x25px so that it +// uses the right size when optimizing favicons +pref("places.favicons.optimizeToDimension", 25); + +// various and sundry awesomebar prefs (should remove/re-evaluate +// these once bug 447900 is fixed) +pref("browser.urlbar.clickSelectsAll", true); +pref("browser.urlbar.doubleClickSelectsAll", true); +pref("browser.urlbar.autoFill", false); +pref("browser.urlbar.matchOnlyTyped", false); +pref("browser.urlbar.matchBehavior", 1); +pref("browser.urlbar.filter.javascript", true); +pref("browser.urlbar.maxRichResults", 8); +pref("browser.urlbar.search.chunkSize", 1000); +pref("browser.urlbar.search.timeout", 100); +pref("browser.urlbar.restrict.history", "^"); +pref("browser.urlbar.restrict.bookmark", "*"); +pref("browser.urlbar.restrict.tag", "+"); +pref("browser.urlbar.match.title", "#"); +pref("browser.urlbar.match.url", "@"); +pref("browser.history.grouping", "day"); +pref("browser.history.showSessions", false); +pref("browser.sessionhistory.max_entries", 50); +pref("browser.history_expire_days", 180); +pref("browser.history_expire_days_min", 90); +pref("browser.history_expire_sites", 40000); +pref("browser.places.migratePostDataAnnotations", true); +pref("browser.places.updateRecentTagsUri", true); +pref("places.frecency.numVisits", 10); +pref("places.frecency.numCalcOnIdle", 50); +pref("places.frecency.numCalcOnMigrate", 50); +pref("places.frecency.updateIdleTime", 60000); +pref("places.frecency.firstBucketCutoff", 4); +pref("places.frecency.secondBucketCutoff", 14); +pref("places.frecency.thirdBucketCutoff", 31); +pref("places.frecency.fourthBucketCutoff", 90); +pref("places.frecency.firstBucketWeight", 100); +pref("places.frecency.secondBucketWeight", 70); +pref("places.frecency.thirdBucketWeight", 50); +pref("places.frecency.fourthBucketWeight", 30); +pref("places.frecency.defaultBucketWeight", 10); +pref("places.frecency.embedVisitBonus", 0); +pref("places.frecency.linkVisitBonus", 100); +pref("places.frecency.typedVisitBonus", 2000); +pref("places.frecency.bookmarkVisitBonus", 150); +pref("places.frecency.downloadVisitBonus", 0); +pref("places.frecency.permRedirectVisitBonus", 0); +pref("places.frecency.tempRedirectVisitBonus", 0); +pref("places.frecency.defaultVisitBonus", 0); +pref("places.frecency.unvisitedBookmarkBonus", 140); +pref("places.frecency.unvisitedTypedBonus", 200); + +// disable color management +pref("gfx.color_management.mode", 0); + +// don't allow JS to move and resize existing windows +pref("dom.disable_window_move_resize", true); + +// prevent click image resizing for nsImageDocument +pref("browser.enable_click_image_resizing", false); + +// open in tab preferences +// 0=default window, 1=current window/tab, 2=new window, 3=new tab in most window +pref("browser.link.open_external", 3); +pref("browser.link.open_newwindow", 3); +// 0=force all new windows to tabs, 1=don't force, 2=only force those with no features set +pref("browser.link.open_newwindow.restriction", 0); + +// controls which bits of private data to clear. by default we clear them all. +pref("privacy.item.cache", true); +pref("privacy.item.cookies", true); +pref("privacy.item.offlineApps", true); +pref("privacy.item.history", true); +pref("privacy.item.formdata", true); +pref("privacy.item.downloads", true); +pref("privacy.item.passwords", true); +pref("privacy.item.sessions", true); +pref("privacy.item.geolocation", true); +pref("privacy.item.siteSettings", true); +pref("privacy.item.syncAccount", true); + +pref("plugins.force.wmode", "opaque"); + +// What default should we use for the time span in the sanitizer: +// 0 - Clear everything +// 1 - Last Hour +// 2 - Last 2 Hours +// 3 - Last 4 Hours +// 4 - Today +pref("privacy.sanitize.timeSpan", 1); +pref("privacy.sanitize.sanitizeOnShutdown", false); +pref("privacy.sanitize.migrateFx3Prefs", false); + +// URL to the Learn More link XXX this is the firefox one. Bug 495578 fixes this. +pref("browser.geolocation.warning.infoURL", "http://www.mozilla.com/%LOCALE%/firefox/geolocation/"); + +// enable geo +pref("geo.enabled", true); + +// JS error console +pref("devtools.errorconsole.enabled", false); + +// kinetic tweakables +pref("browser.ui.kinetic.updateInterval", 16); +pref("browser.ui.kinetic.exponentialC", 1400); +pref("browser.ui.kinetic.polynomialC", 100); +pref("browser.ui.kinetic.swipeLength", 160); +pref("browser.ui.zoom.animationDuration", 200); // ms duration of double-tap zoom animation + +// pinch gesture +pref("browser.ui.pinch.maxGrowth", 150); // max pinch distance growth +pref("browser.ui.pinch.maxShrink", 200); // max pinch distance shrinkage +pref("browser.ui.pinch.scalingFactor", 500); // scaling factor for above pinch limits + +pref("ui.mouse.radius.enabled", true); +pref("ui.touch.radius.enabled", true); + +// plugins +pref("plugin.disable", true); +pref("dom.ipc.plugins.enabled", true); + +// process priority +// higher values give content process less CPU time +pref("dom.ipc.content.nice", 1); + +// product URLs +// The breakpad report server to link to in about:crashes +pref("breakpad.reportURL", "http://crash-stats.mozilla.com/report/index/"); +pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/mobile/%VERSION%/releasenotes/"); +pref("app.sync.tutorialURL", "https://support.mozilla.org/kb/sync-firefox-between-desktop-and-mobile"); +pref("app.support.baseURL", "http://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/"); +pref("app.feedbackURL", "http://input.mozilla.com/feedback/"); +pref("app.privacyURL", "http://www.mozilla.com/legal/privacy/"); +pref("app.creditsURL", "http://www.mozilla.org/credits/"); +pref("app.channelURL", "http://www.mozilla.org/%LOCALE%/firefox/channel/"); +#if MOZ_UPDATE_CHANNEL == beta +pref("app.faqURL", "http://www.mozilla.com/%LOCALE%/mobile/beta/faq/"); +#else +pref("app.faqURL", "http://www.mozilla.com/%LOCALE%/mobile/faq/"); +#endif + +// Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror) +pref("security.alternate_certificate_error_page", "certerror"); + +pref("security.warn_viewing_mixed", false); // Warning is disabled. See Bug 616712. + +// Override some named colors to avoid inverse OS themes + +#ifdef MOZ_OFFICIAL_BRANDING +pref("browser.search.param.yahoo-fr", "moz35"); +pref("browser.search.param.yahoo-fr-cjkt", "moz35"); +pref("browser.search.param.yahoo-fr-ja", "mozff"); +#endif + +/* app update prefs */ +pref("app.update.timer", 60000); // milliseconds (1 min) + +#ifdef MOZ_UPDATER +// temp +pref("app.update.enabled", false); +pref("app.update.timerFirstInterval", 20000); // milliseconds +pref("app.update.auto", false); +pref("app.update.channel", "@MOZ_UPDATE_CHANNEL@"); +pref("app.update.mode", 1); +pref("app.update.silent", false); +pref("app.update.url", "https://aus2.mozilla.org/update/4/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%-xul/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PLATFORM_VERSION%/update.xml"); +pref("app.update.promptWaitTime", 43200); +pref("app.update.idletime", 60); +pref("app.update.showInstalledUI", false); +pref("app.update.incompatible.mode", 0); +pref("app.update.download.backgroundInterval", 0); + +#ifdef MOZ_OFFICIAL_BRANDING +pref("app.update.interval", 86400); +pref("app.update.url.manual", "http://www.mozilla.com/%LOCALE%/m/"); +pref("app.update.url.details", "http://www.mozilla.com/%LOCALE%/mobile/releases/"); +#else +pref("app.update.interval", 28800); +pref("app.update.url.manual", "http://www.mozilla.com/%LOCALE%/mobile/"); +pref("app.update.url.details", "http://www.mozilla.com/%LOCALE%/mobile/"); +#endif +#endif + +// replace newlines with spaces on paste into single-line text boxes +pref("editor.singleLine.pasteNewlines", 2); + +#ifdef MOZ_SERVICES_SYNC +// sync service +pref("services.sync.registerEngines", "Tab,Bookmarks,Form,History,Password,Prefs"); +pref("services.sync.autoconnectDelay", 5); + +// prefs to sync by default +pref("services.sync.prefs.sync.browser.startup.sessionRestore", true); +pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true); +pref("services.sync.prefs.sync.devtools.errorconsole.enabled", true); +pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true); +pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true); +pref("services.sync.prefs.sync.privacy.donottrackheader.enabled", true); +pref("services.sync.prefs.sync.privacy.donottrackheader.value", true); +pref("services.sync.prefs.sync.signon.rememberSignons", true); +#endif + +// threshold where a tap becomes a drag, in 1/240" reference pixels +// The names of the preferences are to be in sync with nsEventStateManager.cpp +pref("ui.dragThresholdX", 50); +pref("ui.dragThresholdY", 50); + +pref("notification.feature.enabled", true); + +// prevent tooltips from showing up +pref("browser.chrome.toolbar_tips", false); + +// 0: don't show fullscreen keyboard +// 1: always show fullscreen keyboard +// -1: show fullscreen keyboard based on threshold pref +pref("widget.ime.android.landscape_fullscreen", -1); +pref("widget.ime.android.fullscreen_threshold", 250); // in hundreths of inches + +// Completely disable pdf.js as an option to preview pdfs within firefox. +// Note: if this is not disabled it does not necessarily mean pdf.js is the pdf +// handler just that it is an option. +pref("pdfjs.disabled", false); +// Used by pdf.js to know the first time firefox is run with it installed so it +// can become the default pdf viewer. +pref("pdfjs.firstRun", true); +// The values of preferredAction and alwaysAskBeforeHandling before pdf.js +// became the default. +pref("pdfjs.previousHandler.preferredAction", 0); +pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false); + +// The maximum amount of decoded image data we'll willingly keep around (we +// might keep around more than this, but we'll try to get down to this value). +// (This is intentionally on the high side; see bug 746055.) +pref("image.mem.max_decoded_image_kb", 256000); + +// enable touch events interfaces +pref("dom.w3c_touch_events.enabled", 1); +pref("dom.w3c_touch_events.safetyX", 5); // escape borders in units of 1/240" +pref("dom.w3c_touch_events.safetyY", 20); // escape borders in units of 1/240" + +#ifdef MOZ_SAFE_BROWSING +// Safe browsing does nothing unless this pref is set +pref("browser.safebrowsing.enabled", true); + +// Prevent loading of pages identified as malware +pref("browser.safebrowsing.malware.enabled", true); + +// Non-enhanced mode (local url lists) URL list to check for updates +pref("browser.safebrowsing.provider.0.updateURL", "http://safebrowsing.clients.google.com/safebrowsing/downloads?client={moz:client}&appver={moz:version}&pver=2.2"); + +pref("browser.safebrowsing.dataProvider", 0); + +// Does the provider name need to be localizable? +pref("browser.safebrowsing.provider.0.name", "Google"); +pref("browser.safebrowsing.provider.0.keyURL", "https://sb-ssl.google.com/safebrowsing/newkey?client={moz:client}&appver={moz:version}&pver=2.2"); +pref("browser.safebrowsing.provider.0.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/report?"); +pref("browser.safebrowsing.provider.0.gethashURL", "http://safebrowsing.clients.google.com/safebrowsing/gethash?client={moz:client}&appver={moz:version}&pver=2.2"); + +// HTML report pages +pref("browser.safebrowsing.provider.0.reportGenericURL", "http://{moz:locale}.phish-generic.mozilla.com/?hl={moz:locale}"); +pref("browser.safebrowsing.provider.0.reportErrorURL", "http://{moz:locale}.phish-error.mozilla.com/?hl={moz:locale}"); +pref("browser.safebrowsing.provider.0.reportPhishURL", "http://{moz:locale}.phish-report.mozilla.com/?hl={moz:locale}"); +pref("browser.safebrowsing.provider.0.reportMalwareURL", "http://{moz:locale}.malware-report.mozilla.com/?hl={moz:locale}"); +pref("browser.safebrowsing.provider.0.reportMalwareErrorURL", "http://{moz:locale}.malware-error.mozilla.com/?hl={moz:locale}"); + +// FAQ URLs +pref("browser.safebrowsing.warning.infoURL", "http://www.mozilla.com/%LOCALE%/%APP%/phishing-protection/"); +pref("browser.geolocation.warning.infoURL", "http://www.mozilla.com/%LOCALE%/%APP%/geolocation/"); + +// Name of the about: page contributed by safebrowsing to handle display of error +// pages on phishing/malware hits. (bug 399233) +pref("urlclassifier.alternate_error_page", "blocked"); + +// The number of random entries to send with a gethash request. +pref("urlclassifier.gethashnoise", 4); + +// The list of tables that use the gethash request to confirm partial results. +pref("urlclassifier.gethashtables", "goog-phish-shavar,goog-malware-shavar"); + +// If an urlclassifier table has not been updated in this number of seconds, +// a gethash request will be forced to check that the result is still in +// the database. +pref("urlclassifier.max-complete-age", 2700); + +// Maximum size of the sqlite3 cache during an update, in bytes +pref("urlclassifier.updatecachemax", 41943040); + +// URL for checking the reason for a malware warning. +pref("browser.safebrowsing.malware.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site="); +#endif + +// True if this is the first time we are showing about:firstrun +pref("browser.firstrun.show.localepicker", false); + +// True if you always want dump() to work +// +// On Android, you also need to do the following for the output +// to show up in logcat: +// +// $ adb shell stop +// $ adb shell setprop log.redirect-stdio true +// $ adb shell start +pref("javascript.options.showInConsole", true); +pref("browser.dom.window.dump.enabled", true); + +// controls if we want camera support +pref("device.camera.enabled", true); +pref("media.realtime_decoder.enabled", true); + +// Mobile manages state by autodetection +pref("network.manage-offline-status", true); diff --git a/browser/metro/shell/Makefile.in b/browser/metro/shell/Makefile.in new file mode 100644 index 000000000000..83541fff4ac6 --- /dev/null +++ b/browser/metro/shell/Makefile.in @@ -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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +MODULE = metro + +include $(DEPTH)/config/autoconf.mk + +DIRS = commandexecutehandler linktool + +ifdef ENABLE_TESTS +DIRS += testing +endif + +export:: + $(NSINSTALL) $(srcdir)/resources.pri $(DIST)/bin + $(RM) $(DIST)/bin/VisualElementsManifest.xml + $(PYTHON) $(topsrcdir)/config/Preprocessor.py -Fsubstitution $(DEFINES) $(ACDEFINES) -DMOZ_APP_DISPLAYNAME=${MOZ_APP_DISPLAYNAME} \ + $(srcdir)/VisualElementsManifest.xml.in > $(DIST)/bin/VisualElementsManifest.xml + +install:: + $(NSINSTALL) $(srcdir)/resources.pri $(DIST)/bin + +# bug 744566 +# $(RM) $(DIST)/bin/resources.pri +# $(MAKEPRI) new -v -pr $(srcdir)/tileresources -cf $(srcdir)/priconfig.xml -mn $(srcdir)/AppManifest.xml -of $(DIST)/bin/resources.pri -o + +include $(topsrcdir)/config/rules.mk diff --git a/browser/metro/shell/VisualElementsManifest.xml.in b/browser/metro/shell/VisualElementsManifest.xml.in new file mode 100644 index 000000000000..a549ae4b1722 --- /dev/null +++ b/browser/metro/shell/VisualElementsManifest.xml.in @@ -0,0 +1,16 @@ + + + + + + diff --git a/browser/metro/shell/commandexecutehandler/CEHHelper.cpp b/browser/metro/shell/commandexecutehandler/CEHHelper.cpp new file mode 100644 index 000000000000..46bd046ce754 --- /dev/null +++ b/browser/metro/shell/commandexecutehandler/CEHHelper.cpp @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CEHHelper.h" + +HANDLE sCon; +LPCWSTR metroDX10Available = L"MetroD3DAvailable"; + +typedef HRESULT (WINAPI*D3D10CreateDevice1Func) + (IDXGIAdapter *, D3D10_DRIVER_TYPE, HMODULE, UINT, + D3D10_FEATURE_LEVEL1, UINT, ID3D10Device1 **); +typedef HRESULT(WINAPI*CreateDXGIFactory1Func)(REFIID , void **); + +void +Log(const wchar_t *fmt, ...) +{ +#if !defined(SHOW_CONSOLE) + return; +#endif + va_list a = NULL; + wchar_t szDebugString[1024]; + if(!lstrlenW(fmt)) + return; + va_start(a,fmt); + vswprintf(szDebugString, 1024, fmt, a); + va_end(a); + if(!lstrlenW(szDebugString)) + return; + + DWORD len; + WriteConsoleW(sCon, szDebugString, lstrlenW(szDebugString), &len, NULL); + WriteConsoleW(sCon, L"\n", 1, &len, NULL); + + if (IsDebuggerPresent()) { + OutputDebugStringW(szDebugString); + OutputDebugStringW(L"\n"); + } +} + +#if defined(SHOW_CONSOLE) +void +SetupConsole() +{ + FILE *fp; + AllocConsole(); + sCon = GetStdHandle(STD_OUTPUT_HANDLE); + int fd = _open_osfhandle(reinterpret_cast(sCon), 0); + fp = _fdopen(fd, "w"); + *stdout = *fp; + setvbuf(stdout, NULL, _IONBF, 0); +} +#endif + +bool +IsDX10Available() +{ + DWORD isDX10Available; + if (GetDWORDRegKey(metroDX10Available, isDX10Available)) { + return isDX10Available; + } + + HMODULE dxgiModule = LoadLibraryA("dxgi.dll"); + if (!dxgiModule) { + SetDWORDRegKey(metroDX10Available, 0); + return false; + } + CreateDXGIFactory1Func createDXGIFactory1 = + (CreateDXGIFactory1Func) GetProcAddress(dxgiModule, "CreateDXGIFactory1"); + if (!createDXGIFactory1) { + SetDWORDRegKey(metroDX10Available, 0); + return false; + } + + HMODULE d3d10module = LoadLibraryA("d3d10_1.dll"); + if (!d3d10module) { + SetDWORDRegKey(metroDX10Available, 0); + return false; + } + D3D10CreateDevice1Func createD3DDevice = + (D3D10CreateDevice1Func) GetProcAddress(d3d10module, + "D3D10CreateDevice1"); + if (!createD3DDevice) { + SetDWORDRegKey(metroDX10Available, 0); + return false; + } + + CComPtr factory1; + if (FAILED(createDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory1))) { + SetDWORDRegKey(metroDX10Available, 0); + return false; + } + + CComPtr adapter1; + if (FAILED(factory1->EnumAdapters1(0, &adapter1))) { + SetDWORDRegKey(metroDX10Available, 0); + return false; + } + + CComPtr device; + // Try for DX10.1 + if (FAILED(createD3DDevice(adapter1, D3D10_DRIVER_TYPE_HARDWARE, NULL, + D3D10_CREATE_DEVICE_BGRA_SUPPORT | + D3D10_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS, + D3D10_FEATURE_LEVEL_10_1, + D3D10_1_SDK_VERSION, &device))) { + // Try for DX10 + if (FAILED(createD3DDevice(adapter1, D3D10_DRIVER_TYPE_HARDWARE, NULL, + D3D10_CREATE_DEVICE_BGRA_SUPPORT | + D3D10_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS, + D3D10_FEATURE_LEVEL_10_0, + D3D10_1_SDK_VERSION, &device))) { + // Try for DX9.3 (we fall back to cairo and cairo has support for D3D 9.3) + if (FAILED(createD3DDevice(adapter1, D3D10_DRIVER_TYPE_HARDWARE, NULL, + D3D10_CREATE_DEVICE_BGRA_SUPPORT | + D3D10_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS, + D3D10_FEATURE_LEVEL_9_3, + D3D10_1_SDK_VERSION, &device))) { + + SetDWORDRegKey(metroDX10Available, 0); + return false; + } + } + } + + + SetDWORDRegKey(metroDX10Available, 1); + return true; +} + +bool +GetDWORDRegKey(LPCWSTR name, DWORD &value) +{ + HKEY key; + LONG result = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Mozilla\\Firefox", + 0, KEY_READ, &key); + if (result != ERROR_SUCCESS) { + return false; + } + + DWORD bufferSize = sizeof(DWORD); + DWORD type; + result = RegQueryValueExW(key, name, nullptr, &type, + reinterpret_cast(&value), + &bufferSize); + RegCloseKey(key); + return result == ERROR_SUCCESS; +} + +bool +SetDWORDRegKey(LPCWSTR name, DWORD value) +{ + HKEY key; + LONG result = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Mozilla\\Firefox", + 0, KEY_WRITE, &key); + if (result != ERROR_SUCCESS) { + return false; + } + + result = RegSetValueEx(key, name, 0, REG_DWORD, + reinterpret_cast(&value), + sizeof(DWORD)); + RegCloseKey(key); + return result == ERROR_SUCCESS; +} diff --git a/browser/metro/shell/commandexecutehandler/CEHHelper.h b/browser/metro/shell/commandexecutehandler/CEHHelper.h new file mode 100644 index 000000000000..b95e75148724 --- /dev/null +++ b/browser/metro/shell/commandexecutehandler/CEHHelper.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#undef WINVER +#undef _WIN32_WINNT +#define WINVER 0x602 +#define _WIN32_WINNT 0x602 + +#include +#include +#include +#include +#include + +//#define SHOW_CONSOLE 1 +extern HANDLE sCon; +extern LPCWSTR metroDX10Available; + +void Log(const wchar_t *fmt, ...); + +#if defined(SHOW_CONSOLE) +static void SetupConsole(); +#endif + +bool IsDX10Available(); +bool GetDWORDRegKey(LPCWSTR name, DWORD &value); +bool SetDWORDRegKey(LPCWSTR name, DWORD value); diff --git a/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp new file mode 100644 index 000000000000..0fa91ec7f2b3 --- /dev/null +++ b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp @@ -0,0 +1,707 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#include "CEHHelper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef SHOW_CONSOLE +#define DEBUG_DELAY_SHUTDOWN 1 +#endif + +// Heartbeat timer duration used while waiting for an incoming request. +#define HEARTBEAT_MSEC 1000 +// Total number of heartbeats we wait before giving up and shutting down. +#define REQUEST_WAIT_TIMEOUT 30 +// Pulled from desktop browser's shell +#define APP_REG_NAME L"Firefox" + +static const WCHAR* kFirefoxExe = L"firefox.exe"; +static const WCHAR* kMetroFirefoxExe = L"firefox.exe"; +static const WCHAR* kDefaultMetroBrowserIDPathKey = L"FirefoxURL"; +static const WCHAR* kDemoMetroBrowserIDPathKey = L"Mozilla.Firefox.URL"; + +template void SafeRelease(T **ppT) +{ + if (*ppT) { + (*ppT)->Release(); + *ppT = NULL; + } +} + +template HRESULT SetInterface(T **ppT, IUnknown *punk) +{ + SafeRelease(ppT); + return punk ? punk->QueryInterface(ppT) : E_NOINTERFACE; +} + +class __declspec(uuid("5100FEC1-212B-4BF5-9BF8-3E650FD794A3")) + CExecuteCommandVerb : public IExecuteCommand, + public IObjectWithSelection, + public IInitializeCommand, + public IObjectWithSite, + public IExecuteCommandApplicationHostEnvironment +{ +public: + + CExecuteCommandVerb() : + mRef(1), + mShellItemArray(NULL), + mUnkSite(NULL), + mTargetIsFileSystemLink(false), + mIsDesktopRequest(true), + mRequestMet(false) + { + } + + bool RequestMet() { return mRequestMet; } + long RefCount() { return mRef; } + + // IUnknown + IFACEMETHODIMP QueryInterface(REFIID aRefID, void **aInt) + { + static const QITAB qit[] = { + QITABENT(CExecuteCommandVerb, IExecuteCommand), + QITABENT(CExecuteCommandVerb, IObjectWithSelection), + QITABENT(CExecuteCommandVerb, IInitializeCommand), + QITABENT(CExecuteCommandVerb, IObjectWithSite), + QITABENT(CExecuteCommandVerb, IExecuteCommandApplicationHostEnvironment), + { 0 }, + }; + return QISearch(this, qit, aRefID, aInt); + } + + IFACEMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&mRef); + } + + IFACEMETHODIMP_(ULONG) Release() + { + long cRef = InterlockedDecrement(&mRef); + if (!cRef) { + delete this; + } + return cRef; + } + + // IExecuteCommand + IFACEMETHODIMP SetKeyState(DWORD aKeyState) + { + mKeyState = aKeyState; + return S_OK; + } + + IFACEMETHODIMP SetParameters(PCWSTR aParameters) + { + Log(L"SetParameters: '%s'", aParameters); + mParameters = aParameters; + return S_OK; + } + + IFACEMETHODIMP SetPosition(POINT aPoint) + { return S_OK; } + + IFACEMETHODIMP SetShowWindow(int aShowFlag) + { return S_OK; } + + IFACEMETHODIMP SetNoShowUI(BOOL aNoUI) + { return S_OK; } + + IFACEMETHODIMP SetDirectory(PCWSTR aDirPath) + { return S_OK; } + + IFACEMETHODIMP Execute(); + + // IObjectWithSelection + IFACEMETHODIMP SetSelection(IShellItemArray *aArray) + { + if (!aArray) { + return E_FAIL; + } + + SetInterface(&mShellItemArray, aArray); + + DWORD count = 0; + aArray->GetCount(&count); + if (!count) { + return E_FAIL; + } + +#ifdef SHOW_CONSOLE + Log(L"SetSelection param count: %d", count); + for (int idx = 0; idx < count; idx++) { + IShellItem* item = NULL; + if (SUCCEEDED(aArray->GetItemAt(idx, &item))) { + LPWSTR str = NULL; + if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &str))) { + if (FAILED(item->GetDisplayName(SIGDN_URL, &str))) { + Log(L"Failed to get a shell item array item."); + item->Release(); + continue; + } + } + item->Release(); + Log(L"SetSelection param: '%s'", str); + CoTaskMemFree(str); + } + } +#endif + + IShellItem* item = NULL; + if (FAILED(aArray->GetItemAt(0, &item))) { + return E_FAIL; + } + + bool isFileSystem = false; + if (!SetTargetPath(item) || !mTarget.GetLength()) { + Log(L"SetTargetPath failed."); + return E_FAIL; + } + item->Release(); + + Log(L"SetSelection target: %s", mTarget); + return S_OK; + } + + IFACEMETHODIMP GetSelection(REFIID aRefID, void **aInt) + { + *aInt = NULL; + return mShellItemArray ? mShellItemArray->QueryInterface(aRefID, aInt) : E_FAIL; + } + + // IInitializeCommand + IFACEMETHODIMP Initialize(PCWSTR aVerb, IPropertyBag* aPropBag) + { + if (!aVerb) + return E_FAIL; + // 'open', 'edit', etc. Based on our registry settings + Log(L"Initialize(%s)", aVerb); + mVerb = aVerb; + return S_OK; + } + + // IObjectWithSite + IFACEMETHODIMP SetSite(IUnknown *aUnkSite) + { + SetInterface(&mUnkSite, aUnkSite); + return S_OK; + } + + IFACEMETHODIMP GetSite(REFIID aRefID, void **aInt) + { + *aInt = NULL; + return mUnkSite ? mUnkSite->QueryInterface(aRefID, aInt) : E_FAIL; + } + + // IExecuteCommandApplicationHostEnvironment + IFACEMETHODIMP GetValue(AHE_TYPE *aLaunchType) + { + Log(L"IExecuteCommandApplicationHostEnvironment::GetValue()"); + *aLaunchType = AHE_DESKTOP; + mIsDesktopRequest = true; + + if (!mUnkSite) { + Log(L"No mUnkSite."); + return S_OK; + } + + HRESULT hr; + IServiceProvider* pSvcProvider = NULL; + hr = mUnkSite->QueryInterface(IID_IServiceProvider, (void**)&pSvcProvider); + if (!pSvcProvider) { + Log(L"Couldn't get IServiceProvider service from explorer. (%X)", hr); + return S_OK; + } + + IExecuteCommandHost* pHost = NULL; + // If we can't get this it's a conventional desktop launch + hr = pSvcProvider->QueryService(SID_ExecuteCommandHost, + IID_IExecuteCommandHost, (void**)&pHost); + if (!pHost) { + Log(L"Couldn't get IExecuteCommandHost service from explorer. (%X)", hr); + SafeRelease(&pSvcProvider); + return S_OK; + } + SafeRelease(&pSvcProvider); + + EC_HOST_UI_MODE mode; + if (FAILED(pHost->GetUIMode(&mode))) { + Log(L"GetUIMode failed."); + SafeRelease(&pHost); + return S_OK; + } + + // 0 - launched from desktop + // 1 - ? + // 2 - launched from tile interface + Log(L"GetUIMode: %d", mode); + + if (!IsDefaultBrowser()) { + mode = ECHUIM_DESKTOP; + } + + if (mode == ECHUIM_DESKTOP) { + Log(L"returning AHE_DESKTOP"); + SafeRelease(&pHost); + return S_OK; + } + SafeRelease(&pHost); + + if (!IsDX10Available()) { + Log(L"returning AHE_DESKTOP because DX10 is not available"); + *aLaunchType = AHE_DESKTOP; + mIsDesktopRequest = true; + } else { + Log(L"returning AHE_IMMERSIVE"); + *aLaunchType = AHE_IMMERSIVE; + mIsDesktopRequest = false; + } + return S_OK; + } + + bool IsDefaultBrowser() + { + bool result = false; + IApplicationAssociationRegistration* pAAR; + HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistration, + NULL, + CLSCTX_INPROC, + IID_IApplicationAssociationRegistration, + (void**)&pAAR); + if (SUCCEEDED(hr)) { + BOOL res; + hr = pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, + APP_REG_NAME, + &res); + Log(L"QueryAppIsDefaultAll: %d", res); + if (!res) + return false; + // Make sure the Prog ID matches what we have + LPWSTR registeredApp; + hr = pAAR->QueryCurrentDefault(L"http", AT_URLPROTOCOL, AL_EFFECTIVE, + ®isteredApp); + Log(L"QueryCurrentDefault: %X", hr); + if (SUCCEEDED(hr)) { + Log(L"registeredApp=%s", registeredApp); + result = !wcsicmp(registeredApp, kDefaultMetroBrowserIDPathKey); + if (!result) { + result = !wcsicmp(registeredApp, kDemoMetroBrowserIDPathKey); + } + CoTaskMemFree(registeredApp); + } else { + result = false; + } + + pAAR->Release(); + return result; + } + return result; + } +private: + ~CExecuteCommandVerb() + { + SafeRelease(&mShellItemArray); + SafeRelease(&mUnkSite); + } + + void LaunchDesktopBrowser(); + bool SetTargetPath(IShellItem* aItem); + bool IsTargetBrowser(); + + long mRef; + IShellItemArray *mShellItemArray; + IUnknown *mUnkSite; + CStringW mVerb; + CStringW mTarget; + CStringW mParameters; + bool mTargetIsFileSystemLink; + DWORD mKeyState; + bool mIsDesktopRequest; + bool mRequestMet; +}; + +/* + * Retrieve our module dir path. + * + * @aPathBuffer Buffer to fill + */ +static bool GetModulePath(CStringW& aPathBuffer) +{ + WCHAR buffer[MAX_PATH]; + memset(buffer, 0, sizeof(buffer)); + + if (!GetModuleFileName(NULL, buffer, MAX_PATH)) { + Log(L"GetModuleFileName failed."); + return false; + } + + WCHAR* slash = wcsrchr(buffer, '\\'); + if (!slash) + return false; + *slash = '\0'; + + aPathBuffer = buffer; + return true; +} + +/* + * Retrieve 'module dir path\firefox.exe' + * + * @aPathBuffer Buffer to fill + */ +static bool GetDesktopBrowserPath(CStringW& aPathBuffer) +{ + if (!GetModulePath(aPathBuffer)) + return false; + + // ceh.exe sits in dist/bin root with the desktop browser. Since this + // is a firefox only component, this hardcoded filename is ok. + aPathBuffer.Append(L"\\"); + aPathBuffer.Append(kFirefoxExe); + return true; +} + +/* + * Retrieve the app model id of the firefox metro browser. + * + * @aPathBuffer Buffer to fill + * @aCharLength Length of buffer to fill in characters + */ +static bool GetDefaultBrowserAppModelID(WCHAR* aIDBuffer, + long aCharLength) +{ + if (!aIDBuffer || aCharLength <= 0) + return false; + + memset(aIDBuffer, 0, (sizeof(WCHAR)*aCharLength)); + + HKEY key; + if (RegOpenKeyExW(HKEY_CLASSES_ROOT, kDefaultMetroBrowserIDPathKey, + 0, KEY_READ, &key) != ERROR_SUCCESS) { + return false; + } + DWORD len = aCharLength * sizeof(WCHAR); + memset(aIDBuffer, 0, len); + if (RegQueryValueExW(key, L"AppUserModelID", NULL, NULL, + (LPBYTE)aIDBuffer, &len) != ERROR_SUCCESS || !len) { + RegCloseKey(key); + return false; + } + RegCloseKey(key); + return true; +} + +/* + * Determines if the current target points directly to a particular + * browser or to a file or url. + */ +bool CExecuteCommandVerb::IsTargetBrowser() +{ + if (!mTarget.GetLength() || !mTargetIsFileSystemLink) + return false; + + CStringW modulePath; + if (!GetModulePath(modulePath)) + return false; + + modulePath.MakeLower(); + + CStringW tmpTarget = mTarget; + tmpTarget.Replace(L"\"", L""); + tmpTarget.MakeLower(); + + CStringW checkPath; + + checkPath = modulePath; + checkPath.Append(L"\\"); + checkPath.Append(kFirefoxExe); + if (tmpTarget == checkPath) { + return true; + } + return false; +} + +/* + * Updates the current target based on the contents of + * a shell item. + */ +bool CExecuteCommandVerb::SetTargetPath(IShellItem* aItem) +{ + if (!aItem) + return false; + + LPWSTR str = NULL; + mTargetIsFileSystemLink = true; + if (FAILED(aItem->GetDisplayName(SIGDN_FILESYSPATH, &str))) { + if (FAILED(aItem->GetDisplayName(SIGDN_URL, &str))) { + return false; + } + mTargetIsFileSystemLink = false; + } + + mTarget = str; + CoTaskMemFree(str); + return true; +} + +/* + * Desktop launch - Launch the destop browser to display the current + * target using shellexecute. + */ +void CExecuteCommandVerb::LaunchDesktopBrowser() +{ + CStringW browserPath; + if (!GetDesktopBrowserPath(browserPath)) { + return; + } + + // If a taskbar shortcut, link or local file is clicked, the target will + // be the browser exe or file. + CStringW params; + if (!IsTargetBrowser()) { + params += "-url "; + params += mTarget; + } + + Log(L"Desktop Launch: verb:%s exe:%s params:%s", mVerb, browserPath, params); + + SHELLEXECUTEINFOW seinfo; + memset(&seinfo, 0, sizeof(seinfo)); + seinfo.cbSize = sizeof(SHELLEXECUTEINFOW); + seinfo.fMask = NULL; + seinfo.hwnd = NULL; + seinfo.lpVerb = NULL; + seinfo.lpFile = browserPath; + seinfo.lpParameters = params; + seinfo.lpDirectory = NULL; + seinfo.nShow = SW_SHOWNORMAL; + + ShellExecuteExW(&seinfo); +} + +class AutoSetRequestMet +{ +public: + explicit AutoSetRequestMet(bool* aFlag) : + mFlag(aFlag) {} + ~AutoSetRequestMet() { if (mFlag) *mFlag = true; } +private: + bool* mFlag; +}; + +IFACEMETHODIMP CExecuteCommandVerb::Execute() +{ + Log(L"Execute()"); + + // We shut down when this flips to true + AutoSetRequestMet asrm(&mRequestMet); + + if (!mTarget.GetLength()) { + return E_FAIL; + } + + // Launch on the desktop + if (mIsDesktopRequest) { + LaunchDesktopBrowser(); + return S_OK; + } + + // Launch into Metro + IApplicationActivationManager* activateMgr = NULL; + DWORD processID; + if (FAILED(CoCreateInstance(CLSID_ApplicationActivationManager, NULL, + CLSCTX_LOCAL_SERVER, + IID_IApplicationActivationManager, + (void**)&activateMgr))) { + Log(L"CoCreateInstance failed, launching on desktop."); + LaunchDesktopBrowser(); + return S_OK; + } + + HRESULT hr; + WCHAR appModelID[256]; + if (!GetDefaultBrowserAppModelID(appModelID, (sizeof(appModelID)/sizeof(WCHAR)))) { + Log(L"GetDefaultBrowserAppModelID failed, launching on desktop."); + activateMgr->Release(); + LaunchDesktopBrowser(); + return S_OK; + } + + // Hand off focus rights to the out-of-process activation server. Without + // this the metro interface won't launch. + hr = CoAllowSetForegroundWindow(activateMgr, NULL); + if (FAILED(hr)) { + Log(L"CoAllowSetForegroundWindow result %X", hr); + activateMgr->Release(); + return false; + } + + Log(L"Metro Launch: verb:%s appid:%s params:%s", mVerb, appModelID, mTarget); + + // shortcuts to the application + if (IsTargetBrowser()) { + hr = activateMgr->ActivateApplication(appModelID, L"", AO_NONE, &processID); + Log(L"ActivateApplication result %X", hr); + // files + } else if (mTargetIsFileSystemLink) { + hr = activateMgr->ActivateForFile(appModelID, mShellItemArray, mVerb, &processID); + Log(L"ActivateForFile result %X", hr); + // protocols + } else { + hr = activateMgr->ActivateForProtocol(appModelID, mShellItemArray, &processID); + Log(L"ActivateForProtocol result %X", hr); + } + activateMgr->Release(); + return S_OK; +} + +class ClassFactory : public IClassFactory +{ +public: + ClassFactory(IUnknown *punkObject); + ~ClassFactory(); + STDMETHODIMP Register(CLSCTX classContent, REGCLS classUse); + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef() { return 2; } + STDMETHODIMP_(ULONG) Release() { return 1; } + STDMETHODIMP CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv); + STDMETHODIMP LockServer(BOOL); +private: + IUnknown* mUnkObject; + DWORD mRegID; +}; + +ClassFactory::ClassFactory(IUnknown* aUnkObj) : + mUnkObject(aUnkObj), + mRegID(0) +{ + if (mUnkObject) { + mUnkObject->AddRef(); + } +} + +ClassFactory::~ClassFactory() +{ + if (mRegID) { + CoRevokeClassObject(mRegID); + } + mUnkObject->Release(); +} + +STDMETHODIMP +ClassFactory::Register(CLSCTX aClass, REGCLS aUse) +{ + return CoRegisterClassObject(__uuidof(CExecuteCommandVerb), + static_cast(this), + aClass, aUse, &mRegID); +} + +STDMETHODIMP +ClassFactory::QueryInterface(REFIID riid, void **ppv) +{ + IUnknown *punk = NULL; + if (riid == IID_IUnknown || riid == IID_IClassFactory) { + punk = static_cast(this); + } + *ppv = punk; + if (punk) { + punk->AddRef(); + return S_OK; + } else { + return E_NOINTERFACE; + } +} + +STDMETHODIMP +ClassFactory::CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv) +{ + *ppv = NULL; + if (punkOuter) + return CLASS_E_NOAGGREGATION; + return mUnkObject->QueryInterface(riid, ppv); +} + +LONG gObjRefCnt; + +STDMETHODIMP +ClassFactory::LockServer(BOOL fLock) +{ + if (fLock) + InterlockedIncrement(&gObjRefCnt); + else + InterlockedDecrement(&gObjRefCnt); + Log(L"ClassFactory::LockServer() %d", gObjRefCnt); + return S_OK; +} + +int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR pszCmdLine, int) +{ +#if defined(SHOW_CONSOLE) + SetupConsole(); +#endif + //Log(pszCmdLine); + + if (!wcslen(pszCmdLine) || StrStrI(pszCmdLine, L"-Embedding")) + { + CoInitialize(NULL); + + CExecuteCommandVerb *pHandler = new CExecuteCommandVerb(); + if (!pHandler) + return E_OUTOFMEMORY; + + IUnknown* ppi; + pHandler->QueryInterface(IID_IUnknown, (void**)&ppi); + if (!ppi) + return E_FAIL; + + ClassFactory classFactory(ppi); + ppi->Release(); + ppi = NULL; + + // REGCLS_SINGLEUSE insures we only get used once and then discarded. + if (FAILED(classFactory.Register(CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE))) + return -1; + + if (!SetTimer(NULL, 1, HEARTBEAT_MSEC, NULL)) { + Log(L"Failed to set timer, can't process request."); + return -1; + } + + MSG msg; + long beatCount = 0; + while (GetMessage(&msg, 0, 0, 0) > 0) { + if (msg.message == WM_TIMER) { + if (++beatCount > REQUEST_WAIT_TIMEOUT || + (pHandler->RequestMet() && pHandler->RefCount() < 2)) { + break; + } + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + +#ifdef DEBUG_DELAY_SHUTDOWN + Sleep(10000); +#endif + CoUninitialize(); + return 0; + } + return 0; +} diff --git a/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.def b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.def new file mode 100644 index 000000000000..5adbfef6fb1b --- /dev/null +++ b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.def @@ -0,0 +1,6 @@ +LIBRARY CommandExecuteHandler.dll + +EXPORTS + DllCanUnloadNow PRIVATE + DllGetClassObject PRIVATE + diff --git a/browser/metro/shell/commandexecutehandler/Makefile.in b/browser/metro/shell/commandexecutehandler/Makefile.in new file mode 100644 index 000000000000..a93523523a7f --- /dev/null +++ b/browser/metro/shell/commandexecutehandler/Makefile.in @@ -0,0 +1,42 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +NO_PROFILE_GUIDED_OPTIMIZE = 1 + +include $(topsrcdir)/config/config.mk + +# We want this exe in dist/bin +DIST_SUBDIR = + +PROGRAM = CommandExecuteHandler$(BIN_SUFFIX) +DIST_PROGRAM = CommandExecuteHandler$(BIN_SUFFIX) + +# Don't link against mozglue.dll +MOZ_GLUE_LDFLAGS = +MOZ_GLUE_PROGRAM_LDFLAGS = + +CPPSRCS = \ + CommandExecuteHandler.cpp \ + CEHHelper.cpp \ + $(NULL) + +OS_LIBS = \ + kernel32.lib \ + user32.lib \ + ole32.lib \ + shlwapi.lib \ + propsys.lib \ + advapi32.lib \ + $(NULL) + +DEFINES += -DUNICODE -D_UNICODE -DNS_NO_XPCOM + +include $(topsrcdir)/config/rules.mk diff --git a/browser/metro/shell/linktool/Makefile.in b/browser/metro/shell/linktool/Makefile.in new file mode 100644 index 000000000000..56bee83fd975 --- /dev/null +++ b/browser/metro/shell/linktool/Makefile.in @@ -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/. + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +NO_PROFILE_GUIDED_OPTIMIZE = 1 + +include $(topsrcdir)/config/config.mk + +DIST_SUBDIR = metro/install + +PROGRAM = linktool$(BIN_SUFFIX) + +CPPSRCS = linktool.cpp + +OS_LIBS = \ + kernel32.lib \ + user32.lib \ + ole32.lib \ + shlwapi.lib \ + shell32.lib \ + propsys.lib \ + $(NULL) + +DEFINES += -DUNICODE -D_UNICODE + +include $(topsrcdir)/config/rules.mk diff --git a/browser/metro/shell/linktool/linktool.cpp b/browser/metro/shell/linktool/linktool.cpp new file mode 100644 index 000000000000..2fba6bf298af --- /dev/null +++ b/browser/metro/shell/linktool/linktool.cpp @@ -0,0 +1,286 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#undef WINVER +#undef _WIN32_WINNT +#define WINVER 0x602 +#define _WIN32_WINNT 0x602 + +#include +#include +#include +#include +#include +#define INITGUID +#include +#include +#include + +// Indicates that an application supports dual desktop and immersive modes. In Windows 8, this property is only applicable for web browsers. +//DEFINE_PROPERTYKEY(PKEY_AppUserModel_IsDualMode, 0x9F4C2855, 0x9F79, 0x4B39, 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3, 11); + +void DumpParameters(LPCWSTR aTargetPath, LPCWSTR aShortcutPath, LPCWSTR aAppModelID, LPCWSTR aDescription) +{ + if (aTargetPath) + wprintf(L"target path: '%s'\n", aTargetPath); + if (aShortcutPath) + wprintf(L"shortcut path: '%s'\n", aShortcutPath); + if (aAppModelID) + wprintf(L"app id: '%s'\n", aAppModelID); + if (aDescription) + wprintf(L"description: '%s'\n", aDescription); +} + +HRESULT +SetShortcutProps(LPCWSTR aShortcutPath, LPCWSTR aAppModelID, bool aSetID, bool aSetMode) +{ + HRESULT hres; + ::CoInitialize(NULL); + + IPropertyStore *m_pps = NULL; + if (FAILED(hres = SHGetPropertyStoreFromParsingName(aShortcutPath, NULL, GPS_READWRITE, IID_PPV_ARGS(&m_pps)))) { + printf("SHGetPropertyStoreFromParsingName failed\n"); + goto Exit; + } + + if (aSetMode) { + PROPVARIANT propvar; + if (FAILED(hres = InitPropVariantFromBoolean(true, &propvar)) || + FAILED(hres = m_pps->SetValue(PKEY_AppUserModel_IsDualMode, propvar))) { + goto Exit; + } + PropVariantClear(&propvar); + } + + if (aSetID && aAppModelID) { + PROPVARIANT propvar; + if (FAILED(hres = InitPropVariantFromString(aAppModelID, &propvar)) || + FAILED(hres = m_pps->SetValue(PKEY_AppUserModel_ID, propvar))) { + goto Exit; + } + PropVariantClear(&propvar); + } + + hres = m_pps->Commit(); + + Exit: + + if (m_pps) { + m_pps->Release(); + } + + CoUninitialize(); + return hres; +} + +HRESULT +PrintShortcutProps(LPCWSTR aTargetPath) +{ + HRESULT hres; + ::CoInitialize(NULL); + + IPropertyStore *m_pps = NULL; + if (FAILED(hres = SHGetPropertyStoreFromParsingName(aTargetPath, NULL, GPS_READWRITE, IID_PPV_ARGS(&m_pps)))) { + printf("SHGetPropertyStoreFromParsingName failed\n"); + goto Exit; + } + + bool found = false; + + PROPVARIANT propvar; + if (SUCCEEDED(hres = m_pps->GetValue(PKEY_AppUserModel_IsDualMode, &propvar)) && propvar.vt == VT_BOOL && propvar.boolVal == -1) { + printf("PKEY_AppUserModel_IsDualMode found\n"); + PropVariantClear(&propvar); + found = true; + } + + if (SUCCEEDED(hres = m_pps->GetValue(PKEY_AppUserModel_ID, &propvar)) && propvar.pwszVal) { + printf("PKEY_AppUserModel_ID found "); + wprintf(L"value: '%s'\n", propvar.pwszVal); + PropVariantClear(&propvar); + found = true; + } + + if (!found) { + printf("no known properties found.\n"); + } + + Exit: + + if (m_pps) { + m_pps->Release(); + } + + CoUninitialize(); + return hres; +} + +HRESULT +CreateLink(LPCWSTR aTargetPath, LPCWSTR aShortcutPath, LPCWSTR aDescription) +{ + HRESULT hres; + IShellLink* psl; + + wprintf(L"creating shortcut: '%s'\n", aShortcutPath); + + CoInitialize(NULL); + + hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, + IID_IShellLink, (LPVOID*)&psl); + if (FAILED(hres)) { + CoUninitialize(); + return hres; + } + psl->SetPath(aTargetPath); + if (aDescription) { + psl->SetDescription(aDescription); + } else { + psl->SetDescription(L""); + } + + IPersistFile* ppf = NULL; + hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf); + + if (SUCCEEDED(hres)) { + hres = ppf->Save(aShortcutPath, TRUE); + ppf->Release(); + } + psl->Release(); + CoUninitialize(); + return hres; +} + +void DumpCommands() +{ + printf("control options:\n"); + printf(" /CREATE create a shortcut for the target file.\n"); + printf(" /UPDATE update properties on the target file.\n"); + printf(" /PRINT print the known properties set on the target file.\n"); + printf("parameters:\n"); + printf(" /T(path) the full path and filename of the target file.\n"); + printf(" /S(path) with CREATE, the full path and filename of the shortcut to create.\n"); + printf(" /D(string) with CREATE, adds a description to the shortcut.\n"); + printf(" /A(id) the app model id to assign to the shortcut or target file.\n"); + printf(" /M enable support for dual desktop and immersive modes on the shortcut or target file.\n"); +} + +int wmain(int argc, WCHAR* argv[]) +{ + WCHAR shortcutPathStr[MAX_PATH]; + WCHAR targetPathStr[MAX_PATH]; + WCHAR appModelIDStr[MAX_PATH]; + WCHAR descriptionStr[MAX_PATH]; + + shortcutPathStr[0] = '\0'; + targetPathStr[0] = '\0'; + appModelIDStr[0] = '\0'; + descriptionStr[0] = '\0'; + + bool createShortcutFound = false; + bool updateFound = false; + bool shortcutPathFound = false; + bool targetPathFound = false; + bool appModelIDFound = false; + bool modeFound = false; + bool descriptionFound = false; + bool printFound = false; + + int idx; + for (idx = 1; idx < argc; idx++) { + if (!wcscmp(L"/CREATE", argv[idx])) { + createShortcutFound = true; + continue; + } + if (!wcscmp(L"/UPDATE", argv[idx])) { + updateFound = true; + continue; + } + if (!wcscmp(L"/PRINT", argv[idx])) { + printFound = true; + continue; + } + + if (!wcsncmp(L"/S", argv[idx], 2) && wcslen(argv[idx]) > 2) { + wcscpy_s(shortcutPathStr, MAX_PATH, (argv[idx]+2)); + shortcutPathFound = true; + continue; + } + if (!wcsncmp(L"/T", argv[idx], 2) && wcslen(argv[idx]) > 2) { + wcscpy_s(targetPathStr, MAX_PATH, (argv[idx]+2)); + targetPathFound = true; + continue; + } + if (!wcsncmp(L"/A", argv[idx], 2) && wcslen(argv[idx]) > 2) { + wcscpy_s(appModelIDStr, MAX_PATH, (argv[idx]+2)); + appModelIDFound = true; + continue; + } + if (!wcsncmp(L"/D", argv[idx], 2) && wcslen(argv[idx]) > 2 && wcslen(argv[idx]) < MAX_PATH) { + wcscpy_s(descriptionStr, MAX_PATH, (argv[idx]+2)); + descriptionFound = true; + continue; + } + if (!wcscmp(L"/M", argv[idx])) { + modeFound = true; + continue; + } + } + + DumpParameters(targetPathStr, shortcutPathStr, appModelIDStr, descriptionStr); + + if (!createShortcutFound && !updateFound && !printFound) { + DumpCommands(); + return 0; + } + + if (!targetPathFound) { + printf("missing target file path.\n"); + return -1; + } + + HRESULT hres; + + if (printFound) { + if (FAILED(hres = PrintShortcutProps(targetPathStr))) { + printf("failed printing target props HRESULT=%X\n", hres); + return -1; + } + return 0; + } + + if (createShortcutFound && !shortcutPathFound) { + printf("missing shortcut file path.\n"); + return -1; + } + + if (updateFound && !appModelIDFound && !modeFound) { + printf("no properties selected.\n"); + return -1; + } + + if (createShortcutFound) { + if (FAILED(hres = CreateLink(targetPathStr, shortcutPathStr, (descriptionFound ? descriptionStr : NULL)))) { + printf("failed creating shortcut HRESULT=%X\n", hres); + return -1; + } + } + + LPCWSTR target; + if (createShortcutFound) { + target = shortcutPathStr; + } else { + target = targetPathStr; + } + + if (appModelIDFound || modeFound) { + if (FAILED(hres = SetShortcutProps(target, (appModelIDFound ? appModelIDStr : NULL), appModelIDFound, modeFound))) { + printf("failed adding property HRESULT=%X\n", hres); + return -1; + } + } + + return 0; +} + diff --git a/browser/metro/shell/priconfig.xml b/browser/metro/shell/priconfig.xml new file mode 100644 index 000000000000..b2bed133b555 --- /dev/null +++ b/browser/metro/shell/priconfig.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/metro/shell/resources.pri b/browser/metro/shell/resources.pri new file mode 100644 index 0000000000000000000000000000000000000000..c57e50a7877e3355d81e11f8d78f387561d640ce GIT binary patch literal 6264 zcmeHM&2Jl35dW<|Hi>bHY552h2!jeTRcR8$p+qPMQle5?WTl8k!I4RAlUT$bvRfsG z-r&H2Gve3-2aY{($bmlqK@S`_``kmtDM%=n@SEAU@$T9iuhZp%c&n#*A2Yw1H*em& z^}5k%l-r&99AII02FL@r$C|?7AqhTfkfB_TlR=2WMYiz9N^GC9|1e-}S->G%Wjg5A>+?UU#fxXk=`UPLg1fOZvMZDk)a2?Me ziqr&yn0oo0QK^(odftZuQeZym6cAl;|lBv6W;b$@m%WK@u;ah|~Cr8j{pj#B=zPvPqo94MyL(^1R4)j$Taid7fU) z;!Pr^k=ORS#7toZ*U2yTCMdf^{_k|v65Vd3NPJq979ZM_7t1Y1gveNw2InLerqpA$ zjx3{&ZF;B6*hvZAil~OJwxdAkks3{r37KBSY>Z$*+<#+*~U+h2#p!fzp;DRCuIFJURT5r>8l0 ziu^O=f0gw-=Vr<9n)1S0MNt#d8h$i1C5o&ye6DoWxiQ&)S`Ar4+t3F}7f=u+>mA%T z^pVm63W|4?XIkcO;hY#v`i-HF3@yq+IR2BNGkQ5(MnM;K8QX^5S2~A%rEB;>7g~}O zPx%r`hVCh)y%}e`ZRi6-3zA@I?_Jfih&@C9HuRF>p2x1zBEDAs75rgnK@zsLg1br= z@w?Jxgb+rgCnYvffz3 zy%- +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const WCHAR* kFirefoxExe = L"firefox.exe"; +static const WCHAR* kDefaultMetroBrowserIDPathKey = L"FirefoxURL"; +static const WCHAR* kDemoMetroBrowserIDPathKey = L"Mozilla.Firefox.URL"; + +static void Log(const wchar_t *fmt, ...) +{ + va_list a = NULL; + wchar_t szDebugString[1024]; + if(!lstrlenW(fmt)) + return; + va_start(a,fmt); + vswprintf(szDebugString, 1024, fmt, a); + va_end(a); + if(!lstrlenW(szDebugString)) + return; + + wprintf(L"INFO | metrotestharness.exe | %s\n", szDebugString); + fflush(stdout); +} + +static void Fail(const wchar_t *fmt, ...) +{ + va_list a = NULL; + wchar_t szDebugString[1024]; + if(!lstrlenW(fmt)) + return; + va_start(a,fmt); + vswprintf(szDebugString, 1024, fmt, a); + va_end(a); + if(!lstrlenW(szDebugString)) + return; + + wprintf(L"TEST-UNEXPECTED-FAIL | metrotestharness.exe | %s\n", szDebugString); + fflush(stdout); +} + +/* + * Retrieve our module dir path. + * + * @aPathBuffer Buffer to fill + */ +static bool GetModulePath(CStringW& aPathBuffer) +{ + WCHAR buffer[MAX_PATH]; + memset(buffer, 0, sizeof(buffer)); + + if (!GetModuleFileName(NULL, buffer, MAX_PATH)) { + Fail(L"GetModuleFileName failed."); + return false; + } + + WCHAR* slash = wcsrchr(buffer, '\\'); + if (!slash) + return false; + *slash = '\0'; + + aPathBuffer = buffer; + return true; +} + +/* + * Retrieve 'module dir path\firefox.exe' + * + * @aPathBuffer Buffer to fill + */ +static bool GetDesktopBrowserPath(CStringW& aPathBuffer) +{ + if (!GetModulePath(aPathBuffer)) + return false; + + // ceh.exe sits in dist/bin root with the desktop browser. Since this + // is a firefox only component, this hardcoded filename is ok. + aPathBuffer.Append(L"\\"); + aPathBuffer.Append(kFirefoxExe); + return true; +} + +/* + * Retrieve the app model id of the firefox metro browser. + * + * @aPathBuffer Buffer to fill + * @aCharLength Length of buffer to fill in characters + */ +static bool GetDefaultBrowserAppModelID(WCHAR* aIDBuffer, + long aCharLength) +{ + if (!aIDBuffer || aCharLength <= 0) + return false; + + memset(aIDBuffer, 0, (sizeof(WCHAR)*aCharLength)); + + HKEY key; + if (RegOpenKeyExW(HKEY_CLASSES_ROOT, kDefaultMetroBrowserIDPathKey, + 0, KEY_READ, &key) != ERROR_SUCCESS) { + if (RegOpenKeyExW(HKEY_CLASSES_ROOT, kDemoMetroBrowserIDPathKey, + 0, KEY_READ, &key) != ERROR_SUCCESS) { + return false; + } + } + DWORD len = aCharLength * sizeof(WCHAR); + memset(aIDBuffer, 0, len); + if (RegQueryValueExW(key, L"AppUserModelID", NULL, NULL, + (LPBYTE)aIDBuffer, &len) != ERROR_SUCCESS || !len) { + RegCloseKey(key); + return false; + } + RegCloseKey(key); + return true; +} + +CString sAppParams; + +static bool Launch() +{ + Log(L"Launching browser..."); + + DWORD processID; + + // The interface that allows us to activate the browser + IApplicationActivationManager* activateMgr = NULL; + if (FAILED(CoCreateInstance(CLSID_ApplicationActivationManager, NULL, + CLSCTX_LOCAL_SERVER, + IID_IApplicationActivationManager, + (void**)&activateMgr))) { + Fail(L"CoCreateInstance CLSID_ApplicationActivationManager failed."); + return false; + } + + HRESULT hr; + WCHAR appModelID[256]; + // Activation is based on the browser's registered app model id + if (!GetDefaultBrowserAppModelID(appModelID, (sizeof(appModelID)/sizeof(WCHAR)))) { + Fail(L"GetDefaultBrowserAppModelID failed."); + activateMgr->Release(); + return false; + } + Log(L"App model id='%s'", appModelID); + + // Hand off focus rights to the out-of-process activation server. Without + // this the metro interface won't launch. + hr = CoAllowSetForegroundWindow(activateMgr, NULL); + if (FAILED(hr)) { + Fail(L"CoAllowSetForegroundWindow result %X", hr); + activateMgr->Release(); + return false; + } + + Log(L"Harness process id: %d", GetCurrentProcessId()); + + // Because we can't pass command line args, we store params in a + // tests.ini file in dist/bin which the browser picks up on launch. + char path[MAX_PATH]; + if (!GetModuleFileNameA(NULL, path, MAX_PATH)) { + Fail(L"GetModuleFileNameA errorno=%d", GetLastError()); + activateMgr->Release(); + return false; + } + char* slash = strrchr(path, '\\'); + if (!slash) + return false; + *slash = '\0'; // no trailing slash + CStringA testFilePath = path; + testFilePath += "\\tests.ini"; + + HANDLE hTestFile = CreateFileA(testFilePath, GENERIC_WRITE, + 0, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + if (hTestFile == INVALID_HANDLE_VALUE) { + Fail(L"CreateFileA errorno=%d", GetLastError()); + activateMgr->Release(); + return false; + } + + CStringA asciiParams = sAppParams; + if (!WriteFile(hTestFile, asciiParams, asciiParams.GetLength(), NULL, 0)) { + CloseHandle(hTestFile); + Fail(L"WriteFile errorno=%d", GetLastError()); + activateMgr->Release(); + return false; + } + FlushFileBuffers(hTestFile); + CloseHandle(hTestFile); + + // Launch firefox + hr = activateMgr->ActivateApplication(appModelID, L"", AO_NOERRORUI, &processID); + if (FAILED(hr)) { + Fail(L"ActivateApplication result %X", hr); + activateMgr->Release(); + return false; + } + + Log(L"Activation succeeded. processid=%d", processID); + + HANDLE child = OpenProcess(SYNCHRONIZE, FALSE, processID); + if (!child) { + Fail(L"Couldn't find child process. (%d)", GetLastError()); + activateMgr->Release(); + return false; + } + + Log(L"Waiting on child process..."); + + MSG msg; + DWORD waitResult = WAIT_TIMEOUT; + while ((waitResult = WaitForSingleObject(child, 10)) != WAIT_OBJECT_0) { + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + Log(L"Exiting."); + activateMgr->Release(); + DeleteFileA(testFilePath); + return true; +} + +int wmain(int argc, WCHAR* argv[]) +{ + CoInitialize(NULL); + + int idx; + for (idx = 1; idx < argc; idx++) { + sAppParams.Append(argv[idx]); + sAppParams.Append(L" "); + } + sAppParams.Trim(); + Log(L"args: '%s'", sAppParams); + Launch(); + + CoUninitialize(); + return 0; +} diff --git a/browser/metro/shell/tileresources/Resources.resw b/browser/metro/shell/tileresources/Resources.resw new file mode 100644 index 000000000000..175b01970edb --- /dev/null +++ b/browser/metro/shell/tileresources/Resources.resw @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Custom String Value + + diff --git a/browser/metro/shell/tileresources/layout.resfiles b/browser/metro/shell/tileresources/layout.resfiles new file mode 100644 index 000000000000..7dc302b5cf3e --- /dev/null +++ b/browser/metro/shell/tileresources/layout.resfiles @@ -0,0 +1,47 @@ +images\PinnedSiteLogo.png +images\PinnedSiteLogo.scale-100.png +images\PinnedSiteLogo.scale-140.png +images\PinnedSiteLogo.scale-180.png +images\PinnedSiteLogo.scale-80.png +images\PinnedSiteSmallLogo.png +images\PinnedSiteSmallLogo.scale-100.png +images\PinnedSiteSmallLogo.scale-140.png +images\PinnedSiteSmallLogo.scale-180.png +images\PinnedSiteSmallLogo.scale-80.png +images\smallLogo.contrast-black_scale-100.png +images\smallLogo.contrast-black_scale-140.png +images\smallLogo.contrast-black_scale-180.png +images\smallLogo.contrast-black_scale-80.png +images\smallLogo.contrast-white_scale-100.png +images\smallLogo.contrast-white_scale-140.png +images\smallLogo.contrast-white_scale-180.png +images\smallLogo.contrast-white_scale-80.png +images\smallLogo.png +images\smallLogo.scale-100.png +images\smallLogo.scale-140.png +images\smallLogo.scale-180.png +images\smallLogo.scale-80.png +images\splashscreen.contrast-black_scale-100.png +images\splashscreen.contrast-black_scale-140.png +images\splashscreen.contrast-black_scale-180.png +images\splashscreen.contrast-white_scale-100.png +images\splashscreen.contrast-white_scale-140.png +images\splashscreen.contrast-white_scale-180.png +images\splashscreen.png +images\splashscreen.scale-100.png +images\splashscreen.scale-140.png +images\splashscreen.scale-180.png +images\tileLogo.contrast-black_scale-100.png +images\tileLogo.contrast-black_scale-140.png +images\tileLogo.contrast-black_scale-180.png +images\tileLogo.contrast-black_scale-80.png +images\tileLogo.contrast-white_scale-100.png +images\tileLogo.contrast-white_scale-140.png +images\tileLogo.contrast-white_scale-180.png +images\tileLogo.contrast-white_scale-80.png +images\tileLogo.png +images\tileLogo.scale-100.png +images\tileLogo.scale-140.png +images\images\tileLogo.scale-180.png +images\tileLogo.scale-80.png +xaml\MainPage.xaml diff --git a/browser/metro/shell/tileresources/resources.resfiles b/browser/metro/shell/tileresources/resources.resfiles new file mode 100644 index 000000000000..e5da0c4fb339 --- /dev/null +++ b/browser/metro/shell/tileresources/resources.resfiles @@ -0,0 +1 @@ +Resources.resw diff --git a/browser/metro/theme/Makefile.in b/browser/metro/theme/Makefile.in new file mode 100644 index 000000000000..1b4da71aeab5 --- /dev/null +++ b/browser/metro/theme/Makefile.in @@ -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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +include $(topsrcdir)/config/rules.mk diff --git a/browser/metro/theme/about.css b/browser/metro/theme/about.css new file mode 100644 index 000000000000..f3daef912624 --- /dev/null +++ b/browser/metro/theme/about.css @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +html { + background: #f0f0f0; + padding: 0 1em; + font-family: "Nokia Sans", Tahoma, sans-serif !important; + font-size: 100% !important; +} + +body { + color: black; + position: relative; + min-width: 330px; + max-width: 50em; + margin: 1em auto; + border: 1px solid gray; + border-radius: 10px; + padding: 3em; + -moz-padding-start: 30px; + background: white; +} + +.aboutPageWideContainer { + max-width: 80%; +} + +#aboutLogoContainer { + border: 1px solid lightgray; + width: 300px; + margin-bottom: 2em; +} + +img { + border: 0; +} + +#version { + font-weight: bold; + color: #909090; + margin: -24px 0 9px 17px; +} + +ul { + margin: 0; + -moz-margin-start: 1.5em; + padding: 0; + list-style: square; +} diff --git a/browser/metro/theme/aboutPage.css b/browser/metro/theme/aboutPage.css new file mode 100644 index 000000000000..71bb7405b1b1 --- /dev/null +++ b/browser/metro/theme/aboutPage.css @@ -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/. */ + +#aboutLogoContainer { + width: 300px; +} + +#version { + font-weight: bold; + color: #909090; + margin: -24px 20px 0 118px; +} + +#update { + float: right; + padding: 8px; + margin-top: -32px; +} + +body[dir="rtl"] #update { + float: left; +} + +#update-message-checking, +#update-message-none, +#update-message-found { + display: none; + float: right; + padding: 8px; + margin-top: -32px; +} + +body[dir="rtl"] #update-message-checking, +body[dir="rtl"] #update-message-none, +body[dir="rtl"] #update-message-found { + float: left; +} + +#aboutLinks { + background-color: white; + padding: 5px; + border: 2px solid #e6e5e3; + font-size: 24px; +} + +#aboutLinks > li { + clear: both; + border-bottom: 2px solid #e6e5e3; + list-style: none; + -moz-padding-end: 16px; +} + +#aboutLinks > li:last-child { + border-bottom: 0; +} + +#aboutLinks > li > a { + padding: 16px; + display: block; + color: #3a3834; + background: url("images/arrowright-16.png") right center no-repeat; +} + +body[dir="rtl"] #aboutLinks > li > a { + background: url("images/arrowleft-16.png") left center no-repeat; +} + +#aboutDetails { + margin-top: 15px; + font-size: 18px; +} diff --git a/browser/metro/theme/browser.css b/browser/metro/theme/browser.css new file mode 100644 index 000000000000..8a9b068de92b --- /dev/null +++ b/browser/metro/theme/browser.css @@ -0,0 +1,1131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +%filter substitution +%include defines.inc + +%define forward_transition_length 150ms +%define forward_width 51px +%define back_width 62px +%define clipped_url_back_width 71px + +/* Sliding Toolbar/Tab Tray ------------------------------------------------- */ + +#tray { + transition: transform 0.2s ease-out; + transform: translateY(-@tray_slide_height@); + width: 100%; +} + +#progress-control { + display: block; + height: @progress_height@; + max-height: @progress_height@; + margin-bottom: -@progress_height@; + opacity: 1; + background: linear-gradient(to right, @progress_start_color@, @progress_end_color@); + transition-property: width; + transition-duration: .3s; + -moz-user-focus: ignore; +} + +#progress-control:-moz-dir(rtl) { + background: linear-gradient(to left, @progress_start_color@, @progress_end_color@); +} + +#progress-control[fade] { + opacity: 0; + transition-property: width, opacity; + transition-duration: .3s, .5s; + transition-timing-function: ease-in, ease-in; +} + +/* in non-tabsonly mode the navigation bar and tab tray float over content. In + tabsonly mode they are always visible and offset content. */ +#tray:not([tabsonly=true]) { + position: fixed; +} + +#tray[visible][expanded] { + transform: none; +} + +#tray[startpage], +#tray[visible]:not([expanded]) { + transform: translateY(-@tabs_height@); +} + +/* Tabs --------------------------------------------------------------------- */ + +#tabs-container { + background: @panel_dark_color@ @panel_dark_background@; + padding: 0; + -moz-padding-start: @metro_spacing_xnormal@; + width: 100%; +} + +#tabs { + -moz-padding-start: @metro_spacing_large@; +} + +#tabs .tabs-list { + display: block; + -moz-user-focus: ignore; + padding: 0; + background-color: transparent; + margin: 0; + overflow: auto; +} + +#tabs > .tabs-scrollbox > .arrowscrollbox-scrollbox { + overflow: hidden; +} + +#tabs[input="imprecise"] > .tabs-scrollbox > .scrollbutton-up, +#tabs[input="imprecise"] > .tabs-scrollbox > .scrollbutton-down { + visibility: collapse !important; +} +#tabs > .tabs-scrollbox > .scrollbutton-up { + list-style-image: url("images/tab-arrows.png") !important; + -moz-image-region: rect(15px 58px 63px 14px) !important; +} +#tabs > .tabs-scrollbox > .scrollbutton-up:hover { + -moz-image-region: rect(14px 102px 62px 58px) !important; +} +#tabs > .tabs-scrollbox > .scrollbutton-up:active { + -moz-image-region: rect(14px 152px 62px 108px) !important; +} +#tabs > .tabs-scrollbox > .scrollbutton-up[disabled="true"] { + -moz-image-region: rect(15px 196px 63px 152px) !important; +} + +#tabs > .tabs-scrollbox > .scrollbutton-down { + list-style-image: url("images/tab-arrows.png") !important; + -moz-image-region: rect(73px 58px 121px 14px) !important; +} +#tabs > .tabs-scrollbox > .scrollbutton-down:hover { + -moz-image-region: rect(72px 102px 120px 58px) !important; +} +#tabs > .tabs-scrollbox > .scrollbutton-down:active { + -moz-image-region: rect(72px 152px 120px 108px) !important; +} +#tabs > .tabs-scrollbox > .scrollbutton-down[disabled="true"] { + -moz-image-region: rect(73px 196px 121px 152px) !important; +} + + +@-moz-keyframes open-documenttab { + 0% { + opacity: 0; + transform: scale(0, 0); + } + + 100% { + opacity: 1; + transform: scale(1, 1); + } +} + +#tray:not([tabsonly=true]) documenttab > .documenttab-container { + animation: open-documenttab; + animation-duration: 0.4s; + animation-timing-function: ease-out; +} + +#tray:not([tabsonly=true]) .documenttab-favicon { + visibility: collapse; +} + +.documenttab-thumbnail { + margin: @metro_spacing_normal@ @metro_spacing_snormal@; + background: white none center top no-repeat; + background-size: cover; + width: @thumbnail_width@; + height: @thumbnail_height@; +} + +.documenttab-title { + margin: @metro_spacing_normal@ @metro_spacing_snormal@; + margin-top: 0; + font-size: @metro_font_normal@; + width: @thumbnail_width@; + padding: 4px @metro_spacing_snormal@ 8px; + + background: #000; + opacity: 0.95; + color: #fff; + box-shadow: 0 0 @metro_spacing_snormal@ rgba(0, 0, 0, 0.25); +} + +.documenttab-crop { + background: transparent url("chrome://browser/skin/images/tab-crop.png") 50% 50% no-repeat; +} + +.documenttab-selection { + background: transparent -moz-image-rect(url("chrome://browser/skin/images/tab-overlay.png"), 0%, 100%, 50%, 0%) 50% 50% no-repeat; +} + +documenttab[selected=true] .documenttab-selection { + background: transparent -moz-image-rect(url("chrome://browser/skin/images/tab-overlay.png"), 50%, 100%, 100%, 0%) 50% 50% no-repeat; +} + +.documenttab-close { + background: none !important; + padding: @metro_spacing_small@ !important; + margin-top: @metro_spacing_snormal@; + -moz-margin-end: @metro_spacing_xsmall@; + border-color: transparent !important; + list-style-image: url("chrome://browser/skin/images/closetab-default.png"); +} + +#tray[tabsonly=true] { + transform: none !important; +} + +#tray[tabsonly=true] #tabs { + -moz-padding-start: @metro_spacing_small@; +} + +#tray[tabsonly=true] #tabs-controls { + -moz-box-align: center; + -moz-box-orient: horizontal; + -moz-box-pack: end; + margin: 0; +} + +#tray[tabsonly=true] #tabs-controls toolbarbutton { + margin-top: 0; + margin-bottom: 0; +} + +#tray[tabsonly=true] documenttab { + height: @toolbar_height@; + margin: 0 -@tab_compression@; +} + +#tray[tabsonly=true] documenttab:first-child { + -moz-margin-start: 0; +} + +#tray[tabsonly=true] documenttab:last-child { + -moz-margin-end: 0; +} + +#tray[tabsonly=true] .documenttab-thumbnail, +#tray[tabsonly=true] .documenttab-selection, +#tray[tabsonly=true] .documenttab-crop { + visibility: collapse; +} + +#tray[tabsonly=true] .documenttab-container { + display: -moz-box; + -moz-box-orient: horizontal; + -moz-box-align: center; + padding: 0 @tab_spacing@; +} + +#tray[tabsonly=true] .documenttab-favicon { + -moz-margin-start: @metro_spacing_normal@; + -moz-margin-end: @metro_spacing_snormal@; +} + +#tray[tabsonly=true] .documenttab-title { + padding: 0; + margin: 0; + height: auto; + background: 0 none; + opacity: 1; + box-shadow: none; + width: @tab_inner_width@; +} + +#tray[tabsonly=true] .documenttab-close { + list-style-image: url("chrome://browser/skin/images/closetab-tab.png"); + position: relative; + padding: 0 !important; + z-index: 1; +} + +#tray[tabsonly=true] documenttab[selected=true] { + background-color: @panel_light_color@; + background-image: url("chrome://browser/skin/images/tab-selection-left.png"), + url("chrome://browser/skin/images/tab-selection-right.png"), + @panel_light_background@; + background-position: left top, right top, center center; + background-repeat: no-repeat, no-repeat, repeat; +} + +#tray[tabsonly=true] documenttab[selected=true] .documenttab-close { + list-style-image: url("chrome://browser/skin/images/closetab-tabselected.png"); +} + +#page, +.selection-overlay { + -moz-stack-sizing: ignore; +} + +.selection-overlay:-moz-focusring { + outline: 0 !important; +} + +.selection-overlay-hidden { + display: none; +} + +#tray[tabsonly=true] documenttab[selected=true] .documenttab-title { + color: #000; +} + +#tabs-controls { + margin-top: @metro_spacing_small@; + -moz-box-align: start; + -moz-box-orient: vertical; + padding: 0 @metro_spacing_small@; +} + +#tabs-controls toolbarbutton { + margin: @toolbar_vertical_spacing@ @toolbar_horizontal_spacing@; +} + +#newtab-button { + list-style-image: url("images/newtab-default.png"); +} + +/* Toolbar ------------------------------------------------------------------ */ + +#toolbar-container { + background: @panel_dark_color@ @panel_dark_background@; + border-bottom: @border_width_small@ solid @border_color@; + -moz-padding-end: @padding_large@; + width: 100%; +} + +#toolbar-container[filtering], +#toolbar-container[startpage] { + border-bottom-width: 0; +} + +#toolbar { + -moz-appearance: none; + -moz-box-align: center; + padding: 0; + -moz-padding-end: @metro_spacing_xxxlarge@; + -moz-padding-start: @metro_spacing_xxnormal@; + border: none; + border-top: @metro_border_thin@ solid #293642; + background-color: @panel_light_color@; + background-image: url("chrome://browser/skin/images/tab-selection-right.png"), + linear-gradient(rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.5)), + @panel_light_background@; + background-repeat: no-repeat, repeat-x; + background-position: right bottom; + min-height: @toolbar_height@; +} + +#toolbar[dir=ltr] { + background-position: left bottom; +} + +#toolbar toolbarbutton { + margin: 0 @toolbar_horizontal_spacing@; +} + +/* Unified back-forward buttons */ +/* TODO: Pull code from mainline firefox to support RTL. */ +#unified-back-forward-button { + -moz-box-align: center; + position: relative; + z-index: 1; +} + +#back-button { + -moz-appearance: none; + margin: 0 !important; + margin-right: -@metro_spacing_normal@ !important; + list-style-image: url(chrome://browser/skin/images/back.png); + -moz-image-region: rect(0 48px 48px 0); + position: relative; + z-index: 1; + padding: 0 !important; + min-height: 48px !important; + max-height: 48px !important; +} + +#back-button[disabled="true"] { + -moz-image-region: rect(0 96px 48px 48px); +} + +#forward-button { + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.5)), @panel_light_background@; + border: @metro_border_thick@ solid rgb(192, 198, 204); + margin: 0 !important; + padding: 0 !important; + -moz-padding-start: 17px !important; + -moz-padding-end: 7px !important; + transition: opacity @forward_transition_length@ ease-out; + list-style-image: url(chrome://browser/skin/images/forward.png); +} + +/* XXX: Hack to move the image up one pixel because + it's not vertically centered for some reason. */ +#forward-button image { + margin: -1px 0 1px 0 !important; +} + +#unified-back-forward-button > #forward-button[disabled="true"] { + opacity: 0; +} + +/* URL bar */ +#unified-back-forward-button + #urlbar-container { + margin: 0; + padding: 0; + padding-left: @back_width@; + -moz-margin-start: -@forward_width@; + -moz-margin-end: @metro_spacing_normal@; + position: relative; + pointer-events: none; + + border: @metro_border_thick@ solid @urlbar_border_color@; + -moz-border-start: 0 none; + background: @field_background_color@; +} + +#unified-back-forward-button + #urlbar-container > #urlbar { + -moz-border-start: none; + pointer-events: all; + transition: margin-left @forward_transition_length@ ease-out; +} + +#unified-back-forward-button[forwarddisabled="true"] + #urlbar-container { + clip-path: url("chrome://browser/content/browser.xul#back-button-clip-path"); + padding-left: @clipped_url_back_width@; +} + +#unified-back-forward-button[forwarddisabled="true"] + #urlbar-container > #urlbar { + margin-left: -@forward_width@; +} + +/* Identity widget */ +#identity-icon { + width: @metro_spacing_xxnormal@; + height: @metro_spacing_xxnormal@; + margin: 0; + -moz-margin-end: @metro_spacing_small@; + padding: 0; + list-style-image: url(chrome://browser/skin/images/identity-icons-generic.png); +} + +#identity-box[mode="verifiedDomain"] > #identity-icon[pageproxystate="valid"] { + list-style-image: url(chrome://browser/skin/images/identity-icons-https.png); +} + +#identity-box[mode="verifiedIdentity"] > #identity-icon[pageproxystate="valid"] { + list-style-image: url(chrome://browser/skin/identity-icons-https-ev.png); +} + +/* Main URL textbox */ +#urlbar-edit { + margin: 0 !important; + min-height: @urlbar_edit_height@; + -moz-appearance: none !important; + border-radius: 0; + border: 0 none !important; + padding: 0 !important; +} + +#urlbar-edit :invalid { + box-shadow: none; +} + +/* Combined stop-reload button */ +#tool-reload { + list-style-image: url("chrome://browser/skin/images/reload.png"); +} + +#tool-stop { + list-style-image: url("chrome://browser/skin/images/stop-hdpi.png"); +} + +#urlbar-icons[mode="loading"] > #tool-reload { + visibility: collapse; +} + +#urlbar-icons[mode="edit"] > #tool-stop, +#urlbar-icons[mode="view"] > #tool-stop { + visibility: collapse; +} + +/* Toggle that displays the tab bar */ +#toolbar-transition { + -moz-padding-end: @metro_spacing_snormal@; + background: @panel_dark_color@ @panel_dark_background@; +} + +#tool-new-tab { + margin: 0; + -moz-margin-start: -@metro_spacing_normal@; + list-style-image: url("images/newtab-default.png"); + transition: opacity ease-out 0.2s; +} + +/* Hide the tab toggle if the tabs are visible */ +#tray[visible][expanded] #tool-new-tab { + opacity: 0; +} + +/* Hide the tab toggle if we're showing classic tabs or we're snap-viewed. */ +#toolbar[viewstate="snapped"], +#tray[tabsonly=true] #toolbar { + background: @panel_light_color@ @panel_light_background@; + -moz-padding-end: 0; +} + +#toolbar-container[viewstate="snapped"], +#tray[tabsonly=true] #toolbar-container { + -moz-padding-end: 0; +} + +#toolbar-transition[viewstate="snapped"], +#tray[tabsonly=true] #toolbar-transition { + visibility: collapse; +} + +/* If we're in the small snap view, compress and simplify the UI. */ +#tray[visible][expanded][viewstate="snapped"] { + margin-top: -@tabs_height@ !important; +} + +#toolbar[viewstate="snapped"] { + -moz-padding-end: 0; +} + +#unified-back-forward-button[viewstate="snapped"] + #urlbar-container { + -moz-margin-end: 0; +} + +/* App Bar ----------------------------------------------------------------- */ + +appbar { + display: block; + position: fixed; + height: @toolbar_height@; + bottom: 0; + transform: translateY(@toolbar_height@); + transition: transform 0.2s ease-out; + width: 100%; +} + +appbar toolbar { + border-top: 1px solid @appbar_top_border@; + border-bottom: 0px; + height: @toolbar_height@; + -moz-appearance: none; + background-color: @appbar_color@; + -moz-box-align: center; + width: 100%; +} + +appbar toolbarbutton { + float: left; + border-width: 0px; + margin: 0 @toolbar_horizontal_spacing@; + padding: 0; + /* Don't inherit background-color from toolbarbutton[checked="true"] */ + background-color: transparent; +} + +appbar toolbarbutton[disabled="true"] { + visibility: collapse; +} + +#appbar:not([viewstate="snapped"])[visible] { + transform: none; +} + +#appbar toolbarbutton { + list-style-image: url(chrome://browser/skin/images/appbar-icons.png); + -moz-image-region: rect(0px, 200px, 40px, 160px); /* Gear icon is default. */ +} +#appbar toolbarbutton:hover { + -moz-image-region: rect(40px, 200px, 80px, 160px); +} +#appbar toolbarbutton:active { + -moz-image-region: rect(80px, 200px, 120px, 160px); +} + +/* About flyout pane */ + +#about-flyoutpanel { + width: 350px; + background-image:url('chrome://browser/skin/images/about-footer.png'); + background-repeat: no-repeat; + background-attachment: fixed; + background-position: right bottom; +} + +#about-flyoutpanel label.text-link { + text-decoration: none; + color: #1167bd; +} + +#about-product-label { + font-weight: bold; +} + +#about-version-label { + margin-top: 11pt; +} + +#about-policy-label { + margin-top: 24pt; +} + +/* Application-Specific */ +#download-button { + -moz-image-region: rect(0px, 40px, 40px, 0px) !important; +} +#download-button:hover { + -moz-image-region: rect(40px, 40px, 80px, 0px) !important; +} +#download-button:active { + -moz-image-region: rect(80px, 40px, 120px, 0px) !important; +} + +#plugin-button { + -moz-image-region: rect(0px, 80px, 40px, 40px) !important; +} +#plugin-button:hover { + -moz-image-region: rect(40px, 80px, 80px, 40px) !important; +} +#plugin-button:active { + -moz-image-region: rect(80px, 80px, 120px, 40px) !important; +} + +/* Page-Specific */ +#zoomout-button { + -moz-image-region: rect(0px, 120px, 40px, 80px) !important; +} +#zoomout-button:hover { + -moz-image-region: rect(40px, 120px, 80px, 80px) !important; +} +#zoomout-button:active { + -moz-image-region: rect(80px, 120px, 120px, 80px) !important; +} + +#zoomin-button { + -moz-image-region: rect(0px, 160px, 40px, 120px) !important; +} +#zoomin-button:hover { + -moz-image-region: rect(40px, 160px, 80px, 120px) !important; +} +#zoomin-button:active { + -moz-image-region: rect(80px, 160px, 120px, 120px) !important; +} + +#pin-button { + -moz-image-region: rect(0px, 240px, 40px, 200px) !important; +} +#pin-button:hover { + -moz-image-region: rect(40px, 240px, 80px, 200px) !important; +} +#pin-button:active { + -moz-image-region: rect(80px, 240px, 120px, 200px) !important; +} +#pin-button[checked="true"] { + -moz-image-region: rect(0px, 280px, 40px, 240px) !important; +} + +#star-button { + -moz-image-region: rect(0px, 360px, 40px, 320px) !important; +} +#star-button:hover { + -moz-image-region: rect(40px, 360px, 80px, 320px) !important; +} +#star-button:active, +#star-button[checked="true"] { + -moz-image-region: rect(80px, 360px, 120px, 320px) !important; +} + +/* Start UI (Autocomplete + New Tab Page) ----------------------------------- */ + +#start-container { + display: none; +} + +#start-container[startpage], +#start-container[filtering] { + display: -moz-box; +} + +#start-scrollbox { + overflow: hidden; +} + +/* if autocomplete is set, hide both start pages, + * else hide the autocomplete screen */ +#start-container[filtering] > .start-page, +#start-container:not([filtering]) > #start-autocomplete { + visibility: collapse; +} + +/* if snapped, hide the fullscreen awesome screen, if viewstate is anything + * other than snapped, hide the snapped awesome screen */ +#start[viewstate="snapped"], +#snapped-start:not([viewstate="snapped"]) { + visibility: collapse; +} + +/* Browser Content Ares ----------------------------------------------------- */ + +/* Hide the browser while the start UI is visible */ +#content-viewport[startpage], +#content-viewport[filtering] { + visibility: collapse; +} + +#browsers { + background: white; +} + +/* Panel UI ---------------------------------------------------------------- */ + +#panel-container { + padding: 60px 40px; +} + +#panel-close-button { + background: transparent; + border: 0 none; + -moz-appearance: none; + margin: 0; + -moz-margin-end: 40px; + list-style-image: url(chrome://browser/skin/images/back.png); + -moz-image-region: rect(0 48px 48px 0); + padding: 0; + min-height: 48px; + max-height: 48px; + -moz-box-pack: center; +} + +#panel-close-button[disabled="true"] { + -moz-image-region: rect(0 96px 48px 48px); +} + +#panel-view-switcher { + border: 0 none !important; + color: #000 !important; + background: transparent; + padding: 0; + font-size: @metro_font_xlarge@; + font-weight: 100; + margin: 0; +} + +#panel-items { + padding-top: 20px; + -moz-padding-start: 88px; +} + +/* Preferences Section - Panel UI ------------------------------------------ */ + +#prefs-flyoutpanel { + width: 400px; +} + +/* Lay out each in a single row */ +setting { + min-height: @touch_row@; /* row size */ + -moz-box-align: center; + -moz-box-orient: horizontal; +} + +/* ...except for some exceptions */ +.setting-expanded { + -moz-box-align: start; + -moz-box-orient: vertical; +} + +setting > vbox { + -moz-box-flex: 1; +} + +settings { + margin-top: 32px; +} + +.settings-title { + font-weight: bold; +} + +/* elements that are not in a group get special treatment */ +#prefs-flyoutpanel > setting { + margin-top: 16px; +} +#prefs-flyoutpanel > setting .preferences-title { + font-weight: bold +} + +setting[type="integer"] > .preferences-alignment, +setting[type="string"] > .preferences-alignment { + -moz-box-flex: 3; +} + +setting[type="file"] > .preferences-alignment, +setting[type="directory"] > .preferences-alignment { + -moz-box-align: center; +} + +.options-box { + -moz-margin-start: 28px; /* sized based on the 32px addon image */ +} + +.options-box > setting:last-child { + border-bottom: 0; +} + +.preferences-description { + font-size: @font_small@ !important; + color: grey; +} + +.preferences-description:empty { + display: none; +} + +/* Console Section - Panel UI ---------------------------------------------- */ + +#console-filter-warnings, +#console-filter-messages { + visibility: visible; +} + +@media (max-width: 499px) { + #console-filter-warnings, + #console-filter-messages { + visibility: collapse; + } +} + +.console-error-msg, +.console-msg-text { + white-space: pre-wrap; +} + +/* Identity popup -------------------------------------------------------- */ + +#identity-popup-container { + padding: @padding_normal@; /* core spacing */ + padding-bottom: @padding_xxxnormal@; +} + +/* Popup Icons */ +#identity-popup-icon { + padding: 0; + list-style-image: url("chrome://browser/skin/images/identity-default-hdpi.png"); +} + +#identity-container[mode="verifiedIdentity"] > box > #identity-popup-icon { + list-style-image: url("chrome://browser/skin/images/identity-ev-hdpi.png"); +} + +#identity-container[mode="verifiedDomain"] > box > #identity-popup-icon { + list-style-image: url("chrome://browser/skin/images/identity-ssl-hdpi.png"); +} + +/* Popup Body Text */ +#identity-popup-content-box { + -moz-padding-start: @padding_normal@; /* core spacing */ + font-size: @font_small@ !important; + white-space: pre-wrap; +} + +/* let the text flow into a second row, if needed */ +#identity-popup-runBy-box { + display: block; +} + +#identity-popup-content-box.unknownIdentity > box > #identity-popup-connectedToLabel, +#identity-popup-content-box.unknownIdentity > #identity-popup-runBy-box, +#identity-popup-content-box.unknownIdentity > box > #identity-popup-content-host, +#identity-popup-content-box.verifiedIdentity > box > #identity-popup-connectedToLabel2, +#identity-popup-content-box.verifiedDomain > box > #identity-popup-connectedToLabel2 { + display: none; +} + +#identity-popup-encryption-label, +#identity-popup-content-verifier { + font-size: @font_tiny@ !important; +} + +#identity-popup-content-host, +#identity-popup-content-owner { + font-weight: bold; +} + +#identity-popup-encryption-icon { + list-style-image: url("chrome://browser/skin/images/unlocked-hdpi.png"); +} + +#identity-container[mode="verifiedIdentity"] > hbox > vbox > #identity-popup-encryption-icon , +#identity-container[mode="verifiedDomain"] > hbox > vbox > #identity-popup-encryption-icon { + list-style-image: url("chrome://browser/skin/images/locked-hdpi.png"); +} + +#identity-popup-encryption-box { + -moz-box-orient: vertical; + -moz-box-align: center; + -moz-box-pack: start; +} + +#identity-popup-connected-box { + -moz-box-orient: horizontal; +} + +#identity-popup-content-supplemental { + white-space: normal; +} + +/* Context Menu ------------------------------------------------------------ */ + +#context-commands richlistitem[disabled="true"] { + display: none; +} + +/* Page Actions and Prompt ------------------------------------------------- */ + +.action-buttons, +#pageactions-container { + background: transparent; + border-top: @border_width_tiny@ solid rgb(205,205,205); + padding: 0; + -moz-user-focus: ignore; +} + +.action-buttons, +#pageactions-container { + display: inline-block; +} + +.action-button, +pageaction { + -moz-border-top-colors: white; + -moz-border-right-colors: rgb(175,175,175); + -moz-border-bottom-colors: rgb(175,175,175); + -moz-border-left-colors: white; + border-style: solid; + border-width: @border_width_tiny@ !important; + height: @touch_button_xlarge@; + min-height: @touch_button_xlarge@; + width: 100%; + min-width: @touch_action_minwidth@; /* keep the button from being too narrow */ +} + +.action-button { + -moz-box-align: center; +} + +.action-button[disabled="true"] { + pointer-events: none; + color: #aaa !important; +} + +.action-button[selected="true"] { + background: transparent; +} + +/* Override button styles */ +.action-button { + margin: 0; + -moz-border-image: none !important; + border-radius: 0; + margin: 0; + background: transparent; +} + +.action-button > .button-box { + padding: 0 @padding_small@ @padding_tiny@ @padding_xsmall@ !important; +} + +.action-button > .button-box > .button-icon { + -moz-margin-end: @margin_normal@; +} + +@media (min-width: 500px) { + .action-button, + pageaction { + width: 50%; + } + + .action-button:last-child:nth-child(odd), + pageaction.odd.last-child { + width: 100%; + } +} + +.action-button:not([disabled]):hover:active, +pageaction:not([disabled]):hover:active { + background: url("chrome://browser/skin/images/popup-selected-item-hdpi.png") repeat-x !important; + background-origin: border-box !important; + background-clip: border-box !important; + -moz-border-top-colors: transparent; + -moz-border-left-colors: transparent; +} + +pageaction > hbox > .pageaction-image { + width: 32px; + height: 32px; + -moz-margin-end: @margin_normal@; +} + +pageaction:not([image]) > hbox >.pageaction-image { + width: 0; +} + +.action-button, +.pageaction-title { + font-size: @font_normal@ !important; + color: #414141 !important; +} + +.pageaction-desc { + font-size: @font_tiny@ !important; + color: #414141; +} + +.pageaction-desc[value=""] { + display: none; +} + +#share-title { + font-size: @font_small@; + padding: @padding_small@; +} + +/* Alert Popup -------------------------------------------------------------- */ +#alerts-container { + color: white; + background-color: #5e6166; + border: @border_width_small@ solid #767973; + border-radius: @border_radius_normal@; + box-shadow: black 0 @border_radius_tiny@ @border_radius_tiny@; + padding: @padding_normal@; /* core spacing on top/bottom */ + margin-bottom: @margin_large@; + transition-property: opacity; + transition-duration: 0.5s; + opacity: 0; +} + +#alerts-container.showing { + opacity: 1; +} + +#alerts-title { + font-size: @font_small@ !important; +} + +#alerts-text { + font-size: @font_xsmall@ !important; + white-space: pre; +} + +#alerts-container { + -moz-margin-end: @margin_large@; +} + +/* helperapp (save-as) popup ----------------------------------------------- */ +#helperapp-target { + font-size: @font_small@ !important; +} + +/* Sync Setup ------------------------------------------------------------- */ +.syncsetup-code { + display: block !important; + margin: @metro_spacing_small@; + padding: @metro_spacing_xsmall@ @metro_spacing_snormal@; + background: @field_background_color@; + border: @metro_border_thick@ solid @field_foreground_color@ !important; + color: @field_foreground_color@; + font-size: @metro_font_large@ !important; + letter-spacing: 0.2em; + text-align: left; + width: 250px; + -moz-box-flex: 1; +} + +.syncsetup-label { + color: #fff; +} + +#syncsetup-customserver { + -moz-margin-start: @margin_xnormal@; +} + +#syncsetup-waiting { + padding: 2em 0 0 0; +} + +#syncsetup-waiting-top { + padding: 1em; +} + +/* content scrollbars */ +.scroller { + opacity: 0; + background-color: rgba(0, 0, 0, 0.4) !important; + -moz-border-top-colors: none !important; + -moz-border-bottom-colors: none !important; + -moz-border-right-colors: none !important; + -moz-border-left-colors: none !important; + border-radius: @border_radius_tiny@; + border: @border_width_tiny@ solid rgba(255, 255, 255, 0.4) !important; +} + +.scroller[panning="true"] { + opacity: 1; +} + +.scroller[orient="vertical"] { + min-width: @scroller_thickness@; + width: @scroller_thickness@; + min-height: @scroller_minimum@; +} + +.scroller[orient="horizontal"] { + min-height: @scroller_thickness@; + height: @scroller_thickness@; + min-width: @scroller_minimum@; +} + +/* Text selection handles */ + +#selectionhandle-start, +#selectionhandle-end { + border: 0px solid gray; + padding: 0px; + margin-top: -30px; + margin-left: -18px; +} + +#selectionhandle-start { + list-style-image: url("chrome://browser/skin/images/selection-monocle.png"); +} + +#selectionhandle-end { + list-style-image: url("chrome://browser/skin/images/selection-monocle.png"); +} + +/* Capture picker ------------------------------------------------------------- */ + +#capturepicker-video { + border: @border_width_tiny@ solid white; +} + +#capturepicker-container { + margin: @margin_normal@; +} + +#capturepicker-container.vertical { + height: 330px; +} diff --git a/browser/metro/theme/config.css b/browser/metro/theme/config.css new file mode 100644 index 000000000000..64ccf41bf379 --- /dev/null +++ b/browser/metro/theme/config.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/. */ + +@media (max-width: 499px) { + #editor-container > hbox { + -moz-box-orient: vertical; + } +} + +richlistitem { + -moz-box-align: center; +} + +richlistitem .preferences-title { + pointer-events: none; + min-width: 200px; + -moz-box-flex: 1; + margin-right: 8px; +} + +/* XXX look + sync */ +richlistitem[default="false"] .preferences-title { + font-weight: bold; +} + +richlistitem .preferences-value { + min-width: 200px; + pointer-events: none; + -moz-box-flex: 4; + text-align: end; + color: grey; +} + +/* Editor */ +#editor-row { + padding: 0; + background: #E9E9E9; +} + +#editor { + border-bottom: 1px solid rgb(207,207,207); +} + +#editor > hbox > #editor-name, +#editor > hbox > #editor-cancel, +#editor > hbox > #editor-done { + display: none; +} + +#editor-container > #editor > hbox > #editor-name, +#editor-container > #editor > hbox > #editor-cancel, +#editor-container > #editor > hbox > #editor-done { + display: -moz-box; +} + +#editor-container > #editor > hbox > #editor-reset { + display: none; +} + +#editor-container > hbox > label { + pointer-events: none; + color: black; +} + +#editor + richlistitem { + display: none; +} + +#editor[default="false"] .preferences-title { + font-weight: bold; +} + +#editor-setting setting { + border-color: transparent !important; +} + +#editor-setting[type="string"] .setting-input { + -moz-box-flex: 4; +} + +#editor-setting[type="string"] .setting-input > textbox { + -moz-box-flex: 1; +} + +/* bug 647650: keep 'text-align: right' here instead of using start/end since + * the field should looks like ltr as much as possible + */ +#editor-setting[type="string"] .setting-input > textbox:-moz-locale-dir(rtl) { + direction: ltr; + text-align: right; +} + +#editor-buttons { + margin: 2px; +} + diff --git a/browser/metro/theme/content.css b/browser/metro/theme/content.css new file mode 100644 index 000000000000..3de8529a0f2f --- /dev/null +++ b/browser/metro/theme/content.css @@ -0,0 +1,320 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +%filter substitution +%include defines.inc + +@namespace url("http://www.w3.org/1999/xhtml"); +@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +/* make clicking on links stand out a bit (bug 532206) */ +* > *:not(embed):focus, * > *:focus > font { + outline: 1px solid #8db8d8 !important; +} + +*:-moz-any-link:focus { + outline-offset: -2px; +} + +/* Style the scrollbars */ +html xul|scrollbar { + display: none; +} + +xul|window xul|scrollbar { + display: block; +} + +xul|window xul|scrollbar[orient="vertical"] { + -moz-appearance: none !important; + opacity: 0; + position: relative; + margin-left: -8px; + min-width: 8px; + background-color: transparent !important; + background-image: none !important; + border: 0px solid transparent !important; +} + +xul|window xul|scrollbar[orient="vertical"]:-moz-locale-dir(rtl) { + margin-left: 2px; + margin-right: -10px; +} + +xul|window xul|scrollbar[orient="vertical"] xul|thumb { + max-width: 6px !important; + min-width: 6px !important; +} + +xul|window xul|scrollbar[orient="horizontal"] { + -moz-appearance: none !important; + opacity: 0; + position: relative; + min-height: 8px; + margin-top: -8px; + background-color: transparent !important; + background-image: none !important; + border: 0px solid transparent !important; +} + +xul|window xul|scrollbar[orient="horizontal"] xul|thumb { + max-height: 6px !important; + min-height: 6px !important; +} + +xul|window xul|*[panning="true"] xul|scrollbar { + opacity: 1; +} + +xul|window xul|scrollbox { + overflow-y: scroll; + overflow-x: scroll; +} + +xul|window xul|scrollbarbutton { + min-height: 8px !important; + min-width: 8px !important; + -moz-appearance: none !important; + visibility: hidden; +} + +xul|window xul|scrollbarbutton[sbattr="scrollbar-up-top"], +xul|window xul|scrollbarbutton[sbattr="scrollbar-bottom-top"] { + display: none; +} + +xul|window xul|scrollbar xul|thumb { + background-color: rgba(0, 0, 0, 0.4) !important; + -moz-border-top-colors: none !important; + -moz-border-bottom-colors: none !important; + -moz-border-right-colors: none !important; + -moz-border-left-colors: none !important; + border: 1px solid rgba(255, 255, 255, 0.4) !important; + border-radius: 3px; +} + +select:not([size]):not([multiple]) > xul|scrollbar, +select[size="1"] > xul|scrollbar, +select:not([size]):not([multiple]) xul|scrollbarbutton, +select[size="1"] xul|scrollbarbutton { + display: block; + margin-left: 0; + min-width: 16px; +} + +/* Override inverse OS themes */ +select, +button, +xul|button, +* > input:not([type="image"]) { + /* -moz-appearance: none !important; See bug 598421 for fixing the platform */ + /*border-radius: 3px;*/ +} + +select[size], +select[multiple], +select[size][multiple], +* > input:not([type="image"]) { + + border-style: solid; + border-color: #7d7d7d; + color: #414141; + /*background: white -moz-linear-gradient(top, rgba(115,115,115,0.5) 0, rgba(215,215,215,0.5) 3px, rgba(255,255,255,0.2) 16px);*/ +} + +/* Selects are handled by the form helper, see bug 685197 */ +select option, select optgroup { + pointer-events: none; +} + +input:-moz-placeholder { + color: GrayText; +} + +select:not([size]):not([multiple]), +select[size="0"], +select[size="1"], +* > input[type="button"], +* > input[type="submit"], +* > input[type="reset"], +button { + border-style: solid; + border-color: #7d7d7d; + color: #414141; + /*background: white -moz-linear-gradient(top, rgba(255,255,255,0.2) 0, rgba(215,215,215,0.5) 18px, rgba(115,115,115,0.5) 100%);*/ +} + +input[type="checkbox"] { + background: white -moz-linear-gradient(top, rgba(115,115,115,0.5) 0, rgba(215,215,215,0.5) 2px, rgba(255,255,255,0.2) 6px); +} + +input[type="radio"] { + background: -moz-radial-gradient(6px 6px, cover, rgba(255,255,255,0.2) 3px, rgba(195,195,195,0.5) 5px, rgba(115,115,115,0.5) 100%); +} + +select { + border-width: 1px; + padding: 1px; +} + +select:not([size]):not([multiple]), +select[size="0"], +select[size="1"] { + padding: 0 1px 0 1px; +} + +* > input:not([type="image"]) { + border-width: 1px; + padding: 1px; +} + +textarea { + /* + resize: none; + border-width: 1px; + padding: 2px 1px 2px 1px;*/ +} + +input[type="button"], +input[type="submit"], +input[type="reset"], +button { + border-width: 1px; + padding: 0 7px 0 7px; +} + +input[type="radio"], +input[type="checkbox"] { + max-width: 14px; + max-height: 14px; + border: 1px solid #a7a7a7 !important; + padding: 2px 1px 2px 1px; +} + +select > button { + border-width: 1px !important; + margin: 0px !important; + padding: 0px !important; + border-radius: 0; + color: #414141; + /* + background-size: 100% 90%; + background-color: transparent; + background-image: -moz-radial-gradient(bottom left, #bbbbbb 40%, #f5f5f5) !important; + background-position: -15px center !important; + background-repeat: no-repeat !important; + */ + /* Use to position an svg arrow on