diff options
author | Matt A. Tobin <email@mattatobin.com> | 2022-02-12 13:57:21 -0600 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2022-02-12 13:57:21 -0600 |
commit | ba7d67bb0711c9066c71bd33e55d9a5d2f9b2cbf (patch) | |
tree | a5c0cfad71c17114c78d8a7d1f31112eb53896df /browser/components | |
parent | c054e324210895e7e2c5b3e84437cba43f201ec8 (diff) | |
download | palemoon-gre-ba7d67bb0711c9066c71bd33e55d9a5d2f9b2cbf.tar.gz |
Lay down Pale Moon 30
Diffstat (limited to 'browser/components')
237 files changed, 62255 insertions, 0 deletions
diff --git a/browser/components/BrowserComponents.manifest b/browser/components/BrowserComponents.manifest new file mode 100644 index 000000000..b12ef02e4 --- /dev/null +++ b/browser/components/BrowserComponents.manifest @@ -0,0 +1,46 @@ +# nsAboutRedirector.js +component {8cc51368-6aa0-43e8-b762-bde9b9fd828c} nsAboutRedirector.js +# Each entry here should be coupled with an entry in nsAboutRedirector.js +contract @mozilla.org/network/protocol/about;1?what=certerror {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=downloads {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=feeds {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=home {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=newtab {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=palemoon {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=permissions {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=privatebrowsing {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=rights {8cc51368-6aa0-43e8-b762-bde9b9fd828c} +contract @mozilla.org/network/protocol/about;1?what=sessionrestore {8cc51368-6aa0-43e8-b762-bde9b9fd828c} + +# nsBrowserContentHandler.js +component {5d0ce354-df01-421a-83fb-7ead0990c24e} nsBrowserContentHandler.js +contract @mozilla.org/browser/clh;1 {5d0ce354-df01-421a-83fb-7ead0990c24e} +component {47cd0651-b1be-4a0f-b5c4-10e5a573ef71} nsBrowserContentHandler.js +contract @mozilla.org/browser/final-clh;1 {47cd0651-b1be-4a0f-b5c4-10e5a573ef71} +contract @mozilla.org/uriloader/content-handler;1?type=text/html {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=application/vnd.mozilla.xul+xml {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/svg+xml {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=text/rdf {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=text/xml {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=application/xhtml+xml {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=text/css {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=text/plain {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/gif {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/jpeg {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/jpg {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/png {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/bmp {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/x-icon {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/vnd.microsoft.icon {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=image/webp {5d0ce354-df01-421a-83fb-7ead0990c24e} +contract @mozilla.org/uriloader/content-handler;1?type=application/http-index-format {5d0ce354-df01-421a-83fb-7ead0990c24e} +category command-line-handler m-browser @mozilla.org/browser/clh;1 +category command-line-handler x-default @mozilla.org/browser/final-clh;1 +category command-line-validator b-browser @mozilla.org/browser/clh;1 + +# nsBrowserGlue.js +component {eab9012e-5f74-4cbc-b2b5-a590235513cc} nsBrowserGlue.js +contract @mozilla.org/browser/browserglue;1 {eab9012e-5f74-4cbc-b2b5-a590235513cc} +category app-startup nsBrowserGlue service,@mozilla.org/browser/browserglue;1 +component {d8903bf6-68d5-4e97-bcd1-e4d3012f721a} nsBrowserGlue.js +contract @mozilla.org/content-permission/prompt;1 {d8903bf6-68d5-4e97-bcd1-e4d3012f721a} diff --git a/browser/components/abouthome/aboutHome.css b/browser/components/abouthome/aboutHome.css new file mode 100644 index 000000000..bb730b489 --- /dev/null +++ b/browser/components/abouthome/aboutHome.css @@ -0,0 +1,349 @@ +%if 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +%endif + +html { + font: message-box; + font-size: 100%; + background-color: #eee; + color: #000; + height: 100%; +} + +body { + margin: 0; + display: -moz-box; + -moz-box-orient: vertical; + width: 100%; + height: 100%; +} + +input, +button { + font-size: inherit; + font-family: inherit; +} + +a { + color: -moz-nativehyperlinktext; + text-decoration: none; +} + +.spacer { + -moz-box-flex: 1; +} + +#topSection { + text-align: center; +} + +#brandLogo { + height: 192px; + width: 192px; + margin: 22px auto 31px; + background-image: url("chrome://branding/content/about-logo.png"); + background-size: 192px auto; + background-position: center center; + background-repeat: no-repeat; +} + +/* SEARCH */ +#searchContainer { + display: -moz-box; + position: relative; + -moz-box-pack: center; + margin: 10px 0 15px; +} + +#searchForm { + width: 470px; + display: -moz-box; + position: relative; + height: 36px; /* 32 px logo + 2*1px pad + 2*1px border */ + -moz-box-flex: 1; + max-width: 600px; +} + +#searchEngineLogo { + border: 1px transparent; + padding: 4px; + margin: 0; + width: 28px; + height: 28px; + position: absolute; +} + +#searchText { + -moz-box-flex: 1; + padding-top: 6px; + padding-bottom: 6px; + padding-inline-start: 38px; /* room for logo */ + padding-inline-end: 8px; + background: rgba(255, 255, 255, 0.9) padding-box; + border: 1px solid; + border-color: rgba(37, 46, 65, 0.15) rgba(37, 46, 65, 0.17) rgba(37, 46, 65, 0.2); + box-shadow: 0 1px 0 rgba(37, 46, 65, 0.02) inset, + 0 0 2px rgba(37, 46, 65, 0.1) inset, + 0 1px 0 rgba(255, 255, 255, 0.2); + border-radius: 2.5px 0 0 2.5px; +} + +#searchText:-moz-dir(rtl) { + border-radius: 0 2.5px 2.5px 0; +} + +#searchText:focus, +#searchText[autofocus] { + border-color: rgba(92, 133, 214, 0.6) rgba(78, 114, 188, 0.6) rgba(41, 82, 163, 0.6); +} + +#searchText::placeholder { + font-style: italic; + opacity: 0.3; +} + +#searchSubmit { + margin-inline-start: -1px; + background: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1)) padding-box; + padding: 0 9px; + border: 1px solid; + border-color: rgba(37, 46, 65, 0.2) rgba(37, 46, 65, 0.2) rgba(37, 46, 65, 0.2); + border-inline-start: 1px solid transparent; + border-radius: 0 2.5px 2.5px 0; + box-shadow: 0 0 2px rgba(255, 255, 255, 0.5) inset, + 0 1px 0 rgba(255, 255, 255, 0.2); + cursor: pointer; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; +} + +#searchSubmit:-moz-dir(rtl) { + border-radius: 2.5px 0 0 2.5px; +} + +#searchText:focus + #searchSubmit, +#searchText + #searchSubmit:hover, +#searchText[autofocus] + #searchSubmit { + border-color: #8da1c8 #768bb5 #6579a2; + color: white; +} + +#searchText:focus + #searchSubmit, +#searchText[autofocus] + #searchSubmit { + background-image: linear-gradient(#85a8e0, #3d75cf); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2) inset, + 0 0 0 1px rgba(255, 255, 255, 0.1) inset, + 0 1px 0 rgba(23, 46, 67, 0.03); +} + +#searchText + #searchSubmit:hover { + background-image: linear-gradient(#85a8e0, #3d75cf); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2) inset, + 0 0 0 1px rgba(255, 255, 255, 0.1) inset, + 0 1px 0 rgba(23, 42, 79, 0.03), + 0 0 4px rgba(0, 34, 102, 0.2);} + +#searchText + #searchSubmit:hover:active { + box-shadow: 0 1px 1px rgba(3, 11, 27, 0.1) inset, + 0 0 1px rgba(3, 11, 27, 0.2) inset; + transition-duration: 0ms; +} + +/* LAUNCHER */ +#launcher { + display: -moz-box; + -moz-box-align: center; + -moz-box-pack: center; + width: 100%; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.03); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02) inset, + 0 -1px 0 rgba(255, 255, 255, 0.25); +} + +#launcher:not([session]), +body[narrow] #launcher[session] { + display: block; /* display separator and restore button on separate lines */ + text-align: center; + white-space: nowrap; /* prevent navigational buttons from wrapping */ +} + +.launchButton { + display: -moz-box; + -moz-box-orient: vertical; + margin: 16px 1px; + padding: 14px 6px; + min-width: 88px; + max-width: 176px; + max-height: 85px; + vertical-align: top; + white-space: normal; + background: transparent padding-box; + border: 1px solid transparent; + border-radius: 2.5px; + color: #525c66; + font-size: 75%; + cursor: pointer; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; +} + +body[narrow] #launcher[session] > .launchButton { + margin: 4px 1px; +} + +.launchButton:hover { + background-color: hsla(211,79%,6%,.03); + border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2); +} + +.launchButton:hover:active { + background-image: linear-gradient(hsla(211,79%,6%,.02), hsla(211,79%,6%,.05)); + border-color: hsla(210,54%,20%,.2) hsla(210,54%,20%,.23) hsla(210,54%,20%,.25); + box-shadow: 0 1px 1px hsla(211,79%,6%,.05) inset, + 0 0 1px hsla(211,79%,6%,.1) inset; + transition-duration: 0ms; +} + +.launchButton[hidden], +#launcher:not([session]) > #restorePreviousSessionSeparator, +#launcher:not([session]) > #restorePreviousSession { + display: none; +} + +#restorePreviousSessionSeparator { + width: 3px; + height: 116px; + margin: 0 10px; + background-image: linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)), + linear-gradient(hsla(211,79%,6%,0), hsla(211,79%,6%,.2), hsla(211,79%,6%,0)), + linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)); + background-position: left top, center, right bottom; + background-size: 1px auto; + background-repeat: no-repeat; +} + +body[narrow] #restorePreviousSessionSeparator { + margin: 0 auto; + width: 512px; + height: 3px; + background-image: linear-gradient(to right, hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)), + linear-gradient(to right, hsla(211,79%,6%,0), hsla(211,79%,6%,.2), hsla(211,79%,6%,0)), + linear-gradient(to right, hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)); + background-size: auto 1px; +} + +#restorePreviousSession { + max-width: none; + font-size: 90%; +} + +body[narrow] #restorePreviousSession { + font-size: 80%; +} + +.launchButton::before { + display: block; + width: 32px; + height: 32px; + margin: 0 auto 6px; + line-height: 0; /* remove extra vertical space due to non-zero font-size */ +} + +#downloads::before { + content: url("chrome://browser/content/abouthome/downloads.png"); +} + +#bookmarks::before { + content: url("chrome://browser/content/abouthome/bookmarks.png"); +} + +#history::before { + content: url("chrome://browser/content/abouthome/history.png"); +} + +#addons::before { + content: url("chrome://browser/content/abouthome/addons.png"); +} + +%ifdef MOZ_SERVICES_SYNC +#sync::before { + content: url("chrome://browser/content/abouthome/sync.png"); +} +%endif + +#settings::before { + content: url("chrome://browser/content/abouthome/settings.png"); +} + +#restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore-large.png"); + height: 48px; + width: 48px; + display: inline-block; /* display on same line as text label */ + vertical-align: middle; + margin-bottom: 0; + margin-inline-end: 8px; +} + +#restorePreviousSession:-moz-dir(rtl)::before { + transform: scaleX(-1); +} + +body[narrow] #restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore.png"); + height: 32px; + width: 32px; +} + +/* [HiDPI] + * At resolutions above 1dppx, prefer downscaling the 2x Retina graphics + * rather than upscaling the original-size ones (bug 818940). + */ +@media not all and (max-resolution: 1dppx) { + #brandLogo { + background-image: url("chrome://branding/content/about-logo@2x.png"); + } + + .launchButton::before { + transform: scale(.5); + transform-origin: 0 0; + } + + #downloads::before { + content: url("chrome://browser/content/abouthome/downloads@2x.png"); + } + + #bookmarks::before { + content: url("chrome://browser/content/abouthome/bookmarks@2x.png"); + } + + #history::before { + content: url("chrome://browser/content/abouthome/history@2x.png"); + } + + #addons::before { + content: url("chrome://browser/content/abouthome/addons@2x.png"); + } + +%ifdef MOZ_SERVICES_SYNC + #sync::before { + content: url("chrome://browser/content/abouthome/sync@2x.png"); + } +%endif + + #settings::before { + content: url("chrome://browser/content/abouthome/settings@2x.png"); + } + + #restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore-large@2x.png"); + } + + body[narrow] #restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore@2x.png"); + } +} + diff --git a/browser/components/abouthome/aboutHome.js b/browser/components/abouthome/aboutHome.js new file mode 100644 index 000000000..686644673 --- /dev/null +++ b/browser/components/abouthome/aboutHome.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include ../shared/searchenginelogos.js + +// This global tracks if the page has been set up before, to prevent double inits +var gInitialized = false; +var gObserver = new MutationObserver(function (mutations) { + for (let mutation of mutations) { + if (mutation.attributeName == "searchEngineURL") { + setupSearchEngine(); + if (!gInitialized) { + gInitialized = true; + } + return; + } + } +}); + +window.addEventListener("pageshow", function () { + // Delay search engine setup, cause browser.js::BrowserOnAboutPageLoad runs + // later and may use asynchronous getters. + window.gObserver.observe(document.documentElement, { attributes: true }); + fitToWidth(); + window.addEventListener("resize", fitToWidth); +}); + +window.addEventListener("pagehide", function() { + window.gObserver.disconnect(); + window.removeEventListener("resize", fitToWidth); +}); + +function onSearchSubmit(aEvent) +{ + let searchTerms = document.getElementById("searchText").value; + let searchURL = document.documentElement.getAttribute("searchEngineURL"); + + if (searchURL && searchTerms.length > 0) { + // Send an event that a search was performed. This was originally + // added so Firefox Health Report could record that a search from + // about:home had occurred. + let engineName = document.documentElement.getAttribute("searchEngineName"); + let event = new CustomEvent("AboutHomeSearchEvent", {detail: engineName}); + document.dispatchEvent(event); + + const SEARCH_TOKEN = "_searchTerms_"; + let searchPostData = document.documentElement.getAttribute("searchEnginePostData"); + if (searchPostData) { + // Check if a post form already exists. If so, remove it. + const POST_FORM_NAME = "searchFormPost"; + let form = document.forms[POST_FORM_NAME]; + if (form) { + form.parentNode.removeChild(form); + } + + // Create a new post form. + form = document.body.appendChild(document.createElement("form")); + form.setAttribute("name", POST_FORM_NAME); + // Set the URL to submit the form to. + form.setAttribute("action", searchURL.replace(SEARCH_TOKEN, searchTerms)); + form.setAttribute("method", "post"); + + // Create new <input type=hidden> elements for search param. + searchPostData = searchPostData.split("&"); + for (let postVar of searchPostData) { + let [name, value] = postVar.split("="); + if (value == SEARCH_TOKEN) { + value = searchTerms; + } + let input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", name); + input.setAttribute("value", value); + form.appendChild(input); + } + // Submit the form. + form.submit(); + } else { + searchURL = searchURL.replace(SEARCH_TOKEN, encodeURIComponent(searchTerms)); + window.location.href = searchURL; + } + } + + aEvent.preventDefault(); +} + + +function setupSearchEngine() +{ + // The "autofocus" attribute doesn't focus the form element + // immediately when the element is first drawn, so the + // attribute is also used for styling when the page first loads. + let searchText = document.getElementById("searchText"); + searchText.addEventListener("blur", function searchText_onBlur() { + searchText.removeEventListener("blur", searchText_onBlur); + searchText.removeAttribute("autofocus"); + }); + + let searchEngineName = document.documentElement.getAttribute("searchEngineName"); + let searchEngineInfo = SEARCH_ENGINES[searchEngineName]; + let logoElt = document.getElementById("searchEngineLogo"); + + // Add search engine logo. + if (searchEngineInfo && searchEngineInfo.image) { + logoElt.parentNode.hidden = false; + logoElt.src = searchEngineInfo.image; + logoElt.alt = searchEngineName; + searchText.placeholder = ""; + } else { + logoElt.parentNode.hidden = false; + logoElt.src = SEARCH_ENGINES['generic'].image; + searchText.placeholder = searchEngineName; + } + +} + +function fitToWidth() { + if (window.scrollMaxX) { + document.body.setAttribute("narrow", "true"); + } else if (document.body.hasAttribute("narrow")) { + document.body.removeAttribute("narrow"); + fitToWidth(); + } +} diff --git a/browser/components/abouthome/aboutHome.xhtml b/browser/components/abouthome/aboutHome.xhtml new file mode 100644 index 000000000..d72ec492e --- /dev/null +++ b/browser/components/abouthome/aboutHome.xhtml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> + %aboutHomeDTD; + <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > + %browserDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&abouthome.pageTitle;</title> + + <link rel="icon" type="image/png" id="favicon" + href="chrome://branding/content/icon32.png"/> + <link rel="stylesheet" type="text/css" media="all" + href="chrome://browser/content/abouthome/aboutHome.css"/> + + <script type="text/javascript;version=1.8" + src="chrome://browser/content/abouthome/aboutHome.js"/> + </head> + + <body dir="&locale.dir;"> + <div class="spacer"/> + <div id="topSection"> + <div id="brandLogo"></div> + + <div id="searchContainer"> + <form name="searchForm" id="searchForm" onsubmit="onSearchSubmit(event)"> + <div id="searchLogoContainer"><img id="searchEngineLogo"/></div> + <input type="text" name="q" value="" id="searchText" maxlength="256" + autofocus="autofocus"/> + <input id="searchSubmit" type="submit" value="&abouthome.searchEngineButton.label;"/> + </form> + </div> + </div> + <div class="spacer"/> + + <div id="launcher"> + <button class="launchButton" id="downloads">&abouthome.downloadsButton.label;</button> + <button class="launchButton" id="bookmarks">&abouthome.bookmarksButton.label;</button> + <button class="launchButton" id="history">&abouthome.historyButton.label;</button> + <button class="launchButton" id="addons">&abouthome.addonsButton.label;</button> +#ifdef MOZ_SERVICES_SYNC + <button class="launchButton" id="sync">&abouthome.syncButton.label;</button> +#endif + <button class="launchButton" id="settings">&abouthome.settingsButton.label;</button> + <div id="restorePreviousSessionSeparator"/> + <button class="launchButton" id="restorePreviousSession">&historyRestoreLastSession.label;</button> + </div> + </body> +</html> diff --git a/browser/components/abouthome/addons.png b/browser/components/abouthome/addons.png Binary files differnew file mode 100644 index 000000000..41519ce49 --- /dev/null +++ b/browser/components/abouthome/addons.png diff --git a/browser/components/abouthome/addons@2x.png b/browser/components/abouthome/addons@2x.png Binary files differnew file mode 100644 index 000000000..d4d04ee8c --- /dev/null +++ b/browser/components/abouthome/addons@2x.png diff --git a/browser/components/abouthome/bookmarks.png b/browser/components/abouthome/bookmarks.png Binary files differnew file mode 100644 index 000000000..5c7e194a6 --- /dev/null +++ b/browser/components/abouthome/bookmarks.png diff --git a/browser/components/abouthome/bookmarks@2x.png b/browser/components/abouthome/bookmarks@2x.png Binary files differnew file mode 100644 index 000000000..7ede00744 --- /dev/null +++ b/browser/components/abouthome/bookmarks@2x.png diff --git a/browser/components/abouthome/downloads.png b/browser/components/abouthome/downloads.png Binary files differnew file mode 100644 index 000000000..3d4d10e7a --- /dev/null +++ b/browser/components/abouthome/downloads.png diff --git a/browser/components/abouthome/downloads@2x.png b/browser/components/abouthome/downloads@2x.png Binary files differnew file mode 100644 index 000000000..d384a22c6 --- /dev/null +++ b/browser/components/abouthome/downloads@2x.png diff --git a/browser/components/abouthome/history.png b/browser/components/abouthome/history.png Binary files differnew file mode 100644 index 000000000..ae742b1aa --- /dev/null +++ b/browser/components/abouthome/history.png diff --git a/browser/components/abouthome/history@2x.png b/browser/components/abouthome/history@2x.png Binary files differnew file mode 100644 index 000000000..696902e7c --- /dev/null +++ b/browser/components/abouthome/history@2x.png diff --git a/browser/components/abouthome/jar.mn b/browser/components/abouthome/jar.mn new file mode 100644 index 000000000..d9499347c --- /dev/null +++ b/browser/components/abouthome/jar.mn @@ -0,0 +1,28 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: +* content/browser/abouthome/aboutHome.xhtml +* content/browser/abouthome/aboutHome.js +* content/browser/abouthome/aboutHome.css + content/browser/abouthome/downloads.png + content/browser/abouthome/bookmarks.png + content/browser/abouthome/history.png + content/browser/abouthome/addons.png +#ifdef MOZ_SERVICES_SYNC + content/browser/abouthome/sync.png +#endif + content/browser/abouthome/settings.png + content/browser/abouthome/restore.png + content/browser/abouthome/restore-large.png + content/browser/abouthome/downloads@2x.png + content/browser/abouthome/bookmarks@2x.png + content/browser/abouthome/history@2x.png + content/browser/abouthome/addons@2x.png +#ifdef MOZ_SERVICES_SYNC + content/browser/abouthome/sync@2x.png +#endif + content/browser/abouthome/settings@2x.png + content/browser/abouthome/restore@2x.png + content/browser/abouthome/restore-large@2x.png
\ No newline at end of file diff --git a/browser/components/abouthome/moz.build b/browser/components/abouthome/moz.build new file mode 100644 index 000000000..8267a660d --- /dev/null +++ b/browser/components/abouthome/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] + diff --git a/browser/components/abouthome/restore-large.png b/browser/components/abouthome/restore-large.png Binary files differnew file mode 100644 index 000000000..ef593e6e1 --- /dev/null +++ b/browser/components/abouthome/restore-large.png diff --git a/browser/components/abouthome/restore-large@2x.png b/browser/components/abouthome/restore-large@2x.png Binary files differnew file mode 100644 index 000000000..d5c71d0b0 --- /dev/null +++ b/browser/components/abouthome/restore-large@2x.png diff --git a/browser/components/abouthome/restore.png b/browser/components/abouthome/restore.png Binary files differnew file mode 100644 index 000000000..5c3d6f437 --- /dev/null +++ b/browser/components/abouthome/restore.png diff --git a/browser/components/abouthome/restore@2x.png b/browser/components/abouthome/restore@2x.png Binary files differnew file mode 100644 index 000000000..5acb63052 --- /dev/null +++ b/browser/components/abouthome/restore@2x.png diff --git a/browser/components/abouthome/settings.png b/browser/components/abouthome/settings.png Binary files differnew file mode 100644 index 000000000..4b0c30990 --- /dev/null +++ b/browser/components/abouthome/settings.png diff --git a/browser/components/abouthome/settings@2x.png b/browser/components/abouthome/settings@2x.png Binary files differnew file mode 100644 index 000000000..c77cb9a92 --- /dev/null +++ b/browser/components/abouthome/settings@2x.png diff --git a/browser/components/abouthome/sync.png b/browser/components/abouthome/sync.png Binary files differnew file mode 100644 index 000000000..11e40cc93 --- /dev/null +++ b/browser/components/abouthome/sync.png diff --git a/browser/components/abouthome/sync@2x.png b/browser/components/abouthome/sync@2x.png Binary files differnew file mode 100644 index 000000000..6354f5bf9 --- /dev/null +++ b/browser/components/abouthome/sync@2x.png diff --git a/browser/components/build/Makefile.in b/browser/components/build/Makefile.in new file mode 100644 index 000000000..2387227ab --- /dev/null +++ b/browser/components/build/Makefile.in @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +include $(topsrcdir)/config/rules.mk + +# Ensure that we don't embed a manifest referencing the CRT. +EMBED_MANIFEST_AT = diff --git a/browser/components/build/moz.build b/browser/components/build/moz.build new file mode 100644 index 000000000..af0abde29 --- /dev/null +++ b/browser/components/build/moz.build @@ -0,0 +1,34 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +EXPORTS += ['nsBrowserCompsCID.h'] + +SOURCES += ['nsModule.cpp'] + +XPCOMBinaryComponent('browsercomps') + +LOCAL_INCLUDES += [ + '../dirprovider', + '../feeds', + '../shell', +] + +if CONFIG['OS_ARCH'] == 'WINNT': + OS_LIBS += [ + 'netapi32', + 'ole32', + 'shell32', + 'shlwapi', + 'version', + ] + DELAYLOAD_DLLS += [ + 'netapi32.dll', + 'advapi32.dll', + 'ole32.dll', + ] + +# GTK: Need to link with glib for GNOME shell service +if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3'): + OS_LIBS += CONFIG['TK_LIBS'] diff --git a/browser/components/build/nsBrowserCompsCID.h b/browser/components/build/nsBrowserCompsCID.h new file mode 100644 index 000000000..bbaa9ab8a --- /dev/null +++ b/browser/components/build/nsBrowserCompsCID.h @@ -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/. */ + +///////////////////////////////////////////////////////////////////////////// + +#define NS_SHELLSERVICE_CID \ +{ 0x63c7b9f4, 0xcc8, 0x43f8, { 0xb6, 0x66, 0xa, 0x66, 0x16, 0x55, 0xcb, 0x73 } } + +#define NS_SHELLSERVICE_CONTRACTID \ + "@mozilla.org/browser/shell-service;1" + +#define NS_RDF_FORWARDPROXY_INFER_DATASOURCE_CID \ +{ 0x7a024bcf, 0xedd5, 0x4d9a, { 0x86, 0x14, 0xd4, 0x4b, 0xe1, 0xda, 0xda, 0xd3 } } + +#define NS_FEEDSNIFFER_CID \ +{ 0x6893e69, 0x71d8, 0x4b23, { 0x81, 0xeb, 0x80, 0x31, 0x4d, 0xaf, 0x3e, 0x66 } } + +#define NS_FEEDSNIFFER_CONTRACTID \ + "@mozilla.org/browser/feeds/sniffer;1" + +#define NS_ABOUTFEEDS_CID \ +{ 0x12ff56ec, 0x58be, 0x402c, { 0xb0, 0x57, 0x1, 0xf9, 0x61, 0xde, 0x96, 0x9b } } + +// 136e2c4d-c5a4-477c-b131-d93d7d704f64 +#define NS_PRIVATE_BROWSING_SERVICE_WRAPPER_CID \ +{ 0x136e2c4d, 0xc5a4, 0x477c, { 0xb1, 0x31, 0xd9, 0x3d, 0x7d, 0x70, 0x4f, 0x64 } } + +// {6DEB193C-F87D-4078-BC78-5E64655B4D62} +#define NS_BROWSERDIRECTORYPROVIDER_CID \ +{ 0x6deb193c, 0xf87d, 0x4078, { 0xbc, 0x78, 0x5e, 0x64, 0x65, 0x5b, 0x4d, 0x62 } } diff --git a/browser/components/build/nsModule.cpp b/browser/components/build/nsModule.cpp new file mode 100644 index 000000000..23aa2843c --- /dev/null +++ b/browser/components/build/nsModule.cpp @@ -0,0 +1,78 @@ +/* -*- Mode: C++; 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/. */ + +#include "mozilla/ModuleUtils.h" + +#include "nsBrowserCompsCID.h" +#include "DirectoryProvider.h" + +#if defined(XP_WIN) +#include "nsWindowsShellService.h" +#elif defined(MOZ_WIDGET_GTK) +#include "nsGNOMEShellService.h" +#endif + +#include "rdf.h" +#include "nsFeedSniffer.h" + +#include "nsNetCID.h" + +using namespace mozilla::browser; + +///////////////////////////////////////////////////////////////////////////// + +NS_GENERIC_FACTORY_CONSTRUCTOR(DirectoryProvider) +#if defined(XP_WIN) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsWindowsShellService) +#elif defined(MOZ_WIDGET_GTK) +NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsGNOMEShellService, Init) +#endif + +NS_GENERIC_FACTORY_CONSTRUCTOR(nsFeedSniffer) + +NS_DEFINE_NAMED_CID(NS_BROWSERDIRECTORYPROVIDER_CID); +#if defined(XP_WIN) +NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID); +#elif defined(MOZ_WIDGET_GTK) +NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID); +#endif +NS_DEFINE_NAMED_CID(NS_FEEDSNIFFER_CID); + +static const mozilla::Module::CIDEntry kBrowserCIDs[] = { + { &kNS_BROWSERDIRECTORYPROVIDER_CID, false, nullptr, DirectoryProviderConstructor }, +#if defined(XP_WIN) + { &kNS_SHELLSERVICE_CID, false, nullptr, nsWindowsShellServiceConstructor }, +#elif defined(MOZ_WIDGET_GTK) + { &kNS_SHELLSERVICE_CID, false, nullptr, nsGNOMEShellServiceConstructor }, +#endif + { &kNS_FEEDSNIFFER_CID, false, nullptr, nsFeedSnifferConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kBrowserContracts[] = { + { NS_BROWSERDIRECTORYPROVIDER_CONTRACTID, &kNS_BROWSERDIRECTORYPROVIDER_CID }, +#if defined(XP_WIN) + { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID }, +#elif defined(MOZ_WIDGET_GTK) + { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID }, +#endif + { NS_FEEDSNIFFER_CONTRACTID, &kNS_FEEDSNIFFER_CID }, + { nullptr } +}; + +static const mozilla::Module::CategoryEntry kBrowserCategories[] = { + { XPCOM_DIRECTORY_PROVIDER_CATEGORY, "browser-directory-provider", NS_BROWSERDIRECTORYPROVIDER_CONTRACTID }, + { NS_CONTENT_SNIFFER_CATEGORY, "Feed Sniffer", NS_FEEDSNIFFER_CONTRACTID }, + { nullptr } +}; + +static const mozilla::Module kBrowserModule = { + mozilla::Module::kVersion, + kBrowserCIDs, + kBrowserContracts, + kBrowserCategories +}; + +NSMODULE_DEFN(nsBrowserCompsModule) = &kBrowserModule; diff --git a/browser/components/certerror/content/aboutCertError.css b/browser/components/certerror/content/aboutCertError.css new file mode 100644 index 000000000..059d8123e --- /dev/null +++ b/browser/components/certerror/content/aboutCertError.css @@ -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/. */ + +/* Logical CSS rules belong here, but presentation & theming rules + should live in the CSS of the appropriate theme */ + +#technicalContentText { + overflow: auto; + white-space: pre-wrap; +} + +.expander[hidden], +.expander[hidden] + *, +.expander[collapsed] + * { + display: none; +} diff --git a/browser/components/certerror/content/aboutCertError.xhtml b/browser/components/certerror/content/aboutCertError.xhtml new file mode 100644 index 000000000..c8a7e44f0 --- /dev/null +++ b/browser/components/certerror/content/aboutCertError.xhtml @@ -0,0 +1,247 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % certerrorDTD + SYSTEM "chrome://browser/locale/aboutCertError.dtd"> + %certerrorDTD; +]> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&certerror.pagetitle;</title> + <link rel="stylesheet" href="chrome://browser/skin/aboutCertError.css" type="text/css" media="all" /> + <link rel="stylesheet" href="chrome://browser/content/certerror/aboutCertError.css" type="text/css" media="all" /> + <!-- This page currently uses the same favicon as neterror.xhtml. + If the location of the favicon is changed for both pages, the + FAVICON_ERRORPAGE_URL symbol in toolkit/components/places/src/nsFaviconService.h + should be updated. If this page starts using a different favicon + than neterror.xhtml nsFaviconService->SetAndLoadFaviconForPage + should be updated to ignore this one as well. --> + <link rel="icon" type="image/png" id="favicon" href="chrome://global/skin/icons/warning-16.png"/> + + <script type="application/javascript"><![CDATA[ + // Error url MUST be formatted like this: + // about:certerror?e=error&u=url&d=desc + + // Note that this file uses document.documentURI to get + // the URL (with the format from above). This is because + // document.location.href gets the current URI off the docshell, + // which is the URL displayed in the location bar, i.e. + // the URI that the user attempted to load. + + function getCSSClass() + { + var url = document.documentURI; + var matches = url.match(/s\=([^&]+)\&/); + // s is optional, if no match just return nothing + if (!matches || matches.length < 2) + return ""; + + // parenthetical match is the second entry + return decodeURIComponent(matches[1]); + } + + function getDescription() + { + var url = document.documentURI; + var desc = url.search(/d\=/); + + // desc == -1 if not found; if so, return an empty string + // instead of what would turn out to be portions of the URI + if (desc == -1) + return ""; + + return decodeURIComponent(url.slice(desc + 2)); + } + + function initPage() + { + // Replace the "#1" string in the intro with the hostname. Trickier + // than it might seem since we want to preserve the <b> tags, but + // not allow for any injection by just using innerHTML. Instead, + // just find the right target text node. + var intro = document.getElementById('introContentP1'); + function replaceWithHost(node) { + if (node.textContent == "#1") + node.textContent = location.host; + else + for(var i = 0; i < node.childNodes.length; i++) + replaceWithHost(node.childNodes[i]); + }; + replaceWithHost(intro); + + if (getCSSClass() == "expertBadCert") { + toggle('technicalContent'); + toggle('expertContent'); + } + + // Disallow overrides if this is a Strict-Transport-Security + // host and the cert is bad (STS Spec section 7.3) or if the + // certerror is in a frame (bug 633691). + if (getCSSClass() == "badStsCert" || window != top) + document.getElementById("expertContent").setAttribute("hidden", "true"); + + var tech = document.getElementById("technicalContentText"); + if (tech) + tech.textContent = getDescription(); + + addDomainErrorLink(); + } + + /* In the case of SSL error pages about domain mismatch, see if + we can hyperlink the user to the correct site. We don't want + to do this generically since it allows MitM attacks to redirect + users to a site under attacker control, but in certain cases + it is safe (and helpful!) to do so. Bug 402210 + */ + function addDomainErrorLink() { + // Rather than textContent, we need to treat description as HTML + var sd = document.getElementById("technicalContentText"); + if (sd) { + var desc = getDescription(); + + // sanitize description text - see bug 441169 + + // First, find the index of the <a> tag we care about, being careful not to + // use an over-greedy regex + var re = /<a id="cert_domain_link" title="([^"]+)">/; + var result = re.exec(desc); + if(!result) + return; + + // Remove sd's existing children + sd.textContent = ""; + + // Everything up to the link should be text content + sd.appendChild(document.createTextNode(desc.slice(0, result.index))); + + // Now create the link itself + var anchorEl = document.createElement("a"); + anchorEl.setAttribute("id", "cert_domain_link"); + anchorEl.setAttribute("title", result[1]); + anchorEl.appendChild(document.createTextNode(result[1])); + sd.appendChild(anchorEl); + + // Finally, append text for anything after the closing </a> + sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length))); + } + + var link = document.getElementById('cert_domain_link'); + if (!link) + return; + + var okHost = link.getAttribute("title"); + var thisHost = document.location.hostname; + var proto = document.location.protocol; + + // If okHost is a wildcard domain ("*.example.com") let's + // use "www" instead. "*.example.com" isn't going to + // get anyone anywhere useful. bug 432491 + okHost = okHost.replace(/^\*\./, "www."); + + /* case #1: + * example.com uses an invalid security certificate. + * + * The certificate is only valid for www.example.com + * + * Make sure to include the "." ahead of thisHost so that + * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com" + * + * We'd normally just use a RegExp here except that we lack a + * library function to escape them properly (bug 248062), and + * domain names are famous for having '.' characters in them, + * which would allow spurious and possibly hostile matches. + */ + if (endsWith(okHost, "." + thisHost)) + link.href = proto + okHost; + + /* case #2: + * browser.garage.maemo.org uses an invalid security certificate. + * + * The certificate is only valid for garage.maemo.org + */ + if (endsWith(thisHost, "." + okHost)) + link.href = proto + okHost; + + // If we set a link, meaning there's something helpful for + // the user here, expand the section by default + if (link.href && getCSSClass() != "expertBadCert") + toggle("technicalContent"); + } + + function endsWith(haystack, needle) { + return haystack.slice(-needle.length) == needle; + } + + function toggle(id) { + var el = document.getElementById(id); + if (el.getAttribute("collapsed")) + el.removeAttribute("collapsed"); + else + el.setAttribute("collapsed", true); + } + ]]></script> + </head> + + <body dir="&locale.dir;"> + + <!-- PAGE CONTAINER (for styling purposes only) --> + <div id="errorPageContainer"> + + <!-- Error Title --> + <div id="errorTitle"> + <h1 id="errorTitleText">&certerror.longpagetitle;</h1> + </div> + + <!-- LONG CONTENT (the section most likely to require scrolling) --> + <div id="errorLongContent"> + <div id="introContent"> + <p id="introContentP1">&certerror.introPara1;</p> + <p>&certerror.introPara2;</p> + </div> + + <div id="whatShouldIDoContent"> + <h2>&certerror.whatShouldIDo.heading;</h2> + <div id="whatShouldIDoContentText"> + <p>&certerror.whatShouldIDo.content;</p> + <button id='getMeOutOfHereButton'>&certerror.getMeOutOfHere.label;</button> + </div> + </div> + + <!-- The following sections can be unhidden by default by setting the + "browser.xul.error_pages.expert_bad_cert" pref to true --> + <h2 id="technicalContent" class="expander" collapsed="true"> + <button onclick="toggle('technicalContent');">&certerror.technical.heading;</button> + </h2> + <p id="technicalContentText"/> + + <h2 id="expertContent" class="expander" collapsed="true"> + <button onclick="toggle('expertContent');">&certerror.expert.heading;</button> + </h2> + <div> + <p>&certerror.expert.content;</p> + <p>&certerror.expert.contentPara2;</p> + <button id='exceptionDialogButton'>&certerror.addException.label;</button> + </div> + </div> + </div> + + <!-- + - Note: It is important to run the script this way, instead of using + - an onload handler. This is because error pages are loaded as + - LOAD_BACKGROUND, which means that onload handlers will not be executed. + --> + <script type="application/javascript">initPage();</script> + + </body> +</html> diff --git a/browser/components/certerror/jar.mn b/browser/components/certerror/jar.mn new file mode 100644 index 000000000..08e071027 --- /dev/null +++ b/browser/components/certerror/jar.mn @@ -0,0 +1,7 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/certerror/aboutCertError.xhtml (content/aboutCertError.xhtml) + content/browser/certerror/aboutCertError.css (content/aboutCertError.css) diff --git a/browser/components/certerror/moz.build b/browser/components/certerror/moz.build new file mode 100644 index 000000000..ecb79e730 --- /dev/null +++ b/browser/components/certerror/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file diff --git a/browser/components/dirprovider/DirectoryProvider.cpp b/browser/components/dirprovider/DirectoryProvider.cpp new file mode 100644 index 000000000..85728b351 --- /dev/null +++ b/browser/components/dirprovider/DirectoryProvider.cpp @@ -0,0 +1,268 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "nsIDirectoryService.h" +#include "DirectoryProvider.h" + +#include "nsIFile.h" +#include "nsISimpleEnumerator.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" + +#include "nsArrayEnumerator.h" +#include "nsEnumeratorUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceDefs.h" +#include "nsCategoryManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMArray.h" +#include "nsDirectoryServiceUtils.h" +#include "mozilla/ModuleUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsStringAPI.h" +#include "nsXULAppAPI.h" +#include "nsIPrefLocalizedString.h" + +namespace mozilla { +namespace browser { + +NS_IMPL_ISUPPORTS(DirectoryProvider, + nsIDirectoryServiceProvider, + nsIDirectoryServiceProvider2) + +NS_IMETHODIMP +DirectoryProvider::GetFile(const char *aKey, bool *aPersist, nsIFile* *aResult) +{ + return NS_ERROR_FAILURE; +} + +static void +AppendFileKey(const char *key, nsIProperties* aDirSvc, + nsCOMArray<nsIFile> &array) +{ + nsCOMPtr<nsIFile> file; + nsresult rv = aDirSvc->Get(key, NS_GET_IID(nsIFile), getter_AddRefs(file)); + if (NS_FAILED(rv)) + return; + + bool exists; + rv = file->Exists(&exists); + if (NS_FAILED(rv) || !exists) + return; + + array.AppendObject(file); +} + +// Appends the distribution-specific search engine directories to the +// array. The directory structure is as follows: + +// appdir/ +// \- distribution/ +// \- searchplugins/ +// |- common/ +// \- locale/ +// |- <locale 1>/ +// ... +// \- <locale N>/ + +// common engines are loaded for all locales. If there is no locale +// directory for the current locale, there is a pref: +// "distribution.searchplugins.defaultLocale" +// which specifies a default locale to use. + +static void +AppendDistroSearchDirs(nsIProperties* aDirSvc, nsCOMArray<nsIFile> &array) +{ + nsCOMPtr<nsIFile> searchPlugins; + nsresult rv = aDirSvc->Get(XRE_APP_DISTRIBUTION_DIR, + NS_GET_IID(nsIFile), + getter_AddRefs(searchPlugins)); + if (NS_FAILED(rv)) + return; + searchPlugins->AppendNative(NS_LITERAL_CSTRING("searchplugins")); + + bool exists; + rv = searchPlugins->Exists(&exists); + if (NS_FAILED(rv) || !exists) + return; + + nsCOMPtr<nsIFile> commonPlugins; + rv = searchPlugins->Clone(getter_AddRefs(commonPlugins)); + if (NS_SUCCEEDED(rv)) { + commonPlugins->AppendNative(NS_LITERAL_CSTRING("common")); + rv = commonPlugins->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) + array.AppendObject(commonPlugins); + } + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + + nsCOMPtr<nsIFile> localePlugins; + rv = searchPlugins->Clone(getter_AddRefs(localePlugins)); + if (NS_FAILED(rv)) + return; + + localePlugins->AppendNative(NS_LITERAL_CSTRING("locale")); + + nsCString locale; + nsCOMPtr<nsIPrefLocalizedString> prefString; + rv = prefs->GetComplexValue("general.useragent.locale", + NS_GET_IID(nsIPrefLocalizedString), + getter_AddRefs(prefString)); + if (NS_SUCCEEDED(rv)) { + nsAutoString wLocale; + prefString->GetData(getter_Copies(wLocale)); + CopyUTF16toUTF8(wLocale, locale); + } else { + rv = prefs->GetCharPref("general.useragent.locale", getter_Copies(locale)); + } + + if (NS_SUCCEEDED(rv)) { + + nsCOMPtr<nsIFile> curLocalePlugins; + rv = localePlugins->Clone(getter_AddRefs(curLocalePlugins)); + if (NS_SUCCEEDED(rv)) { + + curLocalePlugins->AppendNative(locale); + rv = curLocalePlugins->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) { + array.AppendObject(curLocalePlugins); + return; // all done + } + } + } + + // we didn't append the locale dir - try the default one + nsCString defLocale; + rv = prefs->GetCharPref("distribution.searchplugins.defaultLocale", + getter_Copies(defLocale)); + if (NS_SUCCEEDED(rv)) { + + nsCOMPtr<nsIFile> defLocalePlugins; + rv = localePlugins->Clone(getter_AddRefs(defLocalePlugins)); + if (NS_SUCCEEDED(rv)) { + + defLocalePlugins->AppendNative(defLocale); + rv = defLocalePlugins->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) + array.AppendObject(defLocalePlugins); + } + } + } +} + +NS_IMETHODIMP +DirectoryProvider::GetFiles(const char *aKey, nsISimpleEnumerator* *aResult) +{ + nsresult rv; + + if (!strcmp(aKey, NS_APP_SEARCH_DIR_LIST)) { + nsCOMPtr<nsIProperties> dirSvc + (do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID)); + if (!dirSvc) + return NS_ERROR_FAILURE; + + nsCOMArray<nsIFile> baseFiles; + + /** + * We want to preserve the following order, since the search service loads + * engines in first-loaded-wins order. + * - extension search plugin locations (prepended below using + * NS_NewUnionEnumerator) + * - distro search plugin locations + * - user search plugin locations (profile) + * - app search plugin location (shipped engines) + */ + AppendDistroSearchDirs(dirSvc, baseFiles); + AppendFileKey(NS_APP_USER_SEARCH_DIR, dirSvc, baseFiles); + AppendFileKey(NS_APP_SEARCH_DIR, dirSvc, baseFiles); + + nsCOMPtr<nsISimpleEnumerator> baseEnum; + rv = NS_NewArrayEnumerator(getter_AddRefs(baseEnum), baseFiles); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr<nsISimpleEnumerator> list; + rv = dirSvc->Get(XRE_EXTENSIONS_DIR_LIST, + NS_GET_IID(nsISimpleEnumerator), getter_AddRefs(list)); + if (NS_FAILED(rv)) + return rv; + + static char const *const kAppendSPlugins[] = {"searchplugins", nullptr}; + + nsCOMPtr<nsISimpleEnumerator> extEnum = + new AppendingEnumerator(list, kAppendSPlugins); + if (!extEnum) + return NS_ERROR_OUT_OF_MEMORY; + + return NS_NewUnionEnumerator(aResult, extEnum, baseEnum); + } + + return NS_ERROR_FAILURE; +} + +NS_IMPL_ISUPPORTS(DirectoryProvider::AppendingEnumerator, nsISimpleEnumerator) + +NS_IMETHODIMP +DirectoryProvider::AppendingEnumerator::HasMoreElements(bool *aResult) +{ + *aResult = mNext ? true : false; + return NS_OK; +} + +NS_IMETHODIMP +DirectoryProvider::AppendingEnumerator::GetNext(nsISupports* *aResult) +{ + if (aResult) + NS_ADDREF(*aResult = mNext); + + mNext = nullptr; + + nsresult rv; + + // Ignore all errors + + bool more; + while (NS_SUCCEEDED(mBase->HasMoreElements(&more)) && more) { + nsCOMPtr<nsISupports> nextbasesupp; + mBase->GetNext(getter_AddRefs(nextbasesupp)); + + nsCOMPtr<nsIFile> nextbase(do_QueryInterface(nextbasesupp)); + if (!nextbase) + continue; + + nextbase->Clone(getter_AddRefs(mNext)); + if (!mNext) + continue; + + char const *const * i = mAppendList; + while (*i) { + mNext->AppendNative(nsDependentCString(*i)); + ++i; + } + + bool exists; + rv = mNext->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) + break; + + mNext = nullptr; + } + + return NS_OK; +} + +DirectoryProvider::AppendingEnumerator::AppendingEnumerator + (nsISimpleEnumerator* aBase, + char const *const *aAppendList) : + mBase(aBase), + mAppendList(aAppendList) +{ + // Initialize mNext to begin. + GetNext(nullptr); +} + +} // namespace browser +} // namespace mozilla diff --git a/browser/components/dirprovider/DirectoryProvider.h b/browser/components/dirprovider/DirectoryProvider.h new file mode 100644 index 000000000..43fa85ab9 --- /dev/null +++ b/browser/components/dirprovider/DirectoryProvider.h @@ -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/. */ + +#ifndef DirectoryProvider_h__ +#define DirectoryProvider_h__ + +#include "nsIDirectoryService.h" +#include "nsComponentManagerUtils.h" +#include "nsISimpleEnumerator.h" +#include "nsIFile.h" +#include "mozilla/Attributes.h" + +#define NS_BROWSERDIRECTORYPROVIDER_CONTRACTID \ + "@mozilla.org/browser/directory-provider;1" + +namespace mozilla { +namespace browser { + +class DirectoryProvider final : public nsIDirectoryServiceProvider2 +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDIRECTORYSERVICEPROVIDER + NS_DECL_NSIDIRECTORYSERVICEPROVIDER2 + +private: + ~DirectoryProvider() {} + + class AppendingEnumerator final : public nsISimpleEnumerator + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISIMPLEENUMERATOR + + AppendingEnumerator(nsISimpleEnumerator* aBase, + char const *const *aAppendList); + + private: + ~AppendingEnumerator() {} + + nsCOMPtr<nsISimpleEnumerator> mBase; + char const *const *const mAppendList; + nsCOMPtr<nsIFile> mNext; + }; +}; + +} // namespace browser +} // namespace mozilla + +#endif // DirectoryProvider_h__ diff --git a/browser/components/dirprovider/moz.build b/browser/components/dirprovider/moz.build new file mode 100644 index 000000000..3f51743af --- /dev/null +++ b/browser/components/dirprovider/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +EXPORTS.mozilla.browser += ['DirectoryProvider.h'] + +SOURCES += ['DirectoryProvider.cpp'] + +FINAL_LIBRARY = 'browsercomps' + +LOCAL_INCLUDES += ['../build'] diff --git a/browser/components/distribution.js b/browser/components/distribution.js new file mode 100644 index 000000000..86ab6e748 --- /dev/null +++ b/browser/components/distribution.js @@ -0,0 +1,373 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 = [ "DistributionCustomizer" ]; + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = + "distribution-customization-complete"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +this.DistributionCustomizer = function DistributionCustomizer() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + let iniFile = dirSvc.get("XREExeF", Ci.nsIFile); + iniFile.leafName = "distribution"; + iniFile.append("distribution.ini"); + if (iniFile.exists()) { + this._iniFile = iniFile; + } +} + +DistributionCustomizer.prototype = { + _iniFile: null, + + get _ini() { + let ini = Cc["@mozilla.org/xpcom/ini-parser-factory;1"] + .getService(Ci.nsIINIParserFactory) + .createINIParser(this._iniFile); + this.__defineGetter__("_ini", function() ini); + return this._ini; + }, + + get _locale() { + let locale = this._prefs.getCharPref("general.useragent.locale", "en-US"); + this.__defineGetter__("_locale", function() locale); + return this._locale; + }, + + get _prefSvc() { + let svc = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService); + this.__defineGetter__("_prefSvc", function() svc); + return this._prefSvc; + }, + + get _prefs() { + let branch = this._prefSvc.getBranch(null); + this.__defineGetter__("_prefs", function() branch); + return this._prefs; + }, + + get _ioSvc() { + let svc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + this.__defineGetter__("_ioSvc", function() svc); + return this._ioSvc; + }, + + _makeURI: function(spec) { + return this._ioSvc.newURI(spec, null, null); + }, + + _parseBookmarksSection: + function(parentId, section) { + let keys = []; + for (let i in enumerate(this._ini.getKeys(section))) { + keys.push(i); + } + keys.sort(); + + let items = {}; + let defaultItemId = -1; + let maxItemId = -1; + + for (let i = 0; i < keys.length; i++) { + let m = /^item\.(\d+)\.(\w+)\.?(\w*)/.exec(keys[i]); + if (m) { + let [foo, iid, iprop, ilocale] = m; + iid = parseInt(iid); + + if (ilocale) { + continue; + } + + if (!items[iid]) { + items[iid] = {}; + } + if (keys.indexOf(keys[i] + "." + this._locale) >= 0) { + items[iid][iprop] = this._ini.getString(section, keys[i] + "." + + this._locale); + } else { + items[iid][iprop] = this._ini.getString(section, keys[i]); + } + + if (iprop == "type" && items[iid]["type"] == "default") { + defaultItemId = iid; + } + + if (maxItemId < iid) { + maxItemId = iid; + } + } else { + dump("Key did not match: " + keys[i] + "\n"); + } + } + + let prependIndex = 0; + for (let iid = 0; iid <= maxItemId; iid++) { + if (!items[iid]) { + continue; + } + + let index = PlacesUtils.bookmarks.DEFAULT_INDEX; + let newId; + + switch (items[iid]["type"]) { + case "default": + break; + + case "folder": + if (iid < defaultItemId) { + index = prependIndex++; + } + + newId = PlacesUtils.bookmarks.createFolder(parentId, + items[iid]["title"], + index); + + this._parseBookmarksSection(newId, "BookmarksFolder-" + + items[iid]["folderId"]); + + if (items[iid]["description"]) + PlacesUtils.annotations.setItemAnnotation(newId, + "bookmarkProperties/description", + items[iid]["description"], 0, + PlacesUtils.annotations.EXPIRE_NEVER); + + break; + + case "separator": + if (iid < defaultItemId) { + index = prependIndex++; + } + PlacesUtils.bookmarks.insertSeparator(parentId, index); + break; + + case "livemark": + if (iid < defaultItemId) { + index = prependIndex++; + } + + // Don't bother updating the livemark contents on creation. + PlacesUtils.livemarks.addLivemark({ title: items[iid]["title"], + parentId: parentId, + index: index, + feedURI: this._makeURI(items[iid]["feedLink"]), + siteURI: this._makeURI(items[iid]["siteLink"]) + }).then(null, Cu.reportError); + break; + + case "bookmark": + // Fallthrough + default: + if (iid < defaultItemId) { + index = prependIndex++; + } + + newId = PlacesUtils.bookmarks.insertBookmark(parentId, + this._makeURI(items[iid]["link"]), + index, items[iid]["title"]); + + if (items[iid]["description"]) { + PlacesUtils.annotations.setItemAnnotation(newId, "bookmarkProperties/description", + items[iid]["description"], 0, + PlacesUtils.annotations.EXPIRE_NEVER); + } + + break; + } + } + }, + + _customizationsApplied: false, + applyCustomizations: function() { + this._customizationsApplied = true; + if (!this._iniFile) { + return this._checkCustomizationComplete(); + } + + // nsPrefService loads very early. Reload prefs so we can set + // distribution defaults during the prefservice:after-app-defaults + // notification (see applyPrefDefaults below) + this._prefSvc.QueryInterface(Ci.nsIObserver); + this._prefSvc.observe(null, "reload-default-prefs", null); + }, + + _bookmarksApplied: false, + applyBookmarks: function() { + this._bookmarksApplied = true; + if (!this._iniFile) { + return this._checkCustomizationComplete(); + } + + let sections = enumToObject(this._ini.getSections()); + + // The global section, and several of its fields, is required + // (we also check here to be consistent with applyPrefDefaults below) + if (!sections["Global"]) { + return this._checkCustomizationComplete(); + } + let globalPrefs = enumToObject(this._ini.getKeys("Global")); + if (!(globalPrefs["id"] && globalPrefs["version"] && globalPrefs["about"])) { + return this._checkCustomizationComplete(); + } + + let bmProcessedPref; + try { + bmProcessedPref = this._ini.getString("Global", + "bookmarks.initialized.pref"); + } catch(e) { + bmProcessedPref = "distribution." + + this._ini.getString("Global", "id") + ".bookmarksProcessed"; + } + + let bmProcessed = this._prefs.getBoolPref(bmProcessedPref, false); + + if (!bmProcessed) { + if (sections["BookmarksMenu"]) { + this._parseBookmarksSection(PlacesUtils.bookmarksMenuFolderId, + "BookmarksMenu"); + } + if (sections["BookmarksToolbar"]) { + this._parseBookmarksSection(PlacesUtils.toolbarFolderId, + "BookmarksToolbar"); + } + this._prefs.setBoolPref(bmProcessedPref, true); + } + return this._checkCustomizationComplete(); + }, + + _prefDefaultsApplied: false, + applyPrefDefaults: function() { + this._prefDefaultsApplied = true; + if (!this._iniFile) { + return this._checkCustomizationComplete(); + } + + let sections = enumToObject(this._ini.getSections()); + + // The global section, and several of its fields, is required + if (!sections["Global"]) { + return this._checkCustomizationComplete(); + } + let globalPrefs = enumToObject(this._ini.getKeys("Global")); + if (!(globalPrefs["id"] && globalPrefs["version"] && globalPrefs["about"])) { + return this._checkCustomizationComplete(); + } + + let defaults = this._prefSvc.getDefaultBranch(null); + + // Global really contains info we set as prefs. They're only + // separate because they are "special" (read: required) + + defaults.setCharPref("distribution.id", this._ini.getString("Global", "id")); + defaults.setCharPref("distribution.version", + this._ini.getString("Global", "version")); + + let partnerAbout = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + try { + if (globalPrefs["about." + this._locale]) { + partnerAbout.data = this._ini.getString("Global", "about." + this._locale); + } else { + partnerAbout.data = this._ini.getString("Global", "about"); + } + defaults.setComplexValue("distribution.about", + Ci.nsISupportsString, partnerAbout); + } catch(e) { + /* ignore bad prefs due to bug 895473 and move on */ + Cu.reportError(e); + } + + if (sections["Preferences"]) { + for (let key in enumerate(this._ini.getKeys("Preferences"))) { + try { + let value = eval(this._ini.getString("Preferences", key)); + switch (typeof value) { + case "boolean": + defaults.setBoolPref(key, value); + break; + case "number": + defaults.setIntPref(key, value); + break; + case "string": + defaults.setCharPref(key, value); + break; + case "undefined": + defaults.setCharPref(key, value); + break; + } + } catch(e) { + /* ignore bad prefs and move on */ + } + } + } + + // We eval() the localizable prefs as well (even though they'll + // always get set as a string) to keep the INI format consistent: + // string prefs always need to be in quotes + + let localizedStr = Cc["@mozilla.org/pref-localizedstring;1"] + .createInstance(Ci.nsIPrefLocalizedString); + + if (sections["LocalizablePreferences"]) { + for (let key in enumerate(this._ini.getKeys("LocalizablePreferences"))) { + try { + let value = eval(this._ini.getString("LocalizablePreferences", key)); + value = value.replace("%LOCALE%", this._locale, "g"); + localizedStr.data = "data:text/plain," + key + "=" + value; + defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedStr); + } catch(e) { + /* ignore bad prefs and move on */ + } + } + } + + if (sections["LocalizablePreferences-" + this._locale]) { + for (let key in enumerate(this._ini.getKeys("LocalizablePreferences-" + this._locale))) { + try { + let value = eval(this._ini.getString("LocalizablePreferences-" + this._locale, key)); + localizedStr.data = "data:text/plain," + key + "=" + value; + defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedStr); + } catch(e) { + /* ignore bad prefs and move on */ + } + } + } + + return this._checkCustomizationComplete(); + }, + + _checkCustomizationComplete: function() { + let prefDefaultsApplied = this._prefDefaultsApplied || !this._iniFile; + if (this._customizationsApplied && this._bookmarksApplied && + prefDefaultsApplied) { + let os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + os.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC, null); + } + } +}; + +function enumerate(UTF8Enumerator) { + while (UTF8Enumerator.hasMore()) { + yield UTF8Enumerator.getNext(); + } +} + +function enumToObject(UTF8Enumerator) { + let ret = {}; + for (let i in enumerate(UTF8Enumerator)) { + ret[i] = 1; + } + return ret; +} diff --git a/browser/components/downloads/BrowserDownloads.manifest b/browser/components/downloads/BrowserDownloads.manifest new file mode 100644 index 000000000..1881ca1d7 --- /dev/null +++ b/browser/components/downloads/BrowserDownloads.manifest @@ -0,0 +1,4 @@ +component {49507fe5-2cee-4824-b6a3-e999150ce9b8} DownloadsStartup.js +contract @mozilla.org/browser/downloadsstartup;1 {49507fe5-2cee-4824-b6a3-e999150ce9b8} +category profile-after-change DownloadsStartup @mozilla.org/browser/downloadsstartup;1 +component {4d99321e-d156-455b-81f7-e7aa2308134f} DownloadsUI.js diff --git a/browser/components/downloads/DownloadsCommon.jsm b/browser/components/downloads/DownloadsCommon.jsm new file mode 100644 index 000000000..adc999b74 --- /dev/null +++ b/browser/components/downloads/DownloadsCommon.jsm @@ -0,0 +1,1911 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DownloadsCommon", +]; + +/** + * Handles the Downloads panel shared methods and data access. + * + * This file includes the following constructors and global objects: + * + * DownloadsCommon + * This object is exposed directly to the consumers of this JavaScript module, + * and provides shared methods for all the instances of the user interface. + * + * DownloadsData + * Retrieves the list of past and completed downloads from the underlying + * Downloads API data, and provides asynchronous notifications allowing + * to build a consistent view of the available data. + * + * DownloadsIndicatorData + * This object registers itself with DownloadsData as a view, and transforms the + * notifications it receives into overall status data, that is then broadcast to + * the registered download status indicators. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +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"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", + "resource://gre/modules/DownloadUIHelper.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm") +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger", + "resource:///modules/DownloadsLogger.jsm"); + +const nsIDM = Ci.nsIDownloadManager; + +const kDownloadsStringBundleUrl = + "chrome://browser/locale/downloads/downloads.properties"; + +const kPrefConfirmOpenExe = "browser.download.confirmOpenExecutable"; + +const kDownloadsStringsRequiringFormatting = { + sizeWithUnits: true, + shortTimeLeftSeconds: true, + shortTimeLeftMinutes: true, + shortTimeLeftHours: true, + shortTimeLeftDays: true, + statusSeparator: true, + statusSeparatorBeforeNumber: true, + fileExecutableSecurityWarning: true +}; + +const kDownloadsStringsRequiringPluralForm = { + otherDownloads2: true +}; + +const kPartialDownloadSuffix = ".part"; + +const kPrefBranch = Services.prefs.getBranch("browser.download."); + +var PrefObserver = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + getPref: function(name) { + try { + switch (typeof this.prefs[name]) { + case "boolean": + return kPrefBranch.getBoolPref(name); + } + } catch (ex) { } + return this.prefs[name]; + }, + observe: function(aSubject, aTopic, aData) { + if (this.prefs.hasOwnProperty(aData)) { + return this[aData] = this.getPref(aData); + } + }, + register: function(prefs) { + this.prefs = prefs; + kPrefBranch.addObserver("", this, true); + for (let key in prefs) { + let name = key; + XPCOMUtils.defineLazyGetter(this, name, function() { + return PrefObserver.getPref(name); + }); + } + }, +}; + +PrefObserver.register({ + // prefName: defaultValue + debug: false, + animateNotifications: true +}); + + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsCommon + +/** + * This object is exposed directly to the consumers of this JavaScript module, + * and provides shared methods for all the instances of the user interface. + */ +this.DownloadsCommon = { + log: function(...aMessageArgs) { + delete this.log; + this.log = function(...aMessageArgs) { + if (!PrefObserver.debug) { + return; + } + DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs); + } + this.log.apply(this, aMessageArgs); + }, + + error: function(...aMessageArgs) { + delete this.error; + this.error = function(...aMessageArgs) { + if (!PrefObserver.debug) { + return; + } + DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs); + } + this.error.apply(this, aMessageArgs); + }, + /** + * Returns an object whose keys are the string names from the downloads string + * bundle, and whose values are either the translated strings or functions + * returning formatted strings. + */ + get strings() + { + let strings = {}; + let sb = Services.strings.createBundle(kDownloadsStringBundleUrl); + let enumerator = sb.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + let stringName = string.key; + if (stringName in kDownloadsStringsRequiringFormatting) { + strings[stringName] = function() { + // Convert "arguments" to a real array before calling into XPCOM. + return sb.formatStringFromName(stringName, + Array.slice(arguments, 0), + arguments.length); + }; + } else if (stringName in kDownloadsStringsRequiringPluralForm) { + strings[stringName] = function(aCount) { + // Convert "arguments" to a real array before calling into XPCOM. + let formattedString = sb.formatStringFromName(stringName, + Array.slice(arguments, 0), + arguments.length); + return PluralForm.get(aCount, formattedString); + }; + } else { + strings[stringName] = string.value; + } + } + delete this.strings; + return this.strings = strings; + }, + + /** + * Generates a very short string representing the given time left. + * + * @param aSeconds + * Value to be formatted. It represents the number of seconds, it must + * be positive but does not need to be an integer. + * + * @return Formatted string, for example "30s" or "2h". The returned value is + * maximum three characters long, at least in English. + */ + formatTimeLeft: function(aSeconds) + { + // Decide what text to show for the time + let seconds = Math.round(aSeconds); + if (!seconds) { + return ""; + } else if (seconds <= 30) { + return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds); + } + let minutes = Math.round(aSeconds / 60); + if (minutes < 60) { + return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes); + } + let hours = Math.round(minutes / 60); + if (hours < 48) { // two days + return DownloadsCommon.strings["shortTimeLeftHours"](hours); + } + let days = Math.round(hours / 24); + return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99)); + }, + + /** + * Indicates whether we should show the full Download Manager window interface + * instead of the simplified panel interface. The behavior of downloads + * across browsing session is consistent with the selected interface. + */ + get useToolkitUI() + { + /* Toolkit UI is currently incompatible. + * FIXME: Either fix the toolkitUI (make DBConnection work) or remove + * the unused code altogether + */ + //try { + // return Services.prefs.getBoolPref("browser.download.useToolkitUI"); + //} catch (ex) { } + return false; + }, + + /** + * Indicates whether we should show visual notification on the indicator + * when a download event is triggered. + */ + get animateNotifications() + { + return PrefObserver.animateNotifications; + }, + + /** + * Get access to one of the DownloadsData or PrivateDownloadsData objects, + * depending on the privacy status of the window in question. + * + * @param aWindow + * The browser window which owns the download button. + */ + getData: function(aWindow) { + if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + return PrivateDownloadsData; + } else { + return DownloadsData; + } + }, + + /** + * Initializes the data link for both the private and non-private downloads + * data objects. + * + * @param aDownloadManagerService + * Reference to the service implementing nsIDownloadManager. We need + * this because getService isn't available for us when this method is + * called, and we must ensure to register our listeners before the + * getService call for the Download Manager returns. + */ + initializeAllDataLinks: function(aDownloadManagerService) { + DownloadsData.initializeDataLink(aDownloadManagerService); + PrivateDownloadsData.initializeDataLink(aDownloadManagerService); + }, + + /** + * Terminates the data link for both the private and non-private downloads + * data objects. + */ + terminateAllDataLinks: function() { + DownloadsData.terminateDataLink(); + PrivateDownloadsData.terminateDataLink(); + }, + + /** + * Reloads the specified kind of downloads from the non-private store. + * This method must only be called when Private Browsing Mode is disabled. + * + * @param aActiveOnly + * True to load only active downloads from the database. + */ + ensureAllPersistentDataLoaded: + function(aActiveOnly) { + DownloadsData.ensurePersistentDataLoaded(aActiveOnly); + }, + + /** + * Get access to one of the DownloadsIndicatorData or + * PrivateDownloadsIndicatorData objects, depending on the privacy status of + * the window in question. + */ + getIndicatorData: function(aWindow) { + if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + return PrivateDownloadsIndicatorData; + } else { + return DownloadsIndicatorData; + } + }, + + /** + * Returns a reference to the DownloadsSummaryData singleton - creating one + * in the process if one hasn't been instantiated yet. + * + * @param aWindow + * The browser window which owns the download button. + * @param aNumToExclude + * The number of items on the top of the downloads list to exclude + * from the summary. + */ + getSummary: function(aWindow, aNumToExclude) + { + if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + if (this._privateSummary) { + return this._privateSummary; + } + return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude); + } else { + if (this._summary) { + return this._summary; + } + return this._summary = new DownloadsSummaryData(false, aNumToExclude); + } + }, + _summary: null, + _privateSummary: null, + + /** + * Returns the legacy state integer value for the provided Download object. + */ + stateOfDownload(download) { + // Collapse state using the correct priority. + if (!download.stopped) { + return nsIDM.DOWNLOAD_DOWNLOADING; + } + if (download.succeeded) { + return nsIDM.DOWNLOAD_FINISHED; + } + if (download.error) { + if (download.error.becauseBlockedByParentalControls) { + return nsIDM.DOWNLOAD_BLOCKED_PARENTAL; + } + return nsIDM.DOWNLOAD_FAILED; + } + if (download.canceled) { + if (download.hasPartialData) { + return nsIDM.DOWNLOAD_PAUSED; + } + return nsIDM.DOWNLOAD_CANCELED; + } + return nsIDM.DOWNLOAD_NOTSTARTED; + }, + + /** + * Helper function required because the Downloads Panel and the Downloads View + * don't share the controller yet. + */ + removeAndFinalizeDownload(download) { + Downloads.getList(Downloads.ALL) + .then(list => list.remove(download)) + .then(() => download.finalize(true)) + .catch(Cu.reportError); + }, + + /** + * Given an iterable collection of Download objects, generates and returns + * statistics about that collection. + * + * @param downloads An iterable collection of Download objects. + * + * @return Object whose properties are the generated statistics. Currently, + * we return the following properties: + * + * numActive : The total number of downloads. + * numPaused : The total number of paused downloads. + * numDownloading : The total number of downloads being downloaded. + * totalSize : The total size of all downloads once completed. + * totalTransferred: The total amount of transferred data for these + * downloads. + * slowestSpeed : The slowest download rate. + * rawTimeLeft : The estimated time left for the downloads to + * complete. + * percentComplete : The percentage of bytes successfully downloaded. + */ + summarizeDownloads(downloads) { + let summary = { + numActive: 0, + numPaused: 0, + numDownloading: 0, + totalSize: 0, + totalTransferred: 0, + // slowestSpeed is Infinity so that we can use Math.min to + // find the slowest speed. We'll set this to 0 afterwards if + // it's still at Infinity by the time we're done iterating all + // download. + slowestSpeed: Infinity, + rawTimeLeft: -1, + percentComplete: -1 + } + + for (let download of downloads) { + summary.numActive++; + + if (!download.stopped) { + summary.numDownloading++; + if (download.hasProgress && download.speed > 0) { + let sizeLeft = download.totalBytes - download.currentBytes; + summary.rawTimeLeft = Math.max(summary.rawTimeLeft, + sizeLeft / download.speed); + summary.slowestSpeed = Math.min(summary.slowestSpeed, + download.speed); + } + } else if (download.canceled && download.hasPartialData) { + summary.numPaused++; + } + // Only add to total values if we actually know the download size. + if (download.succeeded) { + summary.totalSize += download.target.size; + summary.totalTransferred += download.target.size; + } else if (download.hasProgress) { + summary.totalSize += download.totalBytes; + summary.totalTransferred += download.currentBytes; + } + } + + if (summary.totalSize != 0) { + summary.percentComplete = (summary.totalTransferred / + summary.totalSize) * 100; + } + + if (summary.slowestSpeed == Infinity) { + summary.slowestSpeed = 0; + } + + return summary; + }, + + /** + * If necessary, smooths the estimated number of seconds remaining for one + * or more downloads to complete. + * + * @param aSeconds + * Current raw estimate on number of seconds left for one or more + * downloads. This is a floating point value to help get sub-second + * accuracy for current and future estimates. + */ + smoothSeconds: function(aSeconds, aLastSeconds) + { + // We apply an algorithm similar to the DownloadUtils.getTimeLeft function, + // though tailored to a single time estimation for all downloads. We never + // apply something if the new value is less than half the previous value. + let shouldApplySmoothing = aLastSeconds >= 0 && + aSeconds > aLastSeconds / 2; + if (shouldApplySmoothing) { + // Apply hysteresis to favor downward over upward swings. Trust only 30% + // of the new value if lower, and 10% if higher (exponential smoothing). + let diff = aSeconds - aLastSeconds; + aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff; + + // If the new time is similar, reuse something close to the last time + // left, but subtract a little to provide forward progress. + diff = aSeconds - aLastSeconds; + let diffPercent = diff / aLastSeconds * 100; + if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { + aSeconds = aLastSeconds - (diff < 0 ? .4 : .2); + } + } + + // In the last few seconds of downloading, we are always subtracting and + // never adding to the time left. Ensure that we never fall below one + // second left until all downloads are actually finished. + return aLastSeconds = Math.max(aSeconds, 1); + }, + + /** + * Opens a downloaded file. + * + * @param aFile + * the downloaded file to be opened. + * @param aMimeInfo + * the mime type info object. May be null. + * @param aOwnerWindow + * the window with which this action is associated. + */ + openDownloadedFile: function(aFile, aMimeInfo, aOwnerWindow) { + if (!(aFile instanceof Ci.nsIFile)) + throw new Error("aFile must be a nsIFile object"); + if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) + throw new Error("Invalid value passed for aMimeInfo"); + if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) + throw new Error("aOwnerWindow must be a dom-window object"); + +#ifdef XP_WIN + // On Windows, the system will provide a native confirmation prompt + // for .exe files. Exclude this from our prompt, but prompt on other + // executable types. + let isWindowsExe = aFile.leafName.toLowerCase().endsWith(".exe"); +#else + let isWindowsExe = false; +#endif + + // Confirm opening executable files if required. + if (aFile.isExecutable() && !isWindowsExe) { + let showAlert = true; + try { + showAlert = Services.prefs.getBoolPref(kPrefConfirmOpenExe); + } catch (ex) { + // If the preference does not exist, continue with the prompt. + } + + if (showAlert) { + let name = aFile.leafName; + let message = + DownloadsCommon.strings.fileExecutableSecurityWarning(name, name); + let title = + DownloadsCommon.strings.fileExecutableSecurityWarningTitle; + + let open = Services.prompt.confirm(aOwnerWindow, title, message); + if (!open) { + return; + } + } + } + + // Actually open the file. + try { + if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) { + aMimeInfo.launchWithFile(aFile); + return; + } + } + catch(ex) { } + + // If either we don't have the mime info, or the preferred action failed, + // attempt to launch the file directly. + try { + aFile.launch(); + } + catch(ex) { + // If launch fails, try sending it through the system's external "file:" + // URL handler. + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadUrl(NetUtil.newURI(aFile)); + } + }, + + /** + * Show a downloaded file in the system file manager. + * + * @param aFile + * a downloaded file. + */ + showDownloadedFile: function(aFile) { + if (!(aFile instanceof Ci.nsIFile)) + throw new Error("aFile must be a nsIFile object"); + try { + // Show the directory containing the file and select the file. + aFile.reveal(); + } catch (ex) { + // If reveal fails for some reason (e.g., it's not implemented on unix + // or the file doesn't exist), try using the parent if we have it. + let parent = aFile.parent; + if (parent) { + try { + // Open the parent directory to show where the file should be. + parent.launch(); + } catch (ex) { + // If launch also fails (probably because it's not implemented), let + // the OS handler try to open the parent. + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadUrl(NetUtil.newURI(parent)); + } + } + } + } +}; + +/** + * Returns true if we are executing on Windows Vista or a later version. + */ +XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function() { + let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; + if (os != "WINNT") { + return false; + } + let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); + return parseFloat(sysInfo.getProperty("version")) >= 6; +}); + +/** + * Returns true to indicate that we should hook the panel to the JavaScript API + * for downloads instead of the nsIDownloadManager back-end. + * This is kept for compatibility/leftovers and should be removed later. + */ +XPCOMUtils.defineLazyGetter(DownloadsCommon, "useJSTransfer", function() { + return true; +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsData + +/** + * Retrieves the list of past and completed downloads from the underlying + * Download Manager data, and provides asynchronous notifications allowing to + * build a consistent view of the available data. + * + * This object responds to real-time changes in the underlying Download Manager + * data. For example, the deletion of one or more downloads is notified through + * the nsIObserver interface, while any state or progress change is notified + * through the nsIDownloadProgressListener interface. + * + * Note that using this object does not automatically start the Download Manager + * service. Consumers will see an empty list of downloads until the service is + * actually started. This is useful to display a neutral progress indicator in + * the main browser window until the autostart timeout elapses. + * + * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton + * objects, one accessing non-private downloads, and the other accessing private + * ones. + */ +function DownloadsDataCtor(aPrivate) { + this._isPrivate = aPrivate; + + // Contains all the available Download objects and their integer state. + this.oldDownloadStates = new Map(); + + // Array of view objects that should be notified when the available download + // data changes. + this._views = []; +} + +DownloadsDataCtor.prototype = { + /** + * Starts receiving events for current downloads. + */ + initializeDataLink() { + if (!this._dataLinkInitialized) { + let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE + : Downloads.PUBLIC); + promiseList.then(list => list.addView(this)).then(null, Cu.reportError); + this._dataLinkInitialized = true; + } + }, + _dataLinkInitialized: false, + + /** + * Stops receiving events for current downloads and cancels any pending read. + */ + terminateDataLink: function() + { + Cu.reportError("terminateDataLink not applicable with JS Transfers"); + return; + }, + + /** + * Iterator for all the available Download objects. This is empty until the + * data has been loaded using the JavaScript API for downloads. + */ + get downloads() this.oldDownloadStates.keys(), + + /** + * True if there are finished downloads that can be removed from the list. + */ + get canRemoveFinished() + { + for (let download of this.downloads) { + // Stopped, paused, and failed downloads with partial data are removed. + if (download.stopped && !(download.canceled && download.hasPartialData)) { + return true; + } + } + return false; + }, + + /** + * Asks the back-end to remove finished downloads from the list. + */ + removeFinished: function() + { + let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE + : Downloads.PUBLIC); + promiseList.then(list => list.removeFinished()) + .then(null, Cu.reportError); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Integration with the asynchronous Downloads back-end + + onDownloadAdded(download) { + // Download objects do not store the end time of downloads, as the Downloads + // API does not need to persist this information for all platforms. Once a + // download terminates on a Desktop browser, it becomes a history download, + // for which the end time is stored differently, as a Places annotation. + download.endTime = Date.now(); + + this.oldDownloadStates.set(download, + DownloadsCommon.stateOfDownload(download)); + + for (let view of this._views) { + view.onDownloadAdded(download, true); + } + }, + + onDownloadChanged(download) { + let oldState = this.oldDownloadStates.get(download); + let newState = DownloadsCommon.stateOfDownload(download); + this.oldDownloadStates.set(download, newState); + + if (oldState != newState) { + if (download.succeeded || + (download.canceled && !download.hasPartialData) || + download.error) { + // Store the end time that may be displayed by the views. + download.endTime = Date.now(); + + if (!this._isPrivate) { + try { + let downloadMetaData = { + state: DownloadsCommon.stateOfDownload(download), + endTime: download.endTime, + }; + if (download.succeeded) { + downloadMetaData.fileSize = download.target.size; + } + PlacesUtils.annotations.setPageAnnotation( + NetUtil.newURI(download.source.url), + "downloads/metaData", + JSON.stringify(downloadMetaData), 0, + PlacesUtils.annotations.EXPIRE_WITH_HISTORY); + } catch (ex) { + Cu.reportError(ex); + } + } + } + + for (let view of this._views) { + try { + view.onDownloadStateChanged(download); + } catch (ex) { + Cu.reportError(ex); + } + } + + if (download.succeeded || + (download.error && download.error.becauseBlocked)) { + this._notifyDownloadEvent("finish"); + } + } + + if (!download.newDownloadNotified) { + download.newDownloadNotified = true; + this._notifyDownloadEvent("start"); + } + + for (let view of this._views) { + view.onDownloadChanged(download); + } + }, + + onDownloadRemoved(download) { + this.oldDownloadStates.delete(download); + + for (let view of this._views) { + view.onDownloadRemoved(download); + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Registration of views + + /** + * Adds an object to be notified when the available download data changes. + * The specified object is initialized with the currently available downloads. + * + * @param aView + * DownloadsView object to be added. This reference must be passed to + * removeView before termination. + */ + addView: function(aView) + { + this._views.push(aView); + this._updateView(aView); + }, + + /** + * Removes an object previously added using addView. + * + * @param aView + * DownloadsView object to be removed. + */ + removeView: function(aView) + { + let index = this._views.indexOf(aView); + if (index != -1) { + this._views.splice(index, 1); + } + }, + + /** + * Ensures that the currently loaded data is added to the specified view. + * + * @param aView + * DownloadsView object to be initialized. + */ + _updateView: function(aView) + { + // Indicate to the view that a batch loading operation is in progress. + aView.onDataLoadStarting(); + + // Sort backwards by start time, ensuring that the most recent + // downloads are added first regardless of their state. + // Tycho: + //let loadedItemsArray = [dataItem + // for each (dataItem in this.dataItems) + // if (dataItem)]; + let downloadsArray = [...this.downloads]; + downloadsArray.sort((a, b) => b.startTime - a.startTime); + downloadsArray.forEach(download => aView.onDownloadAdded(download, false)); + + // Notify the view that all data is available unless loading is in progress. + if (!this._pendingStatement) { + aView.onDataLoadCompleted(); + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// In-memory downloads data store + + /** + * Clears the loaded data. + */ + clear: function() + { + this._terminateDataAccess(); + this.dataItems = {}; + }, + + /** + * Returns the data item associated with the provided source object. The + * source can be a download object that we received from the Download Manager + * because of a real-time notification, or a row from the downloads database, + * during the asynchronous data load. + * + * In case we receive download status notifications while we are still + * populating the list of downloads from the database, we want the real-time + * status to take precedence over the state that is read from the database, + * which might be older. This is achieved by creating the download item if + * it's not already in the list, but never updating the returned object using + * the data from the database, if the object already exists. + * + * @param aSource + * Object containing the data with which the item should be initialized + * if it doesn't already exist in the list. This should implement + * either nsIDownload or mozIStorageRow. If the item exists, this + * argument is only used to retrieve the download identifier. + * @param aMayReuseGUID + * If false, indicates that the download should not be added if a + * download with the same identifier was removed in the meantime. This + * ensures that, while loading the list asynchronously, downloads that + * have been removed in the meantime do no reappear inadvertently. + * + * @return New or existing data item, or null if the item was deleted from the + * list of available downloads. + */ + _getOrAddDataItem: function(aSource, aMayReuseGUID) + { + let downloadGuid = (aSource instanceof Ci.nsIDownload) + ? aSource.guid + : aSource.getResultByName("guid"); + if (downloadGuid in this.dataItems) { + let existingItem = this.dataItems[downloadGuid]; + if (existingItem || !aMayReuseGUID) { + // Returns null if the download was removed and we can't reuse the item. + return existingItem; + } + } + DownloadsCommon.log("Creating a new DownloadsDataItem with downloadGuid =", + downloadGuid); + let dataItem = new DownloadsDataItem(aSource); + this.dataItems[downloadGuid] = dataItem; + + // Create the view items before returning. + let addToStartOfList = aSource instanceof Ci.nsIDownload; + this._views.forEach( + function(view) view.onDataItemAdded(dataItem, addToStartOfList) + ); + return dataItem; + }, + + /** + * Removes the data item with the specified identifier. + * + * This method can be called at most once per download identifier. + */ + _removeDataItem: function(aDownloadId) + { + if (aDownloadId in this.dataItems) { + let dataItem = this.dataItems[aDownloadId]; + this.dataItems[aDownloadId] = null; + this._views.forEach( + function(view) view.onDataItemRemoved(dataItem) + ); + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Persistent data loading + + /** + * Represents an executing statement, allowing its cancellation. + */ + _pendingStatement: null, + + /** + * Indicates which kind of items from the persistent downloads database have + * been fully loaded in memory and are available to the views. This can + * assume the value of one of the kLoad constants. + */ + _loadState: 0, + + /** No downloads have been fully loaded yet. */ + get kLoadNone() 0, + /** All the active downloads in the database are loaded in memory. */ + get kLoadActive() 1, + /** All the downloads in the database are loaded in memory. */ + get kLoadAll() 2, + + /** + * Reloads the specified kind of downloads from the persistent database. This + * method must only be called when Private Browsing Mode is disabled. + * + * @param aActiveOnly + * True to load only active downloads from the database. + */ + ensurePersistentDataLoaded: + function(aActiveOnly) + { + if (this == PrivateDownloadsData) { + Cu.reportError("ensurePersistentDataLoaded should not be called on PrivateDownloadsData"); + return; + } + + if (this._pendingStatement) { + // We are already in the process of reloading all downloads. + return; + } + + if (aActiveOnly) { + if (this._loadState == this.kLoadNone) { + DownloadsCommon.log("Loading only active downloads from the persistence database"); + // Indicate to the views that a batch loading operation is in progress. + this._views.forEach( + function(view) view.onDataLoadStarting() + ); + + // Reload the list using the Download Manager service. The list is + // returned in no particular order. + let downloads = Services.downloads.activeDownloads; + while (downloads.hasMoreElements()) { + let download = downloads.getNext().QueryInterface(Ci.nsIDownload); + this._getOrAddDataItem(download, true); + } + this._loadState = this.kLoadActive; + + // Indicate to the views that the batch loading operation is complete. + this._views.forEach( + function(view) view.onDataLoadCompleted() + ); + DownloadsCommon.log("Active downloads done loading."); + } + } else { + if (this._loadState != this.kLoadAll) { + // Load only the relevant columns from the downloads database. The + // columns are read in the _initFromDataRow method of DownloadsDataItem. + // Order by descending download identifier so that the most recent + // downloads are notified first to the listening views. + DownloadsCommon.log("Loading all downloads from the persistence database."); + let dbConnection = Services.downloads.DBConnection; + let statement = dbConnection.createAsyncStatement( + "SELECT guid, target, name, source, referrer, state, " + + "startTime, endTime, currBytes, maxBytes " + + "FROM moz_downloads " + + "ORDER BY startTime DESC" + ); + try { + this._pendingStatement = statement.executeAsync(this); + } finally { + statement.finalize(); + } + } + } + }, + + /** + * Cancels any pending data access and ensures views are notified. + */ + _terminateDataAccess: function() + { + if (this._pendingStatement) { + this._pendingStatement.cancel(); + this._pendingStatement = null; + } + + // Close all the views on the current data. Create a copy of the array + // because some views might unregister while processing this event. + Array.slice(this._views, 0).forEach( + function(view) view.onDataInvalidated() + ); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// mozIStorageStatementCallback + + handleResult: function(aResultSet) + { + for (let row = aResultSet.getNextRow(); + row; + row = aResultSet.getNextRow()) { + // Add the download to the list and initialize it with the data we read, + // unless we already received a notification providing more reliable + // information for this download. + this._getOrAddDataItem(row, false); + } + }, + + handleError: function(aError) + { + DownloadsCommon.error("Database statement execution error (", + aError.result, "): ", aError.message); + }, + + handleCompletion: function(aReason) + { + DownloadsCommon.log("Loading all downloads from database completed with reason:", + aReason); + this._pendingStatement = null; + + // To ensure that we don't inadvertently delete more downloads from the + // database than needed on shutdown, we should update the load state only if + // the operation completed successfully. + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + this._loadState = this.kLoadAll; + } + + // Indicate to the views that the batch loading operation is complete, even + // if the lookup failed or was canceled. The only possible glitch happens + // in case the database backend changes while loading data, when the views + // would open and immediately close. This case is rare enough not to need a + // special treatment. + this._views.forEach( + function(view) view.onDataLoadCompleted() + ); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIObserver + + observe: function(aSubject, aTopic, aData) + { + switch (aTopic) { + case "download-manager-remove-download-guid": + // If a single download was removed, remove the corresponding data item. + if (aSubject) { + let downloadGuid = aSubject.QueryInterface(Ci.nsISupportsCString).data; + DownloadsCommon.log("A single download with id", + downloadGuid, "was removed."); + this._removeDataItem(downloadGuid); + break; + } + + // Multiple downloads have been removed. Iterate over known downloads + // and remove those that don't exist anymore. + DownloadsCommon.log("Multiple downloads were removed."); + for each (let dataItem in this.dataItems) { + if (dataItem) { + // Bug 449811 - We have to bind to the dataItem because Javascript + // doesn't do fresh let-bindings per loop iteration. + let dataItemBinding = dataItem; + Services.downloads.getDownloadByGUID(dataItemBinding.downloadGuid, + function(aStatus, aResult) { + if (aStatus == Components.results.NS_ERROR_NOT_AVAILABLE) { + DownloadsCommon.log("Removing download with id", + dataItemBinding.downloadGuid); + this._removeDataItem(dataItemBinding.downloadGuid); + } + }.bind(this)); + } + } + break; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIDownloadProgressListener + + onDownloadStateChange: function(aOldState, aDownload) + { + if (aDownload.isPrivate != this._isPrivate) { + // Ignore the downloads with a privacy status other than what we are + // tracking. + return; + } + + // When a new download is added, it may have the same identifier of a + // download that we previously deleted during this session, and we also + // want to provide a visible indication that the download started. + let isNew = aOldState == nsIDM.DOWNLOAD_NOTSTARTED || + aOldState == nsIDM.DOWNLOAD_QUEUED; + + let dataItem = this._getOrAddDataItem(aDownload, isNew); + if (!dataItem) { + return; + } + + let wasInProgress = dataItem.inProgress; + + DownloadsCommon.log("A download changed its state to:", aDownload.state); + dataItem.state = aDownload.state; + dataItem.referrer = aDownload.referrer && aDownload.referrer.spec; + dataItem.resumable = aDownload.resumable; + dataItem.startTime = Math.round(aDownload.startTime / 1000); + dataItem.currBytes = aDownload.amountTransferred; + dataItem.maxBytes = aDownload.size; + + if (wasInProgress && !dataItem.inProgress) { + dataItem.endTime = Date.now(); + } + + // When a download is retried, we create a different download object from + // the database with the same ID as before. This means that the nsIDownload + // that the dataItem holds might now need updating. + // + // We only overwrite this in the event that _download exists, because if it + // doesn't, that means that no caller ever tried to get the nsIDownload, + // which means it was never retrieved and doesn't need to be overwritten. + if (dataItem._download) { + dataItem._download = aDownload; + } + + for (let view of this._views) { + try { + view.getViewItem(dataItem).onStateChange(aOldState); + } catch (ex) { + Cu.reportError(ex); + } + } + + if (isNew && !dataItem.newDownloadNotified) { + dataItem.newDownloadNotified = true; + this._notifyDownloadEvent("start"); + } + + // This is a final state of which we are only notified once. + if (dataItem.done) { + this._notifyDownloadEvent("finish"); + } + + // TODO Bug 830415: this isn't the right place to set these annotation. + // It should be set it in places' nsIDownloadHistory implementation. + if (!this._isPrivate && !dataItem.inProgress) { + let downloadMetaData = { state: dataItem.state, + endTime: dataItem.endTime }; + if (dataItem.done) + downloadMetaData.fileSize = dataItem.maxBytes; + + try { + PlacesUtils.annotations.setPageAnnotation( + NetUtil.newURI(dataItem.uri), "downloads/metaData", JSON.stringify(downloadMetaData), 0, + PlacesUtils.annotations.EXPIRE_WITH_HISTORY); + } + catch(ex) { + Cu.reportError(ex); + } + } + }, + + onProgressChange: function(aWebProgress, aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress, aDownload) + { + if (aDownload.isPrivate != this._isPrivate) { + // Ignore the downloads with a privacy status other than what we are + // tracking. + return; + } + + let dataItem = this._getOrAddDataItem(aDownload, false); + if (!dataItem) { + return; + } + + dataItem.currBytes = aDownload.amountTransferred; + dataItem.maxBytes = aDownload.size; + dataItem.speed = aDownload.speed; + dataItem.percentComplete = aDownload.percentComplete; + + this._views.forEach( + function(view) view.getViewItem(dataItem).onProgressChange() + ); + }, + + onStateChange: function() { }, + + onSecurityChange: function() { }, + + ////////////////////////////////////////////////////////////////////////////// + //// Notifications sent to the most recent browser window only + + /** + * Set to true after the first download causes the downloads panel to be + * displayed. + */ + get panelHasShownBefore() { + try { + return Services.prefs.getBoolPref("browser.download.panel.shown"); + } catch (ex) { } + return false; + }, + + set panelHasShownBefore(aValue) { + Services.prefs.setBoolPref("browser.download.panel.shown", aValue); + return aValue; + }, + + /** + * Displays a new or finished download notification in the most recent browser + * window, if one is currently available with the required privacy type. + * + * @param aType + * Set to "start" for new downloads, "finish" for completed downloads. + */ + _notifyDownloadEvent: function(aType) + { + DownloadsCommon.log("Attempting to notify that a new download has started or finished."); + if (DownloadsCommon.useToolkitUI) { + DownloadsCommon.log("Cancelling notification - we're using the toolkit downloads manager."); + return; + } + + // Show the panel in the most recent browser window, if present. + let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate }); + if (!browserWin) { + return; + } + + if (this.panelHasShownBefore) { + // For new downloads after the first one, don't show the panel + // automatically, but provide a visible notification in the topmost + // browser window, if the status indicator is already visible. + DownloadsCommon.log("Showing new download notification."); + browserWin.DownloadsIndicatorView.showEventNotification(aType); + return; + } + this.panelHasShownBefore = true; + browserWin.DownloadsPanel.showPanel(); + } +}; + +XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() { + return new DownloadsDataCtor(true); +}); + +XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() { + return new DownloadsDataCtor(false); +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsViewPrototype + +/** + * A prototype for an object that registers itself with DownloadsData as soon + * as a view is registered with it. + */ +const DownloadsViewPrototype = { + ////////////////////////////////////////////////////////////////////////////// + //// Registration of views + + /** + * Array of view objects that should be notified when the available status + * data changes. + * + * SUBCLASSES MUST OVERRIDE THIS PROPERTY. + */ + _views: null, + + /** + * Determines whether this view object is over the private or non-private + * downloads. + * + * SUBCLASSES MUST OVERRIDE THIS PROPERTY. + */ + _isPrivate: false, + + /** + * Adds an object to be notified when the available status data changes. + * The specified object is initialized with the currently available status. + * + * @param aView + * View object to be added. This reference must be + * passed to removeView before termination. + */ + addView: function(aView) + { + // Start receiving events when the first of our views is registered. + if (this._views.length == 0) { + if (this._isPrivate) { + PrivateDownloadsData.addView(this); + } else { + DownloadsData.addView(this); + } + } + + this._views.push(aView); + this.refreshView(aView); + }, + + /** + * Updates the properties of an object previously added using addView. + * + * @param aView + * View object to be updated. + */ + refreshView: function(aView) + { + // Update immediately even if we are still loading data asynchronously. + // Subclasses must provide these two functions! + this._refreshProperties(); + this._updateView(aView); + }, + + /** + * Removes an object previously added using addView. + * + * @param aView + * View object to be removed. + */ + removeView: function(aView) + { + let index = this._views.indexOf(aView); + if (index != -1) { + this._views.splice(index, 1); + } + + // Stop receiving events when the last of our views is unregistered. + if (this._views.length == 0) { + if (this._isPrivate) { + PrivateDownloadsData.removeView(this); + } else { + DownloadsData.removeView(this); + } + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsData + + /** + * Indicates whether we are still loading downloads data asynchronously. + */ + _loading: false, + + /** + * Called before multiple downloads are about to be loaded. + */ + onDataLoadStarting: function() + { + this._loading = true; + }, + + /** + * Called after data loading finished. + */ + onDataLoadCompleted: function() + { + this._loading = false; + }, + + /** + * Called when the downloads database becomes unavailable (for example, we + * entered Private Browsing Mode and the database backend changed). + * References to existing data should be discarded. + * + * @note Subclasses should override this. + */ + onDataInvalidated: function() + { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Called when a new download data item is available, either during the + * asynchronous data load or when a new download is started. + * + * @param download + * Download object that was just added. + * @param newest + * When true, indicates that this item is the most recent and should be + * added in the topmost position. This happens when a new download is + * started. When false, indicates that the item is the least recent + * with regard to the items that have been already added. The latter + * generally happens during the asynchronous data load. + * + * @note Subclasses should override this. + */ + onDownloadAdded(download, newest) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Called when the overall state of a Download has changed. In particular, + * this is called only once when the download succeeds or is blocked + * permanently, and is never called if only the current progress changed. + * + * The onDownloadChanged notification will always be sent afterwards. + * + * @note Subclasses should override this. + */ + onDownloadStateChanged(download) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Called every time any state property of a Download may have changed, + * including progress properties. + * + * Note that progress notification changes are throttled at the Downloads.jsm + * API level, and there is no throttling mechanism in the front-end. + * + * @note Subclasses should override this. + */ + onDownloadChanged(download) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Called when a data item is removed, ensures that the widget associated with + * the view item is removed from the user interface. + * + * @param download + * Download object that is being removed. + * + * @note Subclasses should override this. + */ + onDownloadRemoved(download) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Private function used to refresh the internal properties being sent to + * each registered view. + * + * @note Subclasses should override this. + */ + _refreshProperties: function() + { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Private function used to refresh an individual view. + * + * @note Subclasses should override this. + */ + _updateView: function() + { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsIndicatorData + +/** + * This object registers itself with DownloadsData as a view, and transforms the + * notifications it receives into overall status data, that is then broadcast to + * the registered download status indicators. + * + * Note that using this object does not automatically start the Download Manager + * service. Consumers will see an empty list of downloads until the service is + * actually started. This is useful to display a neutral progress indicator in + * the main browser window until the autostart timeout elapses. + */ +function DownloadsIndicatorDataCtor(aPrivate) { + this._isPrivate = aPrivate; + this._views = []; +} +DownloadsIndicatorDataCtor.prototype = { + __proto__: DownloadsViewPrototype, + + /** + * Removes an object previously added using addView. + * + * @param aView + * DownloadsIndicatorView object to be removed. + */ + removeView: function(aView) + { + DownloadsViewPrototype.removeView.call(this, aView); + + if (this._views.length == 0) { + this._itemCount = 0; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsData + + onDataLoadCompleted: function() + { + DownloadsViewPrototype.onDataLoadCompleted.call(this); + this._updateViews(); + }, + + /** + * Called when the downloads database becomes unavailable (for example, we + * entered Private Browsing Mode and the database backend changed). + * References to existing data should be discarded. + */ + onDataInvalidated: function() + { + this._itemCount = 0; + }, + + onDownloadAdded(download, newest) { + this._itemCount++; + this._updateViews(); + }, + + onDownloadStateChanged(download) { + if (download.succeeded || download.error) { + this.attention = true; + } + + // Since the state of a download changed, reset the estimated time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + }, + + onDownloadChanged(download) { + this._updateViews(); + }, + + onDownloadRemoved(download) { + this._itemCount--; + this._updateViews(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Propagation of properties to our views + + // The following properties are updated by _refreshProperties and are then + // propagated to the views. See _refreshProperties for details. + _hasDownloads: false, + _counter: "", + _percentComplete: -1, + _paused: false, + + /** + * Indicates whether the download indicators should be highlighted. + */ + set attention(aValue) + { + this._attention = aValue; + this._updateViews(); + return aValue; + }, + _attention: false, + + /** + * Indicates whether the user is interacting with downloads, thus the + * attention indication should not be shown even if requested. + */ + set attentionSuppressed(aValue) + { + this._attentionSuppressed = aValue; + this._attention = false; + this._updateViews(); + return aValue; + }, + _attentionSuppressed: false, + + /** + * Computes aggregate values and propagates the changes to our views. + */ + _updateViews: function() + { + // Do not update the status indicators during batch loads of download items. + if (this._loading) { + return; + } + + this._refreshProperties(); + this._views.forEach(this._updateView, this); + }, + + /** + * Updates the specified view with the current aggregate values. + * + * @param aView + * DownloadsIndicatorView object to be updated. + */ + _updateView: function(aView) + { + aView.hasDownloads = this._hasDownloads; + aView.counter = this._counter; + aView.percentComplete = this._percentComplete; + aView.paused = this._paused; + aView.attention = this._attention && !this._attentionSuppressed; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Property updating based on current download status + + /** + * Number of download items that are available to be displayed. + */ + _itemCount: 0, + + /** + * Floating point value indicating the last number of seconds estimated until + * the longest download will finish. We need to store this value so that we + * don't continuously apply smoothing if the actual download state has not + * changed. This is set to -1 if the previous value is unknown. + */ + _lastRawTimeLeft: -1, + + /** + * Last number of seconds estimated until all in-progress downloads with a + * known size and speed will finish. This value is stored to allow smoothing + * in case of small variations. This is set to -1 if the previous value is + * unknown. + */ + _lastTimeLeft: -1, + + /** + * A generator function for the Download objects this summary is currently + * interested in. This generator is passed off to summarizeDownloads in order + * to generate statistics about the downloads we care about - in this case, + * it's all active downloads. + */ + * _activeDownloads() { + let downloads = this._isPrivate ? PrivateDownloadsData.downloads + : DownloadsData.downloads; + for (let download of downloads) { + if (!download.stopped || (download.canceled && download.hasPartialData)) { + yield download; + } + } + }, + + /** + * Computes aggregate values based on the current state of downloads. + */ + _refreshProperties: function() + { + let summary = + DownloadsCommon.summarizeDownloads(this._activeDownloads()); + + // Determine if the indicator should be shown or get attention. + this._hasDownloads = (this._itemCount > 0); + + // If all downloads are paused, show the progress indicator as paused. + this._paused = summary.numActive > 0 && + summary.numActive == summary.numPaused; + + this._percentComplete = summary.percentComplete; + + // Display the estimated time left, if present. + if (summary.rawTimeLeft == -1) { + // There are no downloads with a known time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + this._counter = ""; + } else { + // Compute the new time left only if state actually changed. + if (this._lastRawTimeLeft != summary.rawTimeLeft) { + this._lastRawTimeLeft = summary.rawTimeLeft; + this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, + this._lastTimeLeft); + } + this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft); + } + } +}; + +XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() { + return new DownloadsIndicatorDataCtor(true); +}); + +XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() { + return new DownloadsIndicatorDataCtor(false); +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsSummaryData + +/** + * DownloadsSummaryData is a view for DownloadsData that produces a summary + * of all downloads after a certain exclusion point aNumToExclude. For example, + * if there were 5 downloads in progress, and a DownloadsSummaryData was + * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData + * would produce a summary of the last 2 downloads. + * + * @param aIsPrivate + * True if the browser window which owns the download button is a private + * window. + * @param aNumToExclude + * The number of items to exclude from the summary, starting from the + * top of the list. + */ +function DownloadsSummaryData(aIsPrivate, aNumToExclude) { + this._numToExclude = aNumToExclude; + // Since we can have multiple instances of DownloadsSummaryData, we + // override these values from the prototype so that each instance can be + // completely separated from one another. + this._loading = false; + + this._downloads = []; + + // Floating point value indicating the last number of seconds estimated until + // the longest download will finish. We need to store this value so that we + // don't continuously apply smoothing if the actual download state has not + // changed. This is set to -1 if the previous value is unknown. + this._lastRawTimeLeft = -1; + + // Last number of seconds estimated until all in-progress downloads with a + // known size and speed will finish. This value is stored to allow smoothing + // in case of small variations. This is set to -1 if the previous value is + // unknown. + this._lastTimeLeft = -1; + + // The following properties are updated by _refreshProperties and are then + // propagated to the views. + this._showingProgress = false; + this._details = ""; + this._description = ""; + this._numActive = 0; + this._percentComplete = -1; + + this._isPrivate = aIsPrivate; + this._views = []; +} + +DownloadsSummaryData.prototype = { + __proto__: DownloadsViewPrototype, + + /** + * Removes an object previously added using addView. + * + * @param aView + * DownloadsSummary view to be removed. + */ + removeView: function(aView) + { + DownloadsViewPrototype.removeView.call(this, aView); + + if (this._views.length == 0) { + // Clear out our collection of Download objects. If we ever have + // another view registered with us, this will get re-populated. + this._downloads = []; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsData - see the documentation in + //// DownloadsViewPrototype for more information on what these functions + //// are used for. + + onDataLoadCompleted: function() + { + DownloadsViewPrototype.onDataLoadCompleted.call(this); + this._updateViews(); + }, + + onDataInvalidated: function() + { + this._dataItems = []; + }, + + onDownloadAdded(download, newest) { + if (newest) { + this._downloads.unshift(download); + } else { + this._downloads.push(download); + } + + this._updateViews(); + }, + + onDownloadStateChanged() { + // Since the state of a download changed, reset the estimated time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + }, + + onDownloadChanged() { + this._updateViews(); + }, + + onDownloadRemoved(download) { + let itemIndex = this._downloads.indexOf(download); + this._downloads.splice(itemIndex, 1); + this._updateViews(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Propagation of properties to our views + + /** + * Computes aggregate values and propagates the changes to our views. + */ + _updateViews: function() + { + // Do not update the status indicators during batch loads of download items. + if (this._loading) { + return; + } + + this._refreshProperties(); + this._views.forEach(this._updateView, this); + }, + + /** + * Updates the specified view with the current aggregate values. + * + * @param aView + * DownloadsIndicatorView object to be updated. + */ + _updateView: function(aView) + { + aView.showingProgress = this._showingProgress; + aView.percentComplete = this._percentComplete; + aView.description = this._description; + aView.details = this._details; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Property updating based on current download status + + /** + * A generator function for the Download objects this summary is currently + * interested in. This generator is passed off to summarizeDownloads in order + * to generate statistics about the downloads we care about - in this case, + * it's the downloads in this._downloads after the first few to exclude, + * which was set when constructing this DownloadsSummaryData instance. + */ + * _downloadsForSummary() { + if (this._downloads.length > 0) { + for (let i = this._numToExclude; i < this._downloads.length; ++i) { + yield this._downloads[i]; + } + } + }, + + /** + * Computes aggregate values based on the current state of downloads. + */ + _refreshProperties: function() + { + // Pre-load summary with default values. + let summary = + DownloadsCommon.summarizeDownloads(this._downloadsForSummary()); + + this._description = DownloadsCommon.strings + .otherDownloads2(summary.numActive); + this._percentComplete = summary.percentComplete; + + // If all downloads are paused, show the progress indicator as paused. + this._showingProgress = summary.numDownloading > 0 || + summary.numPaused > 0; + + // Display the estimated time left, if present. + if (summary.rawTimeLeft == -1) { + // There are no downloads with a known time left. + this._lastRawTimeLeft = -1; + this._lastTimeLeft = -1; + this._details = ""; + } else { + // Compute the new time left only if state actually changed. + if (this._lastRawTimeLeft != summary.rawTimeLeft) { + this._lastRawTimeLeft = summary.rawTimeLeft; + this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, + this._lastTimeLeft); + } + [this._details] = DownloadUtils.getDownloadStatusNoRate( + summary.totalTransferred, summary.totalSize, summary.slowestSpeed, + this._lastTimeLeft); + } + } +} diff --git a/browser/components/downloads/DownloadsLogger.jsm b/browser/components/downloads/DownloadsLogger.jsm new file mode 100644 index 000000000..845f1c91f --- /dev/null +++ b/browser/components/downloads/DownloadsLogger.jsm @@ -0,0 +1,75 @@ +/* -*- Mode: js2; js2-basic-offset: 2; 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/. */ + +/** + * The contents of this file were copied almost entirely from + * toolkit/identity/LogUtils.jsm. Until we've got a more generalized logging + * mechanism for toolkit, I think this is going to be how we roll. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["DownloadsLogger"]; +const PREF_DEBUG = "browser.download.debug"; + +const Cu = Components.utils; +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +this.DownloadsLogger = { + _generateLogMessage: function(args) { + // create a string representation of a list of arbitrary things + let strings = []; + + for (let arg of args) { + if (typeof arg === 'string') { + strings.push(arg); + } else if (arg === undefined) { + strings.push('undefined'); + } else if (arg === null) { + strings.push('null'); + } else { + try { + strings.push(JSON.stringify(arg, null, 2)); + } catch(err) { + strings.push("<<something>>"); + } + } + }; + return 'Downloads: ' + strings.join(' '); + }, + + /** + * log() - utility function to print a list of arbitrary things + * + * Enable with about:config pref browser.download.debug + */ + log: function(...args) { + let output = this._generateLogMessage(args); + dump(output + "\n"); + + // Additionally, make the output visible in the Error Console + Services.console.logStringMessage(output); + }, + + /** + * reportError() - report an error through component utils as well as + * our log function + */ + reportError: function(...aArgs) { + // Report the error in the browser + let output = this._generateLogMessage(aArgs); + Cu.reportError(output); + dump("ERROR:" + output + "\n"); + for (let frame = Components.stack.caller; frame; frame = frame.caller) { + dump("\t" + frame + "\n"); + } + } + +}; diff --git a/browser/components/downloads/DownloadsStartup.js b/browser/components/downloads/DownloadsStartup.js new file mode 100644 index 000000000..363b9642c --- /dev/null +++ b/browser/components/downloads/DownloadsStartup.js @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This component listens to notifications for startup, shutdown and session + * restore, controlling which downloads should be loaded from the database. + * + * To avoid affecting startup performance, this component monitors the current + * session restore state, but defers the actual downloads data manipulation + * until the Download Manager service is loaded. + */ + +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup", + "@mozilla.org/browser/sessionstartup;1", + "nsISessionStartup"); + +const kObservedTopics = [ + "sessionstore-windows-restored", + "sessionstore-browser-state-restored", + "download-manager-initialized", + "download-manager-change-retention", + "last-pb-context-exited", + "browser-lastwindow-close-granted", + "quit-application", + "profile-change-teardown", +]; + +/** + * CID of our implementation of nsIDownloadManagerUI. + */ +const kDownloadsUICid = Components.ID("{4d99321e-d156-455b-81f7-e7aa2308134f}"); + +/** + * Contract ID of the service implementing nsIDownloadManagerUI. + */ +const kDownloadsUIContractId = "@mozilla.org/download-manager-ui;1"; + +/** + * CID of the JavaScript implementation of nsITransfer. + */ +const kTransferCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"); + +/** + * Contract ID of the service implementing nsITransfer. + */ +const kTransferContractId = "@mozilla.org/transfer;1"; + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsStartup + +function DownloadsStartup() { } + +DownloadsStartup.prototype = { + classID: Components.ID("{49507fe5-2cee-4824-b6a3-e999150ce9b8}"), + + _xpcom_factory: XPCOMUtils.generateSingletonFactory(DownloadsStartup), + + ////////////////////////////////////////////////////////////////////////////// + //// nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + ////////////////////////////////////////////////////////////////////////////// + //// nsIObserver + + observe: function(aSubject, aTopic, aData) + { + switch (aTopic) { + case "profile-after-change": + // Override Toolkit's nsIDownloadManagerUI implementation with our own. + // This must be done at application startup and not in the manifest to + // ensure that our implementation overrides the original one. + Components.manager.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(kDownloadsUICid, "", + kDownloadsUIContractId, null); + + Components.manager.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(kTransferCid, "", + kTransferContractId, null); + break; + + case "sessionstore-windows-restored": + case "sessionstore-browser-state-restored": + // Unless there is no saved session, there is a chance that we are + // starting up after a restart or a crash. We should check the disk + // database to see if there are completed downloads to recover and show + // in the panel, in addition to in-progress downloads. + if (gSessionStartup.sessionType != Ci.nsISessionStartup.NO_SESSION) { + this._restoringSession = true; + } + this._ensureDataLoaded(); + break; + + case "download-manager-initialized": + // Don't initialize the JavaScript data and user interface layer if we + // are initializing the Download Manager service during shutdown. + if (this._shuttingDown) { + break; + } + + // Start receiving events for active and new downloads before we return + // from this observer function. We can't defer the execution of this + // step, to ensure that we don't lose events raised in the meantime. + DownloadsCommon.initializeAllDataLinks( + aSubject.QueryInterface(Ci.nsIDownloadManager)); + + this._downloadsServiceInitialized = true; + + // Since this notification is generated during the getService call and + // we need to get the Download Manager service ourselves, we must post + // the handler on the event queue to be executed later. + Services.tm.mainThread.dispatch(this._ensureDataLoaded.bind(this), + Ci.nsIThread.DISPATCH_NORMAL); + break; + + case "download-manager-change-retention": + // If we're using the Downloads Panel, we override the retention + // preference to always retain downloads on completion. + if (!DownloadsCommon.useToolkitUI) { + aSubject.QueryInterface(Ci.nsISupportsPRInt32).data = 2; + } + break; + + case "browser-lastwindow-close-granted": + // When using the panel interface, downloads that are already completed + // should be removed when the last full browser window is closed. This + // event is invoked only if the application is not shutting down yet. + // If the Download Manager service is not initialized, we don't want to + // initialize it just to clean up completed downloads, because they can + // be present only in case there was a browser crash or restart. + if (this._downloadsServiceInitialized && + !DownloadsCommon.useToolkitUI) { + Services.downloads.cleanUp(); + } + break; + + case "last-pb-context-exited": + // Similar to the above notification, but for private downloads. + if (this._downloadsServiceInitialized && + !DownloadsCommon.useToolkitUI) { + Services.downloads.cleanUpPrivate(); + } + break; + + case "quit-application": + // When the application is shutting down, we must free all resources in + // addition to cleaning up completed downloads. If the Download Manager + // service is not initialized, we don't want to initialize it just to + // clean up completed downloads, because they can be present only in + // case there was a browser crash or restart. + this._shuttingDown = true; + if (!this._downloadsServiceInitialized) { + break; + } + + DownloadsCommon.terminateAllDataLinks(); + + // When using the panel interface, downloads that are already completed + // should be removed when quitting the application. + if (!DownloadsCommon.useToolkitUI && aData != "restart") { + this._cleanupOnShutdown = true; + } + break; + + case "profile-change-teardown": + // If we need to clean up, we must do it synchronously after all the + // "quit-application" listeners are invoked, so that the Download + // Manager service has a chance to pause or cancel in-progress downloads + // before we remove completed downloads from the list. Note that, since + // "quit-application" was invoked, we've already exited Private Browsing + // Mode, thus we are always working on the disk database. + if (this._cleanupOnShutdown) { + Services.downloads.cleanUp(); + } + + if (!DownloadsCommon.useToolkitUI) { + // If we got this far, that means that we finished our first session + // with the Downloads Panel without crashing. This means that we don't + // have to force displaying only active downloads on the next startup + // now. + this._firstSessionCompleted = true; + } + break; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Private + + /** + * Indicates whether we're restoring a previous session. This is used by + * _recoverAllDownloads to determine whether or not we should load and + * display all downloads data, or restrict it to only the active downloads. + */ + _restoringSession: false, + + /** + * Indicates whether the Download Manager service has been initialized. This + * flag is required because we want to avoid accessing the service immediately + * at browser startup. The service will start when the user first requests a + * download, or some time after browser startup. + */ + _downloadsServiceInitialized: false, + + /** + * True while we are processing the "quit-application" event, and later. + */ + _shuttingDown: false, + + /** + * True during shutdown if we need to remove completed downloads. + */ + _cleanupOnShutdown: false, + + /** + * True if we should display all downloads, as opposed to just active + * downloads. We decide to display all downloads if we're restoring a session, + * or if we're using the Downloads Panel anytime after the first session with + * it has completed. + */ + get _recoverAllDownloads() { + return this._restoringSession || + (!DownloadsCommon.useToolkitUI && this._firstSessionCompleted); + }, + + /** + * True if we've ever completed a session with the Downloads Panel enabled. + */ + get _firstSessionCompleted() { + return Services.prefs + .getBoolPref("browser.download.panel.firstSessionCompleted"); + }, + + set _firstSessionCompleted(aValue) { + Services.prefs.setBoolPref("browser.download.panel.firstSessionCompleted", + aValue); + return aValue; + }, + + /** + * Ensures that persistent download data is reloaded at the appropriate time. + */ + _ensureDataLoaded: function() + { + if (!this._downloadsServiceInitialized) { + return; + } + + // If the previous session has been already restored, then we ensure that + // all the downloads are loaded. Otherwise, we only ensure that the active + // downloads from the previous session are loaded. + DownloadsCommon.ensureAllPersistentDataLoaded(!this._recoverAllDownloads); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Module + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadsStartup]); diff --git a/browser/components/downloads/DownloadsTaskbar.jsm b/browser/components/downloads/DownloadsTaskbar.jsm new file mode 100644 index 000000000..e1b9f7a27 --- /dev/null +++ b/browser/components/downloads/DownloadsTaskbar.jsm @@ -0,0 +1,176 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Handles the download progress indicator in the taskbar. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DownloadsTaskbar", +]; + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gWinTaskbar", function () { + if (!("@mozilla.org/windows-taskbar;1" in Cc)) { + return null; + } + let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"] + .getService(Ci.nsIWinTaskbar); + return winTaskbar.available && winTaskbar; +}); + +XPCOMUtils.defineLazyGetter(this, "gMacTaskbarProgress", function () { + return ("@mozilla.org/widget/macdocksupport;1" in Cc) && + Cc["@mozilla.org/widget/macdocksupport;1"] + .getService(Ci.nsITaskbarProgress); +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsTaskbar + +/** + * Handles the download progress indicator in the taskbar. + */ +this.DownloadsTaskbar = { + /** + * Underlying DownloadSummary providing the aggregate download information, or + * null if the indicator has never been initialized. + */ + _summary: null, + + /** + * nsITaskbarProgress object to which download information is dispatched. + * This can be null if the indicator has never been initialized or if the + * indicator is currently hidden on Windows. + */ + _taskbarProgress: null, + + /** + * This method is called after a new browser window is opened, and ensures + * that the download progress indicator is displayed in the taskbar. + * + * On Windows, the indicator is attached to the first browser window that + * calls this method. When the window is closed, the indicator is moved to + * another browser window, if available, in no particular order. When there + * are no browser windows visible, the indicator is hidden. + * + * On Mac OS X, the indicator is initialized globally when this method is + * called for the first time. Subsequent calls have no effect. + * + * @param aBrowserWindow + * nsIDOMWindow object of the newly opened browser window to which the + * indicator may be attached. + */ + registerIndicator(aBrowserWindow) { + if (!this._taskbarProgress) { + if (gMacTaskbarProgress) { + // On Mac OS X, we have to register the global indicator only once. + this._taskbarProgress = gMacTaskbarProgress; + // Free the XPCOM reference on shutdown, to prevent detecting a leak. + Services.obs.addObserver(() => { + this._taskbarProgress = null; + gMacTaskbarProgress = null; + }, "quit-application-granted", false); + } else if (gWinTaskbar) { + // On Windows, the indicator is currently hidden because we have no + // previous browser window, thus we should attach the indicator now. + this._attachIndicator(aBrowserWindow); + } else { + // The taskbar indicator is not available on this platform. + return; + } + } + + // Ensure that the DownloadSummary object will be created asynchronously. + if (!this._summary) { + Downloads.getSummary(Downloads.ALL).then(summary => { + // In case the method is re-entered, we simply ignore redundant + // invocations of the callback, instead of keeping separate state. + if (this._summary) { + return; + } + this._summary = summary; + return this._summary.addView(this); + }).then(null, Cu.reportError); + } + }, + + /** + * On Windows, attaches the taskbar indicator to the specified browser window. + */ + _attachIndicator(aWindow) { + // Activate the indicator on the specified window. + let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow).docShell; + this._taskbarProgress = gWinTaskbar.getTaskbarProgress(docShell); + + // If the DownloadSummary object has already been created, we should update + // the state of the new indicator, otherwise it will be updated as soon as + // the DownloadSummary view is registered. + if (this._summary) { + this.onSummaryChanged(); + } + + aWindow.addEventListener("unload", () => { + // Locate another browser window, excluding the one being closed. + let browserWindow = RecentWindow.getMostRecentBrowserWindow(); + if (browserWindow) { + // Move the progress indicator to the other browser window. + this._attachIndicator(browserWindow); + } else { + // The last browser window has been closed. We remove the reference to + // the taskbar progress object so that the indicator will be registered + // again on the next browser window that is opened. + this._taskbarProgress = null; + } + }, false); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// DownloadSummary view + + onSummaryChanged() { + // If the last browser window has been closed, we have no indicator any more. + if (!this._taskbarProgress) { + return; + } + + if (this._summary.allHaveStopped || this._summary.progressTotalBytes == 0) { + this._taskbarProgress.setProgressState( + Ci.nsITaskbarProgress.STATE_NO_PROGRESS, 0, 0); + } else { + // For a brief moment before completion, some download components may + // report more transferred bytes than the total number of bytes. Thus, + // ensure that we never break the expectations of the progress indicator. + let progressCurrentBytes = Math.min(this._summary.progressTotalBytes, + this._summary.progressCurrentBytes); + this._taskbarProgress.setProgressState( + Ci.nsITaskbarProgress.STATE_NORMAL, + progressCurrentBytes, + this._summary.progressTotalBytes); + } + }, +}; diff --git a/browser/components/downloads/DownloadsUI.js b/browser/components/downloads/DownloadsUI.js new file mode 100644 index 000000000..e62bb8148 --- /dev/null +++ b/browser/components/downloads/DownloadsUI.js @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This component implements the nsIDownloadManagerUI interface and opens the + * downloads panel in the most recent browser window when requested. + * + * If a specific preference is set, this component transparently forwards all + * calls to the original implementation in Toolkit, that shows the window UI. + */ + +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "gBrowserGlue", + "@mozilla.org/browser/browserglue;1", + "nsIBrowserGlue"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsUI + +function DownloadsUI() +{ + XPCOMUtils.defineLazyGetter(this, "_toolkitUI", function() { + // Create Toolkit's nsIDownloadManagerUI implementation. + return Components.classesByID["{7dfdf0d1-aff6-4a34-bad1-d0fe74601642}"] + .getService(Ci.nsIDownloadManagerUI); + }); +} + +DownloadsUI.prototype = { + classID: Components.ID("{4d99321e-d156-455b-81f7-e7aa2308134f}"), + + _xpcom_factory: XPCOMUtils.generateSingletonFactory(DownloadsUI), + + ////////////////////////////////////////////////////////////////////////////// + //// nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]), + + ////////////////////////////////////////////////////////////////////////////// + //// nsIDownloadManagerUI + + show: function(aWindowContext, aDownload, aReason, aUsePrivateUI) + { + if (DownloadsCommon.useToolkitUI && !PrivateBrowsingUtils.isWindowPrivate(aWindowContext)) { + this._toolkitUI.show(aWindowContext, aDownload, aReason, aUsePrivateUI); + return; + } + + if (!aReason) { + aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED; + } + + if (aReason == Ci.nsIDownloadManagerUI.REASON_NEW_DOWNLOAD) { + const kMinimized = Ci.nsIDOMChromeWindow.STATE_MINIMIZED; + let browserWin = gBrowserGlue.getMostRecentBrowserWindow(); + + if (!browserWin || browserWin.windowState == kMinimized) { + this._showDownloadManagerUI(aWindowContext, aUsePrivateUI); + } + else { + // If the indicator is visible, then new download notifications are + // already handled by the panel service. + browserWin.DownloadsButton.checkIsVisible(function(isVisible) { + if (!isVisible) { + this._showDownloadManagerUI(aWindowContext, aUsePrivateUI); + } + }.bind(this)); + } + } else { + this._showDownloadManagerUI(aWindowContext, aUsePrivateUI); + } + }, + + get visible() + { + // If we're still using the toolkit downloads manager, delegate the call + // to it. Otherwise, return true for now, until we decide on how we want + // to indicate that a new download has started if a browser window is + // not available or minimized. + return DownloadsCommon.useToolkitUI ? this._toolkitUI.visible : true; + }, + + getAttention: function() + { + if (DownloadsCommon.useToolkitUI) { + this._toolkitUI.getAttention(); + } + }, + + /** + * Helper function that opens the download manager UI. + */ + _showDownloadManagerUI: + function(aWindowContext, aUsePrivateUI) + { + // If we weren't given a window context, try to find a browser window + // to use as our parent - and if that doesn't work, error out and give up. + let parentWindow = aWindowContext; + if (!parentWindow) { + parentWindow = RecentWindow.getMostRecentBrowserWindow({ private: !!aUsePrivateUI }); + if (!parentWindow) { + Components.utils.reportError( + "Couldn't find a browser window to open the Places Downloads View " + + "from."); + return; + } + } + + // If window is private then show it in a tab. + if (PrivateBrowsingUtils.isWindowPrivate(parentWindow)) { + parentWindow.openUILinkIn("about:downloads", "tab"); + return; + } else { + let organizer = Services.wm.getMostRecentWindow("Places:Organizer"); + if (!organizer) { + parentWindow.openDialog("chrome://browser/content/places/places.xul", + "", "chrome,toolbar=yes,dialog=no,resizable", + "Downloads"); + } else { + organizer.PlacesOrganizer.selectLeftPaneQuery("Downloads"); + organizer.focus(); + } + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Module + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadsUI]); diff --git a/browser/components/downloads/DownloadsViewUI.jsm b/browser/components/downloads/DownloadsViewUI.jsm new file mode 100644 index 000000000..218befe06 --- /dev/null +++ b/browser/components/downloads/DownloadsViewUI.jsm @@ -0,0 +1,248 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This module is imported by code that uses the "download.xml" binding, and + * provides prototypes for objects that handle input and display information. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DownloadsViewUI", +]; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +this.DownloadsViewUI = {}; + +/** + * A download element shell is responsible for handling the commands and the + * displayed data for a single element that uses the "download.xml" binding. + * + * The information to display is obtained through the associated Download object + * from the JavaScript API for downloads, and commands are executed using a + * combination of Download methods and DownloadsCommon.jsm helper functions. + * + * Specialized versions of this shell must be defined, and they are required to + * implement the "download" property or getter. Currently these objects are the + * HistoryDownloadElementShell and the DownloadsViewItem for the panel. The + * history view may use a HistoryDownload object in place of a Download object. + */ +this.DownloadsViewUI.DownloadElementShell = function () {} + +this.DownloadsViewUI.DownloadElementShell.prototype = { + /** + * The richlistitem for the download, initialized by the derived object. + */ + element: null, + + /** + * URI string for the file type icon displayed in the download element. + */ + get image() { + if (!this.download.target.path) { + // Old history downloads may not have a target path. + return "moz-icon://.unknown?size=32"; + } + + // When a download that was previously in progress finishes successfully, it + // means that the target file now exists and we can extract its specific + // icon, for example from a Windows executable. To ensure that the icon is + // reloaded, however, we must change the URI used by the XUL image element, + // for example by adding a query parameter. This only works if we add one of + // the parameters explicitly supported by the nsIMozIconURI interface. + return "moz-icon://" + this.download.target.path + "?size=32" + + (this.download.succeeded ? "&state=normal" : ""); + }, + + /** + * The user-facing label for the download. This is normally the leaf name of + * the download target file. In case this is a very old history download for + * which the target file is unknown, the download source URI is displayed. + */ + get displayName() { + if (!this.download.target.path) { + return this.download.source.url; + } + return OS.Path.basename(this.download.target.path); + }, + + get extendedDisplayName() { + let s = DownloadsCommon.strings; + let displayHost = DownloadUtils.getURIHost(this.download.source.url); + return s.statusSeparator(this.displayName, displayHost); + }, + + get extendedDisplayNameTip() { + let s = DownloadsCommon.strings; + let fullHost = DownloadUtils.getURIHost(this.download.source.url); + let referrer = this.download.source.referrer; + if (referrer) { + fullHost += ' (' + DownloadUtils.getURIHost(referrer) + ')'; + } + return s.statusSeparator(this.displayName, fullHost); + }, + + /** + * The progress element for the download, or undefined in case the XBL binding + * has not been applied yet. + */ + get _progressElement() { + if (!this.__progressElement) { + // If the element is not available now, we will try again the next time. + this.__progressElement = + this.element.ownerDocument.getAnonymousElementByAttribute( + this.element, "anonid", + "progressmeter"); + } + return this.__progressElement; + }, + + /** + * Processes a major state change in the user interface, then proceeds with + * the normal progress update. This function is not called for every progress + * update in order to improve performance. + */ + _updateState() { + this.element.setAttribute("displayName", this.displayName); + this.element.setAttribute("extendedDisplayName", this.extendedDisplayName); + this.element.setAttribute("extendedDisplayNameTip", this.extendedDisplayNameTip); + this.element.setAttribute("image", this.image); + this.element.setAttribute("state", + DownloadsCommon.stateOfDownload(this.download)); + + // Since state changed, reset the time left estimation. + this.lastEstimatedSecondsLeft = Infinity; + + this._updateProgress(); + }, + + /** + * Updates the elements that change regularly for in-progress downloads, + * namely the progress bar and the status line. + */ + _updateProgress() { + if (this.download.succeeded) { + // We only need to add or remove this attribute for succeeded downloads. + if (this.download.target.exists) { + this.element.setAttribute("exists", "true"); + } else { + this.element.removeAttribute("exists"); + } + } + + // The progress bar is only displayed for in-progress downloads. + if (this.download.hasProgress) { + this.element.setAttribute("progressmode", "normal"); + this.element.setAttribute("progress", this.download.progress); + } else { + this.element.setAttribute("progressmode", "undetermined"); + } + + // Dispatch the ValueChange event for accessibility, if possible. + if (this._progressElement) { + let event = this.element.ownerDocument.createEvent("Events"); + event.initEvent("ValueChange", true, true); + this._progressElement.dispatchEvent(event); + } + + let status = this.statusTextAndTip; + this.element.setAttribute("status", status.text); + this.element.setAttribute("statusTip", status.tip); + }, + + lastEstimatedSecondsLeft: Infinity, + + /** + * Returns the text for the status line and the associated tooltip. These are + * returned by a single property because they are computed together. The + * result may be overridden by derived objects. + */ + get statusTextAndTip() this.rawStatusTextAndTip, + + /** + * Derived objects may call this to get the status text. + */ + get rawStatusTextAndTip() { + const nsIDM = Ci.nsIDownloadManager; + let s = DownloadsCommon.strings; + + let text = ""; + let tip = ""; + + if (!this.download.stopped) { + let totalBytes = this.download.hasProgress ? this.download.totalBytes + : -1; + // By default, extended status information including the individual + // download rate is displayed in the tooltip. The history view overrides + // the getter and displays the datails in the main area instead. + [text] = DownloadUtils.getDownloadStatusNoRate( + this.download.currentBytes, + totalBytes, + this.download.speed, + this.lastEstimatedSecondsLeft); + let newEstimatedSecondsLeft; + [tip, newEstimatedSecondsLeft] = DownloadUtils.getDownloadStatus( + this.download.currentBytes, + totalBytes, + this.download.speed, + this.lastEstimatedSecondsLeft); + this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft; + } else if (this.download.canceled && this.download.hasPartialData) { + let totalBytes = this.download.hasProgress ? this.download.totalBytes + : -1; + let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes, + totalBytes); + + // We use the same XUL label to display both the state and the amount + // transferred, for example "Paused - 1.1 MB". + text = s.statusSeparatorBeforeNumber(s.statePaused, transfer); + } else if (!this.download.succeeded && !this.download.canceled && + !this.download.error) { + text = s.stateStarting; + } else { + let stateLabel; + + if (this.download.succeeded) { + // For completed downloads, show the file size (e.g. "1.5 MB"). + if (this.download.target.size !== undefined) { + let [size, unit] = + DownloadUtils.convertByteUnits(this.download.target.size); + stateLabel = s.sizeWithUnits(size, unit); + } else { + // History downloads may not have a size defined. + stateLabel = s.sizeUnknown; + } + } else if (this.download.canceled) { + stateLabel = s.stateCanceled; + } else if (this.download.error.becauseBlockedByParentalControls) { + stateLabel = s.stateBlockedParentalControls; + } else { + stateLabel = s.stateFailed; + } + + let referrer = this.download.source.referrer || this.download.source.url; + let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer); + + let date = new Date(this.download.endTime); + let [displayDate, fullDate] = DownloadUtils.getReadableDates(date); + + let firstPart = s.statusSeparator(stateLabel, displayHost); + text = s.statusSeparator(firstPart, displayDate); + tip = s.statusSeparator(fullHost, fullDate); + } + + return { text, tip: tip || text }; + }, +}; diff --git a/browser/components/downloads/content/allDownloadsViewOverlay.css b/browser/components/downloads/content/allDownloadsViewOverlay.css new file mode 100644 index 000000000..c062ae464 --- /dev/null +++ b/browser/components/downloads/content/allDownloadsViewOverlay.css @@ -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/. */ + +/** + * The downloads richlistbox may list thousands of items, and it turns out + * XBL binding attachment, and even more so detachment, is a performance hog. + * This hack makes sure we don't apply any binding to inactive items (inactive + * items are history downloads that haven't been in the visible area). + * We can do this because the richlistbox implementation does not interact + * much with the richlistitem binding. However, this may turn out to have + * some side effects (see bug 828111 for the details). + * + * We might be able to do away with this workaround once bug 653881 is fixed. + */ +richlistitem.download { + -moz-binding: none; +} + +richlistitem.download[active] { + -moz-binding: url('chrome://browser/content/downloads/download.xml#download-full-ui'); +} + +richlistitem.download[active]:-moz-any([state="-1"],/* Starting (initial) */ + [state="0"], /* Downloading */ + [state="4"], /* Paused */ + [state="5"], /* Starting (queued) */ + [state="7"]) /* Scanning */ +{ + -moz-binding: url('chrome://browser/content/downloads/download.xml#download-in-progress-full-ui'); +} + +.download-state:not( [state="0"] /* Downloading */) + .downloadPauseMenuItem, +.download-state:not( [state="4"] /* Paused */) + .downloadResumeMenuItem, +.download-state:not(:-moz-any([state="2"], /* Failed */ + [state="4"]) /* Paused */) + .downloadCancelMenuItem, +.download-state[state]:not(:-moz-any([state="1"], /* Finished */ + [state="2"], /* Failed */ + [state="3"], /* Canceled */ + [state="6"], /* Blocked (parental) */ + [state="8"], /* Blocked (dirty) */ + [state="9"]) /* Blocked (policy) */) + .downloadRemoveFromHistoryMenuItem, +.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ + [state="0"], /* Downloading */ + [state="1"], /* Finished */ + [state="4"], /* Paused */ + [state="5"]) /* Starting (queued) */) + .downloadShowMenuItem, +.download-state[state="7"] /* Scanning */ .downloadCommandsSeparator +{ + display: none; +} diff --git a/browser/components/downloads/content/allDownloadsViewOverlay.js b/browser/components/downloads/content/allDownloadsViewOverlay.js new file mode 100644 index 000000000..58f0642df --- /dev/null +++ b/browser/components/downloads/content/allDownloadsViewOverlay.js @@ -0,0 +1,1397 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI", + "resource:///modules/DownloadsViewUI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const nsIDM = Ci.nsIDownloadManager; + +const DESTINATION_FILE_URI_ANNO = "downloads/destinationFileURI"; +const DOWNLOAD_META_DATA_ANNO = "downloads/metaData"; + +const DOWNLOAD_VIEW_SUPPORTED_COMMANDS = + ["cmd_delete", "cmd_copy", "cmd_paste", "cmd_selectAll", + "downloadsCmd_pauseResume", "downloadsCmd_cancel", + "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry", + "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"]; + +/** + * Represents a download from the browser history. It implements part of the + * interface of the Download object. + * + * @param aPlacesNode + * The Places node from which the history download should be initialized. + */ +function HistoryDownload(aPlacesNode) { + // TODO (bug 829201): history downloads should get the referrer from Places. + this.source = { + url: aPlacesNode.uri, + }; + this.target = { + path: undefined, + exists: false, + size: undefined, + }; + + // In case this download cannot obtain its end time from the Places metadata, + // use the time from the Places node, that is the start time of the download. + this.endTime = aPlacesNode.time / 1000; +} + +HistoryDownload.prototype = { + /** + * Pushes information from Places metadata into this object. + */ + updateFromMetaData(metaData) { + try { + this.target.path = Cc["@mozilla.org/network/protocol;1?name=file"] + .getService(Ci.nsIFileProtocolHandler) + .getFileFromURLSpec(metaData.targetFileSpec).path; + } catch (ex) { + this.target.path = undefined; + } + + if ("state" in metaData) { + this.succeeded = metaData.state == nsIDM.DOWNLOAD_FINISHED; + this.error = metaData.state == nsIDM.DOWNLOAD_FAILED + ? { message: "History download failed." } + : metaData.state == nsIDM.DOWNLOAD_BLOCKED_PARENTAL + ? { becauseBlockedByParentalControls: true } + : null; + this.canceled = metaData.state == nsIDM.DOWNLOAD_CANCELED || + metaData.state == nsIDM.DOWNLOAD_PAUSED; + this.endTime = metaData.endTime; + + // Normal history downloads are assumed to exist until the user interface + // is refreshed, at which point these values may be updated. + this.target.exists = true; + this.target.size = metaData.fileSize; + } else { + // Metadata might be missing from a download that has started but hasn't + // stopped already. Normally, this state is overridden with the one from + // the corresponding in-progress session download. But if the browser is + // terminated abruptly and additionally the file with information about + // in-progress downloads is lost, we may end up using this state. We use + // the failed state to allow the download to be restarted. + // + // On the other hand, if the download is missing the target file + // annotation as well, it is just a very old one, and we can assume it + // succeeded. + this.succeeded = !this.target.path; + this.error = this.target.path ? { message: "Unstarted download." } : null; + this.canceled = false; + + // These properties may be updated if the user interface is refreshed. + this.target.exists = false; + this.target.size = undefined; + } + }, + + /** + * History downloads are never in progress. + */ + stopped: true, + + /** + * No percentage indication is shown for history downloads. + */ + hasProgress: false, + + /** + * History downloads cannot be restarted using their partial data, even if + * they are indicated as paused in their Places metadata. The only way is to + * use the information from a persisted session download, that will be shown + * instead of the history download. In case this session download is not + * available, we show the history download as canceled, not paused. + */ + hasPartialData: false, + + /** + * This method mimicks the "start" method of session downloads, and is called + * when the user retries a history download. + * + * At present, we always ask the user for a new target path when retrying a + * history download. In the future we may consider reusing the known target + * path if the folder still exists and the file name is not already used, + * except when the user preferences indicate that the target path should be + * requested every time a new download is started. + */ + start() { + let browserWin = RecentWindow.getMostRecentBrowserWindow(); + let initiatingDoc = browserWin ? browserWin.document : document; + + // Do not suggest a file name if we don't know the original target. + let leafName = this.target.path ? OS.Path.basename(this.target.path) : null; + DownloadURL(this.source.url, leafName, initiatingDoc); + + return Promise.resolve(); + }, + + /** + * This method mimicks the "refresh" method of session downloads, except that + * it cannot notify that the data changed to the Downloads View. + */ + refresh: Task.async(function* () { + try { + this.target.size = (yield OS.File.stat(this.target.path)).size; + this.target.exists = true; + } catch (ex) { + // We keep the known file size from the metadata, if any. + this.target.exists = false; + } + }), +}; + +/** + * A download element shell is responsible for handling the commands and the + * displayed data for a single download view element. + * + * The shell may contain a session download, a history download, or both. When + * both a history and a session download are present, the session download gets + * priority and its information is displayed. + * + * On construction, a new richlistitem is created, and can be accessed through + * the |element| getter. The shell doesn't insert the item in a richlistbox, the + * caller must do it and remove the element when it's no longer needed. + * + * The caller is also responsible for forwarding status notifications for + * session downloads, calling the onStateChanged and onChanged methods. + * + * @param [optional] aSessionDownload + * The session download, required if aHistoryDownload is not set. + * @param [optional] aHistoryDownload + * The history download, required if aSessionDownload is not set. + */ +function HistoryDownloadElementShell(aSessionDownload, aHistoryDownload) { + this.element = document.createElement("richlistitem"); + this.element._shell = this; + + this.element.classList.add("download"); + this.element.classList.add("download-state"); + + if (aSessionDownload) { + this.sessionDownload = aSessionDownload; + } + if (aHistoryDownload) { + this.historyDownload = aHistoryDownload; + } +} + +HistoryDownloadElementShell.prototype = { + __proto__: DownloadsViewUI.DownloadElementShell.prototype, + + /** + * Manages the "active" state of the shell. By default all the shells without + * a session download are inactive, thus their UI is not updated. They must + * be activated when entering the visible area. Session downloads are always + * active. + */ + ensureActive: function() { + if (!this._active) { + this._active = true; + this.element.setAttribute("active", true); + this._updateUI(); + } + }, + get active() !!this._active, + + /** + * Overrides the base getter to return the Download or HistoryDownload object + * for displaying information and executing commands in the user interface. + */ + get download() this._sessionDownload || this._historyDownload, + + _sessionDownload: null, + get sessionDownload() this._sessionDownload, + set sessionDownload(aValue) { + if (this._sessionDownload != aValue) { + if (!aValue && !this._historyDownload) { + throw new Error("Should always have either a Download or a HistoryDownload"); + } + + this._sessionDownload = aValue; + + this.ensureActive(); + this._updateUI(); + } + return aValue; + }, + + _historyDownload: null, + get historyDownload() this._historyDownload, + set historyDownload(aValue) { + if (this._historyDownload != aValue) { + if (!aValue && !this._sessionDownload) { + throw new Error("Should always have either a Download or a HistoryDownload"); + } + + this._historyDownload = aValue; + + // We don't need to update the UI if we had a session data item, because + // the places information isn't used in this case. + if (!this._sessionDownload) { + this._updateUI(); + } + } + return aValue; + }, + + _updateUI() { + // There is nothing to do if the item has always been invisible. + if (!this.active) { + return; + } + + // Since the state changed, we may need to check the target file again. + this._targetFileChecked = false; + + this._updateState(); + }, + + get statusTextAndTip() { + let status = this.rawStatusTextAndTip; + + // The base object would show extended progress information in the tooltip, + // but we move this to the main view and never display a tooltip. + if (!this.download.stopped) { + status.text = status.tip; + } + status.tip = ""; + + return status; + }, + + onStateChanged() { + this.element.setAttribute("image", this.image); + this.element.setAttribute("state", + DownloadsCommon.stateOfDownload(this.download)); + + if (this.element.selected) { + goUpdateDownloadCommands(); + } else { + goUpdateCommand("downloadsCmd_clearDownloads"); + } + }, + + onChanged() { + this._updateProgress(); + }, + + /* nsIController */ + isCommandEnabled: function(aCommand) { + // The only valid command for inactive elements is cmd_delete. + if (!this.active && aCommand != "cmd_delete") + return false; + switch (aCommand) { + case "downloadsCmd_open": + // This property is false if the download did not succeed. + return this.download.target.exists; + case "downloadsCmd_show": + // TODO: Bug 827010 - Handle part-file asynchronously. + if (this._sessionDownload && this.download.target.partFilePath) { + let partFile = new FileUtils.File(this.download.target.partFilePath); + if (partFile.exists()) { + return true; + } + } + + // This property is false if the download did not succeed. + return this.download.target.exists; + case "downloadsCmd_pauseResume": + return this.download.hasPartialData && !this.download.error; + case "downloadsCmd_retry": + return this.download.canceled || this.download.error; + case "downloadsCmd_openReferrer": + return !!this.download.source.referrer; + case "cmd_delete": + // We don't want in-progress downloads to be removed accidentally. + return this.download.stopped; + case "downloadsCmd_cancel": + return !!this._sessionDownload; + } + return false; + }, + + /* nsIController */ + doCommand: function(aCommand) { + switch (aCommand) { + case "downloadsCmd_open": { + let file = new FileUtils.File(this.download.target.path); + DownloadsCommon.openDownloadedFile(file, null, window); + break; + } + case "downloadsCmd_show": { + let file = new FileUtils.File(this.download.target.path); + DownloadsCommon.showDownloadedFile(file); + break; + } + case "downloadsCmd_openReferrer": { + openURL(this.download.source.referrer); + break; + } + case "downloadsCmd_cancel": { + this.download.cancel().catch(() => {}); + this.download.removePartialData().catch(Cu.reportError); + break; + } + case "cmd_delete": { + if (this._sessionDownload) { + DownloadsCommon.removeAndFinalizeDownload(this.download); + } + if (this._historyDownload) { + let uri = NetUtil.newURI(this.download.source.url); + PlacesUtils.bhistory.removePage(uri); + } + break; + } + case "downloadsCmd_retry": { + // Errors when retrying are already reported as download failures. + this.download.start().catch(() => {}); + break; + } + case "downloadsCmd_pauseResume": { + // This command is only enabled for session downloads. + if (this.download.stopped) { + this.download.start(); + } else { + this.download.cancel(); + } + break; + } + } + }, + + // Returns whether or not the download handled by this shell should + // show up in the search results for the given term. Both the display + // name for the download and the url are searched. + matchesSearchTerm: function(aTerm) { + if (!aTerm) + return true; + aTerm = aTerm.toLowerCase(); + return this.displayName.toLowerCase().contains(aTerm) || + this.download.source.url.toLowerCase().contains(aTerm); + }, + + // Handles return keypress on the element (the keypress listener is + // set in the DownloadsPlacesView object). + doDefaultCommand: function() { + function getDefaultCommandForState(aState) { + switch (aState) { + case nsIDM.DOWNLOAD_FINISHED: + return "downloadsCmd_open"; + case nsIDM.DOWNLOAD_PAUSED: + return "downloadsCmd_pauseResume"; + case nsIDM.DOWNLOAD_NOTSTARTED: + case nsIDM.DOWNLOAD_QUEUED: + return "downloadsCmd_cancel"; + case nsIDM.DOWNLOAD_FAILED: + case nsIDM.DOWNLOAD_CANCELED: + return "downloadsCmd_retry"; + case nsIDM.DOWNLOAD_SCANNING: + return "downloadsCmd_show"; + case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: + case nsIDM.DOWNLOAD_DIRTY: + case nsIDM.DOWNLOAD_BLOCKED_POLICY: + return "downloadsCmd_openReferrer"; + } + return ""; + } + let state = DownloadsCommon.stateOfDownload(this.download); + let command = getDefaultCommandForState(state); + if (command && this.isCommandEnabled(command)) + this.doCommand(command); + }, + + /** + * This method is called by the outer download view, after the controller + * commands have already been updated. In case we did not check for the + * existence of the target file already, we can do it now and then update + * the commands as needed. + */ + onSelect: function() { + if (!this.active) + return; + + // If this is a history download for which no target file information is + // available, we cannot retrieve information about the target file. + if (!this.download.target.path) { + return; + } + + // Start checking for existence. This may be done twice if onSelect is + // called again before the information is collected. + if (!this._targetFileChecked) { + this._checkTargetFileOnSelect().catch(Cu.reportError); + } + }, + + _checkTargetFileOnSelect: Task.async(function* () { + try { + yield this.download.refresh(); + } finally { + // Do not try to check for existence again if this failed once. + this._targetFileChecked = true; + } + + // Update the commands only if the element is still selected. + if (this.element.selected) { + goUpdateDownloadCommands(); + } + + // Ensure the interface has been updated based on the new values. We need to + // do this because history downloads can't trigger update notifications. + this._updateProgress(); + }), +}; + +/** + * A Downloads Places View is a places view designed to show a places query + * for history downloads alongside the session downloads. + * + * As we don't use the places controller, some methods implemented by other + * places views are not implemented by this view. + * + * A richlistitem in this view can represent either a past download or a session + * download, or both. Session downloads are shown first in the view, and as long + * as they exist they "collapses" their history "counterpart" (So we don't show two + * items for every download). + */ +function DownloadsPlacesView(aRichListBox, aActive = true) { + this._richlistbox = aRichListBox; + this._richlistbox._placesView = this; + window.controllers.insertControllerAt(0, this); + + // Map download URLs to download element shells regardless of their type + this._downloadElementsShellsForURI = new Map(); + + // Map download data items to their element shells. + this._viewItemsForDownloads = new WeakMap(); + + // Points to the last session download element. We keep track of this + // in order to keep all session downloads above past downloads. + this._lastSessionDownloadElement = null; + + this._searchTerm = ""; + + this._active = aActive; + + // Register as a downloads view. The places data will be initialized by + // the places setter. + this._initiallySelectedElement = null; + this._downloadsData = DownloadsCommon.getData(window.opener || window); + this._downloadsData.addView(this); + + // Get the Download button out of the attention state since we're about to + // view all downloads. + DownloadsCommon.getIndicatorData(window).attention = false; + + // Make sure to unregister the view if the window is closed. + window.addEventListener("unload", function() { + window.controllers.removeController(this); + this._downloadsData.removeView(this); + this.result = null; + }.bind(this), true); + // Resizing the window may change items visibility. + window.addEventListener("resize", function() { + this._ensureVisibleElementsAreActive(); + }.bind(this), true); +} + +DownloadsPlacesView.prototype = { + get associatedElement() this._richlistbox, + + get active() this._active, + set active(val) { + this._active = val; + if (this._active) + this._ensureVisibleElementsAreActive(); + return this._active; + }, + + /** + * This cache exists in order to optimize the load of the Downloads View, when + * Places annotations for history downloads must be read. In fact, annotations + * are stored in a single table, and reading all of them at once is much more + * efficient than an individual query. + * + * When this property is first requested, it reads the annotations for all the + * history downloads and stores them indefinitely. + * + * The historical annotations are not expected to change for the duration of + * the session, except in the case where a session download is running for the + * same URI as a history download. To ensure we don't use stale data, URIs + * corresponding to session downloads are permanently removed from the cache. + * This is a very small mumber compared to history downloads. + * + * This property returns a Map from each download source URI found in Places + * annotations to an object with the format: + * + * { targetFileSpec, state, endTime, fileSize, ... } + * + * The targetFileSpec property is the value of "downloads/destinationFileURI", + * while the other properties are taken from "downloads/metaData". Any of the + * properties may be missing from the object. + */ + get _cachedPlacesMetaData() { + if (!this.__cachedPlacesMetaData) { + this.__cachedPlacesMetaData = new Map(); + + // Read the metadata annotations first, but ignore invalid JSON. + for (let result of PlacesUtils.annotations.getAnnotationsWithName( + DOWNLOAD_META_DATA_ANNO)) { + try { + this.__cachedPlacesMetaData.set(result.uri.spec, + JSON.parse(result.annotationValue)); + } catch (ex) {} + } + + // Add the target file annotations to the metadata. + for (let result of PlacesUtils.annotations.getAnnotationsWithName( + DESTINATION_FILE_URI_ANNO)) { + let metaData = this.__cachedPlacesMetaData.get(result.uri.spec); + if (!metaData) { + metaData = {}; + this.__cachedPlacesMetaData.set(result.uri.spec, metaData); + } + metaData.targetFileSpec = result.annotationValue; + } + } + + return this.__cachedPlacesMetaData; + }, + __cachedPlacesMetaData: null, + + /** + * Reads current metadata from Places annotations for the specified URI, and + * returns an object with the format: + * + * { targetFileSpec, state, endTime, fileSize, ... } + * + * The targetFileSpec property is the value of "downloads/destinationFileURI", + * while the other properties are taken from "downloads/metaData". Any of the + * properties may be missing from the object. + */ + _getPlacesMetaDataFor(spec) { + let metaData = {}; + + try { + let uri = NetUtil.newURI(spec); + try { + metaData = JSON.parse(PlacesUtils.annotations.getPageAnnotation( + uri, DOWNLOAD_META_DATA_ANNO)); + } catch (ex) {} + metaData.targetFileSpec = PlacesUtils.annotations.getPageAnnotation( + uri, DESTINATION_FILE_URI_ANNO); + } catch (ex) {} + + return metaData; + }, + + /** + * Given a data item for a session download, or a places node for a past + * download, updates the view as necessary. + * 1. If the given data is a places node, we check whether there are any + * elements for the same download url. If there are, then we just reset + * their places node. Otherwise we add a new download element. + * 2. If the given data is a data item, we first check if there's a history + * download in the list that is not associated with a data item. If we + * found one, we use it for the data item as well and reposition it + * alongside the other session downloads. If we don't, then we go ahead + * and create a new element for the download. + * + * @param [optional] sessionDownload + * A Download object, or null for history downloads. + * @param [optional] aPlacesNode + * The Places node for a history download, or null for session downloads. + * @param [optional] aNewest + * @see onDownloadAdded. Ignored for history downloads. + * @param [optional] aDocumentFragment + * To speed up the appending of multiple elements to the end of the + * list which are coming in a single batch (i.e. invalidateContainer), + * a document fragment may be passed to which the new elements would + * be appended. It's the caller's job to ensure the fragment is merged + * to the richlistbox at the end. + */ + _addDownloadData(sessionDownload, aPlacesNode, aNewest = false, + aDocumentFragment = null) { + let downloadURI = aPlacesNode ? aPlacesNode.uri + : sessionDownload.source.url; + let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI); + if (!shellsForURI) { + shellsForURI = new Set(); + this._downloadElementsShellsForURI.set(downloadURI, shellsForURI); + } + + // When a session download is attached to a shell, we ensure not to keep + // stale metadata around for the corresponding history download. This + // prevents stale state from being used if the view is rebuilt. + // + // Note that we will eagerly load the data in the cache at this point, even + // if we have seen no history download. The case where no history download + // will appear at all is rare enough in normal usage, so we can apply this + // simpler solution rather than keeping a list of cache items to ignore. + if (sessionDownload) { + this._cachedPlacesMetaData.delete(sessionDownload.source.url); + } + + let newOrUpdatedShell = null; + + // Trivial: if there are no shells for this download URI, we always + // need to create one. + let shouldCreateShell = shellsForURI.size == 0; + + // However, if we do have shells for this download uri, there are + // few options: + // 1) There's only one shell and it's for a history download (it has + // no data item). In this case, we update this shell and move it + // if necessary + // 2) There are multiple shells, indicating multiple downloads for + // the same download uri are running. In this case we create + // another shell for the download (so we have one shell for each data + // item). + // + // Note: If a cancelled session download is already in the list, and the + // download is retried, onDownloadAdded is called again for the same + // data item. Thus, we also check that we make sure we don't have a view item + // already. + if (!shouldCreateShell && + sessionDownload && !this._viewItemsForDownloads.has(sessionDownload)) { + // If there's a past-download-only shell for this download-uri with no + // associated data item, use it for the new data item. Otherwise, go ahead + // and create another shell. + shouldCreateShell = true; + for (let shell of shellsForURI) { + if (!shell.sessionDownload) { + shouldCreateShell = false; + shell.sessionDownload = sessionDownload; + newOrUpdatedShell = shell; + this._viewItemsForDownloads.set(sessionDownload, shell); + break; + } + } + } + + if (shouldCreateShell) { + // If we are adding a new history download here, it means there is no + // associated session download, thus we must read the Places metadata, + // because it will not be obscured by the session download. + let historyDownload = null; + if (aPlacesNode) { + let metaData = this._cachedPlacesMetaData.get(aPlacesNode.uri) || + this._getPlacesMetaDataFor(aPlacesNode.uri); + historyDownload = new HistoryDownload(aPlacesNode); + historyDownload.updateFromMetaData(metaData); + } + let shell = new HistoryDownloadElementShell(sessionDownload, + historyDownload); + shell.element._placesNode = aPlacesNode; + newOrUpdatedShell = shell; + shellsForURI.add(shell); + if (sessionDownload) { + this._viewItemsForDownloads.set(sessionDownload, shell); + } + } + else if (aPlacesNode) { + // We are updating information for a history download for which we have + // at least one download element shell already. There are two cases: + // 1) There are one or more download element shells for this source URI, + // each with an associated session download. We update the Places node + // because we may need it later, but we don't need to read the Places + // metadata until the last session download is removed. + // 2) Occasionally, we may receive a duplicate notification for a history + // download with no associated session download. We have exactly one + // download element shell in this case, but the metdata cannot have + // changed, just the reference to the Places node object is different. + // So, we update all the node references and keep the metadata intact. + for (let shell of shellsForURI) { + if (!shell.historyDownload) { + // Create the element to host the metadata when needed. + shell.historyDownload = new HistoryDownload(aPlacesNode); + } + shell.element._placesNode = aPlacesNode; + } + } + + if (newOrUpdatedShell) { + if (aNewest) { + this._richlistbox.insertBefore(newOrUpdatedShell.element, + this._richlistbox.firstChild); + if (!this._lastSessionDownloadElement) { + this._lastSessionDownloadElement = newOrUpdatedShell.element; + } + // Some operations like retrying an history download move an element to + // the top of the richlistbox, along with other session downloads. + // More generally, if a new download is added, should be made visible. + this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element); + } else if (sessionDownload) { + let before = this._lastSessionDownloadElement ? + this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild; + this._richlistbox.insertBefore(newOrUpdatedShell.element, before); + this._lastSessionDownloadElement = newOrUpdatedShell.element; + } + else { + let appendTo = aDocumentFragment || this._richlistbox; + appendTo.appendChild(newOrUpdatedShell.element); + } + + if (this.searchTerm) { + newOrUpdatedShell.element.hidden = + !newOrUpdatedShell.element._shell.matchesSearchTerm(this.searchTerm); + } + } + + // If aDocumentFragment is defined this is a batch change, so it's up to + // the caller to append the fragment and activate the visible shells. + if (!aDocumentFragment) { + this._ensureVisibleElementsAreActive(); + goUpdateCommand("downloadsCmd_clearDownloads"); + } + }, + + _removeElement: function(aElement) { + // If the element was selected exclusively, select its next + // sibling first, if not, try for previous sibling, if any. + if ((aElement.nextSibling || aElement.previousSibling) && + this._richlistbox.selectedItems && + this._richlistbox.selectedItems.length == 1 && + this._richlistbox.selectedItems[0] == aElement) { + this._richlistbox.selectItem(aElement.nextSibling || + aElement.previousSibling); + } + + if (this._lastSessionDownloadElement == aElement) + this._lastSessionDownloadElement = aElement.previousSibling; + + this._richlistbox.removeItemFromSelection(aElement); + this._richlistbox.removeChild(aElement); + this._ensureVisibleElementsAreActive(); + goUpdateCommand("downloadsCmd_clearDownloads"); + }, + + _removeHistoryDownloadFromView: + function(aPlacesNode) { + let downloadURI = aPlacesNode.uri; + let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI); + if (shellsForURI) { + for (let shell of shellsForURI) { + if (shell.sessionDownload) { + shell.historyDownload = null; + } + else { + this._removeElement(shell.element); + shellsForURI.delete(shell); + if (shellsForURI.size == 0) + this._downloadElementsShellsForURI.delete(downloadURI); + } + } + } + }, + + _removeSessionDownloadFromView(download) { + let shells = this._downloadElementsShellsForURI + .get(download.source.url); + if (shells.size == 0) + throw new Error("Should have had at leaat one shell for this uri"); + + let shell = this._viewItemsForDownloads.get(download); + if (!shells.has(shell)) + throw new Error("Missing download element shell in shells list for url"); + + // If there's more than one item for this download uri, we can let the + // view item for this this particular data item go away. + // If there's only one item for this download uri, we should only + // keep it if it is associated with a history download. + if (shells.size > 1 || !shell.historyDownload) { + this._removeElement(shell.element); + shells.delete(shell); + if (shells.size == 0) + this._downloadElementsShellsForURI.delete(download.source.url); + } + else { + // We have one download element shell containing both a session download + // and a history download, and we are now removing the session download. + // Previously, we did not use the Places metadata because it was obscured + // by the session download. Since this is no longer the case, we have to + // read the latest metadata before removing the session download. + let url = shell.historyDownload.source.url; + let metaData = this._getPlacesMetaDataFor(url); + shell.historyDownload.updateFromMetaData(metaData); + shell.sessionDownload = null; + // Move it below the session-download items; + if (this._lastSessionDownloadElement == shell.element) { + this._lastSessionDownloadElement = shell.element.previousSibling; + } + else { + let before = this._lastSessionDownloadElement ? + this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild; + this._richlistbox.insertBefore(shell.element, before); + } + } + }, + + _ensureVisibleElementsAreActive: + function() { + if (!this.active || this._ensureVisibleTimer || !this._richlistbox.firstChild) + return; + + this._ensureVisibleTimer = setTimeout(function() { + delete this._ensureVisibleTimer; + if (!this._richlistbox.firstChild) + return; + + let rlbRect = this._richlistbox.getBoundingClientRect(); + let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let nodes = winUtils.nodesFromRect(rlbRect.left, rlbRect.top, + 0, rlbRect.width, rlbRect.height, 0, + true, false); + // nodesFromRect returns nodes in z-index order, and for the same z-index + // sorts them in inverted DOM order, thus starting from the one that would + // be on top. + let firstVisibleNode, lastVisibleNode; + for (let node of nodes) { + if (node.localName === "richlistitem" && node._shell) { + node._shell.ensureActive(); + // The first visible node is the last match. + firstVisibleNode = node; + // While the last visible node is the first match. + if (!lastVisibleNode) + lastVisibleNode = node; + } + } + + // Also activate the first invisible nodes in both boundaries (that is, + // above and below the visible area) to ensure proper keyboard navigation + // in both directions. + let nodeBelowVisibleArea = lastVisibleNode && lastVisibleNode.nextSibling; + if (nodeBelowVisibleArea && nodeBelowVisibleArea._shell) + nodeBelowVisibleArea._shell.ensureActive(); + + let nodeABoveVisibleArea = + firstVisibleNode && firstVisibleNode.previousSibling; + if (nodeABoveVisibleArea && nodeABoveVisibleArea._shell) + nodeABoveVisibleArea._shell.ensureActive(); + }.bind(this), 10); + }, + + _place: "", + get place() this._place, + set place(val) { + // Don't reload everything if we don't have to. + if (this._place == val) { + // XXXmano: places.js relies on this behavior (see Bug 822203). + this.searchTerm = ""; + return val; + } + + this._place = val; + + let history = PlacesUtils.history; + let queries = { }, options = { }; + history.queryStringToQueries(val, queries, { }, options); + if (!queries.value.length) + queries.value = [history.getNewQuery()]; + + let result = history.executeQueries(queries.value, queries.value.length, + options.value); + result.addObserver(this, false); + return val; + }, + + _result: null, + get result() this._result, + set result(val) { + if (this._result == val) + return val; + + if (this._result) { + this._result.removeObserver(this); + this._resultNode.containerOpen = false; + } + + if (val) { + this._result = val; + this._resultNode = val.root; + this._resultNode.containerOpen = true; + this._ensureInitialSelection(); + } + else { + delete this._resultNode; + delete this._result; + } + + return val; + }, + + get selectedNodes() { + return [for (element of this._richlistbox.selectedItems) + if (element._placesNode) + element._placesNode]; + }, + + get selectedNode() { + let selectedNodes = this.selectedNodes; + return selectedNodes.length == 1 ? selectedNodes[0] : null; + }, + + get hasSelection() this.selectedNodes.length > 0, + + containerStateChanged: + function(aNode, aOldState, aNewState) { + this.invalidateContainer(aNode) + }, + + invalidateContainer: + function(aContainer) { + if (aContainer != this._resultNode) + throw new Error("Unexpected container node"); + if (!aContainer.containerOpen) + throw new Error("Root container for the downloads query cannot be closed"); + + let suppressOnSelect = this._richlistbox.suppressOnSelect; + this._richlistbox.suppressOnSelect = true; + try { + // Remove the invalidated history downloads from the list and unset the + // places node for data downloads. + // Loop backwards since _removeHistoryDownloadFromView may removeChild(). + for (let i = this._richlistbox.childNodes.length - 1; i >= 0; --i) { + let element = this._richlistbox.childNodes[i]; + if (element._placesNode) { + this._removeHistoryDownloadFromView(element._placesNode); + } + } + } + finally { + this._richlistbox.suppressOnSelect = suppressOnSelect; + } + + if (aContainer.childCount > 0) { + let elementsToAppendFragment = document.createDocumentFragment(); + for (let i = 0; i < aContainer.childCount; i++) { + try { + this._addDownloadData(null, aContainer.getChild(i), false, + elementsToAppendFragment); + } + catch(ex) { + Cu.reportError(ex); + } + } + + // _addDownloadData may not add new elements if there were already + // data items in place. + if (elementsToAppendFragment.firstChild) { + this._appendDownloadsFragment(elementsToAppendFragment); + this._ensureVisibleElementsAreActive(); + } + } + + goUpdateDownloadCommands(); + }, + + _appendDownloadsFragment: function(aDOMFragment) { + // Workaround multiple reflows hang by removing the richlistbox + // and adding it back when we're done. + + // Hack for bug 836283: reset xbl fields to their old values after the + // binding is reattached to avoid breaking the selection state + let xblFields = new Map(); + for (let [key, value] in Iterator(this._richlistbox)) { + xblFields.set(key, value); + } + + let parentNode = this._richlistbox.parentNode; + let nextSibling = this._richlistbox.nextSibling; + parentNode.removeChild(this._richlistbox); + this._richlistbox.appendChild(aDOMFragment); + parentNode.insertBefore(this._richlistbox, nextSibling); + + for (let [key, value] of xblFields) { + this._richlistbox[key] = value; + } + }, + + nodeInserted: function(aParent, aPlacesNode) { + this._addDownloadData(null, aPlacesNode); + }, + + nodeRemoved: function(aParent, aPlacesNode, aOldIndex) { + this._removeHistoryDownloadFromView(aPlacesNode); + }, + + nodeAnnotationChanged() {}, + nodeIconChanged() {}, + nodeTitleChanged() {}, + nodeKeywordChanged: function() {}, + nodeDateAddedChanged: function() {}, + nodeLastModifiedChanged: function() {}, + nodeHistoryDetailsChanged: function() {}, + nodeTagsChanged: function() {}, + sortingChanged: function() {}, + nodeMoved: function() {}, + nodeURIChanged: function() {}, + batching: function() {}, + + get controller() this._richlistbox.controller, + + get searchTerm() this._searchTerm, + set searchTerm(aValue) { + if (this._searchTerm != aValue) { + for (let element of this._richlistbox.childNodes) { + element.hidden = !element._shell.matchesSearchTerm(aValue); + } + this._ensureVisibleElementsAreActive(); + } + return this._searchTerm = aValue; + }, + + /** + * When the view loads, we want to select the first item. + * However, because session downloads, for which the data is loaded + * asynchronously, always come first in the list, and because the list + * may (or may not) already contain history downloads at that point, it + * turns out that by the time we can select the first item, the user may + * have already started using the view. + * To make things even more complicated, in other cases, the places data + * may be loaded after the session downloads data. Thus we cannot rely on + * the order in which the data comes in. + * We work around this by attempting to select the first element twice, + * once after the places data is loaded and once when the session downloads + * data is done loading. However, if the selection has changed in-between, + * we assume the user has already started using the view and give up. + */ + _ensureInitialSelection: function() { + // Either they're both null, or the selection has not changed in between. + if (this._richlistbox.selectedItem == this._initiallySelectedElement) { + let firstDownloadElement = this._richlistbox.firstChild; + if (firstDownloadElement != this._initiallySelectedElement) { + // We may be called before _ensureVisibleElementsAreActive, + // or before the download binding is attached. Therefore, ensure the + // first item is activated, and pass the item to the richlistbox + // setters only at a point we know for sure the binding is attached. + firstDownloadElement._shell.ensureActive(); + Services.tm.mainThread.dispatch(function() { + this._richlistbox.selectedItem = firstDownloadElement; + this._richlistbox.currentItem = firstDownloadElement; + this._initiallySelectedElement = firstDownloadElement; + }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); + } + } + }, + + onDataLoadStarting: function() { }, + onDataLoadCompleted: function() { + this._ensureInitialSelection(); + }, + + onDownloadAdded(download, newest) { + this._addDownloadData(download, null, newest); + }, + + onDownloadStateChanged(download) { + this._viewItemsForDownloads.get(download).onStateChanged(); + }, + + onDownloadChanged(download) { + this._viewItemsForDownloads.get(download).onChanged(); + }, + + onDownloadRemoved(download) { + this._removeSessionDownloadFromView(download); + }, + + supportsCommand: function(aCommand) { + if (DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1) { + // The clear-downloads command may be performed by the toolbar-button, + // which can be focused on OS X. Thus enable this command even if the + // richlistbox is not focused. + // For other commands, be prudent and disable them unless the richlistview + // is focused. It's important to make the decision here rather than in + // isCommandEnabled. Otherwise our controller may "steal" commands from + // other controls in the window (see goUpdateCommand & + // getControllerForCommand). + if (document.activeElement == this._richlistbox || + aCommand == "downloadsCmd_clearDownloads") { + return true; + } + } + return false; + }, + + isCommandEnabled: function(aCommand) { + switch (aCommand) { + case "cmd_copy": + return this._richlistbox.selectedItems.length > 0; + case "cmd_selectAll": + return true; + case "cmd_paste": + return this._canDownloadClipboardURL(); + case "downloadsCmd_clearDownloads": + return this._canClearDownloads(); + default: + return Array.every(this._richlistbox.selectedItems, function(element) { + return element._shell.isCommandEnabled(aCommand); + }); + } + }, + + _canClearDownloads: function() { + // Downloads can be cleared if there's at least one removable download in + // the list (either a history download or a completed session download). + // Because history downloads are always removable and are listed after the + // session downloads, check from bottom to top. + for (let elt = this._richlistbox.lastChild; elt; elt = elt.previousSibling) { + // Stopped, paused, and failed downloads with partial data are removed. + let download = elt._shell.download; + if (download.stopped && !(download.canceled && download.hasPartialData)) { + return true; + } + } + return false; + }, + + _copySelectedDownloadsToClipboard: + function() { + let urls = [for (element of this._richlistbox.selectedItems) + element._shell.download.source.url]; + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(urls.join("\n"), document); + }, + + _getURLFromClipboardData: function() { + let trans = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + trans.init(null); + + let flavors = ["text/x-moz-url", "text/unicode"]; + flavors.forEach(trans.addDataFlavor); + + Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); + + // Getting the data or creating the nsIURI might fail. + try { + let data = {}; + trans.getAnyTransferData({}, data, {}); + let [url, name] = data.value.QueryInterface(Ci.nsISupportsString) + .data.split("\n"); + if (url) + return [NetUtil.newURI(url, null, null).spec, name]; + } + catch(ex) { } + + return ["", ""]; + }, + + _canDownloadClipboardURL: function() { + let [url, name] = this._getURLFromClipboardData(); + return url != ""; + }, + + _downloadURLFromClipboard: function() { + let [url, name] = this._getURLFromClipboardData(); + let browserWin = RecentWindow.getMostRecentBrowserWindow(); + let initiatingDoc = browserWin ? browserWin.document : document; + DownloadURL(url, name, initiatingDoc); + }, + + doCommand: function(aCommand) { + // Commands may be invoked with keyboard shortcuts even if disabled. + if (!this.isCommandEnabled(aCommand)) { + return; + } + switch (aCommand) { + case "cmd_copy": + this._copySelectedDownloadsToClipboard(); + break; + case "cmd_selectAll": + this._richlistbox.selectAll(); + break; + case "cmd_paste": + this._downloadURLFromClipboard(); + break; + case "downloadsCmd_clearDownloads": + this._downloadsData.removeFinished(); + if (this.result) { + Cc["@mozilla.org/browser/download-history;1"] + .getService(Ci.nsIDownloadHistory) + .removeAllDownloads(); + } + // There may be no selection or focus change as a result + // of these change, and we want the command updated immediately. + goUpdateCommand("downloadsCmd_clearDownloads"); + break; + default: { + // Cloning the nodelist into an array to get a frozen list of selected items. + // Otherwise, the selectedItems nodelist is live and doCommand may alter the + // selection while we are trying to do one particular action, like removing + // items from history. + let selectedElements = [...this._richlistbox.selectedItems]; + for (let element of selectedElements) { + element._shell.doCommand(aCommand); + } + } + } + }, + + onEvent: function() { }, + + onContextMenu: function(aEvent) + { + let element = this._richlistbox.selectedItem; + if (!element || !element._shell) + return false; + + // Set the state attribute so that only the appropriate items are displayed. + let contextMenu = document.getElementById("downloadsContextMenu"); + let download = element._shell.download; + contextMenu.setAttribute("state", + DownloadsCommon.stateOfDownload(download)); + + if (!download.stopped) { + // The hasPartialData property of a download may change at any time after + // it has started, so ensure we update the related command now. + goUpdateCommand("downloadsCmd_pauseResume"); + } + return true; + }, + + onKeyPress: function(aEvent) { + let selectedElements = this._richlistbox.selectedItems; + if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { + // In the content tree, opening bookmarks by pressing return is only + // supported when a single item is selected. To be consistent, do the + // same here. + if (selectedElements.length == 1) { + let element = selectedElements[0]; + if (element._shell) + element._shell.doDefaultCommand(); + } + } + else if (aEvent.charCode == " ".charCodeAt(0)) { + // Pause/Resume every selected download + for (let element of selectedElements) { + if (element._shell.isCommandEnabled("downloadsCmd_pauseResume")) + element._shell.doCommand("downloadsCmd_pauseResume"); + } + } + }, + + onDoubleClick: function(aEvent) { + if (aEvent.button != 0) + return; + + let selectedElements = this._richlistbox.selectedItems; + if (selectedElements.length != 1) + return; + + let element = selectedElements[0]; + if (element._shell) + element._shell.doDefaultCommand(); + }, + + onScroll: function() { + this._ensureVisibleElementsAreActive(); + }, + + onSelect: function() { + goUpdateDownloadCommands(); + + let selectedElements = this._richlistbox.selectedItems; + for (let elt of selectedElements) { + if (elt._shell) + elt._shell.onSelect(); + } + }, + + onDragStart: function(aEvent) { + // TODO Bug 831358: Support d&d for multiple selection. + // For now, we just drag the first element. + let selectedItem = this._richlistbox.selectedItem; + if (!selectedItem) + return; + + let targetPath = selectedItem._shell.download.target.path; + if (!targetPath) { + return; + } + + // We must check for existence synchronously because this is a DOM event. + let file = new FileUtils.File(targetPath); + if (!file.exists()) + return; + + let dt = aEvent.dataTransfer; + dt.mozSetDataAt("application/x-moz-file", file, 0); + let url = Services.io.newFileURI(file).spec; + dt.setData("text/uri-list", url); + dt.setData("text/plain", url); + dt.effectAllowed = "copyMove"; + dt.addElement(selectedItem); + }, + + onDragOver: function(aEvent) { + let types = aEvent.dataTransfer.types; + if (types.contains("text/uri-list") || + types.contains("text/x-moz-url") || + types.contains("text/plain")) { + aEvent.preventDefault(); + } + }, + + onDrop: function(aEvent) { + let dt = aEvent.dataTransfer; + // If dragged item is from our source, do not try to + // redownload already downloaded file. + if (dt.mozGetDataAt("application/x-moz-file", 0)) + return; + + let links = Services.droppedLinkHandler.dropLinks(aEvent); + if (!links.length) + return; + let browserWin = RecentWindow.getMostRecentBrowserWindow(); + let initiatingDoc = browserWin ? browserWin.document : document; + for (let link of links) { + if (link.url.startsWith("about:")) + continue; + DownloadURL(link.url, link.name, initiatingDoc); + } + } +}; + +for (let methodName of ["load", "applyFilter", "selectNode", "selectItems"]) { + DownloadsPlacesView.prototype[methodName] = function() { + throw new Error("|" + methodName + "| is not implemented by the downloads view."); + } +} + +function goUpdateDownloadCommands() { + for (let command of DOWNLOAD_VIEW_SUPPORTED_COMMANDS) { + goUpdateCommand(command); + } +} diff --git a/browser/components/downloads/content/allDownloadsViewOverlay.xul b/browser/components/downloads/content/allDownloadsViewOverlay.xul new file mode 100644 index 000000000..3571adc5c --- /dev/null +++ b/browser/components/downloads/content/allDownloadsViewOverlay.xul @@ -0,0 +1,114 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://browser/content/downloads/allDownloadsViewOverlay.css"?> +<?xml-stylesheet href="chrome://browser/skin/downloads/allDownloadsViewOverlay.css"?> + +<!DOCTYPE overlay [ +<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> +%downloadsDTD; +]> + +<!-- This overlay provides a downloads view that lists both session downloads, + using the DownloadsView API, and history downloads, using places queries. + The view also implements a command controller and a context menu for + managing the downloads list. In order to use this view: + 1. Apply this overlay to your window. + 2. Insert in all the overlay entry-points, namely: + <richlistbox id="downloadsRichListBox"/> + <commandset id="downloadCommands"/> + <menupopup id="downloadsContextMenu"/> + 3. Make sure your window has the editMenuOverlay overlay applied, + because the view implements cmd_copy and cmd_delete. + 4. Make sure your window has the globalOverlay.js script loaded. + 5. To initialize the view + let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox")); + // This is what the Places Library uses. It could be tweaked a bit as long as the + // transition-type is set correctly + view.place = "place:transition=7&sort=4"; +--> +<overlay id="downloadsViewOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://browser/content/downloads/allDownloadsViewOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/contentAreaUtils.js"/> + + <richlistbox flex="1" + seltype="multiple" + id="downloadsRichListBox" context="downloadsContextMenu" + onscroll="return this._placesView.onScroll();" + onkeypress="return this._placesView.onKeyPress(event);" + ondblclick="return this._placesView.onDoubleClick(event);" + oncontextmenu="return this._placesView.onContextMenu(event);" + ondragstart="this._placesView.onDragStart(event);" + ondragover="this._placesView.onDragOver(event);" + ondrop="this._placesView.onDrop(event);" + onfocus="goUpdateDownloadCommands();" + onselect="this._placesView.onSelect();" + onblur="goUpdateDownloadCommands();"/> + + <commandset id="downloadCommands" + commandupdater="true" + events="focus,select,contextmenu" + oncommandupdate="goUpdateDownloadCommands();"> + <command id="downloadsCmd_pauseResume" + oncommand="goDoCommand('downloadsCmd_pauseResume')"/> + <command id="downloadsCmd_cancel" + oncommand="goDoCommand('downloadsCmd_cancel')"/> + <command id="downloadsCmd_open" + oncommand="goDoCommand('downloadsCmd_open')"/> + <command id="downloadsCmd_show" + oncommand="goDoCommand('downloadsCmd_show')"/> + <command id="downloadsCmd_retry" + oncommand="goDoCommand('downloadsCmd_retry')"/> + <command id="downloadsCmd_openReferrer" + oncommand="goDoCommand('downloadsCmd_openReferrer')"/> + <command id="downloadsCmd_clearDownloads" + oncommand="goDoCommand('downloadsCmd_clearDownloads')"/> + </commandset> + + <menupopup id="downloadsContextMenu" class="download-state"> + <menuitem command="downloadsCmd_pauseResume" + class="downloadPauseMenuItem" + label="&cmd.pause.label;" + accesskey="&cmd.pause.accesskey;"/> + <menuitem command="downloadsCmd_pauseResume" + class="downloadResumeMenuItem" + label="&cmd.resume.label;" + accesskey="&cmd.resume.accesskey;"/> + <menuitem command="downloadsCmd_cancel" + class="downloadCancelMenuItem" + label="&cmd.cancel.label;" + accesskey="&cmd.cancel.accesskey;"/> + <menuitem command="cmd_delete" + class="downloadRemoveFromHistoryMenuItem" + label="&cmd.removeFromHistory.label;" + accesskey="&cmd.removeFromHistory.accesskey;"/> + <menuitem command="downloadsCmd_show" + class="downloadShowMenuItem" + label="&cmd.show.label;" + accesskey="&cmd.show.accesskey;" + /> + + <menuseparator class="downloadCommandsSeparator"/> + + <menuitem command="downloadsCmd_openReferrer" + label="&cmd.goToDownloadPage.label;" + accesskey="&cmd.goToDownloadPage.accesskey;"/> + <menuitem command="cmd_copy" + label="&cmd.copyDownloadLink.label;" + accesskey="&cmd.copyDownloadLink.accesskey;"/> + + <menuseparator/> + + <menuitem command="downloadsCmd_clearDownloads" + label="&cmd.clearDownloads.label;" + accesskey="&cmd.clearDownloads.accesskey;"/> + </menupopup> +</overlay> diff --git a/browser/components/downloads/content/contentAreaDownloadsView.css b/browser/components/downloads/content/contentAreaDownloadsView.css new file mode 100644 index 000000000..abaae1f7b --- /dev/null +++ b/browser/components/downloads/content/contentAreaDownloadsView.css @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#downloadsListEmptyDescription { + display: none; +} + +#downloadsRichListBox:empty + #downloadsListEmptyDescription { + display: -moz-box; +} diff --git a/browser/components/downloads/content/contentAreaDownloadsView.js b/browser/components/downloads/content/contentAreaDownloadsView.js new file mode 100644 index 000000000..07bff3ef1 --- /dev/null +++ b/browser/components/downloads/content/contentAreaDownloadsView.js @@ -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/. */ + +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +var ContentAreaDownloadsView = { + init: function() { + let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox")); + // Do not display the Places downloads in private windows + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + view.place = "place:transition=7&sort=4"; + } + } +}; diff --git a/browser/components/downloads/content/contentAreaDownloadsView.xul b/browser/components/downloads/content/contentAreaDownloadsView.xul new file mode 100644 index 000000000..6fecaf2fd --- /dev/null +++ b/browser/components/downloads/content/contentAreaDownloadsView.xul @@ -0,0 +1,42 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/content/downloads/contentAreaDownloadsView.css"?> +<?xml-stylesheet href="chrome://browser/skin/downloads/contentAreaDownloadsView.css"?> + +<?xul-overlay href="chrome://browser/content/downloads/allDownloadsViewOverlay.xul"?> + +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> + +<!DOCTYPE window [ +<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> +%downloadsDTD; +]> + +<window id="contentAreaDownloadsView" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&downloads.title;" + onload="ContentAreaDownloadsView.init();"> + + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" + src="chrome://browser/content/downloads/contentAreaDownloadsView.js"/> + + <commandset id="editMenuCommands"/> + + <keyset id="editMenuKeys"> + </keyset> + + <stack flex="1"> + <richlistbox id="downloadsRichListBox"/> + <description id="downloadsListEmptyDescription" + value="&downloadsListEmpty.label;"/> + </stack> + <commandset id="downloadCommands"/> + <menupopup id="downloadsContextMenu"/> +</window> diff --git a/browser/components/downloads/content/download.css b/browser/components/downloads/content/download.css new file mode 100644 index 000000000..7412fa720 --- /dev/null +++ b/browser/components/downloads/content/download.css @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +richlistitem.download button { + /* These buttons should never get focus, as that would "disable" + the downloads view controller (it's only used when the richlistbox + is focused). */ + -moz-user-focus: none; +} + +/*** Visibility of controls inside download items ***/ + +.download-state:-moz-any( [state="6"], /* Blocked (parental) */ + [state="8"], /* Blocked (dirty) */ + [state="9"]) /* Blocked (policy) */ + > .downloadTypeIcon:not(.blockedIcon), + +.download-state:not(:-moz-any([state="6"], /* Blocked (parental) */ + [state="8"], /* Blocked (dirty) */ + [state="9"]) /* Blocked (policy) */) + > .downloadTypeIcon.blockedIcon, + +.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ + [state="5"], /* Starting (queued) */ + [state="0"], /* Downloading */ + [state="4"], /* Paused */ + [state="7"]) /* Scanning */) + > vbox > .downloadProgress, + +.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ + [state="5"], /* Starting (queued) */ + [state="0"], /* Downloading */ + [state="4"]) /* Paused */) + > .downloadCancel, + +.download-state[state]:not(:-moz-any([state="2"], /* Failed */ + [state="3"]) /* Canceled */) + > .downloadRetry, + +.download-state:not( [state="1"] /* Finished */) + > .downloadShow +{ + display: none; +} diff --git a/browser/components/downloads/content/download.xml b/browser/components/downloads/content/download.xml new file mode 100644 index 000000000..138c1eaf1 --- /dev/null +++ b/browser/components/downloads/content/download.xml @@ -0,0 +1,179 @@ +<?xml version="1.0"?> +<!-- -*- Mode: HTML; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- --> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> + +<bindings id="downloadBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="download" + extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content orient="horizontal" + align="center" + onclick="DownloadsView.onDownloadClick(event);"> + <xul:image class="downloadTypeIcon" + validate="always" + xbl:inherits="src=image"/> + <xul:image class="downloadTypeIcon blockedIcon"/> + <xul:vbox pack="center" + flex="1" + class="downloadContainer" + style="width: &downloadDetails.width;"> + <!-- We're letting localizers put a min-width in here primarily + because of the downloads summary at the bottom of the list of + download items. An element in the summary has the same min-width + on a description, and we don't want the panel to change size if the + summary isn't being displayed, so we ensure that items share the + same minimum width. + --> + <xul:description class="downloadDisplayName" + crop="center" + style="min-width: &downloadsSummary.minWidth2;" + xbl:inherits="value=displayName,tooltiptext=displayName"/> + <xul:progressmeter anonid="progressmeter" + class="downloadProgress" + min="0" + max="100" + xbl:inherits="mode=progressmode,value=progress"/> + <xul:description class="downloadDetails" + crop="end" + xbl:inherits="value=status,tooltiptext=statusTip"/> + </xul:vbox> + <xul:stack> + <xul:button class="downloadButton downloadCancel" + tooltiptext="&cmd.cancel.label;" + oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_cancel');"/> + <xul:button class="downloadButton downloadRetry" + tooltiptext="&cmd.retry.label;" + oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_retry');"/> + <xul:button class="downloadButton downloadShow" + tooltiptext="&cmd.show.label;" + oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_show');"/> + </xul:stack> + </content> + </binding> + + <binding id="download-in-progress" + extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content orient="horizontal" + align="center" + onclick="DownloadsView.onDownloadClick(event);"> + <xul:image class="downloadTypeIcon" + validate="always" + xbl:inherits="src=image"/> + <xul:image class="downloadTypeIcon blockedIcon"/> + <xul:vbox pack="center" + flex="1" + class="downloadContainer" + style="width: &downloadDetails.width;"> + <xul:description class="downloadDisplayName" + crop="center" + style="min-width: &downloadsSummary.minWidth2;" + xbl:inherits="value=displayName,tooltiptext=extendedDisplayNameTip"/> + <xul:progressmeter anonid="progressmeter" + class="downloadProgress" + min="0" + max="100" + xbl:inherits="mode=progressmode,value=progress"/> + <xul:description class="downloadDetails" + crop="end" + xbl:inherits="value=status,tooltiptext=statusTip"/> + </xul:vbox> + <xul:stack> + <xul:button class="downloadButton downloadCancel" + tooltiptext="&cmd.cancel.label;" + oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_cancel');"/> + <xul:button class="downloadButton downloadRetry" + tooltiptext="&cmd.retry.label;" + oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_retry');"/> + <xul:button class="downloadButton downloadShow" + tooltiptext="&cmd.show.label;" + oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_show');"/> + </xul:stack> + </content> + </binding> + + <binding id="download-full-ui" + extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <resources> + <stylesheet src="chrome://browser/content/downloads/download.css"/> + </resources> + + <content orient="horizontal" align="center"> + <xul:image class="downloadTypeIcon" + validate="always" + xbl:inherits="src=image"/> + <xul:image class="downloadTypeIcon blockedIcon"/> + <xul:vbox pack="center" flex="1"> + <xul:description class="downloadDisplayName" + crop="center" + xbl:inherits="value=displayName,tooltiptext=displayName"/> + <xul:progressmeter anonid="progressmeter" + class="downloadProgress" + min="0" + max="100" + xbl:inherits="mode=progressmode,value=progress"/> + <xul:description class="downloadDetails" + style="width: &downloadDetails.width;" + crop="end" + xbl:inherits="value=status,tooltiptext=statusTip"/> + </xul:vbox> + + <xul:button class="downloadButton downloadCancel" + tooltiptext="&cmd.cancel.label;" + oncommand="goDoCommand('downloadsCmd_cancel')"/> + <xul:button class="downloadButton downloadRetry" + tooltiptext="&cmd.retry.label;" + oncommand="goDoCommand('downloadsCmd_retry')"/> + <xul:button class="downloadButton downloadShow" + tooltiptext="&cmd.show.label;" + oncommand="goDoCommand('downloadsCmd_show')"/> + + </content> + </binding> + + <binding id="download-in-progress-full-ui" + extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <resources> + <stylesheet src="chrome://browser/content/downloads/download.css"/> + </resources> + + <content orient="horizontal" align="center"> + <xul:image class="downloadTypeIcon" + validate="always" + xbl:inherits="src=image"/> + <xul:image class="downloadTypeIcon blockedIcon"/> + <xul:vbox pack="center" flex="1"> + <xul:description class="downloadDisplayName" + crop="end" + xbl:inherits="value=extendedDisplayName,tooltiptext=extendedDisplayNameTip"/> + <xul:progressmeter anonid="progressmeter" + class="downloadProgress" + min="0" + max="100" + xbl:inherits="mode=progressmode,value=progress"/> + <xul:description class="downloadDetails" + style="width: &downloadDetails.width;" + crop="end" + xbl:inherits="value=status,tooltiptext=statusTip"/> + </xul:vbox> + + <xul:button class="downloadButton downloadCancel" + tooltiptext="&cmd.cancel.label;" + oncommand="goDoCommand('downloadsCmd_cancel')"/> + <xul:button class="downloadButton downloadRetry" + tooltiptext="&cmd.retry.label;" + oncommand="goDoCommand('downloadsCmd_retry')"/> + <xul:button class="downloadButton downloadShow" + tooltiptext="&cmd.show.label;" + oncommand="goDoCommand('downloadsCmd_show')"/> + + </content> + </binding> +</bindings> diff --git a/browser/components/downloads/content/downloads.css b/browser/components/downloads/content/downloads.css new file mode 100644 index 000000000..825db6834 --- /dev/null +++ b/browser/components/downloads/content/downloads.css @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/*** Download items ***/ + +richlistitem[type="download"] { + -moz-binding: url('chrome://browser/content/downloads/download.xml#download'); +} + +richlistitem[type="download"]:-moz-any([state="-1"],/* Starting (initial) */ + [state="0"], /* Downloading */ + [state="4"], /* Paused */ + [state="5"], /* Starting (queued) */ + [state="7"]) /* Scanning */ +{ + -moz-binding: url('chrome://browser/content/downloads/download.xml#download-in-progress'); +} + +richlistitem[type="download"]:not([selected]) button { + /* Only focus buttons in the selected item. */ + -moz-user-focus: none; +} + +/*** Visibility of controls inside download items ***/ + +.download-state:-moz-any( [state="6"], /* Blocked (parental) */ + [state="8"], /* Blocked (dirty) */ + [state="9"]) /* Blocked (policy) */ + .downloadTypeIcon:not(.blockedIcon), + +.download-state:not(:-moz-any([state="6"], /* Blocked (parental) */ + [state="8"], /* Blocked (dirty) */ + [state="9"]) /* Blocked (policy) */) + .downloadTypeIcon.blockedIcon, + +.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ + [state="0"], /* Downloading */ + [state="4"], /* Paused */ + [state="5"], /* Starting (queued) */ + [state="7"]) /* Scanning */) + .downloadProgress, + +.download-state:not( [state="0"] /* Downloading */) + .downloadPauseMenuItem, + +.download-state:not( [state="4"] /* Paused */) + .downloadResumeMenuItem, + +.download-state:not(:-moz-any([state="2"], /* Failed */ + [state="4"]) /* Paused */) + .downloadCancelMenuItem, + +.download-state:not(:-moz-any([state="1"], /* Finished */ + [state="2"], /* Failed */ + [state="3"], /* Canceled */ + [state="6"], /* Blocked (parental) */ + [state="8"], /* Blocked (dirty) */ + [state="9"]) /* Blocked (policy) */) + .downloadRemoveFromHistoryMenuItem, + +.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ + [state="0"], /* Downloading */ + [state="1"], /* Finished */ + [state="4"], /* Paused */ + [state="5"]) /* Starting (queued) */) + .downloadShowMenuItem, + +.download-state[state="7"] /* Scanning */ .downloadCommandsSeparator + +{ + display: none; +} + +/*** Visibility of download buttons and indicator controls. ***/ + +.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ + [state="0"], /* Downloading */ + [state="4"], /* Paused */ + [state="5"]) /* Starting (queued) */) + .downloadCancel, + +.download-state:not(:-moz-any([state="2"], /* Failed */ + [state="3"]) /* Canceled */) + .downloadRetry, + +.download-state:not( [state="1"] /* Finished */) + .downloadShow, + +#downloads-indicator:-moz-any([progress], + [counter], + [paused]) #downloads-indicator-icon, + +#downloads-indicator:not(:-moz-any([progress], + [counter], + [paused])) + #downloads-indicator-progress-area + +{ + visibility: hidden; +} + +.download-state[state="1"]:not([exists]) .downloadShow +{ + display: none; +} + +#downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryProgress, +#downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryDetails, +#downloadsFooter[showingsummary] > #downloadsHistory, +#downloadsFooter:not([showingsummary]) > #downloadsSummary +{ + display: none; +} + +/* Hacks for toolbar full and text modes, until bug 573329 removes them */ + +toolbar[mode="text"] > #downloads-indicator { + display: -moz-box; + -moz-box-orient: vertical; + -moz-box-pack: center; +} + +toolbar[mode="text"] > #downloads-indicator > .toolbarbutton-text { + -moz-box-ordinal-group: 1; +} + +toolbar[mode="text"] > #downloads-indicator > .toolbarbutton-icon { + display: -moz-box; + -moz-box-ordinal-group: 2; + visibility: collapse; +} diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js new file mode 100644 index 000000000..7a2ba9fee --- /dev/null +++ b/browser/components/downloads/content/downloads.js @@ -0,0 +1,1609 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI", + "resource:///modules/DownloadsViewUI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +/** + * Handles the Downloads panel user interface for each browser window. + * + * This file includes the following constructors and global objects: + * + * DownloadsPanel + * Main entry point for the downloads panel interface. + * + * DownloadsOverlayLoader + * Allows loading the downloads panel and the status indicator interfaces on + * demand, to improve startup performance. + * + * DownloadsView + * Builds and updates the downloads list widget, responding to changes in the + * download state and real-time data. In addition, handles part of the user + * interaction events raised by the downloads list widget. + * + * DownloadsViewItem + * Builds and updates a single item in the downloads list widget, responding to + * changes in the download state and real-time data. + * + * DownloadsViewController + * Handles part of the user interaction events raised by the downloads list + * widget, in particular the "commands" that apply to multiple items, and + * dispatches the commands that apply to individual items. + * + * DownloadsViewItemController + * Handles all the user interaction events, in particular the "commands", + * related to a single item in the downloads list widgets. + */ + +/** + * A few words on focus and focusrings + * + * We do quite a few hacks in the Downloads Panel for focusrings. In fact, we + * basically suppress most if not all XUL-level focusrings, and style/draw + * them ourselves (using :focus instead of -moz-focusring). There are a few + * reasons for this: + * + * 1) Richlists on OSX don't have focusrings; instead, they are shown as + * selected. This makes for some ambiguity when we have a focused/selected + * item in the list, and the mouse is hovering a completed download (which + * highlights). + * 2) Windows doesn't show focusrings until after the first time that tab is + * pressed (and by then you're focusing the second item in the panel). + * 3) Richlistbox sets -moz-focusring even when we select it with a mouse. + * + * In general, the desired behaviour is to focus the first item after pressing + * tab/down, and show that focus with a ring. Then, if the mouse moves over + * the panel, to hide that focus ring; essentially resetting us to the state + * before pressing the key. + * + * We end up capturing the tab/down key events, and preventing their default + * behaviour. We then set a "keyfocus" attribute on the panel, which allows + * us to draw a ring around the currently focused element. If the panel is + * closed or the mouse moves over the panel, we remove the attribute. + */ + +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsPanel + +/** + * Main entry point for the downloads panel interface. + */ +const DownloadsPanel = { + ////////////////////////////////////////////////////////////////////////////// + //// Initialization and termination + + /** + * Internal state of the downloads panel, based on one of the kState + * constants. This is not the same state as the XUL panel element. + */ + _state: 0, + + /** The panel is not linked to downloads data yet. */ + get kStateUninitialized() 0, + /** This object is linked to data, but the panel is invisible. */ + get kStateHidden() 1, + /** The panel will be shown as soon as possible. */ + get kStateWaitingData() 2, + /** The panel is almost shown - we're just waiting to get a handle on the + anchor. */ + get kStateWaitingAnchor() 3, + /** The panel is open. */ + get kStateShown() 4, + + /** + * Location of the panel overlay. + */ + get kDownloadsOverlay() + "chrome://browser/content/downloads/downloadsOverlay.xul", + + /** + * Starts loading the download data in background, without opening the panel. + * Use showPanel instead to load the data and open the panel at the same time. + * + * @param aCallback + * Called when initialization is complete. + */ + initialize: function(aCallback) + { + DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window."); + if (this._state != this.kStateUninitialized) { + DownloadsCommon.log("DownloadsPanel is already initialized."); + DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, + aCallback); + return; + } + this._state = this.kStateHidden; + + window.addEventListener("unload", this.onWindowUnload, false); + + // Ensure that the Download Manager service is running. This resumes + // active downloads if required. If there are downloads to be shown in the + // panel, starting the service will make us load their data asynchronously. + DownloadsCommon.initializeAllDataLinks(); + + + // Now that data loading has eventually started, load the required XUL + // elements and initialize our views. + DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded."); + DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, + function DP_I_callback() { + DownloadsViewController.initialize(); + DownloadsCommon.log("Attaching DownloadsView..."); + DownloadsCommon.getData(window).addView(DownloadsView); + DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) + .addView(DownloadsSummary); + DownloadsCommon.log("DownloadsView attached - the panel for this window", + "should now see download items come in."); + DownloadsPanel._attachEventListeners(); + DownloadsCommon.log("DownloadsPanel initialized."); + aCallback(); + }); + }, + + /** + * Closes the downloads panel and frees the internal resources related to the + * downloads. The downloads panel can be reopened later, even after this + * function has been called. + */ + terminate: function() + { + DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window."); + if (this._state == this.kStateUninitialized) { + DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do."); + return; + } + + window.removeEventListener("unload", this.onWindowUnload, false); + + // Ensure that the panel is closed before shutting down. + this.hidePanel(); + + DownloadsViewController.terminate(); + DownloadsCommon.getData(window).removeView(DownloadsView); + DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) + .removeView(DownloadsSummary); + this._unattachEventListeners(); + + this._state = this.kStateUninitialized; + + DownloadsSummary.active = false; + DownloadsCommon.log("DownloadsPanel terminated."); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Panel interface + + /** + * Main panel element in the browser window, or null if the panel overlay + * hasn't been loaded yet. + */ + get panel() + { + // If the downloads panel overlay hasn't loaded yet, just return null + // without resetting this.panel. + let downloadsPanel = document.getElementById("downloadsPanel"); + if (!downloadsPanel) + return null; + + delete this.panel; + return this.panel = downloadsPanel; + }, + + /** + * Starts opening the downloads panel interface, anchored to the downloads + * button of the browser window. The list of downloads to display is + * initialized the first time this method is called, and the panel is shown + * only when data is ready. + */ + showPanel: function() + { + DownloadsCommon.log("Opening the downloads panel."); + + if (this.isPanelShowing) { + DownloadsCommon.log("Panel is already showing - focusing instead."); + this._focusPanel(); + return; + } + + this.initialize(function DP_SP_callback() { + // Delay displaying the panel because this function will sometimes be + // called while another window is closing (like the window for selecting + // whether to save or open the file), and that would cause the panel to + // close immediately. + setTimeout(function() DownloadsPanel._openPopupIfDataReady(), 0); + }.bind(this)); + + DownloadsCommon.log("Waiting for the downloads panel to appear."); + this._state = this.kStateWaitingData; + }, + + /** + * Hides the downloads panel, if visible, but keeps the internal state so that + * the panel can be reopened quickly if required. + */ + hidePanel: function() + { + DownloadsCommon.log("Closing the downloads panel."); + + if (!this.isPanelShowing) { + DownloadsCommon.log("Downloads panel is not showing - nothing to do."); + return; + } + + this.panel.hidePopup(); + + // Ensure that we allow the panel to be reopened. Note that, if the popup + // was open, then the onPopupHidden event handler has already updated the + // current state, otherwise we must update the state ourselves. + this._state = this.kStateHidden; + DownloadsCommon.log("Downloads panel is now closed."); + }, + + /** + * Indicates whether the panel is shown or will be shown. + */ + get isPanelShowing() + { + return this._state == this.kStateWaitingData || + this._state == this.kStateWaitingAnchor || + this._state == this.kStateShown; + }, + + /** + * Returns whether the user has started keyboard navigation. + */ + get keyFocusing() + { + return this.panel.hasAttribute("keyfocus"); + }, + + /** + * Set to true if the user has started keyboard navigation, and we should be + * showing focusrings in the panel. Also adds a mousemove event handler to + * the panel which disables keyFocusing. + */ + set keyFocusing(aValue) + { + if (aValue) { + this.panel.setAttribute("keyfocus", "true"); + this.panel.addEventListener("mousemove", this); + } else { + this.panel.removeAttribute("keyfocus"); + this.panel.removeEventListener("mousemove", this); + } + return aValue; + }, + + /** + * Handles the mousemove event for the panel, which disables focusring + * visualization. + */ + handleEvent: function(aEvent) + { + if (aEvent.type == "mousemove") { + this.keyFocusing = false; + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsView + + /** + * Called after data loading finished. + */ + onViewLoadCompleted: function() + { + this._openPopupIfDataReady(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// User interface event functions + + onWindowUnload: function() + { + // This function is registered as an event listener, we can't use "this". + DownloadsPanel.terminate(); + }, + + onPopupShown: function(aEvent) + { + // Ignore events raised by nested popups. + if (aEvent.target != aEvent.currentTarget) { + return; + } + + DownloadsCommon.log("Downloads panel has shown."); + this._state = this.kStateShown; + + // Since at most one popup is open at any given time, we can set globally. + DownloadsCommon.getIndicatorData(window).attentionSuppressed = true; + + // Ensure that the first item is selected when the panel is focused. + if (DownloadsView.richListBox.itemCount > 0 && + DownloadsView.richListBox.selectedIndex == -1) { + DownloadsView.richListBox.selectedIndex = 0; + } + + this._focusPanel(); + }, + + onPopupHidden: function(aEvent) + { + // Ignore events raised by nested popups. + if (aEvent.target != aEvent.currentTarget) { + return; + } + + DownloadsCommon.log("Downloads panel has hidden."); + + // Removes the keyfocus attribute so that we stop handling keyboard + // navigation. + this.keyFocusing = false; + + // Since at most one popup is open at any given time, we can set globally. + DownloadsCommon.getIndicatorData(window).attentionSuppressed = false; + + // Allow the anchor to be hidden. + DownloadsButton.releaseAnchor(); + + // Allow the panel to be reopened. + this._state = this.kStateHidden; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Related operations + + /** + * Shows or focuses the user interface dedicated to downloads history. + */ + showDownloadsHistory: function() + { + DownloadsCommon.log("Showing download history."); + // Hide the panel before showing another window, otherwise focus will return + // to the browser window when the panel closes automatically. + this.hidePanel(); + + BrowserDownloadsUI(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Internal functions + + /** + * Attach event listeners to a panel element. These listeners should be + * removed in _unattachEventListeners. This is called automatically after the + * panel has successfully loaded. + */ + _attachEventListeners: function() + { + // Handle keydown to support accel-V. + this.panel.addEventListener("keydown", this._onKeyDown.bind(this), false); + // Handle keypress to be able to preventDefault() events before they reach + // the richlistbox, for keyboard navigation. + this.panel.addEventListener("keypress", this._onKeyPress.bind(this), false); + }, + + /** + * Unattach event listeners that were added in _attachEventListeners. This + * is called automatically on panel termination. + */ + _unattachEventListeners: function() + { + this.panel.removeEventListener("keydown", this._onKeyDown.bind(this), + false); + this.panel.removeEventListener("keypress", this._onKeyPress.bind(this), + false); + }, + + _onKeyPress: function(aEvent) + { + // Handle unmodified keys only. + if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) { + return; + } + + let richListBox = DownloadsView.richListBox; + + // If the user has pressed the tab, up, or down cursor key, start keyboard + // navigation, thus enabling focusrings in the panel. Keyboard navigation + // is automatically disabled if the user moves the mouse on the panel, or + // if the panel is closed. + if ((aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_TAB || + aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP || + aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) && + !this.keyFocusing) { + this.keyFocusing = true; + // Ensure there's a selection, we will show the focus ring around it and + // prevent the richlistbox from changing the selection. + if (DownloadsView.richListBox.selectedIndex == -1) + DownloadsView.richListBox.selectedIndex = 0; + aEvent.preventDefault(); + return; + } + + if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) { + // If the last element in the list is selected, or the footer is already + // focused, focus the footer. + if (richListBox.selectedItem === richListBox.lastChild || + document.activeElement.parentNode.id === "downloadsFooter") { + DownloadsFooter.focus(); + aEvent.preventDefault(); + return; + } + } + + // Pass keypress events to the richlistbox view when it's focused. + if (document.activeElement === richListBox) { + DownloadsView.onDownloadKeyPress(aEvent); + } + }, + + /** + * Keydown listener that listens for the keys to start key focusing, as well + * as the the accel-V "paste" event, which initiates a file download if the + * pasted item can be resolved to a URI. + */ + _onKeyDown: function(aEvent) + { + // If the footer is focused and the downloads list has at least 1 element + // in it, focus the last element in the list when going up. + if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && + document.activeElement.parentNode.id === "downloadsFooter" && + DownloadsView.richListBox.firstChild) { + DownloadsView.richListBox.focus(); + DownloadsView.richListBox.selectedItem = DownloadsView.richListBox.lastChild; + aEvent.preventDefault(); + return; + } + + let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V && + aEvent.ctrlKey; + + if (!pasting) { + return; + } + + DownloadsCommon.log("Received a paste event."); + + let trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(null); + let flavors = ["text/x-moz-url", "text/unicode"]; + flavors.forEach(trans.addDataFlavor); + Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); + // Getting the data or creating the nsIURI might fail + try { + let data = {}; + trans.getAnyTransferData({}, data, {}); + let [url, name] = data.value + .QueryInterface(Ci.nsISupportsString) + .data + .split("\n"); + if (!url) { + return; + } + + let uri = NetUtil.newURI(url); + DownloadsCommon.log("Pasted URL seems valid. Starting download."); + DownloadURL(uri.spec, name, document); + } catch (ex) {} + }, + + /** + * Move focus to the main element in the downloads panel, unless another + * element in the panel is already focused. + */ + _focusPanel: function() + { + // We may be invoked while the panel is still waiting to be shown. + if (this._state != this.kStateShown) { + return; + } + + let element = document.commandDispatcher.focusedElement; + while (element && element != this.panel) { + element = element.parentNode; + } + if (!element) { + if (DownloadsView.richListBox.itemCount > 0) { + DownloadsView.richListBox.focus(); + } else { + DownloadsFooter.focus(); + } + } + }, + + /** + * Opens the downloads panel when data is ready to be displayed. + */ + _openPopupIfDataReady: function() + { + // We don't want to open the popup if we already displayed it, or if we are + // still loading data. + if (this._state != this.kStateWaitingData || DownloadsView.loading) { + return; + } + + this._state = this.kStateWaitingAnchor; + + // Ensure the anchor is visible. If that is not possible, show the panel + // anchored to the top area of the window, near the default anchor position. + DownloadsButton.getAnchor(function DP_OPIDR_callback(aAnchor) { + // If somehow we've switched states already (by getting a panel hiding + // event before an overlay is loaded, for example), bail out. + if (this._state != this.kStateWaitingAnchor) + return; + + // At this point, if the window is minimized, opening the panel could fail + // without any notification, and there would be no way to either open or + // close the panel any more. To prevent this, check if the window is + // minimized and in that case force the panel to the closed state. + if (window.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED) { + DownloadsButton.releaseAnchor(); + this._state = this.kStateHidden; + return; + } + + // When the panel is opened, we check if the target files of visible items + // still exist, and update the allowed items interactions accordingly. We + // do these checks on a background thread, and don't prevent the panel to + // be displayed while these checks are being performed. + for (let viewItem of DownloadsView._visibleViewItems.values()) { + viewItem.download.refresh().catch(Cu.reportError); + } + + if (aAnchor) { + DownloadsCommon.log("Opening downloads panel popup."); + this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false, + null); + } else { + DownloadsCommon.error("We can't find the anchor! Failure case - opening", + "downloads panel on TabsToolbar. We should never", + "get here!"); + Components.utils.reportError( + "Downloads button cannot be found"); + } + }.bind(this)); + } +}; + +XPCOMUtils.defineConstant(this, "DownloadsPanel", DownloadsPanel); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsOverlayLoader + +/** + * Allows loading the downloads panel and the status indicator interfaces on + * demand, to improve startup performance. + */ +const DownloadsOverlayLoader = { + /** + * We cannot load two overlays at the same time, thus we use a queue of + * pending load requests. + */ + _loadRequests: [], + + /** + * True while we are waiting for an overlay to be loaded. + */ + _overlayLoading: false, + + /** + * This object has a key for each overlay URI that is already loaded. + */ + _loadedOverlays: {}, + + /** + * Loads the specified overlay and invokes the given callback when finished. + * + * @param aOverlay + * String containing the URI of the overlay to load in the current + * window. If this overlay has already been loaded using this + * function, then the overlay is not loaded again. + * @param aCallback + * Invoked when loading is completed. If the overlay is already + * loaded, the function is called immediately. + */ + ensureOverlayLoaded: function(aOverlay, aCallback) + { + // The overlay is already loaded, invoke the callback immediately. + if (aOverlay in this._loadedOverlays) { + aCallback(); + return; + } + + // The callback will be invoked when loading is finished. + this._loadRequests.push({ overlay: aOverlay, callback: aCallback }); + if (this._overlayLoading) { + return; + } + + function DOL_EOL_loadCallback() { + this._overlayLoading = false; + this._loadedOverlays[aOverlay] = true; + + // Loading the overlay causes all the persisted XUL attributes to be + // reapplied, including "iconsize" on the toolbars. Until bug 640158 is + // fixed, we must recalculate the correct "iconsize" attributes manually. + retrieveToolbarIconsizesFromTheme(); + + this.processPendingRequests(); + } + + this._overlayLoading = true; + DownloadsCommon.log("Loading overlay ", aOverlay); + document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this)); + }, + + /** + * Re-processes all the currently pending requests, invoking the callbacks + * and/or loading more overlays as needed. In most cases, there will be a + * single request for one overlay, that will be processed immediately. + */ + processPendingRequests: function() + { + // Re-process all the currently pending requests, yet allow more requests + // to be appended at the end of the array if we're not ready for them. + let currentLength = this._loadRequests.length; + for (let i = 0; i < currentLength; i++) { + let request = this._loadRequests.shift(); + + // We must call ensureOverlayLoaded again for each request, to check if + // the associated callback can be invoked now, or if we must still wait + // for the associated overlay to load. + this.ensureOverlayLoaded(request.overlay, request.callback); + } + } +}; + +XPCOMUtils.defineConstant(this, "DownloadsOverlayLoader", DownloadsOverlayLoader); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsView + +/** + * Builds and updates the downloads list widget, responding to changes in the + * download state and real-time data. In addition, handles part of the user + * interaction events raised by the downloads list widget. + */ +const DownloadsView = { + ////////////////////////////////////////////////////////////////////////////// + //// Functions handling download items in the list + + /** + * Maximum number of items shown by the list at any given time. + */ + kItemCountLimit: 3, + + /** + * Indicates whether we are still loading downloads data asynchronously. + */ + loading: false, + + /** + * Ordered array of all Download objects. We need to keep this array because + * only a limited number of items are shown at once, and if an item that is + * currently visible is removed from the list, we might need to take another + * item from the array and make it appear at the bottom. + */ + _downloads: [], + + /** + * Associates the visible Download objects with their corresponding + * DownloadsViewItem object. There is a limited number of view items in the + * panel at any given time. + */ + _visibleViewItems: new Map(), + + /** + * Called when the number of items in the list changes. + */ + _itemCountChanged: function() + { + DownloadsCommon.log("The downloads item count has changed - we are tracking", + this._downloads.length, "downloads in total."); + let count = this._downloads.length; + let hiddenCount = count - this.kItemCountLimit; + + if (count > 0) { + DownloadsCommon.log("Setting the panel's hasdownloads attribute to true."); + DownloadsPanel.panel.setAttribute("hasdownloads", "true"); + } else { + DownloadsCommon.log("Removing the panel's hasdownloads attribute."); + DownloadsPanel.panel.removeAttribute("hasdownloads"); + } + + // If we've got some hidden downloads, we should activate the + // DownloadsSummary. The DownloadsSummary will determine whether or not + // it's appropriate to actually display the summary. + DownloadsSummary.active = hiddenCount > 0; + }, + + /** + * Element corresponding to the list of downloads. + */ + get richListBox() + { + delete this.richListBox; + return this.richListBox = document.getElementById("downloadsListBox"); + }, + + /** + * Element corresponding to the button for showing more downloads. + */ + get downloadsHistory() + { + delete this.downloadsHistory; + return this.downloadsHistory = document.getElementById("downloadsHistory"); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsData + + /** + * Called before multiple downloads are about to be loaded. + */ + onDataLoadStarting: function() + { + DownloadsCommon.log("onDataLoadStarting called for DownloadsView."); + this.loading = true; + }, + + /** + * Called after data loading finished. + */ + onDataLoadCompleted: function() + { + DownloadsCommon.log("onDataLoadCompleted called for DownloadsView."); + + this.loading = false; + + // We suppressed item count change notifications during the batch load, at + // this point we should just call the function once. + this._itemCountChanged(); + + // Notify the panel that all the initially available downloads have been + // loaded. This ensures that the interface is visible, if still required. + DownloadsPanel.onViewLoadCompleted(); + }, + + /** + * Called when the downloads database becomes unavailable (for example, + * entering Private Browsing Mode). References to existing data should be + * discarded. + */ + onDataInvalidated: function() + { + DownloadsCommon.log("Downloads data has been invalidated. Cleaning up", + "DownloadsView."); + + DownloadsPanel.terminate(); + + // Clear the list by replacing with a shallow copy. + let emptyView = this.richListBox.cloneNode(false); + this.richListBox.parentNode.replaceChild(emptyView, this.richListBox); + this.richListBox = emptyView; + this._viewItems = {}; + this._dataItems = []; + }, + + /** + * Called when a new download data item is available, either during the + * asynchronous data load or when a new download is started. + * + * @param aDownload + * Download object that was just added. + * @param aNewest + * When true, indicates that this item is the most recent and should be + * added in the topmost position. This happens when a new download is + * started. When false, indicates that the item is the least recent + * and should be appended. The latter generally happens during the + * asynchronous data load. + */ + onDownloadAdded(download, aNewest) { + DownloadsCommon.log("A new download data item was added - aNewest =", + aNewest); + + if (aNewest) { + this._downloads.unshift(download); + } else { + this._downloads.push(download); + } + + let itemsNowOverflow = this._downloads.length > this.kItemCountLimit; + if (aNewest || !itemsNowOverflow) { + // The newly added item is visible in the panel and we must add the + // corresponding element. This is either because it is the first item, or + // because it was added at the bottom but the list still doesn't overflow. + this._addViewItem(download, aNewest); + } + if (aNewest && itemsNowOverflow) { + // If the list overflows, remove the last item from the panel to make room + // for the new one that we just added at the top. + this._removeViewItem(this._downloads[this.kItemCountLimit]); + } + + // For better performance during batch loads, don't update the count for + // every item, because the interface won't be visible until load finishes. + if (!this.loading) { + this._itemCountChanged(); + } + }, + + onDownloadStateChanged(download) { + let viewItem = this._visibleViewItems.get(download); + if (viewItem) { + viewItem.onStateChanged(); + } + }, + + onDownloadChanged(download) { + let viewItem = this._visibleViewItems.get(download); + if (viewItem) { + viewItem.onChanged(); + } + }, + + /** + * Called when a data item is removed. Ensures that the widget associated + * with the view item is removed from the user interface. + * + * @param download + * Download object that is being removed. + */ + onDownloadRemoved(download) { + DownloadsCommon.log("A download data item was removed."); + + let itemIndex = this._downloads.indexOf(download); + this._downloads.splice(itemIndex, 1); + + if (itemIndex < this.kItemCountLimit) { + // The item to remove is visible in the panel. + this._removeViewItem(download); + if (this._downloads.length >= this.kItemCountLimit) { + // Reinsert the next item into the panel. + this._addViewItem(this._downloads[this.kItemCountLimit - 1], false); + } + } + + this._itemCountChanged(); + }, + + /** + * Associates each richlistitem for a download with its corresponding + * DownloadsViewItemController object. + */ + _controllersForElements: new Map(), + + controllerForElement(element) { + return this._controllersForElements.get(element); + }, + + /** + * Creates a new view item associated with the specified data item, and adds + * it to the top or the bottom of the list. + */ + _addViewItem(download, aNewest) + { + DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.", + "aNewest =", aNewest); + + let element = document.createElement("richlistitem"); + let viewItem = new DownloadsViewItem(download, element); + this._visibleViewItems.set(download, viewItem); + let viewItemController = new DownloadsViewItemController(download); + this._controllersForElements.set(element, viewItemController); + if (aNewest) { + this.richListBox.insertBefore(element, this.richListBox.firstChild); + } else { + this.richListBox.appendChild(element); + } + }, + + /** + * Removes the view item associated with the specified data item. + */ + _removeViewItem(download) { + DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list."); + let element = this._visibleViewItems.get(download).element; + let previousSelectedIndex = this.richListBox.selectedIndex; + this.richListBox.removeChild(element); + if (previousSelectedIndex != -1) { + this.richListBox.selectedIndex = Math.min(previousSelectedIndex, + this.richListBox.itemCount - 1); + } + this._visibleViewItems.delete(download); + this._controllersForElements.delete(element); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// User interface event functions + + /** + * Helper function to do commands on a specific download item. + * + * @param aEvent + * Event object for the event being handled. If the event target is + * not a richlistitem that represents a download, this function will + * walk up the parent nodes until it finds a DOM node that is. + * @param aCommand + * The command to be performed. + */ + onDownloadCommand: function(aEvent, aCommand) + { + let target = aEvent.target; + while (target.nodeName != "richlistitem") { + target = target.parentNode; + } + DownloadsView.controllerForElement(target).doCommand(aCommand); + }, + + onDownloadClick: function(aEvent) + { + // Handle primary clicks only, and exclude the action button. + if (aEvent.button == 0 && + !aEvent.originalTarget.hasAttribute("oncommand")) { + goDoCommand("downloadsCmd_open"); + } + }, + + /** + * Handles keypress events on a download item. + */ + onDownloadKeyPress: function(aEvent) + { + // Pressing the key on buttons should not invoke the action because the + // event has already been handled by the button itself. + if (aEvent.originalTarget.hasAttribute("command") || + aEvent.originalTarget.hasAttribute("oncommand")) { + return; + } + + if (aEvent.charCode == " ".charCodeAt(0)) { + goDoCommand("downloadsCmd_pauseResume"); + return; + } + + if (aEvent.keyCode == KeyEvent.DOM_VK_ENTER || + aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { + goDoCommand("downloadsCmd_doDefault"); + } + }, + + + /** + * Mouse listeners to handle selection on hover. + */ + onDownloadMouseOver: function(aEvent) + { + if (aEvent.originalTarget.parentNode == this.richListBox) + this.richListBox.selectedItem = aEvent.originalTarget; + }, + onDownloadMouseOut: function(aEvent) + { + if (aEvent.originalTarget.parentNode == this.richListBox) { + // If the destination element is outside of the richlistitem, clear the + // selection. + let element = aEvent.relatedTarget; + while (element && element != aEvent.originalTarget) { + element = element.parentNode; + } + if (!element) + this.richListBox.selectedIndex = -1; + } + }, + + onDownloadContextMenu: function(aEvent) + { + let element = this.richListBox.selectedItem; + if (!element) { + return; + } + + DownloadsViewController.updateCommands(); + + // Set the state attribute so that only the appropriate items are displayed. + let contextMenu = document.getElementById("downloadsContextMenu"); + contextMenu.setAttribute("state", element.getAttribute("state")); + }, + + onDownloadDragStart: function(aEvent) + { + let element = this.richListBox.selectedItem; + if (!element) { + return; + } + + // We must check for existence synchronously because this is a DOM event. + let localFile = new FileUtils.File(DownloadsView.controllerForElement(element) + .download.target.path); + if (!localFile.exists()) { + return; + } + + let dataTransfer = aEvent.dataTransfer; + dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0); + dataTransfer.effectAllowed = "copyMove"; + var url = Services.io.newFileURI(localFile).spec; + dataTransfer.setData("text/uri-list", url); + dataTransfer.setData("text/plain", url); + dataTransfer.addElement(element); + + aEvent.stopPropagation(); + } +} + +XPCOMUtils.defineConstant(this, "DownloadsView", DownloadsView); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsViewItem + +/** + * Builds and updates a single item in the downloads list widget, responding to + * changes in the download state and real-time data. + * + * @param download + * Download object to be associated with the view item. + * @param aElement + * XUL element corresponding to the single download item in the view. + */ +function DownloadsViewItem(download, aElement) { + this.download = download; + + this.element = aElement; + this.element._shell = this; + + this.element.setAttribute("type", "download"); + this.element.classList.add("download-state"); + + this._updateState(); +} + +DownloadsViewItem.prototype = { + __proto__: DownloadsViewUI.DownloadElementShell.prototype, + + /** + * The XUL element corresponding to the associated richlistbox item. + */ + _element: null, + + onStateChanged() { + this.element.setAttribute("image", this.image); + this.element.setAttribute("state", + DownloadsCommon.stateOfDownload(this.download)); + }, + + onChanged() { + this._updateProgress(); + }, +}; + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsViewController + +/** + * Handles part of the user interaction events raised by the downloads list + * widget, in particular the "commands" that apply to multiple items, and + * dispatches the commands that apply to individual items. + */ +const DownloadsViewController = { + ////////////////////////////////////////////////////////////////////////////// + //// Initialization and termination + + initialize: function() + { + window.controllers.insertControllerAt(0, this); + }, + + terminate: function() + { + window.controllers.removeController(this); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIController + + supportsCommand: function(aCommand) + { + // Firstly, determine if this is a command that we can handle. + if (!(aCommand in this.commands) && + !(aCommand in DownloadsViewItemController.prototype.commands)) { + return false; + } + // Secondly, determine if focus is on a control in the downloads list. + let element = document.commandDispatcher.focusedElement; + while (element && element != DownloadsView.richListBox) { + element = element.parentNode; + } + // We should handle the command only if the downloads list is among the + // ancestors of the focused element. + return !!element; + }, + + isCommandEnabled: function(aCommand) + { + // Handle commands that are not selection-specific. + if (aCommand == "downloadsCmd_clearList") { + return DownloadsCommon.getData(window).canRemoveFinished; + } + + // Other commands are selection-specific. + let element = DownloadsView.richListBox.selectedItem; + return element && DownloadsView.controllerForElement(element) + .isCommandEnabled(aCommand); + }, + + doCommand: function(aCommand) + { + // If this command is not selection-specific, execute it. + if (aCommand in this.commands) { + this.commands[aCommand].apply(this); + return; + } + + // Other commands are selection-specific. + let element = DownloadsView.richListBox.selectedItem; + if (element) { + // The doCommand function also checks if the command is enabled. + DownloadsView.controllerForElement(element).doCommand(aCommand); + } + }, + + onEvent: function() { }, + + ////////////////////////////////////////////////////////////////////////////// + //// Other functions + + updateCommands: function() + { + Object.keys(this.commands).forEach(goUpdateCommand); + Object.keys(DownloadsViewItemController.prototype.commands) + .forEach(goUpdateCommand); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Selection-independent commands + + /** + * This object contains one key for each command that operates regardless of + * the currently selected item in the list. + */ + commands: { + downloadsCmd_clearList: function() + { + DownloadsCommon.getData(window).removeFinished(); + } + } +}; + +XPCOMUtils.defineConstant(this, "DownloadsViewController", DownloadsViewController); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsViewItemController + +/** + * Handles all the user interaction events, in particular the "commands", + * related to a single item in the downloads list widgets. + */ +function DownloadsViewItemController(download) { + this.download = download; +} + +DownloadsViewItemController.prototype = { + isCommandEnabled: function(aCommand) + { + switch (aCommand) { + case "downloadsCmd_open": { + if (!this.download.succeeded) { + return false; + } + + let file = new FileUtils.File(this.download.target.path); + return file.exists(); + } + case "downloadsCmd_show": { + let file = new FileUtils.File(this.download.target.path); + if (file.exists()) { + return true; + } + + if (!this.download.target.partFilePath) { + return false; + } + + let partFile = new FileUtils.File(this.download.target.partFilePath); + return partFile.exists(); + } + case "downloadsCmd_pauseResume": + return this.download.hasPartialData && !this.download.error; + case "downloadsCmd_retry": + return this.download.canceled || this.download.error; + case "downloadsCmd_openReferrer": + return !!this.download.source.referrer; + case "cmd_delete": + case "downloadsCmd_cancel": + case "downloadsCmd_copyLocation": + case "downloadsCmd_doDefault": + return true; + } + return false; + }, + + doCommand: function(aCommand) + { + if (this.isCommandEnabled(aCommand)) { + this.commands[aCommand].apply(this); + } + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Item commands + + /** + * This object contains one key for each command that operates on this item. + * + * In commands, the "this" identifier points to the controller item. + */ + commands: { + cmd_delete: function() + { + DownloadsCommon.removeAndFinalizeDownload(this.download); + PlacesUtils.bhistory.removePage( + NetUtil.newURI(this.download.source.url)); + }, + + downloadsCmd_cancel: function() + { + this.download.cancel().catch(() => {}); + this.download.removePartialData().catch(Cu.reportError); + }, + + downloadsCmd_open: function() + { + this.download.launch().catch(Cu.reportError); + + // We explicitly close the panel here to give the user the feedback that + // their click has been received, and we're handling the action. + // Otherwise, we'd have to wait for the file-type handler to execute + // before the panel would close. This also helps to prevent the user from + // accidentally opening a file several times. + DownloadsPanel.hidePanel(); + }, + + downloadsCmd_show: function() + { + let file = new FileUtils.File(this.download.target.path); + DownloadsCommon.showDownloadedFile(file); + + // We explicitly close the panel here to give the user the feedback that + // their click has been received, and we're handling the action. + // Otherwise, we'd have to wait for the operating system file manager + // window to open before the panel closed. This also helps to prevent the + // user from opening the containing folder several times. + DownloadsPanel.hidePanel(); + }, + + downloadsCmd_pauseResume: function() + { + if (this.download.stopped) { + this.download.start(); + } else { + this.download.cancel(); + } + }, + + downloadsCmd_retry: function() + { + this.download.start().catch(() => {}); + }, + + downloadsCmd_openReferrer: function() + { + openURL(this.download.source.referrer); + }, + + downloadsCmd_copyLocation: function() + { + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper); + clipboard.copyString(this.download.source.url, document); + }, + + downloadsCmd_doDefault: function() + { + const nsIDM = Ci.nsIDownloadManager; + + // Determine the default command for the current item. + let defaultCommand = function() { + switch (DownloadsCommon.stateOfDownload(this.download)) { + case nsIDM.DOWNLOAD_NOTSTARTED: return "downloadsCmd_cancel"; + case nsIDM.DOWNLOAD_FINISHED: return "downloadsCmd_open"; + case nsIDM.DOWNLOAD_FAILED: return "downloadsCmd_retry"; + case nsIDM.DOWNLOAD_CANCELED: return "downloadsCmd_retry"; + case nsIDM.DOWNLOAD_PAUSED: return "downloadsCmd_pauseResume"; + case nsIDM.DOWNLOAD_QUEUED: return "downloadsCmd_cancel"; + case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer"; + case nsIDM.DOWNLOAD_SCANNING: return "downloadsCmd_show"; + case nsIDM.DOWNLOAD_DIRTY: return "downloadsCmd_openReferrer"; + case nsIDM.DOWNLOAD_BLOCKED_POLICY: return "downloadsCmd_openReferrer"; + } + return ""; + }.apply(this); + if (defaultCommand && this.isCommandEnabled(defaultCommand)) + this.doCommand(defaultCommand); + } + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsSummary + +/** + * Manages the summary at the bottom of the downloads panel list if the number + * of items in the list exceeds the panels limit. + */ +const DownloadsSummary = { + + /** + * Sets the active state of the summary. When active, the summary subscribes + * to the DownloadsCommon DownloadsSummaryData singleton. + * + * @param aActive + * Set to true to activate the summary. + */ + set active(aActive) + { + if (aActive == this._active || !this._summaryNode) { + return this._active; + } + if (aActive) { + DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) + .refreshView(this); + } else { + DownloadsFooter.showingSummary = false; + } + + return this._active = aActive; + }, + + /** + * Returns the active state of the downloads summary. + */ + get active() this._active, + + _active: false, + + /** + * Sets whether or not we show the progress bar. + * + * @param aShowingProgress + * True if we should show the progress bar. + */ + set showingProgress(aShowingProgress) + { + if (aShowingProgress) { + this._summaryNode.setAttribute("inprogress", "true"); + } else { + this._summaryNode.removeAttribute("inprogress"); + } + // If progress isn't being shown, then we simply do not show the summary. + return DownloadsFooter.showingSummary = aShowingProgress; + }, + + /** + * Sets the amount of progress that is visible in the progress bar. + * + * @param aValue + * A value between 0 and 100 to represent the progress of the + * summarized downloads. + */ + set percentComplete(aValue) + { + if (this._progressNode) { + this._progressNode.setAttribute("value", aValue); + } + return aValue; + }, + + /** + * Sets the description for the download summary. + * + * @param aValue + * A string representing the description of the summarized + * downloads. + */ + set description(aValue) + { + if (this._descriptionNode) { + this._descriptionNode.setAttribute("value", aValue); + this._descriptionNode.setAttribute("tooltiptext", aValue); + } + return aValue; + }, + + /** + * Sets the details for the download summary, such as the time remaining, + * the amount of bytes transferred, etc. + * + * @param aValue + * A string representing the details of the summarized + * downloads. + */ + set details(aValue) + { + if (this._detailsNode) { + this._detailsNode.setAttribute("value", aValue); + this._detailsNode.setAttribute("tooltiptext", aValue); + } + return aValue; + }, + + /** + * Focuses the root element of the summary. + */ + focus: function() + { + if (this._summaryNode) { + this._summaryNode.focus(); + } + }, + + /** + * Respond to keydown events on the Downloads Summary node. + * + * @param aEvent + * The keydown event being handled. + */ + onKeyDown: function(aEvent) + { + if (aEvent.charCode == " ".charCodeAt(0) || + aEvent.keyCode == KeyEvent.DOM_VK_ENTER || + aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { + DownloadsPanel.showDownloadsHistory(); + } + }, + + /** + * Respond to click events on the Downloads Summary node. + * + * @param aEvent + * The click event being handled. + */ + onClick: function(aEvent) + { + DownloadsPanel.showDownloadsHistory(); + }, + + /** + * Element corresponding to the root of the downloads summary. + */ + get _summaryNode() + { + let node = document.getElementById("downloadsSummary"); + if (!node) { + return null; + } + delete this._summaryNode; + return this._summaryNode = node; + }, + + /** + * Element corresponding to the progress bar in the downloads summary. + */ + get _progressNode() + { + let node = document.getElementById("downloadsSummaryProgress"); + if (!node) { + return null; + } + delete this._progressNode; + return this._progressNode = node; + }, + + /** + * Element corresponding to the main description of the downloads + * summary. + */ + get _descriptionNode() + { + let node = document.getElementById("downloadsSummaryDescription"); + if (!node) { + return null; + } + delete this._descriptionNode; + return this._descriptionNode = node; + }, + + /** + * Element corresponding to the secondary description of the downloads + * summary. + */ + get _detailsNode() + { + let node = document.getElementById("downloadsSummaryDetails"); + if (!node) { + return null; + } + delete this._detailsNode; + return this._detailsNode = node; + } +}; + +XPCOMUtils.defineConstant(this, "DownloadsSummary", DownloadsSummary); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsFooter + +/** + * Manages events sent to to the footer vbox, which contains both the + * DownloadsSummary as well as the "Show All Downloads" button. + */ +const DownloadsFooter = { + + /** + * Focuses the appropriate element within the footer. If the summary + * is visible, focus it. If not, focus the "Show All Downloads" + * button. + */ + focus: function() + { + if (this._showingSummary) { + DownloadsSummary.focus(); + } else { + DownloadsView.downloadsHistory.focus(); + } + }, + + _showingSummary: false, + + /** + * Sets whether or not the Downloads Summary should be displayed in the + * footer. If not, the "Show All Downloads" button is shown instead. + */ + set showingSummary(aValue) + { + if (this._footerNode) { + if (aValue) { + this._footerNode.setAttribute("showingsummary", "true"); + } else { + this._footerNode.removeAttribute("showingsummary"); + } + this._showingSummary = aValue; + } + return aValue; + }, + + /** + * Element corresponding to the footer of the downloads panel. + */ + get _footerNode() + { + let node = document.getElementById("downloadsFooter"); + if (!node) { + return null; + } + delete this._footerNode; + return this._footerNode = node; + } +}; + +XPCOMUtils.defineConstant(this, "DownloadsFooter", DownloadsFooter); diff --git a/browser/components/downloads/content/downloadsOverlay.xul b/browser/components/downloads/content/downloadsOverlay.xul new file mode 100644 index 000000000..8dc8148bb --- /dev/null +++ b/browser/components/downloads/content/downloadsOverlay.xul @@ -0,0 +1,135 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://browser/content/downloads/downloads.css"?> +<?xml-stylesheet href="chrome://browser/skin/downloads/downloads.css"?> + +<!DOCTYPE overlay SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> + +<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="downloadsOverlay"> + + <commandset> + <command id="downloadsCmd_doDefault" + oncommand="goDoCommand('downloadsCmd_doDefault')"/> + <command id="downloadsCmd_pauseResume" + oncommand="goDoCommand('downloadsCmd_pauseResume')"/> + <command id="downloadsCmd_cancel" + oncommand="goDoCommand('downloadsCmd_cancel')"/> + <command id="downloadsCmd_open" + oncommand="goDoCommand('downloadsCmd_open')"/> + <command id="downloadsCmd_show" + oncommand="goDoCommand('downloadsCmd_show')"/> + <command id="downloadsCmd_retry" + oncommand="goDoCommand('downloadsCmd_retry')"/> + <command id="downloadsCmd_openReferrer" + oncommand="goDoCommand('downloadsCmd_openReferrer')"/> + <command id="downloadsCmd_copyLocation" + oncommand="goDoCommand('downloadsCmd_copyLocation')"/> + <command id="downloadsCmd_clearList" + oncommand="goDoCommand('downloadsCmd_clearList')"/> + </commandset> + + <popupset id="mainPopupSet"> + <!-- The panel has level="top" to ensure that it is never hidden by the + taskbar on Windows. See bug 672365. For accessibility to screen + readers, we use a label on the panel instead of the anchor because the + panel can also be displayed without an anchor. --> + <panel id="downloadsPanel" + aria-label="&downloads.title;" + role="group" + type="arrow" + orient="vertical" + level="top" + consumeoutsideclicks="true" + onpopupshown="DownloadsPanel.onPopupShown(event);" + onpopuphidden="DownloadsPanel.onPopupHidden(event);"> + <!-- The following popup menu should be a child of the panel element, + otherwise flickering may occur when the cursor is moved over the area + of a disabled menu item that overlaps the panel. See bug 492960. --> + <menupopup id="downloadsContextMenu" + class="download-state"> + <menuitem command="downloadsCmd_pauseResume" + class="downloadPauseMenuItem" + label="&cmd.pause.label;" + accesskey="&cmd.pause.accesskey;"/> + <menuitem command="downloadsCmd_pauseResume" + class="downloadResumeMenuItem" + label="&cmd.resume.label;" + accesskey="&cmd.resume.accesskey;"/> + <menuitem command="downloadsCmd_cancel" + class="downloadCancelMenuItem" + label="&cmd.cancel.label;" + accesskey="&cmd.cancel.accesskey;"/> + <menuitem command="cmd_delete" + class="downloadRemoveFromHistoryMenuItem" + label="&cmd.removeFromHistory.label;" + accesskey="&cmd.removeFromHistory.accesskey;"/> + <menuitem command="downloadsCmd_show" + class="downloadShowMenuItem" + label="&cmd.show.label;" + accesskey="&cmd.show.accesskey;" + /> + + <menuseparator class="downloadCommandsSeparator"/> + + <menuitem command="downloadsCmd_openReferrer" + label="&cmd.goToDownloadPage.label;" + accesskey="&cmd.goToDownloadPage.accesskey;"/> + <menuitem command="downloadsCmd_copyLocation" + label="&cmd.copyDownloadLink.label;" + accesskey="&cmd.copyDownloadLink.accesskey;"/> + + <menuseparator/> + + <menuitem command="downloadsCmd_clearList" + label="&cmd.clearList.label;" + accesskey="&cmd.clearList.accesskey;"/> + </menupopup> + + <richlistbox id="downloadsListBox" + class="plain" + flex="1" + context="downloadsContextMenu" + onmouseover="DownloadsView.onDownloadMouseOver(event);" + onmouseout="DownloadsView.onDownloadMouseOut(event);" + oncontextmenu="DownloadsView.onDownloadContextMenu(event);" + ondragstart="DownloadsView.onDownloadDragStart(event);"/> + <description id="emptyDownloads" + mousethrough="always"> + &downloadsPanelEmpty.label; + </description> + + <vbox id="downloadsFooter"> + <hbox id="downloadsSummary" + align="center" + orient="horizontal" + onkeydown="DownloadsSummary.onKeyDown(event);" + onclick="DownloadsSummary.onClick(event);"> + <image class="downloadTypeIcon" /> + <vbox> + <description id="downloadsSummaryDescription" + style="min-width: &downloadsSummary.minWidth2;"/> + <progressmeter id="downloadsSummaryProgress" + class="downloadProgress" + min="0" + max="100" + mode="normal" /> + <description id="downloadsSummaryDetails" + style="width: &downloadDetails.width;" + crop="end"/> + </vbox> + </hbox> + + <button id="downloadsHistory" + class="plain" + label="&downloadsHistory.label;" + accesskey="&downloadsHistory.accesskey;" + oncommand="DownloadsPanel.showDownloadsHistory();"/> + </vbox> + </panel> + </popupset> +</overlay> diff --git a/browser/components/downloads/content/indicator.js b/browser/components/downloads/content/indicator.js new file mode 100644 index 000000000..077699243 --- /dev/null +++ b/browser/components/downloads/content/indicator.js @@ -0,0 +1,608 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Handles the indicator that displays the progress of ongoing downloads, which + * is also used as the anchor for the downloads panel. + * + * This module includes the following constructors and global objects: + * + * DownloadsButton + * Main entry point for the downloads indicator. Depending on how the toolbars + * have been customized, this object determines if we should show a fully + * functional indicator, a placeholder used during customization and in the + * customization palette, or a neutral view as a temporary anchor for the + * downloads panel. + * + * DownloadsIndicatorView + * Builds and updates the actual downloads status widget, responding to changes + * in the global status data, or provides a neutral view if the indicator is + * removed from the toolbars and only used as a temporary anchor. In addition, + * handles the user interaction events raised by the widget. + */ + +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsButton + +/** + * Main entry point for the downloads indicator. Depending on how the toolbars + * have been customized, this object determines if we should show a fully + * functional indicator, a placeholder used during customization and in the + * customization palette, or a neutral view as a temporary anchor for the + * downloads panel. + */ +const DownloadsButton = { + /** + * Location of the indicator overlay. + */ + get kIndicatorOverlay() + "chrome://browser/content/downloads/indicatorOverlay.xul", + + /** + * Returns a reference to the downloads button position placeholder, or null + * if not available because it has been removed from the toolbars. + */ + get _placeholder() + { + return document.getElementById("downloads-button"); + }, + + /** + * This function is called asynchronously just after window initialization. + * + * NOTE: This function should limit the input/output it performs to improve + * startup time, and in particular should not cause the Download Manager + * service to start. + */ + initializeIndicator: function() + { + this._update(); + }, + + /** + * Indicates whether toolbar customization is in progress. + */ + _customizing: false, + + /** + * This function is called when toolbar customization starts. + * + * During customization, we never show the actual download progress indication + * or the event notifications, but we show a neutral placeholder. The neutral + * placeholder is an ordinary button defined in the browser window that can be + * moved freely between the toolbars and the customization palette. + */ + customizeStart: function() + { + // Hide the indicator and prevent it to be displayed as a temporary anchor + // during customization, even if requested using the getAnchor method. + this._customizing = true; + this._anchorRequested = false; + + let indicator = DownloadsIndicatorView.indicator; + if (indicator) { + indicator.collapsed = true; + } + + let placeholder = this._placeholder; + if (placeholder) { + placeholder.collapsed = false; + } + }, + + /** + * This function is called when toolbar customization ends. + */ + customizeDone: function() + { + this._customizing = false; + this._update(); + }, + + /** + * This function is called during initialization or when toolbar customization + * ends. It determines if we should enable or disable the object that keeps + * the indicator updated, and ensures that the placeholder is hidden unless it + * has been moved to the customization palette. + * + * NOTE: This function is also called on startup, thus it should limit the + * input/output it performs, and in particular should not cause the + * Download Manager service to start. + */ + _update: function() { + this._updatePositionInternal(); + + if (!DownloadsCommon.useToolkitUI) { + DownloadsIndicatorView.ensureInitialized(); + } else { + DownloadsIndicatorView.ensureTerminated(); + } + }, + + /** + * Determines the position where the indicator should appear, and moves its + * associated element to the new position. This does not happen if the + * indicator is currently being used as the anchor for the panel, to ensure + * that the panel doesn't flicker because we move the DOM element to which + * it's anchored. + */ + updatePosition: function() + { + if (!this._anchorRequested) { + this._updatePositionInternal(); + } + }, + + /** + * Determines the position where the indicator should appear, and moves its + * associated element to the new position. + * + * @return Anchor element, or null if the indicator is not visible. + */ + _updatePositionInternal: function() + { + let indicator = DownloadsIndicatorView.indicator; + if (!indicator) { + // Exit now if the indicator overlay isn't loaded yet. + return null; + } + + let placeholder = this._placeholder; + if (!placeholder) { + // The placeholder has been removed from the browser window. + indicator.collapsed = true; + // Move the indicator to a safe position on the toolbar, since otherwise + // it may break the merge of adjacent items, like back/forward + urlbar. + indicator.parentNode.appendChild(indicator); + return null; + } + + // Position the indicator where the placeholder is located. We should + // update the position even if the placeholder is located on an invisible + // toolbar, because the toolbar may be displayed later. + placeholder.parentNode.insertBefore(indicator, placeholder); + placeholder.collapsed = true; + indicator.collapsed = false; + + indicator.open = this._anchorRequested; + + // Determine if the placeholder is located on an invisible toolbar. + if (!isElementVisible(placeholder.parentNode)) { + return null; + } + + return DownloadsIndicatorView.indicatorAnchor; + }, + + /** + * Checks whether the indicator is, or will soon be visible in the browser + * window. + * + * @param aCallback + * Called once the indicator overlay has loaded. Gets a boolean + * argument representing the indicator visibility. + */ + checkIsVisible: function(aCallback) + { + function DB_CEV_callback() { + if (!this._placeholder) { + aCallback(false); + } else { + let element = DownloadsIndicatorView.indicator || this._placeholder; + aCallback(isElementVisible(element.parentNode)); + } + } + DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, + DB_CEV_callback.bind(this)); + }, + + /** + * Indicates whether we should try and show the indicator temporarily as an + * anchor for the panel, even if the indicator would be hidden by default. + */ + _anchorRequested: false, + + /** + * Ensures that there is an anchor available for the panel. + * + * @param aCallback + * Called when the anchor is available, passing the element where the + * panel should be anchored, or null if an anchor is not available (for + * example because both the tab bar and the navigation bar are hidden). + */ + getAnchor: function(aCallback) + { + // Do not allow anchoring the panel to the element while customizing. + if (this._customizing) { + aCallback(null); + return; + } + + function DB_GA_callback() { + this._anchorRequested = true; + aCallback(this._updatePositionInternal()); + } + + DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, + DB_GA_callback.bind(this)); + }, + + /** + * Allows the temporary anchor to be hidden. + */ + releaseAnchor: function() + { + this._anchorRequested = false; + this._updatePositionInternal(); + }, + + get _tabsToolbar() + { + delete this._tabsToolbar; + return this._tabsToolbar = document.getElementById("TabsToolbar"); + }, + + get _navBar() + { + delete this._navBar; + return this._navBar = document.getElementById("nav-bar"); + } +}; + +Object.defineProperty(this, "DownloadsButton", { + value: DownloadsButton, + enumerable: true, + writable: false +}); + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadsIndicatorView + +/** + * Builds and updates the actual downloads status widget, responding to changes + * in the global status data, or provides a neutral view if the indicator is + * removed from the toolbars and only used as a temporary anchor. In addition, + * handles the user interaction events raised by the widget. + */ +const DownloadsIndicatorView = { + /** + * True when the view is connected with the underlying downloads data. + */ + _initialized: false, + + /** + * True when the user interface elements required to display the indicator + * have finished loading in the browser window, and can be referenced. + */ + _operational: false, + + /** + * Prepares the downloads indicator to be displayed. + */ + ensureInitialized: function() + { + if (this._initialized) { + return; + } + this._initialized = true; + + window.addEventListener("unload", this.onWindowUnload, false); + DownloadsCommon.getIndicatorData(window).addView(this); + }, + + /** + * Frees the internal resources related to the indicator. + */ + ensureTerminated: function() + { + if (!this._initialized) { + return; + } + this._initialized = false; + + window.removeEventListener("unload", this.onWindowUnload, false); + DownloadsCommon.getIndicatorData(window).removeView(this); + + // Reset the view properties, so that a neutral indicator is displayed if we + // are visible only temporarily as an anchor. + this.counter = ""; + this.percentComplete = 0; + this.paused = false; + this.attention = false; + }, + + /** + * Ensures that the user interface elements required to display the indicator + * are loaded, then invokes the given callback. + */ + _ensureOperational: function(aCallback) + { + if (this._operational) { + aCallback(); + return; + } + + function DIV_EO_callback() { + this._operational = true; + + // If the view is initialized, we need to update the elements now that + // they are finally available in the document. + if (this._initialized) { + DownloadsCommon.getIndicatorData(window).refreshView(this); + } + + aCallback(); + } + + DownloadsOverlayLoader.ensureOverlayLoaded( + DownloadsButton.kIndicatorOverlay, + DIV_EO_callback.bind(this)); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Direct control functions + + /** + * Set while we are waiting for a notification to fade out. + */ + _notificationTimeout: null, + + /** + * If the status indicator is visible in its assigned position, shows for a + * brief time a visual notification of a relevant event, like a new download. + * + * @param aType + * Set to "start" for new downloads, "finish" for completed downloads. + */ + showEventNotification: function(aType) + { + if (!this._initialized) { + return; + } + + if (!DownloadsCommon.animateNotifications) { + return; + } + + // No need to show visual notification if the panel is visible. + if (DownloadsPanel.isPanelShowing) { + return; + } + + function DIV_SEN_callback() { + if (this._notificationTimeout) { + clearTimeout(this._notificationTimeout); + } + + // Now that the overlay is loaded, place the indicator in its final + // position. + DownloadsButton.updatePosition(); + + let indicator = this.indicator; + indicator.setAttribute("notification", aType); + this._notificationTimeout = setTimeout( + function() indicator.removeAttribute("notification"), 1000); + } + + this._ensureOperational(DIV_SEN_callback.bind(this)); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsIndicatorData + + /** + * Indicates whether the indicator should be shown because there are some + * downloads to be displayed. + */ + set hasDownloads(aValue) + { + if (this._hasDownloads != aValue) { + this._hasDownloads = aValue; + + // If there is at least one download, ensure that the view elements are + // loaded before determining the position of the downloads button. + if (aValue) { + this._ensureOperational(function() DownloadsButton.updatePosition()); + } else { + DownloadsButton.updatePosition(); + } + } + return aValue; + }, + get hasDownloads() + { + return this._hasDownloads; + }, + _hasDownloads: false, + + /** + * Status text displayed in the indicator. If this is set to an empty value, + * then the small downloads icon is displayed instead of the text. + */ + set counter(aValue) + { + if (!this._operational) { + return this._counter; + } + + if (this._counter !== aValue) { + this._counter = aValue; + if (this._counter) + this.indicator.setAttribute("counter", "true"); + else + this.indicator.removeAttribute("counter"); + // We have to set the attribute instead of using the property because the + // XBL binding isn't applied if the element is invisible for any reason. + this._indicatorCounter.setAttribute("value", aValue); + } + return aValue; + }, + _counter: null, + + /** + * Progress indication to display, from 0 to 100, or -1 if unknown. The + * progress bar is hidden if the current progress is unknown and no status + * text is set in the "counter" property. + */ + set percentComplete(aValue) + { + if (!this._operational) { + return this._percentComplete; + } + + if (this._percentComplete !== aValue) { + this._percentComplete = aValue; + if (this._percentComplete >= 0) + this.indicator.setAttribute("progress", "true"); + else + this.indicator.removeAttribute("progress"); + // We have to set the attribute instead of using the property because the + // XBL binding isn't applied if the element is invisible for any reason. + this._indicatorProgress.setAttribute("value", Math.max(aValue, 0)); + } + return aValue; + }, + _percentComplete: null, + + /** + * Indicates whether the progress won't advance because of a paused state. + * Setting this property forces a paused progress bar to be displayed, even if + * the current progress information is unavailable. + */ + set paused(aValue) + { + if (!this._operational) { + return this._paused; + } + + if (this._paused != aValue) { + this._paused = aValue; + if (this._paused) { + this.indicator.setAttribute("paused", "true") + } else { + this.indicator.removeAttribute("paused"); + } + } + return aValue; + }, + _paused: false, + + /** + * Set when the indicator should draw user attention to itself. + */ + set attention(aValue) + { + if (!this._operational) { + return this._attention; + } + + if (this._attention != aValue) { + this._attention = aValue; + if (aValue) { + this.indicator.setAttribute("attention", "true"); + } else { + this.indicator.removeAttribute("attention"); + } + } + return aValue; + }, + _attention: false, + + ////////////////////////////////////////////////////////////////////////////// + //// User interface event functions + + onWindowUnload: function() + { + // This function is registered as an event listener, we can't use "this". + DownloadsIndicatorView.ensureTerminated(); + }, + + onCommand: function(aEvent) + { + if (DownloadsCommon.useToolkitUI) { + // The panel won't suppress attention for us, we need to clear now. + DownloadsCommon.getIndicatorData(window).attention = false; + BrowserDownloadsUI(); + } else { + DownloadsPanel.showPanel(); + } + + aEvent.stopPropagation(); + }, + + onDragOver: function(aEvent) + { + browserDragAndDrop.dragOver(aEvent); + }, + + onDrop: function(aEvent) + { + let dt = aEvent.dataTransfer; + // If dragged item is from our source, do not try to + // redownload already downloaded file. + if (dt.mozGetDataAt("application/x-moz-file", 0)) + return; + + let links = browserDragAndDrop.dropLinks(aEvent); + if (!links.length) + return; + let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document; + let handled = false; + for (let link of links) { + if (link.url.startsWith("about:")) + continue; + saveURL(link.url, link.name, null, true, true, null, sourceDoc); + handled = true; + } + if (handled) { + aEvent.preventDefault(); + } + }, + + /** + * Returns a reference to the main indicator element, or null if the element + * is not present in the browser window yet. + */ + get indicator() + { + let indicator = document.getElementById("downloads-indicator"); + if (!indicator) { + return null; + } + + // Once the element is loaded, it will never be unloaded. + delete this.indicator; + return this.indicator = indicator; + }, + + get indicatorAnchor() + { + delete this.indicatorAnchor; + return this.indicatorAnchor = + document.getElementById("downloads-indicator-anchor"); + }, + + get _indicatorCounter() + { + delete this._indicatorCounter; + return this._indicatorCounter = + document.getElementById("downloads-indicator-counter"); + }, + + get _indicatorProgress() + { + delete this._indicatorProgress; + return this._indicatorProgress = + document.getElementById("downloads-indicator-progress"); + } +}; + +Object.defineProperty(this, "DownloadsIndicatorView", { + value: DownloadsIndicatorView, + enumerable: true, + writable: false +}); diff --git a/browser/components/downloads/content/indicatorOverlay.xul b/browser/components/downloads/content/indicatorOverlay.xul new file mode 100644 index 000000000..f62e812b1 --- /dev/null +++ b/browser/components/downloads/content/indicatorOverlay.xul @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<!-- -*- Mode: HTML; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- --> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://browser/content/downloads/downloads.css"?> +<?xml-stylesheet href="chrome://browser/skin/downloads/downloads.css"?> + +<!DOCTYPE overlay [ + <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > + %browserDTD; + <!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd" > + %downloadsDTD; +]> + +<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="indicatorOverlay"> + + <popupset> + <!-- The downloads indicator is placed in its final toolbar location + programmatically, and can be shown temporarily even when its + placeholder is removed from the toolbars. Its initial location within + the document must not be a toolbar or the toolbar palette, otherwise the + toolbar handling code could remove it from the document. --> + <toolbarbutton id="downloads-indicator" + class="toolbarbutton-1 chromeclass-toolbar-additional" + tooltiptext="&downloads.tooltip;" + collapsed="true" + oncommand="DownloadsIndicatorView.onCommand(event);" + ondrop="DownloadsIndicatorView.onDrop(event);" + ondragover="DownloadsIndicatorView.onDragOver(event);" + ondragenter="DownloadsIndicatorView.onDragOver(event);" + ondragleave="DownloadsIndicatorView.onDragLeave(event);" + skipintoolbarset="true"> + <!-- The panel's anchor area is smaller than the outer button, but must + always be visible and must not move or resize when the indicator + state changes, otherwise the panel could change its position or lose + its arrow unexpectedly. --> + <stack id="downloads-indicator-anchor" + class="toolbarbutton-icon"> + <vbox id="downloads-indicator-progress-area" + pack="center"> + <description id="downloads-indicator-counter"/> + <progressmeter id="downloads-indicator-progress" + class="plain" + min="0" + max="100"/> + </vbox> + <vbox id="downloads-indicator-icon"/> + <vbox id="downloads-indicator-notification"/> + </stack> + <label class="toolbarbutton-text" crop="right" flex="1" + value="&downloads.label;"/> + </toolbarbutton> + </popupset> +</overlay> diff --git a/browser/components/downloads/jar.mn b/browser/components/downloads/jar.mn new file mode 100644 index 000000000..567929344 --- /dev/null +++ b/browser/components/downloads/jar.mn @@ -0,0 +1,18 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/downloads/download.xml (content/download.xml) + content/browser/downloads/download.css (content/download.css) + content/browser/downloads/downloads.css (content/downloads.css) + content/browser/downloads/downloads.js (content/downloads.js) + content/browser/downloads/downloadsOverlay.xul (content/downloadsOverlay.xul) + content/browser/downloads/indicator.js (content/indicator.js) + content/browser/downloads/indicatorOverlay.xul (content/indicatorOverlay.xul) + content/browser/downloads/allDownloadsViewOverlay.xul (content/allDownloadsViewOverlay.xul) + content/browser/downloads/allDownloadsViewOverlay.js (content/allDownloadsViewOverlay.js) + content/browser/downloads/allDownloadsViewOverlay.css (content/allDownloadsViewOverlay.css) + content/browser/downloads/contentAreaDownloadsView.xul (content/contentAreaDownloadsView.xul) + content/browser/downloads/contentAreaDownloadsView.js (content/contentAreaDownloadsView.js) + content/browser/downloads/contentAreaDownloadsView.css (content/contentAreaDownloadsView.css) diff --git a/browser/components/downloads/moz.build b/browser/components/downloads/moz.build new file mode 100644 index 000000000..81a3165a3 --- /dev/null +++ b/browser/components/downloads/moz.build @@ -0,0 +1,22 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] + +EXTRA_COMPONENTS += [ + 'BrowserDownloads.manifest', + 'DownloadsStartup.js', + 'DownloadsUI.js', +] + +EXTRA_JS_MODULES += [ + 'DownloadsLogger.jsm', + 'DownloadsTaskbar.jsm', + 'DownloadsViewUI.jsm', +] + +EXTRA_PP_JS_MODULES += [ + 'DownloadsCommon.jsm', +] diff --git a/browser/components/feeds/BrowserFeeds.manifest b/browser/components/feeds/BrowserFeeds.manifest new file mode 100644 index 000000000..011fa79ff --- /dev/null +++ b/browser/components/feeds/BrowserFeeds.manifest @@ -0,0 +1,15 @@ +component {229fa115-9412-4d32-baf3-2fc407f76fb1} FeedConverter.js +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.video.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.audio.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +component {2376201c-bbc6-472f-9b62-7548040a61c6} FeedConverter.js +contract @mozilla.org/browser/feeds/result-service;1 {2376201c-bbc6-472f-9b62-7548040a61c6} +component {4f91ef2e-57ba-472e-ab7a-b4999e42d6c0} FeedConverter.js +contract @mozilla.org/network/protocol;1?name=feed {4f91ef2e-57ba-472e-ab7a-b4999e42d6c0} +component {1c31ed79-accd-4b94-b517-06e0c81999d5} FeedConverter.js +contract @mozilla.org/network/protocol;1?name=pcast {1c31ed79-accd-4b94-b517-06e0c81999d5} +component {49bb6593-3aff-4eb3-a068-2712c28bd58e} FeedWriter.js +contract @mozilla.org/browser/feeds/result-writer;1 {49bb6593-3aff-4eb3-a068-2712c28bd58e} +component {792a7e82-06a0-437c-af63-b2d12e808acc} WebContentConverter.js +contract @mozilla.org/embeddor.implemented/web-content-handler-registrar;1 {792a7e82-06a0-437c-af63-b2d12e808acc} +category app-startup WebContentConverter service,@mozilla.org/embeddor.implemented/web-content-handler-registrar;1 diff --git a/browser/components/feeds/FeedConverter.js b/browser/components/feeds/FeedConverter.js new file mode 100644 index 000000000..5ccb09f0f --- /dev/null +++ b/browser/components/feeds/FeedConverter.js @@ -0,0 +1,591 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/debug.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +function LOG(str) { + dump("*** " + str + "\n"); +} + +const FS_CONTRACTID = "@mozilla.org/browser/feeds/result-service;1"; +const FPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=feed"; +const PCPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=pcast"; + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_ANY = "*/*"; + +const PREF_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default"; + +function getPrefAppForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_APP; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_APP; + + default: + return PREF_SELECTED_APP; + } +} + +function getPrefWebForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_WEB; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_WEB; + + default: + return PREF_SELECTED_WEB; + } +} + +function getPrefActionForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_ACTION; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_ACTION; + + default: + return PREF_SELECTED_ACTION; + } +} + +function getPrefReaderForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_READER; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_READER; + + default: + return PREF_SELECTED_READER; + } +} + +function safeGetCharPref(pref, defaultValue) { + var prefs = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + try { + return prefs.getCharPref(pref); + } + catch (e) { + } + return defaultValue; +} + +function FeedConverter() { +} +FeedConverter.prototype = { + classID: Components.ID("{229fa115-9412-4d32-baf3-2fc407f76fb1}"), + + /** + * This is the downloaded text data for the feed. + */ + _data: null, + + /** + * This is the object listening to the conversion, which is ultimately the + * docshell for the load. + */ + _listener: null, + + /** + * Records if the feed was sniffed + */ + _sniffed: false, + + /** + * See nsIStreamConverter.idl + */ + convert: function(sourceStream, sourceType, destinationType, + context) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * See nsIStreamConverter.idl + */ + asyncConvertData: function(sourceType, destinationType, + listener, context) { + this._listener = listener; + }, + + /** + * Whether or not the preview page is being forced. + */ + _forcePreviewPage: false, + + /** + * Release our references to various things once we're done using them. + */ + _releaseHandles: function() { + this._listener = null; + this._request = null; + this._processor = null; + }, + + /** + * See nsIFeedResultListener.idl + */ + handleResult: function(result) { + // Feeds come in various content types, which our feed sniffer coerces to + // the maybe.feed type. However, feeds are used as a transport for + // different data types, e.g. news/blogs (traditional feed), video/audio + // (podcasts) and photos (photocasts, photostreams). Each of these is + // different in that there's a different class of application suitable for + // handling feeds of that type, but without a content-type differentiation + // it is difficult for us to disambiguate. + // + // The other problem is that if the user specifies an auto-action handler + // for one feed application, the fact that the content type is shared means + // that all other applications will auto-load with that handler too, + // regardless of the content-type. + // + // This means that content-type alone is not enough to determine whether + // or not a feed should be auto-handled. This means that for feeds we need + // to always use this stream converter, even when an auto-action is + // specified, not the basic one provided by WebContentConverter. This + // converter needs to consume all of the data and parse it, and based on + // that determination make a judgment about type. + // + // Since there are no content types for this content, and I'm not going to + // invent any, the upshot is that while a user can set an auto-handler for + // generic feed content, the system will prevent them from setting an auto- + // handler for other stream types. In those cases, the user will always see + // the preview page and have to select a handler. We can guess and show + // a client handler, but will not be able to show web handlers for those + // types. + // + // If this is just a feed, not some kind of specialized application, then + // auto-handlers can be set and we should obey them. + try { + var feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + if (!this._forcePreviewPage && result.doc) { + var feed = result.doc.QueryInterface(Ci.nsIFeed); + var handler = safeGetCharPref(getPrefActionForType(feed.type), "ask"); + + if (handler != "ask") { + if (handler == "reader") + handler = safeGetCharPref(getPrefReaderForType(feed.type), "bookmarks"); + switch (handler) { + case "web": + var wccr = + Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + if ((feed.type == Ci.nsIFeed.TYPE_FEED && + wccr.getAutoHandler(TYPE_MAYBE_FEED)) || + (feed.type == Ci.nsIFeed.TYPE_VIDEO && + wccr.getAutoHandler(TYPE_MAYBE_VIDEO_FEED)) || + (feed.type == Ci.nsIFeed.TYPE_AUDIO && + wccr.getAutoHandler(TYPE_MAYBE_AUDIO_FEED))) { + wccr.loadPreferredHandler(this._request); + return; + } + break; + + default: + LOG("unexpected handler: " + handler); + // fall through -- let feed service handle error + case "bookmarks": + case "client": + try { + var title = feed.title ? feed.title.plainText() : ""; + var desc = feed.subtitle ? feed.subtitle.plainText() : ""; + feedService.addToClientReader(result.uri.spec, title, desc, feed.type); + return; + } catch(ex) { /* fallback to preview mode */ } + } + } + } + + var ios = + Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var chromeChannel; + + // handling a redirect, hence forwarding the loadInfo from the old channel + // to the newchannel. + var oldChannel = this._request.QueryInterface(Ci.nsIChannel); + var loadInfo = oldChannel.loadInfo; + + // If there was no automatic handler, or this was a podcast, + // photostream or some other kind of application, show the preview page + // if the parser returned a document. + if (result.doc) { + + // Store the result in the result service so that the display + // page can access it. + feedService.addFeedResult(result); + + // Now load the actual XUL document. + var aboutFeedsURI = ios.newURI("about:feeds", null, null); + chromeChannel = ios.newChannelFromURIWithLoadInfo(aboutFeedsURI, loadInfo); + chromeChannel.originalURI = result.uri; + chromeChannel.owner = + Services.scriptSecurityManager.getNoAppCodebasePrincipal(aboutFeedsURI); + } else { + chromeChannel = ios.newChannelFromURIWithLoadInfo(result.uri, loadInfo); + } + + chromeChannel.loadGroup = this._request.loadGroup; + chromeChannel.asyncOpen2(this._listener); + } + finally { + this._releaseHandles(); + } + }, + + /** + * See nsIStreamListener.idl + */ + onDataAvailable: function(request, context, inputStream, + sourceOffset, count) { + if (this._processor) + this._processor.onDataAvailable(request, context, inputStream, + sourceOffset, count); + }, + + /** + * See nsIRequestObserver.idl + */ + onStartRequest: function(request, context) { + var channel = request.QueryInterface(Ci.nsIChannel); + + // Check for a header that tells us there was no sniffing + // The value doesn't matter. + try { + var httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + // Make sure to check requestSucceeded before the potentially-throwing + // getResponseHeader. + if (!httpChannel.requestSucceeded) { + // Just give up, but don't forget to cancel the channel first! + request.cancel(Cr.NS_BINDING_ABORTED); + return; + } + var noSniff = httpChannel.getResponseHeader("X-Moz-Is-Feed"); + } + catch (ex) { + this._sniffed = true; + } + + this._request = request; + + // Save and reset the forced state bit early, in case there's some kind of + // error. + var feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + this._forcePreviewPage = feedService.forcePreviewPage; + feedService.forcePreviewPage = false; + + // Parse feed data as it comes in + this._processor = + Cc["@mozilla.org/feed-processor;1"]. + createInstance(Ci.nsIFeedProcessor); + this._processor.listener = this; + this._processor.parseAsync(null, channel.URI); + + this._processor.onStartRequest(request, context); + }, + + /** + * See nsIRequestObserver.idl + */ + onStopRequest: function(request, context, status) { + if (this._processor) + this._processor.onStopRequest(request, context, status); + }, + + /** + * See nsISupports.idl + */ + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIFeedResultListener) || + iid.equals(Ci.nsIStreamConverter) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver)|| + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, +}; + +/** + * Keeps parsed FeedResults around for use elsewhere in the UI after the stream + * converter completes. + */ +function FeedResultService() { +} + +FeedResultService.prototype = { + classID: Components.ID("{2376201c-bbc6-472f-9b62-7548040a61c6}"), + + /** + * A URI spec -> [nsIFeedResult] hash. We have to keep a list as the + * value in case the same URI is requested concurrently. + */ + _results: { }, + + /** + * See nsIFeedResultService.idl + */ + forcePreviewPage: false, + + /** + * See nsIFeedResultService.idl + */ + addToClientReader: function(spec, title, subtitle, feedType) { + var prefs = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + var handler = safeGetCharPref(getPrefActionForType(feedType), "bookmarks"); + if (handler == "ask" || handler == "reader") + handler = safeGetCharPref(getPrefReaderForType(feedType), "bookmarks"); + + switch (handler) { + case "client": + var clientApp = prefs.getComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile); + + // For the benefit of applications that might know how to deal with more + // URLs than just feeds, send feed: URLs in the following format: + // + // http urls: replace scheme with feed, e.g. + // http://foo.com/index.rdf -> feed://foo.com/index.rdf + // other urls: prepend feed: scheme, e.g. + // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf + var ios = + Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var feedURI = ios.newURI(spec, null, null); + if (feedURI.schemeIs("http")) { + feedURI.scheme = "feed"; + spec = feedURI.spec; + } + else + spec = "feed:" + spec; + + // Retrieving the shell service might fail on some systems, most + // notably systems where GNOME is not installed. + try { + var ss = + Cc["@mozilla.org/browser/shell-service;1"]. + getService(Ci.nsIShellService); + ss.openApplicationWithURI(clientApp, spec); + } catch(e) { + // If we couldn't use the shell service, fallback to using a + // nsIProcess instance + var p = + Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + p.init(clientApp); + p.run(false, [spec], 1); + } + break; + + default: + // "web" should have been handled elsewhere + LOG("unexpected handler: " + handler); + // fall through + case "bookmarks": + var wm = + Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + var topWindow = wm.getMostRecentWindow("navigator:browser"); + topWindow.PlacesCommandHook.addLiveBookmark(spec, title, subtitle); + break; + } + }, + + /** + * See nsIFeedResultService.idl + */ + addFeedResult: function(feedResult) { + NS_ASSERT(feedResult.uri != null, "null URI!"); + NS_ASSERT(feedResult.uri != null, "null feedResult!"); + var spec = feedResult.uri.spec; + if(!this._results[spec]) + this._results[spec] = []; + this._results[spec].push(feedResult); + }, + + /** + * See nsIFeedResultService.idl + */ + getFeedResult: function(uri) { + NS_ASSERT(uri != null, "null URI!"); + var resultList = this._results[uri.spec]; + for (var i in resultList) { + if (resultList[i].uri == uri) + return resultList[i]; + } + return null; + }, + + /** + * See nsIFeedResultService.idl + */ + removeFeedResult: function(uri) { + NS_ASSERT(uri != null, "null URI!"); + var resultList = this._results[uri.spec]; + if (!resultList) + return; + var deletions = 0; + for (var i = 0; i < resultList.length; ++i) { + if (resultList[i].uri == uri) { + delete resultList[i]; + ++deletions; + } + } + + // send the holes to the end + resultList.sort(); + // and trim the list + resultList.splice(resultList.length - deletions, deletions); + if (resultList.length == 0) + delete this._results[uri.spec]; + }, + + createInstance: function(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIFeedResultService) || + iid.equals(Ci.nsIFactory) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, +}; + +/** + * A protocol handler that attempts to deal with the variant forms of feed: + * URIs that are actually either http or https. + */ +function GenericProtocolHandler() { +} +GenericProtocolHandler.prototype = { + _init: function(scheme) { + var ios = + Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + this._http = ios.getProtocolHandler("http"); + this._scheme = scheme; + }, + + get scheme() { + return this._scheme; + }, + + get protocolFlags() { + return this._http.protocolFlags; + }, + + get defaultPort() { + return this._http.defaultPort; + }, + + allowPort: function(port, scheme) { + return this._http.allowPort(port, scheme); + }, + + newURI: function(spec, originalCharset, baseURI) { + // Feed URIs can be either nested URIs of the form feed:realURI (in which + // case we create a nested URI for the realURI) or feed://example.com, in + // which case we create a nested URI for the real protocol which is http. + + var scheme = this._scheme + ":"; + if (spec.substr(0, scheme.length) != scheme) + throw Cr.NS_ERROR_MALFORMED_URI; + + var prefix = spec.substr(scheme.length, 2) == "//" ? "http:" : ""; + var inner = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService).newURI(spec.replace(scheme, prefix), + originalCharset, baseURI); + var netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil); + const URI_INHERITS_SECURITY_CONTEXT = Ci.nsIProtocolHandler + .URI_INHERITS_SECURITY_CONTEXT; + if (netutil.URIChainHasFlags(inner, URI_INHERITS_SECURITY_CONTEXT)) + throw Cr.NS_ERROR_MALFORMED_URI; + + var uri = netutil.newSimpleNestedURI(inner); + uri.spec = inner.spec.replace(prefix, scheme); + return uri; + }, + + newChannel2: function(aUri, aLoadInfo) { + var inner = aUri.QueryInterface(Ci.nsINestedURI).innerURI; + var channel = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newChannelFromURIWithLoadInfo(inner, aLoadInfo); + + if (channel instanceof Components.interfaces.nsIHttpChannel) + // Set this so we know this is supposed to be a feed + channel.setRequestHeader("X-Moz-Is-Feed", "1", false); + channel.originalURI = aUri; + return channel; + }, + + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIProtocolHandler) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function FeedProtocolHandler() { + this._init('feed'); +} +FeedProtocolHandler.prototype = new GenericProtocolHandler(); +FeedProtocolHandler.prototype.classID = Components.ID("{4f91ef2e-57ba-472e-ab7a-b4999e42d6c0}"); + +function PodCastProtocolHandler() { + this._init('pcast'); +} +PodCastProtocolHandler.prototype = new GenericProtocolHandler(); +PodCastProtocolHandler.prototype.classID = Components.ID("{1c31ed79-accd-4b94-b517-06e0c81999d5}"); + +var components = [FeedConverter, + FeedResultService, + FeedProtocolHandler, + PodCastProtocolHandler]; + + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/components/feeds/FeedWriter.js b/browser/components/feeds/FeedWriter.js new file mode 100644 index 000000000..ddd78ab1c --- /dev/null +++ b/browser/components/feeds/FeedWriter.js @@ -0,0 +1,1386 @@ +# -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +const 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/NetUtil.jsm"); + +const FEEDWRITER_CID = Components.ID("{49bb6593-3aff-4eb3-a068-2712c28bd58e}"); +const FEEDWRITER_CONTRACTID = "@mozilla.org/browser/feeds/result-writer;1"; + +function LOG(str) { + var prefB = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + var shouldLog = prefB.getBoolPref("feeds.log", false); + + if (shouldLog) + dump("*** Feeds: " + str + "\n"); +} + +/** + * Wrapper function for nsIIOService::newURI. + * @param aURLSpec + * The URL string from which to create an nsIURI. + * @returns an nsIURI object, or null if the creation of the URI failed. + */ +function makeURI(aURLSpec, aCharset) { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + try { + return ios.newURI(aURLSpec, aCharset, null); + } catch (ex) { } + + return null; +} + +const XML_NS = "http://www.w3.org/XML/1998/namespace"; +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const URI_BUNDLE = "chrome://browser/locale/feeds/subscribe.properties"; + +const PREF_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default"; + +const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI"; + +const TITLE_ID = "feedTitleText"; +const SUBTITLE_ID = "feedSubtitleText"; + +function getPrefAppForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_APP; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_APP; + + default: + return PREF_SELECTED_APP; + } +} + +function getPrefWebForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_WEB; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_WEB; + + default: + return PREF_SELECTED_WEB; + } +} + +function getPrefActionForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_ACTION; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_ACTION; + + default: + return PREF_SELECTED_ACTION; + } +} + +function getPrefReaderForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_READER; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_READER; + + default: + return PREF_SELECTED_READER; + } +} + +/** + * Converts a number of bytes to the appropriate unit that results in a + * number that needs fewer than 4 digits + * + * @return a pair: [new value with 3 sig. figs., its unit] + */ +function convertByteUnits(aBytes) { + var units = ["bytes", "kilobyte", "megabyte", "gigabyte"]; + let unitIndex = 0; + + // convert to next unit if it needs 4 digits (after rounding), but only if + // we know the name of the next unit + while ((aBytes >= 999.5) && (unitIndex < units.length - 1)) { + aBytes /= 1024; + unitIndex++; + } + + // Get rid of insignificant bits by truncating to 1 or 0 decimal points + // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 + aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0); + + return [aBytes, units[unitIndex]]; +} + +function FeedWriter() {} +FeedWriter.prototype = { + _mimeSvc : Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService), + + _getPropertyAsBag: function(container, property) { + return container.fields.getProperty(property). + QueryInterface(Ci.nsIPropertyBag2); + }, + + _getPropertyAsString: function(container, property) { + try { + return container.fields.getPropertyAsAString(property); + } + catch (e) { + } + return ""; + }, + + _setContentText: function(id, text) { + this._contentSandbox.element = this._document.getElementById(id); + this._contentSandbox.textNode = text.createDocumentFragment(this._contentSandbox.element); + var codeStr = + "while (element.hasChildNodes()) " + + " element.removeChild(element.firstChild);" + + "element.appendChild(textNode);"; + if (text.base) { + this._contentSandbox.spec = text.base.spec; + codeStr += "element.setAttributeNS('" + XML_NS + "', 'base', spec);"; + } + Cu.evalInSandbox(codeStr, this._contentSandbox); + this._contentSandbox.element = null; + this._contentSandbox.textNode = null; + }, + + /** + * Safely sets the href attribute on an anchor tag, providing the URI + * specified can be loaded according to rules. + * @param element + * The element to set a URI attribute on + * @param attribute + * The attribute of the element to set the URI to, e.g. href or src + * @param uri + * The URI spec to set as the href + */ + _safeSetURIAttribute: + function(element, attribute, uri) { + var secman = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + const flags = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; + try { + secman.checkLoadURIStrWithPrincipal(this._feedPrincipal, uri, flags); + // checkLoadURIStrWithPrincipal will throw if the link URI should not be + // loaded, either because our feedURI isn't allowed to load it or per + // the rules specified in |flags|, so we'll never "linkify" the link... + } + catch (e) { + // Not allowed to load this link because secman.checkLoadURIStr threw + return; + } + + this._contentSandbox.element = element; + this._contentSandbox.uri = uri; + var codeStr = "element.setAttribute('" + attribute + "', uri);"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + /** + * Use this sandbox to run any dom manipulation code on nodes which + * are already inserted into the content document. + */ + __contentSandbox: null, + get _contentSandbox() { + // This whole sandbox setup is totally archaic. It was introduced in bug + // 360529, presumably before the existence of a solid security membrane, + // since all of the manipulation of content here should be made safe by + // Xrays. + // Now that anonymous content is no longer content-accessible, manipulating + // the xml stylesheet content can't be done from content anymore. + // + // The right solution would be to rip out all of this sandbox junk and + // manipulate the DOM directly, but that would require a lot of rewriting. + // So, for now, we just give the sandbox an nsExpandedPrincipal with []. + // This has the effect of giving it Xrays, and making it same-origin with + // the XBL scope, thereby letting it manipulate anonymous content. + if (!this.__contentSandbox) + this.__contentSandbox = new Cu.Sandbox([this._window], + {sandboxName: 'FeedWriter'}); + + return this.__contentSandbox; + }, + + /** + * Calls doCommand for a given XUL element within the context of the + * content document. + * + * @param aElement + * the XUL element to call doCommand() on. + */ + _safeDoCommand: function(aElement) { + this._contentSandbox.element = aElement; + Cu.evalInSandbox("element.doCommand();", this._contentSandbox); + this._contentSandbox.element = null; + }, + + __faviconService: null, + get _faviconService() { + if (!this.__faviconService) + this.__faviconService = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + + return this.__faviconService; + }, + + __bundle: null, + get _bundle() { + if (!this.__bundle) { + this.__bundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(URI_BUNDLE); + } + return this.__bundle; + }, + + _getFormattedString: function(key, params) { + return this._bundle.formatStringFromName(key, params, params.length); + }, + + _getString: function(key) { + return this._bundle.GetStringFromName(key); + }, + + /* Magic helper methods to be used instead of xbl properties */ + _getSelectedItemFromMenulist: function(aList) { + var node = aList.firstChild.firstChild; + while (node) { + if (node.localName == "menuitem" && node.getAttribute("selected") == "true") + return node; + + node = node.nextSibling; + } + + return null; + }, + + _setCheckboxCheckedState: function(aCheckbox, aValue) { + // see checkbox.xml, xbl bindings are not applied within the sandbox! + this._contentSandbox.checkbox = aCheckbox; + var codeStr; + var change = (aValue != (aCheckbox.getAttribute('checked') == 'true')); + if (aValue) + codeStr = "checkbox.setAttribute('checked', 'true'); "; + else + codeStr = "checkbox.removeAttribute('checked'); "; + + if (change) { + this._contentSandbox.document = this._document; + codeStr += "var event = document.createEvent('Events'); " + + "event.initEvent('CheckboxStateChange', true, true);" + + "checkbox.dispatchEvent(event);" + } + + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + /** + * Returns a date suitable for displaying in the feed preview. + * If the date cannot be parsed, the return value is "false". + * @param dateString + * A date as extracted from a feed entry. (entry.updated) + */ + _parseDate: function(dateString) { + // Convert the date into the user's local time zone + dateObj = new Date(dateString); + + // Make sure the date we're given is valid. + if (!dateObj.getTime()) + return false; + + var dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"]. + getService(Ci.nsIScriptableDateFormat); + return dateService.FormatDateTime("", dateService.dateFormatLong, dateService.timeFormatNoSeconds, + dateObj.getFullYear(), dateObj.getMonth()+1, dateObj.getDate(), + dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds()); + }, + + /** + * Returns the feed type. + */ + __feedType: null, + _getFeedType: function() { + if (this.__feedType != null) + return this.__feedType; + + try { + // grab the feed because it's got the feed.type in it. + var container = this._getContainer(); + var feed = container.QueryInterface(Ci.nsIFeed); + this.__feedType = feed.type; + return feed.type; + } catch (ex) { } + + return Ci.nsIFeed.TYPE_FEED; + }, + + /** + * Maps a feed type to a maybe-feed mimetype. + */ + _getMimeTypeForFeedType: function() { + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + return TYPE_MAYBE_VIDEO_FEED; + + case Ci.nsIFeed.TYPE_AUDIO: + return TYPE_MAYBE_AUDIO_FEED; + + default: + return TYPE_MAYBE_FEED; + } + }, + + /** + * Writes the feed title into the preview document. + * @param container + * The feed container + */ + _setTitleText: function(container) { + if (container.title) { + var title = container.title.plainText(); + this._setContentText(TITLE_ID, container.title); + this._contentSandbox.document = this._document; + this._contentSandbox.title = title; + var codeStr = "document.title = title;" + Cu.evalInSandbox(codeStr, this._contentSandbox); + } + + var feed = container.QueryInterface(Ci.nsIFeed); + if (feed && feed.subtitle) + this._setContentText(SUBTITLE_ID, container.subtitle); + }, + + /** + * Writes the title image into the preview document if one is present. + * @param container + * The feed container + */ + _setTitleImage: function(container) { + try { + var parts = container.image; + + // Set up the title image (supplied by the feed) + var feedTitleImage = this._document.getElementById("feedTitleImage"); + this._safeSetURIAttribute(feedTitleImage, "src", + parts.getPropertyAsAString("url")); + + // Set up the title image link + var feedTitleLink = this._document.getElementById("feedTitleLink"); + + var titleText = this._getFormattedString("linkTitleTextFormat", + [parts.getPropertyAsAString("title")]); + this._contentSandbox.feedTitleLink = feedTitleLink; + this._contentSandbox.titleText = titleText; + this._contentSandbox.feedTitleText = this._document.getElementById("feedTitleText"); + this._contentSandbox.titleImageWidth = parseInt(parts.getPropertyAsAString("width")) + 15; + + // Fix the margin on the main title, so that the image doesn't run over + // the underline + var codeStr = "feedTitleLink.setAttribute('title', titleText); " + + "feedTitleText.style.marginRight = titleImageWidth + 'px';"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + this._contentSandbox.feedTitleLink = null; + this._contentSandbox.titleText = null; + this._contentSandbox.feedTitleText = null; + this._contentSandbox.titleImageWidth = null; + + this._safeSetURIAttribute(feedTitleLink, "href", + parts.getPropertyAsAString("link")); + } + catch (e) { + LOG("Failed to set Title Image (this is benign): " + e); + } + }, + + /** + * Writes all entries contained in the feed. + * @param container + * The container of entries in the feed + */ + _writeFeedContent: function(container) { + // Build the actual feed content + var feed = container.QueryInterface(Ci.nsIFeed); + if (feed.items.length == 0) + return; + + this._contentSandbox.feedContent = + this._document.getElementById("feedContent"); + + for (var i = 0; i < feed.items.length; ++i) { + var entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry); + entry.QueryInterface(Ci.nsIFeedContainer); + + var entryContainer = this._document.createElementNS(HTML_NS, "div"); + entryContainer.className = "entry"; + + // If the entry has a title, make it a link + if (entry.title) { + var a = this._document.createElementNS(HTML_NS, "a"); + var span = this._document.createElementNS(HTML_NS, "span"); + a.appendChild(span); + if (entry.title.base) + span.setAttributeNS(XML_NS, "base", entry.title.base.spec); + span.appendChild(entry.title.createDocumentFragment(a)); + + // Entries are not required to have links, so entry.link can be null. + if (entry.link) + this._safeSetURIAttribute(a, "href", entry.link.spec); + + var title = this._document.createElementNS(HTML_NS, "h3"); + title.appendChild(a); + + var lastUpdated = this._parseDate(entry.updated); + if (lastUpdated) { + var dateDiv = this._document.createElementNS(HTML_NS, "div"); + dateDiv.className = "lastUpdated"; + dateDiv.textContent = lastUpdated; + title.appendChild(dateDiv); + } + + entryContainer.appendChild(title); + } + + var body = this._document.createElementNS(HTML_NS, "div"); + var summary = entry.summary || entry.content; + var docFragment = null; + if (summary) { + if (summary.base) + body.setAttributeNS(XML_NS, "base", summary.base.spec); + else + LOG("no base?"); + docFragment = summary.createDocumentFragment(body); + if (docFragment) + body.appendChild(docFragment); + + // If the entry doesn't have a title, append a # permalink + // See http://scripting.com/rss.xml for an example + if (!entry.title && entry.link) { + var a = this._document.createElementNS(HTML_NS, "a"); + a.appendChild(this._document.createTextNode("#")); + this._safeSetURIAttribute(a, "href", entry.link.spec); + body.appendChild(this._document.createTextNode(" ")); + body.appendChild(a); + } + + } + body.className = "feedEntryContent"; + entryContainer.appendChild(body); + + if (entry.enclosures && entry.enclosures.length > 0) { + var enclosuresDiv = this._buildEnclosureDiv(entry); + entryContainer.appendChild(enclosuresDiv); + } + + this._contentSandbox.entryContainer = entryContainer; + this._contentSandbox.clearDiv = + this._document.createElementNS(HTML_NS, "div"); + this._contentSandbox.clearDiv.style.clear = "both"; + + var codeStr = "feedContent.appendChild(entryContainer); " + + "feedContent.appendChild(clearDiv);" + Cu.evalInSandbox(codeStr, this._contentSandbox); + } + + this._contentSandbox.feedContent = null; + this._contentSandbox.entryContainer = null; + this._contentSandbox.clearDiv = null; + }, + + /** + * Takes a url to a media item and returns the best name it can come up with. + * Frequently this is the filename portion (e.g. passing in + * http://example.com/foo.mpeg would return "foo.mpeg"), but in more complex + * cases, this will return the entire url (e.g. passing in + * http://example.com/somedirectory/ would return + * http://example.com/somedirectory/). + * @param aURL + * The URL string from which to create a display name + * @returns a string + */ + _getURLDisplayName: function(aURL) { + var url = makeURI(aURL); + url.QueryInterface(Ci.nsIURL); + if (url == null || url.fileName.length == 0) + return decodeURIComponent(aURL); + + return decodeURIComponent(url.fileName); + }, + + /** + * Takes a FeedEntry with enclosures, generates the HTML code to represent + * them, and returns that. + * @param entry + * FeedEntry with enclosures + * @returns element + */ + _buildEnclosureDiv: function(entry) { + var enclosuresDiv = this._document.createElementNS(HTML_NS, "div"); + enclosuresDiv.className = "enclosures"; + + enclosuresDiv.appendChild(this._document.createTextNode(this._getString("mediaLabel"))); + + var roundme = function(n) { + return (Math.round(n * 100) / 100).toLocaleString(); + } + + for (var i_enc = 0; i_enc < entry.enclosures.length; ++i_enc) { + var enc = entry.enclosures.queryElementAt(i_enc, Ci.nsIWritablePropertyBag2); + + if (!(enc.hasKey("url"))) + continue; + + var enclosureDiv = this._document.createElementNS(HTML_NS, "div"); + enclosureDiv.setAttribute("class", "enclosure"); + + var mozicon = "moz-icon://.txt?size=16"; + var type_text = null; + var size_text = null; + + if (enc.hasKey("type")) { + type_text = enc.get("type"); + try { + var handlerInfoWrapper = this._mimeSvc.getFromTypeAndExtension(enc.get("type"), null); + + if (handlerInfoWrapper) + type_text = handlerInfoWrapper.description; + + if (type_text && type_text.length > 0) + mozicon = "moz-icon://goat?size=16&contentType=" + enc.get("type"); + + } catch (ex) { } + + } + + if (enc.hasKey("length") && /^[0-9]+$/.test(enc.get("length"))) { + var enc_size = convertByteUnits(parseInt(enc.get("length"))); + + var size_text = this._getFormattedString("enclosureSizeText", + [enc_size[0], this._getString(enc_size[1])]); + } + + var iconimg = this._document.createElementNS(HTML_NS, "img"); + iconimg.setAttribute("src", mozicon); + iconimg.setAttribute("class", "type-icon"); + enclosureDiv.appendChild(iconimg); + + enclosureDiv.appendChild(this._document.createTextNode( " " )); + + var enc_href = this._document.createElementNS(HTML_NS, "a"); + enc_href.appendChild(this._document.createTextNode(this._getURLDisplayName(enc.get("url")))); + this._safeSetURIAttribute(enc_href, "href", enc.get("url")); + enclosureDiv.appendChild(enc_href); + + if (type_text && size_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + type_text + ", " + size_text + ")")); + + else if (type_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + type_text + ")")) + + else if (size_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + size_text + ")")) + + enclosuresDiv.appendChild(enclosureDiv); + } + + return enclosuresDiv; + }, + + /** + * Gets a valid nsIFeedContainer object from the parsed nsIFeedResult. + * Displays error information if there was one. + * @param result + * The parsed feed result + * @returns A valid nsIFeedContainer object containing the contents of + * the feed. + */ + _getContainer: function(result) { + var feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + + result = null; + try { + result = + feedService.getFeedResult(this._getOriginalURI(this._window)); + } + catch (e) { + // Ignore. + } + + if (!result) { + LOG("Subscribe Preview: feed not available?!"); + return null; + } + + if (result.bozo) { + LOG("Subscribe Preview: feed result is bozo?!"); + } + + try { + var container = result.doc; + } + catch (e) { + LOG("Subscribe Preview: no result.doc? Why didn't the original reload?"); + return null; + } + return container; + }, + + /** + * Get the human-readable display name of a file. This could be the + * application name. + * @param file + * A nsIFile to look up the name of + * @returns The display name of the application represented by the file. + */ + _getFileDisplayName: function(file) { +#ifdef XP_WIN + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } +#endif + return file.leafName; + }, + + /** + * Helper method to set the selected application and system default + * reader menuitems details from a file object + * @param aMenuItem + * The menuitem on which the attributes should be set + * @param aFile + * The menuitem's associated file + */ + _initMenuItemWithFile: function(aMenuItem, aFile) { + this._contentSandbox.menuitem = aMenuItem; + this._contentSandbox.label = this._getFileDisplayName(aFile); + // For security reasons, access to moz-icon:file://... URIs is + // no longer allowed (indirect file system access from content). + // We use a dummy application instead to get a generic icon. + this._contentSandbox.image = "moz-icon://dummy.exe?size=16"; + var codeStr = "menuitem.setAttribute('label', label); " + + "menuitem.setAttribute('image', image);" + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + /** + * Helper method to get an element in the XBL binding where the handler + * selection UI lives + */ + _getUIElement: function(id) { + return this._document.getAnonymousElementByAttribute( + this._document.getElementById("feedSubscribeLine"), "anonid", id); + }, + + /** + * Displays a prompt from which the user may choose a (client) feed reader. + * @param aCallback the callback method, passes in true if a feed reader was + * selected, false otherwise. + */ + _chooseClientApp: function(aCallback) { + try { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == Ci.nsIFilePicker.returnOK) { + this._selectedApp = fp.file; + if (this._selectedApp) { + // XXXben - we need to compare this with the running instance + // executable just don't know how to do that via script + // XXXmano TBD: can probably add this to nsIShellService +#ifdef XP_WIN +#expand if (fp.file.leafName != "__MOZ_APP_NAME__.exe") { +#else +#expand if (fp.file.leafName != "__MOZ_APP_NAME__-bin") { +#endif + this._initMenuItemWithFile(this._contentSandbox.selectedAppMenuItem, + this._selectedApp); + + // Show and select the selected application menuitem + let codeStr = "selectedAppMenuItem.hidden = false;" + + "selectedAppMenuItem.doCommand();" + Cu.evalInSandbox(codeStr, this._contentSandbox); + if (aCallback) { + aCallback(true); + return; + } + } + } + } + if (aCallback) { + aCallback(false); + } + }.bind(this); + + fp.init(this._window, this._getString("chooseApplicationDialogTitle"), + Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + fp.open(fpCallback); + } catch(ex) { + } + }, + + _setAlwaysUseCheckedState: function(feedType) { + var checkbox = this._getUIElement("alwaysUse"); + if (checkbox) { + var alwaysUse = false; + try { + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + if (prefs.getCharPref(getPrefActionForType(feedType)) != "ask") + alwaysUse = true; + } + catch(ex) { } + this._setCheckboxCheckedState(checkbox, alwaysUse); + } + }, + + _setSubscribeUsingLabel: function() { + var stringLabel = "subscribeFeedUsing"; + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + stringLabel = "subscribeVideoPodcastUsing"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + stringLabel = "subscribeAudioPodcastUsing"; + break; + } + + this._contentSandbox.subscribeUsing = + this._getUIElement("subscribeUsingDescription"); + this._contentSandbox.label = this._getString(stringLabel); + var codeStr = "subscribeUsing.setAttribute('value', label);" + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + _setAlwaysUseLabel: function() { + var checkbox = this._getUIElement("alwaysUse"); + if (checkbox) { + if (this._handlersMenuList) { + var handlerName = this._getSelectedItemFromMenulist(this._handlersMenuList) + .getAttribute("label"); + var stringLabel = "alwaysUseForFeeds"; + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + stringLabel = "alwaysUseForVideoPodcasts"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + stringLabel = "alwaysUseForAudioPodcasts"; + break; + } + + this._contentSandbox.checkbox = checkbox; + this._contentSandbox.label = this._getFormattedString(stringLabel, [handlerName]); + + var codeStr = "checkbox.setAttribute('label', label);"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + } + } + }, + + // nsIDomEventListener + handleEvent: function(event) { + if (event.target.ownerDocument != this._document) { + LOG("FeedWriter.handleEvent: Someone passed the feed writer as a listener to the events of another document!"); + return; + } + + if (event.type == "command") { + switch (event.target.getAttribute("anonid")) { + case "subscribeButton": + this.subscribe(); + break; + case "chooseApplicationMenuItem": + /* Bug 351263: Make sure to not steal focus if the "Choose + * Application" item is being selected with the keyboard. We do this + * by ignoring command events while the dropdown is closed (user + * arrowing through the combobox), but handling them while the + * combobox dropdown is open (user pressed enter when an item was + * selected). If we don't show the filepicker here, it will be shown + * when clicking "Subscribe Now". + */ + var popupbox = this._handlersMenuList.firstChild.boxObject; + if (popupbox.popupState == "hiding") { + this._chooseClientApp(function(aResult) { + if (!aResult) { + // Select the (per-prefs) selected handler if no application + // was selected + this._setSelectedHandler(this._getFeedType()); + } + }.bind(this)); + } + break; + default: + this._setAlwaysUseLabel(); + } + } + }, + + _setSelectedHandler: function(feedType) { + var prefs = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + var handler = prefs.getCharPref(getPrefReaderForType(feedType), "bookmarks"); + + switch (handler) { + case "web": { + if (this._handlersMenuList) { + var url; + try { + url = prefs.getComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString).data; + } catch (ex) { + LOG("FeedWriter._setSelectedHandler: invalid or no handler in prefs"); + return; + } + var handlers = + this._handlersMenuList.getElementsByAttribute("webhandlerurl", url); + if (handlers.length == 0) { + LOG("FeedWriter._setSelectedHandler: selected web handler isn't in the menulist") + return; + } + + this._safeDoCommand(handlers[0]); + } + break; + } + case "client": { + try { + this._selectedApp = + prefs.getComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile); + } + catch(ex) { + this._selectedApp = null; + } + + if (this._selectedApp) { + this._initMenuItemWithFile(this._contentSandbox.selectedAppMenuItem, + this._selectedApp); + var codeStr = "selectedAppMenuItem.hidden = false; " + + "selectedAppMenuItem.doCommand(); "; + + // Only show the default reader menuitem if the default reader + // isn't the selected application + if (this._defaultSystemReader) { + var shouldHide = + this._defaultSystemReader.path == this._selectedApp.path; + codeStr += "defaultHandlerMenuItem.hidden = " + shouldHide + ";" + } + Cu.evalInSandbox(codeStr, this._contentSandbox); + break; + } + } + case "bookmarks": + default: { + var liveBookmarksMenuItem = this._getUIElement("liveBookmarksMenuItem"); + if (liveBookmarksMenuItem) + this._safeDoCommand(liveBookmarksMenuItem); + } + } + }, + + _initSubscriptionUI: function() { + var handlersMenuPopup = this._getUIElement("handlersMenuPopup"); + if (!handlersMenuPopup) + return; + + var feedType = this._getFeedType(); + var codeStr; + + // change the background + var header = this._document.getElementById("feedHeader"); + this._contentSandbox.header = header; + switch (feedType) { + case Ci.nsIFeed.TYPE_VIDEO: + codeStr = "header.className = 'videoPodcastBackground'; "; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + codeStr = "header.className = 'audioPodcastBackground'; "; + break; + + default: + codeStr = "header.className = 'feedBackground'; "; + } + + var liveBookmarksMenuItem = this._getUIElement("liveBookmarksMenuItem"); + + // Last-selected application + var menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "selectedAppMenuItem"); + menuItem.className = "menuitem-iconic selectedAppMenuItem"; + menuItem.setAttribute("handlerType", "client"); + try { + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + this._selectedApp = prefs.getComplexValue(getPrefAppForType(feedType), + Ci.nsILocalFile); + + if (this._selectedApp.exists()) + this._initMenuItemWithFile(menuItem, this._selectedApp); + else { + // Hide the menuitem if the last selected application doesn't exist + menuItem.setAttribute("hidden", true); + } + } + catch(ex) { + // Hide the menuitem until an application is selected + menuItem.setAttribute("hidden", true); + } + this._contentSandbox.handlersMenuPopup = handlersMenuPopup; + this._contentSandbox.selectedAppMenuItem = menuItem; + + codeStr += "handlersMenuPopup.appendChild(selectedAppMenuItem); "; + + // List the default feed reader + try { + this._defaultSystemReader = Cc["@mozilla.org/browser/shell-service;1"]. + getService(Ci.nsIShellService). + defaultFeedReader; + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "defaultHandlerMenuItem"); + menuItem.className = "menuitem-iconic defaultHandlerMenuItem"; + menuItem.setAttribute("handlerType", "client"); + + this._initMenuItemWithFile(menuItem, this._defaultSystemReader); + + // Hide the default reader item if it points to the same application + // as the last-selected application + if (this._selectedApp && + this._selectedApp.path == this._defaultSystemReader.path) + menuItem.hidden = true; + } + catch(ex) { menuItem = null; /* no default reader */ } + + if (menuItem) { + this._contentSandbox.defaultHandlerMenuItem = menuItem; + codeStr += "handlersMenuPopup.appendChild(defaultHandlerMenuItem); "; + } + + // "Choose Application..." menuitem + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "chooseApplicationMenuItem"); + menuItem.className = "menuitem-iconic chooseApplicationMenuItem"; + menuItem.setAttribute("label", this._getString("chooseApplicationMenuItem")); + + this._contentSandbox.chooseAppMenuItem = menuItem; + codeStr += "handlersMenuPopup.appendChild(chooseAppMenuItem); "; + + // separator + this._contentSandbox.chooseAppSep = + menuItem = liveBookmarksMenuItem.nextSibling.cloneNode(false); + codeStr += "handlersMenuPopup.appendChild(chooseAppSep); "; + + Cu.evalInSandbox(codeStr, this._contentSandbox); + + // List of web handlers + var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + var handlers = wccr.getContentHandlers(this._getMimeTypeForFeedType(feedType)); + if (handlers.length != 0) { + for (var i = 0; i < handlers.length; ++i) { + if (!handlers[i].uri) { + LOG("Handler with name " + handlers[i].name + " has no URI!? Skipping..."); + continue; + } + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.className = "menuitem-iconic"; + menuItem.setAttribute("label", handlers[i].name); + menuItem.setAttribute("handlerType", "web"); + menuItem.setAttribute("webhandlerurl", handlers[i].uri); + this._contentSandbox.menuItem = menuItem; + codeStr = "handlersMenuPopup.appendChild(menuItem);"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + + this._setFaviconForWebReader(handlers[i].uri, menuItem); + } + this._contentSandbox.menuItem = null; + } + + this._setSelectedHandler(feedType); + + // "Subscribe using..." + this._setSubscribeUsingLabel(); + + // "Always use..." checkbox initial state + this._setAlwaysUseCheckedState(feedType); + this._setAlwaysUseLabel(); + + // We update the "Always use.." checkbox label whenever the selected item + // in the list is changed + handlersMenuPopup.addEventListener("command", this, false); + + // Set up the "Subscribe Now" button + this._getUIElement("subscribeButton") + .addEventListener("command", this, false); + + // first-run ui + var showFirstRunUI = prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI, true); + if (showFirstRunUI) { + var textfeedinfo1, textfeedinfo2; + switch (feedType) { + case Ci.nsIFeed.TYPE_VIDEO: + textfeedinfo1 = "feedSubscriptionVideoPodcast1"; + textfeedinfo2 = "feedSubscriptionVideoPodcast2"; + break; + case Ci.nsIFeed.TYPE_AUDIO: + textfeedinfo1 = "feedSubscriptionAudioPodcast1"; + textfeedinfo2 = "feedSubscriptionAudioPodcast2"; + break; + default: + textfeedinfo1 = "feedSubscriptionFeed1"; + textfeedinfo2 = "feedSubscriptionFeed2"; + } + + this._contentSandbox.feedinfo1 = + this._document.getElementById("feedSubscriptionInfo1"); + this._contentSandbox.feedinfo1Str = this._getString(textfeedinfo1); + this._contentSandbox.feedinfo2 = + this._document.getElementById("feedSubscriptionInfo2"); + this._contentSandbox.feedinfo2Str = this._getString(textfeedinfo2); + this._contentSandbox.header = header; + codeStr = "feedinfo1.textContent = feedinfo1Str; " + + "feedinfo2.textContent = feedinfo2Str; " + + "header.setAttribute('firstrun', 'true');" + Cu.evalInSandbox(codeStr, this._contentSandbox); + prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false); + } + }, + + /** + * Returns the original URI object of the feed and ensures that this + * component is only ever invoked from the preview document. + * @param aWindow + * The window of the document invoking the BrowserFeedWriter + */ + _getOriginalURI: function(aWindow) { + var chan = aWindow.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShell).currentDocumentChannel; + + var nullPrincipal = Cc["@mozilla.org/nullprincipal;1"]. + createInstance(Ci.nsIPrincipal); + + // this channel is not going to be openend, use a nullPrincipal + // and the most restrctive securityFlag. + let resolvedURI = NetUtil.newChannel({ + uri: "about:feeds", + loadingPrincipal: nullPrincipal, + securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER + }).URI; + + if (resolvedURI.equals(chan.URI)) + return chan.originalURI; + + return null; + }, + + _window: null, + _document: null, + _feedURI: null, + _feedPrincipal: null, + _handlersMenuList: null, + + // BrowserFeedWriter WebIDL methods + init: function(aWindow) { + var window = aWindow; + this._feedURI = this._getOriginalURI(window); + if (!this._feedURI) + return; + + this._window = window; + this._document = window.document; + this._document.getElementById("feedSubscribeLine").offsetTop; + this._handlersMenuList = this._getUIElement("handlersMenuList"); + + var secman = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + this._feedPrincipal = secman.createCodebasePrincipal(this._feedURI, {}); + + LOG("Subscribe Preview: feed uri = " + this._window.location.href); + + // Set up the subscription UI + this._initSubscriptionUI(); + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.addObserver(PREF_SELECTED_ACTION, this, false); + prefs.addObserver(PREF_SELECTED_READER, this, false); + prefs.addObserver(PREF_SELECTED_WEB, this, false); + prefs.addObserver(PREF_SELECTED_APP, this, false); + prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this, false); + prefs.addObserver(PREF_VIDEO_SELECTED_READER, this, false); + prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this, false); + prefs.addObserver(PREF_VIDEO_SELECTED_APP, this, false); + + prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this, false); + prefs.addObserver(PREF_AUDIO_SELECTED_READER, this, false); + prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this, false); + prefs.addObserver(PREF_AUDIO_SELECTED_APP, this, false); + }, + + writeContent: function() { + if (!this._window) + return; + + try { + // Set up the feed content + var container = this._getContainer(); + if (!container) + return; + + this._setTitleText(container); + this._setTitleImage(container); + this._writeFeedContent(container); + } + finally { + this._removeFeedFromCache(); + } + }, + + close: function() { + this._getUIElement("handlersMenuPopup") + .removeEventListener("command", this, false); + this._getUIElement("subscribeButton") + .removeEventListener("command", this, false); + this._document = null; + this._window = null; + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.removeObserver(PREF_SELECTED_ACTION, this); + prefs.removeObserver(PREF_SELECTED_READER, this); + prefs.removeObserver(PREF_SELECTED_WEB, this); + prefs.removeObserver(PREF_SELECTED_APP, this); + prefs.removeObserver(PREF_VIDEO_SELECTED_ACTION, this); + prefs.removeObserver(PREF_VIDEO_SELECTED_READER, this); + prefs.removeObserver(PREF_VIDEO_SELECTED_WEB, this); + prefs.removeObserver(PREF_VIDEO_SELECTED_APP, this); + + prefs.removeObserver(PREF_AUDIO_SELECTED_ACTION, this); + prefs.removeObserver(PREF_AUDIO_SELECTED_READER, this); + prefs.removeObserver(PREF_AUDIO_SELECTED_WEB, this); + prefs.removeObserver(PREF_AUDIO_SELECTED_APP, this); + + this._removeFeedFromCache(); + this.__faviconService = null; + this.__bundle = null; + this._feedURI = null; + this.__contentSandbox = null; + }, + + _removeFeedFromCache: function() { + if (this._feedURI) { + var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + feedService.removeFeedResult(this._feedURI); + this._feedURI = null; + } + }, + + subscribe: function() { + var feedType = this._getFeedType(); + + // Subscribe to the feed using the selected handler and save prefs + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + var defaultHandler = "reader"; + var useAsDefault = this._getUIElement("alwaysUse").getAttribute("checked"); + + var selectedItem = this._getSelectedItemFromMenulist(this._handlersMenuList); + let subscribeCallback = function() { + if (selectedItem.hasAttribute("webhandlerurl")) { + var webURI = selectedItem.getAttribute("webhandlerurl"); + prefs.setCharPref(getPrefReaderForType(feedType), "web"); + + var supportsString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + supportsString.data = webURI; + prefs.setComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString, + supportsString); + + var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + var handler = wccr.getWebContentHandlerByURI(this._getMimeTypeForFeedType(feedType), webURI); + if (handler) { + if (useAsDefault) { + wccr.setAutoHandler(this._getMimeTypeForFeedType(feedType), handler); + } + + this._window.location.href = handler.getHandlerURI(this._window.location.href); + } + } else { + switch (selectedItem.getAttribute("anonid")) { + case "selectedAppMenuItem": + prefs.setComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile, + this._selectedApp); + prefs.setCharPref(getPrefReaderForType(feedType), "client"); + break; + case "defaultHandlerMenuItem": + prefs.setComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile, + this._defaultSystemReader); + prefs.setCharPref(getPrefReaderForType(feedType), "client"); + break; + case "liveBookmarksMenuItem": + defaultHandler = "bookmarks"; + prefs.setCharPref(getPrefReaderForType(feedType), "bookmarks"); + break; + } + var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + + // Pull the title and subtitle out of the document + var feedTitle = this._document.getElementById(TITLE_ID).textContent; + var feedSubtitle = this._document.getElementById(SUBTITLE_ID).textContent; + feedService.addToClientReader(this._window.location.href, feedTitle, feedSubtitle, feedType); + } + + // If "Always use..." is checked, we should set PREF_*SELECTED_ACTION + // to either "reader" (If a web reader or if an application is selected), + // or to "bookmarks" (if the live bookmarks option is selected). + // Otherwise, we should set it to "ask" + if (useAsDefault) { + prefs.setCharPref(getPrefActionForType(feedType), defaultHandler); + } else { + prefs.setCharPref(getPrefActionForType(feedType), "ask"); + } + }.bind(this); + + // Show the file picker before subscribing if the + // choose application menuitem was chosen using the keyboard + if (selectedItem.getAttribute("anonid") == "chooseApplicationMenuItem") { + this._chooseClientApp(function(aResult) { + if (aResult) { + selectedItem = + this._getSelectedItemFromMenulist(this._handlersMenuList); + subscribeCallback(); + } + }.bind(this)); + } else { + subscribeCallback(); + } + }, + + // nsIObserver + observe: function(subject, topic, data) { + if (!this._window) { + // this._window is null unless this.init was called with a trusted + // window object. + return; + } + + var feedType = this._getFeedType(); + + if (topic == "nsPref:changed") { + switch (data) { + case PREF_SELECTED_READER: + case PREF_SELECTED_WEB: + case PREF_SELECTED_APP: + case PREF_VIDEO_SELECTED_READER: + case PREF_VIDEO_SELECTED_WEB: + case PREF_VIDEO_SELECTED_APP: + case PREF_AUDIO_SELECTED_READER: + case PREF_AUDIO_SELECTED_WEB: + case PREF_AUDIO_SELECTED_APP: + this._setSelectedHandler(feedType); + break; + case PREF_SELECTED_ACTION: + case PREF_VIDEO_SELECTED_ACTION: + case PREF_AUDIO_SELECTED_ACTION: + this._setAlwaysUseCheckedState(feedType); + } + } + }, + + /** + * Sets the icon for the given web-reader item in the readers menu. + * The icon is fetched and stored through the favicon service. + * + * @param aReaderUrl + * the reader url. + * @param aMenuItem + * the reader item in the readers menulist. + * + * @note For privacy reasons we cannot set the image attribute directly + * to the icon url. See Bug 358878 for details. + */ + _setFaviconForWebReader: + function(aReaderUrl, aMenuItem) { + var readerURI = makeURI(aReaderUrl); + if (!/^https?$/.test(readerURI.scheme)) { + // Don't try to get a favicon for non http(s) URIs. + return; + } + var faviconURI = makeURI(readerURI.prePath + "/favicon.ico"); + var self = this; + var usePrivateBrowsing = this._window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsILoadContext) + .usePrivateBrowsing; + var nullPrincipal = Cc["@mozilla.org/nullprincipal;1"] + .createInstance(Ci.nsIPrincipal); + this._faviconService.setAndFetchFaviconForPage(readerURI, faviconURI, false, + usePrivateBrowsing ? this._faviconService.FAVICON_LOAD_PRIVATE + : this._faviconService.FAVICON_LOAD_NON_PRIVATE, + function(aURI, aDataLen, aData, aMimeType) { + if (aDataLen > 0) { + var dataURL = "data:" + aMimeType + ";base64," + + btoa(String.fromCharCode.apply(null, aData)); + self._contentSandbox.menuItem = aMenuItem; + self._contentSandbox.dataURL = dataURL; + var codeStr = "menuItem.setAttribute('image', dataURL);"; + Cu.evalInSandbox(codeStr, self._contentSandbox); + self._contentSandbox.menuItem = null; + self._contentSandbox.dataURL = null; + } + }, nullPrincipal); + }, + + classID: FEEDWRITER_CID, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener, Ci.nsIObserver, + Ci.nsINavHistoryObserver, + Ci.nsIDOMGlobalPropertyInitializer]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FeedWriter]); diff --git a/browser/components/feeds/WebContentConverter.js b/browser/components/feeds/WebContentConverter.js new file mode 100644 index 000000000..a6b144c65 --- /dev/null +++ b/browser/components/feeds/WebContentConverter.js @@ -0,0 +1,927 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +function LOG(str) { + dump("*** " + str + "\n"); +} + +const WCCR_CONTRACTID = "@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"; +const WCCR_CLASSID = Components.ID("{792a7e82-06a0-437c-af63-b2d12e808acc}"); + +const WCC_CLASSID = Components.ID("{db7ebf28-cc40-415f-8a51-1b111851df1e}"); +const WCC_CLASSNAME = "Web Service Handler"; + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_ANY = "*/*"; +const TYPE_BLACKLIST = [ + "application/x-www-form-urlencoded", + "application/xhtml+xml", + "application/xml", + "application/mathml+xml", + "application/xslt+xml", + "application/x-xpinstall", + "image/gif", + "image/jpg", + "image/jpeg", + "image/png", + "image/x-png", + "image/webp", +#ifdef MOZ_JXR + "image/jxr", + "image/vnd.ms-photo", +#endif + "image/svg+xml", + "image/bmp", + "image/x-ms-bmp", + "image/icon", + "image/x-icon", + "image/vnd.microsoft.icon", + "multipart/x-mixed-replace", + "multipart/form-data", + "text/cache-manifest", + "text/css", + "text/xsl", + "text/html", + "text/ping", + "text/plain", + "text/xml", + "text/javascript", // To prevent malicious intent blocking scripting. + "text/ecmascript"]; + +const PREF_CONTENTHANDLERS_AUTO = "browser.contentHandlers.auto."; +const PREF_CONTENTHANDLERS_BRANCH = "browser.contentHandlers.types."; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; +const PREF_HANDLER_EXTERNAL_PREFIX = "network.protocol-handler.external"; +const PREF_ALLOW_DIFFERENT_HOST = "gecko.handlerService.allowRegisterFromDifferentHost"; + +const STRING_BUNDLE_URI = "chrome://browser/locale/feeds/subscribe.properties"; + +const NS_ERROR_MODULE_DOM = 2152923136; +const NS_ERROR_DOM_SYNTAX_ERR = NS_ERROR_MODULE_DOM + 12; + +function WebContentConverter() { +} +WebContentConverter.prototype = { + convert: function() { }, + asyncConvertData: function() { }, + onDataAvailable: function() { }, + onStopRequest: function() { }, + + onStartRequest: function(request, context) { + var wccr = + Cc[WCCR_CONTRACTID]. + getService(Ci.nsIWebContentConverterService); + wccr.loadPreferredHandler(request); + }, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIStreamConverter) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +var WebContentConverterFactory = { + createInstance: function(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return new WebContentConverter().QueryInterface(iid); + }, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIFactory) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function ServiceInfo(contentType, uri, name) { + this._contentType = contentType; + this._uri = uri; + this._name = name; +} +ServiceInfo.prototype = { + /** + * See nsIHandlerApp + */ + get name() { + return this._name; + }, + + /** + * See nsIHandlerApp + */ + equals: function(aHandlerApp) { + if (!aHandlerApp) + throw Cr.NS_ERROR_NULL_POINTER; + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo && + aHandlerApp.contentType == this.contentType && + aHandlerApp.uri == this.uri) + return true; + + return false; + }, + + /** + * See nsIWebContentHandlerInfo + */ + get contentType() { + return this._contentType; + }, + + /** + * See nsIWebContentHandlerInfo + */ + get uri() { + return this._uri; + }, + + /** + * See nsIWebContentHandlerInfo + */ + getHandlerURI: function(uri) { + return this._uri.replace(/%s/gi, encodeURIComponent(uri)); + }, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIWebContentHandlerInfo) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function WebContentConverterRegistrar() { + this._contentTypes = { }; + this._autoHandleContentTypes = { }; +} + +WebContentConverterRegistrar.prototype = { + get stringBundle() { + var sb = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(STRING_BUNDLE_URI); + delete WebContentConverterRegistrar.prototype.stringBundle; + return WebContentConverterRegistrar.prototype.stringBundle = sb; + }, + + _getFormattedString: function(key, params) { + return this.stringBundle.formatStringFromName(key, params, params.length); + }, + + _getString: function(key) { + return this.stringBundle.GetStringFromName(key); + }, + + /** + * See nsIWebContentConverterService + */ + getAutoHandler: + function(contentType) { + contentType = this._resolveContentType(contentType); + if (contentType in this._autoHandleContentTypes) + return this._autoHandleContentTypes[contentType]; + return null; + }, + + /** + * See nsIWebContentConverterService + */ + setAutoHandler: + function(contentType, handler) { + if (handler && !this._typeIsRegistered(contentType, handler.uri)) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + contentType = this._resolveContentType(contentType); + this._setAutoHandler(contentType, handler); + + var ps = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + var autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO); + if (handler) + autoBranch.setCharPref(contentType, handler.uri); + else if (autoBranch.prefHasUserValue(contentType)) + autoBranch.clearUserPref(contentType); + + ps.savePrefFile(null); + }, + + /** + * Update the internal data structure (not persistent) + */ + _setAutoHandler: + function(contentType, handler) { + if (handler) + this._autoHandleContentTypes[contentType] = handler; + else if (contentType in this._autoHandleContentTypes) + delete this._autoHandleContentTypes[contentType]; + }, + + /** + * See nsIWebContentConverterService + */ + getWebContentHandlerByURI: + function(contentType, uri) { + var handlers = this.getContentHandlers(contentType, { }); + for (var i = 0; i < handlers.length; ++i) { + if (handlers[i].uri == uri) + return handlers[i]; + } + return null; + }, + + /** + * See nsIWebContentConverterService + */ + loadPreferredHandler: + function(request) { + var channel = request.QueryInterface(Ci.nsIChannel); + var contentType = this._resolveContentType(channel.contentType); + var handler = this.getAutoHandler(contentType); + if (handler) { + request.cancel(Cr.NS_ERROR_FAILURE); + + var webNavigation = + channel.notificationCallbacks.getInterface(Ci.nsIWebNavigation); + webNavigation.loadURI(handler.getHandlerURI(channel.URI.spec), + Ci.nsIWebNavigation.LOAD_FLAGS_NONE, + null, null, null); + } + }, + + /** + * See nsIWebContentConverterService + */ + removeProtocolHandler: + function(aProtocol, aURITemplate) { + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(aProtocol); + var handlers = handlerInfo.possibleApplicationHandlers; + for (let i = 0; i < handlers.length; i++) { + try { // We only want to test web handlers + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + if (handler.uriTemplate == aURITemplate) { + handlers.removeElementAt(i); + var hs = Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + return; + } + } catch (e) { /* it wasn't a web handler */ } + } + }, + + /** + * See nsIWebContentConverterService + */ + removeContentHandler: + function(contentType, uri) { + function notURI(serviceInfo) { + return serviceInfo.uri != uri; + } + + if (contentType in this._contentTypes) { + this._contentTypes[contentType] = + this._contentTypes[contentType].filter(notURI); + } + }, + + /** + * + */ + _mappings: { + "application/rss+xml": TYPE_MAYBE_FEED, + "application/atom+xml": TYPE_MAYBE_FEED, + }, + + /** + * These are types for which there is a separate content converter aside + * from our built in generic one. We should not automatically register + * a factory for creating a converter for these types. + */ + _blockedTypes: { + "application/vnd.mozilla.maybe.feed": true, + }, + + /** + * Determines the "internal" content type based on the _mappings. + * @param contentType + * @returns The resolved contentType value. + */ + _resolveContentType: + function(contentType) { + if (contentType in this._mappings) + return this._mappings[contentType]; + return contentType; + }, + + _makeURI: function(aURL, aOriginCharset, aBaseURI) { + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + return ioService.newURI(aURL, aOriginCharset, aBaseURI); + }, + + _checkAndGetURI: + function(aURIString, aContentWindow) + { + try { + let baseURI = aContentWindow.document.baseURIObject; + var uri = this._makeURI(aURIString, null, baseURI); + } catch (ex) { + // not supposed to throw according to spec + return; + } + + // For security reasons we reject non-http(s) urls (see bug 354316), + // we may need to revise this once we support more content types + // XXX this should be a "security exception" according to spec, but that + // isn't defined yet. + if (uri.scheme != "http" && uri.scheme != "https") + throw("Permission denied to add " + uri.spec + " as a content or protocol handler"); + + // We also reject handlers registered from a different host (see bug 402287) + // The pref allows us to test the feature + var pb = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + if (!pb.getBoolPref(PREF_ALLOW_DIFFERENT_HOST) && + (!["http:", "https:"].includes(aContentWindow.location.protocol) || + aContentWindow.location.hostname != uri.host)) { + throw("Permission denied to add " + uri.spec + " as a content or protocol handler"); + } + + // If the uri doesn't contain '%s', it won't be a good handler + if (uri.spec.indexOf("%s") < 0) + throw NS_ERROR_DOM_SYNTAX_ERR; + + return uri; + }, + + /** + * Determines if a web handler is already registered. + * + * @param aProtocol + * The scheme of the web handler we are checking for. + * @param aURITemplate + * The URI template that the handler uses to handle the protocol. + * @return true if it is already registered, false otherwise. + */ + _protocolHandlerRegistered: + function(aProtocol, aURITemplate) { + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(aProtocol); + var handlers = handlerInfo.possibleApplicationHandlers; + for (let i = 0; i < handlers.length; i++) { + try { // We only want to test web handlers + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + if (handler.uriTemplate == aURITemplate) + return true; + } catch (e) { /* it wasn't a web handler */ } + } + return false; + }, + + /** + * See nsIWebContentHandlerRegistrar + */ + registerProtocolHandler: + function(aProtocol, aURIString, aTitle, aContentWindow) { + LOG("registerProtocolHandler(" + aProtocol + "," + aURIString + "," + aTitle + ")"); + + var uri = this._checkAndGetURI(aURIString, aContentWindow); + + // If the protocol handler is already registered, just return early. + if (this._protocolHandlerRegistered(aProtocol, uri.spec)) { + return; + } + + var browserWindow = this._getBrowserWindowForContentWindow(aContentWindow); + if (PrivateBrowsingUtils.isWindowPrivate(browserWindow)) { + // Inside the private browsing mode, we don't want to alert the user to save + // a protocol handler. We log it to the error console so that web developers + // would have some way to tell what's going wrong. + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService). + logStringMessage("Web page denied access to register a protocol handler inside private browsing mode"); + return; + } + + // First, check to make sure this isn't already handled internally (we don't + // want to let them take over, say "chrome"). + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var handler = ios.getProtocolHandler(aProtocol); + if (!(handler instanceof Ci.nsIExternalProtocolHandler)) { + // This is handled internally, so we don't want them to register + // XXX this should be a "security exception" according to spec, but that + // isn't defined yet. + throw("Permission denied to add " + aURIString + "as a protocol handler"); + } + + // check if it is in the black list + var pb = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + var allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol, + pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default")); + if (!allowed) { + // XXX this should be a "security exception" according to spec + throw("Not allowed to register a protocol handler for " + aProtocol); + } + + // Now Ask the user and provide the proper callback + var message = this._getFormattedString("addProtocolHandler", + [aTitle, uri.host, aProtocol]); + + var notificationIcon = uri.prePath + "/favicon.ico"; + var notificationValue = "Protocol Registration: " + aProtocol; + var addButton = { + label: this._getString("addProtocolHandlerAddButton"), + accessKey: this._getString("addHandlerAddButtonAccesskey"), + protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle }, + + callback: + function(aNotification, aButtonInfo) { + var protocol = aButtonInfo.protocolInfo.protocol; + var uri = aButtonInfo.protocolInfo.uri; + var name = aButtonInfo.protocolInfo.name; + + var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"]. + createInstance(Ci.nsIWebHandlerApp); + handler.name = name; + handler.uriTemplate = uri; + + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(protocol); + handlerInfo.possibleApplicationHandlers.appendElement(handler, false); + + // Since the user has agreed to add a new handler, chances are good + // that the next time they see a handler of this type, they're going + // to want to use it. Reset the handlerInfo to ask before the next + // use. + handlerInfo.alwaysAskBeforeHandling = true; + + var hs = Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + } + }; + var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow); + var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement); + notificationBox.appendNotification(message, + notificationValue, + notificationIcon, + notificationBox.PRIORITY_INFO_LOW, + [addButton]); + }, + + /** + * See nsIWebContentHandlerRegistrar + * If a DOM window is provided, then the request came from content, so we + * prompt the user to confirm the registration. + */ + registerContentHandler: + function(aContentType, aURIString, aTitle, aContentWindow) { + LOG("registerContentHandler(" + aContentType + "," + aURIString + "," + aTitle + ")"); + + // Check against the type blacklist. + // XXX this should be a "security exception" according to spec, but that + // isn't defined yet. + var contentType = this._resolveContentType(aContentType); + for (let blacklistType of TYPE_BLACKLIST) { + if (contentType == blacklistType) { + console.error("Unable to register content handler for prohibited MIME type %s.", contentType); + return; + } + } + + if (aContentWindow) { + var uri = this._checkAndGetURI(aURIString, aContentWindow); + + var browserWindow = this._getBrowserWindowForContentWindow(aContentWindow); + var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow); + var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement); + this._appendFeedReaderNotification(uri, aTitle, notificationBox); + } + else + this._registerContentHandler(contentType, aURIString, aTitle); + }, + + /** + * Returns the browser chrome window in which the content window is in + */ + _getBrowserWindowForContentWindow: + function(aContentWindow) { + return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .wrappedJSObject; + }, + + /** + * Returns the <xul:browser> element associated with the given content + * window. + * + * @param aBrowserWindow + * The browser window in which the content window is in. + * @param aContentWindow + * The content window. It's possible to pass a child content window + * (i.e. the content window of a frame/iframe). + */ + _getBrowserForContentWindow: + function(aBrowserWindow, aContentWindow) { + // This depends on pseudo APIs of browser.js and tabbrowser.xml + aContentWindow = aContentWindow.top; + var browsers = aBrowserWindow.gBrowser.browsers; + for (var i = 0; i < browsers.length; ++i) { + if (browsers[i].contentWindow == aContentWindow) + return browsers[i]; + } + }, + + /** + * Appends a notifcation for the given feed reader details. + * + * The notification could be either a pseudo-dialog which lets + * the user to add the feed reader: + * [ [icon] Add %feed-reader-name% (%feed-reader-host%) as a Feed Reader? (Add) [x] ] + * + * or a simple message for the case where the feed reader is already registered: + * [ [icon] %feed-reader-name% is already registered as a Feed Reader [x] ] + * + * A new notification isn't appended if the given notificationbox has a + * notification for the same feed reader. + * + * @param aURI + * The url of the feed reader as a nsIURI object + * @param aName + * The feed reader name as it was passed to registerContentHandler + * @param aNotificationBox + * The notification box to which a notification might be appended + * @return true if a notification has been appended, false otherwise. + */ + _appendFeedReaderNotification: + function(aURI, aName, aNotificationBox) { + var uriSpec = aURI.spec; + var notificationValue = "feed reader notification: " + uriSpec; + var notificationIcon = aURI.prePath + "/favicon.ico"; + + // Don't append a new notification if the notificationbox + // has a notification for the given feed reader already + if (aNotificationBox.getNotificationWithValue(notificationValue)) + return false; + + var buttons, message; + if (this.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uriSpec)) + message = this._getFormattedString("handlerRegistered", [aName]); + else { + message = this._getFormattedString("addHandler", [aName, aURI.host]); + var self = this; + var addButton = { + _outer: self, + label: self._getString("addHandlerAddButton"), + accessKey: self._getString("addHandlerAddButtonAccesskey"), + feedReaderInfo: { uri: uriSpec, name: aName }, + + /* static */ + callback: + function(aNotification, aButtonInfo) { + var uri = aButtonInfo.feedReaderInfo.uri; + var name = aButtonInfo.feedReaderInfo.name; + var outer = aButtonInfo._outer; + + // The reader could have been added from another window mean while + if (!outer.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uri)) + outer._registerContentHandler(TYPE_MAYBE_FEED, uri, name); + + // avoid reference cycles + aButtonInfo._outer = null; + + return false; + } + }; + buttons = [addButton]; + } + + aNotificationBox.appendNotification(message, + notificationValue, + notificationIcon, + aNotificationBox.PRIORITY_INFO_LOW, + buttons); + return true; + }, + + /** + * Save Web Content Handler metadata to persistent preferences. + * @param contentType + * The content Type being handled + * @param uri + * The uri of the web service + * @param title + * The human readable name of the web service + * + * This data is stored under: + * + * browser.contentHandlers.type0 = content/type + * browser.contentHandlers.uri0 = http://www.foo.com/q=%s + * browser.contentHandlers.title0 = Foo 2.0alphr + */ + _saveContentHandlerToPrefs: + function(contentType, uri, title) { + var ps = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + var i = 0; + var typeBranch = null; + while (true) { + typeBranch = + ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + i + "."); + try { + typeBranch.getCharPref("type"); + ++i; + } + catch (e) { + // No more handlers + break; + } + } + if (typeBranch) { + typeBranch.setCharPref("type", contentType); + var pls = + Cc["@mozilla.org/pref-localizedstring;1"]. + createInstance(Ci.nsIPrefLocalizedString); + pls.data = uri; + typeBranch.setComplexValue("uri", Ci.nsIPrefLocalizedString, pls); + pls.data = title; + typeBranch.setComplexValue("title", Ci.nsIPrefLocalizedString, pls); + + ps.savePrefFile(null); + } + }, + + /** + * Determines if there is a type with a particular uri registered for the + * specified content type already. + * @param contentType + * The content type that the uri handles + * @param uri + * The uri of the + */ + _typeIsRegistered: function(contentType, uri) { + if (!(contentType in this._contentTypes)) + return false; + + var services = this._contentTypes[contentType]; + for (var i = 0; i < services.length; ++i) { + // This uri has already been registered + if (services[i].uri == uri) + return true; + } + return false; + }, + + /** + * Gets a stream converter contract id for the specified content type. + * @param contentType + * The source content type for the conversion. + * @returns A contract id to construct a converter to convert between the + * contentType and *\/*. + */ + _getConverterContractID: function(contentType) { + const template = "@mozilla.org/streamconv;1?from=%s&to=*/*"; + return template.replace(/%s/, contentType); + }, + + /** + * Register a web service handler for a content type. + * + * @param contentType + * the content type being handled + * @param uri + * the URI of the web service + * @param title + * the human readable name of the web service + */ + _registerContentHandler: + function(contentType, uri, title) { + this._updateContentTypeHandlerMap(contentType, uri, title); + this._saveContentHandlerToPrefs(contentType, uri, title); + + if (contentType == TYPE_MAYBE_FEED) { + // Make the new handler the last-selected reader in the preview page + // and make sure the preview page is shown the next time a feed is visited + var pb = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService).getBranch(null); + pb.setCharPref(PREF_SELECTED_READER, "web"); + + var supportsString = + Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + supportsString.data = uri; + pb.setComplexValue(PREF_SELECTED_WEB, Ci.nsISupportsString, + supportsString); + pb.setCharPref(PREF_SELECTED_ACTION, "ask"); + this._setAutoHandler(TYPE_MAYBE_FEED, null); + } + }, + + /** + * Update the content type -> handler map. This mapping is not persisted, use + * registerContentHandler or _saveContentHandlerToPrefs for that purpose. + * @param contentType + * The content Type being handled + * @param uri + * The uri of the web service + * @param title + * The human readable name of the web service + */ + _updateContentTypeHandlerMap: + function(contentType, uri, title) { + if (!(contentType in this._contentTypes)) + this._contentTypes[contentType] = []; + + // Avoid adding duplicates + if (this._typeIsRegistered(contentType, uri)) + return; + + this._contentTypes[contentType].push(new ServiceInfo(contentType, uri, title)); + + if (!(contentType in this._blockedTypes)) { + var converterContractID = this._getConverterContractID(contentType); + var cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID, + WebContentConverterFactory); + } + }, + + /** + * See nsIWebContentConverterService + */ + getContentHandlers: + function(contentType, countRef) { + countRef.value = 0; + if (!(contentType in this._contentTypes)) + return []; + + var handlers = this._contentTypes[contentType]; + countRef.value = handlers.length; + return handlers; + }, + + /** + * See nsIWebContentConverterService + */ + resetHandlersForType: + function(contentType) { + // currently unused within the tree, so only useful for extensions; previous + // impl. was buggy (and even infinite-looped!), so I argue that this is a + // definite improvement + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Registers a handler from the settings on a preferences branch. + * + * @param branch + * an nsIPrefBranch containing "type", "uri", and "title" preferences + * corresponding to the content handler to be registered + */ + _registerContentHandlerWithBranch: function(branch) { + /** + * Since we support up to six predefined readers, we need to handle gaps + * better, since the first branch with user-added values will be .6 + * + * How we deal with that is to check to see if there's no prefs in the + * branch and stop cycling once that's true. This doesn't fix the case + * where a user manually removes a reader, but that's not supported yet! + */ + var vals = branch.getChildList(""); + if (vals.length == 0) + return; + + try { + var type = branch.getCharPref("type"); + var uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data; + var title = branch.getComplexValue("title", + Ci.nsIPrefLocalizedString).data; + this._updateContentTypeHandlerMap(type, uri, title); + } + catch(ex) { + // do nothing, the next branch might have values + } + }, + + /** + * Load the auto handler, content handler and protocol tables from + * preferences. + */ + _init: function() { + var ps = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + + var kids = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH) + .getChildList(""); + + // first get the numbers of the providers by getting all ###.uri prefs + var nums = []; + for (var i = 0; i < kids.length; i++) { + var match = /^(\d+)\.uri$/.exec(kids[i]); + if (!match) + continue; + else + nums.push(match[1]); + } + + // sort them, to get them back in order + nums.sort(function(a, b) {return a - b;}); + + // now register them + for (var i = 0; i < nums.length; i++) { + var branch = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + nums[i] + "."); + this._registerContentHandlerWithBranch(branch); + } + + // We need to do this _after_ registering all of the available handlers, + // so that getWebContentHandlerByURI can return successfully. + try { + var autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO); + var childPrefs = autoBranch.getChildList(""); + for (var i = 0; i < childPrefs.length; ++i) { + var type = childPrefs[i]; + var uri = autoBranch.getCharPref(type); + if (uri) { + var handler = this.getWebContentHandlerByURI(type, uri); + this._setAutoHandler(type, handler); + } + } + } + catch (e) { + // No auto branch yet, that's fine + //LOG("WCCR.init: There is no auto branch, benign"); + } + }, + + /** + * See nsIObserver + */ + observe: function(subject, topic, data) { + var os = + Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + switch (topic) { + case "app-startup": + os.addObserver(this, "browser-ui-startup-complete", false); + break; + case "browser-ui-startup-complete": + os.removeObserver(this, "browser-ui-startup-complete"); + this._init(); + break; + } + }, + + /** + * See nsIFactory + */ + createInstance: function(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + classID: WCCR_CLASSID, + + /** + * See nsISupports + */ + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIWebContentConverterService, + Ci.nsIWebContentHandlerRegistrar, + Ci.nsIObserver, + Ci.nsIFactory]), + + _xpcom_categories: [{ + category: "app-startup", + service: true + }] +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrar]); diff --git a/browser/components/feeds/content/subscribe.css b/browser/components/feeds/content/subscribe.css new file mode 100644 index 000000000..bf2524d14 --- /dev/null +++ b/browser/components/feeds/content/subscribe.css @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#feedSubscribeLine { + -moz-binding: url(subscribe.xml#feedreaderUI); +} diff --git a/browser/components/feeds/content/subscribe.js b/browser/components/feeds/content/subscribe.js new file mode 100644 index 000000000..c06e7b19a --- /dev/null +++ b/browser/components/feeds/content/subscribe.js @@ -0,0 +1,23 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var SubscribeHandler = { + /** + * The nsIFeedWriter object that produces the UI + */ + _feedWriter: null, + + init: function() { + this._feedWriter = new BrowserFeedWriter(); + }, + + writeContent: function() { + this._feedWriter.writeContent(); + }, + + uninit: function() { + this._feedWriter.close(); + } +}; diff --git a/browser/components/feeds/content/subscribe.xhtml b/browser/components/feeds/content/subscribe.xhtml new file mode 100644 index 000000000..8ad069f59 --- /dev/null +++ b/browser/components/feeds/content/subscribe.xhtml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % feedDTD + SYSTEM "chrome://browser/locale/feeds/subscribe.dtd"> + %feedDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<html id="feedHandler" + xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&feedPage.title;</title> + <link rel="stylesheet" + href="chrome://browser/skin/feeds/subscribe.css" + type="text/css" + media="all"/> + <link rel="stylesheet" + href="chrome://browser/content/feeds/subscribe.css" + type="text/css" + media="all"/> + <script type="application/javascript" + src="chrome://browser/content/feeds/subscribe.js"/> + </head> + <body onload="SubscribeHandler.writeContent();" onunload="SubscribeHandler.uninit();"> + <div id="feedHeaderContainer"> + <div id="feedHeader" dir="&locale.dir;"> + <div id="feedIntroText"> + <p id="feedSubscriptionInfo1" /> + <p id="feedSubscriptionInfo2" /> + </div> + <div id="feedSubscribeLine"></div> + </div> + </div> + + <script type="application/javascript"> + SubscribeHandler.init(); + </script> + + <div id="feedBody"> + <div id="feedTitle"> + <a id="feedTitleLink"> + <img id="feedTitleImage"/> + </a> + <div id="feedTitleContainer"> + <h1 id="feedTitleText"/> + <h2 id="feedSubtitleText"/> + </div> + </div> + <div id="feedContent"/> + </div> + </body> +</html> diff --git a/browser/components/feeds/content/subscribe.xml b/browser/components/feeds/content/subscribe.xml new file mode 100644 index 000000000..949bcfd7e --- /dev/null +++ b/browser/components/feeds/content/subscribe.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ + <!ENTITY % feedDTD + SYSTEM "chrome://browser/locale/feeds/subscribe.dtd"> + %feedDTD; +]> +<bindings id="feedBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <binding id="feedreaderUI" bindToUntrustedContent="true"> + <content> + <xul:vbox> + <xul:hbox align="center"> + <xul:description anonid="subscribeUsingDescription" class="subscribeUsingDescription"/> + <xul:menulist anonid="handlersMenuList" class="handlersMenuList" aria-labelledby="subscribeUsingDescription"> + <xul:menupopup anonid="handlersMenuPopup" class="handlersMenuPopup"> + <xul:menuitem anonid="liveBookmarksMenuItem" label="&feedLiveBookmarks;" class="menuitem-iconic liveBookmarksMenuItem" image="chrome://browser/skin/page-livemarks.png" selected="true"/> + <xul:menuseparator/> + </xul:menupopup> + </xul:menulist> + </xul:hbox> + <xul:hbox> + <xul:checkbox anonid="alwaysUse" class="alwaysUse" checked="false"/> + </xul:hbox> + <xul:hbox align="center"> + <xul:spacer flex="1"/> + <xul:button label="&feedSubscribeNow;" anonid="subscribeButton" class="subscribeButton"/> + </xul:hbox> + </xul:vbox> + </content> + <resources> + <stylesheet src="chrome://browser/skin/feeds/subscribe-ui.css"/> + </resources> + </binding> +</bindings> + diff --git a/browser/components/feeds/jar.mn b/browser/components/feeds/jar.mn new file mode 100644 index 000000000..f8896f877 --- /dev/null +++ b/browser/components/feeds/jar.mn @@ -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/. + +browser.jar: + content/browser/feeds/subscribe.xhtml (content/subscribe.xhtml) + content/browser/feeds/subscribe.js (content/subscribe.js) + content/browser/feeds/subscribe.xml (content/subscribe.xml) + content/browser/feeds/subscribe.css (content/subscribe.css) diff --git a/browser/components/feeds/moz.build b/browser/components/feeds/moz.build new file mode 100644 index 000000000..24dd30c82 --- /dev/null +++ b/browser/components/feeds/moz.build @@ -0,0 +1,32 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] + +XPIDL_SOURCES += [ + 'nsIFeedResultService.idl', + 'nsIWebContentConverterRegistrar.idl', +] + +XPIDL_MODULE = 'browser-feeds' + +SOURCES += ['nsFeedSniffer.cpp'] + +EXTRA_COMPONENTS += [ + 'BrowserFeeds.manifest', + 'FeedConverter.js', +] + +EXTRA_PP_COMPONENTS += [ + 'FeedWriter.js', + 'WebContentConverter.js', +] + +FINAL_LIBRARY = 'browsercomps' + +for var in ('MOZ_APP_NAME', 'MOZ_MACBUNDLE_NAME'): + DEFINES[var] = CONFIG[var] + +LOCAL_INCLUDES += ['../build'] diff --git a/browser/components/feeds/nsFeedSniffer.cpp b/browser/components/feeds/nsFeedSniffer.cpp new file mode 100644 index 000000000..f314d3d3b --- /dev/null +++ b/browser/components/feeds/nsFeedSniffer.cpp @@ -0,0 +1,363 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsFeedSniffer.h" + + +#include "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsCOMPtr.h" +#include "nsStringStream.h" + +#include "nsBrowserCompsCID.h" + +#include "nsICategoryManager.h" +#include "nsIServiceManager.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +#include "nsIStreamConverterService.h" +#include "nsIStreamConverter.h" + +#include "nsIStreamListener.h" + +#include "nsIHttpChannel.h" +#include "nsIMIMEHeaderParam.h" + +#include "nsMimeTypes.h" +#include "nsIURI.h" +#include <algorithm> + +#define TYPE_ATOM "application/atom+xml" +#define TYPE_RSS "application/rss+xml" +#define TYPE_MAYBE_FEED "application/vnd.mozilla.maybe.feed" + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RSS "http://purl.org/rss/1.0/" + +#define MAX_BYTES 512u + +NS_IMPL_ISUPPORTS(nsFeedSniffer, + nsIContentSniffer, + nsIStreamListener, + nsIRequestObserver) + +nsresult +nsFeedSniffer::ConvertEncodedData(nsIRequest* request, + const uint8_t* data, + uint32_t length) +{ + nsresult rv = NS_OK; + + mDecodedData = ""; + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request)); + if (!httpChannel) + return NS_ERROR_NO_INTERFACE; + + nsAutoCString contentEncoding; + httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("Content-Encoding"), + contentEncoding); + if (!contentEncoding.IsEmpty()) { + nsCOMPtr<nsIStreamConverterService> converterService(do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID)); + if (converterService) { + ToLowerCase(contentEncoding); + + nsCOMPtr<nsIStreamListener> converter; + rv = converterService->AsyncConvertData(contentEncoding.get(), + "uncompressed", this, nullptr, + getter_AddRefs(converter)); + NS_ENSURE_SUCCESS(rv, rv); + + converter->OnStartRequest(request, nullptr); + + nsCOMPtr<nsIStringInputStream> rawStream = + do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID); + if (!rawStream) + return NS_ERROR_FAILURE; + + rv = rawStream->SetData((const char*)data, length); + NS_ENSURE_SUCCESS(rv, rv); + + rv = converter->OnDataAvailable(request, nullptr, rawStream, 0, length); + NS_ENSURE_SUCCESS(rv, rv); + + converter->OnStopRequest(request, nullptr, NS_OK); + } + } + return rv; +} + +template<int N> +static bool +StringBeginsWithLowercaseLiteral(nsAString& aString, + const char (&aSubstring)[N]) +{ + return StringHead(aString, N).LowerCaseEqualsLiteral(aSubstring); +} + +bool +HasAttachmentDisposition(nsIHttpChannel* httpChannel) +{ + if (!httpChannel) + return false; + + uint32_t disp; + nsresult rv = httpChannel->GetContentDisposition(&disp); + + if (NS_SUCCEEDED(rv) && disp == nsIChannel::DISPOSITION_ATTACHMENT) + return true; + + return false; +} + +/** + * @return the first occurrence of a character within a string buffer, + * or nullptr if not found + */ +static const char* +FindChar(char c, const char *begin, const char *end) +{ + for (; begin < end; ++begin) { + if (*begin == c) + return begin; + } + return nullptr; +} + +/** + * + * Determine if a substring is the "documentElement" in the document. + * + * All of our sniffed substrings: <rss, <feed, <rdf:RDF must be the "document" + * element within the XML DOM, i.e. the root container element. Otherwise, + * it's possible that someone embedded one of these tags inside a document of + * another type, e.g. a HTML document, and we don't want to show the preview + * page if the document isn't actually a feed. + * + * @param start + * The beginning of the data being sniffed + * @param end + * The end of the data being sniffed, right before the substring that + * was found. + * @returns true if the found substring is the documentElement, false + * otherwise. + */ +static bool +IsDocumentElement(const char *start, const char* end) +{ + // For every tag in the buffer, check to see if it's a PI, Doctype or + // comment, our desired substring or something invalid. + while ( (start = FindChar('<', start, end)) ) { + ++start; + if (start >= end) + return false; + + // Check to see if the character following the '<' is either '?' or '!' + // (processing instruction or doctype or comment)... these are valid nodes + // to have in the prologue. + if (*start != '?' && *start != '!') + return false; + + // Now advance the iterator until the '>' (We do this because we don't want + // to sniff indicator substrings that are embedded within other nodes, e.g. + // comments: <!-- <rdf:RDF .. > --> + start = FindChar('>', start, end); + if (!start) + return false; + + ++start; + } + return true; +} + +/** + * Determines whether or not a string exists as the root element in an XML data + * string buffer. + * @param dataString + * The data being sniffed + * @param substring + * The substring being tested for existence and root-ness. + * @returns true if the substring exists and is the documentElement, false + * otherwise. + */ +static bool +ContainsTopLevelSubstring(nsACString& dataString, const char *substring) +{ + int32_t offset = dataString.Find(substring); + if (offset == -1) + return false; + + const char *begin = dataString.BeginReading(); + + // Only do the validation when we find the substring. + return IsDocumentElement(begin, begin + offset); +} + +NS_IMETHODIMP +nsFeedSniffer::GetMIMETypeFromContent(nsIRequest* request, + const uint8_t* data, + uint32_t length, + nsACString& sniffedType) +{ + nsCOMPtr<nsIHttpChannel> channel(do_QueryInterface(request)); + if (!channel) + return NS_ERROR_NO_INTERFACE; + + // Check that this is a GET request, since you can't subscribe to a POST... + nsAutoCString method; + channel->GetRequestMethod(method); + if (!method.EqualsLiteral("GET")) { + sniffedType.Truncate(); + return NS_OK; + } + + // We need to find out if this is a load of a view-source document. In this + // case we do not want to override the content type, since the source display + // does not need to be converted from feed format to XUL. More importantly, + // we don't want to change the content type from something + // nsContentDLF::CreateInstance knows about (e.g. application/xml, text/html + // etc) to something that only the application fe knows about (maybe.feed) + // thus deactivating syntax highlighting. + nsCOMPtr<nsIURI> originalURI; + channel->GetOriginalURI(getter_AddRefs(originalURI)); + + nsAutoCString scheme; + originalURI->GetScheme(scheme); + if (scheme.EqualsLiteral("view-source")) { + sniffedType.Truncate(); + return NS_OK; + } + + // Check the Content-Type to see if it is set correctly. If it is set to + // something specific that we think is a reliable indication of a feed, don't + // bother sniffing since we assume the site maintainer knows what they're + // doing. + nsAutoCString contentType; + channel->GetContentType(contentType); + bool noSniff = contentType.EqualsLiteral(TYPE_RSS) || + contentType.EqualsLiteral(TYPE_ATOM); + + // Check to see if this was a feed request from the location bar or from + // the feed: protocol. This is also a reliable indication. + // The value of the header doesn't matter. + if (!noSniff) { + nsAutoCString sniffHeader; + nsresult foundHeader = + channel->GetRequestHeader(NS_LITERAL_CSTRING("X-Moz-Is-Feed"), + sniffHeader); + noSniff = NS_SUCCEEDED(foundHeader); + } + + if (noSniff) { + // check for an attachment after we have a likely feed. + if(HasAttachmentDisposition(channel)) { + sniffedType.Truncate(); + return NS_OK; + } + + // set the feed header as a response header, since we have good metadata + // telling us that the feed is supposed to be RSS or Atom + channel->SetResponseHeader(NS_LITERAL_CSTRING("X-Moz-Is-Feed"), + NS_LITERAL_CSTRING("1"), false); + sniffedType.AssignLiteral(TYPE_MAYBE_FEED); + return NS_OK; + } + + // Don't sniff arbitrary types. Limit sniffing to situations that + // we think can reasonably arise. + if (!contentType.EqualsLiteral(TEXT_HTML) && + !contentType.EqualsLiteral(APPLICATION_OCTET_STREAM) && + // Same criterion as XMLHttpRequest. Should we be checking for "+xml" + // and check for text/xml and application/xml by hand instead? + contentType.Find("xml") == -1) { + sniffedType.Truncate(); + return NS_OK; + } + + // Now we need to potentially decompress data served with + // Content-Encoding: gzip + nsresult rv = ConvertEncodedData(request, data, length); + if (NS_FAILED(rv)) + return rv; + + // We cap the number of bytes to scan at MAX_BYTES to prevent picking up + // false positives by accidentally reading document content, e.g. a "how to + // make a feed" page. + const char* testData; + if (mDecodedData.IsEmpty()) { + testData = (const char*)data; + length = std::min(length, MAX_BYTES); + } else { + testData = mDecodedData.get(); + length = std::min(mDecodedData.Length(), MAX_BYTES); + } + + // The strategy here is based on that described in: + // http://blogs.msdn.com/rssteam/articles/PublishersGuide.aspx + // for interoperarbility purposes. + + // Thus begins the actual sniffing. + nsDependentCSubstring dataString((const char*)testData, length); + + bool isFeed = false; + + // RSS 0.91/0.92/2.0 + isFeed = ContainsTopLevelSubstring(dataString, "<rss"); + + // Atom 1.0 + if (!isFeed) + isFeed = ContainsTopLevelSubstring(dataString, "<feed"); + + // RSS 1.0 + if (!isFeed) { + isFeed = ContainsTopLevelSubstring(dataString, "<rdf:RDF") && + dataString.Find(NS_RDF) != -1 && + dataString.Find(NS_RSS) != -1; + } + + // If we sniffed a feed, coerce our internal type + if (isFeed && !HasAttachmentDisposition(channel)) + sniffedType.AssignLiteral(TYPE_MAYBE_FEED); + else + sniffedType.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +nsFeedSniffer::OnStartRequest(nsIRequest* request, nsISupports* context) +{ + return NS_OK; +} + +nsresult +nsFeedSniffer::AppendSegmentToString(nsIInputStream* inputStream, + void* closure, + const char* rawSegment, + uint32_t toOffset, + uint32_t count, + uint32_t* writeCount) +{ + nsCString* decodedData = static_cast<nsCString*>(closure); + decodedData->Append(rawSegment, count); + *writeCount = count; + return NS_OK; +} + +NS_IMETHODIMP +nsFeedSniffer::OnDataAvailable(nsIRequest* request, nsISupports* context, + nsIInputStream* stream, uint64_t offset, + uint32_t count) +{ + uint32_t read; + return stream->ReadSegments(AppendSegmentToString, &mDecodedData, count, + &read); +} + +NS_IMETHODIMP +nsFeedSniffer::OnStopRequest(nsIRequest* request, nsISupports* context, + nsresult status) +{ + return NS_OK; +} diff --git a/browser/components/feeds/nsFeedSniffer.h b/browser/components/feeds/nsFeedSniffer.h new file mode 100644 index 000000000..a0eb9862c --- /dev/null +++ b/browser/components/feeds/nsFeedSniffer.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#include "nsIContentSniffer.h" +#include "nsIStreamListener.h" +#include "nsStringAPI.h" +#include "mozilla/Attributes.h" + +class nsFeedSniffer final : public nsIContentSniffer, + nsIStreamListener +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTSNIFFER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + static nsresult AppendSegmentToString(nsIInputStream* inputStream, + void* closure, + const char* rawSegment, + uint32_t toOffset, + uint32_t count, + uint32_t* writeCount); + +protected: + ~nsFeedSniffer() {} + + nsresult ConvertEncodedData(nsIRequest* request, const uint8_t* data, + uint32_t length); + +private: + nsCString mDecodedData; +}; + diff --git a/browser/components/feeds/nsIFeedResultService.idl b/browser/components/feeds/nsIFeedResultService.idl new file mode 100644 index 000000000..cb0f332d1 --- /dev/null +++ b/browser/components/feeds/nsIFeedResultService.idl @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +interface nsIURI; +interface nsIRequest; +interface nsIFeedResult; + +/** + * nsIFeedResultService provides a globally-accessible object for retrieving + * the results of feed processing. + */ +[scriptable, uuid(950a829e-c20e-4dc3-b447-f8b753ae54da)] +interface nsIFeedResultService : nsISupports +{ + /** + * When set to true, forces the preview page to be displayed, regardless + * of the user's preferences. + */ + attribute boolean forcePreviewPage; + + /** + * Adds a URI to the user's specified external feed handler, or live + * bookmarks. + * @param uri + * The uri of the feed to add. + * @param title + * The title of the feed to add. + * @param subtitle + * The subtitle of the feed to add. + * @param feedType + * The nsIFeed type of the feed. See nsIFeed.idl + */ + void addToClientReader(in AUTF8String uri, + in AString title, + in AString subtitle, + in unsigned long feedType); + + /** + * Registers a Feed Result object with a globally accessible service + * so that it can be accessed by a singleton method outside the usual + * flow of control in document loading. + * + * @param feedResult + * An object implementing nsIFeedResult representing the feed. + */ + void addFeedResult(in nsIFeedResult feedResult); + + /** + * Gets a Feed Handler object registered using addFeedResult. + * + * @param uri + * The URI of the feed a handler is being requested for + */ + nsIFeedResult getFeedResult(in nsIURI uri); + + /** + * Unregisters a Feed Handler object registered using addFeedResult. + * @param uri + * The feed URI the handler was registered under. This must be + * the same *instance* the feed was registered under. + */ + void removeFeedResult(in nsIURI uri); +}; diff --git a/browser/components/feeds/nsIWebContentConverterRegistrar.idl b/browser/components/feeds/nsIWebContentConverterRegistrar.idl new file mode 100644 index 000000000..08ce2f4ae --- /dev/null +++ b/browser/components/feeds/nsIWebContentConverterRegistrar.idl @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIMIMEInfo.idl" +#include "nsIWebContentHandlerRegistrar.idl" + +interface nsIRequest; + +[scriptable, uuid(eb361098-5158-4b21-8f98-50b445f1f0b2)] +interface nsIWebContentHandlerInfo : nsIHandlerApp +{ + /** + * The content type handled by the handler + */ + readonly attribute AString contentType; + + /** + * The uri of the handler, with an embedded %s where the URI of the loaded + * document will be encoded. + */ + readonly attribute AString uri; + + /** + * Gets the service URL Spec, with the loading document URI encoded in it. + * @param uri + * The URI of the document being loaded + * @returns The URI of the service with the loading document URI encoded in + * it. + */ + AString getHandlerURI(in AString uri); +}; + +[scriptable, uuid(de7cc06e-e778-45cb-b7db-7a114e1e75b1)] +interface nsIWebContentConverterService : nsIWebContentHandlerRegistrar +{ + /** + * Specifies the handler to be used to automatically handle all links of a + * certain content type from now on. + * @param contentType + * The content type to automatically load with the specified handler + * @param handler + * A web service handler. If this is null, no automatic action is + * performed and the user must choose. + * @throws NS_ERROR_NOT_AVAILABLE if the service refered to by |handler| is + * not already registered. + */ + void setAutoHandler(in AString contentType, in nsIWebContentHandlerInfo handler); + + /** + * Gets the auto handler specified for a particular content type + * @param contentType + * The content type to look up an auto handler for. + * @returns The web service handler that will automatically handle all + * documents of the specified type. null if there is no automatic + * handler. (Handlers may be registered, just none of them specified + * as "automatic"). + */ + nsIWebContentHandlerInfo getAutoHandler(in AString contentType); + + /** + * Gets a web handler for the specified service URI + * @param contentType + * The content type of the service being located + * @param uri + * The service URI of the handler to locate. + * @returns A web service handler that uses the specified uri. + */ + nsIWebContentHandlerInfo getWebContentHandlerByURI(in AString contentType, + in AString uri); + + /** + * Loads the preferred handler when content of a registered type is about + * to be loaded. + * @param request + * The nsIRequest for the load of the content + */ + void loadPreferredHandler(in nsIRequest request); + + /** + * Removes a registered protocol handler + * @param protocol + * The protocol scheme to remove a service handler for + * @param uri + * The uri of the service handler to remove + */ + void removeProtocolHandler(in AString protocol, in AString uri); + + /** + * Removes a registered content handler + * @param contentType + * The content type to remove a service handler for + * @param uri + * The uri of the service handler to remove + */ + void removeContentHandler(in AString contentType, in AString uri); + + /** + * Gets the list of content handlers for a particular type. + * @param contentType + * The content type to get handlers for + * @returns An array of nsIWebContentHandlerInfo objects + */ + void getContentHandlers(in AString contentType, + [optional] out unsigned long count, + [retval,array,size_is(count)] out nsIWebContentHandlerInfo handlers); + + /** + * Resets the list of available content handlers to the default set from + * the distribution. + * @param contentType + * The content type to reset handlers for + */ + void resetHandlersForType(in AString contentType); +}; + diff --git a/browser/components/fuel/fuelApplication.js b/browser/components/fuel/fuelApplication.js new file mode 100644 index 000000000..a4238a65b --- /dev/null +++ b/browser/components/fuel/fuelApplication.js @@ -0,0 +1,822 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", + "resource://gre/modules/Deprecated.jsm"); + +const APPLICATION_CID = Components.ID("fe74cf80-aa2d-11db-abbd-0800200c9a66"); +const APPLICATION_CONTRACTID = "@mozilla.org/fuel/application;1"; + +//================================================= +// Singleton that holds services and utilities +var Utilities = { + get bookmarks() { + let bookmarks = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + this.__defineGetter__("bookmarks", function() bookmarks); + return this.bookmarks; + }, + + get bookmarksObserver() { + let bookmarksObserver = new BookmarksObserver(); + this.__defineGetter__("bookmarksObserver", function() bookmarksObserver); + return this.bookmarksObserver; + }, + + get annotations() { + let annotations = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + this.__defineGetter__("annotations", function() annotations); + return this.annotations; + }, + + get history() { + let history = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + this.__defineGetter__("history", function() history); + return this.history; + }, + + get windowMediator() { + let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + this.__defineGetter__("windowMediator", function() windowMediator); + return this.windowMediator; + }, + + makeURI: function(aSpec) { + if (!aSpec) + return null; + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(aSpec, null, null); + }, + + free: function() { + delete this.bookmarks; + delete this.bookmarksObserver; + delete this.annotations; + delete this.history; + delete this.windowMediator; + } +}; + + +//================================================= +// Window implementation + +var fuelWindowMap = new WeakMap(); +function getWindow(aWindow) { + let fuelWindow = fuelWindowMap.get(aWindow); + if (!fuelWindow) { + fuelWindow = new Window(aWindow); + fuelWindowMap.set(aWindow, fuelWindow); + } + return fuelWindow; +} + +// Don't call new Window() directly; use getWindow instead. +function Window(aWindow) { + this._window = aWindow; + this._events = new Events(); + + this._watch("TabOpen"); + this._watch("TabMove"); + this._watch("TabClose"); + this._watch("TabSelect"); +} + +Window.prototype = { + get events() { + return this._events; + }, + + get _tabbrowser() { + return this._window.getBrowser(); + }, + + /* + * Helper used to setup event handlers on the XBL element. Note that the events + * are actually dispatched to tabs, so we capture them. + */ + _watch: function(aType) { + this._tabbrowser.tabContainer.addEventListener(aType, this, + /* useCapture = */ true); + }, + + handleEvent: function(aEvent) { + this._events.dispatch(aEvent.type, getBrowserTab(this, aEvent.originalTarget.linkedBrowser)); + }, + + get tabs() { + var tabs = []; + var browsers = this._tabbrowser.browsers; + for (var i=0; i<browsers.length; i++) + tabs.push(getBrowserTab(this, browsers[i])); + return tabs; + }, + + get activeTab() { + return getBrowserTab(this, this._tabbrowser.selectedBrowser); + }, + + open: function(aURI) { + return getBrowserTab(this, this._tabbrowser.addTab(aURI.spec).linkedBrowser); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.fuelIWindow]) +}; + +//================================================= +// BrowserTab implementation + +var fuelBrowserTabMap = new WeakMap(); +function getBrowserTab(aFUELWindow, aBrowser) { + let fuelBrowserTab = fuelBrowserTabMap.get(aBrowser); + if (!fuelBrowserTab) { + fuelBrowserTab = new BrowserTab(aFUELWindow, aBrowser); + fuelBrowserTabMap.set(aBrowser, fuelBrowserTab); + } + else { + // This tab may have moved to another window, so make sure its cached + // window is up-to-date. + fuelBrowserTab._window = aFUELWindow; + } + + return fuelBrowserTab; +} + +// Don't call new BrowserTab() directly; call getBrowserTab instead. +function BrowserTab(aFUELWindow, aBrowser) { + this._window = aFUELWindow; + this._browser = aBrowser; + this._events = new Events(); + + this._watch("load"); +} + +BrowserTab.prototype = { + get _tabbrowser() { + return this._window._tabbrowser; + }, + + get uri() { + return this._browser.currentURI; + }, + + get index() { + var tabs = this._tabbrowser.tabs; + for (var i=0; i<tabs.length; i++) { + if (tabs[i].linkedBrowser == this._browser) + return i; + } + return -1; + }, + + get events() { + return this._events; + }, + + get window() { + return this._window; + }, + + get document() { + return this._browser.contentDocument; + }, + + /* + * Helper used to setup event handlers on the XBL element + */ + _watch: function(aType) { + this._browser.addEventListener(aType, this, + /* useCapture = */ true); + }, + + handleEvent: function(aEvent) { + if (aEvent.type == "load") { + if (!(aEvent.originalTarget instanceof Ci.nsIDOMDocument)) + return; + + if (aEvent.originalTarget.defaultView instanceof Ci.nsIDOMWindow && + aEvent.originalTarget.defaultView.frameElement) + return; + } + this._events.dispatch(aEvent.type, this); + }, + /* + * Helper used to determine the index offset of the browsertab + */ + _getTab: function() { + var tabs = this._tabbrowser.tabs; + return tabs[this.index] || null; + }, + + load: function(aURI) { + this._browser.loadURI(aURI.spec, null, null); + }, + + focus: function() { + this._tabbrowser.selectedTab = this._getTab(); + this._tabbrowser.focus(); + }, + + close: function() { + this._tabbrowser.removeTab(this._getTab()); + }, + + moveBefore: function(aBefore) { + this._tabbrowser.moveTabTo(this._getTab(), aBefore.index); + }, + + moveToEnd: function() { + this._tabbrowser.moveTabTo(this._getTab(), this._tabbrowser.browsers.length); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.fuelIBrowserTab]) +}; + + +//================================================= +// Annotations implementation +function Annotations(aId) { + this._id = aId; +} + +Annotations.prototype = { + get names() { + return Utilities.annotations.getItemAnnotationNames(this._id); + }, + + has: function(aName) { + return Utilities.annotations.itemHasAnnotation(this._id, aName); + }, + + get: function(aName) { + if (this.has(aName)) + return Utilities.annotations.getItemAnnotation(this._id, aName); + return null; + }, + + set: function(aName, aValue, aExpiration) { + Utilities.annotations.setItemAnnotation(this._id, aName, aValue, 0, aExpiration); + }, + + remove: function(aName) { + if (aName) + Utilities.annotations.removeItemAnnotation(this._id, aName); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.fuelIAnnotations]) +}; + + +//================================================= +// BookmarksObserver implementation (internal class) +// +// BookmarksObserver is a global singleton which watches the browser's +// bookmarks and sends you events when things change. +// +// You can register three different kinds of event listeners on +// BookmarksObserver, using addListener, addFolderListener, and +// addRootlistener. +// +// - addListener(aId, aEvent, aListener) lets you listen to a specific +// bookmark. You can listen to the "change", "move", and "remove" events. +// +// - addFolderListener(aId, aEvent, aListener) lets you listen to a specific +// bookmark folder. You can listen to "addchild" and "removechild". +// +// - addRootListener(aEvent, aListener) lets you listen to the root bookmark +// node. This lets you hear "add", "remove", and "change" events on all +// bookmarks. +// + +function BookmarksObserver() { + this._eventsDict = {}; + this._folderEventsDict = {}; + this._rootEvents = new Events(); + Utilities.bookmarks.addObserver(this, /* ownsWeak = */ true); +} + +BookmarksObserver.prototype = { + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onItemVisited: function() {}, + + onItemAdded: function(aId, aFolder, aIndex, aItemType, aURI) { + this._rootEvents.dispatch("add", aId); + this._dispatchToEvents("addchild", aId, this._folderEventsDict[aFolder]); + }, + + onItemRemoved: function(aId, aFolder, aIndex) { + this._rootEvents.dispatch("remove", aId); + this._dispatchToEvents("remove", aId, this._eventsDict[aId]); + this._dispatchToEvents("removechild", aId, this._folderEventsDict[aFolder]); + }, + + onItemChanged: function(aId, aProperty, aIsAnnotationProperty, aValue) { + this._rootEvents.dispatch("change", aProperty); + this._dispatchToEvents("change", aProperty, this._eventsDict[aId]); + }, + + onItemMoved: function(aId, aOldParent, aOldIndex, aNewParent, aNewIndex) { + this._dispatchToEvents("move", aId, this._eventsDict[aId]); + }, + + _dispatchToEvents: function(aEvent, aData, aEvents) { + if (aEvents) { + aEvents.dispatch(aEvent, aData); + } + }, + + _addListenerToDict: function(aId, aEvent, aListener, aDict) { + var events = aDict[aId]; + if (!events) { + events = new Events(); + aDict[aId] = events; + } + events.addListener(aEvent, aListener); + }, + + _removeListenerFromDict: function(aId, aEvent, aListener, aDict) { + var events = aDict[aId]; + if (!events) { + return; + } + events.removeListener(aEvent, aListener); + if (events._listeners.length == 0) { + delete aDict[aId]; + } + }, + + addListener: function(aId, aEvent, aListener) { + this._addListenerToDict(aId, aEvent, aListener, this._eventsDict); + }, + + removeListener: function(aId, aEvent, aListener) { + this._removeListenerFromDict(aId, aEvent, aListener, this._eventsDict); + }, + + addFolderListener: function(aId, aEvent, aListener) { + this._addListenerToDict(aId, aEvent, aListener, this._folderEventsDict); + }, + + removeFolderListener: function(aId, aEvent, aListener) { + this._removeListenerFromDict(aId, aEvent, aListener, this._folderEventsDict); + }, + + addRootListener: function(aEvent, aListener) { + this._rootEvents.addListener(aEvent, aListener); + }, + + removeRootListener: function(aEvent, aListener) { + this._rootEvents.removeListener(aEvent, aListener); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarksObserver, + Ci.nsISupportsWeakReference]) +}; + +//================================================= +// Bookmark implementation +// +// Bookmark event listeners are stored in BookmarksObserver, not in the +// Bookmark objects themselves. Thus, you don't have to hold on to a Bookmark +// object in order for your event listener to stay valid, and Bookmark objects +// not kept alive by the extension can be GC'ed. +// +// A consequence of this is that if you have two different Bookmark objects x +// and y for the same bookmark (i.e., x != y but x.id == y.id), and you do +// +// x.addListener("foo", fun); +// y.removeListener("foo", fun); +// +// the second line will in fact remove the listener added in the first line. +// + +function Bookmark(aId, aParent, aType) { + this._id = aId; + this._parent = aParent; + this._type = aType || "bookmark"; + this._annotations = new Annotations(this._id); + + // Our _events object forwards to bookmarksObserver. + var self = this; + this._events = { + addListener: function(aEvent, aListener) { + Utilities.bookmarksObserver.addListener(self._id, aEvent, aListener); + }, + removeListener: function(aEvent, aListener) { + Utilities.bookmarksObserver.removeListener(self._id, aEvent, aListener); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.extIEvents]) + }; + + // For our onItemMoved listener, which updates this._parent. + Utilities.bookmarks.addObserver(this, /* ownsWeak = */ true); +} + +Bookmark.prototype = { + get id() { + return this._id; + }, + + get title() { + return Utilities.bookmarks.getItemTitle(this._id); + }, + + set title(aTitle) { + Utilities.bookmarks.setItemTitle(this._id, aTitle); + }, + + get uri() { + return Utilities.bookmarks.getBookmarkURI(this._id); + }, + + set uri(aURI) { + return Utilities.bookmarks.changeBookmarkURI(this._id, aURI); + }, + + get description() { + return this._annotations.get("bookmarkProperties/description"); + }, + + set description(aDesc) { + this._annotations.set("bookmarkProperties/description", aDesc, Ci.nsIAnnotationService.EXPIRE_NEVER); + }, + + get keyword() { + return Utilities.bookmarks.getKeywordForBookmark(this._id); + }, + + set keyword(aKeyword) { + Utilities.bookmarks.setKeywordForBookmark(this._id, aKeyword); + }, + + get type() { + return this._type; + }, + + get parent() { + return this._parent; + }, + + set parent(aFolder) { + Utilities.bookmarks.moveItem(this._id, aFolder.id, Utilities.bookmarks.DEFAULT_INDEX); + // this._parent is updated in onItemMoved + }, + + get annotations() { + return this._annotations; + }, + + get events() { + return this._events; + }, + + remove : function() { + Utilities.bookmarks.removeItem(this._id); + }, + + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onItemAdded: function() {}, + onItemVisited: function() {}, + onItemRemoved: function() {}, + onItemChanged: function() {}, + + onItemMoved: function(aId, aOldParent, aOldIndex, aNewParent, aNewIndex) { + if (aId == this._id) { + this._parent = new BookmarkFolder(aNewParent, Utilities.bookmarks.getFolderIdForItem(aNewParent)); + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.fuelIBookmark, + Ci.nsINavBookmarksObserver, + Ci.nsISupportsWeakReference]) +}; + + +//================================================= +// BookmarkFolder implementation +// +// As with Bookmark, events on BookmarkFolder are handled by the +// BookmarksObserver singleton. +// + +function BookmarkFolder(aId, aParent) { + this._id = aId; + this._parent = aParent; + this._annotations = new Annotations(this._id); + + // Our event listeners are handled by the BookmarksObserver singleton. This + // is a bit complicated because there are three different kinds of events we + // might want to listen to here: + // + // - If this._parent is null, we're the root bookmark folder, and all our + // listeners should be root listeners. + // + // - Otherwise, events ending with "child" (addchild, removechild) are + // handled by a folder listener. + // + // - Other events are handled by a vanilla bookmark listener. + + var self = this; + this._events = { + addListener: function(aEvent, aListener) { + if (self._parent) { + if (/child$/.test(aEvent)) { + Utilities.bookmarksObserver.addFolderListener(self._id, aEvent, aListener); + } + else { + Utilities.bookmarksObserver.addListener(self._id, aEvent, aListener); + } + } + else { + Utilities.bookmarksObserver.addRootListener(aEvent, aListener); + } + }, + removeListener: function(aEvent, aListener) { + if (self._parent) { + if (/child$/.test(aEvent)) { + Utilities.bookmarksObserver.removeFolderListener(self._id, aEvent, aListener); + } + else { + Utilities.bookmarksObserver.removeListener(self._id, aEvent, aListener); + } + } + else { + Utilities.bookmarksObserver.removeRootListener(aEvent, aListener); + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.extIEvents]) + }; + + // For our onItemMoved listener, which updates this._parent. + Utilities.bookmarks.addObserver(this, /* ownsWeak = */ true); +} + +BookmarkFolder.prototype = { + get id() { + return this._id; + }, + + get title() { + return Utilities.bookmarks.getItemTitle(this._id); + }, + + set title(aTitle) { + Utilities.bookmarks.setItemTitle(this._id, aTitle); + }, + + get description() { + return this._annotations.get("bookmarkProperties/description"); + }, + + set description(aDesc) { + this._annotations.set("bookmarkProperties/description", aDesc, Ci.nsIAnnotationService.EXPIRE_NEVER); + }, + + get type() { + return "folder"; + }, + + get parent() { + return this._parent; + }, + + set parent(aFolder) { + Utilities.bookmarks.moveItem(this._id, aFolder.id, Utilities.bookmarks.DEFAULT_INDEX); + // this._parent is updated in onItemMoved + }, + + get annotations() { + return this._annotations; + }, + + get events() { + return this._events; + }, + + get children() { + var items = []; + + var options = Utilities.history.getNewQueryOptions(); + var query = Utilities.history.getNewQuery(); + query.setFolders([this._id], 1); + var result = Utilities.history.executeQuery(query, options); + var rootNode = result.root; + rootNode.containerOpen = true; + var cc = rootNode.childCount; + for (var i=0; i<cc; ++i) { + var node = rootNode.getChild(i); + if (node.type == node.RESULT_TYPE_FOLDER) { + var folder = new BookmarkFolder(node.itemId, this._id); + items.push(folder); + } + else if (node.type == node.RESULT_TYPE_SEPARATOR) { + var separator = new Bookmark(node.itemId, this._id, "separator"); + items.push(separator); + } + else { + var bookmark = new Bookmark(node.itemId, this._id, "bookmark"); + items.push(bookmark); + } + } + rootNode.containerOpen = false; + + return items; + }, + + addBookmark: function(aTitle, aUri) { + var newBookmarkID = Utilities.bookmarks.insertBookmark(this._id, aUri, Utilities.bookmarks.DEFAULT_INDEX, aTitle); + var newBookmark = new Bookmark(newBookmarkID, this, "bookmark"); + return newBookmark; + }, + + addSeparator: function() { + var newBookmarkID = Utilities.bookmarks.insertSeparator(this._id, Utilities.bookmarks.DEFAULT_INDEX); + var newBookmark = new Bookmark(newBookmarkID, this, "separator"); + return newBookmark; + }, + + addFolder: function(aTitle) { + var newFolderID = Utilities.bookmarks.createFolder(this._id, aTitle, Utilities.bookmarks.DEFAULT_INDEX); + var newFolder = new BookmarkFolder(newFolderID, this); + return newFolder; + }, + + remove: function() { + Utilities.bookmarks.removeItem(this._id); + }, + + // observer + onBeginUpdateBatch: function() {}, + onEndUpdateBatch : function() {}, + onItemAdded : function() {}, + onItemRemoved : function() {}, + onItemChanged : function() {}, + + onItemMoved: function(aId, aOldParent, aOldIndex, aNewParent, aNewIndex) { + if (this._id == aId) { + this._parent = new BookmarkFolder(aNewParent, Utilities.bookmarks.getFolderIdForItem(aNewParent)); + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.fuelIBookmarkFolder, + Ci.nsINavBookmarksObserver, + Ci.nsISupportsWeakReference]) +}; + +//================================================= +// BookmarkRoots implementation +function BookmarkRoots() { +} + +BookmarkRoots.prototype = { + get menu() { + if (!this._menu) + this._menu = new BookmarkFolder(Utilities.bookmarks.bookmarksMenuFolder, null); + + return this._menu; + }, + + get toolbar() { + if (!this._toolbar) + this._toolbar = new BookmarkFolder(Utilities.bookmarks.toolbarFolder, null); + + return this._toolbar; + }, + + get tags() { + if (!this._tags) + this._tags = new BookmarkFolder(Utilities.bookmarks.tagsFolder, null); + + return this._tags; + }, + + get unfiled() { + if (!this._unfiled) + this._unfiled = new BookmarkFolder(Utilities.bookmarks.unfiledBookmarksFolder, null); + + return this._unfiled; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.fuelIBookmarkRoots]) +}; + + +//================================================= +// Factory - Treat Application as a singleton +// XXX This is required, because we're registered for the 'JavaScript global +// privileged property' category, whose handler always calls createInstance. +// See bug 386535. +var gSingleton = null; +var ApplicationFactory = { + createInstance: function(aOuter, aIID) { + if (aOuter != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + + if (gSingleton == null) { + gSingleton = new Application(); + } + + return gSingleton.QueryInterface(aIID); + } +}; + + +#include ../../../platform/components/exthelper/extApplication.js + +//================================================= +// Application constructor +function Application() { + Deprecated.warning("FUEL is deprecated, you should use the standard Toolkit API instead.", + "https://github.com/MoonchildProductions/UXP/issues/1083"); + this.initToolkitHelpers(); +} + +//================================================= +// Application implementation +function ApplicationPrototype() { + // for nsIClassInfo + XPCOMUtils + this.classID = APPLICATION_CID; + + // redefine the default factory for XPCOMUtils + this._xpcom_factory = ApplicationFactory; + + // for nsISupports + this.QueryInterface = XPCOMUtils.generateQI([ + Ci.fuelIApplication, + Ci.extIApplication, + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]); + + // for nsIClassInfo + this.classInfo = XPCOMUtils.generateCI({ + classID: APPLICATION_CID, + contractID: APPLICATION_CONTRACTID, + interfaces: [ + Ci.fuelIApplication, + Ci.extIApplication, + Ci.nsIObserver + ], + flags: Ci.nsIClassInfo.SINGLETON + }); + + // for nsIObserver + this.observe = function(aSubject, aTopic, aData) { + // Call the extApplication version of this function first + var superPrototype = Object.getPrototypeOf(Object.getPrototypeOf(this)); + superPrototype.observe.call(this, aSubject, aTopic, aData); + if (aTopic == "xpcom-shutdown") { + this._obs.removeObserver(this, "xpcom-shutdown"); + Utilities.free(); + } + }; + + Object.defineProperty(this, "bookmarks", { + get: function bookmarks () { + let bookmarks = new BookmarkRoots(); + Object.defineProperty(this, "bookmarks", { value: bookmarks }); + return this.bookmarks; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(this, "windows", { + get: function windows() { + var win = []; + var browserEnum = Utilities.windowMediator.getEnumerator("navigator:browser"); + + while (browserEnum.hasMoreElements()) + win.push(getWindow(browserEnum.getNext())); + + return win; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(this, "activeWindow", { + get: () => getWindow(Utilities.windowMediator.getMostRecentWindow("navigator:browser")), + enumerable: true, + configurable: true + }); + +}; + +// set the proto, defined in extApplication.js +ApplicationPrototype.prototype = extApplication.prototype; + +Application.prototype = new ApplicationPrototype(); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Application]); + diff --git a/browser/components/fuel/fuelApplication.manifest b/browser/components/fuel/fuelApplication.manifest new file mode 100644 index 000000000..67e6d0fe6 --- /dev/null +++ b/browser/components/fuel/fuelApplication.manifest @@ -0,0 +1,3 @@ +component {fe74cf80-aa2d-11db-abbd-0800200c9a66} fuelApplication.js +contract @mozilla.org/fuel/application;1 {fe74cf80-aa2d-11db-abbd-0800200c9a66} +category JavaScript-global-privileged-property Application @mozilla.org/fuel/application;1 diff --git a/browser/components/fuel/fuelIApplication.idl b/browser/components/fuel/fuelIApplication.idl new file mode 100644 index 000000000..69b51b0f5 --- /dev/null +++ b/browser/components/fuel/fuelIApplication.idl @@ -0,0 +1,347 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "extIApplication.idl" + +interface nsIVariant; +interface nsIURI; +interface nsIDOMHTMLDocument; + +interface fuelIBookmarkFolder; +interface fuelIBrowserTab; + +/** + * Interface representing a collection of annotations associated + * with a bookmark or bookmark folder. + */ +[scriptable, uuid(335c9292-91a1-4ca0-ad0b-07d5f63ed6cd)] +interface fuelIAnnotations : nsISupports +{ + /** + * Array of the annotation names associated with the owning item + */ + readonly attribute nsIVariant names; + + /** + * Determines if an annotation exists with the given name. + * @param aName + * The name of the annotation + * @returns true if an annotation exists with the given name, + * false otherwise. + */ + boolean has(in AString aName); + + /** + * Gets the value of an annotation with the given name. + * @param aName + * The name of the annotation + * @returns A variant containing the value of the annotation. Supports + * string, boolean and number. + */ + nsIVariant get(in AString aName); + + /** + * Sets the value of an annotation with the given name. + * @param aName + * The name of the annotation + * @param aValue + * The new value of the annotation. Supports string, boolean + * and number + * @param aExpiration + * The expiration policy for the annotation. + * See nsIAnnotationService. + */ + void set(in AString aName, in nsIVariant aValue, in int32_t aExpiration); + + /** + * Removes the named annotation from the owner item. + * @param aName + * The name of annotation. + */ + void remove(in AString aName); +}; + + +/** + * Interface representing a bookmark item. + */ +[scriptable, uuid(808585b6-7568-4b26-8c62-545221bf2b8c)] +interface fuelIBookmark : nsISupports +{ + /** + * The id of the bookmark. + */ + readonly attribute long long id; + + /** + * The title of the bookmark. + */ + attribute AString title; + + /** + * The uri of the bookmark. + */ + attribute nsIURI uri; + + /** + * The description of the bookmark. + */ + attribute AString description; + + /** + * The keyword associated with the bookmark. + */ + attribute AString keyword; + + /** + * The type of the bookmark. + * values: "bookmark", "separator" + */ + readonly attribute AString type; + + /** + * The parent folder of the bookmark. + */ + attribute fuelIBookmarkFolder parent; + + /** + * The annotations object for the bookmark. + */ + readonly attribute fuelIAnnotations annotations; + + /** + * The events object for the bookmark. + * supports: "remove", "change", "visit", "move" + */ + readonly attribute extIEvents events; + + /** + * Removes the item from the parent folder. Used to + * delete a bookmark or separator + */ + void remove(); +}; + + +/** + * Interface representing a bookmark folder. Folders + * can hold bookmarks, separators and other folders. + */ +[scriptable, uuid(9f42fe20-52de-4a55-8632-a459c7716aa0)] +interface fuelIBookmarkFolder : nsISupports +{ + /** + * The id of the folder. + */ + readonly attribute long long id; + + /** + * The title of the folder. + */ + attribute AString title; + + /** + * The description of the folder. + */ + attribute AString description; + + /** + * The type of the folder. + * values: "folder" + */ + readonly attribute AString type; + + /** + * The parent folder of the folder. + */ + attribute fuelIBookmarkFolder parent; + + /** + * The annotations object for the folder. + */ + readonly attribute fuelIAnnotations annotations; + + /** + * The events object for the folder. + * supports: "add", "addchild", "remove", "removechild", "change", "move" + */ + readonly attribute extIEvents events; + + /** + * Array of all bookmarks, separators and folders contained + * in this folder. + */ + readonly attribute nsIVariant children; + + /** + * Adds a new child bookmark to this folder. + * @param aTitle + * The title of bookmark. + * @param aURI + * The uri of bookmark. + */ + fuelIBookmark addBookmark(in AString aTitle, in nsIURI aURI); + + /** + * Adds a new child separator to this folder. + */ + fuelIBookmark addSeparator(); + + /** + * Adds a new child folder to this folder. + * @param aTitle + * The title of folder. + */ + fuelIBookmarkFolder addFolder(in AString aTitle); + + /** + * Removes the folder from the parent folder. + */ + void remove(); +}; + +/** + * Interface representing a container for bookmark roots. Roots + * are the top level parents for the various types of bookmarks in the system. + */ +[scriptable, uuid(c9a80870-eb3c-11dc-95ff-0800200c9a66)] +interface fuelIBookmarkRoots : nsISupports +{ + /** + * The folder for the 'bookmarks menu' root. + */ + readonly attribute fuelIBookmarkFolder menu; + + /** + * The folder for the 'personal toolbar' root. + */ + readonly attribute fuelIBookmarkFolder toolbar; + + /** + * The folder for the 'tags' root. + */ + readonly attribute fuelIBookmarkFolder tags; + + /** + * The folder for the 'unfiled bookmarks' root. + */ + readonly attribute fuelIBookmarkFolder unfiled; +}; + +/** + * Interface representing a browser window. + */ +[scriptable, uuid(207edb28-eb5e-424e-a862-b0e97C8de866)] +interface fuelIWindow : nsISupports +{ + /** + * A collection of browser tabs within the browser window. + */ + readonly attribute nsIVariant tabs; + + /** + * The currently-active tab within the browser window. + */ + readonly attribute fuelIBrowserTab activeTab; + + /** + * Open a new browser tab, pointing to the specified URI. + * @param aURI + * The uri to open the browser tab to + */ + fuelIBrowserTab open(in nsIURI aURI); + + /** + * The events object for the browser window. + * supports: "TabOpen", "TabClose", "TabMove", "TabSelect" + */ + readonly attribute extIEvents events; +}; + +/** + * Interface representing a browser tab. + */ +[scriptable, uuid(3073ceff-777c-41ce-9ace-ab37268147c1)] +interface fuelIBrowserTab : nsISupports +{ + /** + * The current uri of this tab. + */ + readonly attribute nsIURI uri; + + /** + * The current index of this tab in the browser window. + */ + readonly attribute int32_t index; + + /** + * The browser window that is holding the tab. + */ + readonly attribute fuelIWindow window; + + /** + * The content document of the browser tab. + */ + readonly attribute nsIDOMHTMLDocument document; + + /** + * The events object for the browser tab. + * supports: "load" + */ + readonly attribute extIEvents events; + + /** + * Load a new URI into this browser tab. + * @param aURI + * The uri to load into the browser tab + */ + void load(in nsIURI aURI); + + /** + * Give focus to this browser tab, and bring it to the front. + */ + void focus(); + + /** + * Close the browser tab. This may not actually close the tab + * as script may abort the close operation. + */ + void close(); + + /** + * Moves this browser tab before another browser tab within the window. + * @param aBefore + * The tab before which the target tab will be moved + */ + void moveBefore(in fuelIBrowserTab aBefore); + + /** + * Move this browser tab to the last tab within the window. + */ + void moveToEnd(); +}; + +/** + * Interface for managing and accessing the applications systems + */ +[scriptable, uuid(fe74cf80-aa2d-11db-abbd-0800200c9a66)] +interface fuelIApplication : extIApplication +{ + /** + * The root bookmarks object for the application. + * Contains all the bookmark roots in the system. + */ + readonly attribute fuelIBookmarkRoots bookmarks; + + /** + * An array of browser windows within the application. + */ + readonly attribute nsIVariant windows; + + /** + * The currently active browser window. + */ + readonly attribute fuelIWindow activeWindow; +}; diff --git a/browser/components/fuel/moz.build b/browser/components/fuel/moz.build new file mode 100644 index 000000000..a81933d69 --- /dev/null +++ b/browser/components/fuel/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +XPIDL_SOURCES += ['fuelIApplication.idl'] + +XPIDL_MODULE = 'fuel' + +EXTRA_COMPONENTS += ['fuelApplication.manifest'] + +EXTRA_PP_COMPONENTS += ['fuelApplication.js'] + diff --git a/browser/components/moz.build b/browser/components/moz.build new file mode 100644 index 000000000..410efcf1c --- /dev/null +++ b/browser/components/moz.build @@ -0,0 +1,43 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +DIRS += [ + 'abouthome', + 'certerror', + 'dirprovider', + 'downloads', + 'feeds', + 'fuel', + 'newtab', + 'pageinfo', + 'places', + 'permissions', + 'preferences', + 'privatebrowsing', + 'search', + 'sessionstore', + 'shell', + 'statusbar', +] + +DIRS += ['build'] + +XPIDL_SOURCES += [ + 'nsIBrowserGlue.idl', + 'nsIBrowserHandler.idl', +] + +XPIDL_MODULE = 'browsercompsbase' + +EXTRA_PP_COMPONENTS += [ + 'BrowserComponents.manifest', + 'nsAboutRedirector.js', + 'nsBrowserContentHandler.js', + 'nsBrowserGlue.js', +] + +EXTRA_JS_MODULES += [ + 'distribution.js', +]
\ No newline at end of file diff --git a/browser/components/newtab/cells.js b/browser/components/newtab/cells.js new file mode 100644 index 000000000..cc1b8ee75 --- /dev/null +++ b/browser/components/newtab/cells.js @@ -0,0 +1,126 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This class manages a cell's DOM node (not the actually cell content, a site). + * It's mostly read-only, i.e. all manipulation of both position and content + * aren't handled here. + */ +function Cell(aGrid, aNode) { + this._grid = aGrid; + this._node = aNode; + this._node._newtabCell = this; + + // Register drag-and-drop event handlers. + ["dragenter", "dragover", "dragexit", "drop"].forEach(function(aType) { + this._node.addEventListener(aType, this, false); + }, this); +} + +Cell.prototype = { + /** + * The grid. + */ + _grid: null, + + /** + * The cell's DOM node. + */ + get node() { return this._node; }, + + /** + * The cell's offset in the grid. + */ + get index() { + let index = this._grid.cells.indexOf(this); + + // Cache this value, overwrite the getter. + Object.defineProperty(this, "index", {value: index, enumerable: true}); + + return index; + }, + + /** + * The previous cell in the grid. + */ + get previousSibling() { + let prev = this.node.previousElementSibling; + prev = prev && prev._newtabCell; + + // Cache this value, overwrite the getter. + Object.defineProperty(this, "previousSibling", {value: prev, enumerable: true}); + + return prev; + }, + + /** + * The next cell in the grid. + */ + get nextSibling() { + let next = this.node.nextElementSibling; + next = next && next._newtabCell; + + // Cache this value, overwrite the getter. + Object.defineProperty(this, "nextSibling", {value: next, enumerable: true}); + + return next; + }, + + /** + * The site contained in the cell, if any. + */ + get site() { + let firstChild = this.node.firstElementChild; + return firstChild && firstChild._newtabSite; + }, + + /** + * Checks whether the cell contains a pinned site. + * @return Whether the cell contains a pinned site. + */ + containsPinnedSite: function() { + let site = this.site; + return site && site.isPinned(); + }, + + /** + * Checks whether the cell contains a site (is empty). + * @return Whether the cell is empty. + */ + isEmpty: function() { + return !this.site; + }, + + /** + * Handles all cell events. + */ + handleEvent: function(aEvent) { + // We're not responding to external drag/drop events + // when our parent window is in private browsing mode. + if (inPrivateBrowsingMode() && !gDrag.draggedSite) + return; + + if (aEvent.type != "dragexit" && !gDrag.isValid(aEvent)) + return; + + switch (aEvent.type) { + case "dragenter": + aEvent.preventDefault(); + gDrop.enter(this, aEvent); + break; + case "dragover": + aEvent.preventDefault(); + break; + case "dragexit": + gDrop.exit(this, aEvent); + break; + case "drop": + aEvent.preventDefault(); + gDrop.drop(this, aEvent); + break; + } + } +}; diff --git a/browser/components/newtab/drag.js b/browser/components/newtab/drag.js new file mode 100644 index 000000000..566e3755f --- /dev/null +++ b/browser/components/newtab/drag.js @@ -0,0 +1,151 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton implements site dragging functionality. + */ +var gDrag = { + /** + * The site offset to the drag start point. + */ + _offsetX: null, + _offsetY: null, + + /** + * The site that is dragged. + */ + _draggedSite: null, + get draggedSite() { return this._draggedSite; }, + + /** + * The cell width/height at the point the drag started. + */ + _cellWidth: null, + _cellHeight: null, + get cellWidth() { return this._cellWidth; }, + get cellHeight() { return this._cellHeight; }, + + /** + * Start a new drag operation. + * @param aSite The site that's being dragged. + * @param aEvent The 'dragstart' event. + */ + start: function(aSite, aEvent) { + this._draggedSite = aSite; + + // Mark nodes as being dragged. + let selector = ".newtab-site, .newtab-control, .newtab-thumbnail"; + let parentCell = aSite.node.parentNode; + let nodes = parentCell.querySelectorAll(selector); + for (let i = 0; i < nodes.length; i++) + nodes[i].setAttribute("dragged", "true"); + + parentCell.setAttribute("dragged", "true"); + + this._setDragData(aSite, aEvent); + + // Store the cursor offset. + let node = aSite.node; + let rect = node.getBoundingClientRect(); + this._offsetX = aEvent.clientX - rect.left; + this._offsetY = aEvent.clientY - rect.top; + + // Store the cell dimensions. + let cellNode = aSite.cell.node; + this._cellWidth = cellNode.offsetWidth; + this._cellHeight = cellNode.offsetHeight; + + gTransformation.freezeSitePosition(aSite); + }, + + /** + * Handles the 'drag' event. + * @param aSite The site that's being dragged. + * @param aEvent The 'drag' event. + */ + drag: function(aSite, aEvent) { + // Get the viewport size. + let {clientWidth, clientHeight} = document.documentElement; + + // We'll want a padding of 5px. + let border = 5; + + // Enforce minimum constraints to keep the drag image inside the window. + let left = Math.max(scrollX + aEvent.clientX - this._offsetX, border); + let top = Math.max(scrollY + aEvent.clientY - this._offsetY, border); + + // Enforce maximum constraints to keep the drag image inside the window. + left = Math.min(left, scrollX + clientWidth - this.cellWidth - border); + top = Math.min(top, scrollY + clientHeight - this.cellHeight - border); + + // Update the drag image's position. + gTransformation.setSitePosition(aSite, {left: left, top: top}); + }, + + /** + * Ends the current drag operation. + * @param aSite The site that's being dragged. + * @param aEvent The 'dragend' event. + */ + end: function(aSite, aEvent) { + let nodes = gGrid.node.querySelectorAll("[dragged]") + for (let i = 0; i < nodes.length; i++) + nodes[i].removeAttribute("dragged"); + + // Slide the dragged site back into its cell (may be the old or the new cell). + gTransformation.slideSiteTo(aSite, aSite.cell, {unfreeze: true}); + + this._draggedSite = null; + }, + + /** + * Checks whether we're responsible for a given drag event. + * @param aEvent The drag event to check. + * @return Whether we should handle this drag and drop operation. + */ + isValid: function(aEvent) { + let link = gDragDataHelper.getLinkFromDragEvent(aEvent); + + // Check that the drag data is non-empty. + // Can happen when dragging places folders. + if (!link || !link.url) { + return false; + } + + // Check that we're not accepting URLs which would inherit the caller's + // principal (such as javascript: or data:). + return gLinkChecker.checkLoadURI(link.url); + }, + + /** + * Initializes the drag data for the current drag operation. + * @param aSite The site that's being dragged. + * @param aEvent The 'dragstart' event. + */ + _setDragData: function(aSite, aEvent) { + let {url, title} = aSite; + + let dt = aEvent.dataTransfer; + dt.mozCursor = "default"; + dt.effectAllowed = "move"; + dt.setData("text/plain", url); + dt.setData("text/uri-list", url); + dt.setData("text/x-moz-url", url + "\n" + title); + dt.setData("text/html", "<a href=\"" + url + "\">" + url + "</a>"); + + // Create and use an empty drag element. We don't want to use the default + // drag image with its default opacity. + let dragElement = document.createElementNS(HTML_NAMESPACE, "div"); + dragElement.classList.add("newtab-drag"); + let scrollbox = document.getElementById("newtab-vertical-margin"); + scrollbox.appendChild(dragElement); + dt.setDragImage(dragElement, 0, 0); + + // After the 'dragstart' event has been processed we can remove the + // temporary drag element from the DOM. + setTimeout(() => scrollbox.removeChild(dragElement), 0); + } +}; diff --git a/browser/components/newtab/dragDataHelper.js b/browser/components/newtab/dragDataHelper.js new file mode 100644 index 000000000..e92b9bb1c --- /dev/null +++ b/browser/components/newtab/dragDataHelper.js @@ -0,0 +1,22 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +var gDragDataHelper = { + get mimeType() { + return "text/x-moz-url"; + }, + + getLinkFromDragEvent: function(aEvent) { + let dt = aEvent.dataTransfer; + if (!dt || !dt.types.includes(this.mimeType)) { + return null; + } + + let data = dt.getData(this.mimeType) || ""; + let [url, title] = data.split(/[\r\n]+/); + return {url: url, title: title}; + } +}; diff --git a/browser/components/newtab/drop.js b/browser/components/newtab/drop.js new file mode 100644 index 000000000..fe402a29b --- /dev/null +++ b/browser/components/newtab/drop.js @@ -0,0 +1,150 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +// A little delay that prevents the grid from being too sensitive when dragging +// sites around. +const DELAY_REARRANGE_MS = 100; + +/** + * This singleton implements site dropping functionality. + */ +var gDrop = { + /** + * The last drop target. + */ + _lastDropTarget: null, + + /** + * Handles the 'dragenter' event. + * @param aCell The drop target cell. + */ + enter: function(aCell) { + this._delayedRearrange(aCell); + }, + + /** + * Handles the 'dragexit' event. + * @param aCell The drop target cell. + * @param aEvent The 'dragexit' event. + */ + exit: function(aCell, aEvent) { + if (aEvent.dataTransfer && !aEvent.dataTransfer.mozUserCancelled) { + this._delayedRearrange(); + } else { + // The drag operation has been cancelled. + this._cancelDelayedArrange(); + this._rearrange(); + } + }, + + /** + * Handles the 'drop' event. + * @param aCell The drop target cell. + * @param aEvent The 'dragexit' event. + */ + drop: function(aCell, aEvent) { + // The cell that is the drop target could contain a pinned site. We need + // to find out where that site has gone and re-pin it there. + if (aCell.containsPinnedSite()) + this._repinSitesAfterDrop(aCell); + + // Pin the dragged or insert the new site. + this._pinDraggedSite(aCell, aEvent); + + this._cancelDelayedArrange(); + + // Update the grid and move all sites to their new places. + gUpdater.updateGrid(); + }, + + /** + * Re-pins all pinned sites in their (new) positions. + * @param aCell The drop target cell. + */ + _repinSitesAfterDrop: function(aCell) { + let sites = gDropPreview.rearrange(aCell); + + // Filter out pinned sites. + let pinnedSites = sites.filter(function(aSite) { + return aSite && aSite.isPinned(); + }); + + // Re-pin all shifted pinned cells. + pinnedSites.forEach(aSite => aSite.pin(sites.indexOf(aSite))); + }, + + /** + * Pins the dragged site in its new place. + * @param aCell The drop target cell. + * @param aEvent The 'dragexit' event. + */ + _pinDraggedSite: function(aCell, aEvent) { + let index = aCell.index; + let draggedSite = gDrag.draggedSite; + + if (draggedSite) { + // Pin the dragged site at its new place. + if (aCell != draggedSite.cell) + draggedSite.pin(index); + } else { + let link = gDragDataHelper.getLinkFromDragEvent(aEvent); + if (link) { + // A new link was dragged onto the grid. Create it by pinning its URL. + gPinnedLinks.pin(link, index); + + // Make sure the newly added link is not blocked. + gBlockedLinks.unblock(link); + } + } + }, + + /** + * Time a rearrange with a little delay. + * @param aCell The drop target cell. + */ + _delayedRearrange: function(aCell) { + // The last drop target didn't change so there's no need to re-arrange. + if (this._lastDropTarget == aCell) + return; + + let self = this; + + function callback() { + self._rearrangeTimeout = null; + self._rearrange(aCell); + } + + this._cancelDelayedArrange(); + this._rearrangeTimeout = setTimeout(callback, DELAY_REARRANGE_MS); + + // Store the last drop target. + this._lastDropTarget = aCell; + }, + + /** + * Cancels a timed rearrange, if any. + */ + _cancelDelayedArrange: function() { + if (this._rearrangeTimeout) { + clearTimeout(this._rearrangeTimeout); + this._rearrangeTimeout = null; + } + }, + + /** + * Rearrange all sites in the grid depending on the current drop target. + * @param aCell The drop target cell. + */ + _rearrange: function(aCell) { + let sites = gGrid.sites; + + // We need to rearrange the grid only if there's a current drop target. + if (aCell) + sites = gDropPreview.rearrange(aCell); + + gTransformation.rearrangeSites(sites, {unfreeze: !aCell}); + } +}; diff --git a/browser/components/newtab/dropPreview.js b/browser/components/newtab/dropPreview.js new file mode 100644 index 000000000..219b84c89 --- /dev/null +++ b/browser/components/newtab/dropPreview.js @@ -0,0 +1,222 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton provides the ability to re-arrange the current grid to + * indicate the transformation that results from dropping a cell at a certain + * position. + */ +var gDropPreview = { + /** + * Rearranges the sites currently contained in the grid when a site would be + * dropped onto the given cell. + * @param aCell The drop target cell. + * @return The re-arranged array of sites. + */ + rearrange: function(aCell) { + let sites = gGrid.sites; + + // Insert the dragged site into the current grid. + this._insertDraggedSite(sites, aCell); + + // After the new site has been inserted we need to correct the positions + // of all pinned tabs that have been moved around. + this._repositionPinnedSites(sites, aCell); + + return sites; + }, + + /** + * Inserts the currently dragged site into the given array of sites. + * @param aSites The array of sites to insert into. + * @param aCell The drop target cell. + */ + _insertDraggedSite: function(aSites, aCell) { + let dropIndex = aCell.index; + let draggedSite = gDrag.draggedSite; + + // We're currently dragging a site. + if (draggedSite) { + let dragCell = draggedSite.cell; + let dragIndex = dragCell.index; + + // Move the dragged site into its new position. + if (dragIndex != dropIndex) { + aSites.splice(dragIndex, 1); + aSites.splice(dropIndex, 0, draggedSite); + } + // We're handling an external drag item. + } else { + aSites.splice(dropIndex, 0, null); + } + }, + + /** + * Correct the position of all pinned sites that might have been moved to + * different positions after the dragged site has been inserted. + * @param aSites The array of sites containing the dragged site. + * @param aCell The drop target cell. + */ + _repositionPinnedSites: + function(aSites, aCell) { + + // Collect all pinned sites. + let pinnedSites = this._filterPinnedSites(aSites, aCell); + + // Correct pinned site positions. + pinnedSites.forEach(function(aSite) { + aSites[aSites.indexOf(aSite)] = aSites[aSite.cell.index]; + aSites[aSite.cell.index] = aSite; + }, this); + + // There might be a pinned cell that got pushed out of the grid, try to + // sneak it in by removing a lower-priority cell. + if (this._hasOverflowedPinnedSite(aSites, aCell)) + this._repositionOverflowedPinnedSite(aSites, aCell); + }, + + /** + * Filter pinned sites out of the grid that are still on their old positions + * and have not moved. + * @param aSites The array of sites to filter. + * @param aCell The drop target cell. + * @return The filtered array of sites. + */ + _filterPinnedSites: function(aSites, aCell) { + let draggedSite = gDrag.draggedSite; + + // When dropping on a cell that contains a pinned site make sure that all + // pinned cells surrounding the drop target are moved as well. + let range = this._getPinnedRange(aCell); + + return aSites.filter(function(aSite, aIndex) { + // The site must be valid, pinned and not the dragged site. + if (!aSite || aSite == draggedSite || !aSite.isPinned()) + return false; + + let index = aSite.cell.index; + + // If it's not in the 'pinned range' it's a valid pinned site. + return (index > range.end || index < range.start); + }); + }, + + /** + * Determines the range of pinned sites surrounding the drop target cell. + * @param aCell The drop target cell. + * @return The range of pinned cells. + */ + _getPinnedRange: function(aCell) { + let dropIndex = aCell.index; + let range = {start: dropIndex, end: dropIndex}; + + // We need a pinned range only when dropping on a pinned site. + if (aCell.containsPinnedSite()) { + let links = gPinnedLinks.links; + + // Find all previous siblings of the drop target that are pinned as well. + while (range.start && links[range.start - 1]) + range.start--; + + let maxEnd = links.length - 1; + + // Find all next siblings of the drop target that are pinned as well. + while (range.end < maxEnd && links[range.end + 1]) + range.end++; + } + + return range; + }, + + /** + * Checks if the given array of sites contains a pinned site that has + * been pushed out of the grid. + * @param aSites The array of sites to check. + * @param aCell The drop target cell. + * @return Whether there is an overflowed pinned cell. + */ + _hasOverflowedPinnedSite: + function(aSites, aCell) { + + // If the drop target isn't pinned there's no way a pinned site has been + // pushed out of the grid so we can just exit here. + if (!aCell.containsPinnedSite()) + return false; + + let cells = gGrid.cells; + + // No cells have been pushed out of the grid, nothing to do here. + if (aSites.length <= cells.length) + return false; + + let overflowedSite = aSites[cells.length]; + + // Nothing to do if the site that got pushed out of the grid is not pinned. + return (overflowedSite && overflowedSite.isPinned()); + }, + + /** + * We have a overflowed pinned site that we need to re-position so that it's + * visible again. We try to find a lower-priority cell (empty or containing + * an unpinned site) that we can move it to. + * @param aSites The array of sites. + * @param aCell The drop target cell. + */ + _repositionOverflowedPinnedSite: + function(aSites, aCell) { + + // Try to find a lower-priority cell (empty or containing an unpinned site). + let index = this._indexOfLowerPrioritySite(aSites, aCell); + + if (index > -1) { + let cells = gGrid.cells; + let dropIndex = aCell.index; + + // Move all pinned cells to their new positions to let the overflowed + // site fit into the grid. + for (let i = index + 1, lastPosition = index; i < aSites.length; i++) { + if (i != dropIndex) { + aSites[lastPosition] = aSites[i]; + lastPosition = i; + } + } + + // Finally, remove the overflowed site from its previous position. + aSites.splice(cells.length, 1); + } + }, + + /** + * Finds the index of the last cell that is empty or contains an unpinned + * site. These are considered to be of a lower priority. + * @param aSites The array of sites. + * @param aCell The drop target cell. + * @return The cell's index. + */ + _indexOfLowerPrioritySite: + function(aSites, aCell) { + + let cells = gGrid.cells; + let dropIndex = aCell.index; + + // Search (beginning with the last site in the grid) for a site that is + // empty or unpinned (an thus lower-priority) and can be pushed out of the + // grid instead of the pinned site. + for (let i = cells.length - 1; i >= 0; i--) { + // The cell that is our drop target is not a good choice. + if (i == dropIndex) + continue; + + let site = aSites[i]; + + // We can use the cell only if it's empty or the site is un-pinned. + if (!site || !site.isPinned()) + return i; + } + + return -1; + } +}; diff --git a/browser/components/newtab/dropTargetShim.js b/browser/components/newtab/dropTargetShim.js new file mode 100644 index 000000000..698a9e33e --- /dev/null +++ b/browser/components/newtab/dropTargetShim.js @@ -0,0 +1,232 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton provides a custom drop target detection. We need this because + * the default DnD target detection relies on the cursor's position. We want + * to pick a drop target based on the dragged site's position. + */ +var gDropTargetShim = { + /** + * Cache for the position of all cells, cleaned after drag finished. + */ + _cellPositions: null, + + /** + * The last drop target that was hovered. + */ + _lastDropTarget: null, + + /** + * Initializes the drop target shim. + */ + init: function() { + gGrid.node.addEventListener("dragstart", this, true); + }, + + /** + * Add all event listeners needed during a drag operation. + */ + _addEventListeners: function() { + gGrid.node.addEventListener("dragend", this); + + let docElement = document.documentElement; + docElement.addEventListener("dragover", this); + docElement.addEventListener("dragenter", this); + docElement.addEventListener("drop", this); + }, + + /** + * Remove all event listeners that were needed during a drag operation. + */ + _removeEventListeners: function() { + gGrid.node.removeEventListener("dragend", this); + + let docElement = document.documentElement; + docElement.removeEventListener("dragover", this); + docElement.removeEventListener("dragenter", this); + docElement.removeEventListener("drop", this); + }, + + /** + * Handles all shim events. + */ + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "dragstart": + this._dragstart(aEvent); + break; + case "dragenter": + aEvent.preventDefault(); + break; + case "dragover": + this._dragover(aEvent); + break; + case "drop": + this._drop(aEvent); + break; + case "dragend": + this._dragend(aEvent); + break; + } + }, + + /** + * Handles the 'dragstart' event. + * @param aEvent The 'dragstart' event. + */ + _dragstart: function(aEvent) { + if (aEvent.target.classList.contains("newtab-link")) { + gGrid.lock(); + this._addEventListeners(); + } + }, + + /** + * Handles the 'dragover' event. + * @param aEvent The 'dragover' event. + */ + _dragover: function(aEvent) { + // XXX bug 505521 - Use the dragover event to retrieve the + // current mouse coordinates while dragging. + let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode; + gDrag.drag(sourceNode._newtabSite, aEvent); + + // Find the current drop target, if there's one. + this._updateDropTarget(aEvent); + + // If we have a valid drop target, + // let the drag-and-drop service know. + if (this._lastDropTarget) { + aEvent.preventDefault(); + } + }, + + /** + * Handles the 'drop' event. + * @param aEvent The 'drop' event. + */ + _drop: function(aEvent) { + // We're accepting all drops. + aEvent.preventDefault(); + + // remember that drop event was seen, this explicitly + // assumes that drop event preceeds dragend event + this._dropSeen = true; + + // Make sure to determine the current drop target + // in case the dragover event hasn't been fired. + this._updateDropTarget(aEvent); + + // A site was successfully dropped. + this._dispatchEvent(aEvent, "drop", this._lastDropTarget); + }, + + /** + * Handles the 'dragend' event. + * @param aEvent The 'dragend' event. + */ + _dragend: function(aEvent) { + if (this._lastDropTarget) { + if (aEvent.dataTransfer.mozUserCancelled || !this._dropSeen) { + // The drag operation was cancelled or no drop event was generated + this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); + this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); + } + + // Clean up. + this._lastDropTarget = null; + this._cellPositions = null; + } + + this._dropSeen = false; + gGrid.unlock(); + this._removeEventListeners(); + }, + + /** + * Tries to find the current drop target and will fire + * appropriate dragenter, dragexit, and dragleave events. + * @param aEvent The current drag event. + */ + _updateDropTarget: function(aEvent) { + // Let's see if we find a drop target. + let target = this._findDropTarget(aEvent); + + if (target != this._lastDropTarget) { + if (this._lastDropTarget) + // We left the last drop target. + this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); + + if (target) + // We're now hovering a (new) drop target. + this._dispatchEvent(aEvent, "dragenter", target); + + if (this._lastDropTarget) + // We left the last drop target. + this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); + + this._lastDropTarget = target; + } + }, + + /** + * Determines the current drop target by matching the dragged site's position + * against all cells in the grid. + * @return The currently hovered drop target or null. + */ + _findDropTarget: function() { + // These are the minimum intersection values - we want to use the cell if + // the site is >= 50% hovering its position. + let minWidth = gDrag.cellWidth / 2; + let minHeight = gDrag.cellHeight / 2; + + let cellPositions = this._getCellPositions(); + let rect = gTransformation.getNodePosition(gDrag.draggedSite.node); + + // Compare each cell's position to the dragged site's position. + for (let i = 0; i < cellPositions.length; i++) { + let inter = rect.intersect(cellPositions[i].rect); + + // If the intersection is big enough we found a drop target. + if (inter.width >= minWidth && inter.height >= minHeight) + return cellPositions[i].cell; + } + + // No drop target found. + return null; + }, + + /** + * Gets the positions of all cell nodes. + * @return The (cached) cell positions. + */ + _getCellPositions: function() { + if (this._cellPositions) + return this._cellPositions; + + return this._cellPositions = gGrid.cells.map(function(cell) { + return {cell: cell, rect: gTransformation.getNodePosition(cell.node)}; + }); + }, + + /** + * Dispatches a custom DragEvent on the given target node. + * @param aEvent The source event. + * @param aType The event type. + * @param aTarget The target node that receives the event. + */ + _dispatchEvent: function(aEvent, aType, aTarget) { + let node = aTarget.node; + let event = document.createEvent("DragEvent"); + + // The event should not bubble to prevent recursion. + event.initDragEvent(aType, false, true, window, 0, 0, 0, 0, 0, false, false, + false, false, 0, node, aEvent.dataTransfer); + + node.dispatchEvent(event); + } +}; diff --git a/browser/components/newtab/grid.js b/browser/components/newtab/grid.js new file mode 100644 index 000000000..118159f9c --- /dev/null +++ b/browser/components/newtab/grid.js @@ -0,0 +1,175 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton represents the grid that contains all sites. + */ +var gGrid = { + /** + * The DOM node of the grid. + */ + _node: null, + _gridDefaultContent: null, + get node() { return this._node; }, + + /** + * The cached DOM fragment for sites. + */ + _siteFragment: null, + + /** + * All cells contained in the grid. + */ + _cells: [], + get cells() { return this._cells; }, + + /** + * All sites contained in the grid's cells. Sites may be empty. + */ + get sites() { + // return [for (cell of this.cells) cell.site]; + let aSites = []; + for (let cell of this.cells) { + aSites.push(cell.site); + } + return aSites; + }, + + // Tells whether the grid has already been initialized. + get ready() { return !!this._ready; }, + + // Returns whether the page has finished loading yet. + get isDocumentLoaded() { return document.readyState == "complete"; }, + + /** + * Initializes the grid. + * @param aSelector The query selector of the grid. + */ + init: function() { + this._node = document.getElementById("newtab-grid"); + this._gridDefaultContent = this._node.lastChild; + this._createSiteFragment(); + + gLinks.populateCache(() => { + this._refreshGrid(); + this._ready = true; + }); + }, + + /** + * Creates a new site in the grid. + * @param aLink The new site's link. + * @param aCell The cell that will contain the new site. + * @return The newly created site. + */ + createSite: function(aLink, aCell) { + let node = aCell.node; + node.appendChild(this._siteFragment.cloneNode(true)); + return new Site(node.firstElementChild, aLink); + }, + + /** + * Handles all grid events. + */ + handleEvent: function(aEvent) { + // Any specific events should go here. + }, + + /** + * Locks the grid to block all pointer events. + */ + lock: function() { + this.node.setAttribute("locked", "true"); + }, + + /** + * Unlocks the grid to allow all pointer events. + */ + unlock: function() { + this.node.removeAttribute("locked"); + }, + + /** + * Renders the grid. + */ + refresh() { + this._refreshGrid(); + }, + + /** + * Renders the grid, including cells and sites. + */ + _refreshGrid() { + let row = document.createElementNS(HTML_NAMESPACE, "div"); + row.classList.add("newtab-row"); + let cell = document.createElementNS(HTML_NAMESPACE, "div"); + cell.classList.add("newtab-cell"); + + // Clear the grid + this._node.innerHTML = ""; + + // Creates the structure of one row + for (let i = 0; i < gGridPrefs.gridColumns; i++) { + row.appendChild(cell.cloneNode(true)); + } + + // Creates the grid + for (let j = 0; j < gGridPrefs.gridRows; j++) { + this._node.appendChild(row.cloneNode(true)); + } + + // Create cell array. + let cellElements = this.node.querySelectorAll(".newtab-cell"); + let cells = Array.from(cellElements, (cell) => new Cell(this, cell)); + + // Fetch links. + let links = gLinks.getLinks(); + + // Create sites. + let numLinks = Math.min(links.length, cells.length); + for (let i = 0; i < numLinks; i++) { + if (links[i]) { + this.createSite(links[i], cells[i]); + } + } + + this._cells = cells; + }, + + /** + * Creates the DOM fragment that is re-used when creating sites. + */ + _createSiteFragment: function() { + let site = document.createElementNS(HTML_NAMESPACE, "div"); + site.classList.add("newtab-site"); + site.setAttribute("draggable", "true"); + + // Create the site's inner HTML code. + site.innerHTML = + '<a class="newtab-link">' + + ' <span class="newtab-thumbnail placeholder"/>' + + ' <span class="newtab-thumbnail thumbnail"/>' + + ' <span class="newtab-title"/>' + + '</a>' + + '<input type="button" title="' + newTabString("pin") + '"' + + ' class="newtab-control newtab-control-pin"/>' + + '<input type="button" title="' + newTabString("block") + '"' + + ' class="newtab-control newtab-control-block"/>'; + + this._siteFragment = document.createDocumentFragment(); + this._siteFragment.appendChild(site); + }, + + /** + * Test a tile at a given position for being pinned or history + * @param position Position in sites array + */ + _isHistoricalTile: function(aPos) { + let site = this.sites[aPos]; + return site && (site.isPinned() || site.link && site.link.type == "history"); + } + +}; diff --git a/browser/components/newtab/jar.mn b/browser/components/newtab/jar.mn new file mode 100644 index 000000000..2d6291422 --- /dev/null +++ b/browser/components/newtab/jar.mn @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/newtab/newTab.xhtml +* content/browser/newtab/newTab.js + content/browser/newtab/newTab.css
\ No newline at end of file diff --git a/browser/components/newtab/moz.build b/browser/components/newtab/moz.build new file mode 100644 index 000000000..8267a660d --- /dev/null +++ b/browser/components/newtab/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] + diff --git a/browser/components/newtab/newTab.css b/browser/components/newtab/newTab.css new file mode 100644 index 000000000..3cbcf452f --- /dev/null +++ b/browser/components/newtab/newTab.css @@ -0,0 +1,336 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +html { + width: 100%; + height: 100%; +} + +body { + font: message-box; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + background-color: #eee; + display: -moz-box; + position: relative; + -moz-box-flex: 1; + -moz-user-focus: normal; + -moz-box-orient: vertical; +} + +input { + font: message-box; + font-size: 16px; +} + +input[type=button] { + cursor: pointer; +} + +/* UNDO */ +#newtab-undo-container { + transition: opacity 100ms ease-out; + -moz-box-align: center; + -moz-box-pack: center; +} + +#newtab-undo-container[undo-disabled] { + opacity: 0; + pointer-events: none; +} + +/* TOGGLE */ +#newtab-toggle { + position: absolute; + top: 12px; + right: 12px; +} + +#newtab-toggle:-moz-locale-dir(rtl) { + left: 12px; + right: auto; +} + +/* MARGINS */ +#newtab-vertical-margin { + display: -moz-box; + position: relative; + -moz-box-flex: 1; + -moz-box-orient: vertical; +} + +#newtab-margin-undo-container { + display: -moz-box; + left: 6px; + position: absolute; + top: 6px; + z-index: 1; +} + +#newtab-margin-undo-container:dir(rtl) { + left: auto; + right: 6px; +} + +#newtab-undo-close-button:dir(rtl) { + float:left; +} + +#newtab-horizontal-margin { + display: -moz-box; + -moz-box-flex: 5; +} + +#newtab-margin-top { + min-height: 10px; + max-height: 30px; + display: -moz-box; + -moz-box-flex: 1; + -moz-box-align: center; + -moz-box-pack: center; +} + +#newtab-margin-bottom { + min-height: 40px; + max-height: 80px; + -moz-box-flex: 1; +} + +.newtab-side-margin { + min-width: 40px; + max-width: 300px; + -moz-box-flex: 1; +} + +/* GRID */ +#newtab-grid { + display: -moz-box; + -moz-box-flex: 5; + -moz-box-orient: vertical; + min-width: 600px; + min-height: 400px; + transition: 175ms ease-out; + transition-property: opacity; +} + +#newtab-grid[page-disabled] { + opacity: 0; +} + +#newtab-grid[locked], +#newtab-grid[page-disabled] { + pointer-events: none; +} + +/* ROWS */ +.newtab-row { + display: -moz-box; + -moz-box-orient: horizontal; + -moz-box-direction: normal; + -moz-box-flex: 1; +} + +/* + * Thumbnail image sizes are determined in the preferences: + * toolkit.pageThumbs.minWidth + * toolkit.pageThumbs.minHeight + */ +/* CELLS */ +.newtab-cell { + display: -moz-box; + -moz-box-flex: 1; +} + +/* SITES */ +.newtab-site { + position: relative; + -moz-box-flex: 1; + transition: 150ms ease-out; + transition-property: top, left, opacity; +} + +.newtab-site[frozen] { + position: absolute; + pointer-events: none; +} + +.newtab-site[dragged] { + transition-property: none; + z-index: 10; +} + +/* LINK + THUMBNAILS */ +.newtab-link, +.newtab-thumbnail { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; +} + +/* TITLES */ +.newtab-title { + overflow: hidden; + position: absolute; + right: 0; + text-align: center; +} + +.newtab-title { + bottom: 0; + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; +} + +.newtab-title { + left: 0; + padding: 0 4px; +} + +/* CONTROLS */ +.newtab-control { + position: absolute; + opacity: 0; + transition: opacity 100ms ease-out; +} + +.newtab-control:-moz-focusring, +.newtab-cell:not([ignorehover]) > .newtab-site:hover > .newtab-control { + opacity: 1; +} + +.newtab-control[dragged] { + opacity: 0 !important; +} + +@media (-moz-touch-enabled) { + .newtab-control { + opacity: 1; + } +} + +/* DRAG & DROP */ + +/* + * This is just a temporary drag element used for dataTransfer.setDragImage() + * so that we can use custom drag images and elements. It needs an opacity of + * 0.01 so that the core code detects that it's in fact a visible element. + */ +.newtab-drag { + width: 1px; + height: 1px; + background-color: #fff; + opacity: 0.01; +} + +/* SEARCH */ +#searchContainer { + display: -moz-box; + position: relative; + -moz-box-pack: center; + margin: 10px 0 15px; +} + +#searchForm { + width: 470px; + display: -moz-box; + position: relative; + height: 36px; /* 32 px logo + 2*1px pad + 2*1px border */ + -moz-box-flex: 1; + max-width: 600px; +} + +#searchEngineLogo { + border: 1px transparent; + padding: 2px 4px 2px 2px; + margin: 0; + width: 32px; + height: 32px; + position: absolute; +} + +#searchText { + -moz-box-flex: 1; + padding-top: 6px; + padding-bottom: 6px; + padding-inline-start: 38px; /* room for logo */ + padding-inline-end: 8px; + background: rgba(255, 255, 255, 0.9) padding-box; + border: 1px solid; + border-color: rgba(37, 46, 65, 0.15) rgba(37, 46, 65, 0.17) rgba(37, 46, 65, 0.2); + box-shadow: 0 1px 0 rgba(37, 46, 65, 0.02) inset, + 0 0 2px rgba(37, 46, 65, 0.1) inset, + 0 1px 0 rgba(255, 255, 255, 0.2); + border-radius: 2.5px 0 0 2.5px; +} + +#searchText:-moz-dir(rtl) { + border-radius: 0 2.5px 2.5px 0; +} + +#searchText:focus, +#searchText[autofocus] { + border-color: rgba(92, 133, 214, 0.6) rgba(78, 114, 188, 0.6) rgba(41, 82, 163, 0.6); +} + +#searchText::placeholder { + font-style: italic; + opacity: 0.3; +} + +#searchSubmit { + margin-inline-start: -1px; + background: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1)) padding-box; + padding: 0 9px; + border: 1px solid; + border-color: rgba(37, 46, 65, 0.2) rgba(37, 46, 65, 0.2) rgba(37, 46, 65, 0.2); + border-inline-start: 1px solid transparent; + border-radius: 0 2.5px 2.5px 0; + box-shadow: 0 0 2px rgba(255, 255, 255, 0.5) inset, + 0 1px 0 rgba(255, 255, 255, 0.2); + cursor: pointer; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; +} + +#searchSubmit:-moz-dir(rtl) { + border-radius: 2.5px 0 0 2.5px; +} + +#searchText:focus + #searchSubmit, +#searchText + #searchSubmit:hover, +#searchText[autofocus] + #searchSubmit { + border-color: #8da1c8 #768bb5 #6579a2; + color: white; +} + +#searchText:focus + #searchSubmit, +#searchText[autofocus] + #searchSubmit { + background-image: linear-gradient(#85a8e0, #3d75cf); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2) inset, + 0 0 0 1px rgba(255, 255, 255, 0.1) inset, + 0 1px 0 rgba(23, 46, 67, 0.03); +} + +#searchText + #searchSubmit:hover { + background-image: linear-gradient(#85a8e0, #3d75cf); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2) inset, + 0 0 0 1px rgba(255, 255, 255, 0.1) inset, + 0 1px 0 rgba(23, 42, 79, 0.03), + 0 0 4px rgba(0, 34, 102, 0.2);} + +#searchText + #searchSubmit:hover:active { + box-shadow: 0 1px 1px rgba(3, 11, 27, 0.1) inset, + 0 0 1px rgba(3, 11, 27, 0.2) inset; + transition-duration: 0ms; +} + +.contentSearchSuggestionTable { + font: message-box; + font-size: 16px; +} diff --git a/browser/components/newtab/newTab.js b/browser/components/newtab/newTab.js new file mode 100644 index 000000000..0022f21bb --- /dev/null +++ b/browser/components/newtab/newTab.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/. */ + +"use strict"; + +var Cu = Components.utils; +var Ci = Components.interfaces; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PageThumbs.jsm"); +Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm"); +Cu.import("resource://gre/modules/NewTabUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Rect", + "resource://gre/modules/Geometry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +var { + links: gLinks, + allPages: gAllPages, + linkChecker: gLinkChecker, + pinnedLinks: gPinnedLinks, + blockedLinks: gBlockedLinks, + gridPrefs: gGridPrefs +} = NewTabUtils; + +XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() { + return Services.strings. + createBundle("chrome://browser/locale/newTab.properties"); +}); + +function newTabString(name, args) { + let stringName = "newtab." + name; + if (!args) { + return gStringBundle.GetStringFromName(stringName); + } + return gStringBundle.formatStringFromName(stringName, args, args.length); +} + +function inPrivateBrowsingMode() { + return PrivateBrowsingUtils.isContentWindowPrivate(window); +} + +const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; +const XUL_NAMESPACE = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +const TILES_EXPLAIN_LINK = "https://support.mozilla.org/kb/how-do-tiles-work-firefox"; +const TILES_INTRO_LINK = "https://www.mozilla.org/firefox/tiles/"; +const TILES_PRIVACY_LINK = "https://www.mozilla.org/privacy/"; + +#include transformations.js +#include page.js +#include grid.js +#include cells.js +#include sites.js +#include drag.js +#include dragDataHelper.js +#include drop.js +#include dropTargetShim.js +#include dropPreview.js +#include updater.js +#include undo.js +#include search.js + +// Everything is loaded. Initialize the New Tab Page. +gPage.init(); diff --git a/browser/components/newtab/newTab.xhtml b/browser/components/newtab/newTab.xhtml new file mode 100644 index 000000000..de000e723 --- /dev/null +++ b/browser/components/newtab/newTab.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd"> + %newTabDTD; + <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> + %browserDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>&newtab.pageTitle;</title> + + <link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/" /> + <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/content/newtab/newTab.css" /> + <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/newtab/newTab.css" /> +</head> + +<body dir="&locale.dir;"> + <div id="newtab-vertical-margin"> + <div id="newtab-margin-top"/> + + <div id="newtab-margin-undo-container"> + <div id="newtab-undo-container" undo-disabled="true"> + <label id="newtab-undo-label">&newtab.undo.removedLabel;</label> + <button id="newtab-undo-button" tabindex="-1" + class="newtab-undo-button">&newtab.undo.undoButton;</button> + <button id="newtab-undo-restore-button" tabindex="-1" + class="newtab-undo-button">&newtab.undo.restoreButton;</button> + <button id="newtab-undo-close-button" tabindex="-1" title="&newtab.undo.closeTooltip;"/> + </div> + </div> + + <div id="searchContainer"> + <form name="searchForm" id="searchForm" onsubmit="onSearchSubmit(event)"> + <div id="searchLogoContainer"><img id="searchEngineLogo"/></div> + <input type="text" name="q" value="" id="searchText" maxlength="256"/> + <input id="searchSubmit" type="submit" value="&newtab.searchEngineButton.label;"/> + </form> + </div> + + <div id="newtab-horizontal-margin"> + <div class="newtab-side-margin"/> + <div id="newtab-grid"> + <!-- site grid --> + </div> + <div class="newtab-side-margin"/> + </div> + + <div id="newtab-margin-bottom"/> + <input id="newtab-toggle" type="button"/> + </div> +</body> +<script type="text/javascript;version=1.8" src="chrome://browser/content/newtab/newTab.js"/> +</html> diff --git a/browser/components/newtab/page.js b/browser/components/newtab/page.js new file mode 100644 index 000000000..39a3b1c85 --- /dev/null +++ b/browser/components/newtab/page.js @@ -0,0 +1,239 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +// The amount of time we wait while coalescing updates for hidden pages. +const SCHEDULE_UPDATE_TIMEOUT_MS = 1000; + +/** + * This singleton represents the whole 'New Tab Page' and takes care of + * initializing all its components. + */ +var gPage = { + /** + * Initializes the page. + */ + init: function() { + // Add ourselves to the list of pages to receive notifications. + gAllPages.register(this); + + // Listen for 'unload' to unregister this page. + addEventListener("unload", this, false); + + // Listen for toggle button clicks. + let button = document.getElementById("newtab-toggle"); + button.addEventListener("click", e => this.toggleEnabled(e)); + + // XXX bug 991111 - Not all click events are correctly triggered when + // listening from xhtml nodes -- in particular middle clicks on sites, so + // listen from the xul window and filter then delegate + addEventListener("click", this, false); + + // Check if the new tab feature is enabled. + let enabled = gAllPages.enabled; + if (enabled) + this._init(); + + this._updateAttributes(enabled); + }, + + /** + * Listens for notifications specific to this page. + */ + observe: function(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed") { + let enabled = gAllPages.enabled; + this._updateAttributes(enabled); + + // Initialize the whole page if we haven't done that, yet. + if (enabled) { + this._init(); + } else { + gUndoDialog.hide(); + } + } else if (aTopic == "page-thumbnail:create" && gGrid.ready) { + for (let site of gGrid.sites) { + if (site && site.url === aData) { + site.refreshThumbnail(); + } + } + } + }, + + /** + * Updates the page's grid right away for visible pages. If the page is + * currently hidden, i.e. in a background tab or in the preloader, then we + * batch multiple update requests and refresh the grid once after a short + * delay. Accepts a single parameter the specifies the reason for requesting + * a page update. The page may decide to delay or prevent a requested updated + * based on the given reason. + */ + update(reason = "") { + // Update immediately if we're visible. + if (!document.hidden) { + // Ignore updates where reason=links-changed as those signal that the + // provider's set of links changed. We don't want to update visible pages + // in that case, it is ok to wait until the user opens the next tab. + if (reason != "links-changed" && gGrid.ready) { + gGrid.refresh(); + } + + return; + } + + // Bail out if we scheduled before. + if (this._scheduleUpdateTimeout) { + return; + } + + this._scheduleUpdateTimeout = setTimeout(() => { + // Refresh if the grid is ready. + if (gGrid.ready) { + gGrid.refresh(); + } + + this._scheduleUpdateTimeout = null; + }, SCHEDULE_UPDATE_TIMEOUT_MS); + }, + + /** + * Internally initializes the page. This runs only when/if the feature + * is/gets enabled. + */ + _init: function() { + if (this._initialized) + return; + + this._initialized = true; + + // XXX: This comment makes no sense for what it does. There is no HC check, + // and it changes the button unconditionally to a "play" button, which is + // wrong for a search action. Commented out for now. + + // Set submit button label for when CSS background are disabled (e.g. + // high contrast mode). + //document.getElementById("searchSubmit").value = + // document.body.getAttribute("dir") == "ltr" ? "\u25B6" : "\u25C0"; + + if (document.hidden) { + addEventListener("visibilitychange", this); + } else { + setTimeout(() => this.onPageFirstVisible()); + } + + // Initialize and render the grid. + gGrid.init(); + + // Initialize the drop target shim. + gDropTargetShim.init(); + }, + + /** + * Updates the 'page-disabled' attributes of the respective DOM nodes. + * @param aValue Whether the New Tab Page is enabled or not. + */ + _updateAttributes: function(aValue) { + // Set the nodes' states. + let nodeSelector = "#newtab-grid, #searchContainer"; + for (let node of document.querySelectorAll(nodeSelector)) { + if (aValue) + node.removeAttribute("page-disabled"); + else + node.setAttribute("page-disabled", "true"); + } + + // Enables/disables the control and link elements. + let inputSelector = ".newtab-control, .newtab-link"; + for (let input of document.querySelectorAll(inputSelector)) { + if (aValue) + input.removeAttribute("tabindex"); + else + input.setAttribute("tabindex", "-1"); + } + }, + + /** + * Handles unload event + */ + _handleUnloadEvent: function() { + gAllPages.unregister(this); + }, + + /** + * Handles all page events. + */ + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "load": + this.onPageVisibleAndLoaded(); + break; + case "unload": + this._handleUnloadEvent(); + break; + case "click": + let {button, target} = aEvent; + // Go up ancestors until we find a Site or not + while (target) { + if (target.hasOwnProperty("_newtabSite")) { + target._newtabSite.onClick(aEvent); + break; + } + target = target.parentNode; + } + break; + case "dragover": + if (gDrag.isValid(aEvent) && gDrag.draggedSite) + aEvent.preventDefault(); + break; + case "drop": + if (gDrag.isValid(aEvent) && gDrag.draggedSite) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + break; + case "visibilitychange": + // Cancel any delayed updates for hidden pages now that we're visible. + if (this._scheduleUpdateTimeout) { + clearTimeout(this._scheduleUpdateTimeout); + this._scheduleUpdateTimeout = null; + + // An update was pending so force an update now. + this.update(); + } + + setTimeout(() => this.onPageFirstVisible()); + removeEventListener("visibilitychange", this); + break; + } + }, + + onPageFirstVisible: function() { + for (let site of gGrid.sites) { + if (site) { + // The site may need to modify and/or re-render itself if + // something changed after newtab was created by preloader. + // For example, the suggested tile endTime may have passed. + site.onFirstVisible(); + } + } + + // save timestamp to compute page life-span delta + this._firstVisibleTime = Date.now(); + + if (document.readyState == "complete") { + this.onPageVisibleAndLoaded(); + } else { + addEventListener("load", this); + } + }, + + onPageVisibleAndLoaded() { + }, + + toggleEnabled: function(aEvent) { + gAllPages.enabled = !gAllPages.enabled; + aEvent.stopPropagation(); + } +}; diff --git a/browser/components/newtab/search.js b/browser/components/newtab/search.js new file mode 100644 index 000000000..78bc171ef --- /dev/null +++ b/browser/components/newtab/search.js @@ -0,0 +1,95 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +#include ../shared/searchenginelogos.js + +// This global tracks if the page has been set up before, to prevent double inits +var gInitialized = false; +var gObserver = new MutationObserver(function (mutations) { + for (let mutation of mutations) { + if (mutation.attributeName == "searchEngineURL") { + setupSearchEngine(); + if (!gInitialized) { + gInitialized = true; + } + return; + } + } +}); + +window.addEventListener("pageshow", function () { + window.gObserver.observe(document.documentElement, { attributes: true }); +}); + +window.addEventListener("pagehide", function() { + window.gObserver.disconnect(); +}); + +function onSearchSubmit(aEvent) { + let searchTerms = document.getElementById("searchText").value; + let searchURL = document.documentElement.getAttribute("searchEngineURL"); + + if (searchURL && searchTerms.length > 0) { + const SEARCH_TOKEN = "_searchTerms_"; + let searchPostData = document.documentElement.getAttribute("searchEnginePostData"); + if (searchPostData) { + // Check if a post form already exists. If so, remove it. + const POST_FORM_NAME = "searchFormPost"; + let form = document.forms[POST_FORM_NAME]; + if (form) { + form.parentNode.removeChild(form); + } + + // Create a new post form. + form = document.body.appendChild(document.createElement("form")); + form.setAttribute("name", POST_FORM_NAME); + // Set the URL to submit the form to. + form.setAttribute("action", searchURL.replace(SEARCH_TOKEN, searchTerms)); + form.setAttribute("method", "post"); + + // Create new <input type=hidden> elements for search param. + searchPostData = searchPostData.split("&"); + for (let postVar of searchPostData) { + let [name, value] = postVar.split("="); + if (value == SEARCH_TOKEN) { + value = searchTerms; + } + let input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", name); + input.setAttribute("value", value); + form.appendChild(input); + } + // Submit the form. + form.submit(); + } else { + searchURL = searchURL.replace(SEARCH_TOKEN, encodeURIComponent(searchTerms)); + window.location.href = searchURL; + } + } + + aEvent.preventDefault(); +} + + +function setupSearchEngine() { + let searchText = document.getElementById("searchText"); + let searchEngineName = document.documentElement.getAttribute("searchEngineName"); + let searchEngineInfo = SEARCH_ENGINES[searchEngineName]; + let logoElt = document.getElementById("searchEngineLogo"); + + // Add search engine logo. + if (searchEngineInfo && searchEngineInfo.image) { + logoElt.parentNode.hidden = false; + logoElt.src = searchEngineInfo.image; + logoElt.alt = searchEngineName; + searchText.placeholder = ""; + } else { + logoElt.parentNode.hidden = false; + logoElt.src = SEARCH_ENGINES['generic'].image; + searchText.placeholder = searchEngineName; + } +} diff --git a/browser/components/newtab/sites.js b/browser/components/newtab/sites.js new file mode 100644 index 000000000..5da301c0c --- /dev/null +++ b/browser/components/newtab/sites.js @@ -0,0 +1,337 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +const THUMBNAIL_PLACEHOLDER_ENABLED = + Services.prefs.getBoolPref("browser.newtabpage.thumbnailPlaceholder"); + +/** + * This class represents a site that is contained in a cell and can be pinned, + * moved around or deleted. + */ +function Site(aNode, aLink) { + this._node = aNode; + this._node._newtabSite = this; + + this._link = aLink; + + this._render(); + this._addEventHandlers(); +} + +Site.prototype = { + /** + * The site's DOM node. + */ + get node() { return this._node; }, + + /** + * The site's link. + */ + get link() { return this._link; }, + + /** + * The url of the site's link. + */ + get url() { return this.link.url; }, + + /** + * The title of the site's link. + */ + get title() { return this.link.title || this.link.url; }, + + /** + * The site's parent cell. + */ + get cell() { + let parentNode = this.node.parentNode; + return parentNode && parentNode._newtabCell; + }, + + /** + * Pins the site on its current or a given index. + * @param aIndex The pinned index (optional). + * @return true if link changed type after pin + */ + pin: function(aIndex) { + if (typeof aIndex == "undefined") + aIndex = this.cell.index; + + this._updateAttributes(true); + let changed = gPinnedLinks.pin(this._link, aIndex); + if (changed) { + // render site again + this._render(); + } + return changed; + }, + + /** + * Unpins the site and calls the given callback when done. + */ + unpin: function() { + if (this.isPinned()) { + this._updateAttributes(false); + gPinnedLinks.unpin(this._link); + gUpdater.updateGrid(); + } + }, + + /** + * Checks whether this site is pinned. + * @return Whether this site is pinned. + */ + isPinned: function() { + return gPinnedLinks.isPinned(this._link); + }, + + /** + * Blocks the site (removes it from the grid) and calls the given callback + * when done. + */ + block: function() { + if (!gBlockedLinks.isBlocked(this._link)) { + gUndoDialog.show(this); + gBlockedLinks.block(this._link); + gUpdater.updateGrid(); + } + }, + + /** + * Gets the DOM node specified by the given query selector. + * @param aSelector The query selector. + * @return The DOM node we found. + */ + _querySelector: function(aSelector) { + return this.node.querySelector(aSelector); + }, + + /** + * Updates attributes for all nodes which status depends on this site being + * pinned or unpinned. + * @param aPinned Whether this site is now pinned or unpinned. + */ + _updateAttributes: function(aPinned) { + let control = this._querySelector(".newtab-control-pin"); + + if (aPinned) { + this.node.setAttribute("pinned", true); + control.setAttribute("title", newTabString("unpin")); + } else { + this.node.removeAttribute("pinned"); + control.setAttribute("title", newTabString("pin")); + } + }, + + _newTabString: function(str, substrArr) { + let regExp = /%[0-9]\$S/g; + let matches; + while ((matches = regExp.exec(str))) { + let match = matches[0]; + let index = match.charAt(1); // Get the digit in the regExp. + str = str.replace(match, substrArr[index - 1]); + } + return str; + }, + + /** + * Checks for and modifies link at campaign end time + */ + _checkLinkEndTime: function() { + if (this.link.endTime && this.link.endTime < Date.now()) { + let oldUrl = this.url; + // chop off the path part from url + this.link.url = Services.io.newURI(this.url, null, null).resolve("/"); + // clear supplied images - this triggers thumbnail download for new url + delete this.link.imageURI; + // remove endTime to avoid further time checks + delete this.link.endTime; + gPinnedLinks.replace(oldUrl, this.link); + } + }, + + /** + * Renders the site's data (fills the HTML fragment). + */ + _render: function() { + // first check for end time, as it may modify the link + this._checkLinkEndTime(); + // setup display variables + let url = this.url; + let title = this.link.type == "history" ? this.link.baseDomain : + this.title; + let tooltip = (this.title == url ? this.title : this.title + "\n" + url); + + let link = this._querySelector(".newtab-link"); + link.setAttribute("title", tooltip); + link.setAttribute("href", url); + this.node.setAttribute("type", this.link.type); + + let titleNode = this._querySelector(".newtab-title"); + titleNode.textContent = title; + if (this.link.titleBgColor) { + titleNode.style.backgroundColor = this.link.titleBgColor; + } + + if (this.isPinned()) + this._updateAttributes(true); + // Capture the page if the thumbnail is missing, which will cause page.js + // to be notified and call our refreshThumbnail() method. + this.captureIfMissing(); + // but still display whatever thumbnail might be available now. + this.refreshThumbnail(); + }, + + /** + * Called when the site's tab becomes visible for the first time. + * Since the newtab may be preloaded long before it's displayed, + * check for changed conditions and re-render if needed + */ + onFirstVisible: function() { + if (this.link.endTime && this.link.endTime < Date.now()) { + // site needs to change landing url and background image + this._render(); + } + else { + this.captureIfMissing(); + } + }, + + /** + * Captures the site's thumbnail in the background, but only if there's no + * existing thumbnail and the page allows background captures. + */ + captureIfMissing: function() { + if (!document.hidden && !this.link.imageURI) { + BackgroundPageThumbs.captureIfMissing(this.url); + } + }, + + /** + * Refreshes the thumbnail for the site. + */ + refreshThumbnail: function() { + let link = this.link; + + let thumbnail = this._querySelector(".newtab-thumbnail.thumbnail"); + if (link.bgColor) { + thumbnail.style.backgroundColor = link.bgColor; + } + let uri = link.imageURI || PageThumbs.getThumbnailURL(this.url); + thumbnail.style.backgroundImage = 'url("' + uri + '")'; + + if (THUMBNAIL_PLACEHOLDER_ENABLED && + link.type == "history" && + link.baseDomain) { + let placeholder = this._querySelector(".newtab-thumbnail.placeholder"); + let charCodeSum = 0; + for (let c of link.baseDomain) { + charCodeSum += c.charCodeAt(0); + } + const COLORS = 16; + let hue = Math.round((charCodeSum % COLORS) / COLORS * 360); + placeholder.style.backgroundColor = "hsl(" + hue + ",80%,40%)"; + placeholder.textContent = link.baseDomain.substr(0,1).toUpperCase(); + } + }, + + _ignoreHoverEvents: function(element) { + element.addEventListener("mouseover", () => { + this.cell.node.setAttribute("ignorehover", "true"); + }); + element.addEventListener("mouseout", () => { + this.cell.node.removeAttribute("ignorehover"); + }); + }, + + /** + * Adds event handlers for the site and its buttons. + */ + _addEventHandlers: function() { + // Register drag-and-drop event handlers. + this._node.addEventListener("dragstart", this, false); + this._node.addEventListener("dragend", this, false); + this._node.addEventListener("mouseover", this, false); + }, + + /** + * Speculatively opens a connection to the current site. + */ + _speculativeConnect: function() { + let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect); + let uri = Services.io.newURI(this.url, null, null); + try { + // This can throw for certain internal URLs, when they wind up in + // about:newtab. Be sure not to propagate the error. + sc.speculativeConnect(uri, null); + } catch (e) {} + }, + + _toggleLegalText: function(buttonClass, explanationTextClass) { + let button = this._querySelector(buttonClass); + if (button.hasAttribute("active")) { + let explain = this._querySelector(explanationTextClass); + explain.parentNode.removeChild(explain); + + button.removeAttribute("active"); + } + }, + + /** + * Handles site click events. + */ + onClick: function(aEvent) { + let action; + let pinned = this.isPinned(); + let tileIndex = this.cell.index; + let {button, target} = aEvent; + + // Handle tile/thumbnail link click + if (target.classList.contains("newtab-link") || + target.parentElement.classList.contains("newtab-link")) { + // Record for primary and middle clicks + if (button == 0 || button == 1) { + action = "click"; + } + } + // Only handle primary clicks for the remaining targets + else if (button == 0) { + aEvent.preventDefault(); + if (target.classList.contains("newtab-control-block")) { + this.block(); + action = "block"; + } + else if (pinned && target.classList.contains("newtab-control-pin")) { + this.unpin(); + action = "unpin"; + } + else if (!pinned && target.classList.contains("newtab-control-pin")) { + if (this.pin()) { + // link has changed - update rest of the pages + gAllPages.update(gPage); + } + action = "pin"; + } + } + }, + + /** + * Handles all site events. + */ + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "mouseover": + this._node.removeEventListener("mouseover", this, false); + this._speculativeConnect(); + break; + case "dragstart": + gDrag.start(this, aEvent); + break; + case "dragend": + gDrag.end(this, aEvent); + break; + } + } +}; diff --git a/browser/components/newtab/transformations.js b/browser/components/newtab/transformations.js new file mode 100644 index 000000000..6dd63b1c0 --- /dev/null +++ b/browser/components/newtab/transformations.js @@ -0,0 +1,270 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton allows to transform the grid by repositioning a site's node + * in the DOM and by showing or hiding the node. It additionally provides + * convenience methods to work with a site's DOM node. + */ +var gTransformation = { + /** + * Returns the width of the left and top border of a cell. We need to take it + * into account when measuring and comparing site and cell positions. + */ + get _cellBorderWidths() { + let cstyle = window.getComputedStyle(gGrid.cells[0].node, null); + let widths = { + left: parseInt(cstyle.getPropertyValue("border-left-width")), + top: parseInt(cstyle.getPropertyValue("border-top-width")) + }; + + // Cache this value, overwrite the getter. + Object.defineProperty(this, "_cellBorderWidths", + {value: widths, enumerable: true}); + + return widths; + }, + + /** + * Gets a DOM node's position. + * @param aNode The DOM node. + * @return A Rect instance with the position. + */ + getNodePosition: function(aNode) { + let {left, top, width, height} = aNode.getBoundingClientRect(); + return new Rect(left + scrollX, top + scrollY, width, height); + }, + + /** + * Fades a given node from zero to full opacity. + * @param aNode The node to fade. + * @param aCallback The callback to call when finished. + */ + fadeNodeIn: function(aNode, aCallback) { + this._setNodeOpacity(aNode, 1, function() { + // Clear the style property. + aNode.style.opacity = ""; + + if (aCallback) + aCallback(); + }); + }, + + /** + * Fades a given node from full to zero opacity. + * @param aNode The node to fade. + * @param aCallback The callback to call when finished. + */ + fadeNodeOut: function(aNode, aCallback) { + this._setNodeOpacity(aNode, 0, aCallback); + }, + + /** + * Fades a given site from zero to full opacity. + * @param aSite The site to fade. + * @param aCallback The callback to call when finished. + */ + showSite: function(aSite, aCallback) { + this.fadeNodeIn(aSite.node, aCallback); + }, + + /** + * Fades a given site from full to zero opacity. + * @param aSite The site to fade. + * @param aCallback The callback to call when finished. + */ + hideSite: function(aSite, aCallback) { + this.fadeNodeOut(aSite.node, aCallback); + }, + + /** + * Allows to set a site's position. + * @param aSite The site to re-position. + * @param aPosition The desired position for the given site. + */ + setSitePosition: function(aSite, aPosition) { + let style = aSite.node.style; + let {top, left} = aPosition; + + style.top = top + "px"; + style.left = left + "px"; + }, + + /** + * Freezes a site in its current position by positioning it absolute. + * @param aSite The site to freeze. + */ + freezeSitePosition: function(aSite) { + if (this._isFrozen(aSite)) + return; + + let style = aSite.node.style; + let comp = getComputedStyle(aSite.node, null); + style.width = comp.getPropertyValue("width"); + style.height = comp.getPropertyValue("height"); + + aSite.node.setAttribute("frozen", "true"); + this.setSitePosition(aSite, this.getNodePosition(aSite.node)); + }, + + /** + * Unfreezes a site by removing its absolute positioning. + * @param aSite The site to unfreeze. + */ + unfreezeSitePosition: function(aSite) { + if (!this._isFrozen(aSite)) + return; + + let style = aSite.node.style; + style.left = style.top = style.width = style.height = ""; + aSite.node.removeAttribute("frozen"); + }, + + /** + * Slides the given site to the target node's position. + * @param aSite The site to move. + * @param aTarget The slide target. + * @param aOptions Set of options (see below). + * unfreeze - unfreeze the site after sliding + * callback - the callback to call when finished + */ + slideSiteTo: function(aSite, aTarget, aOptions) { + let currentPosition = this.getNodePosition(aSite.node); + let targetPosition = this.getNodePosition(aTarget.node) + let callback = aOptions && aOptions.callback; + + let self = this; + + function finish() { + if (aOptions && aOptions.unfreeze) + self.unfreezeSitePosition(aSite); + + if (callback) + callback(); + } + + // We need to take the width of a cell's border into account. + targetPosition.left += this._cellBorderWidths.left; + targetPosition.top += this._cellBorderWidths.top; + + // Nothing to do here if the positions already match. + if (currentPosition.left == targetPosition.left && + currentPosition.top == targetPosition.top) { + finish(); + } else { + this.setSitePosition(aSite, targetPosition); + this._whenTransitionEnded(aSite.node, ["left", "top"], finish); + } + }, + + /** + * Rearranges a given array of sites and moves them to their new positions or + * fades in/out new/removed sites. + * @param aSites An array of sites to rearrange. + * @param aOptions Set of options (see below). + * unfreeze - unfreeze the site after rearranging + * callback - the callback to call when finished + */ + rearrangeSites: function(aSites, aOptions) { + let batch = []; + let cells = gGrid.cells; + let callback = aOptions && aOptions.callback; + let unfreeze = aOptions && aOptions.unfreeze; + + aSites.forEach(function(aSite, aIndex) { + // Do not re-arrange empty cells or the dragged site. + if (!aSite || aSite == gDrag.draggedSite) + return; + + batch.push(new Promise(resolve => { + if (!cells[aIndex]) { + // The site disappeared from the grid, hide it. + this.hideSite(aSite, resolve); + } else if (this._getNodeOpacity(aSite.node) != 1) { + // The site disappeared before but is now back, show it. + this.showSite(aSite, resolve); + } else { + // The site's position has changed, move it around. + this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: resolve}); + } + })); + }, this); + + if (callback) { + Promise.all(batch).then(callback); + } + }, + + /** + * Listens for the 'transitionend' event on a given node and calls the given + * callback. + * @param aNode The node that is transitioned. + * @param aProperties The properties we'll wait to be transitioned. + * @param aCallback The callback to call when finished. + */ + _whenTransitionEnded: + function(aNode, aProperties, aCallback) { + + let props = new Set(aProperties); + aNode.addEventListener("transitionend", function onEnd(e) { + if (props.has(e.propertyName)) { + aNode.removeEventListener("transitionend", onEnd); + aCallback(); + } + }); + }, + + /** + * Gets a given node's opacity value. + * @param aNode The node to get the opacity value from. + * @return The node's opacity value. + */ + _getNodeOpacity: function(aNode) { + let cstyle = window.getComputedStyle(aNode, null); + return cstyle.getPropertyValue("opacity"); + }, + + /** + * Sets a given node's opacity. + * @param aNode The node to set the opacity value for. + * @param aOpacity The opacity value to set. + * @param aCallback The callback to call when finished. + */ + _setNodeOpacity: + function(aNode, aOpacity, aCallback) { + + if (this._getNodeOpacity(aNode) == aOpacity) { + if (aCallback) + aCallback(); + } else { + if (aCallback) { + this._whenTransitionEnded(aNode, ["opacity"], aCallback); + } + + aNode.style.opacity = aOpacity; + } + }, + + /** + * Moves a site to the cell with the given index. + * @param aSite The site to move. + * @param aIndex The target cell's index. + * @param aOptions Options that are directly passed to slideSiteTo(). + */ + _moveSite: function(aSite, aIndex, aOptions) { + this.freezeSitePosition(aSite); + this.slideSiteTo(aSite, gGrid.cells[aIndex], aOptions); + }, + + /** + * Checks whether a site is currently frozen. + * @param aSite The site to check. + * @return Whether the given site is frozen. + */ + _isFrozen: function(aSite) { + return aSite.node.hasAttribute("frozen"); + } +}; diff --git a/browser/components/newtab/undo.js b/browser/components/newtab/undo.js new file mode 100644 index 000000000..9abcabf0f --- /dev/null +++ b/browser/components/newtab/undo.js @@ -0,0 +1,116 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * Dialog allowing to undo the removal of single site or to completely restore + * the grid's original state. + */ +var gUndoDialog = { + /** + * The undo dialog's timeout in miliseconds. + */ + HIDE_TIMEOUT_MS: 15000, + + /** + * Contains undo information. + */ + _undoData: null, + + /** + * Initializes the undo dialog. + */ + init: function() { + this._undoContainer = document.getElementById("newtab-undo-container"); + this._undoContainer.addEventListener("click", this, false); + this._undoButton = document.getElementById("newtab-undo-button"); + this._undoCloseButton = document.getElementById("newtab-undo-close-button"); + this._undoRestoreButton = document.getElementById("newtab-undo-restore-button"); + }, + + /** + * Shows the undo dialog. + * @param aSite The site that just got removed. + */ + show: function(aSite) { + if (this._undoData) + clearTimeout(this._undoData.timeout); + + this._undoData = { + index: aSite.cell.index, + wasPinned: aSite.isPinned(), + blockedLink: aSite.link, + timeout: setTimeout(this.hide.bind(this), this.HIDE_TIMEOUT_MS) + }; + + this._undoContainer.removeAttribute("undo-disabled"); + this._undoButton.removeAttribute("tabindex"); + this._undoCloseButton.removeAttribute("tabindex"); + this._undoRestoreButton.removeAttribute("tabindex"); + }, + + /** + * Hides the undo dialog. + */ + hide: function() { + if (!this._undoData) + return; + + clearTimeout(this._undoData.timeout); + this._undoData = null; + this._undoContainer.setAttribute("undo-disabled", "true"); + this._undoButton.setAttribute("tabindex", "-1"); + this._undoCloseButton.setAttribute("tabindex", "-1"); + this._undoRestoreButton.setAttribute("tabindex", "-1"); + }, + + /** + * The undo dialog event handler. + * @param aEvent The event to handle. + */ + handleEvent: function(aEvent) { + switch (aEvent.target.id) { + case "newtab-undo-button": + this._undo(); + break; + case "newtab-undo-restore-button": + this._undoAll(); + break; + case "newtab-undo-close-button": + this.hide(); + break; + } + }, + + /** + * Undo the last blocked site. + */ + _undo: function() { + if (!this._undoData) + return; + + let {index, wasPinned, blockedLink} = this._undoData; + gBlockedLinks.unblock(blockedLink); + + if (wasPinned) { + gPinnedLinks.pin(blockedLink, index); + } + + gUpdater.updateGrid(); + this.hide(); + }, + + /** + * Undo all blocked sites. + */ + _undoAll: function() { + NewTabUtils.undoAll(function() { + gUpdater.updateGrid(); + this.hide(); + }.bind(this)); + } +}; + +gUndoDialog.init(); diff --git a/browser/components/newtab/updater.js b/browser/components/newtab/updater.js new file mode 100644 index 000000000..e1c03e029 --- /dev/null +++ b/browser/components/newtab/updater.js @@ -0,0 +1,177 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton provides functionality to update the current grid to a new + * set of pinned and blocked sites. It adds, moves and removes sites. + */ +var gUpdater = { + /** + * Updates the current grid according to its pinned and blocked sites. + * This removes old, moves existing and creates new sites to fill gaps. + * @param aCallback The callback to call when finished. + */ + updateGrid: function(aCallback) { + let links = gLinks.getLinks().slice(0, gGrid.cells.length); + + // Find all sites that remain in the grid. + let sites = this._findRemainingSites(links); + + // Remove sites that are no longer in the grid. + this._removeLegacySites(sites, () => { + // Freeze all site positions so that we can move their DOM nodes around + // without any visual impact. + this._freezeSitePositions(sites); + + // Move the sites' DOM nodes to their new position in the DOM. This will + // have no visual effect as all the sites have been frozen and will + // remain in their current position. + this._moveSiteNodes(sites); + + // Now it's time to animate the sites actually moving to their new + // positions. + this._rearrangeSites(sites, () => { + // Try to fill empty cells and finish. + this._fillEmptyCells(links, aCallback); + + // Update other pages that might be open to keep them synced. + gAllPages.update(gPage); + }); + }); + }, + + /** + * Takes an array of links and tries to correlate them to sites contained in + * the current grid. If no corresponding site can be found (i.e. the link is + * new and a site will be created) then just set it to null. + * @param aLinks The array of links to find sites for. + * @return Array of sites mapped to the given links (can contain null values). + */ + _findRemainingSites: function(aLinks) { + let map = {}; + + // Create a map to easily retrieve the site for a given URL. + gGrid.sites.forEach(function(aSite) { + if (aSite) + map[aSite.url] = aSite; + }); + + // Map each link to its corresponding site, if any. + return aLinks.map(function(aLink) { + return aLink && (aLink.url in map) && map[aLink.url]; + }); + }, + + /** + * Freezes the given sites' positions. + * @param aSites The array of sites to freeze. + */ + _freezeSitePositions: function(aSites) { + aSites.forEach(function(aSite) { + if (aSite) + gTransformation.freezeSitePosition(aSite); + }); + }, + + /** + * Moves the given sites' DOM nodes to their new positions. + * @param aSites The array of sites to move. + */ + _moveSiteNodes: function(aSites) { + let cells = gGrid.cells; + + // Truncate the given array of sites to not have more sites than cells. + // This can happen when the user drags a bookmark (or any other new kind + // of link) onto the grid. + let sites = aSites.slice(0, cells.length); + + sites.forEach(function(aSite, aIndex) { + let cell = cells[aIndex]; + let cellSite = cell.site; + + // The site's position didn't change. + if (!aSite || cellSite != aSite) { + let cellNode = cell.node; + + // Empty the cell if necessary. + if (cellSite) + cellNode.removeChild(cellSite.node); + + // Put the new site in place, if any. + if (aSite) + cellNode.appendChild(aSite.node); + } + }, this); + }, + + /** + * Rearranges the given sites and slides them to their new positions. + * @param aSites The array of sites to re-arrange. + * @param aCallback The callback to call when finished. + */ + _rearrangeSites: function(aSites, aCallback) { + let options = {callback: aCallback, unfreeze: true}; + gTransformation.rearrangeSites(aSites, options); + }, + + /** + * Removes all sites from the grid that are not in the given links array or + * exceed the grid. + * @param aSites The array of sites remaining in the grid. + * @param aCallback The callback to call when finished. + */ + _removeLegacySites: function(aSites, aCallback) { + let batch = []; + + // Delete sites that were removed from the grid. + gGrid.sites.forEach(function(aSite) { + // The site must be valid and not in the current grid. + if (!aSite || aSites.indexOf(aSite) != -1) + return; + + batch.push(new Promise(resolve => { + // Fade out the to-be-removed site. + gTransformation.hideSite(aSite, function() { + let node = aSite.node; + + // Remove the site from the DOM. + node.parentNode.removeChild(node); + resolve(); + }); + })); + }); + + Promise.all(batch).then(aCallback); + }, + + /** + * Tries to fill empty cells with new links if available. + * @param aLinks The array of links. + * @param aCallback The callback to call when finished. + */ + _fillEmptyCells: function(aLinks, aCallback) { + let {cells, sites} = gGrid; + + // Find empty cells and fill them. + Promise.all(sites.map((aSite, aIndex) => { + if (aSite || !aLinks[aIndex]) + return null; + + return new Promise(resolve => { + // Create the new site and fade it in. + let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]); + + // Set the site's initial opacity to zero. + site.node.style.opacity = 0; + + // Flush all style changes for the dynamically inserted site to make + // the fade-in transition work. + window.getComputedStyle(site.node).opacity; + gTransformation.showSite(site, resolve); + }); + })).then(aCallback).catch(console.exception); + } +}; diff --git a/browser/components/nsAboutRedirector.js b/browser/components/nsAboutRedirector.js new file mode 100644 index 000000000..5142526e3 --- /dev/null +++ b/browser/components/nsAboutRedirector.js @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// See: netwerk/protocol/about/nsIAboutModule.idl +const URI_SAFE_FOR_UNTRUSTED_CONTENT = Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT; +const ALLOW_SCRIPT = Ci.nsIAboutModule.ALLOW_SCRIPT; +const HIDE_FROM_ABOUTABOUT = Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT; +const MAKE_LINKABLE = Ci.nsIAboutModule.MAKE_LINKABLE; + +function AboutRedirector() {} +AboutRedirector.prototype = { + classDescription: "Browser about: Redirector", + classID: Components.ID("{8cc51368-6aa0-43e8-b762-bde9b9fd828c}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]), + + // Each entry in the map has the key as the part after the "about:" and the + // value as a record with url and flags entries. Note that each addition here + // should be coupled with a corresponding addition in BrowserComponents.manifest. + _redirMap: { + "certerror": { + url: "chrome://browser/content/certerror/aboutCertError.xhtml", + flags: (URI_SAFE_FOR_UNTRUSTED_CONTENT | ALLOW_SCRIPT | HIDE_FROM_ABOUTABOUT) + }, + "downloads": { + url: "chrome://browser/content/downloads/contentAreaDownloadsView.xul", + flags: ALLOW_SCRIPT + }, + "feeds": { + url: "chrome://browser/content/feeds/subscribe.xhtml", + flags: (URI_SAFE_FOR_UNTRUSTED_CONTENT | ALLOW_SCRIPT | HIDE_FROM_ABOUTABOUT) + }, + "home": { + url: "chrome://browser/content/abouthome/aboutHome.xhtml", + flags: (URI_SAFE_FOR_UNTRUSTED_CONTENT | MAKE_LINKABLE | ALLOW_SCRIPT) + }, + "newtab": { + url: "chrome://browser/content/newtab/newTab.xhtml", + flags: ALLOW_SCRIPT + }, + "palemoon": { + url: "chrome://browser/content/palemoon.xhtml", + flags: (URI_SAFE_FOR_UNTRUSTED_CONTENT | HIDE_FROM_ABOUTABOUT) + }, + "permissions": { + url: "chrome://browser/content/permissions/aboutPermissions.xul", + flags: ALLOW_SCRIPT + }, + "privatebrowsing": { + url: "chrome://browser/content/aboutPrivateBrowsing.xhtml", + flags: ALLOW_SCRIPT + }, + "rights": { + url: "chrome://global/content/aboutRights.xhtml", + flags: (URI_SAFE_FOR_UNTRUSTED_CONTENT | MAKE_LINKABLE | ALLOW_SCRIPT) + }, + "sessionrestore": { + url: "chrome://browser/content/aboutSessionRestore.xhtml", + flags: ALLOW_SCRIPT + }, + }, + + /** + * Gets the module name from the given URI. + */ + _getModuleName: function(aURI) { + // Strip out the first ? or #, and anything following it + let name = (/[^?#]+/.exec(aURI.path))[0]; + return name.toLowerCase(); + }, + + getURIFlags: function(aURI) { + let name = this._getModuleName(aURI); + if (!(name in this._redirMap)) { + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + return this._redirMap[name].flags; + }, + + newChannel: function(aURI, aLoadInfo) { + let name = this._getModuleName(aURI); + if (!(name in this._redirMap)) { + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + + let newURI = Services.io.newURI(this._redirMap[name].url, null, null); + let channel = Services.io.newChannelFromURIWithLoadInfo(newURI, aLoadInfo); + channel.originalURI = aURI; + + if (this._redirMap[name].flags & Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT) { + let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(aURI); + channel.owner = principal; + } + + return channel; + } +}; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([AboutRedirector]); diff --git a/browser/components/nsBrowserContentHandler.js b/browser/components/nsBrowserContentHandler.js new file mode 100644 index 000000000..62cd343a9 --- /dev/null +++ b/browser/components/nsBrowserContentHandler.js @@ -0,0 +1,803 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); + +const nsISupports = Components.interfaces.nsISupports; + +const nsIBrowserDOMWindow = Components.interfaces.nsIBrowserDOMWindow; +const nsIBrowserHandler = Components.interfaces.nsIBrowserHandler; +const nsIBrowserHistory = Components.interfaces.nsIBrowserHistory; +const nsIChannel = Components.interfaces.nsIChannel; +const nsICommandLine = Components.interfaces.nsICommandLine; +const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler; +const nsIContentHandler = Components.interfaces.nsIContentHandler; +const nsIDocShellTreeItem = Components.interfaces.nsIDocShellTreeItem; +const nsIDOMChromeWindow = Components.interfaces.nsIDOMChromeWindow; +const nsIDOMWindow = Components.interfaces.nsIDOMWindow; +const nsIFileURL = Components.interfaces.nsIFileURL; +const nsIInterfaceRequestor = Components.interfaces.nsIInterfaceRequestor; +const nsINetUtil = Components.interfaces.nsINetUtil; +const nsIPrefBranch = Components.interfaces.nsIPrefBranch; +const nsIPrefLocalizedString = Components.interfaces.nsIPrefLocalizedString; +const nsISupportsString = Components.interfaces.nsISupportsString; +const nsIURIFixup = Components.interfaces.nsIURIFixup; +const nsIWebNavigation = Components.interfaces.nsIWebNavigation; +const nsIWindowMediator = Components.interfaces.nsIWindowMediator; +const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher; +const nsIWebNavigationInfo = Components.interfaces.nsIWebNavigationInfo; +const nsIBrowserSearchService = Components.interfaces.nsIBrowserSearchService; +const nsICommandLineValidator = Components.interfaces.nsICommandLineValidator; + +const NS_BINDING_ABORTED = Components.results.NS_BINDING_ABORTED; +const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001; +const NS_ERROR_ABORT = Components.results.NS_ERROR_ABORT; + +const URI_INHERITS_SECURITY_CONTEXT = Components.interfaces.nsIHttpProtocolHandler + .URI_INHERITS_SECURITY_CONTEXT; + +function shouldLoadURI(aURI) { + if (aURI && !aURI.schemeIs("chrome")) { + return true; + } + + dump("*** Preventing external load of chrome: URI into browser window\n"); + dump(" Use -chrome <uri> instead\n"); + return false; +} + +function resolveURIInternal(aCmdLine, aArgument) { + var uri = aCmdLine.resolveURI(aArgument); + var urifixup = Components.classes["@mozilla.org/docshell/urifixup;1"] + .getService(nsIURIFixup); + + if (!(uri instanceof nsIFileURL)) { + return urifixup.createFixupURI(aArgument, + urifixup.FIXUP_FLAG_FIX_SCHEME_TYPOS); + } + + try { + if (uri.file.exists()) { + return uri; + } + } catch(e) { + Components.utils.reportError(e); + } + + // We have interpreted the argument as a relative file URI, but the file + // doesn't exist. Try URI fixup heuristics: see bug 290782. + + try { + uri = urifixup.createFixupURI(aArgument, 0); + } catch(e) { + Components.utils.reportError(e); + } + + return uri; +} + +var gFirstWindow = false; + +const OVERRIDE_NONE = 0; +const OVERRIDE_NEW_PROFILE = 1; +const OVERRIDE_NEW_MSTONE = 2; +const OVERRIDE_NEW_BUILD_ID = 3; +/** + * Determines whether a home page override is needed. + * Returns: + * OVERRIDE_NEW_PROFILE if this is the first run with a new profile. + * OVERRIDE_NEW_MSTONE if this is the first run with a build with a different + * Goanna milestone (i.e. right after an upgrade). + * OVERRIDE_NEW_BUILD_ID if this is the first run with a new build ID of the + * same Goanna milestone (i.e. after a nightly upgrade). + * OVERRIDE_NONE otherwise. + */ +function needHomepageOverride(prefb) { + var savedmstone = prefb.getCharPref("browser.startup.homepage_override.mstone", ""); + + if (savedmstone == "ignore") { + return OVERRIDE_NONE; + } + + var mstone = Services.appinfo.greVersion; + + var savedBuildID = prefb.getCharPref("browser.startup.homepage_override.buildID", ""); + + var buildID = Services.appinfo.platformBuildID; + + if (mstone != savedmstone) { + // Bug 462254. Previous releases had a default pref to suppress the EULA + // agreement if the platform's installer had already shown one. Now with + // about:rights we've removed the EULA stuff and default pref, but we need + // a way to make existing profiles retain the default that we removed. + if (savedmstone) { + prefb.setBoolPref("browser.rights.3.shown", true); + } + + prefb.setCharPref("browser.startup.homepage_override.mstone", mstone); + prefb.setCharPref("browser.startup.homepage_override.buildID", buildID); + return (savedmstone ? OVERRIDE_NEW_MSTONE : OVERRIDE_NEW_PROFILE); + } + + if (buildID != savedBuildID) { + prefb.setCharPref("browser.startup.homepage_override.buildID", buildID); + return OVERRIDE_NEW_BUILD_ID; + } + + return OVERRIDE_NONE; +} + +/** + * Gets the override page for the first run after the application has been + * updated. + * @param defaultOverridePage + * The default override page. + * @return The override page. + */ +function getPostUpdateOverridePage(defaultOverridePage) { + var um = Components.classes["@mozilla.org/updates/update-manager;1"] + .getService(Components.interfaces.nsIUpdateManager); + try { + // If the updates.xml file is deleted then getUpdateAt will throw. + var update = um.getUpdateAt(0) + .QueryInterface(Components.interfaces.nsIPropertyBag); + } catch(e) { + // This should never happen. + Components.utils.reportError("Unable to find update: " + e); + return defaultOverridePage; + } + + let actions = update.getProperty("actions"); + // When the update doesn't specify actions fallback to the original behavior + // of displaying the default override page. + if (!actions) { + return defaultOverridePage; + } + + // The existence of silent or the non-existence of showURL in the actions both + // mean that an override page should not be displayed. + if (actions.indexOf("silent") != -1 || actions.indexOf("showURL") == -1) { + return ""; + } + + return update.getProperty("openURL") || defaultOverridePage; +} + +// Flag used to indicate that the arguments to openWindow can be passed directly. +const NO_EXTERNAL_URIS = 1; + +function openWindow(parent, url, target, features, args, noExternalArgs) { + var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(nsIWindowWatcher); + + if (noExternalArgs == NO_EXTERNAL_URIS) { + // Just pass in the defaultArgs directly + var argstring; + if (args) { + argstring = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(nsISupportsString); + argstring.data = args; + } + + return wwatch.openWindow(parent, url, target, features, argstring); + } + + // Pass an array to avoid the browser "|"-splitting behavior. + var argArray = Components.classes["@mozilla.org/supports-array;1"] + .createInstance(Components.interfaces.nsISupportsArray); + + // add args to the arguments array + var stringArgs = null; + if (args instanceof Array) { + // array + stringArgs = args; + } else if (args) { + // string + stringArgs = [args]; + } + + if (stringArgs) { + // put the URIs into argArray + var uriArray = Components.classes["@mozilla.org/supports-array;1"] + .createInstance(Components.interfaces.nsISupportsArray); + stringArgs.forEach(function(uri) { + var sstring = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(nsISupportsString); + sstring.data = uri; + uriArray.AppendElement(sstring); + }); + argArray.AppendElement(uriArray); + } else { + argArray.AppendElement(null); + } + + // Pass these as null to ensure that we always trigger the "single URL" + // behavior in browser.js's gBrowserInit.onLoad (which handles the window + // arguments) + argArray.AppendElement(null); // charset + argArray.AppendElement(null); // referer + argArray.AppendElement(null); // postData + argArray.AppendElement(null); // allowThirdPartyFixup + + return wwatch.openWindow(parent, url, target, features, argArray); +} + +function openPreferences() { + var features = "chrome,titlebar,toolbar,centerscreen,dialog=no"; + var url = "chrome://browser/content/preferences/preferences.xul"; + + var win = getMostRecentWindow("Browser:Preferences"); + if (win) { + win.focus(); + } else { + openWindow(null, url, "_blank", features); + } +} + +function getMostRecentWindow(aType) { + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(nsIWindowMediator); + return wm.getMostRecentWindow(aType); +} + +function doSearch(searchTerm, cmdLine) { + var ss = Components.classes["@mozilla.org/browser/search-service;1"] + .getService(nsIBrowserSearchService); + + var submission = ss.defaultEngine.getSubmission(searchTerm); + + // fill our nsISupportsArray with uri-as-wstring, null, null, postData + var sa = Components.classes["@mozilla.org/supports-array;1"] + .createInstance(Components.interfaces.nsISupportsArray); + + var wuri = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + wuri.data = submission.uri.spec; + + sa.AppendElement(wuri); + sa.AppendElement(null); + sa.AppendElement(null); + sa.AppendElement(submission.postData); + + // XXXbsmedberg: use handURIToExistingBrowser to obey tabbed-browsing + // preferences, but need nsIBrowserDOMWindow extensions + + var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(nsIWindowWatcher); + + return wwatch.openWindow(null, gBrowserContentHandler.chromeURL, + "_blank", + "chrome,dialog=no,all" + + gBrowserContentHandler.getFeatures(cmdLine), + sa); +} + +function nsBrowserContentHandler() {} +nsBrowserContentHandler.prototype = { + classID: Components.ID("{5d0ce354-df01-421a-83fb-7ead0990c24e}"), + + _xpcom_factory: { + createInstance: function(outer, iid) { + if (outer) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return gBrowserContentHandler.QueryInterface(iid); + } + }, + + /* helper functions */ + + mChromeURL: null, + + get chromeURL() { + if (this.mChromeURL) { + return this.mChromeURL; + } + + var prefb = Components.classes["@mozilla.org/preferences-service;1"] + .getService(nsIPrefBranch); + this.mChromeURL = prefb.getCharPref("browser.chromeURL"); + + return this.mChromeURL; + }, + + /* nsISupports */ + QueryInterface: XPCOMUtils.generateQI([ nsICommandLineHandler, + nsIBrowserHandler, + nsIContentHandler, + nsICommandLineValidator ]), + + /* nsICommandLineHandler */ + handle: function(cmdLine) { + if (cmdLine.handleFlag("browser", false)) { + // Passing defaultArgs, so use NO_EXTERNAL_URIS + openWindow(null, this.chromeURL, "_blank", + "chrome,dialog=no,all" + this.getFeatures(cmdLine), + this.defaultArgs, NO_EXTERNAL_URIS); + cmdLine.preventDefault = true; + } + + try { + var remoteCommand = cmdLine.handleFlagWithParam("remote", true); + } catch(e) { + throw NS_ERROR_ABORT; + } + + if (remoteCommand != null) { + try { + var a = /^\s*(\w+)\(([^\)]*)\)\s*$/.exec(remoteCommand); + var remoteVerb; + if (a) { + remoteVerb = a[1].toLowerCase(); + var remoteParams = []; + var sepIndex = a[2].lastIndexOf(","); + if (sepIndex == -1) { + remoteParams[0] = a[2]; + } else { + remoteParams[0] = a[2].substring(0, sepIndex); + remoteParams[1] = a[2].substring(sepIndex + 1); + } + } + + switch (remoteVerb) { + case "openurl": + case "openfile": + // openURL(<url>) + // openURL(<url>,new-window) + // openURL(<url>,new-tab) + + // First param is the URL, second param (if present) is the "target" + // (tab, window) + var url = remoteParams[0]; + var target = nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW; + if (remoteParams[1]) { + var targetParam = remoteParams[1].toLowerCase() + .replace(/^\s*|\s*$/g, ""); + if (targetParam == "new-tab") { + target = nsIBrowserDOMWindow.OPEN_NEWTAB; + } else if (targetParam == "new-window") { + target = nsIBrowserDOMWindow.OPEN_NEWWINDOW; + } else { + // The "target" param isn't one of our supported values, so + // assume it's part of a URL that contains commas. + url += "," + remoteParams[1]; + } + } + + var uri = resolveURIInternal(cmdLine, url); + handURIToExistingBrowser(uri, target, cmdLine); + break; + + case "xfedocommand": + // xfeDoCommand(openBrowser) + if (remoteParams[0].toLowerCase() != "openbrowser") { + throw NS_ERROR_ABORT; + } + + // Passing defaultArgs, so use NO_EXTERNAL_URIS + openWindow(null, this.chromeURL, "_blank", + "chrome,dialog=no,all" + this.getFeatures(cmdLine), + this.defaultArgs, NO_EXTERNAL_URIS); + break; + + default: + // Somebody sent us a remote command we don't know how to process: + // just abort. + throw "Unknown remote command."; + } + + cmdLine.preventDefault = true; + } catch (e) { + Components.utils.reportError(e); + // If we had a -remote flag but failed to process it, throw + // NS_ERROR_ABORT so that the xremote code knows to return a failure + // back to the handling code. + throw NS_ERROR_ABORT; + } + } + + var uriparam; + try { + while ((uriparam = cmdLine.handleFlagWithParam("new-window", false))) { + var uri = resolveURIInternal(cmdLine, uriparam); + if (!shouldLoadURI(uri)) { + continue; + } + openWindow(null, this.chromeURL, "_blank", + "chrome,dialog=no,all" + this.getFeatures(cmdLine), + uri.spec); + cmdLine.preventDefault = true; + } + } catch(e) { + Components.utils.reportError(e); + } + + try { + while ((uriparam = cmdLine.handleFlagWithParam("new-tab", false))) { + var uri = resolveURIInternal(cmdLine, uriparam); + handURIToExistingBrowser(uri, nsIBrowserDOMWindow.OPEN_NEWTAB, cmdLine); + cmdLine.preventDefault = true; + } + } catch(e) { + Components.utils.reportError(e); + } + + var chromeParam = cmdLine.handleFlagWithParam("chrome", false); + if (chromeParam) { + + // Handle the old preference dialog URL separately (bug 285416) + if (chromeParam == "chrome://browser/content/pref/pref.xul") { + openPreferences(); + cmdLine.preventDefault = true; + } else { + try { + // only load URIs which do not inherit chrome privs + var features = "chrome,dialog=no,all" + this.getFeatures(cmdLine); + var uri = resolveURIInternal(cmdLine, chromeParam); + var netutil = Components.classes["@mozilla.org/network/util;1"] + .getService(nsINetUtil); + if (!netutil.URIChainHasFlags(uri, URI_INHERITS_SECURITY_CONTEXT)) { + openWindow(null, uri.spec, "_blank", features); + cmdLine.preventDefault = true; + } + } catch(e) { + Components.utils.reportError(e); + } + } + } + if (cmdLine.handleFlag("preferences", false)) { + openPreferences(); + cmdLine.preventDefault = true; + } + if (cmdLine.handleFlag("silent", false)) { + cmdLine.preventDefault = true; + } + + try { + var privateWindowParam = cmdLine.handleFlagWithParam("private-window", false); + if (privateWindowParam) { + let resolvedURI = resolveURIInternal(cmdLine, privateWindowParam); + handURIToExistingBrowser(resolvedURI, nsIBrowserDOMWindow.OPEN_NEWTAB, cmdLine, true); + cmdLine.preventDefault = true; + } + } catch(e) { + if (e.result != Components.results.NS_ERROR_INVALID_ARG) { + throw e; + } + // NS_ERROR_INVALID_ARG is thrown when flag exists, but has no param. + if (cmdLine.handleFlag("private-window", false)) { + openWindow(null, this.chromeURL, "_blank", + "chrome,dialog=no,private,all" + this.getFeatures(cmdLine), + "about:privatebrowsing"); + cmdLine.preventDefault = true; + } + } + + var searchParam = cmdLine.handleFlagWithParam("search", false); + if (searchParam) { + doSearch(searchParam, cmdLine); + cmdLine.preventDefault = true; + } + + // The global PB Service consumes this flag, so only eat it in per-window + // PB builds. + if (cmdLine.handleFlag("private", false)) { + PrivateBrowsingUtils.enterTemporaryAutoStartMode(); + } + + var fileParam = cmdLine.handleFlagWithParam("file", false); + if (fileParam) { + var file = cmdLine.resolveFile(fileParam); + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var uri = ios.newFileURI(file); + openWindow(null, this.chromeURL, "_blank", + "chrome,dialog=no,all" + this.getFeatures(cmdLine), + uri.spec); + cmdLine.preventDefault = true; + } + +#ifdef XP_WIN + // Handle "? searchterm" for Windows Vista start menu integration + for (var i = cmdLine.length - 1; i >= 0; --i) { + var param = cmdLine.getArgument(i); + if (param.match(/^\? /)) { + cmdLine.removeArguments(i, i); + cmdLine.preventDefault = true; + + searchParam = param.substr(2); + doSearch(searchParam, cmdLine); + } + } +#endif + }, + + helpInfo: " --browser Open a browser window.\n" + + " --new-window <url> Open <url> in a new window.\n" + + " --new-tab <url> Open <url> in a new tab.\n" + + " --private-window <url> Open <url> in a new private window.\n" + + " --preferences Open Preferences dialog.\n" + + " --search <term> Search <term> with your default search engine.\n", + + /* nsIBrowserHandler */ + + get defaultArgs() { + var prefb = Components.classes["@mozilla.org/preferences-service;1"] + .getService(nsIPrefBranch); + + if (!gFirstWindow) { + gFirstWindow = true; + if (PrivateBrowsingUtils.isInTemporaryAutoStartMode) { + return "about:privatebrowsing"; + } + } + + var overridePage = ""; + var haveUpdateSession = false; + try { + // Read the old value of homepage_override.mstone before + // needHomepageOverride updates it, so that we can later add it to the + // URL if we do end up showing an overridePage. This makes it possible + // to have the overridePage's content vary depending on the version we're + // upgrading from. + let old_mstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone", "unknown"); + let override = needHomepageOverride(prefb); + if (override != OVERRIDE_NONE) { + switch (override) { + case OVERRIDE_NEW_PROFILE: + // New profile. + overridePage = Services.urlFormatter.formatURLPref("startup.homepage_welcome_url"); + break; + case OVERRIDE_NEW_MSTONE: + // Check whether we have a session to restore. If we do, we assume + // that this is an "update" session. + var ss = Components.classes["@mozilla.org/browser/sessionstartup;1"] + .getService(Components.interfaces.nsISessionStartup); + haveUpdateSession = ss.doRestore(); + overridePage = Services.urlFormatter.formatURLPref("startup.homepage_override_url"); + if (prefb.prefHasUserValue("app.update.postupdate")) { + overridePage = getPostUpdateOverridePage(overridePage); + } + + overridePage = overridePage.replace("%OLD_VERSION%", old_mstone); + break; + } + } + } catch(ex) {} + + // formatURLPref might return "about:blank" if getting the pref fails + if (overridePage == "about:blank") { + overridePage = ""; + } + + var startPage = ""; + try { + var choice = prefb.getIntPref("browser.startup.page"); + if (choice == 1 || choice == 3) { + startPage = this.startPage; + } + } catch(e) { + Components.utils.reportError(e); + } + + // Only show the startPage if we're not restoring an update session. + if (overridePage && startPage && !haveUpdateSession) { + return overridePage + "|" + startPage; + } + + return overridePage || startPage || "about:logopage"; + }, + + get startPage() { + var uri = Services.prefs.getComplexValue("browser.startup.homepage", + nsIPrefLocalizedString).data; + if (!uri) { + Services.prefs.clearUserPref("browser.startup.homepage"); + uri = Services.prefs.getComplexValue("browser.startup.homepage", + nsIPrefLocalizedString).data; + } + return uri; + }, + + mFeatures: null, + + getFeatures: function(cmdLine) { + if (this.mFeatures === null) { + this.mFeatures = ""; + + try { + var width = cmdLine.handleFlagWithParam("width", false); + var height = cmdLine.handleFlagWithParam("height", false); + + if (width) { + this.mFeatures += ",width=" + width; + } + if (height) { + this.mFeatures += ",height=" + height; + } + } catch(e) {} + + // The global PB Service consumes this flag, so only eat it in per-window + // PB builds. + if (PrivateBrowsingUtils.isInTemporaryAutoStartMode) { + this.mFeatures = ",private"; + } + } + + return this.mFeatures; + }, + + /* nsIContentHandler */ + + handleContent: function(contentType, context, request) { + try { + var webNavInfo = Components.classes["@mozilla.org/webnavigation-info;1"] + .getService(nsIWebNavigationInfo); + if (!webNavInfo.isTypeSupported(contentType, null)) { + throw NS_ERROR_WONT_HANDLE_CONTENT; + } + } catch(e) { + throw NS_ERROR_WONT_HANDLE_CONTENT; + } + + request.QueryInterface(nsIChannel); + handURIToExistingBrowser(request.URI, nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, null); + request.cancel(NS_BINDING_ABORTED); + }, + + /* nsICommandLineValidator */ + validate: function(cmdLine) { + // Other handlers may use osint so only handle the osint flag if the url + // flag is also present and the command line is valid. + var osintFlagIdx = cmdLine.findFlag("osint", false); + var urlFlagIdx = cmdLine.findFlag("url", false); + if (urlFlagIdx > -1 && (osintFlagIdx > -1 || + cmdLine.state == nsICommandLine.STATE_REMOTE_EXPLICIT)) { + var urlParam = cmdLine.getArgument(urlFlagIdx + 1); + if (cmdLine.length != urlFlagIdx + 2 || /firefoxurl:/.test(urlParam)) { + throw NS_ERROR_ABORT; + } + cmdLine.handleFlag("osint", false) + } + }, +}; + +var gBrowserContentHandler = new nsBrowserContentHandler(); + +function handURIToExistingBrowser(uri, location, cmdLine, forcePrivate) { + if (!shouldLoadURI(uri)) { + return; + } + + // Unless using a private window is forced, open external links in private + // windows only if we're in perma-private mode. + var allowPrivate = forcePrivate || PrivateBrowsingUtils.permanentPrivateBrowsing; + var navWin = RecentWindow.getMostRecentBrowserWindow({private: allowPrivate}); + if (!navWin) { + // if we couldn't load it in an existing window, open a new one + var features = "chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine); + if (forcePrivate) { + features += ",private"; + } + openWindow(null, gBrowserContentHandler.chromeURL, "_blank", features, uri.spec); + return; + } + + var navNav = navWin.QueryInterface(nsIInterfaceRequestor) + .getInterface(nsIWebNavigation); + var rootItem = navNav.QueryInterface(nsIDocShellTreeItem).rootTreeItem; + var rootWin = rootItem.QueryInterface(nsIInterfaceRequestor) + .getInterface(nsIDOMWindow); + var bwin = rootWin.QueryInterface(nsIDOMChromeWindow).browserDOMWindow; + bwin.openURI(uri, null, location, + nsIBrowserDOMWindow.OPEN_EXTERNAL); +} + +function nsDefaultCommandLineHandler() {} +nsDefaultCommandLineHandler.prototype = { + classID: Components.ID("{47cd0651-b1be-4a0f-b5c4-10e5a573ef71}"), + + /* nsISupports */ + QueryInterface: function(iid) { + if (!iid.equals(nsISupports) && + !iid.equals(nsICommandLineHandler)) + throw Components.results.NS_ERROR_NO_INTERFACE; + + return this; + }, + +#ifdef XP_WIN + _haveProfile: false, +#endif + + /* nsICommandLineHandler */ + handle: function(cmdLine) { + var urilist = []; + +#ifdef XP_WIN + // If we don't have a profile selected yet (e.g. the Profile Manager is + // displayed) we will crash if we open an url and then select a profile. To + // prevent this handle all url command line flags and set the command line's + // preventDefault to true to prevent the display of the ui. The initial + // command line will be retained when nsAppRunner calls LaunchChild though + // urls launched after the initial launch will be lost. + if (!this._haveProfile) { + try { + // This will throw when a profile has not been selected. + var fl = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); + var dir = fl.get("ProfD", Components.interfaces.nsILocalFile); + this._haveProfile = true; + } catch(e) { + while ((ar = cmdLine.handleFlagWithParam("url", false))) {} + cmdLine.preventDefault = true; + } + } +#endif + + try { + var ar; + while ((ar = cmdLine.handleFlagWithParam("url", false))) { + var uri = resolveURIInternal(cmdLine, ar); + urilist.push(uri); + } + } catch(e) { + Components.utils.reportError(e); + } + + let count = cmdLine.length; + + for (let i = 0; i < count; ++i) { + var curarg = cmdLine.getArgument(i); + if (curarg.match(/^-/)) { + Components.utils.reportError("Warning: unrecognized command line flag " + curarg + "\n"); + // To emulate the pre-nsICommandLine behavior, we ignore + // the argument after an unrecognized flag. + ++i; + } else { + try { + urilist.push(resolveURIInternal(cmdLine, curarg)); + } catch(e) { + Components.utils.reportError("Error opening URI '" + curarg + "' from the command line: " + e + "\n"); + } + } + } + + if (urilist.length) { + if (cmdLine.state != nsICommandLine.STATE_INITIAL_LAUNCH && + urilist.length == 1) { + // Try to find an existing window and load our URI into the + // current tab, new tab, or new window as prefs determine. + try { + handURIToExistingBrowser(urilist[0], nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, cmdLine); + return; + } catch(e) {} + } + + var URLlist = urilist.filter(shouldLoadURI).map(function(u) u.spec); + if (URLlist.length) { + openWindow(null, gBrowserContentHandler.chromeURL, "_blank", + "chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine), + URLlist); + } + + } else if (!cmdLine.preventDefault) { + // Passing defaultArgs, so use NO_EXTERNAL_URIS + openWindow(null, gBrowserContentHandler.chromeURL, "_blank", + "chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine), + gBrowserContentHandler.defaultArgs, NO_EXTERNAL_URIS); + } + }, + + helpInfo : "", +}; + +var components = [nsBrowserContentHandler, nsDefaultCommandLineHandler]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js new file mode 100644 index 000000000..505fdbc50 --- /dev/null +++ b/browser/components/nsBrowserGlue.js @@ -0,0 +1,2171 @@ +# -*- 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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// Define Lazy Service Getters +XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", + "@mozilla.org/alerts-service;1", "nsIAlertsService"); + +// Define Lazy Module Getters +[ + ["AddonManager", "resource://gre/modules/AddonManager.jsm"], + ["NetUtil", "resource://gre/modules/NetUtil.jsm"], + ["UserAgentOverrides", "resource://gre/modules/UserAgentOverrides.jsm"], + ["FileUtils", "resource://gre/modules/FileUtils.jsm"], + ["PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"], + ["BookmarkHTMLUtils", "resource://gre/modules/BookmarkHTMLUtils.jsm"], + ["BookmarkJSONUtils", "resource://gre/modules/BookmarkJSONUtils.jsm"], + ["PageThumbs", "resource://gre/modules/PageThumbs.jsm"], + ["NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"], + ["BrowserNewTabPreloader", "resource:///modules/BrowserNewTabPreloader.jsm"], + ["PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"], + ["RecentWindow", "resource:///modules/RecentWindow.jsm"], + ["Task", "resource://gre/modules/Task.jsm"], + ["PlacesBackups", "resource://gre/modules/PlacesBackups.jsm"], + ["OS", "resource://gre/modules/osfile.jsm"], + ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"], + ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"], + ["AutoCompletePopup", "resource:///modules/private/AutoCompletePopup.jsm"], + ["DateTimePickerHelper", "resource://gre/modules/DateTimePickerHelper.jsm"], + ["ShellService", "resource:///modules/ShellService.jsm"], +].forEach(([name, resource]) => XPCOMUtils.defineLazyModuleGetter(this, name, resource)); + +// Define Lazy Getters + +XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() { + return Services.strings.createBundle('chrome://branding/locale/brand.properties'); +}); + +XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() { + return Services.strings.createBundle('chrome://browser/locale/browser.properties'); +}); + +// We try to backup bookmarks at idle times, to avoid doing that at shutdown. +// Number of idle seconds before trying to backup bookmarks. 15 minutes. +const BOOKMARKS_BACKUP_IDLE_TIME = 15 * 60; +// Minimum interval in milliseconds between backups. +const BOOKMARKS_BACKUP_INTERVAL = 86400 * 1000; +// Maximum number of backups to create. Old ones will be purged. +const BOOKMARKS_BACKUP_MAX_BACKUPS = 10; + +// Factory object +const BrowserGlueServiceFactory = { + _instance: null, + createInstance: function(outer, iid) { + if (outer != null) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this._instance == null ? + this._instance = new BrowserGlue() : + this._instance; + } +}; + +// Constructor + +function BrowserGlue() { + XPCOMUtils.defineLazyServiceGetter(this, "_idleService", + "@mozilla.org/widget/idleservice;1", + "nsIIdleService"); + + XPCOMUtils.defineLazyGetter(this, "_distributionCustomizer", function() { + Cu.import("resource:///modules/distribution.js"); + return new DistributionCustomizer(); + }); + + XPCOMUtils.defineLazyGetter(this, "_sanitizer", + function() { + let sanitizerScope = {}; + Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", sanitizerScope); + return sanitizerScope.Sanitizer; + }); + + this._init(); +} + +# We don't have the concept of zero-window sessions on any supported OS-es +# and therefore have to observe the browser-lastwindow-close-* topics. +#define OBSERVE_LASTWINDOW_CLOSE_TOPICS 1 + +BrowserGlue.prototype = { + _saveSession: false, + _isIdleObserver: false, + _isPlacesInitObserver: false, + _isPlacesLockedObserver: false, + _isPlacesShutdownObserver: false, + _isPlacesDatabaseLocked: false, + _migrationImportsDefaultBookmarks: false, + + _setPrefToSaveSession: function(aForce) { + if (!this._saveSession && !aForce) { + return; + } + + Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true); + + // This method can be called via [NSApplication terminate:] on Mac, which + // ends up causing prefs not to be flushed to disk, so we need to do that + // explicitly here. See bug 497652. + Services.prefs.savePrefFile(null); + }, + +#ifdef MOZ_SERVICES_SYNC + _setSyncAutoconnectDelay: function() { + // Assume that a non-zero value for services.sync.autoconnectDelay should override + if (Services.prefs.prefHasUserValue("services.sync.autoconnectDelay")) { + let prefDelay = Services.prefs.getIntPref("services.sync.autoconnectDelay"); + + if (prefDelay > 0) { + return; + } + } + + // delays are in seconds + const MAX_DELAY = 300; + let delay = 3; + let browserEnum = Services.wm.getEnumerator("navigator:browser"); + while (browserEnum.hasMoreElements()) { + delay += browserEnum.getNext().gBrowser.tabs.length; + } + delay = delay <= MAX_DELAY ? delay : MAX_DELAY; + + Cu.import("resource://services-sync/main.js"); + Weave.Service.scheduler.delayedAutoConnect(delay); + }, +#endif + + // nsIObserver implementation + observe: function(subject, topic, data) { + switch (topic) { + case "notifications-open-settings": + this._openPermissions(subject); + break; + case "prefservice:after-app-defaults": + this._onAppDefaults(); + break; + case "final-ui-startup": + this._finalUIStartup(); + break; + case "browser-delayed-startup-finished": + this._onFirstWindowLoaded(); + Services.obs.removeObserver(this, "browser-delayed-startup-finished"); + break; + case "sessionstore-windows-restored": + this._onWindowsRestored(); + break; + case "browser:purge-session-history": + // reset the console service's error buffer + Services.console.logStringMessage(null); // clear the console (in case it's open) + Services.console.reset(); + break; + case "quit-application-requested": + this._onQuitRequest(subject, data); + break; + case "quit-application-granted": + // This pref must be set here because SessionStore will use its value + // on quit-application. + this._setPrefToSaveSession(); + try { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]. + getService(Ci.nsIAppStartup); + appStartup.trackStartupCrashEnd(); + } catch(e) { + Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e); + } + DateTimePickerHelper.uninit(); + break; +#ifdef OBSERVE_LASTWINDOW_CLOSE_TOPICS + case "browser-lastwindow-close-requested": + // The application is not actually quitting, but the last full browser + // window is about to be closed. + this._onQuitRequest(subject, "lastwindow"); + break; + case "browser-lastwindow-close-granted": + this._setPrefToSaveSession(); + break; +#endif +#ifdef MOZ_SERVICES_SYNC + case "weave:service:ready": + this._setSyncAutoconnectDelay(); + break; + case "weave:engine:clients:display-uri": + this._onDisplaySyncURI(subject); + break; +#endif + case "session-save": + this._setPrefToSaveSession(true); + subject.QueryInterface(Ci.nsISupportsPRBool); + subject.data = true; + break; + case "places-init-complete": + if (!this._migrationImportsDefaultBookmarks) { + this._initPlaces(false); + } + + Services.obs.removeObserver(this, "places-init-complete"); + this._isPlacesInitObserver = false; + // no longer needed, since history was initialized completely. + Services.obs.removeObserver(this, "places-database-locked"); + this._isPlacesLockedObserver = false; + break; + case "places-database-locked": + this._isPlacesDatabaseLocked = true; + // Stop observing, so further attempts to load history service + // will not show the prompt. + Services.obs.removeObserver(this, "places-database-locked"); + this._isPlacesLockedObserver = false; + break; + case "places-shutdown": + if (this._isPlacesShutdownObserver) { + Services.obs.removeObserver(this, "places-shutdown"); + this._isPlacesShutdownObserver = false; + } + // places-shutdown is fired when the profile is about to disappear. + this._onPlacesShutdown(); + break; + case "idle": + if (this._idleService.idleTime > BOOKMARKS_BACKUP_IDLE_TIME * 1000) { + this._backupBookmarks(); + } + break; + case "distribution-customization-complete": + Services.obs.removeObserver(this, "distribution-customization-complete"); + // Customization has finished, we don't need the customizer anymore. + delete this._distributionCustomizer; + break; + case "browser-glue-test": // used by tests + if (data == "post-update-notification") { + if (Services.prefs.prefHasUserValue("app.update.postupdate")) { + this._showUpdateNotification(); + } + } else if (data == "force-ui-migration") { + this._migrateUI(); + } else if (data == "force-distribution-customization") { + this._distributionCustomizer.applyPrefDefaults(); + this._distributionCustomizer.applyCustomizations(); + // To apply distribution bookmarks use "places-init-complete". + } else if (data == "force-places-init") { + this._initPlaces(false); + } + break; + case "initial-migration-will-import-default-bookmarks": + this._migrationImportsDefaultBookmarks = true; + break; + case "initial-migration-did-import-default-bookmarks": + this._initPlaces(true); + break; + case "handle-xul-text-link": + let linkHandled = subject.QueryInterface(Ci.nsISupportsPRBool); + if (!linkHandled.data) { + let win = this.getMostRecentBrowserWindow(); + if (win) { + data = JSON.parse(data); + win.openUILinkIn(data.href, "tab"); + linkHandled.data = true; + } + } + break; + case "profile-before-change": + this._onProfileShutdown(); + break; + case "profile-after-change": + this._onProfileAfterChange(); + this._promptForMasterPassword(); + break; + case "browser-search-engine-modified": + if (data != "engine-default" && data != "engine-current") { + break; + } + // Enforce that the search service's defaultEngine is always equal to + // its currentEngine. The search service will notify us any time either + // of them are changed (either by directly setting the relevant prefs, + // i.e. if add-ons try to change this directly, or if the + // nsIBrowserSearchService setters are called). + // No need to initialize the search service, since it's guaranteed to be + // initialized already when this notification fires. + let ss = Services.search; + if (ss.currentEngine.name == ss.defaultEngine.name) { + return; + } + if (data == "engine-current") { + ss.defaultEngine = ss.currentEngine; + } else { + ss.currentEngine = ss.defaultEngine; + } + break; + case "browser-search-service": + if (data != "init-complete") { + return; + } + Services.obs.removeObserver(this, "browser-search-service"); + this._syncSearchEngines(); + break; + } + }, + + _syncSearchEngines: function() { + // Only do this if the search service is already initialized. This function + // gets called in finalUIStartup and from a browser-search-service observer, + // to catch both cases (search service initialization occurring before and + // after final-ui-startup) + if (Services.search.isInitialized) { + Services.search.defaultEngine = Services.search.currentEngine; + } + }, + + // initialization (called on application startup) + _init: function() { + let os = Services.obs; + os.addObserver(this, "notifications-open-settings", false); + os.addObserver(this, "prefservice:after-app-defaults", false); + os.addObserver(this, "final-ui-startup", false); + os.addObserver(this, "browser-delayed-startup-finished", false); + os.addObserver(this, "sessionstore-windows-restored", false); + os.addObserver(this, "browser:purge-session-history", false); + os.addObserver(this, "quit-application-requested", false); + os.addObserver(this, "quit-application-granted", false); +#ifdef OBSERVE_LASTWINDOW_CLOSE_TOPICS + os.addObserver(this, "browser-lastwindow-close-requested", false); + os.addObserver(this, "browser-lastwindow-close-granted", false); +#endif +#ifdef MOZ_SERVICES_SYNC + os.addObserver(this, "weave:service:ready", false); + os.addObserver(this, "weave:engine:clients:display-uri", false); +#endif + os.addObserver(this, "session-save", false); + os.addObserver(this, "places-init-complete", false); + this._isPlacesInitObserver = true; + os.addObserver(this, "places-database-locked", false); + this._isPlacesLockedObserver = true; + os.addObserver(this, "distribution-customization-complete", false); + os.addObserver(this, "places-shutdown", false); + this._isPlacesShutdownObserver = true; + os.addObserver(this, "handle-xul-text-link", false); + os.addObserver(this, "profile-before-change", false); + os.addObserver(this, "profile-after-change", false); + os.addObserver(this, "browser-search-engine-modified", false); + os.addObserver(this, "browser-search-service", false); + }, + + // cleanup (called on application shutdown) + _dispose: function() { + let os = Services.obs; + os.removeObserver(this, "notifications-open-settings"); + os.removeObserver(this, "prefservice:after-app-defaults"); + os.removeObserver(this, "final-ui-startup"); + os.removeObserver(this, "sessionstore-windows-restored"); + os.removeObserver(this, "browser:purge-session-history"); + os.removeObserver(this, "quit-application-requested"); + os.removeObserver(this, "quit-application-granted"); +#ifdef OBSERVE_LASTWINDOW_CLOSE_TOPICS + os.removeObserver(this, "browser-lastwindow-close-requested"); + os.removeObserver(this, "browser-lastwindow-close-granted"); +#endif +#ifdef MOZ_SERVICES_SYNC + os.removeObserver(this, "weave:service:ready"); + os.removeObserver(this, "weave:engine:clients:display-uri"); +#endif + os.removeObserver(this, "session-save"); + if (this._isIdleObserver) { + this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME); + } + if (this._isPlacesInitObserver) { + os.removeObserver(this, "places-init-complete"); + } + if (this._isPlacesLockedObserver) { + os.removeObserver(this, "places-database-locked"); + } + if (this._isPlacesShutdownObserver) { + os.removeObserver(this, "places-shutdown"); + } + os.removeObserver(this, "handle-xul-text-link"); + os.removeObserver(this, "profile-before-change"); + os.removeObserver(this, "profile-after-change"); + os.removeObserver(this, "browser-search-engine-modified"); + try { + os.removeObserver(this, "browser-search-service"); + } catch(ex) { + // may have already been removed by the observer + } + }, + + // profile is available + _onProfileAfterChange: function() { + this._copyDefaultProfileFiles(); + }, + + _promptForMasterPassword: function() { + if (!Services.prefs.getBoolPref("signon.startup.prompt", false)) + return; + + // Try to avoid the multiple master password prompts on startup scenario + // by prompting for the master password upfront. + let token = Components.classes["@mozilla.org/security/pk11tokendb;1"] + .getService(Components.interfaces.nsIPK11TokenDB) + .getInternalKeyToken(); + + // Only log in to the internal token if it is already initialized, + // otherwise we get a "Change Master Password" dialog. + try { + if (!token.needsUserInit) + token.login(false); + } catch (ex) { + // If user cancels an exception is expected. + } + }, + + _onAppDefaults: function() { + // apply distribution customizations (prefs) + // other customizations are applied in _finalUIStartup() + this._distributionCustomizer.applyPrefDefaults(); + }, + + // runs on startup, before the first command line handler is invoked + // (i.e. before the first window is opened) + _finalUIStartup: function() { + this._sanitizer.onStartup(); + // check if we're in safe mode + if (Services.appinfo.inSafeMode) { + Services.ww.openWindow(null, "chrome://browser/content/safeMode.xul", + "_blank", "chrome,centerscreen,modal,resizable=no", null); + } + + // apply distribution customizations + // prefs are applied in _onAppDefaults() + this._distributionCustomizer.applyCustomizations(); + + // handle any UI migration + this._migrateUI(); + + this._setUpUserAgentOverrides(); + + this._syncSearchEngines(); + + PageThumbs.init(); + NewTabUtils.init(); + BrowserNewTabPreloader.init(); + FormValidationHandler.init(); + + AutoCompletePopup.init(); + + LoginManagerParent.init(); + + Services.obs.notifyObservers(null, "browser-ui-startup-complete", ""); + }, + + // Copies additional profile files from the default profile to the current profile. + // Only files not covered by the regular profile creation process. + // Currently only the userchrome examples. + _copyDefaultProfileFiles: function() { + // Copy default chrome example files if they do not exist in the current profile. + var profileDir = Services.dirsvc.get("ProfD", Components.interfaces.nsILocalFile); + profileDir.append("chrome"); + + // The chrome directory in the current/new profile already exists so no copying. + if (profileDir.exists()) + return; + + let defaultProfileDir = Services.dirsvc.get("DefRt", + Components.interfaces.nsIFile); + defaultProfileDir.append("profile"); + defaultProfileDir.append("chrome"); + + if (defaultProfileDir.exists() && defaultProfileDir.isDirectory()) { + try { + this._copyDir(defaultProfileDir, profileDir); + } catch (e) { + Components.utils.reportError(e); + } + } + }, + + // Simple copy function for copying complete aSource Directory to aDestiniation. + _copyDir: function(aSource, aDestination) + { + let enumerator = aSource.directoryEntries; + + while (enumerator.hasMoreElements()) { + let file = enumerator.getNext().QueryInterface(Components.interfaces.nsIFile); + + if (file.isDirectory()) { + let subdir = aDestination.clone(); + subdir.append(file.leafName); + + // Create the target directory. If it already exists continue copying files. + try { + subdir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY); + } catch (ex) { + if (ex.result != Components.results.NS_ERROR_FILE_ALREADY_EXISTS) + throw ex; + } + // Directory created. Now copy the files. + this._copyDir(file, subdir); + } else { + try { + file.copyTo(aDestination, null); + } catch (e) { + Components.utils.reportError(e); + } + } + } + }, + + _setUpUserAgentOverrides: function() { + UserAgentOverrides.init(); + + if (Services.prefs.getBoolPref("general.useragent.complexOverride.moodle")) { + UserAgentOverrides.addComplexOverride(function(aHttpChannel, aOriginalUA) { + let cookies; + try { + cookies = aHttpChannel.getRequestHeader("Cookie"); + } catch(e) { + // no cookie sent + } + if (cookies && cookies.indexOf("MoodleSession") > -1) { + return aOriginalUA.replace(/Goanna\/[^ ]*/, "Goanna/20100101"); + } + return null; + }); + } + }, + + _trackSlowStartup: function() { + if (Services.startup.interrupted || + Services.prefs.getBoolPref("browser.slowStartup.notificationDisabled")) { + return; + } + + let currentTime = Date.now() - Services.startup.getStartupInfo().process; + let averageTime = 0; + let samples = 0; + try { + averageTime = Services.prefs.getIntPref("browser.slowStartup.averageTime"); + samples = Services.prefs.getIntPref("browser.slowStartup.samples"); + } catch(e) {} + + averageTime = (averageTime * samples + currentTime) / ++samples; + + if (samples >= Services.prefs.getIntPref("browser.slowStartup.maxSamples")) { + if (averageTime > Services.prefs.getIntPref("browser.slowStartup.timeThreshold")) { + this._showSlowStartupNotification(); + } + averageTime = 0; + samples = 0; + } + + Services.prefs.setIntPref("browser.slowStartup.averageTime", averageTime); + Services.prefs.setIntPref("browser.slowStartup.samples", samples); + }, + + _showSlowStartupNotification: function() { + let win = this.getMostRecentBrowserWindow(); + if (!win) { + return; + } + + let productName = gBrandBundle.GetStringFromName("brandFullName"); + let message = win.gNavigatorBundle.getFormattedString("slowStartup.message", [productName]); + + let buttons = [ + { + label: win.gNavigatorBundle.getString("slowStartup.helpButton.label"), + accessKey: win.gNavigatorBundle.getString("slowStartup.helpButton.accesskey"), + callback: function() { + win.openUILinkIn(Services.prefs.getCharPref("browser.slowstartup.help.url"), "tab"); + } + }, + { + label: win.gNavigatorBundle.getString("slowStartup.disableNotificationButton.label"), + accessKey: win.gNavigatorBundle.getString("slowStartup.disableNotificationButton.accesskey"), + callback: function() { + Services.prefs.setBoolPref("browser.slowStartup.notificationDisabled", true); + } + } + ]; + + let nb = win.document.getElementById("global-notificationbox"); + nb.appendNotification(message, "slow-startup", + "chrome://browser/skin/slowStartup-16.png", + nb.PRIORITY_INFO_LOW, buttons); + }, + + // the first browser window has finished initializing + _onFirstWindowLoaded: function() { +#ifdef XP_WIN + // For Windows, initialize the jump list module. + const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; + if (WINTASKBAR_CONTRACTID in Cc && + Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) { + let temp = {}; + Cu.import("resource:///modules/WindowsJumpLists.jsm", temp); + temp.WinTaskbarJumpList.startup(); + } +#endif + + DateTimePickerHelper.init(); + + this._trackSlowStartup(); + }, + + /** + * Profile shutdown handler (contains profile cleanup routines). + * All components depending on Places should be shut down in + * _onPlacesShutdown() and not here. + */ + _onProfileShutdown: function() { + BrowserNewTabPreloader.uninit(); + UserAgentOverrides.uninit(); + FormValidationHandler.uninit(); + AutoCompletePopup.uninit(); + this._dispose(); + }, + + // All initial windows have opened. + _onWindowsRestored: function() { + // Show update notification, if needed. + if (Services.prefs.prefHasUserValue("app.update.postupdate")) { + this._showUpdateNotification(); + } + + // Load the "more info" page for a locked places.sqlite + // This property is set earlier by places-database-locked topic. + if (this._isPlacesDatabaseLocked) { + this._showPlacesLockedNotificationBox(); + } + + // For any add-ons that were installed disabled and can be enabled offer + // them to the user. + let changedIDs = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED); + if (changedIDs.length > 0) { + let win = this.getMostRecentBrowserWindow(); + AddonManager.getAddonsByIDs(changedIDs, function(aAddons) { + aAddons.forEach(function(aAddon) { + // If the add-on isn't user disabled or can't be enabled then skip it. + if (!aAddon.userDisabled || !(aAddon.permissions & AddonManager.PERM_CAN_ENABLE)) { + return; + } + + win.openUILinkIn("about:newaddon?id=" + aAddon.id, "tab"); + }) + }); + } + + // Perform default browser checking. + if (ShellService) { + let shouldCheck = ShellService.shouldCheckDefaultBrowser; + + const skipDefaultBrowserCheck = + Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheckOnFirstRun") && + Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheck"); + + const usePromptLimit = false; + let promptCount = + usePromptLimit ? Services.prefs.getIntPref("browser.shell.defaultBrowserCheckCount") : 0; + + let willRecoverSession = false; + try { + let ss = Cc["@mozilla.org/browser/sessionstartup;1"]. + getService(Ci.nsISessionStartup); + willRecoverSession = + (ss.sessionType == Ci.nsISessionStartup.RECOVER_SESSION); + } catch(ex) { + // never mind; suppose SessionStore is broken + } + + // startup check, check all assoc + let isDefault = false; + let isDefaultError = false; + try { + isDefault = ShellService.isDefaultBrowser(true, false); + } catch(ex) { + isDefaultError = true; + } + + if (isDefault) { + let now = (Math.floor(Date.now() / 1000)).toString(); + Services.prefs.setCharPref("browser.shell.mostRecentDateSetAsDefault", now); + } + + let willPrompt = shouldCheck && !isDefault && !willRecoverSession; + + // Skip the "Set Default Browser" check during first-run or after the + // browser has been run a few times. + if (willPrompt) { + Services.tm.mainThread.dispatch(function() { + var win = this.getMostRecentBrowserWindow(); + var brandBundle = win.document.getElementById("bundle_brand"); + var shellBundle = win.document.getElementById("bundle_shell"); + + var brandShortName = brandBundle.getString("brandShortName"); + var promptTitle = shellBundle.getString("setDefaultBrowserTitle"); + var promptMessage = shellBundle.getFormattedString("setDefaultBrowserMessage", + [brandShortName]); + var checkboxLabel = shellBundle.getFormattedString("setDefaultBrowserDontAsk", + [brandShortName]); + var checkEveryTime = { value: shouldCheck }; + var ps = Services.prompt; + var rv = ps.confirmEx(win, promptTitle, promptMessage, + ps.STD_YES_NO_BUTTONS, + null, null, null, checkboxLabel, checkEveryTime); + if (rv == 0) { + var claimAllTypes = true; +#ifdef XP_WIN + try { + // In Windows 8+, the UI for selecting default protocol is much + // nicer than the UI for setting file type associations. So we + // only show the protocol association screen on Windows 8. + // Windows 8 is version 6.2. + let version = Services.sysinfo.getProperty("version"); + claimAllTypes = (parseFloat(version) < 6.2); + } catch (ex) {} +#endif + ShellService.setDefaultBrowser(claimAllTypes, false); + } + ShellService.shouldCheckDefaultBrowser = checkEveryTime.value; + }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); + } + } + }, + + _onQuitRequest: function(aCancelQuit, aQuitType) { + // If user has already dismissed quit request, then do nothing + if ((aCancelQuit instanceof Ci.nsISupportsPRBool) && aCancelQuit.data) { + return; + } + + // There are several cases where we won't show a dialog here: + // 1. There is only 1 tab open in 1 window + // 2. The session will be restored at startup, indicated by + // browser.startup.page == 3 or browser.sessionstore.resume_session_once == true + // 3. browser.warnOnQuit == false + // 4. The browser is currently in Private Browsing mode + // 5. The browser will be restarted. + // + // Otherwise these are the conditions and the associated dialogs that will be shown: + // 1. aQuitType == "lastwindow" or "quit" and browser.showQuitWarning == true + // - The quit dialog will be shown + // 2. aQuitType == "lastwindow" && browser.tabs.warnOnClose == true + // - The "closing multiple tabs" dialog will be shown + // + // aQuitType == "lastwindow" is overloaded. "lastwindow" is used to indicate + // "the last window is closing but we're not quitting (a non-browser window is open)" + // and also "we're quitting by closing the last window". + + if (aQuitType == "restart") { + return; + } + + var windowcount = 0; + var pagecount = 0; + var browserEnum = Services.wm.getEnumerator("navigator:browser"); + let allWindowsPrivate = true; + while (browserEnum.hasMoreElements()) { + windowcount++; + + var browser = browserEnum.getNext(); + if (!PrivateBrowsingUtils.isWindowPrivate(browser)) { + allWindowsPrivate = false; + } + var tabbrowser = browser.document.getElementById("content"); + if (tabbrowser) { + pagecount += tabbrowser.browsers.length - tabbrowser._numPinnedTabs; + } + } + + this._saveSession = false; + if (pagecount < 2) { + return; + } + + if (!aQuitType) { + aQuitType = "quit"; + } + + // browser.warnOnQuit is a hidden global boolean to override all quit prompts + // browser.showQuitWarning specifically covers quitting + // browser.tabs.warnOnClose is the global "warn when closing multiple tabs" pref + + var sessionWillBeRestored = Services.prefs.getIntPref("browser.startup.page") == 3 || + Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); + if (sessionWillBeRestored || !Services.prefs.getBoolPref("browser.warnOnQuit")) { + return; + } + + let win = Services.wm.getMostRecentWindow("navigator:browser"); + + // On last window close or quit && showQuitWarning, we want to show the + // quit warning. + if (!Services.prefs.getBoolPref("browser.showQuitWarning")) { + if (aQuitType == "lastwindow") { + // If aQuitType is "lastwindow" and we aren't showing the quit warning, + // we should show the window closing warning instead. warnAboutClosing + // tabs checks browser.tabs.warnOnClose and returns if it's ok to close + // the window. It doesn't actually close the window. + aCancelQuit.data = + !win.gBrowser.warnAboutClosingTabs(win.gBrowser.closingTabsEnum.ALL); + } + return; + } + + let prompt = Services.prompt; + let quitBundle = Services.strings.createBundle("chrome://browser/locale/quitDialog.properties"); + + let appName = gBrandBundle.GetStringFromName("brandShortName"); + let quitDialogTitle = quitBundle.formatStringFromName("quitDialogTitle", + [appName], 1); + let neverAskText = quitBundle.GetStringFromName("neverAsk2"); + let neverAsk = {value: false}; + + let choice; + if (allWindowsPrivate) { + let text = quitBundle.formatStringFromName("messagePrivate", [appName], 1); + let flags = prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_0 + + prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_1 + + prompt.BUTTON_POS_0_DEFAULT; + choice = prompt.confirmEx(win, quitDialogTitle, text, flags, + quitBundle.GetStringFromName("quitTitle"), + quitBundle.GetStringFromName("cancelTitle"), + null, + neverAskText, neverAsk); + + // The order of the buttons differs between the prompt.confirmEx calls + // here so we need to fix this for proper handling below. + if (choice == 0) { + choice = 2; + } + } else { + let text = quitBundle.formatStringFromName( + windowcount == 1 ? "messageNoWindows" : "message", [appName], 1); + let flags = prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_0 + + prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_1 + + prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_2 + + prompt.BUTTON_POS_0_DEFAULT; + choice = prompt.confirmEx(win, quitDialogTitle, text, flags, + quitBundle.GetStringFromName("saveTitle"), + quitBundle.GetStringFromName("cancelTitle"), + quitBundle.GetStringFromName("quitTitle"), + neverAskText, neverAsk); + } + + switch (choice) { + case 2: // Quit + if (neverAsk.value) { + Services.prefs.setBoolPref("browser.showQuitWarning", false); + } + break; + case 1: // Cancel + aCancelQuit.QueryInterface(Ci.nsISupportsPRBool); + aCancelQuit.data = true; + break; + case 0: // Save & Quit + this._saveSession = true; + if (neverAsk.value) { + // always save state when shutting down + Services.prefs.setIntPref("browser.startup.page", 3); + } + break; + } + }, + + _showUpdateNotification: function() { + Services.prefs.clearUserPref("app.update.postupdate"); + + var um = Cc["@mozilla.org/updates/update-manager;1"]. + getService(Ci.nsIUpdateManager); + try { + // If the updates.xml file is deleted then getUpdateAt will throw. + var update = um.getUpdateAt(0).QueryInterface(Ci.nsIPropertyBag); + } catch(e) { + // This should never happen. + Cu.reportError("Unable to find update: " + e); + return; + } + + var actions = update.getProperty("actions"); + if (!actions || actions.indexOf("silent") != -1) { + return; + } + + var formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Ci.nsIURLFormatter); + var appName = gBrandBundle.GetStringFromName("brandShortName"); + + function getNotifyString(aPropData) { + var propValue = update.getProperty(aPropData.propName); + if (!propValue) { + if (aPropData.prefName) { + propValue = formatter.formatURLPref(aPropData.prefName); + } else if (aPropData.stringParams) { + propValue = gBrowserBundle.formatStringFromName(aPropData.stringName, + aPropData.stringParams, + aPropData.stringParams.length); + } else { + propValue = gBrowserBundle.GetStringFromName(aPropData.stringName); + } + } + return propValue; + } + + if (actions.indexOf("showNotification") != -1) { + let text = getNotifyString({propName: "notificationText", + stringName: "puNotifyText", + stringParams: [appName]}); + let url = getNotifyString({propName: "notificationURL", + prefName: "startup.homepage_override_url"}); + let label = getNotifyString({propName: "notificationButtonLabel", + stringName: "pu.notifyButton.label"}); + let key = getNotifyString({propName: "notificationButtonAccessKey", + stringName: "pu.notifyButton.accesskey"}); + + let win = this.getMostRecentBrowserWindow(); + let notifyBox = win.gBrowser.getNotificationBox(); + + let buttons = [ + { + label: label, + accessKey: key, + popup: null, + callback: function(aNotificationBar, aButton) { + win.openUILinkIn(url, "tab"); + } + } + ]; + + let notification = notifyBox.appendNotification(text, "post-update-notification", + null, notifyBox.PRIORITY_INFO_LOW, + buttons); + notification.persistence = -1; // Until user closes it + } + + if (actions.indexOf("showAlert") == -1) { + return; + } + + let title = getNotifyString({ propName: "alertTitle", + stringName: "puAlertTitle", + stringParams: [appName] }); + let text = getNotifyString({ propName: "alertText", + stringName: "puAlertText", + stringParams: [appName] }); + let url = getNotifyString({ propName: "alertURL", + prefName: "startup.homepage_override_url" }); + + var self = this; + function clickCallback(subject, topic, data) { + // This callback will be called twice but only once with this topic + if (topic != "alertclickcallback") { + return; + } + let win = self.getMostRecentBrowserWindow(); + win.openUILinkIn(data, "tab"); + } + + try { + // This will throw NS_ERROR_NOT_AVAILABLE if the notification cannot + // be displayed per the idl. + AlertsService.showAlertNotification(null, title, text, + true, url, clickCallback); + } catch(e) { + Cu.reportError(e); + } + }, + + /** + * Initialize Places + * - imports the bookmarks html file if bookmarks database is empty, try to + * restore bookmarks from a JSON/JSONLZ4 backup if the backend indicates + * that the database was corrupt. + * + * These prefs can be set up by the frontend: + * + * WARNING: setting these preferences to true will overwite existing bookmarks + * + * - browser.places.importBookmarksHTML + * Set to true will import the bookmarks.html file from the profile folder. + * - browser.places.smartBookmarksVersion + * Set during HTML import to indicate that Smart Bookmarks were created. + * Set to -1 to disable Smart Bookmarks creation. + * Set to 0 to restore current Smart Bookmarks. + * - browser.bookmarks.restore_default_bookmarks + * Set to true by safe-mode dialog to indicate we must restore default + * bookmarks. + */ + _initPlaces: function(aInitialMigrationPerformed) { + // 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). + // If the database is corrupt or has been newly created we should + // import bookmarks. + var dbStatus = PlacesUtils.history.databaseStatus; + var importBookmarks = !aInitialMigrationPerformed && + (dbStatus == PlacesUtils.history.DATABASE_STATUS_CREATE || + dbStatus == PlacesUtils.history.DATABASE_STATUS_CORRUPT); + + // Check if user or an extension has required to import bookmarks.html + var importBookmarksHTML = false; + try { + importBookmarksHTML = + Services.prefs.getBoolPref("browser.places.importBookmarksHTML"); + if (importBookmarksHTML) + importBookmarks = true; + } catch(ex) {} + + Task.spawn(function() { + // Check if Safe Mode or the user has required to restore bookmarks from + // default profile's bookmarks.html + var restoreDefaultBookmarks = false; + try { + restoreDefaultBookmarks = + Services.prefs.getBoolPref("browser.bookmarks.restore_default_bookmarks"); + if (restoreDefaultBookmarks) { + // Ensure that we already have a bookmarks backup for today. + yield this._backupBookmarks(); + importBookmarks = true; + } + } catch(ex) {} + + // If the user did not require to restore default bookmarks, or import + // from bookmarks.html, we will try to restore from JSON/JSONLZ4 + if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) { + // get latest JSON/JSONLZ4 backup + var bookmarksBackupFile = yield PlacesBackups.getMostRecentBackup(); + if (bookmarksBackupFile) { + // restore from JSON/JSONLZ4 backup + yield BookmarkJSONUtils.importFromFile(bookmarksBackupFile, true); + importBookmarks = false; + } else { + // We have created a new database but we don't have any backup available + importBookmarks = true; + if (yield OS.File.exists(BookmarkHTMLUtils.defaultPath)) { + // If bookmarks.html is available in current profile import it... + importBookmarksHTML = true; + } else { + // ...otherwise we will restore defaults + restoreDefaultBookmarks = true; + } + } + } + + // If bookmarks are not imported, then initialize smart bookmarks. This + // happens during a common startup. + // Otherwise, if any kind of import runs, smart bookmarks creation should be + // delayed till the import operations has finished. Not doing so would + // cause them to be overwritten by the newly imported bookmarks. + if (!importBookmarks) { + // Now apply distribution customized bookmarks. + // This should always run after Places initialization. + try { + this._distributionCustomizer.applyBookmarks(); + this.ensurePlacesDefaultQueriesInitialized(); + } catch(e) { + Cu.reportError(e); + } + } else { + // An import operation is about to run. + // Don't try to recreate smart bookmarks if autoExportHTML is true or + // smart bookmarks are disabled. + var autoExportHTML = Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML", false); + var smartBookmarksVersion = Services.prefs.getIntPref("browser.places.smartBookmarksVersion", 0); + if (!autoExportHTML && smartBookmarksVersion != -1) { + Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0); + } + + var bookmarksUrl = null; + if (restoreDefaultBookmarks) { + // User wants to restore bookmarks.html file from default profile folder + bookmarksUrl = "resource:///defaults/profile/bookmarks.html"; + } else if (yield OS.File.exists(BookmarkHTMLUtils.defaultPath)) { + bookmarksUrl = OS.Path.toFileURI(BookmarkHTMLUtils.defaultPath); + } + + if (bookmarksUrl) { + // Import from bookmarks.html file. + try { + BookmarkHTMLUtils.importFromURL(bookmarksUrl, true).then( + null, + function onFailure() { + Cu.reportError(new Error("Bookmarks.html file could be corrupt.")); + } + ).then( + function onComplete() { + try { + // Now apply distribution customized bookmarks. + // This should always run after Places initialization. + this._distributionCustomizer.applyBookmarks(); + // Ensure that smart bookmarks are created once the operation + // is complete. + this.ensurePlacesDefaultQueriesInitialized(); + } catch(e) { + Cu.reportError(e); + } + }.bind(this) + ); + } catch(e) { + Cu.reportError( + new Error("Bookmarks.html file could be corrupt." + "\n" + + e.message)); + } + } else { + Cu.reportError(new Error("Unable to find bookmarks.html file.")); + } + + // See #1083: + // "Delete all bookmarks except for backups" in Safe Mode doesn't work + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let observer = { + "observe": function() { + delete observer.timer; + // Reset preferences, so we won't try to import again at next run + if (importBookmarksHTML) { + Services.prefs.setBoolPref("browser.places.importBookmarksHTML", false); + } + if (restoreDefaultBookmarks) { + Services.prefs.setBoolPref("browser.bookmarks.restore_default_bookmarks", + false); + } + }, + "timer": timer, + }; + timer.init(observer, 100, Ci.nsITimer.TYPE_ONE_SHOT); + } + + // Initialize bookmark archiving on idle. + // Once a day, either on idle or shutdown, bookmarks are backed up. + if (!this._isIdleObserver) { + this._idleService.addIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME); + this._isIdleObserver = true; + } + + }.bind(this)).catch(ex => { + Cu.reportError(ex); + }).then(result => { + // NB: deliberately after the catch so that we always do this, even if + // we threw halfway through initializing in the Task above. + Services.obs.notifyObservers(null, "places-browser-init-complete", ""); + }); + }, + + /** + * Places shut-down tasks + * - back up bookmarks if needed. + * - export bookmarks as HTML, if so configured. + * - finalize components depending on Places. + */ + _onPlacesShutdown: function() { + this._sanitizer.onShutdown(); + PageThumbs.uninit(); + + if (this._isIdleObserver) { + this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME); + this._isIdleObserver = false; + } + + let waitingForBackupToComplete = true; + this._backupBookmarks().then( + function onSuccess() { + waitingForBackupToComplete = false; + }, + function onFailure() { + Cu.reportError("Unable to backup bookmarks."); + waitingForBackupToComplete = false; + } + ); + + // Backup bookmarks to bookmarks.html to support apps that depend + // on the legacy format. + let waitingForHTMLExportToComplete = false; + // If this fails to get the preference value, we don't export. + if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) { + // Exceptionally, since this is a non-default setting and HTML format is + // discouraged in favor of the JSON/JSONLZ4 backups, we spin the event + // loop on shutdown, to wait for the export to finish. We cannot safely + // spin the event loop on shutdown until we include a watchdog to prevent + // potential hangs (bug 518683). The asynchronous shutdown operations + // will then be handled by a shutdown service (bug 435058). + waitingForHTMLExportToComplete = true; + BookmarkHTMLUtils.exportToFile(BookmarkHTMLUtils.defaultPath).then( + function onSuccess() { + waitingForHTMLExportToComplete = false; + }, + function onFailure() { + Cu.reportError("Unable to auto export html."); + waitingForHTMLExportToComplete = false; + } + ); + } + + // The events loop should spin at least once because waitingForBackupToComplete + // is true before checking whether backup should be made. + let thread = Services.tm.currentThread; + while (waitingForBackupToComplete || waitingForHTMLExportToComplete) { + thread.processNextEvent(true); + } + }, + + /** + * Backup bookmarks. + */ + _backupBookmarks: function() { + return Task.spawn(function() { + let lastBackupFile = yield PlacesBackups.getMostRecentBackup(); + // Should backup bookmarks if there are no backups or the maximum + // interval between backups elapsed. + if (!lastBackupFile || + new Date() - PlacesBackups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_INTERVAL) { + let maxBackups = BOOKMARKS_BACKUP_MAX_BACKUPS; + try { + maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups"); + } catch(ex) { + // Use default. + } + + // Don't force creation. + yield PlacesBackups.create(maxBackups); + } + }); + }, + + /** + * Show the notificationBox for a locked places database. + */ + _showPlacesLockedNotificationBox: function() { + var applicationName = gBrandBundle.GetStringFromName("brandShortName"); + var placesBundle = Services.strings.createBundle("chrome://browser/locale/places/places.properties"); + var title = placesBundle.GetStringFromName("lockPrompt.title"); + var text = placesBundle.formatStringFromName("lockPrompt.text", [applicationName], 1); + var buttonText = placesBundle.GetStringFromName("lockPromptInfoButton.label"); + var accessKey = placesBundle.GetStringFromName("lockPromptInfoButton.accessKey"); + + var helpTopic = "places-locked"; + var url = Cc["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter) + .formatURLPref("app.support.baseURL"); + url += helpTopic; + + var win = this.getMostRecentBrowserWindow(); + + var buttons = [ + { + label: buttonText, + accessKey: accessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + win.openUILinkIn(url, "tab"); + } + } + ]; + + var notifyBox = win.gBrowser.getNotificationBox(); + var notification = notifyBox.appendNotification(text, title, null, + notifyBox.PRIORITY_CRITICAL_MEDIUM, + buttons); + notification.persistence = -1; // Until user closes it + }, + + _migrateUI: function() { + const UI_VERSION = 25; + const BROWSER_DOCURL = "chrome://browser/content/browser.xul#"; + let currentUIVersion = 0; + try { + currentUIVersion = Services.prefs.getIntPref("browser.migration.version"); + } catch(ex) {} + if (currentUIVersion >= UI_VERSION) { + return; + } + + this._rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); + this._dataSource = this._rdf.GetDataSource("rdf:local-store"); + this._dirty = false; + + if (currentUIVersion < 2) { + // This code adds the customizable bookmarks button. + let currentsetResource = this._rdf.GetResource("currentset"); + let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); + let currentset = this._getPersist(toolbarResource, currentsetResource); + // Need to migrate only if toolbar is customized and the element is not found. + if (currentset && + currentset.indexOf("bookmarks-menu-button-container") == -1) { + currentset += ",bookmarks-menu-button-container"; + this._setPersist(toolbarResource, currentsetResource, currentset); + } + } + + if (currentUIVersion < 3) { + // This code merges the reload/stop/go button into the url bar. + let currentsetResource = this._rdf.GetResource("currentset"); + let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); + let currentset = this._getPersist(toolbarResource, currentsetResource); + // Need to migrate only if toolbar is customized and all 3 elements are found. + if (currentset && + currentset.indexOf("reload-button") != -1 && + currentset.indexOf("stop-button") != -1 && + currentset.indexOf("urlbar-container") != -1 && + currentset.indexOf("urlbar-container,reload-button,stop-button") == -1) { + currentset = currentset.replace(/(^|,)reload-button($|,)/, "$1$2") + .replace(/(^|,)stop-button($|,)/, "$1$2") + .replace(/(^|,)urlbar-container($|,)/, + "$1urlbar-container,reload-button,stop-button$2"); + this._setPersist(toolbarResource, currentsetResource, currentset); + } + } + + if (currentUIVersion < 4) { + // This code moves the home button to the immediate left of the bookmarks menu button. + let currentsetResource = this._rdf.GetResource("currentset"); + let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); + let currentset = this._getPersist(toolbarResource, currentsetResource); + // Need to migrate only if toolbar is customized and the elements are found. + if (currentset && + currentset.indexOf("home-button") != -1 && + currentset.indexOf("bookmarks-menu-button-container") != -1) { + currentset = currentset.replace(/(^|,)home-button($|,)/, "$1$2") + .replace(/(^|,)bookmarks-menu-button-container($|,)/, + "$1home-button,bookmarks-menu-button-container$2"); + this._setPersist(toolbarResource, currentsetResource, currentset); + } + } + + if (currentUIVersion < 5) { + // This code uncollapses PersonalToolbar if its collapsed status is not + // persisted, and user customized it or changed default bookmarks. + let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "PersonalToolbar"); + let collapsedResource = this._rdf.GetResource("collapsed"); + let collapsed = this._getPersist(toolbarResource, collapsedResource); + // If the user does not have a persisted value for the toolbar's + // "collapsed" attribute, try to determine whether it's customized. + if (collapsed === null) { + // We consider the toolbar customized if it has more than + // 3 children, or if it has a persisted currentset value. + let currentsetResource = this._rdf.GetResource("currentset"); + let toolbarIsCustomized = !!this._getPersist(toolbarResource, + currentsetResource); + function getToolbarFolderCount() { + let toolbarFolder = + PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root; + let toolbarChildCount = toolbarFolder.childCount; + toolbarFolder.containerOpen = false; + return toolbarChildCount; + } + + if (toolbarIsCustomized || getToolbarFolderCount() > 3) { + this._setPersist(toolbarResource, collapsedResource, "false"); + } + } + } + + if (currentUIVersion < 6) { + // convert tabsontop attribute to pref + let toolboxResource = this._rdf.GetResource(BROWSER_DOCURL + "navigator-toolbox"); + let tabsOnTopResource = this._rdf.GetResource("tabsontop"); + let tabsOnTopAttribute = this._getPersist(toolboxResource, tabsOnTopResource); + if (tabsOnTopAttribute) + Services.prefs.setBoolPref("browser.tabs.onTop", tabsOnTopAttribute == "true"); + } + + // Migration at version 7 only occurred for users who wanted to try the new + // Downloads Panel feature before its release. Since migration at version + // 9 adds the button by default, this step has been removed. + + if (currentUIVersion < 8) { + // Reset homepage pref for users who have it set to google.com/firefox + let uri = Services.prefs.getComplexValue("browser.startup.homepage", + Ci.nsIPrefLocalizedString).data; + if (uri && /^https?:\/\/(www\.)?google(\.\w{2,3}){1,2}\/firefox\/?$/.test(uri)) { + Services.prefs.clearUserPref("browser.startup.homepage"); + } + } + + if (currentUIVersion < 9) { + // This code adds the customizable downloads buttons. + let currentsetResource = this._rdf.GetResource("currentset"); + let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); + let currentset = this._getPersist(toolbarResource, currentsetResource); + + // Since the Downloads button is located in the navigation bar by default, + // migration needs to happen only if the toolbar was customized using a + // previous UI version, and the button was not already placed on the + // toolbar manually. + if (currentset && + currentset.indexOf("downloads-button") == -1) { + // The element is added either after the search bar or before the home + // button. As a last resort, the element is added just before the + // non-customizable window controls. + if (currentset.indexOf("search-container") != -1) { + currentset = currentset.replace(/(^|,)search-container($|,)/, + "$1search-container,downloads-button$2") + } else if (currentset.indexOf("home-button") != -1) { + currentset = currentset.replace(/(^|,)home-button($|,)/, + "$1downloads-button,home-button$2") + } else { + currentset = currentset.replace(/(^|,)window-controls($|,)/, + "$1downloads-button,window-controls$2") + } + this._setPersist(toolbarResource, currentsetResource, currentset); + } + + Services.prefs.clearUserPref("browser.download.useToolkitUI"); + Services.prefs.clearUserPref("browser.library.useNewDownloadsView"); + } + +#ifdef XP_WIN + if (currentUIVersion < 10) { + // For Windows systems with display set to > 96dpi (i.e. systemDefaultScale + // will return a value > 1.0), we want to discard any saved full-zoom settings, + // as we'll now be scaling the content according to the system resolution + // scale factor (Windows "logical DPI" setting) + let sm = Cc["@mozilla.org/gfx/screenmanager;1"].getService(Ci.nsIScreenManager); + if (sm.systemDefaultScale > 1.0) { + let cps2 = Cc["@mozilla.org/content-pref/service;1"]. + getService(Ci.nsIContentPrefService2); + cps2.removeByName("browser.content.full-zoom", null); + } + } +#endif + + if (currentUIVersion < 11) { + Services.prefs.clearUserPref("dom.disable_window_move_resize"); + Services.prefs.clearUserPref("dom.disable_window_flip"); + Services.prefs.clearUserPref("dom.event.contextmenu.enabled"); + Services.prefs.clearUserPref("javascript.enabled"); + Services.prefs.clearUserPref("permissions.default.image"); + } + + if (currentUIVersion < 12) { + // Remove bookmarks-menu-button-container, then place + // bookmarks-menu-button into its position. + let currentsetResource = this._rdf.GetResource("currentset"); + let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); + let currentset = this._getPersist(toolbarResource, currentsetResource); + // Need to migrate only if toolbar is customized. + if (currentset) { + if (currentset.contains("bookmarks-menu-button-container")) { + currentset = currentset.replace(/(^|,)bookmarks-menu-button-container($|,)/, + "$1bookmarks-menu-button$2"); + this._setPersist(toolbarResource, currentsetResource, currentset); + } + } + } + + if (currentUIVersion < 16) { + // Migrate Sync from pmsync.palemoon.net to pmsync.palemoon.org + try { + let syncURL = Services.prefs.getCharPref("services.sync.clusterURL"); + let newSyncURL = syncURL.replace(/pmsync\.palemoon\.net/i,"pmsync.palemoon.org"); + if (newSyncURL != syncURL) { + Services.prefs.setCharPref("services.sync.clusterURL", newSyncURL); + } + } catch(ex) { + // Pref not found: Sync not in use, nothing to do. + } + } + + if (currentUIVersion < 17) { + this._notifyNotificationsUpgrade(); + } + + if (currentUIVersion < 18) { + // Make sure the doNotTrack value conforms to the conversion from + // three-state to two-state. (This reverts a setting of "please track me" + // to the default "don't say anything"). + try { + if (Services.prefs.getBoolPref("privacy.donottrackheader.enabled") && + Services.prefs.getIntPref("privacy.donottrackheader.value") != 1) { + Services.prefs.clearUserPref("privacy.donottrackheader.enabled"); + Services.prefs.clearUserPref("privacy.donottrackheader.value"); + } + } + catch (ex) {} + } + +#ifndef MOZ_JXR + // Until JPEG-XR decoder is implemented (UXP #144) + if (currentUIVersion < 19) { + try { + let ihaPref = "image.http.accept"; + let ihaValue = Services.prefs.getCharPref(ihaPref); + if (ihaValue.includes("image/jxr,")) { + Services.prefs.setCharPref(ihaPref, ihaValue.replace("image/jxr,", "")); + } else if (ihaValue.includes("image/jxr")) { + Services.prefs.clearUserPref(ihaPref); + } + } catch(ex) {} + } +#endif + + if (currentUIVersion < 20) { + // HPKP change of UI preference; reset enforcement level + Services.prefs.clearUserPref("security.cert_pinning.enforcement_level"); + } + + if (currentUIVersion < 23) { + if (Services.prefs.prefHasUserValue("layers.acceleration.disabled")) { + let HWADisabled = Services.prefs.getBoolPref("layers.acceleration.disabled"); + Services.prefs.setBoolPref("layers.acceleration.enabled", !HWADisabled); + Services.prefs.setBoolPref("gfx.direct2d.disabled", HWADisabled); + } + if (Services.prefs.getBoolPref("layers.acceleration.force-enabled", false)) { + Services.prefs.setBoolPref("layers.acceleration.force", true); + } + Services.prefs.clearUserPref("layers.acceleration.disabled"); + Services.prefs.clearUserPref("layers.acceleration.force-enabled"); + } + + if (currentUIVersion < 24) { + // AbortController's worker signalling was fixed so reset user prefs that + // might have been set as workaround for web compat issues in the meantime. + Services.prefs.clearUserPref("dom.abortController.enabled"); + } + + if (currentUIVersion < 25) { + // DoNotTrack is now GPC. Carry across user preference. + if (Services.prefs.prefHasUserValue("privacy.donottrackheader.enabled")) { + let DNTEnabled = Services.prefs.getBoolPref("privacy.donottrackheader.enabled"); + Service.prefs.setBoolPref("privacy.GPCheader.enabled", DNTEnabled); + Services.prefs.clearUserPref("privacy.donottrackheader.enabled"); + } + } + + // Clear out dirty storage + if (this._dirty) { + this._dataSource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush(); + } + + delete this._rdf; + delete this._dataSource; + + // Update the migration version. + Services.prefs.setIntPref("browser.migration.version", UI_VERSION); + }, + + _hasExistingNotificationPermission: function() { + let enumerator = Services.perms.enumerator; + while (enumerator.hasMoreElements()) { + let permission = enumerator.getNext().QueryInterface(Ci.nsIPermission); + if (permission.type == "desktop-notification") { + return true; + } + } + return false; + }, + + _notifyNotificationsUpgrade: function() { + if (!this._hasExistingNotificationPermission()) { + return; + } + function clickCallback(subject, topic, data) { + if (topic != "alertclickcallback") { + return; + } + let win = RecentWindow.getMostRecentBrowserWindow(); + win.openUILinkIn(data, "tab"); + } + // Show the application icon for XUL notifications. We assume system-level + // notifications will include their own icon. + let imageURL = this._hasSystemAlertsService() ? "" : + "chrome://branding/content/about-logo.png"; + let title = gBrowserBundle.GetStringFromName("webNotifications.upgradeTitle"); + let text = gBrowserBundle.GetStringFromName("webNotifications.upgradeBody"); + let url = Services.urlFormatter.formatURLPref("browser.push.warning.infoURL"); + + try { + AlertsService.showAlertNotification(imageURL, title, text, + true, url, clickCallback); + } catch(e) { + Cu.reportError(e); + } + }, + + _openPermissions: function(aPrincipal) { + var win = this.getMostRecentBrowserWindow(); + var url = "about:permissions"; + try { + url = url + "?filter=" + aPrincipal.URI.host; + } catch(e) {} + win.openUILinkIn(url, "tab"); + }, + + _hasSystemAlertsService: function() { + try { + return !!Cc["@mozilla.org/system-alerts-service;1"].getService( + Ci.nsIAlertsService); + } catch(e) {} + return false; + }, + + _getPersist: function(aSource, aProperty) { + var target = this._dataSource.GetTarget(aSource, aProperty, true); + if (target instanceof Ci.nsIRDFLiteral) { + return target.Value; + } + return null; + }, + + _setPersist: function(aSource, aProperty, aTarget) { + this._dirty = true; + try { + var oldTarget = this._dataSource.GetTarget(aSource, aProperty, true); + if (oldTarget) { + if (aTarget) { + this._dataSource.Change(aSource, aProperty, oldTarget, this._rdf.GetLiteral(aTarget)); + } else { + this._dataSource.Unassert(aSource, aProperty, oldTarget); + } + } else { + this._dataSource.Assert(aSource, aProperty, this._rdf.GetLiteral(aTarget), true); + } + + // Add the entry to the persisted set for this document if it's not there. + // This code is mostly borrowed from XULDocument::Persist. + let docURL = aSource.ValueUTF8.split("#")[0]; + let docResource = this._rdf.GetResource(docURL); + let persistResource = this._rdf.GetResource("http://home.netscape.com/NC-rdf#persist"); + if (!this._dataSource.HasAssertion(docResource, persistResource, aSource, true)) { + this._dataSource.Assert(docResource, persistResource, aSource, true); + } + } catch(ex) {} + }, + + // ------------------------------ + // public nsIBrowserGlue members + // ------------------------------ + + sanitize: function(aParentWindow) { + this._sanitizer.sanitize(aParentWindow); + }, + + ensurePlacesDefaultQueriesInitialized: + function() { + // This is actual version of the smart bookmarks, must be increased every + // time smart bookmarks change. + // When adding a new smart bookmark below, its newInVersion property must + // be set to the version it has been added in, we will compare its value + // to users' smartBookmarksVersion and add new smart bookmarks without + // recreating old deleted ones. + const SMART_BOOKMARKS_VERSION = 4; + const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark"; + const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion"; + const SMART_BOOKMARKS_MAX_PREF = "browser.places.smartBookmarks.max"; + const SMART_BOOKMARKS_OLDMAX_PREF = "browser.places.smartBookmarks.old-max"; + + const MAX_RESULTS = Services.prefs.getIntPref(SMART_BOOKMARKS_MAX_PREF, 10); + let OLD_MAX_RESULTS = Services.prefs.getIntPref(SMART_BOOKMARKS_OLDMAX_PREF, 10); + + // Get current smart bookmarks version. If not set, create them. + let smartBookmarksCurrentVersion = Services.prefs.getIntPref(SMART_BOOKMARKS_PREF, 0); + + // If version is current and max hasn't changed or smart bookmarks are disabled, just bail out. + if (smartBookmarksCurrentVersion == -1 || + (smartBookmarksCurrentVersion >= SMART_BOOKMARKS_VERSION && + OLD_MAX_RESULTS == MAX_RESULTS)) { + return; + } + + // We're going to recreate the smart bookmarks and set the current max, so store it. + if (Services.prefs.prefHasUserValue(SMART_BOOKMARKS_MAX_PREF)) { + Services.prefs.setIntPref(SMART_BOOKMARKS_OLDMAX_PREF, MAX_RESULTS); + } else { + // The max value is default, no need to track the temp value. + Services.prefs.clearUserPref(SMART_BOOKMARKS_OLDMAX_PREF); + } + + let batch = { + runBatched: function() { + let menuIndex = 0; + let toolbarIndex = 0; + let bundle = Services.strings.createBundle("chrome://browser/locale/places/places.properties"); + + let smartBookmarks = { + MostVisited: { + title: bundle.GetStringFromName("mostVisitedTitle"), + uri: NetUtil.newURI("place:sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING + + "&maxResults=" + MAX_RESULTS), + parent: PlacesUtils.toolbarFolderId, + position: toolbarIndex++, + newInVersion: 1 + }, + RecentlyBookmarked: { + title: bundle.GetStringFromName("recentlyBookmarkedTitle"), + uri: NetUtil.newURI("place:folder=BOOKMARKS_MENU" + + "&folder=UNFILED_BOOKMARKS" + + "&folder=TOOLBAR" + + "&queryType=" + + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS + + "&sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING + + "&maxResults=" + MAX_RESULTS + + "&excludeQueries=1"), + parent: PlacesUtils.bookmarksMenuFolderId, + position: menuIndex++, + newInVersion: 1 + }, + RecentTags: { + title: bundle.GetStringFromName("recentTagsTitle"), + uri: NetUtil.newURI("place:"+ + "type=" + + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY + + "&sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING + + "&maxResults=" + MAX_RESULTS), + parent: PlacesUtils.bookmarksMenuFolderId, + position: menuIndex++, + newInVersion: 1 + } + }; + + // Set current itemId, parent and position if Smart Bookmark exists, + // we will use these informations to create the new version at the same + // position. + let smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO); + smartBookmarkItemIds.forEach(function(itemId) { + let queryId = PlacesUtils.annotations.getItemAnnotation(itemId, SMART_BOOKMARKS_ANNO); + if (queryId in smartBookmarks) { + let smartBookmark = smartBookmarks[queryId]; + smartBookmark.itemId = itemId; + smartBookmark.parent = PlacesUtils.bookmarks.getFolderIdForItem(itemId); + smartBookmark.position = PlacesUtils.bookmarks.getItemIndex(itemId); + } else { + // We don't remove old Smart Bookmarks because the user could still + // find them useful, or could have personalized them. + // Instead we remove the Smart Bookmark annotation. + PlacesUtils.annotations.removeItemAnnotation(itemId, SMART_BOOKMARKS_ANNO); + } + }); + + for (let queryId in smartBookmarks) { + let smartBookmark = smartBookmarks[queryId]; + + // We update or create only changed or new smart bookmarks. + // Also we respect user choices, so we won't try to create a smart + // bookmark if it has been removed. + if (smartBookmarksCurrentVersion > 0 && + smartBookmark.newInVersion <= smartBookmarksCurrentVersion && + !smartBookmark.itemId) { + continue; + } + + // Remove old version of the smart bookmark if it exists, since it + // will be replaced in place. + if (smartBookmark.itemId) { + PlacesUtils.bookmarks.removeItem(smartBookmark.itemId); + } + + // Create the new smart bookmark and store its updated itemId. + smartBookmark.itemId = + PlacesUtils.bookmarks.insertBookmark(smartBookmark.parent, + smartBookmark.uri, + smartBookmark.position, + smartBookmark.title); + PlacesUtils.annotations.setItemAnnotation(smartBookmark.itemId, + SMART_BOOKMARKS_ANNO, + queryId, 0, + PlacesUtils.annotations.EXPIRE_NEVER); + } + + // If we are creating all Smart Bookmarks from ground up, add a + // separator below them in the bookmarks menu. + if (smartBookmarksCurrentVersion == 0 && + smartBookmarkItemIds.length == 0) { + let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, + menuIndex); + // Don't add a separator if the menu was empty or there is one already. + if (id != -1 && + PlacesUtils.bookmarks.getItemType(id) != PlacesUtils.bookmarks.TYPE_SEPARATOR) { + PlacesUtils.bookmarks.insertSeparator(PlacesUtils.bookmarksMenuFolderId, + menuIndex); + } + } + } + }; + + try { + PlacesUtils.bookmarks.runInBatchMode(batch, null); + } catch(ex) { + Components.utils.reportError(ex); + } finally { + Services.prefs.setIntPref(SMART_BOOKMARKS_PREF, SMART_BOOKMARKS_VERSION); + Services.prefs.savePrefFile(null); + } + }, + + // this returns the most recent non-popup browser window + getMostRecentBrowserWindow: function() { + return RecentWindow.getMostRecentBrowserWindow(); + }, + +#ifdef MOZ_SERVICES_SYNC + /** + * Called as an observer when Sync's "display URI" notification is fired. + * + * We open the received URI in a background tab. + * + * Eventually, this will likely be replaced by a more robust tab syncing + * feature. This functionality is considered somewhat evil by UX because it + * opens a new tab automatically without any prompting. However, it is a + * lesser evil than sending a tab to a specific device (from e.g. Fennec) + * and having nothing happen on the receiving end. + */ + _onDisplaySyncURI: function(data) { + try { + let tabbrowser = RecentWindow.getMostRecentBrowserWindow({private: false}).gBrowser; + + // The payload is wrapped weirdly because of how Sync does notifications. + tabbrowser.addTab(data.wrappedJSObject.object.uri); + } catch(ex) { + Cu.reportError("Error displaying tab received by Sync: " + ex); + } + }, +#endif + + // for XPCOM + classID: Components.ID("{eab9012e-5f74-4cbc-b2b5-a590235513cc}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference, + Ci.nsIBrowserGlue]), + + // redefine the default factory for XPCOMUtils + _xpcom_factory: BrowserGlueServiceFactory, +} + +function ContentPermissionPrompt() {} +ContentPermissionPrompt.prototype = { + classID: Components.ID("{d8903bf6-68d5-4e97-bcd1-e4d3012f721a}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]), + + _getChromeWindow: function(aWindow) { + var chromeWin = aWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + return chromeWin; + }, + + _getBrowserForRequest: function(aRequest) { + let requestingWindow = aRequest.window.top; + // find the requesting browser or iframe + let browser = requestingWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + return browser; + }, + + /** + * Show a permission prompt. + * + * @param aRequest The permission request. + * @param aMessage The message to display on the prompt. + * @param aPermission The type of permission to prompt. + * @param aActions An array of actions of the form: + * [main action, secondary actions, ...] + * Actions are of the form { stringId, action, expireType, callback } + * Permission is granted if action is null or ALLOW_ACTION. + * @param aNotificationId The id of the PopupNotification. + * @param aAnchorId The id for the PopupNotification anchor. + * @param aOptions Options for the PopupNotification + */ + _showPrompt: function(aRequest, aMessage, aPermission, aActions, + aNotificationId, aAnchorId, aOptions) { + function onFullScreen() { + popup.remove(); + } + + var requestingWindow = aRequest.window.top; + var chromeWin = this._getChromeWindow(requestingWindow).wrappedJSObject; + var browser = chromeWin.gBrowser.getBrowserForDocument(requestingWindow.document); + if (!browser) { + // find the requesting browser or iframe + browser = requestingWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + } + var requestPrincipal = aRequest.principal; + + // Transform the prompt actions into PopupNotification actions. + var popupNotificationActions = []; + for (var i = 0; i < aActions.length; i++) { + let promptAction = aActions[i]; + + // Don't offer action in PB mode if the action remembers permission for more than a session. + if (PrivateBrowsingUtils.isWindowPrivate(chromeWin) && + promptAction.expireType != Ci.nsIPermissionManager.EXPIRE_SESSION && + promptAction.action) { + continue; + } + + var action = { + label: gBrowserBundle.GetStringFromName(promptAction.stringId), + accessKey: gBrowserBundle.GetStringFromName(promptAction.stringId + ".accesskey"), + callback: function() { + if (promptAction.callback) { + promptAction.callback(); + } + + // Remember permissions. + if (promptAction.action) { + Services.perms.addFromPrincipal(requestPrincipal, aPermission, + promptAction.action, promptAction.expireType); + } + + // Grant permission if action is null or ALLOW_ACTION. + if (!promptAction.action || promptAction.action == Ci.nsIPermissionManager.ALLOW_ACTION) { + aRequest.allow(); + } else { + aRequest.cancel(); + } + }, + }; + + popupNotificationActions.push(action); + } + + var mainAction = popupNotificationActions.length ? + popupNotificationActions[0] : null; + var secondaryActions = popupNotificationActions.splice(1); + + if (aRequest.type == "pointerLock") { + // If there's no mainAction, this is the autoAllow warning prompt. + let autoAllow = !mainAction; + + if (!aOptions) { + aOptions = {}; + } + + aOptions.removeOnDismissal = autoAllow; + aOptions.eventCallback = type => { + if (type == "removed") { + browser.removeEventListener("mozfullscreenchange", onFullScreen, true); + if (autoAllow) { + aRequest.allow(); + } + } + } + + } + + var popup = chromeWin.PopupNotifications.show(browser, aNotificationId, aMessage, aAnchorId, + mainAction, secondaryActions, aOptions); + if (aRequest.type == "pointerLock") { + // pointerLock is automatically allowed in fullscreen mode (and revoked + // upon exit), so if the page enters fullscreen mode after requesting + // pointerLock (but before the user has granted permission), we should + // remove the now-impotent notification. + browser.addEventListener("mozfullscreenchange", onFullScreen, true); + } + }, + + _promptGeo : function(aRequest) { + var requestingURI = aRequest.principal.URI; + + var message; + + // Share location action. + var actions = [{ stringId: "geolocation.shareLocation", + action: null, + expireType: null, + callback: function() { + // Telemetry stub (left here for safety and compatibility reasons) + } + }]; + + if (requestingURI.schemeIs("file")) { + message = gBrowserBundle.formatStringFromName("geolocation.shareWithFile", + [requestingURI.path], 1); + } else { + message = gBrowserBundle.formatStringFromName("geolocation.shareWithSite", + [requestingURI.host], 1); + // Always share location action. + actions.push({ + stringId: "geolocation.alwaysShareLocation", + action: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: null, + callback: function() { + // Telemetry stub (left here for safety and compatibility reasons) + } + }); + + // Never share location action. + actions.push({ + stringId: "geolocation.neverShareLocation", + action: Ci.nsIPermissionManager.DENY_ACTION, + expireType: null, + callback: function() { + // Telemetry stub (left here for safety and compatibility reasons) + } + }); + } + + var options = { learnMoreURL: Services.urlFormatter.formatURLPref("browser.geolocation.warning.infoURL") }; + + this._showPrompt(aRequest, message, "geo", actions, "geolocation", + "geo-notification-icon", options); + }, + + _promptWebNotifications : function(aRequest) { + var requestingURI = aRequest.principal.URI; + + var message = gBrowserBundle.formatStringFromName("webNotifications.showFromSite", + [requestingURI.host], 1); + + var actions; + + var browser = this._getBrowserForRequest(aRequest); + // Only show "allow for session" in PB mode, we don't + // support "allow for session" in non-PB mode. + if (PrivateBrowsingUtils.isBrowserPrivate(browser)) { + actions = [ + { + stringId: "webNotifications.showForSession", + action: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: Ci.nsIPermissionManager.EXPIRE_SESSION, + callback: function() {}, + } + ]; + } else { + actions = [ + { + stringId: "webNotifications.showForSession", + action: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: Ci.nsIPermissionManager.EXPIRE_SESSION, + callback: function() {}, + }, + { + stringId: "webNotifications.alwaysShow", + action: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: null, + callback: function() {}, + }, + { + stringId: "webNotifications.neverShow", + action: Ci.nsIPermissionManager.DENY_ACTION, + expireType: null, + callback: function() {}, + } + ]; + } + var options = { + learnMoreURL: Services.urlFormatter.formatURLPref("browser.push.warning.infoURL"), + }; + + this._showPrompt(aRequest, message, "desktop-notification", actions, + "web-notifications", + "web-notifications-notification-icon", options); + }, + + _promptPointerLock: function(aRequest, autoAllow) { + let requestingURI = aRequest.principal.URI; + + let originString = requestingURI.schemeIs("file") ? requestingURI.path : requestingURI.host; + let message = gBrowserBundle.formatStringFromName(autoAllow ? + "pointerLock.autoLock.title2" : "pointerLock.title2", + [originString], 1); + // If this is an autoAllow info prompt, offer no actions. + // _showPrompt() will allow the request when it's dismissed. + let actions = []; + if (!autoAllow) { + actions = [ + { + stringId: "pointerLock.allow2", + action: null, + expireType: null, + callback: function() {}, + }, + { + stringId: "pointerLock.alwaysAllow", + action: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: null, + callback: function() {}, + }, + { + stringId: "pointerLock.neverAllow", + action: Ci.nsIPermissionManager.DENY_ACTION, + expireType: null, + callback: function() {}, + } + ]; + } + + this._showPrompt(aRequest, message, "pointerLock", actions, "pointerLock", + "pointerLock-notification-icon", null); + }, + + prompt: function(request) { + // Only allow exactly one permission rquest here. + let types = request.types.QueryInterface(Ci.nsIArray); + if (types.length != 1) { + request.cancel(); + return; + } + let perm = types.queryElementAt(0, Ci.nsIContentPermissionType); + + const kFeatureKeys = { "geolocation" : "geo", + "desktop-notification" : "desktop-notification", + "pointerLock" : "pointerLock", + }; + + // Make sure that we support the request. + if (!(perm.type in kFeatureKeys)) { + return; + } + + var requestingPrincipal = request.principal; + var requestingURI = requestingPrincipal.URI; + + // Ignore requests from non-nsIStandardURLs + if (!(requestingURI instanceof Ci.nsIStandardURL)) + return; + + var autoAllow = false; + var permissionKey = kFeatureKeys[perm.type]; + var result = Services.perms.testExactPermissionFromPrincipal(requestingPrincipal, permissionKey); + + if (result == Ci.nsIPermissionManager.DENY_ACTION) { + request.cancel(); + return; + } + + if (result == Ci.nsIPermissionManager.ALLOW_ACTION) { + autoAllow = true; + // For pointerLock, we still want to show a warning prompt. + if (request.type != "pointerLock") { + request.allow(); + return; + } + } + + // Show the prompt. + switch (perm.type) { + case "geolocation": + this._promptGeo(request); + break; + case "desktop-notification": + this._promptWebNotifications(request); + break; + case "pointerLock": + this._promptPointerLock(request, autoAllow); + break; + } + } +}; // ContentPermissionPrompt + +var components = [BrowserGlue, ContentPermissionPrompt]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/components/nsIBrowserGlue.idl b/browser/components/nsIBrowserGlue.idl new file mode 100644 index 000000000..1bb82a9d2 --- /dev/null +++ b/browser/components/nsIBrowserGlue.idl @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIDOMWindow; + +/** + * nsIBrowserGlue is a dirty and rather fluid interface to host shared utility + * methods used by browser UI code, but which are not local to a browser window. + * The component implementing this interface is meant to be a singleton + * (service) and should progressively replace some of the shared "glue" code + * scattered in browser/base/content (e.g. bits of utilOverlay.js, + * contentAreaUtils.js, globalOverlay.js, browser.js), avoiding dynamic + * inclusion and initialization of a ton of JS code for *each* window. + * Dued to its nature and origin, this interface won't probably be the most + * elegant or stable in the mozilla codebase, but its aim is rather pragmatic: + * 1) reducing the performance overhead which affects browser window load; + * 2) allow global hooks (e.g. startup and shutdown observers) which survive + * browser windows to accomplish browser-related activities, such as shutdown + * sanitization (see bug #284086) + * + */ + +[scriptable, uuid(781df699-17dc-4237-b3d7-876ddb7085e3)] +interface nsIBrowserGlue : nsISupports +{ + /** + * Deletes privacy sensitive data according to user preferences + * + * @param aParentWindow an optionally null window which is the parent of the + * sanitization dialog + * + */ + void sanitize(in nsIDOMWindow aParentWindow); + + /** + * Add Smart Bookmarks special queries to bookmarks menu and toolbar folder. + */ + void ensurePlacesDefaultQueriesInitialized(); + + /** + * Gets the most recent window that's a browser (but not a popup) + */ + nsIDOMWindow getMostRecentBrowserWindow(); +}; diff --git a/browser/components/nsIBrowserHandler.idl b/browser/components/nsIBrowserHandler.idl new file mode 100644 index 000000000..74292f9d9 --- /dev/null +++ b/browser/components/nsIBrowserHandler.idl @@ -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/. */ + +#include "nsISupports.idl" + +interface nsICommandLine; + +[scriptable, uuid(8D3F5A9D-118D-4548-A137-CF7718679069)] +interface nsIBrowserHandler : nsISupports +{ + attribute AUTF8String startPage; + attribute AUTF8String defaultArgs; + + /** + * Extract the width and height specified on the command line, if present. + * @return A feature string with a prepended comma, e.g. ",width=500,height=400" + */ + AUTF8String getFeatures(in nsICommandLine aCmdLine); +}; diff --git a/browser/components/pageinfo/feeds.js b/browser/components/pageinfo/feeds.js new file mode 100644 index 000000000..468d8c19d --- /dev/null +++ b/browser/components/pageinfo/feeds.js @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function initFeedTab() +{ + const feedTypes = { + "application/rss+xml": gBundle.getString("feedRss"), + "application/atom+xml": gBundle.getString("feedAtom"), + "text/xml": gBundle.getString("feedXML"), + "application/xml": gBundle.getString("feedXML"), + "application/rdf+xml": gBundle.getString("feedXML") + }; + + // get the feeds + var linkNodes = gDocument.getElementsByTagName("link"); + var length = linkNodes.length; + for (var i = 0; i < length; i++) { + var link = linkNodes[i]; + if (!link.href) + continue; + + var rel = link.rel && link.rel.toLowerCase(); + var rels = {}; + if (rel) { + for each (let relVal in rel.split(/\s+/)) + rels[relVal] = true; + } + + if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) { + var type = isValidFeed(link, gDocument.nodePrincipal, "feed" in rels); + if (type) { + type = feedTypes[type] || feedTypes["application/rss+xml"]; + addRow(link.title, type, link.href); + } + } + } + + var feedListbox = document.getElementById("feedListbox"); + document.getElementById("feedTab").hidden = feedListbox.getRowCount() == 0; +} + +function onSubscribeFeed() +{ + var listbox = document.getElementById("feedListbox"); + openUILinkIn(listbox.selectedItem.getAttribute("feedURL"), "current", + { ignoreAlt: true }); +} + +function addRow(name, type, url) +{ + var item = document.createElement("richlistitem"); + item.setAttribute("feed", "true"); + item.setAttribute("name", name); + item.setAttribute("type", type); + item.setAttribute("feedURL", url); + document.getElementById("feedListbox").appendChild(item); +} diff --git a/browser/components/pageinfo/feeds.xml b/browser/components/pageinfo/feeds.xml new file mode 100644 index 000000000..782c05a73 --- /dev/null +++ b/browser/components/pageinfo/feeds.xml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ + <!ENTITY % pageInfoDTD SYSTEM "chrome://browser/locale/pageInfo.dtd"> + %pageInfoDTD; +]> + +<bindings id="feedBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="feed" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:vbox flex="1"> + <xul:hbox flex="1"> + <xul:textbox flex="1" readonly="true" xbl:inherits="value=name" + class="feedTitle"/> + <xul:label xbl:inherits="value=type"/> + </xul:hbox> + <xul:vbox> + <xul:vbox align="start"> + <xul:hbox> + <xul:label xbl:inherits="value=feedURL,tooltiptext=feedURL" class="text-link" flex="1" + onclick="openUILink(this.value, event);" crop="end"/> + </xul:hbox> + </xul:vbox> + </xul:vbox> + <xul:hbox flex="1" class="feed-subscribe"> + <xul:spacer flex="1"/> + <xul:button label="&feedSubscribe;" accesskey="&feedSubscribe.accesskey;" + oncommand="onSubscribeFeed()"/> + </xul:hbox> + </xul:vbox> + </content> + </binding> +</bindings> diff --git a/browser/components/pageinfo/jar.mn b/browser/components/pageinfo/jar.mn new file mode 100644 index 000000000..c0c947ffe --- /dev/null +++ b/browser/components/pageinfo/jar.mn @@ -0,0 +1,13 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/pageinfo/pageInfo.xul + content/browser/pageinfo/pageInfo.js + content/browser/pageinfo/pageInfo.css + content/browser/pageinfo/pageInfo.xml + content/browser/pageinfo/feeds.js + content/browser/pageinfo/feeds.xml + content/browser/pageinfo/permissions.js + content/browser/pageinfo/security.js
\ No newline at end of file diff --git a/browser/components/pageinfo/moz.build b/browser/components/pageinfo/moz.build new file mode 100644 index 000000000..8267a660d --- /dev/null +++ b/browser/components/pageinfo/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] + diff --git a/browser/components/pageinfo/pageInfo.css b/browser/components/pageinfo/pageInfo.css new file mode 100644 index 000000000..622b56bb5 --- /dev/null +++ b/browser/components/pageinfo/pageInfo.css @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#viewGroup > radio { + -moz-binding: url("chrome://browser/content/pageinfo/pageInfo.xml#viewbutton"); +} + +richlistitem[feed] { + -moz-binding: url("chrome://browser/content/pageinfo/feeds.xml#feed"); +} + +richlistitem[feed]:not([selected="true"]) .feed-subscribe { + display: none; +} + +groupbox[closed="true"] > .groupbox-body { + visibility: collapse; +} + +#thepreviewimage { + display: block; +/* This following entry can be removed when Bug 522850 is fixed. */ + min-width: 1px; +} diff --git a/browser/components/pageinfo/pageInfo.js b/browser/components/pageinfo/pageInfo.js new file mode 100644 index 000000000..600174ad9 --- /dev/null +++ b/browser/components/pageinfo/pageInfo.js @@ -0,0 +1,1286 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Cu = Components.utils; +Cu.import("resource://gre/modules/LoadContextInfo.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +//******** define a js object to implement nsITreeView +function pageInfoTreeView(treeid, copycol) +{ + // copycol is the index number for the column that we want to add to + // the copy-n-paste buffer when the user hits accel-c + this.treeid = treeid; + this.copycol = copycol; + this.rows = 0; + this.tree = null; + this.data = [ ]; + this.selection = null; + this.sortcol = -1; + this.sortdir = false; +} + +pageInfoTreeView.prototype = { + set rowCount(c) { throw "rowCount is a readonly property"; }, + get rowCount() { return this.rows; }, + + setTree: function(tree) + { + this.tree = tree; + }, + + getCellText: function(row, column) + { + // row can be null, but js arrays are 0-indexed. + // colidx cannot be null, but can be larger than the number + // of columns in the array. In this case it's the fault of + // whoever typoed while calling this function. + return this.data[row][column.index] || ""; + }, + + setCellValue: function(row, column, value) + { + }, + + setCellText: function(row, column, value) + { + this.data[row][column.index] = value; + }, + + addRow: function(row) + { + this.rows = this.data.push(row); + this.rowCountChanged(this.rows - 1, 1); + if (this.selection.count == 0 && this.rowCount && !gImageElement) + this.selection.select(0); + }, + + rowCountChanged: function(index, count) + { + this.tree.rowCountChanged(index, count); + }, + + invalidate: function() + { + this.tree.invalidate(); + }, + + clear: function() + { + if (this.tree) + this.tree.rowCountChanged(0, -this.rows); + this.rows = 0; + this.data = [ ]; + }, + + handleCopy: function(row) + { + return (row < 0 || this.copycol < 0) ? "" : (this.data[row][this.copycol] || ""); + }, + + performActionOnRow: function(action, row) + { + if (action == "copy") { + var data = this.handleCopy(row) + this.tree.treeBody.parentNode.setAttribute("copybuffer", data); + } + }, + + onPageMediaSort : function(columnname) + { + var tree = document.getElementById(this.treeid); + var treecol = tree.columns.getNamedColumn(columnname); + + this.sortdir = + gTreeUtils.sort( + tree, + this, + this.data, + treecol.index, + function textComparator(a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }, + this.sortcol, + this.sortdir + ); + + this.sortcol = treecol.index; + }, + + getRowProperties: function(row) { return ""; }, + getCellProperties: function(row, column) { return ""; }, + getColumnProperties: function(column) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function() { }, + canDrop: function(index, orientation) { return false; }, + drop: function(row, orientation) { return false; }, + getParentIndex: function(index) { return 0; }, + hasNextSibling: function(index, after) { return false; }, + getLevel: function(index) { return 0; }, + getImageSrc: function(row, column) { }, + getProgressMode: function(row, column) { }, + getCellValue: function(row, column) { }, + toggleOpenState: function(index) { }, + cycleHeader: function(col) { }, + selectionChanged: function() { }, + cycleCell: function(row, column) { }, + isEditable: function(row, column) { return false; }, + isSelectable: function(row, column) { return false; }, + performAction: function(action) { }, + performActionOnCell: function(action, row, column) { } +}; + +// mmm, yummy. global variables. +var gWindow = null; +var gDocument = null; +var gImageElement = null; + +// column number to help using the data array +const COL_IMAGE_ADDRESS = 0; +const COL_IMAGE_TYPE = 1; +const COL_IMAGE_SIZE = 2; +const COL_IMAGE_ALT = 3; +const COL_IMAGE_COUNT = 4; +const COL_IMAGE_NODE = 5; +const COL_IMAGE_BG = 6; + +// column number to copy from, second argument to pageInfoTreeView's constructor +const COPYCOL_NONE = -1; +const COPYCOL_META_CONTENT = 1; +const COPYCOL_IMAGE = COL_IMAGE_ADDRESS; + +// one nsITreeView for each tree in the window +var gMetaView = new pageInfoTreeView('metatree', COPYCOL_META_CONTENT); +var gImageView = new pageInfoTreeView('imagetree', COPYCOL_IMAGE); + +gImageView.getCellProperties = function(row, col) { + var data = gImageView.data[row]; + var item = gImageView.data[row][COL_IMAGE_NODE]; + var props = ""; + if (!checkProtocol(data) || + item instanceof HTMLEmbedElement || + (item instanceof HTMLObjectElement && !item.type.startsWith("image/"))) + props += "broken"; + + if (col.element.id == "image-address") + props += " ltr"; + + return props; +}; + +gImageView.getCellText = function(row, column) { + var value = this.data[row][column.index]; + if (column.index == COL_IMAGE_SIZE) { + if (value == -1) { + return gStrings.unknown; + } else { + var kbSize = Number(Math.round(value / 1024 * 100) / 100); + return gBundle.getFormattedString("mediaFileSize", [kbSize]); + } + } + return value || ""; +}; + +gImageView.onPageMediaSort = function(columnname) { + var tree = document.getElementById(this.treeid); + var treecol = tree.columns.getNamedColumn(columnname); + + var comparator; + if (treecol.index == COL_IMAGE_SIZE || treecol.index == COL_IMAGE_COUNT) { + comparator = function numComparator(a, b) { return a - b; }; + } else { + comparator = function textComparator(a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }; + } + + this.sortdir = + gTreeUtils.sort( + tree, + this, + this.data, + treecol.index, + comparator, + this.sortcol, + this.sortdir + ); + + this.sortcol = treecol.index; +}; + +var gImageHash = { }; + +// localized strings (will be filled in when the document is loaded) +// this isn't all of them, these are just the ones that would otherwise have been loaded inside a loop +var gStrings = { }; +var gBundle; + +const PERMISSION_CONTRACTID = "@mozilla.org/permissionmanager;1"; +const PREFERENCES_CONTRACTID = "@mozilla.org/preferences-service;1"; +const ATOM_CONTRACTID = "@mozilla.org/atom-service;1"; + +// a number of services I'll need later +// the cache services +const nsICacheStorageService = Components.interfaces.nsICacheStorageService; +const nsICacheStorage = Components.interfaces.nsICacheStorage; +const cacheService = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"].getService(nsICacheStorageService); + +var loadContextInfo = LoadContextInfo.fromLoadContext( + window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsILoadContext), false); +var diskStorage = cacheService.diskCacheStorage(loadContextInfo, false); + +const nsICookiePermission = Components.interfaces.nsICookiePermission; +const nsIPermissionManager = Components.interfaces.nsIPermissionManager; + +const nsICertificateDialogs = Components.interfaces.nsICertificateDialogs; +const CERTIFICATEDIALOGS_CONTRACTID = "@mozilla.org/nsCertificateDialogs;1" + +// clipboard helper +function getClipboardHelper() { + try { + return Components.classes["@mozilla.org/widget/clipboardhelper;1"].getService(Components.interfaces.nsIClipboardHelper); + } catch(e) { + // do nothing, later code will handle the error + } +} +const gClipboardHelper = getClipboardHelper(); + +// Interface for image loading content +const nsIImageLoadingContent = Components.interfaces.nsIImageLoadingContent; + +// namespaces, don't need all of these yet... +const XLinkNS = "http://www.w3.org/1999/xlink"; +const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const XMLNS = "http://www.w3.org/XML/1998/namespace"; +const XHTMLNS = "http://www.w3.org/1999/xhtml"; +const XHTML2NS = "http://www.w3.org/2002/06/xhtml2" + +const XHTMLNSre = "^http\:\/\/www\.w3\.org\/1999\/xhtml$"; +const XHTML2NSre = "^http\:\/\/www\.w3\.org\/2002\/06\/xhtml2$"; +const XHTMLre = RegExp(XHTMLNSre + "|" + XHTML2NSre, ""); + +/* Overlays register functions here. + * These arrays are used to hold callbacks that Page Info will call at + * various stages. Use them by simply appending a function to them. + * For example, add a function to onLoadRegistry by invoking + * "onLoadRegistry.push(XXXLoadFunc);" + * The XXXLoadFunc should be unique to the overlay module, and will be + * invoked as "XXXLoadFunc();" + */ + +// These functions are called to build the data displayed in the Page +// Info window. The global variables gDocument and gWindow are set. +var onLoadRegistry = [ ]; + +// These functions are called to remove old data still displayed in +// the window when the document whose information is displayed +// changes. For example, at this time, the list of images of the Media +// tab is cleared. +var onResetRegistry = [ ]; + +// These are called once for each subframe of the target document and +// the target document itself. The frame is passed as an argument. +var onProcessFrame = [ ]; + +// These functions are called once for each element (in all subframes, if any) +// in the target document. The element is passed as an argument. +var onProcessElement = [ ]; + +// These functions are called once when all the elements in all of the target +// document (and all of its subframes, if any) have been processed +var onFinished = [ ]; + +// These functions are called once when the Page Info window is closed. +var onUnloadRegistry = [ ]; + +// These functions are called once when an image preview is shown. +var onImagePreviewShown = [ ]; + +/* Called when PageInfo window is loaded. Arguments are: + * window.arguments[0] - (optional) an object consisting of + * - doc: (optional) document to use for source. if not provided, + * the calling window's document will be used + * - initialTab: (optional) id of the inital tab to display + */ +function onLoadPageInfo() +{ + gBundle = document.getElementById("pageinfobundle"); + gStrings.unknown = gBundle.getString("unknown"); + gStrings.notSet = gBundle.getString("notset"); + gStrings.mediaImg = gBundle.getString("mediaImg"); + gStrings.mediaBGImg = gBundle.getString("mediaBGImg"); + gStrings.mediaBorderImg = gBundle.getString("mediaBorderImg"); + gStrings.mediaListImg = gBundle.getString("mediaListImg"); + gStrings.mediaCursor = gBundle.getString("mediaCursor"); + gStrings.mediaObject = gBundle.getString("mediaObject"); + gStrings.mediaEmbed = gBundle.getString("mediaEmbed"); + gStrings.mediaLink = gBundle.getString("mediaLink"); + gStrings.mediaInput = gBundle.getString("mediaInput"); + gStrings.mediaVideo = gBundle.getString("mediaVideo"); + gStrings.mediaAudio = gBundle.getString("mediaAudio"); + + var args = "arguments" in window && + window.arguments.length >= 1 && + window.arguments[0]; + + if (!args || !args.doc) { + gWindow = window.opener.content; + gDocument = gWindow.document; + } + + // init media view + var imageTree = document.getElementById("imagetree"); + imageTree.view = gImageView; + + /* Select the requested tab, if the name is specified */ + loadTab(args); + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(window, "page-info-dialog-loaded", null); + + // Make sure the page info window gets focus even if a doorhanger might + // otherwise (async) steal it. + window.focus(); +} + +function loadPageInfo() +{ + var titleFormat = gWindow != gWindow.top ? "pageInfo.frame.title" + : "pageInfo.page.title"; + document.title = gBundle.getFormattedString(titleFormat, [gDocument.location]); + + document.getElementById("main-window").setAttribute("relatedUrl", gDocument.location); + + // do the easy stuff first + makeGeneralTab(); + + // and then the hard stuff + makeTabs(gDocument, gWindow); + + initFeedTab(); + onLoadPermission(gDocument.nodePrincipal); + + /* Call registered overlay init functions */ + onLoadRegistry.forEach(function(func) { func(); }); +} + +function resetPageInfo(args) +{ + /* Reset Meta tags part */ + gMetaView.clear(); + + /* Reset Media tab */ + var mediaTab = document.getElementById("mediaTab"); + if (!mediaTab.hidden) { + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .removeObserver(imagePermissionObserver, "perm-changed"); + mediaTab.hidden = true; + } + gImageView.clear(); + gImageHash = {}; + + /* Reset Feeds Tab */ + var feedListbox = document.getElementById("feedListbox"); + while (feedListbox.firstChild) + feedListbox.removeChild(feedListbox.firstChild); + + /* Call registered overlay reset functions */ + onResetRegistry.forEach(function(func) { func(); }); + + /* Rebuild the data */ + loadTab(args); +} + +function onUnloadPageInfo() +{ + // Remove the observer, only if there is at least 1 image. + if (!document.getElementById("mediaTab").hidden) { + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .removeObserver(imagePermissionObserver, "perm-changed"); + } + + /* Call registered overlay unload functions */ + onUnloadRegistry.forEach(function(func) { func(); }); +} + +function doHelpButton() +{ + const helpTopics = { + "generalPanel": "pageinfo_general", + "mediaPanel": "pageinfo_media", + "feedPanel": "pageinfo_feed", + "permPanel": "pageinfo_permissions", + "securityPanel": "pageinfo_security" + }; + + var deck = document.getElementById("mainDeck"); + var helpdoc = helpTopics[deck.selectedPanel.id] || "pageinfo_general"; + openHelpLink(helpdoc); +} + +function showTab(id) +{ + var deck = document.getElementById("mainDeck"); + var pagel = document.getElementById(id + "Panel"); + deck.selectedPanel = pagel; +} + +function loadTab(args) +{ + if (args && args.doc) { + gDocument = args.doc; + gWindow = gDocument.defaultView; + } + + gImageElement = args && args.imageElement; + + /* Load the page info */ + loadPageInfo(); + + var initialTab = (args && args.initialTab) || "generalTab"; + var radioGroup = document.getElementById("viewGroup"); + initialTab = document.getElementById(initialTab) || document.getElementById("generalTab"); + radioGroup.selectedItem = initialTab; + radioGroup.selectedItem.doCommand(); + radioGroup.focus(); +} + +function onClickMore() +{ + var radioGrp = document.getElementById("viewGroup"); + var radioElt = document.getElementById("securityTab"); + radioGrp.selectedItem = radioElt; + showTab('security'); +} + +function toggleGroupbox(id) +{ + var elt = document.getElementById(id); + if (elt.hasAttribute("closed")) { + elt.removeAttribute("closed"); + if (elt.flexWhenOpened) + elt.flex = elt.flexWhenOpened; + } + else { + elt.setAttribute("closed", "true"); + if (elt.flex) { + elt.flexWhenOpened = elt.flex; + elt.flex = 0; + } + } +} + +function openCacheEntry(key, cb) +{ + var checkCacheListener = { + onCacheEntryCheck: function(entry, appCache) { + return Components.interfaces.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + onCacheEntryAvailable: function(entry, isNew, appCache, status) { + cb(entry); + }, + get mainThreadOnly() { return true; } + }; + diskStorage.asyncOpenURI(Services.io.newURI(key, null, null), "", nsICacheStorage.OPEN_READONLY, checkCacheListener); +} + +function makeGeneralTab() +{ + var title = (gDocument.title) ? gBundle.getFormattedString("pageTitle", [gDocument.title]) : gBundle.getString("noPageTitle"); + document.getElementById("titletext").value = title; + + var url = gDocument.location.toString(); + setItemValue("urltext", url); + + var referrer = ("referrer" in gDocument && gDocument.referrer); + setItemValue("refertext", referrer); + + var mode = ("compatMode" in gDocument && gDocument.compatMode == "BackCompat") ? "generalQuirksMode" : "generalStrictMode"; + document.getElementById("modetext").value = gBundle.getString(mode); + + // find out the mime type + var mimeType = gDocument.contentType; + setItemValue("typetext", mimeType); + + // get the document characterset + var encoding = gDocument.characterSet; + document.getElementById("encodingtext").value = encoding; + + // get the meta tags + var metaNodes = gDocument.getElementsByTagName("meta"); + var length = metaNodes.length; + + var metaGroup = document.getElementById("metaTags"); + if (!length) + metaGroup.collapsed = true; + else { + var metaTagsCaption = document.getElementById("metaTagsCaption"); + if (length == 1) + metaTagsCaption.label = gBundle.getString("generalMetaTag"); + else + metaTagsCaption.label = gBundle.getFormattedString("generalMetaTags", [length]); + var metaTree = document.getElementById("metatree"); + metaTree.view = gMetaView; + + for (var i = 0; i < length; i++) + gMetaView.addRow([metaNodes[i].name || metaNodes[i].httpEquiv, metaNodes[i].content]); + + metaGroup.collapsed = false; + } + + // get the date of last modification + var modifiedText = formatDate(gDocument.lastModified, gStrings.notSet); + document.getElementById("modifiedtext").value = modifiedText; + + // get cache info + var cacheKey = url.replace(/#.*$/, ""); + openCacheEntry(cacheKey, function(cacheEntry) { + var sizeText; + if (cacheEntry) { + var pageSize = cacheEntry.dataSize; + var kbSize = formatNumber(Math.round(pageSize / 1024 * 100) / 100); + sizeText = gBundle.getFormattedString("generalSize", [kbSize, formatNumber(pageSize)]); + } + setItemValue("sizetext", sizeText); + }); + + securityOnLoad(); +} + +//******** Generic Build-a-tab +// Assumes the views are empty. Only called once to build the tabs, and +// does so by farming the task off to another thread via setTimeout(). +// The actual work is done with a TreeWalker that calls doGrab() once for +// each element node in the document. + +var gFrameList = [ ]; + +function makeTabs(aDocument, aWindow) +{ + goThroughFrames(aDocument, aWindow); + processFrames(); +} + +function goThroughFrames(aDocument, aWindow) +{ + gFrameList.push(aDocument); + if (aWindow && aWindow.frames.length > 0) { + var num = aWindow.frames.length; + for (var i = 0; i < num; i++) + goThroughFrames(aWindow.frames[i].document, aWindow.frames[i]); // recurse through the frames + } +} + +function processFrames() +{ + if (gFrameList.length) { + var doc = gFrameList[0]; + onProcessFrame.forEach(function(func) { func(doc); }); + var iterator = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT, grabAll); + gFrameList.shift(); + setTimeout(doGrab, 10, iterator); + onFinished.push(selectImage); + } + else + onFinished.forEach(function(func) { func(); }); +} + +function doGrab(iterator) +{ + for (var i = 0; i < 500; ++i) + if (!iterator.nextNode()) { + processFrames(); + return; + } + + setTimeout(doGrab, 10, iterator); +} + +function addImage(url, type, alt, elem, isBg) +{ + if (!url) + return; + + if (!gImageHash.hasOwnProperty(url)) + gImageHash[url] = { }; + if (!gImageHash[url].hasOwnProperty(type)) + gImageHash[url][type] = { }; + if (!gImageHash[url][type].hasOwnProperty(alt)) { + gImageHash[url][type][alt] = gImageView.data.length; + var row = [url, type, -1, alt, 1, elem, isBg]; + gImageView.addRow(row); + + // Fill in cache data asynchronously + openCacheEntry(url, function(cacheEntry) { + // The data at row[2] corresponds to the data size. + if (cacheEntry) { + row[2] = cacheEntry.dataSize; + // Invalidate the row to trigger a repaint. + gImageView.tree.invalidateRow(gImageView.data.indexOf(row)); + } + }); + + // Add the observer, only once. + if (gImageView.data.length == 1) { + document.getElementById("mediaTab").hidden = false; + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .addObserver(imagePermissionObserver, "perm-changed", false); + } + } + else { + var i = gImageHash[url][type][alt]; + gImageView.data[i][COL_IMAGE_COUNT]++; + if (elem == gImageElement) + gImageView.data[i][COL_IMAGE_NODE] = elem; + } +} + +function grabAll(elem) +{ + // check for images defined in CSS (e.g. background, borders), any node may have multiple + var computedStyle = elem.ownerDocument.defaultView.getComputedStyle(elem, ""); + + if (computedStyle) { + var addImgFunc = function (label, val) { + if (val.primitiveType == CSSPrimitiveValue.CSS_URI) { + addImage(val.getStringValue(), label, gStrings.notSet, elem, true); + } + else if (val.primitiveType == CSSPrimitiveValue.CSS_STRING) { + // This is for -moz-image-rect. + // TODO: Reimplement once bug 714757 is fixed + var strVal = val.getStringValue(); + if (strVal.search(/^.*url\(\"?/) > -1) { + url = strVal.replace(/^.*url\(\"?/,"").replace(/\"?\).*$/,""); + addImage(url, label, gStrings.notSet, elem, true); + } + } + else if (val.cssValueType == CSSValue.CSS_VALUE_LIST) { + // recursively resolve multiple nested CSS value lists + for (var i = 0; i < val.length; i++) + addImgFunc(label, val.item(i)); + } + }; + + addImgFunc(gStrings.mediaBGImg, computedStyle.getPropertyCSSValue("background-image")); + addImgFunc(gStrings.mediaBorderImg, computedStyle.getPropertyCSSValue("border-image-source")); + addImgFunc(gStrings.mediaListImg, computedStyle.getPropertyCSSValue("list-style-image")); + addImgFunc(gStrings.mediaCursor, computedStyle.getPropertyCSSValue("cursor")); + } + + // one swi^H^H^Hif-else to rule them all + if (elem instanceof HTMLImageElement) + addImage(elem.src, gStrings.mediaImg, + (elem.hasAttribute("alt")) ? elem.alt : gStrings.notSet, elem, false); + else if (elem instanceof SVGImageElement) { + try { + // Note: makeURLAbsolute will throw if either the baseURI is not a valid URI + // or the URI formed from the baseURI and the URL is not a valid URI + var href = makeURLAbsolute(elem.baseURI, elem.href.baseVal); + addImage(href, gStrings.mediaImg, "", elem, false); + } catch (e) { } + } + else if (elem instanceof HTMLVideoElement) { + addImage(elem.currentSrc, gStrings.mediaVideo, "", elem, false); + } + else if (elem instanceof HTMLAudioElement) { + addImage(elem.currentSrc, gStrings.mediaAudio, "", elem, false); + } + else if (elem instanceof HTMLLinkElement) { + if (elem.rel && /\bicon\b/i.test(elem.rel)) + addImage(elem.href, gStrings.mediaLink, "", elem, false); + } + else if (elem instanceof HTMLInputElement || elem instanceof HTMLButtonElement) { + if (elem.type.toLowerCase() == "image") + addImage(elem.src, gStrings.mediaInput, + (elem.hasAttribute("alt")) ? elem.alt : gStrings.notSet, elem, false); + } + else if (elem instanceof HTMLObjectElement) + addImage(elem.data, gStrings.mediaObject, getValueText(elem), elem, false); + else if (elem instanceof HTMLEmbedElement) + addImage(elem.src, gStrings.mediaEmbed, "", elem, false); + + onProcessElement.forEach(function(func) { func(elem); }); + + return NodeFilter.FILTER_ACCEPT; +} + +//******** Link Stuff +function openURL(target) +{ + var url = target.parentNode.childNodes[2].value; + window.open(url, "_blank", "chrome"); +} + +function onBeginLinkDrag(event,urlField,descField) +{ + if (event.originalTarget.localName != "treechildren") + return; + + var tree = event.target; + if (!("treeBoxObject" in tree)) + tree = tree.parentNode; + + var row = tree.treeBoxObject.getRowAt(event.clientX, event.clientY); + if (row == -1) + return; + + // Adding URL flavor + var col = tree.columns[urlField]; + var url = tree.view.getCellText(row, col); + col = tree.columns[descField]; + var desc = tree.view.getCellText(row, col); + + var dt = event.dataTransfer; + dt.setData("text/x-moz-url", url + "\n" + desc); + dt.setData("text/url-list", url); + dt.setData("text/plain", url); +} + +//******** Image Stuff +function getSelectedRows(tree) +{ + var start = { }; + var end = { }; + var numRanges = tree.view.selection.getRangeCount(); + + var rowArray = [ ]; + for (var t = 0; t < numRanges; t++) { + tree.view.selection.getRangeAt(t, start, end); + for (var v = start.value; v <= end.value; v++) + rowArray.push(v); + } + + return rowArray; +} + +function getSelectedRow(tree) +{ + var rows = getSelectedRows(tree); + return (rows.length == 1) ? rows[0] : -1; +} + +function selectSaveFolder(aCallback) +{ + const nsILocalFile = Components.interfaces.nsILocalFile; + const nsIFilePicker = Components.interfaces.nsIFilePicker; + let titleText = gBundle.getString("mediaSelectFolder"); + let fp = Components.classes["@mozilla.org/filepicker;1"]. + createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + aCallback(fp.file.QueryInterface(nsILocalFile)); + } else { + aCallback(null); + } + }; + + fp.init(window, titleText, nsIFilePicker.modeGetFolder); + fp.appendFilters(nsIFilePicker.filterAll); + try { + let prefs = Components.classes[PREFERENCES_CONTRACTID]. + getService(Components.interfaces.nsIPrefBranch); + let initialDir = prefs.getComplexValue("browser.download.dir", nsILocalFile); + if (initialDir) { + fp.displayDirectory = initialDir; + } + } catch (ex) { + } + fp.open(fpCallback); +} + +function saveMedia() +{ + var tree = document.getElementById("imagetree"); + var rowArray = getSelectedRows(tree); + if (rowArray.length == 1) { + var row = rowArray[0]; + var item = gImageView.data[row][COL_IMAGE_NODE]; + var url = gImageView.data[row][COL_IMAGE_ADDRESS]; + + if (url) { + var titleKey = "SaveImageTitle"; + + if (item instanceof HTMLVideoElement) + titleKey = "SaveVideoTitle"; + else if (item instanceof HTMLAudioElement) + titleKey = "SaveAudioTitle"; + + saveURL(url, null, titleKey, false, false, makeURI(item.baseURI), gDocument); + } + } else { + selectSaveFolder(function(aDirectory) { + if (aDirectory) { + var saveAnImage = function(aURIString, aChosenData, aBaseURI) { + internalSave(aURIString, null, null, null, null, false, "SaveImageTitle", + aChosenData, aBaseURI, gDocument); + }; + + for (var i = 0; i < rowArray.length; i++) { + var v = rowArray[i]; + var dir = aDirectory.clone(); + var item = gImageView.data[v][COL_IMAGE_NODE]; + var uriString = gImageView.data[v][COL_IMAGE_ADDRESS]; + var uri = makeURI(uriString); + + try { + uri.QueryInterface(Components.interfaces.nsIURL); + dir.append(decodeURIComponent(uri.fileName)); + } catch(ex) { + /* data: uris */ + } + + if (i == 0) { + saveAnImage(uriString, new AutoChosen(dir, uri), makeURI(item.baseURI)); + } else { + // This delay is a hack which prevents the download manager + // from opening many times. See bug 377339. + setTimeout(saveAnImage, 200, uriString, new AutoChosen(dir, uri), + makeURI(item.baseURI)); + } + } + } + }); + } +} + +function onBlockImage() +{ + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + + var checkbox = document.getElementById("blockImage"); + var uri = makeURI(document.getElementById("imageurltext").value); + if (checkbox.checked) + permissionManager.add(uri, "image", nsIPermissionManager.DENY_ACTION); + else + permissionManager.remove(uri, "image"); +} + +function onImageSelect() +{ + var previewBox = document.getElementById("mediaPreviewBox"); + var mediaSaveBox = document.getElementById("mediaSaveBox"); + var splitter = document.getElementById("mediaSplitter"); + var tree = document.getElementById("imagetree"); + var count = tree.view.selection.count; + if (count == 0) { + previewBox.collapsed = true; + mediaSaveBox.collapsed = true; + splitter.collapsed = true; + tree.flex = 1; + } + else if (count > 1) { + splitter.collapsed = true; + previewBox.collapsed = true; + mediaSaveBox.collapsed = false; + tree.flex = 1; + } + else { + mediaSaveBox.collapsed = true; + splitter.collapsed = false; + previewBox.collapsed = false; + tree.flex = 0; + makePreview(getSelectedRows(tree)[0]); + } +} + +function makePreview(row) +{ + var imageTree = document.getElementById("imagetree"); + var item = gImageView.data[row][COL_IMAGE_NODE]; + var url = gImageView.data[row][COL_IMAGE_ADDRESS]; + var isBG = gImageView.data[row][COL_IMAGE_BG]; + var isAudio = false; + + setItemValue("imageurltext", url); + + var imageText; + if (!isBG && + !(item instanceof SVGImageElement) && + !(gDocument instanceof ImageDocument)) { + imageText = item.title || item.alt; + + if (!imageText && !(item instanceof HTMLImageElement)) + imageText = getValueText(item); + } + setItemValue("imagetext", imageText); + + setItemValue("imagelongdesctext", item.longDesc); + + // get cache info + var cacheKey = url.replace(/#.*$/, ""); + openCacheEntry(cacheKey, function(cacheEntry) { + // find out the file size + var sizeText; + if (cacheEntry) { + var imageSize = cacheEntry.dataSize; + var kbSize = Math.round(imageSize / 1024 * 100) / 100; + sizeText = gBundle.getFormattedString("generalSize", + [formatNumber(kbSize), formatNumber(imageSize)]); + } + else + sizeText = gBundle.getString("mediaUnknownNotCached"); + setItemValue("imagesizetext", sizeText); + + var mimeType; + var numFrames = 1; + if (item instanceof HTMLObjectElement || + item instanceof HTMLEmbedElement || + item instanceof HTMLLinkElement) + mimeType = item.type; + + if (!mimeType && !isBG && item instanceof nsIImageLoadingContent) { + var imageRequest = item.getRequest(nsIImageLoadingContent.CURRENT_REQUEST); + if (imageRequest) { + mimeType = imageRequest.mimeType; + var image = imageRequest.image; + if (image) + numFrames = image.numFrames; + } + } + + if (!mimeType) + mimeType = getContentTypeFromHeaders(cacheEntry); + + // if we have a data url, get the MIME type from the url + if (!mimeType && url.startsWith("data:")) { + let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url); + if (dataMimeType) + mimeType = dataMimeType[1].toLowerCase(); + } + + var imageType; + if (mimeType) { + // We found the type, try to display it nicely + let imageMimeType = /^image\/(.*)/i.exec(mimeType); + if (imageMimeType) { + imageType = imageMimeType[1].toUpperCase(); + if (numFrames > 1) + imageType = gBundle.getFormattedString("mediaAnimatedImageType", + [imageType, numFrames]); + else + imageType = gBundle.getFormattedString("mediaImageType", [imageType]); + } + else { + // the MIME type doesn't begin with image/, display the raw type + imageType = mimeType; + } + } + else { + // We couldn't find the type, fall back to the value in the treeview + imageType = gImageView.data[row][COL_IMAGE_TYPE]; + } + setItemValue("imagetypetext", imageType); + + var imageContainer = document.getElementById("theimagecontainer"); + var oldImage = document.getElementById("thepreviewimage"); + + var isProtocolAllowed = checkProtocol(gImageView.data[row]); + + var newImage = new Image; + newImage.id = "thepreviewimage"; + var physWidth = 0, physHeight = 0; + var width = 0, height = 0; + + if ((item instanceof HTMLLinkElement || item instanceof HTMLInputElement || + item instanceof HTMLImageElement || + item instanceof SVGImageElement || + (item instanceof HTMLObjectElement && mimeType && mimeType.startsWith("image/")) || isBG) && isProtocolAllowed) { + newImage.setAttribute("src", url); + physWidth = newImage.width || 0; + physHeight = newImage.height || 0; + + // "width" and "height" attributes must be set to newImage, + // even if there is no "width" or "height attribute in item; + // otherwise, the preview image cannot be displayed correctly. + if (!isBG) { + newImage.width = ("width" in item && item.width) || newImage.naturalWidth; + newImage.height = ("height" in item && item.height) || newImage.naturalHeight; + } + else { + // the Width and Height of an HTML tag should not be used for its background image + // (for example, "table" can have "width" or "height" attributes) + newImage.width = newImage.naturalWidth; + newImage.height = newImage.naturalHeight; + } + + if (item instanceof SVGImageElement) { + newImage.width = item.width.baseVal.value; + newImage.height = item.height.baseVal.value; + } + + width = newImage.width; + height = newImage.height; + + document.getElementById("theimagecontainer").collapsed = false + document.getElementById("brokenimagecontainer").collapsed = true; + } + else if (item instanceof HTMLVideoElement && isProtocolAllowed) { + newImage = document.createElementNS("http://www.w3.org/1999/xhtml", "video"); + newImage.id = "thepreviewimage"; + newImage.src = url; + newImage.controls = true; + width = physWidth = item.videoWidth; + height = physHeight = item.videoHeight; + + document.getElementById("theimagecontainer").collapsed = false; + document.getElementById("brokenimagecontainer").collapsed = true; + } + else if (item instanceof HTMLAudioElement && isProtocolAllowed) { + newImage = new Audio; + newImage.id = "thepreviewimage"; + newImage.src = url; + newImage.controls = true; + isAudio = true; + + document.getElementById("theimagecontainer").collapsed = false; + document.getElementById("brokenimagecontainer").collapsed = true; + } + else { + // fallback image for protocols not allowed (e.g., javascript:) + // or elements not [yet] handled (e.g., object, embed). + document.getElementById("brokenimagecontainer").collapsed = false; + document.getElementById("theimagecontainer").collapsed = true; + } + + var imageSize = ""; + if (url && !isAudio) { + if (width != physWidth || height != physHeight) { + imageSize = gBundle.getFormattedString("mediaDimensionsScaled", + [formatNumber(physWidth), + formatNumber(physHeight), + formatNumber(width), + formatNumber(height)]); + } + else { + imageSize = gBundle.getFormattedString("mediaDimensions", + [formatNumber(width), + formatNumber(height)]); + } + } + setItemValue("imagedimensiontext", imageSize); + + makeBlockImage(url); + + imageContainer.removeChild(oldImage); + imageContainer.appendChild(newImage); + + onImagePreviewShown.forEach(function(func) { func(); }); + }); +} + +function makeBlockImage(url) +{ + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + var prefs = Components.classes[PREFERENCES_CONTRACTID] + .getService(Components.interfaces.nsIPrefBranch); + + var checkbox = document.getElementById("blockImage"); + var imagePref = prefs.getIntPref("permissions.default.image"); + if (!(/^https?:/.test(url)) || imagePref == 2) + // We can't block the images from this host because either is is not + // for http(s) or we don't load images at all + checkbox.hidden = true; + else { + var uri = makeURI(url); + if (uri.host) { + checkbox.hidden = false; + checkbox.label = gBundle.getFormattedString("mediaBlockImage", [uri.host]); + var perm = permissionManager.testPermission(uri, "image"); + checkbox.checked = perm == nsIPermissionManager.DENY_ACTION; + } + else + checkbox.hidden = true; + } +} + +var imagePermissionObserver = { + observe: function (aSubject, aTopic, aData) + { + if (document.getElementById("mediaPreviewBox").collapsed) + return; + + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Components.interfaces.nsIPermission); + if (permission.type == "image") { + var imageTree = document.getElementById("imagetree"); + var row = getSelectedRow(imageTree); + var item = gImageView.data[row][COL_IMAGE_NODE]; + var url = gImageView.data[row][COL_IMAGE_ADDRESS]; + if (permission.matchesURI(makeURI(url), true)) { + makeBlockImage(url); + } + } + } + } +} + +function getContentTypeFromHeaders(cacheEntryDescriptor) +{ + if (!cacheEntryDescriptor) + return null; + + return (/^Content-Type:\s*(.*?)\s*(?:\;|$)/mi + .exec(cacheEntryDescriptor.getMetaDataElement("response-head")))[1]; +} + +//******** Other Misc Stuff +// Modified from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html +// parse a node to extract the contents of the node +function getValueText(node) +{ + var valueText = ""; + + // form input elements don't generally contain information that is useful to our callers, so return nothing + if (node instanceof HTMLInputElement || + node instanceof HTMLSelectElement || + node instanceof HTMLTextAreaElement) + return valueText; + + // otherwise recurse for each child + var length = node.childNodes.length; + for (var i = 0; i < length; i++) { + var childNode = node.childNodes[i]; + var nodeType = childNode.nodeType; + + // text nodes are where the goods are + if (nodeType == Node.TEXT_NODE) + valueText += " " + childNode.nodeValue; + // and elements can have more text inside them + else if (nodeType == Node.ELEMENT_NODE) { + // images are special, we want to capture the alt text as if the image weren't there + if (childNode instanceof HTMLImageElement) + valueText += " " + getAltText(childNode); + else + valueText += " " + getValueText(childNode); + } + } + + return stripWS(valueText); +} + +// Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html +// traverse the tree in search of an img or area element and grab its alt tag +function getAltText(node) +{ + var altText = ""; + + if (node.alt) + return node.alt; + var length = node.childNodes.length; + for (var i = 0; i < length; i++) + if ((altText = getAltText(node.childNodes[i]) != undefined)) // stupid js warning... + return altText; + return ""; +} + +// Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html +// strip leading and trailing whitespace, and replace multiple consecutive whitespace characters with a single space +function stripWS(text) +{ + var middleRE = /\s+/g; + var endRE = /(^\s+)|(\s+$)/g; + + text = text.replace(middleRE, " "); + return text.replace(endRE, ""); +} + +function setItemValue(id, value) +{ + var item = document.getElementById(id); + if (value) { + item.parentNode.collapsed = false; + item.value = value; + } + else + item.parentNode.collapsed = true; +} + +function formatNumber(number) +{ + return (+number).toLocaleString(); // coerce number to a numeric value before calling toLocaleString() +} + +function formatDate(datestr, unknown) +{ + // scriptable date formatter, for pretty printing dates + var dateService = Components.classes["@mozilla.org/intl/scriptabledateformat;1"] + .getService(Components.interfaces.nsIScriptableDateFormat); + + var date = new Date(datestr); + if (!date.valueOf()) + return unknown; + + return dateService.FormatDateTime("", dateService.dateFormatLong, + dateService.timeFormatSeconds, + date.getFullYear(), date.getMonth()+1, date.getDate(), + date.getHours(), date.getMinutes(), date.getSeconds()); +} + +function doCopy() +{ + if (!gClipboardHelper) + return; + + var elem = document.commandDispatcher.focusedElement; + + if (elem && "treeBoxObject" in elem) { + var view = elem.view; + var selection = view.selection; + var text = [], tmp = ''; + var min = {}, max = {}; + + var count = selection.getRangeCount(); + + for (var i = 0; i < count; i++) { + selection.getRangeAt(i, min, max); + + for (var row = min.value; row <= max.value; row++) { + view.performActionOnRow("copy", row); + + tmp = elem.getAttribute("copybuffer"); + if (tmp) + text.push(tmp); + elem.removeAttribute("copybuffer"); + } + } + gClipboardHelper.copyString(text.join("\n"), document); + } +} + +function doSelectAll() +{ + var elem = document.commandDispatcher.focusedElement; + + if (elem && "treeBoxObject" in elem) + elem.view.selection.selectAll(); +} + +function selectImage() +{ + if (!gImageElement) + return; + + var tree = document.getElementById("imagetree"); + for (var i = 0; i < tree.view.rowCount; i++) { + if (gImageElement == gImageView.data[i][COL_IMAGE_NODE] && + !gImageView.data[i][COL_IMAGE_BG]) { + tree.view.selection.select(i); + tree.treeBoxObject.ensureRowIsVisible(i); + tree.focus(); + return; + } + } +} + +function checkProtocol(img) +{ + var url = img[COL_IMAGE_ADDRESS]; + return /^data:image\//i.test(url) || + /^(https?|ftp|file|about|chrome|resource):/.test(url); +} diff --git a/browser/components/pageinfo/pageInfo.xml b/browser/components/pageinfo/pageInfo.xml new file mode 100644 index 000000000..20d330046 --- /dev/null +++ b/browser/components/pageinfo/pageInfo.xml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<bindings id="pageInfoBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <!-- based on preferences.xml paneButton --> + <binding id="viewbutton" extends="chrome://global/content/bindings/radio.xml#radio"> + <content> + <xul:image class="viewButtonIcon" xbl:inherits="src"/> + <xul:label class="viewButtonLabel" xbl:inherits="value=label"/> + </content> + <implementation implements="nsIAccessibleProvider"> + <property name="accessibleType" readonly="true"> + <getter> + <![CDATA[ + return Components.interfaces.nsIAccessibleProvider.XULListitem; + ]]> + </getter> + </property> + </implementation> + </binding> + +</bindings> diff --git a/browser/components/pageinfo/pageInfo.xul b/browser/components/pageinfo/pageInfo.xul new file mode 100644 index 000000000..35f331ab6 --- /dev/null +++ b/browser/components/pageinfo/pageInfo.xul @@ -0,0 +1,495 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://browser/content/pageinfo/pageInfo.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/pageInfo.css" type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % pageInfoDTD SYSTEM "chrome://browser/locale/pageInfo.dtd"> + %pageInfoDTD; +]> + +<window id="main-window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + windowtype="Browser:page-info" + onload="onLoadPageInfo()" + onunload="onUnloadPageInfo()" + align="stretch" + screenX="10" screenY="10" + width="&pageInfoWindow.width;" height="&pageInfoWindow.height;" + persist="screenX screenY width height sizemode"> + + <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + <script type="application/javascript" src="chrome://global/content/treeUtils.js"/> + <script type="application/javascript" src="chrome://browser/content/pageinfo/pageInfo.js"/> + <script type="application/javascript" src="chrome://browser/content/pageinfo/feeds.js"/> + <script type="application/javascript" src="chrome://browser/content/pageinfo/permissions.js"/> + <script type="application/javascript" src="chrome://browser/content/pageinfo/security.js"/> + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + + <stringbundleset id="pageinfobundleset"> + <stringbundle id="pageinfobundle" src="chrome://browser/locale/pageInfo.properties"/> + <stringbundle id="pkiBundle" src="chrome://pippki/locale/pippki.properties"/> + <stringbundle id="browserBundle" src="chrome://browser/locale/browser.properties"/> + </stringbundleset> + + <commandset id="pageInfoCommandSet"> + <command id="cmd_close" oncommand="window.close();"/> + <command id="cmd_help" oncommand="doHelpButton();"/> + <command id="cmd_copy" oncommand="doCopy();"/> + <command id="cmd_selectall" oncommand="doSelectAll();"/> + + <!-- permissions tab --> + <command id="cmd_imageDef" oncommand="onCheckboxClick('image');"/> + <command id="cmd_popupDef" oncommand="onCheckboxClick('popup');"/> + <command id="cmd_cookieDef" oncommand="onCheckboxClick('cookie');"/> + <command id="cmd_desktop-notificationDef" oncommand="onCheckboxClick('desktop-notification');"/> + <command id="cmd_installDef" oncommand="onCheckboxClick('install');"/> + <command id="cmd_geoDef" oncommand="onCheckboxClick('geo');"/> + <command id="cmd_pluginsDef" oncommand="onCheckboxClick('plugins');"/> + <command id="cmd_imageToggle" oncommand="onRadioClick('image');"/> + <command id="cmd_popupToggle" oncommand="onRadioClick('popup');"/> + <command id="cmd_cookieToggle" oncommand="onRadioClick('cookie');"/> + <command id="cmd_desktop-notificationToggle" oncommand="onRadioClick('desktop-notification');"/> + <command id="cmd_installToggle" oncommand="onRadioClick('install');"/> + <command id="cmd_geoToggle" oncommand="onRadioClick('geo');"/> + <command id="cmd_pluginsToggle" oncommand="onPluginRadioClick(event);"/> + </commandset> + + <keyset id="pageInfoKeySet"> + <key key="&closeWindow.key;" modifiers="accel" command="cmd_close"/> + <key keycode="VK_ESCAPE" command="cmd_close"/> + <key keycode="VK_F1" command="cmd_help"/> + <key key="©.key;" modifiers="accel" command="cmd_copy"/> + <key key="&selectall.key;" modifiers="accel" command="cmd_selectall"/> + <key key="&selectall.key;" modifiers="alt" command="cmd_selectall"/> + </keyset> + + <menupopup id="picontext"> + <menuitem id="menu_selectall" label="&selectall.label;" command="cmd_selectall" accesskey="&selectall.accesskey;"/> + <menuitem id="menu_copy" label="©.label;" command="cmd_copy" accesskey="©.accesskey;"/> + </menupopup> + + <windowdragbox id="topBar" class="viewGroupWrapper"> + <radiogroup id="viewGroup" class="chromeclass-toolbar" orient="horizontal"> + <radio id="generalTab" label="&generalTab;" accesskey="&generalTab.accesskey;" + oncommand="showTab('general');"/> + <radio id="mediaTab" label="&mediaTab;" accesskey="&mediaTab.accesskey;" + oncommand="showTab('media');" hidden="true"/> + <radio id="feedTab" label="&feedTab;" accesskey="&feedTab.accesskey;" + oncommand="showTab('feed');" hidden="true"/> + <radio id="permTab" label="&permTab;" accesskey="&permTab.accesskey;" + oncommand="showTab('perm');"/> + <radio id="securityTab" label="&securityTab;" accesskey="&securityTab.accesskey;" + oncommand="showTab('security');"/> + <!-- Others added by overlay --> + </radiogroup> + </windowdragbox> + + <deck id="mainDeck" flex="1"> + <!-- General page information --> + <vbox id="generalPanel"> + <textbox class="header" readonly="true" id="titletext"/> + <grid id="generalGrid"> + <columns> + <column/> + <column class="gridSeparator"/> + <column flex="1"/> + </columns> + <rows id="generalRows"> + <row id="generalURLRow"> + <label control="urltext" value="&generalURL;"/> + <separator/> + <textbox readonly="true" id="urltext"/> + </row> + <row id="generalSeparatorRow1"> + <separator class="thin"/> + </row> + <row id="generalTypeRow"> + <label control="typetext" value="&generalType;"/> + <separator/> + <textbox readonly="true" id="typetext"/> + </row> + <row id="generalModeRow"> + <label control="modetext" value="&generalMode;"/> + <separator/> + <textbox readonly="true" crop="end" id="modetext"/> + </row> + <row id="generalEncodingRow"> + <label control="encodingtext" value="&generalEncoding;"/> + <separator/> + <textbox readonly="true" id="encodingtext"/> + </row> + <row id="generalSizeRow"> + <label control="sizetext" value="&generalSize;"/> + <separator/> + <textbox readonly="true" id="sizetext"/> + </row> + <row id="generalReferrerRow"> + <label control="refertext" value="&generalReferrer;"/> + <separator/> + <textbox readonly="true" id="refertext"/> + </row> + <row id="generalSeparatorRow2"> + <separator class="thin"/> + </row> + <row id="generalModifiedRow"> + <label control="modifiedtext" value="&generalModified;"/> + <separator/> + <textbox readonly="true" id="modifiedtext"/> + </row> + </rows> + </grid> + <separator class="thin"/> + <groupbox id="metaTags" flex="1" class="collapsable treebox"> + <caption id="metaTagsCaption" onclick="toggleGroupbox('metaTags');"/> + <tree id="metatree" flex="1" hidecolumnpicker="true" contextmenu="picontext"> + <treecols> + <treecol id="meta-name" label="&generalMetaName;" + persist="width" flex="1" + onclick="gMetaView.onPageMediaSort('meta-name');"/> + <splitter class="tree-splitter"/> + <treecol id="meta-content" label="&generalMetaContent;" + persist="width" flex="4" + onclick="gMetaView.onPageMediaSort('meta-content');"/> + </treecols> + <treechildren id="metatreechildren" flex="1"/> + </tree> + </groupbox> + <groupbox id="securityBox"> + <caption id="securityBoxCaption" label="&securityHeader;"/> + <description id="general-security-identity" class="header"/> + <description id="general-security-privacy" class="header"/> + <hbox id="securityDetailsButtonBox" align="right"> + <button id="security-view-details" label="&generalSecurityDetails;" + accesskey="&generalSecurityDetails.accesskey;" + oncommand="onClickMore();"/> + </hbox> + </groupbox> + </vbox> + + <!-- Media information --> + <vbox id="mediaPanel"> + <tree id="imagetree" onselect="onImageSelect();" contextmenu="picontext" + ondragstart="onBeginLinkDrag(event,'image-address','image-alt')"> + <treecols> + <treecol sortSeparators="true" primary="true" persist="width" flex="10" + width="10" id="image-address" label="&mediaAddress;" + onclick="gImageView.onPageMediaSort('image-address');"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="2" + width="2" id="image-type" label="&mediaType;" + onclick="gImageView.onPageMediaSort('image-type');"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="2" + width="2" id="image-size" label="&mediaSize;" value="size" + onclick="gImageView.onPageMediaSort('image-size');"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="4" + width="4" id="image-alt" label="&mediaAltHeader;" + onclick="gImageView.onPageMediaSort('image-alt');"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="1" + width="1" id="image-count" label="&mediaCount;" + onclick="gImageView.onPageMediaSort('image-count');"/> + </treecols> + <treechildren id="imagetreechildren" flex="1"/> + </tree> + <splitter orient="vertical" id="mediaSplitter"/> + <vbox flex="1" id="mediaPreviewBox" collapsed="true"> + <grid id="mediaGrid"> + <columns> + <column id="mediaLabelColumn"/> + <column class="gridSeparator"/> + <column flex="1"/> + </columns> + <rows id="mediaRows"> + <row id="mediaLocationRow"> + <label control="imageurltext" value="&mediaLocation;"/> + <separator/> + <textbox readonly="true" id="imageurltext"/> + </row> + <row id="mediaTypeRow"> + <label control="imagetypetext" value="&generalType;"/> + <separator/> + <textbox readonly="true" id="imagetypetext"/> + </row> + <row id="mediaSizeRow"> + <label control="imagesizetext" value="&generalSize;"/> + <separator/> + <textbox readonly="true" id="imagesizetext"/> + </row> + <row id="mediaDimensionRow"> + <label control="imagedimensiontext" value="&mediaDimension;"/> + <separator/> + <textbox readonly="true" id="imagedimensiontext"/> + </row> + <row id="mediaTextRow"> + <label control="imagetext" value="&mediaText;"/> + <separator/> + <textbox readonly="true" id="imagetext"/> + </row> + <row id="mediaLongdescRow"> + <label control="imagelongdesctext" value="&mediaLongdesc;"/> + <separator/> + <textbox readonly="true" id="imagelongdesctext"/> + </row> + </rows> + </grid> + <hbox id="imageSaveBox" align="end"> + <vbox id="blockImageBox"> + <checkbox id="blockImage" hidden="true" oncommand="onBlockImage()" + accesskey="&mediaBlockImage.accesskey;"/> + <label control="thepreviewimage" value="&mediaPreview;" class="header"/> + </vbox> + <spacer id="imageSaveBoxSpacer" flex="1"/> + <button label="&mediaSaveAs;" accesskey="&mediaSaveAs.accesskey;" + icon="save" id="imagesaveasbutton" + oncommand="saveMedia();"/> + </hbox> + <vbox id="imagecontainerbox" class="inset iframe" flex="1" pack="center"> + <hbox id="theimagecontainer" pack="center"> + <image id="thepreviewimage"/> + </hbox> + <hbox id="brokenimagecontainer" pack="center" collapsed="true"> + <image id="brokenimage" src="resource://gre-resources/broken-image.png"/> + </hbox> + </vbox> + </vbox> + <hbox id="mediaSaveBox" collapsed="true"> + <spacer id="mediaSaveBoxSpacer" flex="1"/> + <button label="&mediaSaveAs;" accesskey="&mediaSaveAs2.accesskey;" + icon="save" id="mediasaveasbutton" + oncommand="saveMedia();"/> + </hbox> + </vbox> + + <!-- Feeds --> + <vbox id="feedPanel"> + <richlistbox id="feedListbox" flex="1"/> + </vbox> + + <!-- Permissions --> + <vbox id="permPanel"> + <hbox id="permHostBox"> + <label value="&permissionsFor;" control="hostText" /> + <textbox id="hostText" class="header" readonly="true" + crop="end" flex="1"/> + </hbox> + + <vbox id="permList" flex="1"> + <vbox class="permission" id="permImageRow"> + <label class="permissionLabel" id="permImageLabel" + value="&permImage;" control="imageRadioGroup"/> + <hbox id="permImageBox" role="group" aria-labelledby="permImageLabel"> + <checkbox id="imageDef" command="cmd_imageDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="imageRadioGroup" orient="horizontal"> + <radio id="image#1" command="cmd_imageToggle" label="&permAllow;"/> + <radio id="image#2" command="cmd_imageToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permPopupRow"> + <label class="permissionLabel" id="permPopupLabel" + value="&permPopup;" control="popupRadioGroup"/> + <hbox id="permPopupBox" role="group" aria-labelledby="permPopupLabel"> + <checkbox id="popupDef" command="cmd_popupDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="popupRadioGroup" orient="horizontal"> + <radio id="popup#1" command="cmd_popupToggle" label="&permAllow;"/> + <radio id="popup#2" command="cmd_popupToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permCookieRow"> + <label class="permissionLabel" id="permCookieLabel" + value="&permCookie;" control="cookieRadioGroup"/> + <hbox id="permCookieBox" role="group" aria-labelledby="permCookieLabel"> + <checkbox id="cookieDef" command="cmd_cookieDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="cookieRadioGroup" orient="horizontal"> + <radio id="cookie#1" command="cmd_cookieToggle" label="&permAllow;"/> + <radio id="cookie#8" command="cmd_cookieToggle" label="&permAllowSession;"/> + <radio id="cookie#9" command="cmd_cookieToggle" label="&permAllowFirstPartyOnly;"/> + <radio id="cookie#2" command="cmd_cookieToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permNotificationRow"> + <label class="permissionLabel" id="permNotificationLabel" + value="&permNotifications;" control="desktop-notificationRadioGroup"/> + <hbox role="group" aria-labelledby="permNotificationLabel"> + <checkbox id="desktop-notificationDef" command="cmd_desktop-notificationDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="desktop-notificationRadioGroup" orient="horizontal"> + <radio id="desktop-notification#0" command="cmd_desktop-notificationToggle" label="&permAskAlways;"/> + <radio id="desktop-notification#1" command="cmd_desktop-notificationToggle" label="&permAllow;"/> + <radio id="desktop-notification#2" command="cmd_desktop-notificationToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permInstallRow"> + <label class="permissionLabel" id="permInstallLabel" + value="&permInstall;" control="installRadioGroup"/> + <hbox id="permInstallBox" role="group" aria-labelledby="permInstallLabel"> + <checkbox id="installDef" command="cmd_installDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="installRadioGroup" orient="horizontal"> + <radio id="install#1" command="cmd_installToggle" label="&permAllow;"/> + <radio id="install#2" command="cmd_installToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permGeoRow" > + <label class="permissionLabel" id="permGeoLabel" + value="&permGeo;" control="geoRadioGroup"/> + <hbox id="permGeoBox" role="group" aria-labelledby="permGeoLabel"> + <checkbox id="geoDef" command="cmd_geoDef" label="&permAskAlways;"/> + <spacer flex="1"/> + <radiogroup id="geoRadioGroup" orient="horizontal"> + <radio id="geo#1" command="cmd_geoToggle" label="&permAllow;"/> + <radio id="geo#2" command="cmd_geoToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permPluginsRow"> + <label class="permissionLabel" id="permPluginsLabel" + value="&permPlugins;" control="pluginsRadioGroup"/> + <hbox id="permPluginTemplate" role="group" aria-labelledby="permPluginsLabel" align="baseline"> + <label class="permPluginTemplateLabel"/> + <spacer flex="1"/> + <radiogroup class="permPluginTemplateRadioGroup" orient="horizontal" command="cmd_pluginsToggle"> + <radio class="permPluginTemplateRadioDefault" label="&permUseDefault;"/> + <radio class="permPluginTemplateRadioAsk" label="&permAskAlways;"/> + <radio class="permPluginTemplateRadioAllow" label="&permAllow;"/> + <radio class="permPluginTemplateRadioBlock" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + </vbox> + </vbox> + + <!-- Security & Privacy --> + <vbox id="securityPanel"> + <!-- Identity Section --> + <groupbox id="security-identity-groupbox" flex="1"> + <caption id="security-identity" label="&securityView.identity.header;"/> + <grid id="security-identity-grid" flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows id="security-identity-rows"> + <!-- Domain --> + <row id="security-identity-domain-row"> + <label id="security-identity-domain-label" + class="fieldLabel" + value="&securityView.identity.domain;" + control="security-identity-domain-value"/> + <textbox id="security-identity-domain-value" + class="fieldValue" readonly="true"/> + </row> + <!-- Owner --> + <row id="security-identity-owner-row"> + <label id="security-identity-owner-label" + class="fieldLabel" + value="&securityView.identity.owner;" + control="security-identity-owner-value"/> + <textbox id="security-identity-owner-value" + class="fieldValue" readonly="true"/> + </row> + <!-- Verifier --> + <row id="security-identity-verifier-row"> + <label id="security-identity-verifier-label" + class="fieldLabel" + value="&securityView.identity.verifier;" + control="security-identity-verifier-value"/> + <textbox id="security-identity-verifier-value" + class="fieldValue" readonly="true" /> + </row> + </rows> + </grid> + <spacer flex="1"/> + <!-- Cert button --> + <hbox id="security-view-cert-box" pack="end"> + <button id="security-view-cert" label="&securityView.certView;" + accesskey="&securityView.accesskey;" + oncommand="security.viewCert();"/> + </hbox> + </groupbox> + + <!-- Privacy & History section --> + <groupbox id="security-privacy-groupbox" flex="1"> + <caption id="security-privacy" label="&securityView.privacy.header;" /> + <grid id="security-privacy-grid"> + <columns> + <column flex="1"/> + <column flex="1"/> + </columns> + <rows id="security-privacy-rows"> + <!-- History --> + <row id="security-privacy-history-row"> + <label id="security-privacy-history-label" + control="security-privacy-history-value" + class="fieldLabel">&securityView.privacy.history;</label> + <textbox id="security-privacy-history-value" + class="fieldValue" + value="&securityView.unknown;" + readonly="true"/> + </row> + <!-- Cookies --> + <row id="security-privacy-cookies-row"> + <label id="security-privacy-cookies-label" + control="security-privacy-cookies-value" + class="fieldLabel">&securityView.privacy.cookies;</label> + <hbox id="security-privacy-cookies-box" align="center"> + <textbox id="security-privacy-cookies-value" + class="fieldValue" + value="&securityView.unknown;" + flex="1" + readonly="true"/> + <button id="security-view-cookies" + label="&securityView.privacy.viewCookies;" + accesskey="&securityView.privacy.viewCookies.accessKey;" + oncommand="security.viewCookies();"/> + </hbox> + </row> + <!-- Passwords --> + <row id="security-privacy-passwords-row"> + <label id="security-privacy-passwords-label" + control="security-privacy-passwords-value" + class="fieldLabel">&securityView.privacy.passwords;</label> + <hbox id="security-privacy-passwords-box" align="center"> + <textbox id="security-privacy-passwords-value" + class="fieldValue" + value="&securityView.unknown;" + flex="1" + readonly="true"/> + <button id="security-view-password" + label="&securityView.privacy.viewPasswords;" + accesskey="&securityView.privacy.viewPasswords.accessKey;" + oncommand="security.viewPasswords();"/> + </hbox> + </row> + </rows> + </grid> + </groupbox> + + <!-- Technical Details section --> + <groupbox id="security-technical-groupbox" flex="1"> + <caption id="security-technical" label="&securityView.technical.header;" /> + <vbox id="security-technical-box" flex="1"> + <label id="security-technical-shortform" class="fieldValue"/> + <description id="security-technical-longform1" class="fieldLabel"/> + <description id="security-technical-longform2" class="fieldLabel"/> + </vbox> + </groupbox> + </vbox> + <!-- Others added by overlay --> + </deck> + +</window> diff --git a/browser/components/pageinfo/permissions.js b/browser/components/pageinfo/permissions.js new file mode 100644 index 000000000..c9e999971 --- /dev/null +++ b/browser/components/pageinfo/permissions.js @@ -0,0 +1,341 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 UNKNOWN = nsIPermissionManager.UNKNOWN_ACTION; // 0 +const ALLOW = nsIPermissionManager.ALLOW_ACTION; // 1 +const DENY = nsIPermissionManager.DENY_ACTION; // 2 +const SESSION = nsICookiePermission.ACCESS_SESSION; // 8 + +const IMAGE_DENY = 2; + +const COOKIE_DENY = 2; +const COOKIE_SESSION = 2; + +var gPermURI; +var gPermPrincipal; +var gPrefs; +var gUsageRequest; + +var gPermObj = { + image: function() + { + if (gPrefs.getIntPref("permissions.default.image") == IMAGE_DENY) { + return DENY; + } + return ALLOW; + }, + popup: function() + { + if (gPrefs.getBoolPref("dom.disable_open_during_load")) { + return DENY; + } + return ALLOW; + }, + cookie: function() + { + if (gPrefs.getIntPref("network.cookie.cookieBehavior") == COOKIE_DENY) { + return DENY; + } + if (gPrefs.getIntPref("network.cookie.lifetimePolicy") == COOKIE_SESSION) { + return SESSION; + } + return ALLOW; + }, + "desktop-notification": function() + { + if (!gPrefs.getBoolPref("dom.webnotifications.enabled")) { + return DENY; + } + return UNKNOWN; + }, + install: function() + { + if (Services.prefs.getBoolPref("xpinstall.whitelist.required")) { + return DENY; + } + return ALLOW; + }, + geo: function() + { + if (!gPrefs.getBoolPref("geo.enabled")) { + return DENY; + } + return ALLOW; + }, + plugins: function() + { + return UNKNOWN; + }, +}; + +var permissionObserver = { + observe: function(aSubject, aTopic, aData) + { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface( + Components.interfaces.nsIPermission); + if (permission.matchesURI(gPermURI, true)) { + if (permission.type in gPermObj) + initRow(permission.type); + else if (permission.type.startsWith("plugin")) + setPluginsRadioState(); + } + } + } +}; + +function onLoadPermission(principal) +{ + gPrefs = Components.classes[PREFERENCES_CONTRACTID] + .getService(Components.interfaces.nsIPrefBranch); + + var uri = gDocument.documentURIObject; + var permTab = document.getElementById("permTab"); + if (/^https?$/.test(uri.scheme)) { + gPermURI = uri; + gPermPrincipal = principal; + var hostText = document.getElementById("hostText"); + hostText.value = gPermURI.prePath; + + for (var i in gPermObj) + initRow(i); + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.addObserver(permissionObserver, "perm-changed", false); + onUnloadRegistry.push(onUnloadPermission); + permTab.hidden = false; + } + else + permTab.hidden = true; +} + +function onUnloadPermission() +{ + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.removeObserver(permissionObserver, "perm-changed"); + + if (gUsageRequest) { + gUsageRequest.cancel(); + gUsageRequest = null; + } +} + +function initRow(aPartId) +{ + if (aPartId == "plugins") { + initPluginsRow(); + return; + } + + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + + var checkbox = document.getElementById(aPartId + "Def"); + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + // Desktop Notification, Geolocation and PointerLock permission consumers + // use testExactPermission, not testPermission. + var perm; + if (aPartId == "desktop-notification" || aPartId == "geo" || aPartId == "pointerLock") + perm = permissionManager.testExactPermission(gPermURI, aPartId); + else + perm = permissionManager.testPermission(gPermURI, aPartId); + + if (perm) { + checkbox.checked = false; + command.removeAttribute("disabled"); + } + else { + checkbox.checked = true; + command.setAttribute("disabled", "true"); + perm = gPermObj[aPartId](); + } + setRadioState(aPartId, perm); +} + +function onCheckboxClick(aPartId) +{ + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + var checkbox = document.getElementById(aPartId + "Def"); + if (checkbox.checked) { + permissionManager.remove(gPermURI, aPartId); + command.setAttribute("disabled", "true"); + var perm = gPermObj[aPartId](); + setRadioState(aPartId, perm); + } + else { + onRadioClick(aPartId); + command.removeAttribute("disabled"); + } +} + +function onPluginRadioClick(aEvent) { + onRadioClick(aEvent.originalTarget.getAttribute("id").split('#')[0]); +} + +function onRadioClick(aPartId) +{ + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + + var radioGroup = document.getElementById(aPartId + "RadioGroup"); + var id = radioGroup.selectedItem.id; + var permission = id.split('#')[1]; + if (permission == UNKNOWN) { + permissionManager.remove(gPermURI, aPartId); + } else { + permissionManager.add(gPermURI, aPartId, permission); + } +} + +function setRadioState(aPartId, aValue) +{ + var radio = document.getElementById(aPartId + "#" + aValue); + radio.radioGroup.selectedItem = radio; +} + +// XXX copied this from browser-plugins.js - is there a way to share? +function makeNicePluginName(aName) { + if (aName == "Shockwave Flash") + return "Adobe Flash"; + + // Clean up the plugin name by stripping off any trailing version numbers + // or "plugin". EG, "Foo Bar Plugin 1.23_02" --> "Foo Bar" + // Do this by first stripping the numbers, etc. off the end, and then + // removing "Plugin" (and then trimming to get rid of any whitespace). + // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled) + let newName = aName.replace(/[\s\d\.\-\_\(\)]+$/, "").replace(/\bplug-?in\b/i, "").trim(); + return newName; +} + +function fillInPluginPermissionTemplate(aPermissionString, aPluginObject) { + let permPluginTemplate = document.getElementById("permPluginTemplate") + .cloneNode(true); + permPluginTemplate.setAttribute("permString", aPermissionString); + permPluginTemplate.setAttribute("tooltiptext", aPluginObject.description); + let attrs = []; + attrs.push([".permPluginTemplateLabel", "value", aPluginObject.name]); + attrs.push([".permPluginTemplateRadioGroup", "id", aPermissionString + "RadioGroup"]); + attrs.push([".permPluginTemplateRadioDefault", "id", aPermissionString + "#0"]); + let permPluginTemplateRadioAsk = ".permPluginTemplateRadioAsk"; + if (Services.prefs.getBoolPref("plugins.click_to_play") || + aPluginObject.vulnerable) { + attrs.push([permPluginTemplateRadioAsk, "id", aPermissionString + "#3"]); + } else { + permPluginTemplate.querySelector(permPluginTemplateRadioAsk) + .setAttribute("disabled", "true"); + } + attrs.push([".permPluginTemplateRadioAllow", "id", aPermissionString + "#1"]); + attrs.push([".permPluginTemplateRadioBlock", "id", aPermissionString + "#2"]); + + for (let attr of attrs) { + permPluginTemplate.querySelector(attr[0]).setAttribute(attr[1], attr[2]); + } + + return permPluginTemplate; +} + +function clearPluginPermissionTemplate() { + let permPluginTemplate = document.getElementById("permPluginTemplate"); + permPluginTemplate.hidden = true; + permPluginTemplate.removeAttribute("permString"); + permPluginTemplate.removeAttribute("tooltiptext"); + document.querySelector(".permPluginTemplateLabel").removeAttribute("value"); + document.querySelector(".permPluginTemplateRadioGroup").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioAsk").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioAllow").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioBlock").removeAttribute("id"); +} + +function initPluginsRow() { + let vulnerableLabel = document.getElementById("browserBundle") + .getString("pluginActivateVulnerable.label"); + let pluginHost = Components.classes["@mozilla.org/plugin/host;1"] + .getService(Components.interfaces.nsIPluginHost); + let tags = pluginHost.getPluginTags(); + + let permissionMap = new Map(); + + for (let plugin of tags) { + if (plugin.disabled) { + continue; + } + for (let mimeType of plugin.getMimeTypes()) { + if (mimeType == "application/x-shockwave-flash" && plugin.name != "Shockwave Flash") { + continue; + } + let permString = pluginHost.getPermissionStringForType(mimeType); + if (!permissionMap.has(permString)) { + let name = makeNicePluginName(plugin.name) + " " + plugin.version; + let vulnerable = false; + if (permString.startsWith("plugin-vulnerable:")) { + name += " \u2014 " + vulnerableLabel; + vulnerable = true; + } + permissionMap.set(permString, { + "name": name, + "description": plugin.description, + "vulnerable": vulnerable + }); + } + } + } + + // Tycho: + // let entries = [ + // { + // "permission": item[0], + // "obj": item[1], + // } + // for (item of permissionMap) + // ]; + let entries = []; + for (let item of permissionMap) { + entries.push({ + "permission": item[0], + "obj": item[1] + }); + } + entries.sort(function(a, b) { + return ((a.obj.name < b.obj.name) ? -1 : (a.obj.name == b.obj.name ? 0 : 1)); + }); + + // Tycho: + // let permissionEntries = [ + // fillInPluginPermissionTemplate(p.permission, p.obj) for (p of entries) + // ]; + let permissionEntries = []; + entries.forEach(function(p) { + permissionEntries.push(fillInPluginPermissionTemplate(p.permission, p.obj)); + }); + + let permPluginsRow = document.getElementById("permPluginsRow"); + clearPluginPermissionTemplate(); + if (permissionEntries.length < 1) { + permPluginsRow.hidden = true; + return; + } + + for (let permissionEntry of permissionEntries) { + permPluginsRow.appendChild(permissionEntry); + } + + setPluginsRadioState(); +} + +function setPluginsRadioState() { + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + let box = document.getElementById("permPluginsRow"); + for (let permissionEntry of box.childNodes) { + if (permissionEntry.hasAttribute("permString")) { + let permString = permissionEntry.getAttribute("permString"); + let permission = permissionManager.testPermission(gPermURI, permString); + setRadioState(permString, permission); + } + } +} diff --git a/browser/components/pageinfo/security.js b/browser/components/pageinfo/security.js new file mode 100644 index 000000000..e791ab92a --- /dev/null +++ b/browser/components/pageinfo/security.js @@ -0,0 +1,378 @@ +/* -*- Mode: Java; 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/. */ + +var security = { + // Display the server certificate (static) + viewCert : function () { + var cert = security._cert; + viewCertHelper(window, cert); + }, + + _getSecurityInfo : function() { + const nsIX509Cert = Components.interfaces.nsIX509Cert; + const nsIX509CertDB = Components.interfaces.nsIX509CertDB; + const nsX509CertDB = "@mozilla.org/security/x509certdb;1"; + const nsISSLStatusProvider = Components.interfaces.nsISSLStatusProvider; + const nsISSLStatus = Components.interfaces.nsISSLStatus; + + // We don't have separate info for a frame, return null until further notice + // (see bug 138479) + if (gWindow != gWindow.top) + return null; + + var hName = null; + try { + hName = gWindow.location.host; + } + catch (exception) { } + + var ui = security._getSecurityUI(); + if (!ui) + return null; + + var isBroken = + (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IS_BROKEN); + var isMixed = + (ui.state & (Components.interfaces.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT | + Components.interfaces.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT)); + var isInsecure = + (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IS_INSECURE); + var isEV = + (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL); + ui.QueryInterface(nsISSLStatusProvider); + var status = ui.SSLStatus; + + if (!isInsecure && status) { + status.QueryInterface(nsISSLStatus); + var cert = status.serverCert; + var issuerName = + this.mapIssuerOrganization(cert.issuerOrganization) || cert.issuerName; + + var retval = { + hostName : hName, + cAName : issuerName, + encryptionAlgorithm : undefined, + encryptionStrength : undefined, + encryptionSuite : undefined, + version: undefined, + isBroken : isBroken, + isMixed : isMixed, + isEV : isEV, + cert : cert, + fullLocation : gWindow.location + }; + + var version; + try { + retval.encryptionAlgorithm = status.cipherName; + retval.encryptionStrength = status.secretKeyLength; + retval.encryptionSuite = status.cipherSuite; + version = status.protocolVersion; + } + catch (e) { + } + + switch (version) { + case nsISSLStatus.SSL_VERSION_3: + retval.version = "SSL 3"; + break; + case nsISSLStatus.TLS_VERSION_1: + retval.version = "TLS 1.0"; + break; + case nsISSLStatus.TLS_VERSION_1_1: + retval.version = "TLS 1.1"; + break; + case nsISSLStatus.TLS_VERSION_1_2: + retval.version = "TLS 1.2" + break; + case nsISSLStatus.TLS_VERSION_1_3: + retval.version = "TLS 1.3" + break; + } + + return retval; + } else { + return { + hostName : hName, + cAName : "", + encryptionAlgorithm : "", + encryptionStrength : 0, + encryptionSuite : "", + version: "", + isBroken : isBroken, + isMixed : isMixed, + isEV : isEV, + cert : null, + fullLocation : gWindow.location + }; + } + }, + + // Find the secureBrowserUI object (if present) + _getSecurityUI : function() { + if (window.opener.gBrowser) + return window.opener.gBrowser.securityUI; + return null; + }, + + // Interface for mapping a certificate issuer organization to + // the value to be displayed. + // Bug 82017 - this implementation should be moved to pipnss C++ code + mapIssuerOrganization: function(name) { + if (!name) return null; + + if (name == "RSA Data Security, Inc.") return "Verisign, Inc."; + + // No mapping required + return name; + }, + + /** + * Open the cookie manager window + */ + viewCookies : function() + { + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var win = wm.getMostRecentWindow("Browser:Cookies"); + var eTLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"]. + getService(Components.interfaces.nsIEffectiveTLDService); + + var eTLD; + var uri = gDocument.documentURIObject; + try { + eTLD = eTLDService.getBaseDomain(uri); + } + catch (e) { + // getBaseDomain will fail if the host is an IP address or is empty + eTLD = uri.asciiHost; + } + + if (win) { + win.gCookiesWindow.setFilter(eTLD); + win.focus(); + } + else + window.openDialog("chrome://browser/content/preferences/cookies.xul", + "Browser:Cookies", "", {filterString : eTLD}); + }, + + /** + * Open the login manager window + */ + viewPasswords : function() + { + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var win = wm.getMostRecentWindow("Toolkit:PasswordManager"); + if (win) { + win.setFilter(this._getSecurityInfo().hostName); + win.focus(); + } + else + window.openDialog("chrome://passwordmgr/content/passwordManager.xul", + "Toolkit:PasswordManager", "", + {filterString : this._getSecurityInfo().hostName}); + }, + + _cert : null +}; + +function securityOnLoad() { + var info = security._getSecurityInfo(); + if (!info) { + document.getElementById("securityTab").hidden = true; + document.getElementById("securityBox").collapsed = true; + return; + } + else { + document.getElementById("securityTab").hidden = false; + document.getElementById("securityBox").collapsed = false; + } + + const pageInfoBundle = document.getElementById("pageinfobundle"); + + /* Set Identity section text */ + setText("security-identity-domain-value", info.hostName); + + var owner, verifier, generalPageIdentityString; + if (info.cert && !info.isBroken) { + // Try to pull out meaningful values. Technically these fields are optional + // so we'll employ fallbacks where appropriate. The EV spec states that Org + // fields must be specified for subject and issuer so that case is simpler. + if (info.isEV) { + owner = info.cert.organization; + verifier = security.mapIssuerOrganization(info.cAName); + generalPageIdentityString = pageInfoBundle.getFormattedString("generalSiteIdentity", + [owner, verifier]); + } + else { + // Technically, a non-EV cert might specify an owner in the O field or not, + // depending on the CA's issuing policies. However we don't have any programmatic + // way to tell those apart, and no policy way to establish which organization + // vetting standards are good enough (that's what EV is for) so we default to + // treating these certs as domain-validated only. + owner = pageInfoBundle.getString("securityNoOwner"); + verifier = security.mapIssuerOrganization(info.cAName || + info.cert.issuerCommonName || + info.cert.issuerName); + generalPageIdentityString = owner; + } + } + else { + // We don't have valid identity credentials. + owner = pageInfoBundle.getString("securityNoOwner"); + verifier = pageInfoBundle.getString("notset"); + generalPageIdentityString = owner; + } + + setText("security-identity-owner-value", owner); + setText("security-identity-verifier-value", verifier); + setText("general-security-identity", generalPageIdentityString); + + /* Manage the View Cert button*/ + var viewCert = document.getElementById("security-view-cert"); + if (info.cert) { + security._cert = info.cert; + viewCert.collapsed = false; + } + else + viewCert.collapsed = true; + + /* Set Privacy & History section text */ + var yesStr = pageInfoBundle.getString("yes"); + var noStr = pageInfoBundle.getString("no"); + + var uri = gDocument.documentURIObject; + setText("security-privacy-cookies-value", + hostHasCookies(uri) ? yesStr : noStr); + setText("security-privacy-passwords-value", + realmHasPasswords(uri) ? yesStr : noStr); + + var visitCount = previousVisitCount(info.hostName); + if(visitCount > 1) { + setText("security-privacy-history-value", + pageInfoBundle.getFormattedString("securityNVisits", [visitCount.toLocaleString()])); + } + else if (visitCount == 1) { + setText("security-privacy-history-value", + pageInfoBundle.getString("securityOneVisit")); + } + else { + setText("security-privacy-history-value", noStr); + } + + /* Set the Technical Detail section messages */ + const pkiBundle = document.getElementById("pkiBundle"); + var hdr; + var msg1; + var msg2; + + if (info.isBroken) { + if (info.isMixed) { + hdr = pkiBundle.getString("pageInfo_MixedContent"); + } else { + hdr = pkiBundle.getFormattedString("pageInfo_BrokenEncryption", + [info.encryptionAlgorithm, + info.encryptionStrength + "", + info.version]); + } + msg1 = pkiBundle.getString("pageInfo_Privacy_Broken1"); + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } + else if (info.encryptionStrength > 0) { + hdr = pkiBundle.getFormattedString("pageInfo_EncryptionWithBitsAndProtocol", + [info.encryptionAlgorithm, + info.encryptionStrength + "", + info.version]); + msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1"); + msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2"); + security._cert = info.cert; + } + else { + hdr = pkiBundle.getString("pageInfo_NoEncryption"); + if (info.hostName != null) + msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [info.hostName]); + else + msg1 = pkiBundle.getString("pageInfo_Privacy_None3"); + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } + setText("security-technical-shortform", hdr); + setText("security-technical-longform1", msg1); + setText("security-technical-longform2", msg2); + setText("general-security-privacy", hdr); +} + +function setText(id, value) +{ + var element = document.getElementById(id); + if (!element) + return; + if (element.localName == "textbox" || element.localName == "label") + element.value = value; + else { + if (element.hasChildNodes()) + element.removeChild(element.firstChild); + var textNode = document.createTextNode(value); + element.appendChild(textNode); + } +} + +function viewCertHelper(parent, cert) +{ + if (!cert) + return; + + var cd = Components.classes[CERTIFICATEDIALOGS_CONTRACTID].getService(nsICertificateDialogs); + cd.viewCert(parent, cert); +} + +/** + * Return true iff we have cookies for uri + */ +function hostHasCookies(uri) { + var cookieManager = Components.classes["@mozilla.org/cookiemanager;1"] + .getService(Components.interfaces.nsICookieManager2); + + return cookieManager.countCookiesFromHost(uri.asciiHost) > 0; +} + +/** + * Return true iff realm (proto://host:port) (extracted from uri) has + * saved passwords + */ +function realmHasPasswords(uri) { + var passwordManager = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + return passwordManager.countLogins(uri.prePath, "", "") > 0; +} + +/** + * Return the number of previous visits recorded for host before today. + * + * @param host - the domain name to look for in history + */ +function previousVisitCount(host, endTimeReference) { + if (!host) + return false; + + var historyService = Components.classes["@mozilla.org/browser/nav-history-service;1"] + .getService(Components.interfaces.nsINavHistoryService); + + var options = historyService.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_VISIT; + + // Search for visits to this host before today + var query = historyService.getNewQuery(); + query.endTimeReference = query.TIME_RELATIVE_TODAY; + query.endTime = 0; + query.domain = host; + + var result = historyService.executeQuery(query, options); + result.root.containerOpen = true; + var cc = result.root.childCount; + result.root.containerOpen = false; + return cc; +} diff --git a/browser/components/permissions/aboutPermissions.css b/browser/components/permissions/aboutPermissions.css new file mode 100644 index 000000000..d73b6a879 --- /dev/null +++ b/browser/components/permissions/aboutPermissions.css @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.site { + -moz-binding: url("chrome://browser/content/permissions/aboutPermissions.xml#site"); +} + +.pluginPermission { + -moz-binding: url("chrome://browser/content/permissions/aboutPermissions.xml#pluginPermission"); +} diff --git a/browser/components/permissions/aboutPermissions.js b/browser/components/permissions/aboutPermissions.js new file mode 100644 index 000000000..421b65a0e --- /dev/null +++ b/browser/components/permissions/aboutPermissions.js @@ -0,0 +1,1335 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/DownloadUtils.jsm"); +Cu.import("resource://gre/modules/AddonManager.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/ForgetAboutSite.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +var gSecMan = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + +var gFaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + +var gPlacesDatabase = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsPIPlacesDatabase). + DBConnection. + clone(true); + +var gSitesStmt = gPlacesDatabase.createAsyncStatement( + "SELECT url " + + "FROM moz_places " + + "WHERE rev_host > '.' " + + "AND visit_count > 0 " + + "GROUP BY rev_host " + + "ORDER BY MAX(frecency) DESC " + + "LIMIT :limit"); + +var gVisitStmt = gPlacesDatabase.createAsyncStatement( + "SELECT SUM(visit_count) AS count " + + "FROM moz_places " + + "WHERE rev_host = :rev_host"); + +var gFlash = { + name: "Shockwave Flash", + betterName: "Adobe Flash", + type: "application/x-shockwave-flash", +}; + +// XXX: +// Is there a better way to do this rather than this hacky comparison? +// Copied this from toolkit/components/passwordmgr/crypto-SDR.js +const MASTER_PASSWORD_MESSAGE = "User canceled master password entry"; + +/** + * Permission types that should be tested with testExactPermission, as opposed + * to testPermission. This is based on what consumers use to test these + * permissions. + */ +const TEST_EXACT_PERM_TYPES = ["desktop-notification", "geo", "pointerLock"]; + +/** + * Site object represents a single site, uniquely identified by a principal. + */ +function Site(principal) { + this.principal = principal; + this.listitem = null; +} + +Site.prototype = { + /** + * Gets the favicon to use for the site. The callback only gets called if + * a favicon is found for either the http URI or the https URI. + * + * @param aCallback + * A callback function that takes a favicon image URL as a parameter. + */ + getFavicon: function(aCallback) { + function invokeCallback(aFaviconURI) { + try { + // Use getFaviconLinkForIcon to get image data from the database instead + // of using the favicon URI to fetch image data over the network. + aCallback(gFaviconService.getFaviconLinkForIcon(aFaviconURI).spec); + } catch (e) { + Cu.reportError("AboutPermissions: " + e); + } + } + + // Get the favicon for the origin + gFaviconService.getFaviconURLForPage(this.principal.URI, function (aURI) { + if (aURI) { + invokeCallback(aURI); + } + }.bind(this)); + }, + + /** + * Gets the number of history visits for the site. + * + * @param aCallback + * A function that takes the visit count (a number) as a parameter. + */ + getVisitCount: function(aCallback) { + // XXX This won't be a very reliable system, as it will count both http: and https: visits + // Unfortunately, I don't think that there is a much better way to do it right now. + let rev_host = this.principal.URI.host.split("").reverse().join("") + "."; + gVisitStmt.params.rev_host = rev_host; + gVisitStmt.executeAsync({ + handleResult: function(aResults) { + let row = aResults.getNextRow(); + let count = row.getResultByName("count") || 0; + try { + aCallback(count); + } catch (e) { + Cu.reportError("AboutPermissions: " + e); + } + }, + handleError: function(aError) { + Cu.reportError("AboutPermissions: " + aError); + }, + handleCompletion: function(aReason) { + } + }); + }, + + /** + * Gets the permission value stored for a specified permission type. + * + * @param aType + * The permission type string stored in permission manager. + * e.g. "cookie", "geo", "popup", "image" + * @param aResultObj + * An object that stores the permission value set for aType. + * + * @return A boolean indicating whether or not a permission is set. + */ + getPermission: function(aType, aResultObj) { + // Password saving isn't a nsIPermissionManager permission type, so handle + // it seperately. + if (aType == "password") { + aResultObj.value = this.loginSavingEnabled + ? Ci.nsIPermissionManager.ALLOW_ACTION + : Ci.nsIPermissionManager.DENY_ACTION; + return true; + } + + let permissionValue; + if (TEST_EXACT_PERM_TYPES.indexOf(aType) == -1) { + permissionValue = Services.perms.testPermissionFromPrincipal(this.principal, aType); + } else { + permissionValue = Services.perms.testExactPermissionFromPrincipal(this.principal, aType); + } + aResultObj.value = permissionValue; + + if (aType.startsWith("plugin")) { + if (permissionValue == Ci.nsIPermissionManager.PROMPT_ACTION) { + aResultObj.value = Ci.nsIPermissionManager.UNKNOWN_ACTION; + return true; + } + } + + return permissionValue != Ci.nsIPermissionManager.UNKNOWN_ACTION; + }, + + /** + * Sets a permission for the site given a permission type and value. + * + * @param aType + * The permission type string stored in permission manager. + * e.g. "cookie", "geo", "popup", "image" + * @param aPerm + * The permission value to set for the permission type. This should + * be one of the constants defined in nsIPermissionManager. + */ + setPermission: function(aType, aPerm) { + // Password saving isn't a nsIPermissionManager permission type, so handle + // it seperately. + if (aType == "password") { + this.loginSavingEnabled = aPerm == Ci.nsIPermissionManager.ALLOW_ACTION; + return; + } + + if (aType.startsWith("plugin")) { + if (aPerm == Ci.nsIPermissionManager.UNKNOWN_ACTION) { + aPerm = Ci.nsIPermissionManager.PROMPT_ACTION; + } + } + + Services.perms.addFromPrincipal(this.principal, aType, aPerm); + }, + + /** + * Clears a user-set permission value for the site given a permission type. + * + * @param aType + * The permission type string stored in permission manager. + * e.g. "cookie", "geo", "popup", "image" + */ + clearPermission: function(aType) { + Services.perms.removeFromPrincipal(this.principal, aType); + }, + + /** + * Gets logins stored for the site. + * + * @return An array of the logins stored for the site. + */ + get logins() { + try { + let logins = Services.logins.findLogins({}, + this.principal.originNoSuffix, "", ""); + return logins; + } catch (e) { + if (!e.message.includes(MASTER_PASSWORD_MESSAGE)) { + Cu.reportError("AboutPermissions: " + e); + } + return []; + } + }, + + get loginSavingEnabled() { + // Only say that login saving is blocked if it is blocked for both + // http and https. + try { + return Services.logins.getLoginSavingEnabled(this.principal.originNoSuffix); + } catch (e) { + if (!e.message.includes(MASTER_PASSWORD_MESSAGE)) { + Cu.reportError("AboutPermissions: " + e); + } + return false; + } + }, + + set loginSavingEnabled(isEnabled) { + try { + Services.logins.setLoginSavingEnabled(this.principal.originNoSuffix, isEnabled); + } catch (e) { + if (!e.message.includes(MASTER_PASSWORD_MESSAGE)) { + Cu.reportError("AboutPermissions: " + e); + } + } + }, + + /** + * Gets cookies stored for the site and base domain. + * + * @return An array of the cookies set for the site and base domain. + */ + get cookies() { + let cookies = []; + let enumerator = Services.cookies.enumerator; + while (enumerator.hasMoreElements()) { + let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); + if (cookie.host.hasRootDomain( + AboutPermissions.domainFromHost(this.principal.URI.host))) { + cookies.push(cookie); + } + } + return cookies; + }, + + /** + * Removes a set of specific cookies from the browser. + */ + clearCookies: function() { + this.cookies.forEach(function(aCookie) { + Services.cookies.remove(aCookie.host, aCookie.name, aCookie.path, false, + aCookie.originAttributes); + }); + }, + + /** + * Removes all data from the browser corresponding to the site. + */ + forgetSite: function() { + // XXX This removes data for an entire domain, rather than just + // an origin. This may produce confusing results, as data will + // be cleared for the http:// as well as the https:// domain + // if you try to forget the https:// site. + ForgetAboutSite.removeDataFromDomain(this.principal.URI.host) + .catch(Cu.reportError); + } +} + +/** + * PermissionDefaults object keeps track of default permissions for sites based + * on global preferences. + * + * Inspired by pageinfo/permissions.js + */ +var PermissionDefaults = { + UNKNOWN: Ci.nsIPermissionManager.UNKNOWN_ACTION, // 0 + ALLOW: Ci.nsIPermissionManager.ALLOW_ACTION, // 1 + DENY: Ci.nsIPermissionManager.DENY_ACTION, // 2 + SESSION: Ci.nsICookiePermission.ACCESS_SESSION, // 8 + + get password() { + if (Services.prefs.getBoolPref("signon.rememberSignons")) { + return this.ALLOW; + } + return this.DENY; + }, + set password(aValue) { + let value = (aValue != this.DENY); + Services.prefs.setBoolPref("signon.rememberSignons", value); + }, + + IMAGE_ALLOW: 1, + IMAGE_DENY: 2, + IMAGE_ALLOW_FIRST_PARTY_ONLY: 3, + + get image() { + if (Services.prefs.getIntPref("permissions.default.image") + == this.IMAGE_DENY) { + return this.IMAGE_DENY; + } else if (Services.prefs.getIntPref("permissions.default.image") + == this.IMAGE_ALLOW_FIRST_PARTY_ONLY) { + return this.IMAGE_ALLOW_FIRST_PARTY_ONLY; + } + return this.IMAGE_ALLOW; + }, + set image(aValue) { + let value = this.IMAGE_ALLOW; + if (aValue == this.IMAGE_DENY) { + value = this.IMAGE_DENY; + } else if (aValue == this.IMAGE_ALLOW_FIRST_PARTY_ONLY) { + value = this.IMAGE_ALLOW_FIRST_PARTY_ONLY; + } + Services.prefs.setIntPref("permissions.default.image", value); + }, + + get popup() { + if (Services.prefs.getBoolPref("dom.disable_open_during_load")) { + return this.DENY; + } + return this.ALLOW; + }, + set popup(aValue) { + let value = (aValue == this.DENY); + Services.prefs.setBoolPref("dom.disable_open_during_load", value); + }, + + // For use with network.cookie.* prefs. + COOKIE_ACCEPT: 0, + COOKIE_DENY: 2, + COOKIE_NORMAL: 0, + COOKIE_SESSION: 2, + + get cookie() { + if (Services.prefs.getIntPref("network.cookie.cookieBehavior") + == this.COOKIE_DENY) { + return this.DENY; + } + + if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") + == this.COOKIE_SESSION) { + return this.SESSION; + } + return this.ALLOW; + }, + set cookie(aValue) { + let value = (aValue == this.DENY) ? this.COOKIE_DENY : this.COOKIE_ACCEPT; + Services.prefs.setIntPref("network.cookie.cookieBehavior", value); + + let lifetimeValue = aValue == this.SESSION ? this.COOKIE_SESSION : + this.COOKIE_NORMAL; + Services.prefs.setIntPref("network.cookie.lifetimePolicy", lifetimeValue); + }, + + get ["desktop-notification"]() { + if (!Services.prefs.getBoolPref("dom.webnotifications.enabled")) { + return this.DENY; + } + // We always ask for permission to enable notifications for a specific + // site, so there is no global ALLOW. + return this.UNKNOWN; + }, + set ["desktop-notification"](aValue) { + let value = (aValue != this.DENY); + Services.prefs.setBoolPref("dom.webnotifications.enabled", value); + }, + + get install() { + if (Services.prefs.getBoolPref("xpinstall.whitelist.required")) { + return this.DENY; + } + return this.ALLOW; + }, + set install(aValue) { + let value = (aValue == this.DENY); + Services.prefs.setBoolPref("xpinstall.whitelist.required", value); + }, + + get geo() { + if (!Services.prefs.getBoolPref("geo.enabled")) { + return this.DENY; + } + // We always ask for permission to share location with a specific site, + // so there is no global ALLOW. + return this.UNKNOWN; + }, + set geo(aValue) { + let value = (aValue != this.DENY); + Services.prefs.setBoolPref("geo.enabled", value); + }, +} + +/** + * AboutPermissions manages the about:permissions page. + */ +var AboutPermissions = { + /** + * Maximum number of sites to return from the places database. + */ + PLACES_SITES_LIMIT_MAX: 100, + + /** + * When adding sites to the dom sites-list, divide workload into intervals. + */ + LIST_BUILD_DELAY: 100, // delay between intervals + + /** + * Stores a mapping of origin strings to Site objects. + */ + _sites: {}, + + /** + * Using a getter for sitesFilter to avoid races with tests. + */ + get sitesFilter () { + delete this.sitesFilter; + return this.sitesFilter = document.getElementById("sites-filter"); + }, + + sitesList: null, + _selectedSite: null, + + /** + * For testing, track initializations so we can send notifications. + */ + _initPlacesDone: false, + _initServicesDone: false, + + /** + * This reflects the permissions that we expose in the UI. These correspond + * to permission type strings in the permission manager, PermissionDefaults, + * and element ids in aboutPermissions.xul. + * + * Potential future additions: "sts/use", "sts/subd" + */ + _supportedPermissions: ["password", "image", "popup", "cookie", + "desktop-notification", "install", "geo"], + + /** + * Permissions that don't have a global "Allow" option. + */ + _noGlobalAllow: ["desktop-notification", "geo"], + + /** + * Permissions that don't have a global "Deny" option. + */ + _noGlobalDeny: [], + + _stringBundleBrowser: Services.strings + .createBundle("chrome://browser/locale/browser.properties"), + + _stringBundleAboutPermissions: Services.strings.createBundle( + "chrome://browser/locale/permissions/aboutPermissions.properties"), + + _initPart1: function() { + this.initPluginList(); + this.cleanupPluginList(); + + this.getSitesFromPlaces(); + + this.enumerateServicesGenerator = this.getEnumerateServicesGenerator(); + setTimeout(this.enumerateServicesDriver.bind(this), this.LIST_BUILD_DELAY); + }, + + _initPart2: function() { + this._supportedPermissions.forEach(function(aType) { + this.updatePermission(aType); + }, this); + }, + + /** + * Called on page load. + */ + init: function() { + this.sitesList = document.getElementById("sites-list"); + + this._initPart1(); + + // Attach observers in case data changes while the page is open. + Services.prefs.addObserver("signon.rememberSignons", this, false); + Services.prefs.addObserver("permissions.default.image", this, false); + Services.prefs.addObserver("dom.disable_open_during_load", this, false); + Services.prefs.addObserver("network.cookie.", this, false); + Services.prefs.addObserver("dom.webnotifications.enabled", this, false); + Services.prefs.addObserver("xpinstall.whitelist.required", this, false); + Services.prefs.addObserver("geo.enabled", this, false); + Services.prefs.addObserver("plugins.click_to_play", this, false); + Services.prefs.addObserver("permissions.places-sites-limit", this, false); + + Services.obs.addObserver(this, "perm-changed", false); + Services.obs.addObserver(this, "passwordmgr-storage-changed", false); + Services.obs.addObserver(this, "cookie-changed", false); + Services.obs.addObserver(this, "browser:purge-domain-data", false); + Services.obs.addObserver(this, "plugin-info-updated", false); + Services.obs.addObserver(this, "plugin-list-updated", false); + Services.obs.addObserver(this, "blocklist-updated", false); + + this._observersInitialized = true; + Services.obs.notifyObservers(null, "browser-permissions-preinit", null); + + this._initPart2(); + + // Process about:permissions?filter=<string> + // About URIs don't support query params, so do this manually + var loc = document.location.href; + var matches = /[?&]filter\=([^&]+)/i.exec(loc); + if (matches) { + this.sitesFilter.value = decodeURIComponent(matches[1]); + } + }, + + sitesReload: function() { + Object.getOwnPropertyNames(this._sites).forEach(function(prop) { + AboutPermissions.deleteFromSitesList(prop); + }); + this._initPart1(); + this._initPart2(); + }, + + // XXX copied this from browser-plugins.js - is there a way to share? + // Map the plugin's name to a filtered version more suitable for user UI. + makeNicePluginName: function(aName) { + if (aName == gFlash.name) { + return gFlash.betterName; + } + + // Clean up the plugin name by stripping off any trailing version numbers + // or "plugin". EG, "Foo Bar Plugin 1.23_02" --> "Foo Bar" + // Do this by first stripping the numbers, etc. off the end, and then + // removing "Plugin" (and then trimming to get rid of any whitespace). + // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled.) + let newName = aName.replace( + /[\s\d\.\-\_\(\)]+$/, "").replace(/\bplug-?in\b/i, "").trim(); + return newName; + }, + + initPluginList: function() { + let pluginHost = Cc["@mozilla.org/plugin/host;1"] + .getService(Ci.nsIPluginHost); + let tags = pluginHost.getPluginTags(); + + let permissionMap = new Map(); + + let permissionEntries = []; + let XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + for (let plugin of tags) { + for (let mimeType of plugin.getMimeTypes()) { + if ((mimeType == gFlash.type) && (plugin.name != gFlash.name)) { + continue; + } + let permString = pluginHost.getPermissionStringForType(mimeType); + if (!permissionMap.has(permString)) { + let permissionEntry = document.createElementNS(XUL_NS, "box"); + permissionEntry.setAttribute("label", + this.makeNicePluginName(plugin.name) + + " " + plugin.version); + permissionEntry.setAttribute("tooltiptext", plugin.description); + permissionEntry.setAttribute("vulnerable", ""); + permissionEntry.setAttribute("mimeType", mimeType); + permissionEntry.setAttribute("permString", permString); + permissionEntry.setAttribute("class", "pluginPermission"); + permissionEntry.setAttribute("id", permString + "-entry"); + // If the plugin is disabled, it makes no sense to change its + // click-to-play status, so don't add it. + if (plugin.disabled) { + permissionEntry.hidden = true; + } else { + permissionEntry.hidden = false; + } + permissionEntries.push(permissionEntry); + this._supportedPermissions.push(permString); + this._noGlobalDeny.push(permString); + Object.defineProperty(PermissionDefaults, permString, { + get: function() { + if ((Services.prefs.getBoolPref("plugins.click_to_play") && + plugin.clicktoplay) || + permString.startsWith("plugin-vulnerable:")) { + return PermissionDefaults.UNKNOWN; + } + return PermissionDefaults.ALLOW; + }, + set: function(aValue) { + this.clicktoplay = (aValue == PermissionDefaults.UNKNOWN); + }.bind(plugin), + configurable: true + }); + permissionMap.set(permString, ""); + } + } + } + + if (permissionEntries.length > 0) { + permissionEntries.sort(function(entryA, entryB) { + let labelA = entryA.getAttribute("label"); + let labelB = entryB.getAttribute("label"); + return ((labelA < labelB) ? -1 : (labelA == labelB ? 0 : 1)); + }); + } + + let pluginsBox = document.getElementById("plugins-box"); + while (pluginsBox.hasChildNodes()) { + pluginsBox.removeChild(pluginsBox.firstChild); + } + for (let permissionEntry of permissionEntries) { + pluginsBox.appendChild(permissionEntry); + } + }, + + cleanupPluginList: function() { + let pluginsPrefItem = document.getElementById("plugins-pref-item"); + let pluginsBox = document.getElementById("plugins-box"); + let pluginsBoxEmpty = true; + let pluginsBoxSibling = pluginsBox.firstChild; + while (pluginsBoxSibling) { + if (!pluginsBoxSibling.hidden) { + pluginsBoxEmpty = false; + break; + } + pluginsBoxSibling = pluginsBoxSibling.nextSibling; + } + if (pluginsBoxEmpty) { + pluginsPrefItem.collapsed = true; + } else { + pluginsPrefItem.collapsed = false; + } + }, + + /** + * Called on page unload. + */ + cleanUp: function() { + if (this._observersInitialized) { + Services.prefs.removeObserver("signon.rememberSignons", this, false); + Services.prefs.removeObserver("permissions.default.image", this, false); + Services.prefs.removeObserver("dom.disable_open_during_load", this, false); + Services.prefs.removeObserver("network.cookie.", this, false); + Services.prefs.removeObserver("dom.webnotifications.enabled", this, false); + Services.prefs.removeObserver("xpinstall.whitelist.required", this, false); + Services.prefs.removeObserver("geo.enabled", this, false); + Services.prefs.removeObserver("plugins.click_to_play", this, false); + Services.prefs.removeObserver("permissions.places-sites-limit", this, false); + + Services.obs.removeObserver(this, "perm-changed"); + Services.obs.removeObserver(this, "passwordmgr-storage-changed"); + Services.obs.removeObserver(this, "cookie-changed"); + Services.obs.removeObserver(this, "browser:purge-domain-data"); + Services.obs.removeObserver(this, "plugin-info-updated"); + Services.obs.removeObserver(this, "plugin-list-updated"); + Services.obs.removeObserver(this, "blocklist-updated"); + } + + gSitesStmt.finalize(); + gVisitStmt.finalize(); + gPlacesDatabase.asyncClose(null); + }, + + observe: function(aSubject, aTopic, aData) { + switch(aTopic) { + case "perm-changed": + // Permissions changes only affect individual sites. + if (!this._selectedSite) { + break; + } + // aSubject is null when nsIPermisionManager::removeAll() is called. + if (!aSubject) { + this._supportedPermissions.forEach(function(aType) { + this.updatePermission(aType); + }, this); + break; + } + let permission = aSubject.QueryInterface(Ci.nsIPermission); + // We can't compare selectedSite.principal and permission.principal here + // because we need to handle the case where a parent domain was changed + // in a way that affects the subdomain. + if (this._supportedPermissions.indexOf(permission.type) != -1) { + this.updatePermission(permission.type); + } + break; + case "nsPref:changed": + if (aData == "permissions.places-sites-limit") { + this.sitesReload(); + return; + } + let plugin = false; + if (aData.startsWith("plugin")) { + plugin = true; + } + if (plugin) { + this.initPluginList(); + } + this._supportedPermissions.forEach(function(aType) { + if (!plugin || (plugin && aType.startsWith("plugin"))) { + this.updatePermission(aType); + } + }, this); + if (plugin) { + this.cleanupPluginList(); + } + break; + case "passwordmgr-storage-changed": + this.updatePermission("password"); + if (this._selectedSite) { + this.updatePasswordsCount(); + } + break; + case "cookie-changed": + if (this._selectedSite) { + this.updateCookiesCount(); + } + break; + case "browser:purge-domain-data": + this.deleteFromSitesList(aData); + break; + case "plugin-info-updated": + case "plugin-list-updated": + case "blocklist-updated": + this.initPluginList(); + this._supportedPermissions.forEach(function(aType) { + if (aType.startsWith("plugin")) { + this.updatePermission(aType); + } + }, this); + this.cleanupPluginList(); + break; + } + }, + + /** + * Creates Site objects for the top-frecency sites in the places database + * and stores them in _sites. + * The number of sites created is controlled by _placesSitesLimit. + */ + getSitesFromPlaces: function() { + let _placesSitesLimit = Services.prefs.getIntPref( + "permissions.places-sites-limit"); + if (_placesSitesLimit <= 0) { + return; + } + if (_placesSitesLimit > this.PLACES_SITES_LIMIT_MAX) { + _placesSitesLimit = this.PLACES_SITES_LIMIT_MAX; + } + + gSitesStmt.params.limit = _placesSitesLimit; + gSitesStmt.executeAsync({ + handleResult: function(aResults) { + AboutPermissions.startSitesListBatch(); + let row; + while (row = aResults.getNextRow()) { + let spec = row.getResultByName("url"); + let uri = NetUtil.newURI(spec); + let principal = gSecMan.getNoAppCodebasePrincipal(uri); + + AboutPermissions.addPrincipal(principal); + } + AboutPermissions.endSitesListBatch(); + }, + handleError: function(aError) { + Cu.reportError("AboutPermissions: " + aError); + }, + handleCompletion: function(aReason) { + // Notify oberservers for testing purposes. + AboutPermissions._initPlacesDone = true; + if (AboutPermissions._initServicesDone) { + Services.obs.notifyObservers( + null, "browser-permissions-initialized", null); + } + } + }); + }, + + /** + * Drives getEnumerateServicesGenerator to work in intervals. + */ + enumerateServicesDriver: function() { + if (this.enumerateServicesGenerator.next()) { + // Build top sitesList items faster so that the list never seems sparse + let delay = Math.min(this.sitesList.itemCount * 5, this.LIST_BUILD_DELAY); + setTimeout(this.enumerateServicesDriver.bind(this), delay); + } else { + this.enumerateServicesGenerator.close(); + this._initServicesDone = true; + if (this._initPlacesDone) { + Services.obs.notifyObservers( + null, "browser-permissions-initialized", null); + } + } + }, + + /** + * Finds sites that have non-default permissions and creates Site objects + * for them if they are not already stored in _sites. + */ + getEnumerateServicesGenerator: function() { + let itemCnt = 1; + let schemeChrome = "chrome"; + + try { + let logins = Services.logins.getAllLogins(); + logins.forEach(function(aLogin) { + try { + // aLogin.hostname is a string in origin URL format + // (e.g. "http://foo.com"). + // newURI will throw for add-ons logins stored in chrome:// URIs + // i.e.: "chrome://weave" (Sync) + if (!aLogin.hostname.startsWith(schemeChrome + ":")) { + let uri = NetUtil.newURI(aLogin.hostname); + let principal = gSecMan.getNoAppCodebasePrincipal(uri); + this.addPrincipal(principal); + } + } catch (e) { + Cu.reportError("AboutPermissions: " + e); + } + itemCnt++; + }, this); + + let disabledHosts = Services.logins.getAllDisabledHosts(); + disabledHosts.forEach(function(aHostname) { + try { + // aHostname is a string in origin URL format (e.g. "http://foo.com"). + // newURI will throw for add-ons logins stored in chrome:// URIs + // i.e.: "chrome://weave" (Sync) + if (!aHostname.startsWith(schemeChrome + ":")) { + let uri = NetUtil.newURI(aHostname); + let principal = gSecMan.getNoAppCodebasePrincipal(uri); + this.addPrincipal(principal); + } + } catch (e) { + Cu.reportError("AboutPermissions: " + e); + } + itemCnt++; + }, this); + } catch (e) { + if (!e.message.includes(MASTER_PASSWORD_MESSAGE)) { + Cu.reportError("AboutPermissions: " + e); + } + } + + let enumerator = Services.perms.enumerator; + while (enumerator.hasMoreElements()) { + let permission = enumerator.getNext().QueryInterface(Ci.nsIPermission); + // Only include sites with exceptions set for supported permission types. + if (this._supportedPermissions.indexOf(permission.type) != -1) { + this.addPrincipal(permission.principal); + } + itemCnt++; + } + + yield false; + }, + + /** + * Creates a new Site and adds it to _sites if it's not already there. + * + * @param aPrincipal + * A principal. + */ + addPrincipal: function(aPrincipal) { + if (aPrincipal.origin in this._sites) { + return; + } + let site = new Site(aPrincipal); + this._sites[aPrincipal.origin] = site; + this.addToSitesList(site); + }, + + /** + * Populates sites-list richlistbox with data from Site object. + * + * @param aSite + * A Site object. + */ + addToSitesList: function(aSite) { + let item = document.createElement("richlistitem"); + item.setAttribute("class", "site"); + item.setAttribute("value", aSite.principal.origin); + + aSite.getFavicon(function(aURL) { + item.setAttribute("favicon", aURL); + }); + aSite.listitem = item; + + // Make sure to only display relevant items when list is filtered. + let filterValue = this.sitesFilter.value.toLowerCase(); + item.collapsed = aSite.principal.origin.toLowerCase().indexOf(filterValue) == -1; + + (this._listFragment || this.sitesList).appendChild(item); + }, + + startSitesListBatch: function() { + if (!this._listFragment) + this._listFragment = document.createDocumentFragment(); + }, + + endSitesListBatch: function() { + if (this._listFragment) { + this.sitesList.appendChild(this._listFragment); + this._listFragment = null; + } + }, + + /** + * Hides sites in richlistbox based on search text in sites-filter textbox. + */ + filterSitesList: function() { + let siteItems = this.sitesList.children; + let filterValue = this.sitesFilter.value.toLowerCase(); + + if (filterValue == "") { + for (let i = 0, iLen = siteItems.length; i < iLen; i++) { + siteItems[i].collapsed = false; + } + return; + } + + for (let i = 0, iLen = siteItems.length; i < iLen; i++) { + let siteValue = siteItems[i].value.toLowerCase(); + siteItems[i].collapsed = siteValue.indexOf(filterValue) == -1; + } + }, + + /** + * Removes all evidence of the selected site. The "forget this site" observer + * will call deleteFromSitesList to update the UI. + */ + forgetSite: function() { + this._selectedSite.forgetSite(); + }, + + /** + * Deletes sites for a host and all of its sub-domains. Removes these sites + * from _sites and removes their corresponding elements from the DOM. + * + * @param aHost + * The host string corresponding to the site to delete. + */ + deleteFromSitesList: function(aHost) { + for (let origin in this._sites) { + let site = this._sites[origin]; + if (site.principal.URI.host.hasRootDomain(aHost)) { + if (site == this._selectedSite) { + // Replace site-specific interface with "All Sites" interface. + this.sitesList.selectedItem = + document.getElementById("all-sites-item"); + } + + this.sitesList.removeChild(site.listitem); + delete this._sites[site.principal.origin]; + } + } + }, + + /** + * Shows interface for managing site-specific permissions. + */ + onSitesListSelect: function(event) { + if (event.target.selectedItem.id == "all-sites-item") { + // Clear the header label value from the previously selected site. + document.getElementById("site-label").value = ""; + this.manageDefaultPermissions(); + return; + } + + let origin = event.target.value; + let site = this._selectedSite = this._sites[origin]; + document.getElementById("site-label").value = origin; + document.getElementById("header-deck").selectedPanel = + document.getElementById("site-header"); + + this.updateVisitCount(); + this.updatePermissionsBox(); + }, + + /** + * Shows interface for managing default permissions. This corresponds to + * the "All Sites" list item. + */ + manageDefaultPermissions: function() { + this._selectedSite = null; + + document.getElementById("header-deck").selectedPanel = + document.getElementById("defaults-header"); + + this.updatePermissionsBox(); + }, + + /** + * Updates permissions interface based on selected site. + */ + updatePermissionsBox: function() { + this._supportedPermissions.forEach(function(aType) { + this.updatePermission(aType); + }, this); + + this.updatePasswordsCount(); + this.updateCookiesCount(); + }, + + /** + * Sets menulist for a given permission to the correct state, based on + * the stored permission. + * + * @param aType + * The permission type string stored in permission manager. + * e.g. "cookie", "geo", "popup", "image" + */ + updatePermission: function(aType) { + let allowItem = document.getElementById( + aType + "-" + PermissionDefaults.ALLOW); + allowItem.hidden = !this._selectedSite && + this._noGlobalAllow.indexOf(aType) != -1; + let denyItem = document.getElementById( + aType + "-" + PermissionDefaults.DENY); + denyItem.hidden = !this._selectedSite && + this._noGlobalDeny.indexOf(aType) != -1; + + let permissionMenulist = document.getElementById(aType + "-menulist"); + let permissionSetDefault = document.getElementById(aType + "-set-default"); + let permissionValue; + let permissionDefault; + let pluginPermissionEntry; + let elementsPrefSetDefault = document.querySelectorAll(".pref-set-default"); + if (!this._selectedSite) { + let _visibility = "collapse"; + for (let i = 0, iLen = elementsPrefSetDefault.length; i < iLen; i++) { + elementsPrefSetDefault[i].style.visibility = _visibility; + } + permissionSetDefault.style.visibility = _visibility; + // If there is no selected site, we are updating the default permissions + // interface. + permissionValue = PermissionDefaults[aType]; + permissionDefault = permissionValue; + if (aType == "image") { + // (aType + "-3") corresponds to ALLOW_FIRST_PARTY_ONLY, + // which is reserved for global preferences only. + document.getElementById(aType + "-3").hidden = false; + } else if (aType == "cookie") { + // (aType + "-9") corresponds to ALLOW_FIRST_PARTY_ONLY, + // which is reserved for site-specific preferences only. + document.getElementById(aType + "-9").hidden = true; + } else if (aType.startsWith("plugin")) { + pluginPermissionEntry = document.getElementById(aType + "-entry"); + pluginPermissionEntry.setAttribute("vulnerable", ""); + let vulnerable = false; + if (pluginPermissionEntry.isBlocklisted()) { + permissionMenulist.disabled = true; + permissionMenulist.setAttribute("tooltiptext", + AboutPermissions._stringBundleAboutPermissions + .GetStringFromName("pluginBlocklisted")); + vulnerable = true; + } else { + permissionMenulist.disabled = false; + permissionMenulist.setAttribute("tooltiptext", ""); + } + if (Services.prefs.getBoolPref("plugins.click_to_play") || vulnerable) { + document.getElementById(aType + "-0").disabled = false; + } else { + document.getElementById(aType + "-0").disabled = true; + } + } + } else { + let _visibility = "visible"; + for (let i = 0, iLen = elementsPrefSetDefault.length; i < iLen; i++) { + elementsPrefSetDefault[i].style.visibility = _visibility; + } + permissionSetDefault.style.visibility = _visibility; + permissionDefault = PermissionDefaults[aType]; + if (aType == "image") { + document.getElementById(aType + "-3").hidden = true; + } else if (aType == "cookie") { + document.getElementById(aType + "-9").hidden = false; + } else if (aType.startsWith("plugin")) { + pluginPermissionEntry = document.getElementById(aType + "-entry"); + let permString = pluginPermissionEntry.getAttribute("permString"); + let vulnerable = false; + if (permString.startsWith("plugin-vulnerable:")) { + let nameVulnerable = " \u2014 " + + AboutPermissions._stringBundleBrowser + .GetStringFromName("pluginActivateVulnerable.label"); + pluginPermissionEntry.setAttribute("vulnerable", nameVulnerable); + vulnerable = true; + } + if (Services.prefs.getBoolPref("plugins.click_to_play") || vulnerable) { + document.getElementById(aType + "-0").disabled = false; + } else { + document.getElementById(aType + "-0").disabled = true; + } + permissionMenulist.disabled = false; + permissionMenulist.setAttribute("tooltiptext", ""); + } + let result = {}; + permissionValue = this._selectedSite.getPermission(aType, result) ? + result.value : permissionDefault; + } + + if (aType == "image") { + if (document.getElementById(aType + "-" + permissionValue).hidden) { + // ALLOW + permissionValue = 1; + } + } + if (aType.startsWith("plugin")) { + if (document.getElementById(aType + "-" + permissionValue).disabled) { + // ALLOW + permissionValue = 1; + } + } + + if (!aType.startsWith("plugin")) { + let _elementDefault = document.getElementById(aType + "-default"); + if (!this._selectedSite || (permissionValue == permissionDefault)) { + _elementDefault.setAttribute("value", ""); + } else { + _elementDefault.setAttribute("value", "*"); + } + } else { + let _elementDefaultVisibility; + if (!this._selectedSite || (permissionValue == permissionDefault)) { + _elementDefaultVisibility = false; + } else { + _elementDefaultVisibility = true; + } + pluginPermissionEntry.setDefaultVisibility(_elementDefaultVisibility); + } + + permissionMenulist.selectedItem = document.getElementById( + aType + "-" + permissionValue); + }, + + onPermissionCommand: function(event, _default) { + let pluginHost = Cc["@mozilla.org/plugin/host;1"] + .getService(Ci.nsIPluginHost); + let permissionMimeType = event.currentTarget.getAttribute("mimeType"); + let permissionType = event.currentTarget.getAttribute("type"); + let permissionValue = event.target.value; + + if (!this._selectedSite) { + if (permissionType.startsWith("plugin")) { + let addonValue = AddonManager.STATE_ASK_TO_ACTIVATE; + switch(permissionValue) { + case "1": + addonValue = false; + break; + case "2": + addonValue = true; + break; + } + + AddonManager.getAddonsByTypes(["plugin"], function(addons) { + for (let addon of addons) { + for (let type of addon.pluginMimeTypes) { + if ((type.type == gFlash.type) && (addon.name != gFlash.name)) { + continue; + } + if (type.type.toLowerCase() == permissionMimeType.toLowerCase()) { + addon.userDisabled = addonValue; + return; + } + } + } + }); + } else { + // If there is no selected site, we are setting the default permission. + PermissionDefaults[permissionType] = permissionValue; + } + } else { + if (_default) { + this._selectedSite.clearPermission(permissionType); + } else { + this._selectedSite.setPermission(permissionType, permissionValue); + } + } + }, + + updateVisitCount: function() { + this._selectedSite.getVisitCount(function(aCount) { + let visitForm = AboutPermissions._stringBundleAboutPermissions + .GetStringFromName("visitCount"); + let visitLabel = PluralForm.get(aCount, visitForm) + .replace("#1", aCount); + document.getElementById("site-visit-count").value = visitLabel; + }); + }, + + updatePasswordsCount: function() { + if (!this._selectedSite) { + document.getElementById("passwords-count").hidden = true; + document.getElementById("passwords-manage-all-button").hidden = false; + return; + } + + let passwordsCount = this._selectedSite.logins.length; + let passwordsForm = this._stringBundleAboutPermissions + .GetStringFromName("passwordsCount"); + let passwordsLabel = PluralForm.get(passwordsCount, passwordsForm) + .replace("#1", passwordsCount); + + document.getElementById("passwords-label").value = passwordsLabel; + document.getElementById("passwords-manage-button").disabled = + (passwordsCount < 1); + document.getElementById("passwords-manage-all-button").hidden = true; + document.getElementById("passwords-count").hidden = false; + }, + + /** + * Opens password manager dialog. + */ + managePasswords: function() { + let selectedOrigin = ""; + if (this._selectedSite) { + selectedOrigin = this._selectedSite.principal.URI.prePath; + } + + let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager"); + if (win) { + win.setFilter(selectedOrigin); + win.focus(); + } else { + window.openDialog("chrome://passwordmgr/content/passwordManager.xul", + "Toolkit:PasswordManager", "", + {filterString : selectedOrigin}); + } + }, + + domainFromHost: function(aHost) { + let domain = aHost; + try { + domain = Services.eTLD.getBaseDomainFromHost(aHost); + } catch (e) { + // getBaseDomainFromHost will fail if the host is an IP address + // or is empty. + } + + return domain; + }, + + updateCookiesCount: function() { + if (!this._selectedSite) { + document.getElementById("cookies-count").hidden = true; + document.getElementById("cookies-clear-all-button").hidden = false; + document.getElementById("cookies-manage-all-button").hidden = false; + return; + } + + let cookiesCount = this._selectedSite.cookies.length; + let cookiesForm = this._stringBundleAboutPermissions + .GetStringFromName("cookiesCount"); + let cookiesLabel = PluralForm.get(cookiesCount, cookiesForm) + .replace("#1", cookiesCount); + + document.getElementById("cookies-label").value = cookiesLabel; + document.getElementById("cookies-clear-button").disabled = + (cookiesCount < 1); + document.getElementById("cookies-manage-button").disabled = + (cookiesCount < 1); + document.getElementById("cookies-clear-all-button").hidden = true; + document.getElementById("cookies-manage-all-button").hidden = true; + document.getElementById("cookies-count").hidden = false; + }, + + /** + * Clears cookies for the selected site and base domain. + */ + clearCookies: function() { + if (!this._selectedSite) { + return; + } + let site = this._selectedSite; + site.clearCookies(site.cookies); + this.updateCookiesCount(); + }, + + /** + * Opens cookie manager dialog. + */ + manageCookies: function() { + // Cookies are stored by-host, and thus we filter the cookie window + // using only the host of the selected principal's origin + let selectedHost = ""; + let selectedDomain = ""; + if (this._selectedSite) { + selectedHost = this._selectedSite.principal.URI.host; + selectedDomain = this.domainFromHost(selectedHost); + } + + let win = Services.wm.getMostRecentWindow("Browser:Cookies"); + if (win) { + win.gCookiesWindow.setFilter(selectedDomain); + win.focus(); + } else { + window.openDialog("chrome://browser/content/preferences/cookies.xul", + "Browser:Cookies", "", {filterString : selectedDomain}); + } + }, + + /** + * Focusses the filter box. + */ + focusFilterBox: function() { + this.sitesFilter.focus(); + } +} + +// See toolkit/forgetaboutsite/ForgetAboutSite.jsm +String.prototype.hasRootDomain = function(aDomain) { + let index = this.indexOf(aDomain); + if (index == -1) { + return false; + } + + if (this == aDomain) { + return true; + } + + let prevChar = this[index - 1]; + return (index == (this.length - aDomain.length)) && + (prevChar == "." || prevChar == "/"); +} diff --git a/browser/components/permissions/aboutPermissions.xml b/browser/components/permissions/aboutPermissions.xml new file mode 100644 index 000000000..2932ea08c --- /dev/null +++ b/browser/components/permissions/aboutPermissions.xml @@ -0,0 +1,113 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ +<!ENTITY % aboutPermissionsDTD SYSTEM "chrome://browser/locale/permissions/aboutPermissions.dtd" > +%aboutPermissionsDTD; +]> + +<bindings xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + <binding id="site" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:hbox class="site-container" align="center" flex="1"> + <xul:image xbl:inherits="src=favicon" class="site-favicon"/> + <xul:label xbl:inherits="value,selected" class="site-domain" crop="end" flex="1"/> + </xul:hbox> + </content> + </binding> + + <binding id="pluginPermission"> + <content> + <xul:hbox flex="1" align="baseline"> + <xul:label xbl:inherits="value=label" class="plugins-label"/> + <xul:label xbl:inherits="value=vulnerable" class="plugins-vulnerable"/> + <xul:label xbl:inherits="value=default" anonid="plugins-default" class="plugins-default"/> + <xul:spacer flex="1"/> + <xul:menulist anonid="plugins-menulist" + class="pref-menulist" + oncommand="AboutPermissions.onPermissionCommand(event, false);"> + <xul:menupopup> + <xul:menuitem anonid="ask" value="0" label="&permission.alwaysAsk;"/> + <xul:menuitem anonid="allow" value="1" label="&permission.allow;"/> + <xul:menuitem anonid="block" value="2" label="&permission.block;"/> + </xul:menupopup> + </xul:menulist> + <xul:button xbl:inherits="value=set-default" + anonid="plugins-set-default" + class="pref-set-default" + label="&permission.default;" + oncommand="AboutPermissions.onPermissionCommand(event, true);"/> + </xul:hbox> + </content> + <implementation> + <constructor><![CDATA[ + let mimeType = this.getAttribute("mimeType"); + let permString = this.getAttribute("permString"); + let menulist = document.getAnonymousElementByAttribute(this, "anonid", "plugins-menulist"); + menulist.setAttribute("id", permString + "-menulist"); + menulist.setAttribute("mimeType", mimeType); + menulist.setAttribute("type", permString); + let askitem = document.getAnonymousElementByAttribute(this, "anonid", "ask"); + askitem.setAttribute("id", permString + "-0"); + let allowitem = document.getAnonymousElementByAttribute(this, "anonid", "allow"); + allowitem.setAttribute("id", permString + "-1"); + let blockitem = document.getAnonymousElementByAttribute(this, "anonid", "block"); + blockitem.setAttribute("id", permString + "-2"); + let _default = document.getAnonymousElementByAttribute(this, "anonid", "plugins-default"); + this.setDefaultVisibility(false); + _default.setAttribute("value", "*"); + let _setDefault = document.getAnonymousElementByAttribute(this, "anonid", "plugins-set-default"); + _setDefault.setAttribute("id", permString + "-set-default"); + _setDefault.setAttribute("class", "pref-set-default"); + _setDefault.setAttribute("type", permString); + ]]> + </constructor> + <method name="setDefaultVisibility"> + <parameter name="visibility" /> + <body><![CDATA[ + let _default = document.getAnonymousElementByAttribute(this, "anonid", "plugins-default"); + if (visibility) { + _default.style.visibility = "visible"; + } else { + _default.style.visibility = "hidden"; + } + ]]> + </body> + </method> + <method name="isClickToPlay"> + <body><![CDATA[ + let pluginHost = Components.classes["@mozilla.org/plugin/host;1"] + .getService(Components.interfaces.nsIPluginHost); + let mimeType = this.getAttribute("mimeType"); + return (pluginHost.getStateForType(mimeType) + == Components.interfaces.nsIPluginTag.STATE_CLICKTOPLAY); + ]]> + </body> + </method> + <method name="isBlocklisted"> + <body><![CDATA[ + let pluginHost = Components.classes["@mozilla.org/plugin/host;1"] + .getService(Components.interfaces.nsIPluginHost); + let blocklistService = Components.classes["@mozilla.org/extensions/blocklist;1"] + .getService(Components.interfaces.nsIBlocklistService); + let mimeType = this.getAttribute("mimeType"); + let tags = pluginHost.getPluginTags(); + let blocklistState = Components.interfaces.nsIBlocklistService.STATE_NOT_BLOCKED; + for (let plugin of tags) { + if (plugin.getMimeTypes()[0] == mimeType) { + blocklistState = blocklistService.getPluginBlocklistState(plugin); + break; + } + } + return (blocklistState == Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE || + blocklistState == Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE); + ]]> + </body> + </method> + </implementation> + </binding> +</bindings> diff --git a/browser/components/permissions/aboutPermissions.xul b/browser/components/permissions/aboutPermissions.xul new file mode 100644 index 000000000..dfee14756 --- /dev/null +++ b/browser/components/permissions/aboutPermissions.xul @@ -0,0 +1,313 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/permissions/aboutPermissions.css"?> +<?xml-stylesheet href="chrome://browser/skin/permissions/aboutPermissions.css"?> + +<!DOCTYPE page [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % aboutPermissionsDTD SYSTEM "chrome://browser/locale/permissions/aboutPermissions.dtd" > +%aboutPermissionsDTD; +]> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xhtml="http://www.w3.org/1999/xhtml" + id="permissions-page" title="&permissionsManager.title;" + onload="AboutPermissions.init();" + onunload="AboutPermissions.cleanUp();" + disablefastfind="true" + role="application"> + + <script type="application/javascript" + src="chrome://browser/content/permissions/aboutPermissions.js"/> + + <keyset> + <key key="&focusSearch.key;" modifiers="accel" oncommand="AboutPermissions.focusFilterBox();"/> + </keyset> + + <hbox id="permissions-header"> + <label id="permissions-pagetitle">&permissionsManager.title;</label> + </hbox> + <hbox flex="1" id="permissions-content" class="main-content"> + + <vbox id="sites-box"> + <button id="sites-reload" + label="&permissions.sitesReload;" + oncommand="AboutPermissions.sitesReload();"/> + <textbox id="sites-filter" + emptytext="&sites.search;" + oncommand="AboutPermissions.filterSitesList();" + type="search"/> + <richlistbox id="sites-list" + flex="1" + class="list" + onselect="AboutPermissions.onSitesListSelect(event);"> + <richlistitem id="all-sites-item" + class="site" + value="&sites.allSites;"/> + </richlistbox> + </vbox> + + <vbox id="permissions-box" flex="1"> + + <deck id="header-deck"> + <hbox id="site-header" class="pref-item" align="center"> + <description id="site-description"> + &header.site.start;<label id="site-label"/>&header.site.end; + </description> + <label id="site-visit-count"/> + <spacer flex="1"/> + <button id="forget-site-button" + label="&permissions.forgetSite;" + oncommand="AboutPermissions.forgetSite();"/> + </hbox> + + <hbox id="defaults-header" class="pref-item" align="center"> + <description id="defaults-description"> + &header.defaults; + </description> + </hbox> + </deck> + + <!-- Passwords --> + <hbox id="password-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="password"/> + <vbox> + <hbox> + <label class="pref-title" value="&password.label;"/> + <label id="password-default" class="pref-default" value="*"/> + </hbox> + <hbox align="center"> + <menulist id="password-menulist" + class="pref-menulist" + type="password" + oncommand="AboutPermissions.onPermissionCommand(event, false);"> + <menupopup> + <menuitem id="password-1" value="1" label="&permission.allow;"/> + <menuitem id="password-2" value="2" label="&permission.block;"/> + </menupopup> + </menulist> + <button id="password-set-default" + class="pref-set-default" + label="&permission.default;" + type="password" + oncommand="AboutPermissions.onPermissionCommand(event, true);"/> + <button id="passwords-manage-all-button" + label="&password.manage;" + oncommand="AboutPermissions.managePasswords();"/> + </hbox> + <hbox id="passwords-count" align="center"> + <label id="passwords-label"/> + <button id="passwords-manage-button" + label="&password.manage;" + oncommand="AboutPermissions.managePasswords();"/> + </hbox> + </vbox> + </hbox> + + <!-- Image Blocking --> + <hbox id="image-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="image"/> + <vbox> + <hbox> + <label class="pref-title" value="&image.label;"/> + <label id="image-default" class="pref-default" value="*"/> + </hbox> + <hbox> + <menulist id="image-menulist" + class="pref-menulist" + type="image" + oncommand="AboutPermissions.onPermissionCommand(event, false);"> + <menupopup> + <menuitem id="image-1" value="1" label="&permission.allow;"/> + <menuitem id="image-2" value="2" label="&permission.block;"/> + <menuitem id="image-3" value="3" label="&permission.allowFirstPartyOnly;"/> + </menupopup> + </menulist> + <button id="image-set-default" + class="pref-set-default" + label="&permission.default;" + type="image" + oncommand="AboutPermissions.onPermissionCommand(event, true);"/> + </hbox> + </vbox> + </hbox> + + <!-- Pop-up Blocking --> + <hbox id="popup-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="popup"/> + <vbox> + <hbox> + <label class="pref-title" value="&popup.label;"/> + <label id="popup-default" class="pref-default" value="*"/> + </hbox> + <hbox> + <menulist id="popup-menulist" + class="pref-menulist" + type="popup" + oncommand="AboutPermissions.onPermissionCommand(event, false);"> + <menupopup> + <menuitem id="popup-1" value="1" label="&permission.allow;"/> + <menuitem id="popup-2" value="2" label="&permission.block;"/> + </menupopup> + </menulist> + <button id="popup-set-default" + class="pref-set-default" + label="&permission.default;" + type="popup" + oncommand="AboutPermissions.onPermissionCommand(event, true);"/> + </hbox> + </vbox> + </hbox> + + <!-- Cookies --> + <hbox id="cookie-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="cookie"/> + <vbox> + <hbox> + <label class="pref-title" value="&cookie.label;"/> + <label id="cookie-default" class="pref-default" value="*"/> + </hbox> + <hbox align="center"> + <menulist id="cookie-menulist" + class="pref-menulist" + type="cookie" + oncommand="AboutPermissions.onPermissionCommand(event, false);"> + <menupopup> + <menuitem id="cookie-1" value="1" label="&permission.allow;"/> + <menuitem id="cookie-8" value="8" label="&permission.allowForSession;"/> + <menuitem id="cookie-9" value="9" label="&permission.allowFirstPartyOnly;"/> + <menuitem id="cookie-2" value="2" label="&permission.block;"/> + </menupopup> + </menulist> + <button id="cookie-set-default" + class="pref-set-default" + label="&permission.default;" + type="cookie" + oncommand="AboutPermissions.onPermissionCommand(event, true);"/> + <button id="cookies-clear-all-button" + label="&cookie.removeAll;" + oncommand="Services.cookies.removeAll();"/> + <button id="cookies-manage-all-button" + label="&cookie.manage;" + oncommand="AboutPermissions.manageCookies();"/> + </hbox> + <hbox id="cookies-count" align="center"> + <label id="cookies-label"/> + <button id="cookies-clear-button" + label="&cookie.remove;" + oncommand="AboutPermissions.clearCookies();"/> + <button id="cookies-manage-button" + label="&cookie.manage;" + oncommand="AboutPermissions.manageCookies();"/> + </hbox> + </vbox> + </hbox> + + <!-- Desktop Notifications --> + <hbox id="desktop-notification-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="desktop-notification"/> + <vbox> + <hbox> + <label class="pref-title" value="&desktop-notification.label;"/> + <label id="desktop-notification-default" class="pref-default" value="*"/> + </hbox> + <hbox> + <menulist id="desktop-notification-menulist" + class="pref-menulist" + type="desktop-notification" + oncommand="AboutPermissions.onPermissionCommand(event, false);"> + <menupopup> + <menuitem id="desktop-notification-0" value="0" label="&permission.alwaysAsk;"/> + <menuitem id="desktop-notification-1" value="1" label="&permission.allow;"/> + <menuitem id="desktop-notification-2" value="2" label="&permission.block;"/> + </menupopup> + </menulist> + <button id="desktop-notification-set-default" + class="pref-set-default" + label="&permission.default;" + type="desktop-notification" + oncommand="AboutPermissions.onPermissionCommand(event, true);"/> + </hbox> + </vbox> + </hbox> + + <!-- Addons Blocking --> + <hbox id="install-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="install"/> + <vbox> + <hbox> + <label class="pref-title" value="&install.label;"/> + <label id="install-default" class="pref-default" value="*"/> + </hbox> + <hbox> + <menulist id="install-menulist" + class="pref-menulist" + type="install" + oncommand="AboutPermissions.onPermissionCommand(event, false);"> + <menupopup> + <menuitem id="install-1" value="1" label="&permission.allow;"/> + <menuitem id="install-2" value="2" label="&permission.block;"/> + </menupopup> + </menulist> + <button id="install-set-default" + class="pref-set-default" + label="&permission.default;" + type="install" + oncommand="AboutPermissions.onPermissionCommand(event, true);"/> + </hbox> + </vbox> + </hbox> + + <!-- Geolocation --> + <hbox id="geo-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="geo"/> + <vbox> + <hbox> + <label class="pref-title" value="&geo.label;"/> + <label id="geo-default" class="pref-default" value="*"/> + </hbox> + <hbox> + <menulist id="geo-menulist" + class="pref-menulist" + type="geo" + oncommand="AboutPermissions.onPermissionCommand(event, false);"> + <menupopup> + <menuitem id="geo-0" value="0" label="&permission.alwaysAsk;"/> + <menuitem id="geo-1" value="1" label="&permission.allow;"/> + <menuitem id="geo-2" value="2" label="&permission.block;"/> + </menupopup> + </menulist> + <button id="geo-set-default" + class="pref-set-default" + label="&permission.default;" + type="geo" + oncommand="AboutPermissions.onPermissionCommand(event, true);"/> + </hbox> + </vbox> + </hbox> + + <!-- Opt-in activation of Plug-ins --> + <hbox id="plugins-pref-item" + class="pref-item" align="top"> + <image class="pref-icon" type="plugins"/> + <vbox> + <label class="pref-title" value="&plugins.label;"/> + <vbox id="plugins-box"/> + </vbox> + </hbox> + </vbox> + </hbox> + +</page> diff --git a/browser/components/permissions/jar.mn b/browser/components/permissions/jar.mn new file mode 100644 index 000000000..c78893837 --- /dev/null +++ b/browser/components/permissions/jar.mn @@ -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/. + +browser.jar: + content/browser/permissions/aboutPermissions.xul + content/browser/permissions/aboutPermissions.js + content/browser/permissions/aboutPermissions.css + content/browser/permissions/aboutPermissions.xml diff --git a/browser/components/permissions/moz.build b/browser/components/permissions/moz.build new file mode 100644 index 000000000..e3d80cf11 --- /dev/null +++ b/browser/components/permissions/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] diff --git a/browser/components/places/PlacesUIUtils.jsm b/browser/components/places/PlacesUIUtils.jsm new file mode 100644 index 000000000..8a7d4a00f --- /dev/null +++ b/browser/components/places/PlacesUIUtils.jsm @@ -0,0 +1,1375 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["PlacesUIUtils"]; + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); + +XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() { + Cu.import("resource://gre/modules/PlacesUtils.jsm"); + return PlacesUtils; +}); + +this.PlacesUIUtils = { + ORGANIZER_LEFTPANE_VERSION: 7, + ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder", + ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery", + + LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar", + DESCRIPTION_ANNO: "bookmarkProperties/description", + + TYPE_TAB_DROP: "application/x-moz-tabbrowser-tab", + + /** + * Makes a URI from a spec, and do fixup + * @param aSpec + * The string spec of the URI + * @returns A URI object for the spec. + */ + createFixedURI: function(aSpec) { + return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE); + }, + + getFormattedString: function(key, params) { + return bundle.formatStringFromName(key, params, params.length); + }, + + /** + * Get a localized plural string for the specified key name and numeric value + * substituting parameters. + * + * @param aKey + * String, key for looking up the localized string in the bundle + * @param aNumber + * Number based on which the final localized form is looked up + * @param aParams + * Array whose items will substitute #1, #2,... #n parameters + * in the string. + * + * @see https://developer.mozilla.org/en/Localization_and_Plurals + * @return The localized plural string. + */ + getPluralString: function(aKey, aNumber, aParams) { + let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey)); + + // Replace #1 with aParams[0], #2 with aParams[1], and so on. + return str.replace(/\#(\d+)/g, function(matchedId, matchedNumber) { + let param = aParams[parseInt(matchedNumber, 10) - 1]; + return param !== undefined ? param : matchedId; + }); + }, + + getString: function(key) { + return bundle.GetStringFromName(key); + }, + + get _copyableAnnotations() [ + this.DESCRIPTION_ANNO, + this.LOAD_IN_SIDEBAR_ANNO, + PlacesUtils.POST_DATA_ANNO, + PlacesUtils.READ_ONLY_ANNO, + ], + + /** + * Get a transaction for copying a uri item (either a bookmark or a history + * entry) from one container to another. + * + * @param aData + * JSON object of dropped or pasted item properties + * @param aContainer + * The container being copied into + * @param aIndex + * The index within the container the item is copied to + * @return A nsITransaction object that performs the copy. + * + * @note Since a copy creates a completely new item, only some internal + * annotations are synced from the old one. + * @see this._copyableAnnotations for the list of copyable annotations. + */ + _getURIItemCopyTransaction: + function(aData, aContainer, aIndex) + { + let transactions = []; + if (aData.dateAdded) { + transactions.push( + new PlacesEditItemDateAddedTransaction(null, aData.dateAdded) + ); + } + if (aData.lastModified) { + transactions.push( + new PlacesEditItemLastModifiedTransaction(null, aData.lastModified) + ); + } + + let keyword = aData.keyword || null; + let annos = []; + if (aData.annos) { + annos = aData.annos.filter(function(aAnno) { + return this._copyableAnnotations.indexOf(aAnno.name) != -1; + }, this); + } + + return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri), + aContainer, aIndex, aData.title, + keyword, annos, transactions); + }, + + /** + * Gets a transaction for copying (recursively nesting to include children) + * a folder (or container) and its contents from one folder to another. + * + * @param aData + * Unwrapped dropped folder data - Obj containing folder and children + * @param aContainer + * The container we are copying into + * @param aIndex + * The index in the destination container to insert the new items + * @return A nsITransaction object that will perform the copy. + * + * @note Since a copy creates a completely new item, only some internal + * annotations are synced from the old one. + * @see this._copyableAnnotations for the list of copyable annotations. + */ + _getFolderCopyTransaction(aData, aContainer, aIndex) { + function getChildItemsTransactions(aRoot) { + let transactions = []; + let index = aIndex; + for (let i = 0; i < aRoot.childCount; ++i) { + let child = aRoot.getChild(i); + // Temporary hacks until we switch to PlacesTransactions.jsm. + let isLivemark = + PlacesUtils.annotations.itemHasAnnotation(child.itemId, + PlacesUtils.LMANNO_FEEDURI); + let [node] = PlacesUtils.unwrapNodes( + PlacesUtils.wrapNode(child, PlacesUtils.TYPE_X_MOZ_PLACE, isLivemark), + PlacesUtils.TYPE_X_MOZ_PLACE + ); + + // Make sure that items are given the correct index, this will be + // passed by the transaction manager to the backend for the insertion. + // Insertion behaves differently for DEFAULT_INDEX (append). + if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) { + index = i; + } + + if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { + if (node.livemark && node.annos) { + transactions.push( + PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index) + ); + } + else { + transactions.push( + PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index) + ); + } + } + else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) { + transactions.push(new PlacesCreateSeparatorTransaction(-1, index)); + } + else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) { + transactions.push( + PlacesUIUtils._getURIItemCopyTransaction(node, -1, index) + ); + } + else { + throw new Error("Unexpected item under a bookmarks folder"); + } + } + return transactions; + } + + if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder. + let transactions = []; + if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { + let {root} = PlacesUtils.getFolderContents(aData.id, false, false); + let urls = PlacesUtils.getURLsForContainerNode(root); + root.containerOpen = false; + for (let { uri } of urls) { + transactions.push( + new PlacesTagURITransaction(NetUtil.newURI(uri), [aData.title]) + ); + } + } + return new PlacesAggregatedTransaction("addTags", transactions); + } + + if (aData.livemark && aData.annos) { // Copying a livemark. + return this._getLivemarkCopyTransaction(aData, aContainer, aIndex); + } + + let {root} = PlacesUtils.getFolderContents(aData.id, false, false); + let transactions = getChildItemsTransactions(root); + root.containerOpen = false; + + if (aData.dateAdded) { + transactions.push( + new PlacesEditItemDateAddedTransaction(null, aData.dateAdded) + ); + } + if (aData.lastModified) { + transactions.push( + new PlacesEditItemLastModifiedTransaction(null, aData.lastModified) + ); + } + + let annos = []; + if (aData.annos) { + annos = aData.annos.filter(function(aAnno) { + return this._copyableAnnotations.indexOf(aAnno.name) != -1; + }, this); + } + + return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex, + annos, transactions); + }, + + /** + * Gets a transaction for copying a live bookmark item from one container to + * another. + * + * @param aData + * Unwrapped live bookmarkmark data + * @param aContainer + * The container we are copying into + * @param aIndex + * The index in the destination container to insert the new items + * @return A nsITransaction object that will perform the copy. + * + * @note Since a copy creates a completely new item, only some internal + * annotations are synced from the old one. + * @see this._copyableAnnotations for the list of copyable annotations. + */ + _getLivemarkCopyTransaction: + function(aData, aContainer, aIndex) + { + if (!aData.livemark || !aData.annos) { + throw new Error("node is not a livemark"); + } + + let feedURI, siteURI; + let annos = []; + if (aData.annos) { + annos = aData.annos.filter(function(aAnno) { + if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) { + feedURI = PlacesUtils._uri(aAnno.value); + } + else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) { + siteURI = PlacesUtils._uri(aAnno.value); + } + return this._copyableAnnotations.indexOf(aAnno.name) != -1 + }, this); + } + + return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title, + aContainer, aIndex, annos); + }, + + /** + * Test if a bookmark item = a live bookmark item. + * + * @param aItemId + * item identifier + * @return true if a live bookmark item, false otherwise. + * + * @note Maybe this should be removed later, see bug 1072833. + */ + _isLivemark: + function(aItemId) + { + // Since this check may be done on each dragover event, it's worth maintaining + // a cache. + let self = this._isLivemark; + if (!("ids" in self)) { + const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI; + + let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO); + self.ids = new Set(idsVec); + + let obs = Object.freeze({ + QueryInterface: XPCOMUtils.generateQI(Ci.nsIAnnotationObserver), + + onItemAnnotationSet(itemId, annoName) { + if (annoName == LIVEMARK_ANNO) + self.ids.add(itemId); + }, + + onItemAnnotationRemoved(itemId, annoName) { + // If annoName is set to an empty string, the item is gone. + if (annoName == LIVEMARK_ANNO || annoName == "") + self.ids.delete(itemId); + }, + + onPageAnnotationSet() { }, + onPageAnnotationRemoved() { }, + }); + PlacesUtils.annotations.addObserver(obs); + PlacesUtils.registerShutdownFunction(() => { + PlacesUtils.annotations.removeObserver(obs); + }); + } + return self.ids.has(aItemId); + }, + + /** + * Constructs a Transaction for the drop or paste of a blob of data into + * a container. + * @param data + * The unwrapped data blob of dropped or pasted data. + * @param type + * The content type of the data + * @param container + * The container the data was dropped or pasted into + * @param index + * The index within the container the item was dropped or pasted at + * @param copy + * The drag action was copy, so don't move folders or links. + * @returns An object implementing nsITransaction that can perform + * the move/insert. + */ + makeTransaction: + function(data, type, container, index, copy) + { + switch (data.type) { + case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: + if (copy) { + return this._getFolderCopyTransaction(data, container, index); + } + + // Otherwise move the item. + return new PlacesMoveItemTransaction(data.id, container, index); + break; + case PlacesUtils.TYPE_X_MOZ_PLACE: + if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked. + return this._getURIItemCopyTransaction(data, container, index); + } + + // Otherwise move the item. + return new PlacesMoveItemTransaction(data.id, container, index); + break; + case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: + if (copy) { + // There is no data in a separator, so copying it just amounts to + // inserting a new separator. + return new PlacesCreateSeparatorTransaction(container, index); + } + + // Otherwise move the item. + return new PlacesMoveItemTransaction(data.id, container, index); + break; + default: + if (type == PlacesUtils.TYPE_X_MOZ_URL || + type == PlacesUtils.TYPE_UNICODE || + type == this.TYPE_TAB_DROP) { + let title = type != PlacesUtils.TYPE_UNICODE ? data.title + : data.uri; + return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri), + container, index, title); + } + } + return null; + }, + + /** + * Shows the bookmark dialog corresponding to the specified info. + * + * @param aInfo + * Describes the item to be edited/added in the dialog. + * See documentation at the top of bookmarkProperties.js + * @param aWindow + * Owner window for the new dialog. + * + * @see documentation at the top of bookmarkProperties.js + * @return true if any transaction has been performed, false otherwise. + */ + showBookmarkDialog: + function(aInfo, aParentWindow) { + // Preserve size attributes differently based on the fact the dialog has + // a folder picker or not, since it needs more horizontal space than the + // other controls. + let hasFolderPicker = !("hiddenRows" in aInfo) || + aInfo.hiddenRows.indexOf("folderPicker") == -1; + // Use a different chrome url to persist different sizes. + let dialogURL = hasFolderPicker ? + "chrome://browser/content/places/bookmarkProperties2.xul" : + "chrome://browser/content/places/bookmarkProperties.xul"; + + let features = "centerscreen,chrome,modal,resizable=yes"; + aParentWindow.openDialog(dialogURL, "", features, aInfo); + return ("performed" in aInfo && aInfo.performed); + }, + + _getTopBrowserWin: function() { + return RecentWindow.getMostRecentBrowserWindow(); + }, + + /** + * Returns the closet ancestor places view for the given DOM node + * @param aNode + * a DOM node + * @return the closet ancestor places view if exists, null otherwsie. + */ + getViewForNode: function(aNode) { + let node = aNode; + + // The view for a <menu> of which its associated menupopup is a places + // view, is the menupopup. + if (node.localName == "menu" && !node._placesNode && + node.lastChild._placesView) + return node.lastChild._placesView; + + while (node instanceof Ci.nsIDOMElement) { + if (node._placesView) + return node._placesView; + if (node.localName == "tree" && node.getAttribute("type") == "places") + return node; + + node = node.parentNode; + } + + return null; + }, + + /** + * By calling this before visiting an URL, the visit will be associated to a + * TRANSITION_TYPED transition (if there is no a referrer). + * This is used when visiting pages from the history menu, history sidebar, + * url bar, url autocomplete results, and history searches from the places + * organizer. If this is not called visits will be marked as + * TRANSITION_LINK. + */ + markPageAsTyped: function(aURL) { + PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL)); + }, + + /** + * By calling this before visiting an URL, the visit will be associated to a + * TRANSITION_BOOKMARK transition. + * This is used when visiting pages from the bookmarks menu, + * personal toolbar, and bookmarks from within the places organizer. + * If this is not called visits will be marked as TRANSITION_LINK. + */ + markPageAsFollowedBookmark: function(aURL) { + PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL)); + }, + + /** + * By calling this before visiting an URL, any visit in frames will be + * associated to a TRANSITION_FRAMED_LINK transition. + * This is actually used to distinguish user-initiated visits in frames + * so automatic visits can be correctly ignored. + */ + markPageAsFollowedLink: function(aURL) { + PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL)); + }, + + /** + * Allows opening of javascript/data URI only if the given node is + * bookmarked (see bug 224521). + * @param aURINode + * a URI node + * @param aWindow + * a window on which a potential error alert is shown on. + * @return true if it's safe to open the node in the browser, false otherwise. + * + */ + checkURLSecurity: function(aURINode, aWindow) { + if (PlacesUtils.nodeIsBookmark(aURINode)) + return true; + + var uri = PlacesUtils._uri(aURINode.uri); + if (uri.schemeIs("javascript") || uri.schemeIs("data")) { + const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; + var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(BRANDING_BUNDLE_URI). + GetStringFromName("brandShortName"); + + var errorStr = this.getString("load-js-data-url-error"); + Services.prompt.alert(aWindow, brandShortName, errorStr); + return false; + } + return true; + }, + + /** + * Get the description associated with a document, as specified in a <META> + * element. + * @param doc + * A DOM Document to get a description for + * @returns A description string if a META element was discovered with a + * "description" or "httpequiv" attribute, empty string otherwise. + */ + getDescriptionFromDocument: function(doc) { + var metaElements = doc.getElementsByTagName("META"); + for (var i = 0; i < metaElements.length; ++i) { + if (metaElements[i].name.toLowerCase() == "description" || + metaElements[i].httpEquiv.toLowerCase() == "description") { + return metaElements[i].content; + } + } + return ""; + }, + + /** + * Retrieve the description of an item + * @param aItemId + * item identifier + * @returns the description of the given item, or an empty string if it is + * not set. + */ + getItemDescription: function(aItemId) { + if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO)) + return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO); + return ""; + }, + + /** + * Check whether or not the given node represents a removable entry (either in + * history or in bookmarks). + * + * @param aNode + * a node, except the root node of a query. + * @return true if the aNode represents a removable entry, false otherwise. + */ + canUserRemove: function(aNode) { + let parentNode = aNode.parent; + if (!parentNode) + throw new Error("canUserRemove doesn't accept root nodes"); + + // If it's not a bookmark, we can remove it unless it's a child of a + // livemark. + if (aNode.itemId == -1) { + // Rather than executing a db query, checking the existence of the feedURI + // annotation, detect livemark children by the fact that they are the only + // direct non-bookmark children of bookmark folders. + return !PlacesUtils.nodeIsFolder(parentNode); + } + + // Generally it's always possible to remove children of a query. + if (PlacesUtils.nodeIsQuery(parentNode)) + return true; + + // Otherwise it has to be a child of an editable folder. + return !this.isContentsReadOnly(parentNode); + }, + + /** + * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH + * TO GUIDS IS COMPLETE (BUG 1071511). + * + * Check whether or not the given node or item-id points to a folder which + * should not be modified by the user (i.e. its children should be unremovable + * and unmovable, new children should be disallowed, etc). + * These semantics are not inherited, meaning that read-only folder may + * contain editable items (for instance, the places root is read-only, but all + * of its direct children aren't). + * + * You should only pass folder item ids or folder nodes for aNodeOrItemId. + * While this is only enforced for the node case (if an item id of a separator + * or a bookmark is passed, false is returned), it's considered the caller's + * job to ensure that it checks a folder. + * Also note that folder-shortcuts should only be passed as result nodes. + * Otherwise they are just treated as bookmarks (i.e. false is returned). + * + * @param aNodeOrItemId + * any item id or result node. + * @throws if aNodeOrItemId is neither an item id nor a folder result node. + * @note livemark "folders" are considered read-only (but see bug 1072833). + * @return true if aItemId points to a read-only folder, false otherwise. + */ + isContentsReadOnly: function(aNodeOrItemId) { + let itemId; + if (typeof(aNodeOrItemId) == "number") { + itemId = aNodeOrItemId; + } + else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) { + itemId = PlacesUtils.getConcreteItemId(aNodeOrItemId); + } + else { + throw new Error("invalid value for aNodeOrItemId"); + } + + if (itemId == PlacesUtils.placesRootId || this._isLivemark(itemId)) + return true; + + // leftPaneFolderId, and as a result, allBookmarksFolderId, is a lazy getter + // performing at least a synchronous DB query (and on its very first call + // in a fresh profile, it also creates the entire structure). + // Therefore we don't want to this function, which is called very often by + // isCommandEnabled, to ever be the one that invokes it first, especially + // because isCommandEnabled may be called way before the left pane folder is + // even created (for example, if the user only uses the bookmarks menu or + // toolbar for managing bookmarks). To do so, we avoid comparing to those + // special folder if the lazy getter is still in place. This is safe merely + // because the only way to access the left pane contents goes through + // "resolving" the leftPaneFolderId getter. + if ("get" in Object.getOwnPropertyDescriptor(this, "leftPaneFolderId")) + return false; + + return itemId == this.leftPaneFolderId || + itemId == this.allBookmarksFolderId; + }, + + /** + * Gives the user a chance to cancel loading lots of tabs at once + */ + _confirmOpenInTabs: + function(numTabsToOpen, aWindow) { + const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen"; + var reallyOpen = true; + + if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) { + if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) { + // default to true: if it were false, we wouldn't get this far + var warnOnOpen = { value: true }; + + var messageKey = "tabs.openWarningMultipleBranded"; + var openKey = "tabs.openButtonMultiple"; + const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; + var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(BRANDING_BUNDLE_URI). + GetStringFromName("brandShortName"); + + var buttonPressed = Services.prompt.confirmEx( + aWindow, + this.getString("tabs.openWarningTitle"), + this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]), + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1), + this.getString(openKey), null, null, + this.getFormattedString("tabs.openWarningPromptMeBranded", + [brandShortName]), + warnOnOpen + ); + + reallyOpen = (buttonPressed == 0); + // don't set the pref unless they press OK and it's false + if (reallyOpen && !warnOnOpen.value) + Services.prefs.setBoolPref(WARN_ON_OPEN_PREF, false); + } + } + + return reallyOpen; + }, + + /** aItemsToOpen needs to be an array of objects of the form: + * {uri: string, isBookmark: boolean} + */ + _openTabset: function(aItemsToOpen, aEvent, aWindow) { + if (!aItemsToOpen.length) + return; + + // Prefer the caller window if it's a browser window, otherwise use + // the top browser window. + var browserWindow = null; + browserWindow = + aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ? + aWindow : this._getTopBrowserWin(); + + var urls = []; + let skipMarking = browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow); + for (let item of aItemsToOpen) { + urls.push(item.uri); + if (skipMarking) { + continue; + } + + if (item.isBookmark) + this.markPageAsFollowedBookmark(item.uri); + else + this.markPageAsTyped(item.uri); + } + + // whereToOpenLink doesn't return "window" when there's no browser window + // open (Bug 630255). + var where = browserWindow ? + browserWindow.whereToOpenLink(aEvent, false, true) : "window"; + if (where == "window") { + // There is no browser window open, thus open a new one. + var uriList = PlacesUtils.toISupportsString(urls.join("|")); + var args = Cc["@mozilla.org/supports-array;1"]. + createInstance(Ci.nsISupportsArray); + args.AppendElement(uriList); + browserWindow = Services.ww.openWindow(aWindow, + "chrome://browser/content/browser.xul", + null, "chrome,dialog=no,all", args); + return; + } + + var loadInBackground = where == "tabshifted" ? true : false; + // For consistency, we want all the bookmarks to open in new tabs, instead + // of having one of them replace the currently focused tab. Hence we call + // loadTabs with aReplace set to false. + browserWindow.gBrowser.loadTabs(urls, loadInBackground, false); + }, + + openLiveMarkNodesInTabs: + function(aNode, aEvent, aView) { + let window = aView.ownerWindow; + + PlacesUtils.livemarks.getLivemark({id: aNode.itemId}) + .then(aLivemark => { + urlsToOpen = []; + + let nodes = aLivemark.getNodesForContainer(aNode); + for (let node of nodes) { + urlsToOpen.push({uri: node.uri, isBookmark: false}); + } + + if (this._confirmOpenInTabs(urlsToOpen.length, window)) { + this._openTabset(urlsToOpen, aEvent, window); + } + }, Cu.reportError); + }, + + openContainerNodeInTabs: + function(aNode, aEvent, aView) { + let window = aView.ownerWindow; + + let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode); + if (this._confirmOpenInTabs(urlsToOpen.length, window)) { + this._openTabset(urlsToOpen, aEvent, window); + } + }, + + openURINodesInTabs: function(aNodes, aEvent, aView) { + let window = aView.ownerWindow; + + let urlsToOpen = []; + for (var i=0; i < aNodes.length; i++) { + // Skip over separators and folders. + if (PlacesUtils.nodeIsURI(aNodes[i])) + urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])}); + } + if (this._confirmOpenInTabs(urlsToOpen.length, window)) { + this._openTabset(urlsToOpen, aEvent, window); + } + }, + + /** + * Loads the node's URL in the appropriate tab or window or as a web + * panel given the user's preference specified by modifier keys tracked by a + * DOM mouse/key event. + * @param aNode + * An uri result node. + * @param aEvent + * The DOM mouse/key event with modifier keys set that track the + * user's preferred destination window or tab. + * @param aView + * The controller associated with aNode. + */ + openNodeWithEvent: + function(aNode, aEvent, aView) { + let window = aView.ownerWindow; + this._openNodeIn(aNode, window.whereToOpenLink(aEvent, false, true), window); + }, + + /** + * Loads the node's URL in the appropriate tab or window or as a + * web panel. + * see also openUILinkIn + */ + openNodeIn: function(aNode, aWhere, aView, aPrivate) { + let window = aView.ownerWindow; + this._openNodeIn(aNode, aWhere, window, aPrivate); + }, + + _openNodeIn: function(aNode, aWhere, aWindow, aPrivate=false) { + if (aNode && PlacesUtils.nodeIsURI(aNode) && + this.checkURLSecurity(aNode, aWindow)) { + let isBookmark = PlacesUtils.nodeIsBookmark(aNode); + + if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) { + if (isBookmark) + this.markPageAsFollowedBookmark(aNode.uri); + else + this.markPageAsTyped(aNode.uri); + } + + // Check whether the node is a bookmark which should be opened as + // a web panel + if (aWhere == "current" && isBookmark) { + if (PlacesUtils.annotations + .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) { + let browserWin = this._getTopBrowserWin(); + if (browserWin) { + browserWin.openWebPanel(aNode.title, aNode.uri); + return; + } + } + } + aWindow.openUILinkIn(aNode.uri, aWhere, { + inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground"), + private: aPrivate, + }); + } + }, + + /** + * Helper for guessing scheme from an url string. + * Used to avoid nsIURI overhead in frequently called UI functions. + * + * @param aUrlString the url to guess the scheme from. + * + * @return guessed scheme for this url string. + * + * @note this is not supposed be perfect, so use it only for UI purposes. + */ + guessUrlSchemeForUI: function(aUrlString) { + return aUrlString.substr(0, aUrlString.indexOf(":")); + }, + + getBestTitle: function(aNode, aDoNotCutTitle) { + var title; + if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) { + // if node title is empty, try to set the label using host and filename + // PlacesUtils._uri() will throw if aNode.uri is not a valid URI + try { + var uri = PlacesUtils._uri(aNode.uri); + var host = uri.host; + var fileName = uri.QueryInterface(Ci.nsIURL).fileName; + // if fileName is empty, use path to distinguish labels + if (aDoNotCutTitle) { + title = host + uri.path; + } else { + title = host + (fileName ? + (host ? "/" + this.ellipsis + "/" : "") + fileName : + uri.path); + } + } + catch (e) { + // Use (no title) for non-standard URIs (data:, javascript:, ...) + title = ""; + } + } + else + title = aNode.title; + + return title || this.getString("noTitle"); + }, + + get leftPaneQueries() { + // build the map + this.leftPaneFolderId; + return this.leftPaneQueries; + }, + + // Get the folder id for the organizer left-pane folder. + get leftPaneFolderId() { + let leftPaneRoot = -1; + let allBookmarksId; + + // Shortcuts to services. + let bs = PlacesUtils.bookmarks; + let as = PlacesUtils.annotations; + + // This is the list of the left pane queries. + let queries = { + "PlacesRoot": { title: "" }, + "History": { title: this.getString("OrganizerQueryHistory") }, + "Downloads": { title: this.getString("OrganizerQueryDownloads") }, + "Tags": { title: this.getString("OrganizerQueryTags") }, + "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") }, + "BookmarksToolbar": + { title: null, + concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"), + concreteId: PlacesUtils.toolbarFolderId }, + "BookmarksMenu": + { title: null, + concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"), + concreteId: PlacesUtils.bookmarksMenuFolderId }, + "UnfiledBookmarks": + { title: null, + concreteTitle: PlacesUtils.getString("UnsortedBookmarksFolderTitle"), + concreteId: PlacesUtils.unfiledBookmarksFolderId }, + }; + // All queries but PlacesRoot. + const EXPECTED_QUERY_COUNT = 7; + + // Removes an item and associated annotations, ignoring eventual errors. + function safeRemoveItem(aItemId) { + try { + if (as.itemHasAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) && + !(as.getItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) in queries)) { + // Some extension annotated their roots with our query annotation, + // so we should not delete them. + return; + } + // removeItemAnnotation does not check if item exists, nor the anno, + // so this is safe to do. + as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO); + as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO); + // This will throw if the annotation is an orphan. + bs.removeItem(aItemId); + } + catch(e) { /* orphan anno */ } + } + + // Returns true if item really exists, false otherwise. + function itemExists(aItemId) { + try { + bs.getItemIndex(aItemId); + return true; + } + catch(e) { + return false; + } + } + + // Get all items marked as being the left pane folder. + let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO); + if (items.length > 1) { + // Something went wrong, we cannot have more than one left pane folder, + // remove all left pane folders and continue. We will create a new one. + items.forEach(safeRemoveItem); + } + else if (items.length == 1 && items[0] != -1) { + leftPaneRoot = items[0]; + // Check that organizer left pane root is valid. + let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO); + if (version != this.ORGANIZER_LEFTPANE_VERSION || + !itemExists(leftPaneRoot)) { + // Invalid root, we must rebuild the left pane. + safeRemoveItem(leftPaneRoot); + leftPaneRoot = -1; + } + } + + if (leftPaneRoot != -1) { + // A valid left pane folder has been found. + // Build the leftPaneQueries Map. This is used to quickly access them, + // associating a mnemonic name to the real item ids. + delete this.leftPaneQueries; + this.leftPaneQueries = {}; + + let items = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO); + // While looping through queries we will also check for their validity. + let queriesCount = 0; + for (let i = 0; i < items.length; i++) { + let queryName = as.getItemAnnotation(items[i], this.ORGANIZER_QUERY_ANNO); + + // Some extension did use our annotation to decorate their items + // with icons, so we should check only our elements, to avoid dataloss. + if (!(queryName in queries)) + continue; + + let query = queries[queryName]; + query.itemId = items[i]; + + if (!itemExists(query.itemId)) { + // Orphan annotation, bail out and create a new left pane root. + break; + } + + // Check that all queries have valid parents. + let parentId = bs.getFolderIdForItem(query.itemId); + if (items.indexOf(parentId) == -1 && parentId != leftPaneRoot) { + // The parent is not part of the left pane, bail out and create a new + // left pane root. + break; + } + + // Titles could have been corrupted or the user could have changed his + // locale. Check title and eventually fix it. + if (bs.getItemTitle(query.itemId) != query.title) + bs.setItemTitle(query.itemId, query.title); + if ("concreteId" in query) { + if (bs.getItemTitle(query.concreteId) != query.concreteTitle) + bs.setItemTitle(query.concreteId, query.concreteTitle); + } + + // Add the query to our cache. + this.leftPaneQueries[queryName] = query.itemId; + queriesCount++; + } + + if (queriesCount != EXPECTED_QUERY_COUNT) { + // Queries number is wrong, so the left pane must be corrupt. + // Note: we can't just remove the leftPaneRoot, because some query could + // have a bad parent, so we have to remove all items one by one. + items.forEach(safeRemoveItem); + safeRemoveItem(leftPaneRoot); + } + else { + // Everything is fine, return the current left pane folder. + delete this.leftPaneFolderId; + return this.leftPaneFolderId = leftPaneRoot; + } + } + + // Create a new left pane folder. + var callback = { + // Helper to create an organizer special query. + create_query: function(aQueryName, aParentId, aQueryUrl) { + let itemId = bs.insertBookmark(aParentId, + PlacesUtils._uri(aQueryUrl), + bs.DEFAULT_INDEX, + queries[aQueryName].title); + // Mark as special organizer query. + as.setItemAnnotation(itemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aQueryName, + 0, as.EXPIRE_NEVER); + // We should never backup this, since it changes between profiles. + as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, + 0, as.EXPIRE_NEVER); + // Add to the queries map. + PlacesUIUtils.leftPaneQueries[aQueryName] = itemId; + return itemId; + }, + + // Helper to create an organizer special folder. + create_folder: function(aFolderName, aParentId, aIsRoot) { + // Left Pane Root Folder. + let folderId = bs.createFolder(aParentId, + queries[aFolderName].title, + bs.DEFAULT_INDEX); + // We should never backup this, since it changes between profiles. + as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, + 0, as.EXPIRE_NEVER); + + if (aIsRoot) { + // Mark as special left pane root. + as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO, + PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, + 0, as.EXPIRE_NEVER); + } + else { + // Mark as special organizer folder. + as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName, + 0, as.EXPIRE_NEVER); + PlacesUIUtils.leftPaneQueries[aFolderName] = folderId; + } + return folderId; + }, + + runBatched: function(aUserData) { + delete PlacesUIUtils.leftPaneQueries; + PlacesUIUtils.leftPaneQueries = { }; + + // Left Pane Root Folder. + leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true); + + // History Query. + this.create_query("History", leftPaneRoot, + "place:type=" + + Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY + + "&sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); + + // Downloads. + this.create_query("Downloads", leftPaneRoot, + "place:transition=" + + Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + + "&sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); + + // Tags Query. + this.create_query("Tags", leftPaneRoot, + "place:type=" + + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY + + "&sort=" + + Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING); + + // All Bookmarks Folder. + allBookmarksId = this.create_folder("AllBookmarks", leftPaneRoot, false); + + // All Bookmarks->Bookmarks Toolbar Query. + this.create_query("BookmarksToolbar", allBookmarksId, + "place:folder=TOOLBAR"); + + // All Bookmarks->Bookmarks Menu Query. + this.create_query("BookmarksMenu", allBookmarksId, + "place:folder=BOOKMARKS_MENU"); + + // All Bookmarks->Unfiled Bookmarks Query. + this.create_query("UnfiledBookmarks", allBookmarksId, + "place:folder=UNFILED_BOOKMARKS"); + } + }; + bs.runInBatchMode(callback, null); + // Maybe: PlacesUtils.bookmarks.runInBatchMode(callback, null); ? + + delete this.leftPaneFolderId; + return this.leftPaneFolderId = leftPaneRoot; + }, + + /** + * Get the folder id for the organizer left-pane folder. + */ + get allBookmarksFolderId() { + // ensure the left-pane root is initialized; + this.leftPaneFolderId; + delete this.allBookmarksFolderId; + return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"]; + }, + + /** + * If an item is a left-pane query, returns the name of the query + * or an empty string if not. + * + * @param aItemId id of a container + * @returns the name of the query, or empty string if not a left-pane query + */ + getLeftPaneQueryNameFromId: function(aItemId) { + var queryName = ""; + // If the let pane hasn't been built, use the annotation service + // directly, to avoid building the left pane too early. + if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) { + try { + queryName = PlacesUtils.annotations. + getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO); + } + catch (ex) { + // doesn't have the annotation + queryName = ""; + } + } + else { + // If the left pane has already been built, use the name->id map + // cached in PlacesUIUtils. + for (let [name, id] in Iterator(this.leftPaneQueries)) { + if (aItemId == id) + queryName = name; + } + } + return queryName; + }, + + /** + * Returns the passed URL with a #moz-resolution fragment + * for the specified dimensions and devicePixelRatio. + * + * @param aWindow + * A window from where we want to get the device + * pixel Ratio + * + * @param aURL + * The URL where we should add the fragment + * + * @param aWidth + * The target image width + * + * @param aHeight + * The target image height + * + * @return The URL with the fragment at the end + */ + getImageURLForResolution: + function(aWindow, aURL, aWidth, aHeight) { + return aURL; + } +}; + +XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF", + "@mozilla.org/rdf/rdf-service;1", + "nsIRDFService"); + +XPCOMUtils.defineLazyGetter(PlacesUIUtils, "localStore", function() { + return PlacesUIUtils.RDF.GetDataSource("rdf:local-store"); +}); + +XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() { + return Services.prefs.getComplexValue("intl.ellipsis", + Ci.nsIPrefLocalizedString).data; +}); + +XPCOMUtils.defineLazyServiceGetter(this, "URIFixup", + "@mozilla.org/docshell/urifixup;1", + "nsIURIFixup"); + +XPCOMUtils.defineLazyGetter(this, "bundle", function() { + const PLACES_STRING_BUNDLE_URI = + "chrome://browser/locale/places/places.properties"; + return Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(PLACES_STRING_BUNDLE_URI); +}); + +XPCOMUtils.defineLazyServiceGetter(this, "focusManager", + "@mozilla.org/focus-manager;1", + "nsIFocusManager"); + +/** + * This is a compatibility shim for old PUIU.ptm users. + * + * If you're looking for transactions and writing new code using them, directly + * use the transactions objects exported by the PlacesUtils.jsm module. + * + * This object will be removed once enough users are converted to the new API. + */ +XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() { + // Ensure PlacesUtils is imported in scope. + PlacesUtils; + + return { + aggregateTransactions: function(aName, aTransactions) + new PlacesAggregatedTransaction(aName, aTransactions), + + createFolder: function(aName, aContainer, aIndex, aAnnotations, + aChildItemsTransactions) + new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations, + aChildItemsTransactions), + + createItem: function(aURI, aContainer, aIndex, aTitle, aKeyword, + aAnnotations, aChildTransactions) + new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle, + aKeyword, aAnnotations, + aChildTransactions), + + createSeparator: function(aContainer, aIndex) + new PlacesCreateSeparatorTransaction(aContainer, aIndex), + + createLivemark: function(aFeedURI, aSiteURI, aName, aContainer, aIndex, + aAnnotations) + new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer, + aIndex, aAnnotations), + + moveItem: function(aItemId, aNewContainer, aNewIndex) + new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex), + + removeItem: function(aItemId) + new PlacesRemoveItemTransaction(aItemId), + + editItemTitle: function(aItemId, aNewTitle) + new PlacesEditItemTitleTransaction(aItemId, aNewTitle), + + editBookmarkURI: function(aItemId, aNewURI) + new PlacesEditBookmarkURITransaction(aItemId, aNewURI), + + setItemAnnotation: function(aItemId, aAnnotationObject) + new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject), + + setPageAnnotation: function(aURI, aAnnotationObject) + new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject), + + editBookmarkKeyword: function(aItemId, aNewKeyword) + new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword), + + editBookmarkPostData: function(aItemId, aPostData) + new PlacesEditBookmarkPostDataTransaction(aItemId, aPostData), + + editLivemarkSiteURI: function(aLivemarkId, aSiteURI) + new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI), + + editLivemarkFeedURI: function(aLivemarkId, aFeedURI) + new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI), + + editItemDateAdded: function(aItemId, aNewDateAdded) + new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded), + + editItemLastModified: function(aItemId, aNewLastModified) + new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified), + + sortFolderByName: function(aFolderId) + new PlacesSortFolderByNameTransaction(aFolderId), + + tagURI: function(aURI, aTags) + new PlacesTagURITransaction(aURI, aTags), + + untagURI: function(aURI, aTags) + new PlacesUntagURITransaction(aURI, aTags), + + /** + * Transaction for setting/unsetting Load-in-sidebar annotation. + * + * @param aBookmarkId + * id of the bookmark where to set Load-in-sidebar annotation. + * @param aLoadInSidebar + * boolean value. + * @returns nsITransaction object. + */ + setLoadInSidebar: function(aItemId, aLoadInSidebar) + { + let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO, + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: aLoadInSidebar, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + return new PlacesSetItemAnnotationTransaction(aItemId, annoObj); + }, + + /** + * Transaction for editing the description of a bookmark or a folder. + * + * @param aItemId + * id of the item to edit. + * @param aDescription + * new description. + * @returns nsITransaction object. + */ + editItemDescription: function(aItemId, aDescription) + { + let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO, + type: Ci.nsIAnnotationService.TYPE_STRING, + flags: 0, + value: aDescription, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + return new PlacesSetItemAnnotationTransaction(aItemId, annoObj); + }, + + //////////////////////////////////////////////////////////////////////////// + //// nsITransactionManager forwarders. + + beginBatch: function() + PlacesUtils.transactionManager.beginBatch(null), + + endBatch: function() + PlacesUtils.transactionManager.endBatch(false), + + doTransaction: function(txn) + PlacesUtils.transactionManager.doTransaction(txn), + + undoTransaction: function() + PlacesUtils.transactionManager.undoTransaction(), + + redoTransaction: function() + PlacesUtils.transactionManager.redoTransaction(), + + get numberOfUndoItems() + PlacesUtils.transactionManager.numberOfUndoItems, + get numberOfRedoItems() + PlacesUtils.transactionManager.numberOfRedoItems, + get maxTransactionCount() + PlacesUtils.transactionManager.maxTransactionCount, + set maxTransactionCount(val) + PlacesUtils.transactionManager.maxTransactionCount = val, + + clear: function() + PlacesUtils.transactionManager.clear(), + + peekUndoStack: function() + PlacesUtils.transactionManager.peekUndoStack(), + + peekRedoStack: function() + PlacesUtils.transactionManager.peekRedoStack(), + + getUndoStack: function() + PlacesUtils.transactionManager.getUndoStack(), + + getRedoStack: function() + PlacesUtils.transactionManager.getRedoStack(), + + AddListener: function(aListener) + PlacesUtils.transactionManager.AddListener(aListener), + + RemoveListener: function(aListener) + PlacesUtils.transactionManager.RemoveListener(aListener) + } +}); diff --git a/browser/components/places/content/bookmarkProperties.js b/browser/components/places/content/bookmarkProperties.js new file mode 100644 index 000000000..7eae82715 --- /dev/null +++ b/browser/components/places/content/bookmarkProperties.js @@ -0,0 +1,675 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * The panel is initialized based on data given in the js object passed + * as window.arguments[0]. The object must have the following fields set: + * @ action (String). Possible values: + * - "add" - for adding a new item. + * @ type (String). Possible values: + * - "bookmark" + * @ loadBookmarkInSidebar - optional, the default state for the + * "Load this bookmark in the sidebar" field. + * - "folder" + * @ URIList (Array of nsIURI objects) - optional, list of uris to + * be bookmarked under the new folder. + * - "livemark" + * @ uri (nsIURI object) - optional, the default uri for the new item. + * The property is not used for the "folder with items" type. + * @ title (String) - optional, the default title for the new item. + * @ description (String) - optional, the default description for the new + * item. + * @ defaultInsertionPoint (InsertionPoint JS object) - optional, the + * default insertion point for the new item. + * @ keyword (String) - optional, the default keyword for the new item. + * @ postData (String) - optional, POST data to accompany the keyword. + * @ charSet (String) - optional, character-set to accompany the keyword. + * Notes: + * 1) If |uri| is set for a bookmark/livemark item and |title| isn't, + * the dialog will query the history tables for the title associated + * with the given uri. If the dialog is set to adding a folder with + * bookmark items under it (see URIList), a default static title is + * used ("[Folder Name]"). + * 2) The index field of the default insertion point is ignored if + * the folder picker is shown. + * - "edit" - for editing a bookmark item or a folder. + * @ type (String). Possible values: + * - "bookmark" + * @ itemId (Integer) - the id of the bookmark item. + * - "folder" (also applies to livemarks) + * @ itemId (Integer) - the id of the folder. + * @ hiddenRows (Strings array) - optional, list of rows to be hidden + * regardless of the item edited or added by the dialog. + * Possible values: + * - "title" + * - "location" + * - "description" + * - "keyword" + * - "tags" + * - "loadInSidebar" + * - "feedLocation" + * - "siteLocation" + * - "folderPicker" - hides both the tree and the menu. + * @ readOnly (Boolean) - optional, states if the panel should be read-only + * + * window.arguments[0].performed is set to true if any transaction has + * been performed by the dialog. + */ + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const BOOKMARK_ITEM = 0; +const BOOKMARK_FOLDER = 1; +const LIVEMARK_CONTAINER = 2; + +const ACTION_EDIT = 0; +const ACTION_ADD = 1; + +var elementsHeight = new Map(); + +var BookmarkPropertiesPanel = { + + /** UI Text Strings */ + __strings: null, + get _strings() { + if (!this.__strings) { + this.__strings = document.getElementById("stringBundle"); + } + return this.__strings; + }, + + _action: null, + _itemType: null, + _itemId: -1, + _uri: null, + _loadInSidebar: false, + _title: "", + _description: "", + _URIs: [], + _keyword: "", + _postData: null, + _charSet: "", + _feedURI: null, + _siteURI: null, + + _defaultInsertionPoint: null, + _hiddenRows: [], + _batching: false, + _readOnly: false, + + /** + * This method returns the correct label for the dialog's "accept" + * button based on the variant of the dialog. + */ + _getAcceptLabel: function() { + if (this._action == ACTION_ADD) { + if (this._URIs.length) + return this._strings.getString("dialogAcceptLabelAddMulti"); + + if (this._itemType == LIVEMARK_CONTAINER) + return this._strings.getString("dialogAcceptLabelAddLivemark"); + + if (this._dummyItem || this._loadInSidebar) + return this._strings.getString("dialogAcceptLabelAddItem"); + + return this._strings.getString("dialogAcceptLabelSaveItem"); + } + return this._strings.getString("dialogAcceptLabelEdit"); + }, + + /** + * This method returns the correct title for the current variant + * of this dialog. + */ + _getDialogTitle: function() { + if (this._action == ACTION_ADD) { + if (this._itemType == BOOKMARK_ITEM) + return this._strings.getString("dialogTitleAddBookmark"); + if (this._itemType == LIVEMARK_CONTAINER) + return this._strings.getString("dialogTitleAddLivemark"); + + // add folder + NS_ASSERT(this._itemType == BOOKMARK_FOLDER, "Unknown item type"); + if (this._URIs.length) + return this._strings.getString("dialogTitleAddMulti"); + + return this._strings.getString("dialogTitleAddFolder"); + } + if (this._action == ACTION_EDIT) { + return this._strings.getFormattedString("dialogTitleEdit", [this._title]); + } + return ""; + }, + + /** + * Determines the initial data for the item edited or added by this dialog + */ + _determineItemInfo: function() { + var dialogInfo = window.arguments[0]; + this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT; + this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : []; + if (this._action == ACTION_ADD) { + NS_ASSERT("type" in dialogInfo, "missing type property for add action"); + + if ("title" in dialogInfo) + this._title = dialogInfo.title; + + if ("defaultInsertionPoint" in dialogInfo) { + this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint; + } + else + this._defaultInsertionPoint = + new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); + + switch (dialogInfo.type) { + case "bookmark": + this._itemType = BOOKMARK_ITEM; + if ("uri" in dialogInfo) { + NS_ASSERT(dialogInfo.uri instanceof Ci.nsIURI, + "uri property should be a uri object"); + this._uri = dialogInfo.uri; + if (typeof(this._title) != "string") { + this._title = this._getURITitleFromHistory(this._uri) || + this._uri.spec; + } + } + else { + this._uri = PlacesUtils._uri("about:blank"); + this._title = this._strings.getString("newBookmarkDefault"); + this._dummyItem = true; + } + + if ("loadBookmarkInSidebar" in dialogInfo) + this._loadInSidebar = dialogInfo.loadBookmarkInSidebar; + + if ("keyword" in dialogInfo) { + this._keyword = dialogInfo.keyword; + this._isAddKeywordDialog = true; + if ("postData" in dialogInfo) + this._postData = dialogInfo.postData; + if ("charSet" in dialogInfo) + this._charSet = dialogInfo.charSet; + } + break; + + case "folder": + this._itemType = BOOKMARK_FOLDER; + if (!this._title) { + if ("URIList" in dialogInfo) { + this._title = this._strings.getString("bookmarkAllTabsDefault"); + this._URIs = dialogInfo.URIList; + } + else + this._title = this._strings.getString("newFolderDefault"); + this._dummyItem = true; + } + break; + + case "livemark": + this._itemType = LIVEMARK_CONTAINER; + if ("feedURI" in dialogInfo) + this._feedURI = dialogInfo.feedURI; + if ("siteURI" in dialogInfo) + this._siteURI = dialogInfo.siteURI; + + if (!this._title) { + if (this._feedURI) { + this._title = this._getURITitleFromHistory(this._feedURI) || + this._feedURI.spec; + } + else + this._title = this._strings.getString("newLivemarkDefault"); + } + } + + if ("description" in dialogInfo) + this._description = dialogInfo.description; + } + else { // edit + NS_ASSERT("itemId" in dialogInfo); + this._itemId = dialogInfo.itemId; + this._title = PlacesUtils.bookmarks.getItemTitle(this._itemId); + this._readOnly = !!dialogInfo.readOnly; + + switch (dialogInfo.type) { + case "bookmark": + this._itemType = BOOKMARK_ITEM; + + this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + // keyword + this._keyword = PlacesUtils.bookmarks + .getKeywordForBookmark(this._itemId); + // Load In Sidebar + this._loadInSidebar = PlacesUtils.annotations + .itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + break; + + case "folder": + this._itemType = BOOKMARK_FOLDER; + PlacesUtils.livemarks.getLivemark({ id: this._itemId }) + .then(aLivemark => { + this._itemType = LIVEMARK_CONTAINER; + this._feedURI = aLivemark.feedURI; + this._siteURI = aLivemark.siteURI; + this._fillEditProperties(); + + let acceptButton = document.documentElement.getButton("accept"); + acceptButton.disabled = !this._inputIsValid(); + + let newHeight = window.outerHeight + + this._element("descriptionField").boxObject.height; + window.resizeTo(window.outerWidth, newHeight); + }, () => undefined); + + break; + } + + // Description + if (PlacesUtils.annotations + .itemHasAnnotation(this._itemId, PlacesUIUtils.DESCRIPTION_ANNO)) { + this._description = PlacesUtils.annotations + .getItemAnnotation(this._itemId, + PlacesUIUtils.DESCRIPTION_ANNO); + } + } + }, + + /** + * This method returns the title string corresponding to a given URI. + * If none is available from the bookmark service (probably because + * the given URI doesn't appear in bookmarks or history), we synthesize + * a title from the first 100 characters of the URI. + * + * @param aURI + * nsIURI object for which we want the title + * + * @returns a title string + */ + _getURITitleFromHistory: function(aURI) { + NS_ASSERT(aURI instanceof Ci.nsIURI); + + // get the title from History + return PlacesUtils.history.getPageTitle(aURI); + }, + + /** + * This method should be called by the onload of the Bookmark Properties + * dialog to initialize the state of the panel. + */ + onDialogLoad: Task.async(function* () { + this._determineItemInfo(); + + document.title = this._getDialogTitle(); + var acceptButton = document.documentElement.getButton("accept"); + acceptButton.label = this._getAcceptLabel(); + + // Do not use sizeToContent, otherwise, due to bug 90276, the dialog will + // grow at every opening. + // Since elements can be uncollapsed asynchronously, we must observe their + // mutations and resize the dialog using a cached element size. + this._height = window.outerHeight; + this._mutationObserver = new MutationObserver(mutations => { + for (let mutation of mutations) { + let target = mutation.target; + let id = target.id; + if (!/^editBMPanel_.*(Row|Checkbox)$/.test(id)) + continue; + + let collapsed = target.getAttribute("collapsed") === "true"; + let wasCollapsed = mutation.oldValue === "true"; + if (collapsed == wasCollapsed) + continue; + + if (collapsed) { + this._height -= elementsHeight.get(id); + elementsHeight.delete(id); + } else { + elementsHeight.set(id, target.boxObject.height); + this._height += elementsHeight.get(id); + } + window.resizeTo(window.outerWidth, this._height); + } + }); + + this._mutationObserver.observe(document, + { subtree: true, + attributeOldValue: true, + attributeFilter: ["collapsed"] }); + + // Some controls are flexible and we want to update their cached size when + // the dialog is resized. + window.addEventListener("resize", this); + + this._beginBatch(); + + switch (this._action) { + case ACTION_EDIT: + this._fillEditProperties(); + acceptButton.disabled = this._readOnly; + break; + case ACTION_ADD: + yield this._fillAddProperties(); + // if this is an uri related dialog disable accept button until + // the user fills an uri value. + if (this._itemType == BOOKMARK_ITEM) + acceptButton.disabled = !this._inputIsValid(); + break; + } + + if (!this._readOnly) { + // Listen on uri fields to enable accept button if input is valid + if (this._itemType == BOOKMARK_ITEM) { + this._element("locationField") + .addEventListener("input", this, false); + if (this._isAddKeywordDialog) { + this._element("keywordField") + .addEventListener("input", this, false); + } + } + else if (this._itemType == LIVEMARK_CONTAINER) { + this._element("feedLocationField") + .addEventListener("input", this, false); + this._element("siteLocationField") + .addEventListener("input", this, false); + } + } + + // Ensure the Name Picker textbox is focused on load + var namePickerElem = document.getElementById('editBMPanel_namePicker'); + namePickerElem.focus(); + namePickerElem.select(); + }), + + // nsIDOMEventListener + handleEvent: function(aEvent) { + var target = aEvent.target; + switch (aEvent.type) { + case "input": + if (target.id == "editBMPanel_locationField" || + target.id == "editBMPanel_feedLocationField" || + target.id == "editBMPanel_siteLocationField" || + target.id == "editBMPanel_keywordField") { + // Check uri fields to enable accept button if input is valid + document.documentElement + .getButton("accept").disabled = !this._inputIsValid(); + } + break; + case "resize": + for (let [id, oldHeight] of elementsHeight) { + let newHeight = document.getElementById(id).boxObject.height; + this._height += - oldHeight + newHeight; + elementsHeight.set(id, newHeight); + } + break; + } + }, + + _beginBatch: function() { + if (this._batching) + return; + + PlacesUtils.transactionManager.beginBatch(null); + this._batching = true; + }, + + _endBatch: function() { + if (!this._batching) + return; + + PlacesUtils.transactionManager.endBatch(false); + this._batching = false; + }, + + _fillEditProperties: function() { + gEditItemOverlay.initPanel(this._itemId, + { hiddenRows: this._hiddenRows, + forceReadOnly: this._readOnly }); + }, + + _fillAddProperties: Task.async(function* () { + yield this._createNewItem(); + // Edit the new item + gEditItemOverlay.initPanel(this._itemId, + { hiddenRows: this._hiddenRows }); + // Empty location field if the uri is about:blank, this way inserting a new + // url will be easier for the user, Accept button will be automatically + // disabled by the input listener until the user fills the field. + var locationField = this._element("locationField"); + if (locationField.value == "about:blank") + locationField.value = ""; + }), + + // nsISupports + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDOMEventListener) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_NOINTERFACE; + }, + + _element: function(aID) { + return document.getElementById("editBMPanel_" + aID); + }, + + onDialogUnload: function() { + // gEditItemOverlay does not exist anymore here, so don't rely on it. + this._mutationObserver.disconnect(); + delete this._mutationObserver; + + window.removeEventListener("resize", this); + + // Calling removeEventListener with arguments which do not identify any + // currently registered EventListener on the EventTarget has no effect. + this._element("locationField") + .removeEventListener("input", this, false); + this._element("feedLocationField") + .removeEventListener("input", this, false); + this._element("siteLocationField") + .removeEventListener("input", this, false); + }, + + onDialogAccept: function() { + // We must blur current focused element to save its changes correctly + document.commandDispatcher.focusedElement.blur(); + // The order here is important! We have to uninit the panel first, otherwise + // late changes could force it to commit more transactions. + gEditItemOverlay.uninitPanel(true); + this._endBatch(); + window.arguments[0].performed = true; + }, + + onDialogCancel: function() { + // The order here is important! We have to uninit the panel first, otherwise + // changes done as part of Undo may change the panel contents and by + // that force it to commit more transactions. + gEditItemOverlay.uninitPanel(true); + this._endBatch(); + PlacesUtils.transactionManager.undoTransaction(); + window.arguments[0].performed = false; + }, + + /** + * This method checks to see if the input fields are in a valid state. + * + * @returns true if the input is valid, false otherwise + */ + _inputIsValid: function() { + if (this._itemType == BOOKMARK_ITEM && + !this._containsValidURI("locationField")) + return false; + if (this._isAddKeywordDialog && !this._element("keywordField").value.length) + return false; + + return true; + }, + + /** + * Determines whether the XUL textbox with the given ID contains a + * string that can be converted into an nsIURI. + * + * @param aTextboxID + * the ID of the textbox element whose contents we'll test + * + * @returns true if the textbox contains a valid URI string, false otherwise + */ + _containsValidURI: function(aTextboxID) { + try { + var value = this._element(aTextboxID).value; + if (value) { + PlacesUIUtils.createFixedURI(value); + return true; + } + } catch (e) { } + return false; + }, + + /** + * [New Item Mode] Get the insertion point details for the new item, given + * dialog state and opening arguments. + * + * The container-identifier and insertion-index are returned separately in + * the form of [containerIdentifier, insertionIndex] + */ + _getInsertionPointDetails: function() { + var containerId = this._defaultInsertionPoint.itemId; + var indexInContainer = this._defaultInsertionPoint.index; + + return [containerId, indexInContainer]; + }, + + /** + * Returns a transaction for creating a new bookmark item representing the + * various fields and opening arguments of the dialog. + */ + _getCreateNewBookmarkTransaction: + function(aContainer, aIndex) { + var annotations = []; + var childTransactions = []; + + if (this._description) { + let annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO, + type : Ci.nsIAnnotationService.TYPE_STRING, + flags : 0, + value : this._description, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + let editItemTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj); + childTransactions.push(editItemTxn); + } + + if (this._loadInSidebar) { + let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO, + value : true }; + let setLoadTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj); + childTransactions.push(setLoadTxn); + } + + if (this._postData) { + let postDataTxn = new PlacesEditBookmarkPostDataTransaction(-1, this._postData); + childTransactions.push(postDataTxn); + } + + //XXX TODO: this should be in a transaction! + if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window)) + PlacesUtils.setCharsetForURI(this._uri, this._charSet); + + let createTxn = new PlacesCreateBookmarkTransaction(this._uri, + aContainer, + aIndex, + this._title, + this._keyword, + annotations, + childTransactions); + + return new PlacesAggregatedTransaction(this._getDialogTitle(), + [createTxn]); + }, + + /** + * Returns a childItems-transactions array representing the URIList with + * which the dialog has been opened. + */ + _getTransactionsForURIList: function() { + var transactions = []; + for (var i = 0; i < this._URIs.length; ++i) { + var uri = this._URIs[i]; + var title = this._getURITitleFromHistory(uri); + var createTxn = new PlacesCreateBookmarkTransaction(uri, -1, + PlacesUtils.bookmarks.DEFAULT_INDEX, + title); + transactions.push(createTxn); + } + return transactions; + }, + + /** + * Returns a transaction for creating a new folder item representing the + * various fields and opening arguments of the dialog. + */ + _getCreateNewFolderTransaction: + function(aContainer, aIndex) { + var annotations = []; + var childItemsTransactions; + if (this._URIs.length) + childItemsTransactions = this._getTransactionsForURIList(); + + if (this._description) + annotations.push(this._getDescriptionAnnotation(this._description)); + + return new PlacesCreateFolderTransaction(this._title, aContainer, + aIndex, annotations, + childItemsTransactions); + }, + + /** + * Returns a transaction for creating a new live-bookmark item representing + * the various fields and opening arguments of the dialog. + */ + _getCreateNewLivemarkTransaction: + function(aContainer, aIndex) { + return new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI, + this._title, + aContainer, aIndex); + }, + + /** + * Dialog-accept code-path for creating a new item (any type) + */ + _createNewItem: Task.async(function* () { + var [container, index] = this._getInsertionPointDetails(); + var txn; + + switch (this._itemType) { + case BOOKMARK_FOLDER: + txn = this._getCreateNewFolderTransaction(container, index); + break; + case LIVEMARK_CONTAINER: + txn = this._getCreateNewLivemarkTransaction(container, index); + break; + default: // BOOKMARK_ITEM + txn = this._getCreateNewBookmarkTransaction(container, index); + } + + PlacesUtils.transactionManager.doTransaction(txn); + // This is a temporary hack until we use PlacesTransactions.jsm + if (txn._promise) { + yield txn._promise; + } + + let folderGuid = yield PlacesUtils.promiseItemGuid(container); + let bm = yield PlacesUtils.bookmarks.fetch({ + parentGuid: folderGuid, + index: index + }); + this._itemId = yield PlacesUtils.promiseItemId(bm.guid); + }) +}; diff --git a/browser/components/places/content/bookmarkProperties.xul b/browser/components/places/content/bookmarkProperties.xul new file mode 100644 index 000000000..2c04f8b05 --- /dev/null +++ b/browser/components/places/content/bookmarkProperties.xul @@ -0,0 +1,43 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> + +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?> + +<!DOCTYPE dialog [ + <!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd"> + %editBookmarkOverlayDTD; +]> + +<dialog id="bookmarkproperties" + buttons="accept, cancel" + buttoniconaccept="save" + ondialogaccept="BookmarkPropertiesPanel.onDialogAccept();" + ondialogcancel="BookmarkPropertiesPanel.onDialogCancel();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="BookmarkPropertiesPanel.onDialogLoad();" + onunload="BookmarkPropertiesPanel.onDialogUnload();" + style="min-width: 30em;" + persist="screenX screenY width"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="stringBundle" + src="chrome://browser/locale/places/bookmarkProperties.properties"/> + </stringbundleset> + + <script type="application/javascript" + src="chrome://browser/content/places/editBookmarkOverlay.js"/> + <script type="application/javascript" + src="chrome://browser/content/places/bookmarkProperties.js"/> + +<vbox id="editBookmarkPanelContent"/> + +</dialog> diff --git a/browser/components/places/content/bookmarksPanel.js b/browser/components/places/content/bookmarksPanel.js new file mode 100644 index 000000000..c964bd094 --- /dev/null +++ b/browser/components/places/content/bookmarksPanel.js @@ -0,0 +1,25 @@ +/* -*- Mode: Java; 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/. */ + +function init() { + document.getElementById("bookmarks-view").place = + "place:queryType=1&folder=" + window.top.PlacesUIUtils.allBookmarksFolderId; +} + +function searchBookmarks(aSearchString) { + var tree = document.getElementById('bookmarks-view'); + if (!aSearchString) + tree.place = tree.place; + else + tree.applyFilter(aSearchString, + [PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.unfiledBookmarksFolderId, + PlacesUtils.toolbarFolderId]); +} + +window.addEventListener("SidebarFocused", + function() + document.getElementById("search-box").focus(), + false); diff --git a/browser/components/places/content/bookmarksPanel.xul b/browser/components/places/content/bookmarksPanel.xul new file mode 100644 index 000000000..45744bb05 --- /dev/null +++ b/browser/components/places/content/bookmarksPanel.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> <!-- -*- Mode: SGML; 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/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +<!DOCTYPE page SYSTEM "chrome://browser/locale/places/places.dtd"> + +<page id="bookmarksPanel" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="init();" + onunload="SidebarUtils.setMouseoverURL('');"> + + <script type="application/javascript" + src="chrome://browser/content/bookmarks/sidebarUtils.js"/> + <script type="application/javascript" + src="chrome://browser/content/bookmarks/bookmarksPanel.js"/> + + <commandset id="placesCommands"/> + <commandset id="editMenuCommands"/> + <keyset id="placesCommandKeys"/> + <menupopup id="placesContext"/> + + <!-- Bookmarks and history tooltip --> + <tooltip id="bhTooltip"/> + + <hbox id="sidebar-search-container" align="center"> + <label id="sidebar-search-label" + value="&search.label;" accesskey="&search.accesskey;" control="search-box"/> + <textbox id="search-box" flex="1" type="search" class="compact" + aria-controls="bookmarks-view" + oncommand="searchBookmarks(this.value);"/> + </hbox> + + <tree id="bookmarks-view" class="sidebar-placesTree" type="places" + flex="1" + hidecolumnpicker="true" + context="placesContext" + onkeypress="SidebarUtils.handleTreeKeyPress(event);" + onclick="SidebarUtils.handleTreeClick(this, event, true);" + onmousemove="SidebarUtils.handleTreeMouseMove(event);" + onmouseout="SidebarUtils.setMouseoverURL('');"> + <treecols> + <treecol id="title" flex="1" primary="true" hideheader="true"/> + </treecols> + <treechildren id="bookmarks-view-children" view="bookmarks-view" + class="sidebar-placesTreechildren" flex="1" tooltip="bhTooltip"/> + </tree> +</page> diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js new file mode 100644 index 000000000..a80e5f817 --- /dev/null +++ b/browser/components/places/content/browserPlacesViews.js @@ -0,0 +1,1726 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +/** + * The base view implements everything that's common to the toolbar and + * menu views. + */ +function PlacesViewBase(aPlace) { + this.place = aPlace; + this._controller = new PlacesController(this); + this._viewElt.controllers.appendController(this._controller); +} + +PlacesViewBase.prototype = { + // The xul element that holds the entire view. + _viewElt: null, + get viewElt() this._viewElt, + + get associatedElement() this._viewElt, + + get controllers() this._viewElt.controllers, + + // The xul element that represents the root container. + _rootElt: null, + + // Set to true for views that are represented by native widgets (i.e. + // the native mac menu). + _nativeView: false, + + QueryInterface: XPCOMUtils.generateQI( + [Components.interfaces.nsINavHistoryResultObserver, + Components.interfaces.nsISupportsWeakReference]), + + _place: "", + get place() this._place, + set place(val) { + this._place = val; + + let history = PlacesUtils.history; + let queries = { }, options = { }; + history.queryStringToQueries(val, queries, { }, options); + if (!queries.value.length) + queries.value = [history.getNewQuery()]; + + let result = history.executeQueries(queries.value, queries.value.length, + options.value); + result.addObserver(this, false); + return val; + }, + + _result: null, + get result() this._result, + set result(val) { + if (this._result == val) + return val; + + if (this._result) { + this._result.removeObserver(this); + this._resultNode.containerOpen = false; + } + + if (this._rootElt.localName == "menupopup") + this._rootElt._built = false; + + this._result = val; + if (val) { + this._resultNode = val.root; + this._rootElt._placesNode = this._resultNode; + this._domNodes = new Map(); + this._domNodes.set(this._resultNode, this._rootElt); + + // This calls _rebuild through invalidateContainer. + this._resultNode.containerOpen = true; + } + else { + this._resultNode = null; + delete this._domNodes; + } + + return val; + }, + + /** + * Gets the DOM node used for the given places node. + * + * @param aPlacesNode + * a places result node. + * @throws if there is no DOM node set for aPlacesNode. + */ + _getDOMNodeForPlacesNode: + function(aPlacesNode) { + let node = this._domNodes.get(aPlacesNode, null); + if (!node) { + throw new Error("No DOM node set for aPlacesNode.\nnode.type: " + + aPlacesNode.type + ". node.parent: " + aPlacesNode); + } + return node; + }, + + get controller() this._controller, + + get selType() "single", + selectItems: function() { }, + selectAll: function() { }, + + get selectedNode() { + if (this._contextMenuShown) { + let anchor = this._contextMenuShown.triggerNode; + if (!anchor) + return null; + + if (anchor._placesNode) + return this._rootElt == anchor ? null : anchor._placesNode; + + anchor = anchor.parentNode; + return this._rootElt == anchor ? null : (anchor._placesNode || null); + } + return null; + }, + + get hasSelection() this.selectedNode != null, + + get selectedNodes() { + let selectedNode = this.selectedNode; + return selectedNode ? [selectedNode] : []; + }, + + get removableSelectionRanges() { + // On static content the current selectedNode would be the selection's + // parent node. We don't want to allow removing a node when the + // selection is not explicit. + if (document.popupNode && + (document.popupNode == "menupopup" || !document.popupNode._placesNode)) + return []; + + return [this.selectedNodes]; + }, + + get draggableSelection() [this._draggedElt], + + get insertionPoint() { + // There is no insertion point for history queries, so bail out now and + // save a lot of work when updating commands. + let resultNode = this._resultNode; + if (PlacesUtils.nodeIsQuery(resultNode) && + PlacesUtils.asQuery(resultNode).queryOptions.queryType == + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) + return null; + + // By default, the insertion point is at the top level, at the end. + let index = PlacesUtils.bookmarks.DEFAULT_INDEX; + let container = this._resultNode; + let orientation = Ci.nsITreeView.DROP_BEFORE; + let isTag = false; + + let selectedNode = this.selectedNode; + if (selectedNode) { + let popup = document.popupNode; + if (!popup._placesNode || popup._placesNode == this._resultNode || + popup._placesNode.itemId == -1) { + // If a static menuitem is selected, or if the root node is selected, + // the insertion point is inside the folder, at the end. + container = selectedNode; + orientation = Ci.nsITreeView.DROP_ON; + } + else { + // In all other cases the insertion point is before that node. + container = selectedNode.parent; + index = container.getChildIndex(selectedNode); + isTag = PlacesUtils.nodeIsTagQuery(container); + } + } + + if (PlacesControllerDragHelper.disallowInsertion(container)) + return null; + + return new InsertionPoint(PlacesUtils.getConcreteItemId(container), + index, orientation, isTag); + }, + + buildContextMenu: function(aPopup) { + this._contextMenuShown = aPopup; + window.updateCommands("places"); + return this.controller.buildContextMenu(aPopup); + }, + + destroyContextMenu: function(aPopup) { + this._contextMenuShown = null; + }, + + _cleanPopup: function(aPopup, aDelay) { + // Remove Places nodes from the popup. + let child = aPopup._startMarker; + while (child.nextSibling != aPopup._endMarker) { + let sibling = child.nextSibling; + if (sibling._placesNode && !aDelay) { + aPopup.removeChild(sibling); + } + else if (sibling._placesNode && aDelay) { + // HACK (bug 733419): the popups originating from the OS X native + // menubar don't live-update while open, thus we don't clean it + // until the next popupshowing, to avoid zombie menuitems. + if (!aPopup._delayedRemovals) + aPopup._delayedRemovals = []; + aPopup._delayedRemovals.push(sibling); + child = child.nextSibling; + } + else { + child = child.nextSibling; + } + } + }, + + _rebuildPopup: function(aPopup) { + let resultNode = aPopup._placesNode; + if (!resultNode.containerOpen) + return; + + if (this.controller.hasCachedLivemarkInfo(resultNode)) { + this._setEmptyPopupStatus(aPopup, false); + aPopup._built = true; + this._populateLivemarkPopup(aPopup); + return; + } + + this._cleanPopup(aPopup); + + let cc = resultNode.childCount; + if (cc > 0) { + this._setEmptyPopupStatus(aPopup, false); + + for (let i = 0; i < cc; ++i) { + let child = resultNode.getChild(i); + this._insertNewItemToPopup(child, aPopup, null); + } + } + else { + this._setEmptyPopupStatus(aPopup, true); + } + aPopup._built = true; + }, + + _removeChild: function(aChild) { + // If document.popupNode pointed to this child, null it out, + // otherwise controller's command-updating may rely on the removed + // item still being "selected". + if (document.popupNode == aChild) + document.popupNode = null; + + aChild.parentNode.removeChild(aChild); + }, + + _setEmptyPopupStatus: + function(aPopup, aEmpty) { + if (!aPopup._emptyMenuitem) { + let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder"); + aPopup._emptyMenuitem = document.createElement("menuitem"); + aPopup._emptyMenuitem.setAttribute("label", label); + aPopup._emptyMenuitem.setAttribute("disabled", true); + } + + if (aEmpty) { + aPopup.setAttribute("emptyplacesresult", "true"); + // Don't add the menuitem if there is static content. + if (!aPopup._startMarker.previousSibling && + !aPopup._endMarker.nextSibling) + aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker); + } + else { + aPopup.removeAttribute("emptyplacesresult"); + try { + aPopup.removeChild(aPopup._emptyMenuitem); + } catch (ex) {} + } + }, + + _createMenuItemForPlacesNode: + function(aPlacesNode) { + this._domNodes.delete(aPlacesNode); + + let element; + let type = aPlacesNode.type; + if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { + element = document.createElement("menuseparator"); + } + else { + let itemId = aPlacesNode.itemId; + if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) { + element = document.createElement("menuitem"); + element.className = "menuitem-iconic bookmark-item menuitem-with-favicon"; + element.setAttribute("scheme", + PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri)); + } + else if (PlacesUtils.containerTypes.indexOf(type) != -1) { + element = document.createElement("menu"); + element.setAttribute("container", "true"); + + if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) { + element.setAttribute("query", "true"); + if (PlacesUtils.nodeIsTagQuery(aPlacesNode)) + element.setAttribute("tagContainer", "true"); + else if (PlacesUtils.nodeIsDay(aPlacesNode)) + element.setAttribute("dayContainer", "true"); + else if (PlacesUtils.nodeIsHost(aPlacesNode)) + element.setAttribute("hostContainer", "true"); + } + else if (itemId != -1) { + PlacesUtils.livemarks.getLivemark({ id: itemId }) + .then(aLivemark => { + element.setAttribute("livemark", "true"); + this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark); + }, () => undefined); + } + + let popup = document.createElement("menupopup"); + popup._placesNode = PlacesUtils.asContainer(aPlacesNode); + + if (!this._nativeView) { + popup.setAttribute("placespopup", "true"); + } + + element.appendChild(popup); + element.className = "menu-iconic bookmark-item"; + + this._domNodes.set(aPlacesNode, popup); + } + else + throw "Unexpected node"; + + element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode)); + + let icon = aPlacesNode.icon; + if (icon) + element.setAttribute("image", + PlacesUIUtils.getImageURLForResolution(window, icon)); + } + + element._placesNode = aPlacesNode; + if (!this._domNodes.has(aPlacesNode)) + this._domNodes.set(aPlacesNode, element); + + return element; + }, + + _insertNewItemToPopup: + function(aNewChild, aPopup, aBefore) { + let element = this._createMenuItemForPlacesNode(aNewChild); + let before = aBefore || aPopup._endMarker; + aPopup.insertBefore(element, before); + return element; + }, + + _setLivemarkSiteURIMenuItem: + function(aPopup) { + let livemarkInfo = this.controller.getCachedLivemarkInfo(aPopup._placesNode); + let siteUrl = livemarkInfo && livemarkInfo.siteURI ? + livemarkInfo.siteURI.spec : null; + if (!siteUrl && aPopup._siteURIMenuitem) { + aPopup.removeChild(aPopup._siteURIMenuitem); + aPopup._siteURIMenuitem = null; + aPopup.removeChild(aPopup._siteURIMenuseparator); + aPopup._siteURIMenuseparator = null; + } + else if (siteUrl && !aPopup._siteURIMenuitem) { + // Add "Open (Feed Name)" menuitem. + aPopup._siteURIMenuitem = document.createElement("menuitem"); + aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem"; + aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl); + aPopup._siteURIMenuitem.setAttribute("oncommand", + "openUILink(this.getAttribute('targetURI'), event);"); + + // If a user middle-clicks this item we serve the oncommand event. + // We are using checkForMiddleClick because of Bug 246720. + // Note: stopPropagation is needed to avoid serving middle-click + // with BT_onClick that would open all items in tabs. + aPopup._siteURIMenuitem.setAttribute("onclick", + "checkForMiddleClick(this, event); event.stopPropagation();"); + let label = + PlacesUIUtils.getFormattedString("menuOpenLivemarkOrigin.label", + [aPopup.parentNode.getAttribute("label")]) + aPopup._siteURIMenuitem.setAttribute("label", label); + aPopup.insertBefore(aPopup._siteURIMenuitem, aPopup._startMarker); + + aPopup._siteURIMenuseparator = document.createElement("menuseparator"); + aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker); + } + }, + + /** + * Add, update or remove the livemark status menuitem. + * @param aPopup + * The livemark container popup + * @param aStatus + * The livemark status + */ + _setLivemarkStatusMenuItem: + function(aPopup, aStatus) { + let statusMenuitem = aPopup._statusMenuitem; + if (!statusMenuitem) { + // Create the status menuitem and cache it in the popup object. + statusMenuitem = document.createElement("menuitem"); + statusMenuitem.className = "livemarkstatus-menuitem"; + statusMenuitem.setAttribute("disabled", true); + aPopup._statusMenuitem = statusMenuitem; + } + + if (aStatus == Ci.mozILivemark.STATUS_LOADING || + aStatus == Ci.mozILivemark.STATUS_FAILED) { + // Status has changed, update the cached status menuitem. + let stringId = aStatus == Ci.mozILivemark.STATUS_LOADING ? + "bookmarksLivemarkLoading" : "bookmarksLivemarkFailed"; + statusMenuitem.setAttribute("label", PlacesUIUtils.getString(stringId)); + if (aPopup._startMarker.nextSibling != statusMenuitem) + aPopup.insertBefore(statusMenuitem, aPopup._startMarker.nextSibling); + } + else { + // The livemark has finished loading. + if (aPopup._statusMenuitem.parentNode == aPopup) + aPopup.removeChild(aPopup._statusMenuitem); + } + }, + + toggleCutNode: function(aPlacesNode, aValue) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // We may get the popup for menus, but we need the menu itself. + if (elt.localName == "menupopup") + elt = elt.parentNode; + if (aValue) + elt.setAttribute("cutting", "true"); + else + elt.removeAttribute("cutting"); + }, + + nodeURIChanged: function(aPlacesNode, aURIString) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // Here we need the <menu>. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + elt.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(aURIString)); + }, + + nodeIconChanged: function(aPlacesNode) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // There's no UI representation for the root node, thus there's nothing to + // be done when the icon changes. + if (elt == this._rootElt) + return; + + // Here we need the <menu>. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + let icon = aPlacesNode.icon; + if (!icon) + elt.removeAttribute("image"); + else if (icon != elt.getAttribute("image")) + elt.setAttribute("image", + PlacesUIUtils.getImageURLForResolution(window, icon)); + }, + + nodeAnnotationChanged: + function(aPlacesNode, aAnno) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // All livemarks have a feedURI, so use it as our indicator of a livemark + // being modified. + if (aAnno == PlacesUtils.LMANNO_FEEDURI) { + let menu = elt.parentNode; + if (!menu.hasAttribute("livemark")) { + menu.setAttribute("livemark", "true"); + } + + PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId }) + .then(aLivemark => { + // Controller will use this to build the meta data for the node. + this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark); + this.invalidateContainer(aPlacesNode); + }, () => undefined); + } + }, + + nodeTitleChanged: + function(aPlacesNode, aNewTitle) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // There's no UI representation for the root node, thus there's + // nothing to be done when the title changes. + if (elt == this._rootElt) + return; + + // Here we need the <menu>. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + if (!aNewTitle && elt.localName != "toolbarbutton") { + // Many users consider toolbars as shortcuts containers, so explicitly + // allow empty labels on toolbarbuttons. For any other element try to be + // smarter, guessing a title from the uri. + elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode)); + } + else { + elt.setAttribute("label", aNewTitle); + } + }, + + nodeRemoved: + function(aParentPlacesNode, aPlacesNode, aIndex) { + let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // Here we need the <menu>. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + if (parentElt._built) { + parentElt.removeChild(elt); + + // Figure out if we need to show the "<Empty>" menu-item. + // TODO Bug 517701: This doesn't seem to handle the case of an empty + // root. + if (parentElt._startMarker.nextSibling == parentElt._endMarker) + this._setEmptyPopupStatus(parentElt, true); + } + }, + + nodeHistoryDetailsChanged: + function(aPlacesNode, aTime, aCount) { + if (aPlacesNode.parent && + this.controller.hasCachedLivemarkInfo(aPlacesNode.parent)) { + // Find the node in the parent. + let popup = this._getDOMNodeForPlacesNode(aPlacesNode.parent); + for (let child = popup._startMarker.nextSibling; + child != popup._endMarker; + child = child.nextSibling) { + if (child._placesNode && child._placesNode.uri == aPlacesNode.uri) { + if (aCount) + child.setAttribute("visited", "true"); + else + child.removeAttribute("visited"); + break; + } + } + } + }, + + nodeTagsChanged: function() { }, + nodeDateAddedChanged: function() { }, + nodeLastModifiedChanged: function() { }, + nodeKeywordChanged: function() { }, + sortingChanged: function() { }, + batching: function() { }, + + nodeInserted: + function(aParentPlacesNode, aPlacesNode, aIndex) { + let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); + if (!parentElt._built) + return; + + let index = Array.indexOf(parentElt.childNodes, parentElt._startMarker) + + aIndex + 1; + this._insertNewItemToPopup(aPlacesNode, parentElt, + parentElt.childNodes[index]); + this._setEmptyPopupStatus(parentElt, false); + }, + + nodeMoved: + function(aPlacesNode, + aOldParentPlacesNode, aOldIndex, + aNewParentPlacesNode, aNewIndex) { + // Note: the current implementation of moveItem does not actually + // use this notification when the item in question is moved from one + // folder to another. Instead, it calls nodeRemoved and nodeInserted + // for the two folders. Thus, we can assume old-parent == new-parent. + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // Here we need the <menu>. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + // If our root node is a folder, it might be moved. There's nothing + // we need to do in that case. + if (elt == this._rootElt) + return; + + let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode); + if (parentElt._built) { + // Move the node. + parentElt.removeChild(elt); + let index = Array.indexOf(parentElt.childNodes, parentElt._startMarker) + + aNewIndex + 1; + parentElt.insertBefore(elt, parentElt.childNodes[index]); + } + }, + + containerStateChanged: + function(aPlacesNode, aOldState, aNewState) { + if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED || + aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) { + this.invalidateContainer(aPlacesNode); + + if (PlacesUtils.nodeIsFolder(aPlacesNode)) { + let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; + if (queryOptions.excludeItems) { + return; + } + + PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId }) + .then(aLivemark => { + let shouldInvalidate = + !this.controller.hasCachedLivemarkInfo(aPlacesNode); + this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark); + if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) { + aLivemark.registerForUpdates(aPlacesNode, this); + // Prioritize the current livemark. + aLivemark.reload(); + PlacesUtils.livemarks.reloadLivemarks(); + if (shouldInvalidate) + this.invalidateContainer(aPlacesNode); + } + else { + aLivemark.unregisterForUpdates(aPlacesNode); + } + }, () => undefined); + } + } + }, + + _populateLivemarkPopup: function(aPopup) + { + this._setLivemarkSiteURIMenuItem(aPopup); + // Show the loading status only if there are no entries yet. + if (aPopup._startMarker.nextSibling == aPopup._endMarker) + this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING); + + PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId }) + .then(aLivemark => { + let placesNode = aPopup._placesNode; + if (!placesNode.containerOpen) + return; + + if (aLivemark.status != Ci.mozILivemark.STATUS_LOADING) + this._setLivemarkStatusMenuItem(aPopup, aLivemark.status); + this._cleanPopup(aPopup, + this._nativeView && aPopup.parentNode.hasAttribute("open")); + + let children = aLivemark.getNodesForContainer(placesNode); + for (let i = 0; i < children.length; i++) { + let child = children[i]; + this.nodeInserted(placesNode, child, i); + if (child.accessCount) + this._getDOMNodeForPlacesNode(child).setAttribute("visited", true); + else + this._getDOMNodeForPlacesNode(child).removeAttribute("visited"); + } + }, Components.utils.reportError); + }, + + invalidateContainer: function(aPlacesNode) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + elt._built = false; + + // If the menupopup is open we should live-update it. + if (elt.parentNode.open) + this._rebuildPopup(elt); + }, + + uninit: function() { + if (this._result) { + this._result.removeObserver(this); + this._resultNode.containerOpen = false; + this._resultNode = null; + this._result = null; + } + + if (this._controller) { + this._controller.terminate(); + // Removing the controller will fail if it is already no longer there. + // This can happen if the view element was removed/reinserted without + // our knowledge. There is no way to check for that having happened + // without the possibility of an exception. :-( + try { + this._viewElt.controllers.removeController(this._controller); + } catch (ex) { + } finally { + this._controller = null; + } + } + + delete this._viewElt._placesView; + }, + + get isRTL() { + if ("_isRTL" in this) + return this._isRTL; + + return this._isRTL = document.defaultView + .getComputedStyle(this.viewElt, "") + .direction == "rtl"; + }, + + get ownerWindow() window, + + /** + * Adds an "Open All in Tabs" menuitem to the bottom of the popup. + * @param aPopup + * a Places popup. + */ + _mayAddCommandsItems: function(aPopup) { + // The command items are never added to the root popup. + if (aPopup == this._rootElt) + return; + + let hasMultipleURIs = false; + + // Check if the popup contains at least 2 menuitems with places nodes. + // We don't currently support opening multiple uri nodes when they are not + // populated by the result. + if (aPopup._placesNode.childCount > 0) { + let currentChild = aPopup.firstChild; + let numURINodes = 0; + while (currentChild) { + if (currentChild.localName == "menuitem" && currentChild._placesNode) { + if (++numURINodes == 2) + break; + } + currentChild = currentChild.nextSibling; + } + hasMultipleURIs = numURINodes > 1; + } + + let isLiveMark = false; + if (this.controller.hasCachedLivemarkInfo(aPopup._placesNode)) { + hasMultipleURIs = true; + isLiveMark = true; + } + + if (!hasMultipleURIs) { + // We don't have to show any option. + if (aPopup._endOptOpenAllInTabs) { + aPopup.removeChild(aPopup._endOptOpenAllInTabs); + aPopup._endOptOpenAllInTabs = null; + + aPopup.removeChild(aPopup._endOptSeparator); + aPopup._endOptSeparator = null; + } + } + else if (!aPopup._endOptOpenAllInTabs) { + // Create a separator before options. + aPopup._endOptSeparator = document.createElement("menuseparator"); + aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator"; + aPopup.appendChild(aPopup._endOptSeparator); + + // Add the "Open All in Tabs" menuitem. + aPopup._endOptOpenAllInTabs = document.createElement("menuitem"); + aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem"; + if (isLiveMark) { + aPopup._endOptOpenAllInTabs.setAttribute("oncommand", + "PlacesUIUtils.openLiveMarkNodesInTabs(this.parentNode._placesNode, event, " + + "PlacesUIUtils.getViewForNode(this));"); + } else { + aPopup._endOptOpenAllInTabs.setAttribute("oncommand", + "PlacesUIUtils.openContainerNodeInTabs(this.parentNode._placesNode, event, " + + "PlacesUIUtils.getViewForNode(this));"); + } + aPopup._endOptOpenAllInTabs.setAttribute("onclick", + "checkForMiddleClick(this, event); event.stopPropagation();"); + aPopup._endOptOpenAllInTabs.setAttribute("label", + gNavigatorBundle.getString("menuOpenAllInTabs.label")); + aPopup.appendChild(aPopup._endOptOpenAllInTabs); + } + }, + + _ensureMarkers: function(aPopup) { + if (aPopup._startMarker) + return; + + // _startMarker is an hidden menuseparator that lives before places nodes. + aPopup._startMarker = document.createElement("menuseparator"); + aPopup._startMarker.hidden = true; + aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild); + + // _endMarker is an hidden menuseparator that lives after places nodes. + aPopup._endMarker = document.createElement("menuseparator"); + aPopup._endMarker.hidden = true; + aPopup.appendChild(aPopup._endMarker); + + // Move the markers to the right position. + let firstNonStaticNodeFound = false; + for (let i = 0; i < aPopup.childNodes.length; i++) { + let child = aPopup.childNodes[i]; + // Menus that have static content at the end, but are initially empty, + // use a special "builder" attribute to figure out where to start + // inserting places nodes. + if (child.getAttribute("builder") == "end") { + aPopup.insertBefore(aPopup._endMarker, child); + break; + } + + if (child._placesNode && !firstNonStaticNodeFound) { + firstNonStaticNodeFound = true; + aPopup.insertBefore(aPopup._startMarker, child); + } + } + if (!firstNonStaticNodeFound) { + aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker); + } + }, + + _onPopupShowing: function(aEvent) { + // Avoid handling popupshowing of inner views. + let popup = aEvent.originalTarget; + + this._ensureMarkers(popup); + + // Remove any delayed element, see _cleanPopup for details. + if ("_delayedRemovals" in popup) { + while (popup._delayedRemovals.length > 0) { + popup.removeChild(popup._delayedRemovals.shift()); + } + } + + if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) { + if (!popup._placesNode.containerOpen) + popup._placesNode.containerOpen = true; + if (!popup._built) + this._rebuildPopup(popup); + + this._mayAddCommandsItems(popup); + } + }, + + _addEventListeners: + function(aObject, aEventNames, aCapturing) { + for (let i = 0; i < aEventNames.length; i++) { + aObject.addEventListener(aEventNames[i], this, aCapturing); + } + }, + + _removeEventListeners: + function(aObject, aEventNames, aCapturing) { + for (let i = 0; i < aEventNames.length; i++) { + aObject.removeEventListener(aEventNames[i], this, aCapturing); + } + }, +}; + +function PlacesToolbar(aPlace) { + let startTime = Date.now(); + // Add some smart getters for our elements. + let thisView = this; + [ + ["_viewElt", "PlacesToolbar"], + ["_rootElt", "PlacesToolbarItems"], + ["_dropIndicator", "PlacesToolbarDropIndicator"], + ["_chevron", "PlacesChevron"], + ["_chevronPopup", "PlacesChevronPopup"] + ].forEach(function(elementGlobal) { + let [name, id] = elementGlobal; + thisView.__defineGetter__(name, function() { + let element = document.getElementById(id); + if (!element) + return null; + + delete thisView[name]; + return thisView[name] = element; + }); + }); + + this._viewElt._placesView = this; + + this._addEventListeners(this._viewElt, this._cbEvents, false); + this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true); + this._addEventListeners(this._rootElt, ["overflow", "underflow"], true); + this._addEventListeners(window, ["resize", "unload"], false); + + // If personal-bookmarks has been dragged to the tabs toolbar, + // we have to track addition and removals of tabs, to properly + // recalculate the available space for bookmarks. + // TODO (bug 734730): Use a performant mutation listener when available. + if (this._viewElt.parentNode.parentNode == document.getElementById("TabsToolbar")) { + this._addEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false); + } + + PlacesViewBase.call(this, aPlace); +} + +PlacesToolbar.prototype = { + __proto__: PlacesViewBase.prototype, + + _cbEvents: ["dragstart", "dragover", "dragexit", "dragend", "drop", + "mousemove", "mouseover", "mouseout"], + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDOMEventListener) || + aIID.equals(Ci.nsITimerCallback)) + return this; + + return PlacesViewBase.prototype.QueryInterface.apply(this, arguments); + }, + + uninit: function() { + this._removeEventListeners(this._viewElt, this._cbEvents, false); + this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"], + true); + this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true); + this._removeEventListeners(window, ["resize", "unload"], false); + this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false); + + PlacesViewBase.prototype.uninit.apply(this, arguments); + }, + + _openedMenuButton: null, + _allowPopupShowing: true, + + _rebuild: function() { + // Clear out references to existing nodes, since they will be removed + // and re-added. + if (this._overFolder.elt) + this._clearOverFolder(); + + this._openedMenuButton = null; + while (this._rootElt.hasChildNodes()) { + this._rootElt.removeChild(this._rootElt.firstChild); + } + + let cc = this._resultNode.childCount; + for (let i = 0; i < cc; ++i) { + this._insertNewItem(this._resultNode.getChild(i), null); + } + + if (this._chevronPopup.hasAttribute("type")) { + // Chevron has already been initialized, but since we are forcing + // a rebuild of the toolbar, it has to be rebuilt. + // Otherwise, it will be initialized when the toolbar overflows. + this._chevronPopup.place = this.place; + } + }, + + _insertNewItem: + function(aChild, aBefore) { + this._domNodes.delete(aChild); + + let type = aChild.type; + let button; + if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { + button = document.createElement("toolbarseparator"); + } + else { + button = document.createElement("toolbarbutton"); + button.className = "bookmark-item"; + button.setAttribute("label", aChild.title || ""); + let icon = aChild.icon; + if (icon) + button.setAttribute("image", + PlacesUIUtils.getImageURLForResolution(window, icon)); + + if (PlacesUtils.containerTypes.indexOf(type) != -1) { + button.setAttribute("type", "menu"); + button.setAttribute("container", "true"); + + if (PlacesUtils.nodeIsQuery(aChild)) { + button.setAttribute("query", "true"); + if (PlacesUtils.nodeIsTagQuery(aChild)) + button.setAttribute("tagContainer", "true"); + } + else if (PlacesUtils.nodeIsFolder(aChild)) { + PlacesUtils.livemarks.getLivemark({ id: aChild.itemId }) + .then(aLivemark => { + button.setAttribute("livemark", "true"); + this.controller.cacheLivemarkInfo(aChild, aLivemark); + }, () => undefined); + } + + let popup = document.createElement("menupopup"); + popup.setAttribute("placespopup", "true"); + button.appendChild(popup); + popup._placesNode = PlacesUtils.asContainer(aChild); + popup.setAttribute("context", "placesContext"); + + this._domNodes.set(aChild, popup); + } + else if (PlacesUtils.nodeIsURI(aChild)) { + button.setAttribute("scheme", + PlacesUIUtils.guessUrlSchemeForUI(aChild.uri)); + } + } + + button._placesNode = aChild; + if (!this._domNodes.has(aChild)) + this._domNodes.set(aChild, button); + + if (aBefore) + this._rootElt.insertBefore(button, aBefore); + else + this._rootElt.appendChild(button); + }, + + _updateChevronPopupNodesVisibility: + function() { + for (let i = 0, node = this._chevronPopup._startMarker.nextSibling; + node != this._chevronPopup._endMarker; + i++, node = node.nextSibling) { + node.hidden = this._rootElt.childNodes[i].style.visibility != "hidden"; + } + }, + + _onChevronPopupShowing: + function(aEvent) { + // Handle popupshowing only for the chevron popup, not for nested ones. + if (aEvent.target != this._chevronPopup) + return; + + if (!this._chevron._placesView) + this._chevron._placesView = new PlacesMenu(aEvent, this.place); + + this._updateChevronPopupNodesVisibility(); + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "unload": + this.uninit(); + break; + case "resize": + // This handler updates nodes visibility in both the toolbar + // and the chevron popup when a window resize does not change + // the overflow status of the toolbar. + this.updateChevron(); + break; + case "overflow": + if (aEvent.target != aEvent.currentTarget) + return; + + // Ignore purely vertical overflows. + if (aEvent.detail == 0) + return; + + // Attach the popup binding to the chevron popup if it has not yet + // been initialized. + if (!this._chevronPopup.hasAttribute("type")) { + this._chevronPopup.setAttribute("place", this.place); + this._chevronPopup.setAttribute("type", "places"); + } + this._chevron.collapsed = false; + this.updateChevron(); + break; + case "underflow": + if (aEvent.target != aEvent.currentTarget) + return; + + // Ignore purely vertical underflows. + if (aEvent.detail == 0) + return; + + this.updateChevron(); + this._chevron.collapsed = true; + break; + case "TabOpen": + case "TabClose": + this.updateChevron(); + break; + case "dragstart": + this._onDragStart(aEvent); + break; + case "dragover": + this._onDragOver(aEvent); + break; + case "dragexit": + this._onDragExit(aEvent); + break; + case "dragend": + this._onDragEnd(aEvent); + break; + case "drop": + this._onDrop(aEvent); + break; + case "mouseover": + this._onMouseOver(aEvent); + break; + case "mousemove": + this._onMouseMove(aEvent); + break; + case "mouseout": + this._onMouseOut(aEvent); + break; + case "popupshowing": + this._onPopupShowing(aEvent); + break; + case "popuphidden": + this._onPopupHidden(aEvent); + break; + default: + throw "Trying to handle unexpected event."; + } + }, + + updateChevron: function() { + // If the chevron is collapsed there's nothing to update. + if (this._chevron.collapsed) + return; + + // Update the chevron on a timer. This will avoid repeated work when + // lot of changes happen in a small timeframe. + if (this._updateChevronTimer) + this._updateChevronTimer.cancel(); + + this._updateChevronTimer = this._setTimer(100); + }, + + _updateChevronTimerCallback: function() { + let scrollRect = this._rootElt.getBoundingClientRect(); + let childOverflowed = false; + for (let i = 0; i < this._rootElt.childNodes.length; i++) { + let child = this._rootElt.childNodes[i]; + // Once a child overflows, all the next ones will. + if (!childOverflowed) { + let childRect = child.getBoundingClientRect(); + childOverflowed = this.isRTL ? (childRect.left < scrollRect.left) + : (childRect.right > scrollRect.right); + + } + child.style.visibility = childOverflowed ? "hidden" : "visible"; + } + + // We rebuild the chevron on popupShowing, so if it is open + // we must update it. + if (this._chevron.open) + this._updateChevronPopupNodesVisibility(); + }, + + nodeInserted: + function(aParentPlacesNode, aPlacesNode, aIndex) { + let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); + if (parentElt == this._rootElt) { + let children = this._rootElt.childNodes; + this._insertNewItem(aPlacesNode, + aIndex < children.length ? children[aIndex] : null); + this.updateChevron(); + return; + } + + PlacesViewBase.prototype.nodeInserted.apply(this, arguments); + }, + + nodeRemoved: + function(aParentPlacesNode, aPlacesNode, aIndex) { + let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // Here we need the <menu>. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + if (parentElt == this._rootElt) { + this._removeChild(elt); + this.updateChevron(); + return; + } + + PlacesViewBase.prototype.nodeRemoved.apply(this, arguments); + }, + + nodeMoved: + function(aPlacesNode, + aOldParentPlacesNode, aOldIndex, + aNewParentPlacesNode, aNewIndex) { + let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode); + if (parentElt == this._rootElt) { + // Container is on the toolbar. + + // Move the element. + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // Here we need the <menu>. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + this._removeChild(elt); + this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]); + + // The chevron view may get nodeMoved after the toolbar. In such a case, + // we should ensure (by manually swapping menuitems) that the actual nodes + // are in the final position before updateChevron tries to updates their + // visibility, or the chevron may go out of sync. + // Luckily updateChevron runs on a timer, so, by the time it updates + // nodes, the menu has already handled the notification. + + this.updateChevron(); + return; + } + + PlacesViewBase.prototype.nodeMoved.apply(this, arguments); + }, + + nodeAnnotationChanged: + function(aPlacesNode, aAnno) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + if (elt == this._rootElt) + return; + + // We're notified for the menupopup, not the containing toolbarbutton. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + if (elt.parentNode == this._rootElt) { + // Node is on the toolbar. + + // All livemarks have a feedURI, so use it as our indicator. + if (aAnno == PlacesUtils.LMANNO_FEEDURI) { + elt.setAttribute("livemark", true); + + PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId }) + .then(aLivemark => { + this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark); + this.invalidateContainer(aPlacesNode); + }, Components.utils.reportError); + } + } + else { + // Node is in a submenu. + PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments); + } + }, + + nodeTitleChanged: function(aPlacesNode, aNewTitle) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + + // There's no UI representation for the root node, thus there's + // nothing to be done when the title changes. + if (elt == this._rootElt) + return; + + PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments); + + // Here we need the <menu>. + if (elt.localName == "menupopup") + elt = elt.parentNode; + + if (elt.parentNode == this._rootElt) { + // Node is on the toolbar + this.updateChevron(); + } + }, + + invalidateContainer: function(aPlacesNode) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode); + if (elt == this._rootElt) { + // Container is the toolbar itself. + this._rebuild(); + return; + } + + PlacesViewBase.prototype.invalidateContainer.apply(this, arguments); + }, + + _overFolder: { elt: null, + openTimer: null, + hoverTime: 350, + closeTimer: null }, + + _clearOverFolder: function() { + // The mouse is no longer dragging over the stored menubutton. + // Close the menubutton, clear out drag styles, and clear all + // timers for opening/closing it. + if (this._overFolder.elt && this._overFolder.elt.lastChild) { + if (!this._overFolder.elt.lastChild.hasAttribute("dragover")) { + this._overFolder.elt.lastChild.hidePopup(); + } + this._overFolder.elt.removeAttribute("dragover"); + this._overFolder.elt = null; + } + if (this._overFolder.openTimer) { + this._overFolder.openTimer.cancel(); + this._overFolder.openTimer = null; + } + if (this._overFolder.closeTimer) { + this._overFolder.closeTimer.cancel(); + this._overFolder.closeTimer = null; + } + }, + + /** + * This function returns information about where to drop when dragging over + * the toolbar. The returned object has the following properties: + * - ip: the insertion point for the bookmarks service. + * - beforeIndex: child index to drop before, for the drop indicator. + * - folderElt: the folder to drop into, if applicable. + */ + _getDropPoint: function(aEvent) { + let result = this.result; + if (!PlacesUtils.nodeIsFolder(this._resultNode)) + return null; + + let dropPoint = { ip: null, beforeIndex: null, folderElt: null }; + let elt = aEvent.target; + if (elt._placesNode && elt != this._rootElt && + elt.localName != "menupopup") { + let eltRect = elt.getBoundingClientRect(); + let eltIndex = Array.indexOf(this._rootElt.childNodes, elt); + if (PlacesUtils.nodeIsFolder(elt._placesNode) && + !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) { + // This is a folder. + // If we are in the middle of it, drop inside it. + // Otherwise, drop before it, with regards to RTL mode. + let threshold = eltRect.width * 0.25; + if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold) + : (aEvent.clientX < eltRect.left + threshold)) { + // Drop before this folder. + dropPoint.ip = + new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode), + eltIndex, Ci.nsITreeView.DROP_BEFORE); + dropPoint.beforeIndex = eltIndex; + } + else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold) + : (aEvent.clientX < eltRect.right - threshold)) { + // Drop inside this folder. + dropPoint.ip = + new InsertionPoint(PlacesUtils.getConcreteItemId(elt._placesNode), + -1, Ci.nsITreeView.DROP_ON, + PlacesUtils.nodeIsTagQuery(elt._placesNode)); + dropPoint.beforeIndex = eltIndex; + dropPoint.folderElt = elt; + } + else { + // Drop after this folder. + let beforeIndex = + (eltIndex == this._rootElt.childNodes.length - 1) ? + -1 : eltIndex + 1; + + dropPoint.ip = + new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode), + beforeIndex, Ci.nsITreeView.DROP_BEFORE); + dropPoint.beforeIndex = beforeIndex; + } + } + else { + // This is a non-folder node or a read-only folder. + // Drop before it with regards to RTL mode. + let threshold = eltRect.width * 0.5; + if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold) + : (aEvent.clientX < eltRect.left + threshold)) { + // Drop before this bookmark. + dropPoint.ip = + new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode), + eltIndex, Ci.nsITreeView.DROP_BEFORE); + dropPoint.beforeIndex = eltIndex; + } + else { + // Drop after this bookmark. + let beforeIndex = + eltIndex == this._rootElt.childNodes.length - 1 ? + -1 : eltIndex + 1; + dropPoint.ip = + new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode), + beforeIndex, Ci.nsITreeView.DROP_BEFORE); + dropPoint.beforeIndex = beforeIndex; + } + } + } + else { + // We are most likely dragging on the empty area of the + // toolbar, we should drop after the last node. + dropPoint.ip = + new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode), + -1, Ci.nsITreeView.DROP_BEFORE); + dropPoint.beforeIndex = -1; + } + + return dropPoint; + }, + + _setTimer: function(aTime) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT); + return timer; + }, + + notify: function(aTimer) { + if (aTimer == this._updateChevronTimer) { + this._updateChevronTimer = null; + this._updateChevronTimerCallback(); + } + + // * Timer to turn off indicator bar. + else if (aTimer == this._ibTimer) { + this._dropIndicator.collapsed = true; + this._ibTimer = null; + } + + // * Timer to open a menubutton that's being dragged over. + else if (aTimer == this._overFolder.openTimer) { + // Set the autoopen attribute on the folder's menupopup so that + // the menu will automatically close when the mouse drags off of it. + this._overFolder.elt.lastChild.setAttribute("autoopened", "true"); + this._overFolder.elt.open = true; + this._overFolder.openTimer = null; + } + + // * Timer to close a menubutton that's been dragged off of. + else if (aTimer == this._overFolder.closeTimer) { + // Close the menubutton if we are not dragging over it or one of + // its children. The autoopened attribute will let the menu know to + // close later if the menu is still being dragged over. + let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget; + let inHierarchy = false; + while (currentPlacesNode) { + if (currentPlacesNode == this._rootElt) { + inHierarchy = true; + break; + } + currentPlacesNode = currentPlacesNode.parentNode; + } + // The _clearOverFolder() function will close the menu for + // _overFolder.elt. So null it out if we don't want to close it. + if (inHierarchy) + this._overFolder.elt = null; + + // Clear out the folder and all associated timers. + this._clearOverFolder(); + } + }, + + _onMouseOver: function(aEvent) { + let button = aEvent.target; + if (button.parentNode == this._rootElt && button._placesNode && + PlacesUtils.nodeIsURI(button._placesNode)) + window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri, null); + }, + + _onMouseOut: function(aEvent) { + window.XULBrowserWindow.setOverLink("", null); + }, + + _cleanupDragDetails: function() { + // Called on dragend and drop. + PlacesControllerDragHelper.currentDropTarget = null; + this._draggedElt = null; + if (this._ibTimer) + this._ibTimer.cancel(); + + this._dropIndicator.collapsed = true; + }, + + _onDragStart: function(aEvent) { + // Sub menus have their own d&d handlers. + let draggedElt = aEvent.target; + if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode) + return; + + if (draggedElt.localName == "toolbarbutton" && + draggedElt.getAttribute("type") == "menu") { + // If the drag gesture on a container is toward down we open instead + // of dragging. + let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY; + let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX; + if ((translateY) >= Math.abs(translateX/2)) { + // Don't start the drag. + aEvent.preventDefault(); + // Open the menu. + draggedElt.open = true; + return; + } + + // If the menu is open, close it. + if (draggedElt.open) { + draggedElt.lastChild.hidePopup(); + draggedElt.open = false; + } + } + + // Activate the view and cache the dragged element. + this._draggedElt = draggedElt._placesNode; + this._rootElt.focus(); + + this._controller.setDataTransfer(aEvent); + aEvent.stopPropagation(); + }, + + _onDragOver: function(aEvent) { + // Cache the dataTransfer + PlacesControllerDragHelper.currentDropTarget = aEvent.target; + let dt = aEvent.dataTransfer; + + let dropPoint = this._getDropPoint(aEvent); + if (!dropPoint || !dropPoint.ip || + !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) { + this._dropIndicator.collapsed = true; + aEvent.stopPropagation(); + return; + } + + if (this._ibTimer) { + this._ibTimer.cancel(); + this._ibTimer = null; + } + + if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) { + // Dropping over a menubutton or chevron button. + // Set styles and timer to open relative menupopup. + let overElt = dropPoint.folderElt || this._chevron; + if (this._overFolder.elt != overElt) { + this._clearOverFolder(); + this._overFolder.elt = overElt; + this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime); + } + if (!this._overFolder.elt.hasAttribute("dragover")) + this._overFolder.elt.setAttribute("dragover", "true"); + + this._dropIndicator.collapsed = true; + } + else { + // Dragging over a normal toolbarbutton, + // show indicator bar and move it to the appropriate drop point. + let ind = this._dropIndicator; + let halfInd = ind.clientWidth / 2; + let translateX; + if (this.isRTL) { + halfInd = Math.ceil(halfInd); + translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd; + if (this._rootElt.firstChild) { + if (dropPoint.beforeIndex == -1) + translateX += this._rootElt.lastChild.getBoundingClientRect().left; + else { + translateX += this._rootElt.childNodes[dropPoint.beforeIndex] + .getBoundingClientRect().right; + } + } + } + else { + halfInd = Math.floor(halfInd); + translateX = 0 - this._rootElt.getBoundingClientRect().left + + halfInd; + if (this._rootElt.firstChild) { + if (dropPoint.beforeIndex == -1) + translateX += this._rootElt.lastChild.getBoundingClientRect().right; + else { + translateX += this._rootElt.childNodes[dropPoint.beforeIndex] + .getBoundingClientRect().left; + } + } + } + + ind.style.transform = "translate(" + Math.round(translateX) + "px)"; + ind.style.MozMarginStart = (-ind.clientWidth) + "px"; + ind.collapsed = false; + + // Clear out old folder information. + this._clearOverFolder(); + } + + aEvent.preventDefault(); + aEvent.stopPropagation(); + }, + + _onDrop: function(aEvent) { + PlacesControllerDragHelper.currentDropTarget = aEvent.target; + + let dropPoint = this._getDropPoint(aEvent); + if (dropPoint && dropPoint.ip) { + PlacesControllerDragHelper.onDrop(dropPoint.ip, aEvent.dataTransfer) + aEvent.preventDefault(); + } + + this._cleanupDragDetails(); + aEvent.stopPropagation(); + }, + + _onDragExit: function(aEvent) { + PlacesControllerDragHelper.currentDropTarget = null; + + // Set timer to turn off indicator bar (if we turn it off + // here, dragenter might be called immediately after, creating + // flicker). + if (this._ibTimer) + this._ibTimer.cancel(); + this._ibTimer = this._setTimer(10); + + // If we hovered over a folder, close it now. + if (this._overFolder.elt) + this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime); + }, + + _onDragEnd: function(aEvent) { + this._cleanupDragDetails(); + }, + + _onPopupShowing: function(aEvent) { + if (!this._allowPopupShowing) { + this._allowPopupShowing = true; + aEvent.preventDefault(); + return; + } + + let parent = aEvent.target.parentNode; + if (parent.localName == "toolbarbutton") + this._openedMenuButton = parent; + + PlacesViewBase.prototype._onPopupShowing.apply(this, arguments); + }, + + _onPopupHidden: function(aEvent) { + let popup = aEvent.target; + let placesNode = popup._placesNode; + // Avoid handling popuphidden of inner views + if (placesNode && PlacesUIUtils.getViewForNode(popup) == this) { + // UI performance: folder queries are cheap, keep the resultnode open + // so we don't rebuild its contents whenever the popup is reopened. + // Though, we want to always close feed containers so their expiration + // status will be checked at next opening. + if (!PlacesUtils.nodeIsFolder(placesNode) || + this.controller.hasCachedLivemarkInfo(placesNode)) { + placesNode.containerOpen = false; + } + } + + let parent = popup.parentNode; + if (parent.localName == "toolbarbutton") { + this._openedMenuButton = null; + // Clear the dragover attribute if present, if we are dragging into a + // folder in the hierachy of current opened popup we don't clear + // this attribute on clearOverFolder. See Notify for closeTimer. + if (parent.hasAttribute("dragover")) + parent.removeAttribute("dragover"); + } + }, + + _onMouseMove: function(aEvent) { + // Used in dragStart to prevent dragging folders when dragging down. + this._cachedMouseMoveEvent = aEvent; + + if (this._openedMenuButton == null || + PlacesControllerDragHelper.getSession()) + return; + + let target = aEvent.originalTarget; + if (this._openedMenuButton != target && + target.localName == "toolbarbutton" && + target.type == "menu") { + this._openedMenuButton.open = false; + target.open = true; + } + } +}; + +/** + * View for Places menus. This object should be created during the first + * popupshowing that's dispatched on the menu. + */ +function PlacesMenu(aPopupShowingEvent, aPlace) { + this._rootElt = aPopupShowingEvent.target; // <menupopup> + this._viewElt = this._rootElt.parentNode; // <menu> + this._viewElt._placesView = this; + this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true); + this._addEventListeners(window, ["unload"], false); + + PlacesViewBase.call(this, aPlace); + this._onPopupShowing(aPopupShowingEvent); +} + +PlacesMenu.prototype = { + __proto__: PlacesViewBase.prototype, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDOMEventListener)) + return this; + + return PlacesViewBase.prototype.QueryInterface.apply(this, arguments); + }, + + _removeChild: function(aChild) { + PlacesViewBase.prototype._removeChild.apply(this, arguments); + }, + + uninit: function() { + this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"], + true); + this._removeEventListeners(window, ["unload"], false); + + PlacesViewBase.prototype.uninit.apply(this, arguments); + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "unload": + this.uninit(); + break; + case "popupshowing": + this._onPopupShowing(aEvent); + break; + case "popuphidden": + this._onPopupHidden(aEvent); + break; + } + }, + + _onPopupHidden: function(aEvent) { + // Avoid handling popuphidden of inner views. + let popup = aEvent.originalTarget; + let placesNode = popup._placesNode; + if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this) + return; + + // UI performance: folder queries are cheap, keep the resultnode open + // so we don't rebuild its contents whenever the popup is reopened. + // Though, we want to always close feed containers so their expiration + // status will be checked at next opening. + if (!PlacesUtils.nodeIsFolder(placesNode) || + this.controller.hasCachedLivemarkInfo(placesNode)) + placesNode.containerOpen = false; + + // The autoopened attribute is set for folders which have been + // automatically opened when dragged over. Turn off this attribute + // when the folder closes because it is no longer applicable. + popup.removeAttribute("autoopened"); + popup.removeAttribute("dragstart"); + } +}; + diff --git a/browser/components/places/content/controller.js b/browser/components/places/content/controller.js new file mode 100644 index 000000000..33312330f --- /dev/null +++ b/browser/components/places/content/controller.js @@ -0,0 +1,1895 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +// XXXmano: we should move most/all of these constants to PlacesUtils +const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1"; + +// No change to the view, preserve current selection +const RELOAD_ACTION_NOTHING = 0; +// Inserting items new to the view, select the inserted rows +const RELOAD_ACTION_INSERT = 1; +// Removing items from the view, select the first item after the last selected +const RELOAD_ACTION_REMOVE = 2; +// Moving items within a view, don't treat the dropped items as additional +// rows. +const RELOAD_ACTION_MOVE = 3; + +// When removing a bunch of pages we split them in chunks to give some breath +// to the main-thread. +const REMOVE_PAGES_CHUNKLEN = 300; + +/** + * Represents an insertion point within a container where we can insert + * items. + * @param aItemId + * The identifier of the parent container + * @param aIndex + * The index within the container where we should insert + * @param aOrientation + * The orientation of the insertion. NOTE: the adjustments to the + * insertion point to accommodate the orientation should be done by + * the person who constructs the IP, not the user. The orientation + * is provided for informational purposes only! + * @param [optional] aIsTag + * Indicates if parent container is a tag + * @param [optional] aDropNearItemId + * When defined we will calculate index based on this itemId + * @constructor + */ +function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag, + aDropNearItemId) { + this.itemId = aItemId; + this._index = aIndex; + this.orientation = aOrientation; + this.isTag = aIsTag; + this.dropNearItemId = aDropNearItemId; +} + +InsertionPoint.prototype = { + set index(val) { + return this._index = val; + }, + + get index() { + if (this.dropNearItemId > 0) { + // If dropNearItemId is set up we must calculate the real index of + // the item near which we will drop. + var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId); + return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1; + } + return this._index; + } +}; + +/** + * Places Controller + */ + +function PlacesController(aView) { + this._view = aView; + XPCOMUtils.defineLazyServiceGetter(this, "clipboard", + "@mozilla.org/widget/clipboard;1", + "nsIClipboard"); + XPCOMUtils.defineLazyGetter(this, "profileName", function() { + return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName; + }); + + this._cachedLivemarkInfoObjects = new Map(); +} + +PlacesController.prototype = { + /** + * The places view. + */ + _view: null, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIClipboardOwner + ]), + + // nsIClipboardOwner + LosingOwnership: function(aXferable) { + this.cutNodes = []; + }, + + terminate: function() { + this._releaseClipboardOwnership(); + }, + + supportsCommand: function(aCommand) { + // Non-Places specific commands that we also support + switch (aCommand) { + case "cmd_undo": + case "cmd_redo": + case "cmd_cut": + case "cmd_copy": + case "cmd_paste": + case "cmd_delete": + case "cmd_selectAll": + return true; + } + + // All other Places Commands are prefixed with "placesCmd_" ... this + // filters out other commands that we do _not_ support (see 329587). + const CMD_PREFIX = "placesCmd_"; + return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX); + }, + + isCommandEnabled: function(aCommand) { + switch (aCommand) { + case "cmd_undo": + return PlacesUtils.transactionManager.numberOfUndoItems > 0; + case "cmd_redo": + return PlacesUtils.transactionManager.numberOfRedoItems > 0; + case "cmd_cut": + case "placesCmd_cut": + case "placesCmd_moveBookmarks": + for (let node of this._view.selectedNodes) { + // If selection includes history nodes or tags-as-bookmark, disallow + // cutting. + if (node.itemId == -1 || + (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) { + return false; + } + } + // Otherwise fall through to the cmd_delete check. + case "cmd_delete": + case "placesCmd_delete": + case "placesCmd_deleteDataHost": + return this._hasRemovableSelection(); + case "cmd_copy": + case "placesCmd_copy": + return this._view.hasSelection; + case "cmd_paste": + case "placesCmd_paste": + return this._canInsert(true) && this._isClipboardDataPasteable(); + case "cmd_selectAll": + if (this._view.selType != "single") { + let rootNode = this._view.result.root; + if (rootNode.containerOpen && rootNode.childCount > 0) + return true; + } + return false; + case "placesCmd_open": + case "placesCmd_open:window": + case "placesCmd_open:privatewindow": + case "placesCmd_open:tab": + var selectedNode = this._view.selectedNode; + return selectedNode && PlacesUtils.nodeIsURI(selectedNode); + case "placesCmd_new:folder": + case "placesCmd_new:livemark": + return this._canInsert(); + case "placesCmd_new:bookmark": + return this._canInsert(); + case "placesCmd_new:separator": + return this._canInsert() && + !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems && + this._view.result.sortingMode == + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; + case "placesCmd_show:info": + var selectedNode = this._view.selectedNode; + return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1 + case "placesCmd_reload": + // Livemark containers + var selectedNode = this._view.selectedNode; + return selectedNode && this.hasCachedLivemarkInfo(selectedNode); + case "placesCmd_sortBy:name": + var selectedNode = this._view.selectedNode; + return selectedNode && + PlacesUtils.nodeIsFolder(selectedNode) && + !PlacesUIUtils.isContentsReadOnly(selectedNode) && + this._view.result.sortingMode == + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; + case "placesCmd_createBookmark": + var node = this._view.selectedNode; + return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1; + case "placesCmd_openParentFolder": + return true; + default: + return false; + } + }, + + doCommand: function(aCommand) { + switch (aCommand) { + case "cmd_undo": + PlacesUtils.transactionManager.undoTransaction(); + break; + case "cmd_redo": + PlacesUtils.transactionManager.redoTransaction(); + break; + case "cmd_cut": + case "placesCmd_cut": + this.cut(); + break; + case "cmd_copy": + case "placesCmd_copy": + this.copy(); + break; + case "cmd_paste": + case "placesCmd_paste": + this.paste(); + break; + case "cmd_delete": + case "placesCmd_delete": + this.remove("Remove Selection"); + break; + case "placesCmd_deleteDataHost": + var host; + if (PlacesUtils.nodeIsHost(this._view.selectedNode)) { + var queries = this._view.selectedNode.getQueries(); + host = queries[0].domain; + } + else + host = NetUtil.newURI(this._view.selectedNode.uri).host; + ForgetAboutSite.removeDataFromDomain(host) + .catch(Components.utils.reportError); + break; + case "cmd_selectAll": + this.selectAll(); + break; + case "placesCmd_open": + PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view); + break; + case "placesCmd_open:window": + PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view); + break; + case "placesCmd_open:privatewindow": + PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true); + break; + case "placesCmd_open:tab": + PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view); + break; + case "placesCmd_new:folder": + this.newItem("folder"); + break; + case "placesCmd_new:bookmark": + this.newItem("bookmark"); + break; + case "placesCmd_new:livemark": + this.newItem("livemark"); + break; + case "placesCmd_new:separator": + this.newSeparator(); + break; + case "placesCmd_show:info": + this.showBookmarkPropertiesForSelection(); + break; + case "placesCmd_moveBookmarks": + this.moveSelectedBookmarks(); + break; + case "placesCmd_reload": + this.reloadSelectedLivemark(); + break; + case "placesCmd_sortBy:name": + this.sortFolderByName(); + break; + case "placesCmd_createBookmark": + let node = this._view.selectedNode; + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , hiddenRows: [ "description" + , "keyword" + , "location" + , "loadInSidebar" ] + , uri: NetUtil.newURI(node.uri) + , title: node.title + }, window.top); + break; + case "placesCmd_openParentFolder": + this.openParentFolder(); + break; + } + }, + + onEvent: function(eventName) { }, + + + /** + * Determine whether or not the selection can be removed, either by the + * delete or cut operations based on whether or not any of its contents + * are non-removable. We don't need to worry about recursion here since it + * is a policy decision that a removable item not be placed inside a non- + * removable item. + * @returns true if all nodes in the selection can be removed, + * false otherwise. + */ + _hasRemovableSelection() { + var ranges = this._view.removableSelectionRanges; + if (!ranges.length) + return false; + + var root = this._view.result.root; + + for (var j = 0; j < ranges.length; j++) { + var nodes = ranges[j]; + for (var i = 0; i < nodes.length; ++i) { + // Disallow removing the view's root node + if (nodes[i] == root) + return false; + + if (!PlacesUIUtils.canUserRemove(nodes[i])) + return false; + } + } + + return true; + }, + + /** + * Determines whether or not nodes can be inserted relative to the selection. + */ + _canInsert: function(isPaste) { + var ip = this._view.insertionPoint; + return ip != null && (isPaste || ip.isTag != true); + }, + + /** + * Looks at the data on the clipboard to see if it is paste-able. + * Paste-able data is: + * - in a format that the view can receive + * @returns true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor, + - clipboard data is of type TEXT_UNICODE and + is a valid URI. + */ + _isClipboardDataPasteable: function() { + // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely + // pasteable, with no need to unwrap all the nodes. + + var flavors = PlacesControllerDragHelper.placesFlavors; + var clipboard = this.clipboard; + var hasPlacesData = + clipboard.hasDataMatchingFlavors(flavors, flavors.length, + Ci.nsIClipboard.kGlobalClipboard); + if (hasPlacesData) + return this._view.insertionPoint != null; + + // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow + // pasting of valid "text/unicode" and "text/x-moz-url" data + var xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + xferable.init(null); + + xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL); + xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE); + clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); + + try { + // getAnyTransferData will throw if no data is available. + var data = { }, type = { }; + xferable.getAnyTransferData(type, data, { }); + data = data.value.QueryInterface(Ci.nsISupportsString).data; + if (type.value != PlacesUtils.TYPE_X_MOZ_URL && + type.value != PlacesUtils.TYPE_UNICODE) + return false; + + // unwrapNodes() will throw if the data blob is malformed. + var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value); + return this._view.insertionPoint != null; + } + catch (e) { + // getAnyTransferData or unwrapNodes failed + return false; + } + }, + + /** + * Gathers information about the selected nodes according to the following + * rules: + * "link" node is a URI + * "bookmark" node is a bookmark + * "livemarkChild" node is a child of a livemark + * "tagChild" node is a child of a tag + * "folder" node is a folder + * "query" node is a query + * "separator" node is a separator line + * "host" node is a host + * + * @returns an array of objects corresponding the selected nodes. Each + * object has each of the properties above set if its corresponding + * node matches the rule. In addition, the annotations names for each + * node are set on its corresponding object as properties. + * Notes: + * 1) This can be slow, so don't call it anywhere performance critical! + */ + _buildSelectionMetadata: function() { + var metadata = []; + var nodes = this._view.selectedNodes; + + for (var i = 0; i < nodes.length; i++) { + var nodeData = {}; + var node = nodes[i]; + var nodeType = node.type; + var uri = null; + + // We don't use the nodeIs* methods here to avoid going through the type + // property way too often + switch (nodeType) { + case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY: + nodeData["query"] = true; + if (node.parent) { + switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) { + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY: + nodeData["host"] = true; + break; + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY: + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY: + nodeData["day"] = true; + break; + } + } + break; + case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER: + case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT: + nodeData["folder"] = true; + break; + case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR: + nodeData["separator"] = true; + break; + case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI: + nodeData["link"] = true; + uri = NetUtil.newURI(node.uri); + if (PlacesUtils.nodeIsBookmark(node)) { + nodeData["bookmark"] = true; + var parentNode = node.parent; + if (parentNode) { + if (PlacesUtils.nodeIsTagQuery(parentNode)) + nodeData["tagChild"] = true; + } + } else { + var parentNode = node.parent; + if (parentNode) { + if (this.hasCachedLivemarkInfo(parentNode)) + nodeData["livemarkChild"] = true; + } + } + break; + } + + // annotations + if (uri) { + let names = PlacesUtils.annotations.getPageAnnotationNames(uri); + for (let j = 0; j < names.length; ++j) + nodeData[names[j]] = true; + } + + // For items also include the item-specific annotations + if (node.itemId != -1) { + let names = PlacesUtils.annotations + .getItemAnnotationNames(node.itemId); + for (let j = 0; j < names.length; ++j) + nodeData[names[j]] = true; + } + metadata.push(nodeData); + } + + return metadata; + }, + + /** + * Determines if a context-menu item should be shown + * @param aMenuItem + * the context menu item + * @param aMetaData + * meta data about the selection + * @returns true if the conditions (see buildContextMenu) are satisfied + * and the item can be displayed, false otherwise. + */ + _shouldShowMenuItem: function(aMenuItem, aMetaData) { + var selectiontype = aMenuItem.getAttribute("selectiontype"); + if (!selectiontype) { + selectiontype = "single|multiple"; + } + var selectionTypes = selectiontype.split("|"); + if (selectionTypes.indexOf("any") != -1) { + return true; + } + var count = aMetaData.length; + if (count > 1 && selectionTypes.indexOf("multiple") == -1) + return false; + if (count == 1 && selectionTypes.indexOf("single") == -1) + return false; + // NB: if there is no selection, we show the item if (and only if) + // the selectiontype includes 'none' - the metadata list will be + // empty so none of the other criteria will apply anyway. + if (count == 0) + return selectionTypes.indexOf("none") != -1; + + var forceHideAttr = aMenuItem.getAttribute("forcehideselection"); + if (forceHideAttr) { + var forceHideRules = forceHideAttr.split("|"); + for (let i = 0; i < aMetaData.length; ++i) { + for (let j = 0; j < forceHideRules.length; ++j) { + if (forceHideRules[j] in aMetaData[i]) + return false; + } + } + } + + var selectionAttr = aMenuItem.getAttribute("selection"); + if (!selectionAttr) { + return !aMenuItem.hidden; + } + + if (selectionAttr == "any") + return true; + + var showRules = selectionAttr.split("|"); + var anyMatched = false; + function metaDataNodeMatches(metaDataNode, rules) { + for (var i = 0; i < rules.length; i++) { + if (rules[i] in metaDataNode) + return true; + } + return false; + } + + for (var i = 0; i < aMetaData.length; ++i) { + if (metaDataNodeMatches(aMetaData[i], showRules)) + anyMatched = true; + else + return false; + } + return anyMatched; + }, + + /** + * Detects information (meta-data rules) about the current selection in the + * view (see _buildSelectionMetadata) and sets the visibility state for each + * of the menu-items in the given popup with the following rules applied: + * 1) The "selectiontype" attribute may be set on a menu-item to "single" + * if the menu-item should be visible only if there is a single node + * selected, or to "multiple" if the menu-item should be visible only if + * multiple nodes are selected, or to "none" if the menuitems should be + * visible for if there are no selected nodes, or to a |-separated + * combination of these. + * If the attribute is not set or set to an invalid value, the menu-item + * may be visible irrespective of the selection. + * 2) The "selection" attribute may be set on a menu-item to the various + * meta-data rules for which it may be visible. The rules should be + * separated with the | character. + * 3) A menu-item may be visible only if at least one of the rules set in + * its selection attribute apply to each of the selected nodes in the + * view. + * 4) The "forcehideselection" attribute may be set on a menu-item to rules + * for which it should be hidden. This attribute takes priority over the + * selection attribute. A menu-item would be hidden if at least one of the + * given rules apply to one of the selected nodes. The rules should be + * separated with the | character. + * 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to + * true if it should be hidden when there's no insertion point + * 6) The visibility state of a menu-item is unchanged if none of these + * attribute are set. + * 7) These attributes should not be set on separators for which the + * visibility state is "auto-detected." + * 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to + * true if it should be hidden inside the private browsing mode + * @param aPopup + * The menupopup to build children into. + * @return true if at least one item is visible, false otherwise. + */ + buildContextMenu: function(aPopup) { + var metadata = this._buildSelectionMetadata(); + var ip = this._view.insertionPoint; + var noIp = !ip || ip.isTag; + + var separator = null; + var visibleItemsBeforeSep = false; + var usableItemCount = 0; + for (var i = 0; i < aPopup.childNodes.length; ++i) { + var item = aPopup.childNodes[i]; + if (item.localName != "menuseparator") { + // We allow pasting into tag containers, so special case that. + var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" && + noIp && !(ip && ip.isTag && item.id == "placesContext_paste"); + // Show the "Open Containing Folder" menu-item only when the context is + // in the Library or in the Sidebar, and only when there's no insertion + // point. + var hideParentFolderItem = item.id == "placesContext_openParentFolder" && + (!/tree/i.test(this._view.localName) || ip); + var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" && + PrivateBrowsingUtils.isWindowPrivate(window); + var shouldHideItem = hideIfNoIP || hideIfPrivate || hideParentFolderItem || + !this._shouldShowMenuItem(item, metadata); + item.hidden = item.disabled = shouldHideItem; + + if (!item.hidden) { + visibleItemsBeforeSep = true; + usableItemCount++; + + // Show the separator above the menu-item if any + if (separator) { + separator.hidden = false; + separator = null; + } + } + } + else { // menuseparator + // Initially hide it. It will be unhidden if there will be at least one + // visible menu-item above and below it. + item.hidden = true; + + // We won't show the separator at all if no items are visible above it + if (visibleItemsBeforeSep) + separator = item; + + // New separator, count again: + visibleItemsBeforeSep = false; + } + } + + // Set Open Folder/Links In Tabs items enabled state if they're visible + if (usableItemCount > 0) { + var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs"); + if (!openContainerInTabsItem.hidden) { + var containerToUse = this._view.selectedNode || this._view.result.root; + if (PlacesUtils.nodeIsContainer(containerToUse)) { + if (!PlacesUtils.hasChildURIs(containerToUse)) { + openContainerInTabsItem.disabled = true; + // Ensure that we don't display the menu if nothing is enabled: + usableItemCount--; + } + } + } + } + + return usableItemCount > 0; + }, + + /** + * Select all links in the current view. + */ + selectAll: function() { + this._view.selectAll(); + }, + + /** + * Opens the bookmark properties for the selected URI Node. + */ + showBookmarkPropertiesForSelection: + function() { + var node = this._view.selectedNode; + if (!node) + return; + + var itemType = PlacesUtils.nodeIsFolder(node) || + PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark"; + var concreteId = PlacesUtils.getConcreteItemId(node); + var isRootItem = PlacesUtils.isRootItem(concreteId); + var itemId = node.itemId; + if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) { + // If this is a root or the Tags query we use the concrete itemId to catch + // the correct title for the node. + itemId = concreteId; + } + + PlacesUIUtils.showBookmarkDialog({ action: "edit" + , type: itemType + , itemId: itemId + , readOnly: isRootItem + , hiddenRows: [ "folderPicker" ] + }, window.top); + }, + + /** + * This method can be run on a URI parameter to ensure that it didn't + * receive a string instead of an nsIURI object. + */ + _assertURINotString: function(value) { + NS_ASSERT((typeof(value) == "object") && !(value instanceof String), + "This method should be passed a URI as a nsIURI object, not as a string."); + }, + + /** + * Reloads the selected livemark if any. + */ + reloadSelectedLivemark: function() { + var selectedNode = this._view.selectedNode; + if (selectedNode) { + let itemId = selectedNode.itemId; + PlacesUtils.livemarks.getLivemark({ id: itemId }) + .then(aLivemark => { + aLivemark.reload(true); + }, Components.utils.reportError); + } + }, + + /** + * Opens the links in the selected folder, or the selected links in new tabs. + */ + openSelectionInTabs: function(aEvent) { + var node = this._view.selectedNode; + var nodes = this._view.selectedNodes; + // In the case of no selection, open the root node: + if (!node && !nodes.length) { + node = this._view.result.root; + } + if (node && PlacesUtils.nodeIsContainer(node)) + PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._view); + else + PlacesUIUtils.openURINodesInTabs(nodes, aEvent, this._view); + }, + + /** + * Shows the Add Bookmark UI for the current insertion point. + * + * @param aType + * the type of the new item (bookmark/livemark/folder) + */ + newItem: function(aType) { + let ip = this._view.insertionPoint; + if (!ip) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + let performed = + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: aType + , defaultInsertionPoint: ip + , hiddenRows: [ "folderPicker" ] + }, window.top); + if (performed) { + // Select the new item. + let insertedNodeId = PlacesUtils.bookmarks + .getIdForItemAt(ip.itemId, ip.index); + this._view.selectItems([insertedNodeId], false); + } + }, + + /** + * Create a new Bookmark separator somewhere. + */ + newSeparator: function() { + var ip = this._view.insertionPoint; + if (!ip) + throw Cr.NS_ERROR_NOT_AVAILABLE; + var txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index); + PlacesUtils.transactionManager.doTransaction(txn); + // select the new item + var insertedNodeId = PlacesUtils.bookmarks + .getIdForItemAt(ip.itemId, ip.index); + this._view.selectItems([insertedNodeId], false); + }, + + /** + * Opens a dialog for moving the selected nodes. + */ + moveSelectedBookmarks: function() { + window.openDialog("chrome://browser/content/places/moveBookmarks.xul", + "", "chrome, modal", + this._view.selectedNodes); + }, + + /** + * Sort the selected folder by name. + */ + sortFolderByName: function() { + var itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode); + var txn = new PlacesSortFolderByNameTransaction(itemId); + PlacesUtils.transactionManager.doTransaction(txn); + }, + + /** + * Open the parent folder for the selected bookmarks search result. + */ + openParentFolder: function() { + var view; + if (!document.popupNode) { + view = document.commandDispatcher.focusedElement; + } else { + view = PlacesUIUtils.getViewForNode(document.popupNode); // XULElement + } + if (!view || view.getAttribute("type") != "places") + return; + var node = view.selectedNode; // nsINavHistoryResultNode + var aItemId = node.itemId; + var aFolderItemId = this.getParentFolderByItemId(aItemId); + if (aFolderItemId) + this.selectFolderByItemId(view, aFolderItemId, aItemId); + }, + + getParentFolderByItemId: function(aItemId) { + var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Components.interfaces.nsINavBookmarksService); + var parentFolderId = bmsvc.getFolderIdForItem(aItemId); + + return parentFolderId; + }, + + selectItems2: function(view, aIDs) { + var ids = aIDs; // Don't manipulate the caller's array. + + // Array of nodes found by findNodes which are to be selected + var nodes = []; + + // Array of nodes found by findNodes which should be opened + var nodesToOpen = []; + + // A set of URIs of container-nodes that were previously searched, + // and thus shouldn't be searched again. This is empty at the initial + // start of the recursion and gets filled in as the recursion + // progresses. + var nodesURIChecked = []; + + /** + * Recursively search through a node's children for items + * with the given IDs. When a matching item is found, remove its ID + * from the IDs array, and add the found node to the nodes dictionary. + * + * NOTE: This method will leave open any node that had matching items + * in its subtree. + */ + function findNodes(node) { + var foundOne = false; + // See if node matches an ID we wanted; add to results. + // For simple folder queries, check both itemId and the concrete + // item id. + var index = ids.indexOf(node.itemId); + if (index == -1 && + node.type == Components.interfaces.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) { + index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId); //xxx Bug 556739 3.7a5pre + } + + if (index != -1) { + nodes.push(node); + foundOne = true; + ids.splice(index, 1); + } + + if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) || + nodesURIChecked.indexOf(node.uri) != -1) + return foundOne; + + nodesURIChecked.push(node.uri); + PlacesUtils.asContainer(node); // xxx Bug 556739 3.7a6pre + + // Remember the beginning state so that we can re-close + // this node if we don't find any additional results here. + var previousOpenness = node.containerOpen; + node.containerOpen = true; + for (var child = 0; child < node.childCount && ids.length > 0; + child++) { + var childNode = node.getChild(child); + var found = findNodes(childNode); + if (!foundOne) + foundOne = found; + } + + // If we didn't find any additional matches in this node's + // subtree, revert the node to its previous openness. + if (foundOne) + nodesToOpen.unshift(node); + node.containerOpen = previousOpenness; + return foundOne; + } // findNodes + + // Disable notifications while looking for nodes. + let result = view.result; + let didSuppressNotifications = result.suppressNotifications; + if (!didSuppressNotifications) + result.suppressNotifications = true + try { + findNodes(view.result.root); + } + finally { + if (!didSuppressNotifications) + result.suppressNotifications = false; + } + + // For all the nodes we've found, highlight the corresponding + // index in the tree. + var resultview = view.view; + var selection = resultview.selection; + selection.selectEventsSuppressed = true; + selection.clearSelection(); + // Open nodes containing found items. + for (var i = 0; i < nodesToOpen.length; i++) { + nodesToOpen[i].containerOpen = true; + } + for (var i = 0; i < nodes.length; i++) { + if (PlacesUtils.nodeIsContainer(nodes[i])) + continue; + + var index = resultview.treeIndexForNode(nodes[i]); + selection.rangedSelect(index, index, true); + } + selection.selectEventsSuppressed = false; + }, + + selectFolderByItemId: function(view, aFolderItemId, aItemId) { + // Library + if (view.getAttribute("id") == "placeContent") { + view = document.getElementById("placesList"); + // Select a folder node in folder pane. + this.selectItems2(view, [aFolderItemId]); + view.selectItems([aFolderItemId]); + if (view.currentIndex) + view.treeBoxObject.ensureRowIsVisible(view.currentIndex); + // Reselect child node. + setTimeout(function(aItemId, view) { + var aView = view.ownerDocument.getElementById("placeContent"); + aView.selectItems([aItemId]); + if (aView.currentIndex) + aView.treeBoxObject.ensureRowIsVisible(aView.currentIndex); + }, 0, aItemId, view); + return; + } + + // Bookmarks Sidebar + if (!view) + return; + view.place = view.place; + + if ('FlatBookmarksOverlay' in window) { + var sidebarwin = view.ownerDocument.defaultView; + var searchBox = sidebarwin.document.getElementById("search-box"); + searchBox.value = ""; + searchBox.doCommand(); + sidebarwin.FlatBookmarks._setTreePlace(sidebarwin.FlatBookmarks._makePlaceForFolder(aFolderItemId)); + view.selectItems([aItemId]); + var tbo = view.treeBoxObject; + tbo.ensureRowIsVisible(view.currentIndex); + view.focus(); + return; + } + + view.findNode = function flatChildNodes(node, aIDs) { + var ids = aIDs; // Don't manipulate the caller's array. + + // Array of nodes found by findNodes which are to be selected + var nodes = []; + + // Array of nodes found by findNodes which should be opened + var nodesToOpen = []; + + // A set of URIs of container-nodes that were previously searched, + // and thus shouldn't be searched again. This is empty at the initial + // start of the recursion and gets filled in as the recursion + // progresses. + var nodesURIChecked = []; + + /** + * Recursively search through a node's children for items + * with the given IDs. When a matching item is found, remove its ID + * from the IDs array, and add the found node to the nodes dictionary. + * + * NOTE: This method will leave open any node that had matching items + * in its subtree. + */ + function findNodes(node) { + var foundOne = false; + // See if node matches an ID we wanted; add to results. + // For simple folder queries, check both itemId and the concrete + // item id. + var index = ids.indexOf(node.itemId); + if (index == -1 && + node.type == Components.interfaces.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) { + index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId); // xxx Bug 556739 3.7a5pre + } + + if (index != -1) { + nodes.push(node); + foundOne = true; + ids.splice(index, 1); + } + + if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) || + nodesURIChecked.indexOf(node.uri) != -1) + return foundOne; + + nodesURIChecked.push(node.uri); + PlacesUtils.asContainer(node); // xxx Bug 556739 3.7a6pre + // Remember the beginning state so that we can re-close + // this node if we don't find any additional results here. + var previousOpenness = node.containerOpen; + node.containerOpen = true; + for (var child = 0; child < node.childCount && ids.length > 0; + child++) { + var childNode = node.getChild(child); + if (PlacesUtils.nodeIsQuery(childNode)) + continue; + var found = findNodes(childNode); + if (!foundOne) + foundOne = found; + } + + // If we didn't find any additional matches in this node's + // subtree, revert the node to its previous openness. + if (foundOne) + nodesToOpen.unshift(node); + node.containerOpen = previousOpenness; + return foundOne; + } // findNodes + + // Disable notifications while looking for nodes. + let result = this.result; + let didSuppressNotifications = result.suppressNotifications; + if (!didSuppressNotifications) + result.suppressNotifications = true + try { + findNodes(this.result.root); + } + finally { + if (!didSuppressNotifications) + result.suppressNotifications = false; + } + + // Open nodes containing found items. + for (var i = 0; i < nodesToOpen.length; i++) { + nodesToOpen[i].containerOpen = true; + } + return nodes; + }; // findNode + + // For all the nodes we've found, highlight the corresponding + // index in the tree. + var resultview = view.view; + var selection = view.view.selection; + selection.selectEventsSuppressed = true; + selection.clearSelection(); + var nodes = view.findNode(view.result.root, [aFolderItemId]); + if (nodes.length > 0) { + var index = resultview.treeIndexForNode(nodes[0]); + nodes = view.findNode(nodes[0], [aItemId]); + if (nodes.length > 0) { + index = resultview.treeIndexForNode(nodes[0]); + selection.rangedSelect(index, index, true); + } + } + selection.selectEventsSuppressed = false; + + var tbo = view.treeBoxObject; + tbo.ensureRowIsVisible(view.currentIndex); + view.focus(); + return; + }, + + /** + * Walk the list of folders we're removing in this delete operation, and + * see if the selected node specified is already implicitly being removed + * because it is a child of that folder. + * @param node + * Node to check for containment. + * @param pastFolders + * List of folders the calling function has already traversed + * @returns true if the node should be skipped, false otherwise. + */ + _shouldSkipNode: function(node, pastFolders) { + /** + * Determines if a node is contained by another node within a resultset. + * @param node + * The node to check for containment for + * @param parent + * The parent container to check for containment in + * @returns true if node is a member of parent's children, false otherwise. + */ + function isContainedBy(node, parent) { + var cursor = node.parent; + while (cursor) { + if (cursor == parent) + return true; + cursor = cursor.parent; + } + return false; + } + + for (var j = 0; j < pastFolders.length; ++j) { + if (isContainedBy(node, pastFolders[j])) + return true; + } + return false; + }, + + /** + * Creates a set of transactions for the removal of a range of items. + * A range is an array of adjacent nodes in a view. + * @param [in] range + * An array of nodes to remove. Should all be adjacent. + * @param [out] transactions + * An array of transactions. + * @param [optional] removedFolders + * An array of folder nodes that have already been removed. + */ + _removeRange: function(range, transactions, removedFolders) { + NS_ASSERT(transactions instanceof Array, "Must pass a transactions array"); + if (!removedFolders) + removedFolders = []; + + for (var i = 0; i < range.length; ++i) { + var node = range[i]; + if (this._shouldSkipNode(node, removedFolders)) + continue; + + if (PlacesUtils.nodeIsTagQuery(node.parent)) { + // This is a uri node inside a tag container. It needs a special + // untag transaction. + var tagItemId = PlacesUtils.getConcreteItemId(node.parent); + var uri = NetUtil.newURI(node.uri); + let txn = new PlacesUntagURITransaction(uri, [tagItemId]); + transactions.push(txn); + } + else if (PlacesUtils.nodeIsTagQuery(node) && node.parent && + PlacesUtils.nodeIsQuery(node.parent) && + PlacesUtils.asQuery(node.parent).queryOptions.resultType == + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) { + // This is a tag container. + // Untag all URIs tagged with this tag only if the tag container is + // child of the "Tags" query in the library, in all other places we + // must only remove the query node. + var tag = node.title; + var URIs = PlacesUtils.tagging.getURIsForTag(tag); + for (var j = 0; j < URIs.length; j++) { + let txn = new PlacesUntagURITransaction(URIs[j], [tag]); + transactions.push(txn); + } + } + else if (PlacesUtils.nodeIsURI(node) && + PlacesUtils.nodeIsQuery(node.parent) && + PlacesUtils.asQuery(node.parent).queryOptions.queryType == + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { + // This is a uri node inside an history query. + PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri)); + // History deletes are not undoable, so we don't have a transaction. + } + else if (node.itemId == -1 && + PlacesUtils.nodeIsQuery(node) && + PlacesUtils.asQuery(node).queryOptions.queryType == + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { + // This is a dynamically generated history query, like queries + // grouped by site, time or both. Dynamically generated queries don't + // have an itemId even if they are descendants of a bookmark. + this._removeHistoryContainer(node); + // History deletes are not undoable, so we don't have a transaction. + } + else { + // This is a common bookmark item. + if (PlacesUtils.nodeIsFolder(node)) { + // If this is a folder we add it to our array of folders, used + // to skip nodes that are children of an already removed folder. + removedFolders.push(node); + } + let txn = new PlacesRemoveItemTransaction(node.itemId); + transactions.push(txn); + } + } + }, + + /** + * Removes the set of selected ranges from bookmarks. + * @param txnName + * See |remove|. + */ + _removeRowsFromBookmarks: function(txnName) { + var ranges = this._view.removableSelectionRanges; + var transactions = []; + var removedFolders = []; + + for (var i = 0; i < ranges.length; i++) + this._removeRange(ranges[i], transactions, removedFolders); + + if (transactions.length > 0) { + var txn = new PlacesAggregatedTransaction(txnName, transactions); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + /** + * Removes the set of selected ranges from history. + * + * @note history deletes are not undoable. + */ + _removeRowsFromHistory: function() { + let nodes = this._view.selectedNodes; + let URIs = []; + for (let i = 0; i < nodes.length; ++i) { + let node = nodes[i]; + if (PlacesUtils.nodeIsURI(node)) { + let uri = NetUtil.newURI(node.uri); + // Avoid duplicates. + if (URIs.indexOf(uri) < 0) { + URIs.push(uri); + } + } + else if (PlacesUtils.nodeIsQuery(node) && + PlacesUtils.asQuery(node).queryOptions.queryType == + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { + this._removeHistoryContainer(node); + } + } + + // Do removal in chunks to give some breath to main-thread. + function pagesChunkGenerator(aURIs) { + while (aURIs.length) { + let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN); + PlacesUtils.bhistory.removePages(URIslice, URIslice.length); + Services.tm.mainThread.dispatch(function() { + try { + gen.next(); + } catch (ex if ex instanceof StopIteration) {} + }, Ci.nsIThread.DISPATCH_NORMAL); + yield; + } + } + let gen = pagesChunkGenerator(URIs); + gen.next(); + }, + + /** + * Removes history visits for an history container node. + * @param [in] aContainerNode + * The container node to remove. + * + * @note history deletes are not undoable. + */ + _removeHistoryContainer: function(aContainerNode) { + if (PlacesUtils.nodeIsHost(aContainerNode)) { + // Site container. + PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true); + } + else if (PlacesUtils.nodeIsDay(aContainerNode)) { + // Day container. + let query = aContainerNode.getQueries()[0]; + let beginTime = query.beginTime; + let endTime = query.endTime; + NS_ASSERT(query && beginTime && endTime, + "A valid date container query should exist!"); + // We want to exclude beginTime from the removal because + // removePagesByTimeframe includes both extremes, while date containers + // exclude the lower extreme. So, if we would not exclude it, we would + // end up removing more history than requested. + PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime); + } + }, + + /** + * Removes the selection + * @param aTxnName + * A name for the transaction if this is being performed + * as part of another operation. + */ + remove: function(aTxnName) { + if (!this._hasRemovableSelection()) + return; + + NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name"); + + var root = this._view.result.root; + + if (PlacesUtils.nodeIsFolder(root)) + this._removeRowsFromBookmarks(aTxnName); + else if (PlacesUtils.nodeIsQuery(root)) { + var queryType = PlacesUtils.asQuery(root).queryOptions.queryType; + if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) + this._removeRowsFromBookmarks(aTxnName); + else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) + this._removeRowsFromHistory(); + else + NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED"); + } + else + NS_ASSERT(false, "unexpected root"); + }, + + /** + * Fills a DataTransfer object with the content of the selection that can be + * dropped elsewhere. + * @param aEvent + * The dragstart event. + */ + setDataTransfer: function(aEvent) { + let dt = aEvent.dataTransfer; + let doCopy = ["copyLink", "copy", "link"].indexOf(dt.effectAllowed) != -1; + + let result = this._view.result; + let didSuppressNotifications = result.suppressNotifications; + if (!didSuppressNotifications) + result.suppressNotifications = true; + + function addData(type, index, feedURI) { + let wrapNode = PlacesUtils.wrapNode(node, type, feedURI); + dt.mozSetDataAt(type, wrapNode, index); + } + + function addURIData(index, feedURI) { + addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI); + addData(PlacesUtils.TYPE_UNICODE, index, feedURI); + addData(PlacesUtils.TYPE_HTML, index, feedURI); + } + + try { + let nodes = this._view.draggableSelection; + for (let i = 0; i < nodes.length; ++i) { + var node = nodes[i]; + + // This order is _important_! It controls how this and other + // applications select data to be inserted based on type. + addData(PlacesUtils.TYPE_X_MOZ_PLACE, i); + + // Drop the feed uri for livemark containers + let livemarkInfo = this.getCachedLivemarkInfo(node); + if (livemarkInfo) { + addURIData(i, livemarkInfo.feedURI.spec); + } + else if (node.uri) { + addURIData(i); + } + } + } + finally { + if (!didSuppressNotifications) + result.suppressNotifications = false; + } + }, + + get clipboardAction () { + let action = {}; + let actionOwner; + try { + let xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + xferable.init(null); + xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION) + this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); + xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {}); + [action, actionOwner] = + action.value.QueryInterface(Ci.nsISupportsString).data.split(","); + } catch(ex) { + // Paste from external sources don't have any associated action, just + // fallback to a copy action. + return "copy"; + } + // For cuts also check who inited the action, since cuts across different + // instances should instead be handled as copies (The sources are not + // available for this instance). + if (action == "cut" && actionOwner != this.profileName) + action = "copy"; + + return action; + }, + + _releaseClipboardOwnership: function() { + if (this.cutNodes.length > 0) { + // This clears the logical clipboard, doesn't remove data. + this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard); + } + }, + + _clearClipboard: function() { + let xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + xferable.init(null); + // Empty transferables may cause crashes, so just add an unknown type. + const TYPE = "text/x-moz-place-empty"; + xferable.addDataFlavor(TYPE); + xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0); + this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard); + }, + + _populateClipboard: function(aNodes, aAction) { + // This order is _important_! It controls how this and other applications + // select data to be inserted based on type. + let contents = [ + { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] }, + { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] }, + { type: PlacesUtils.TYPE_HTML, entries: [] }, + { type: PlacesUtils.TYPE_UNICODE, entries: [] }, + ]; + + // Avoid handling descendants of a copied node, the transactions take care + // of them automatically. + let copiedFolders = []; + aNodes.forEach(function(node) { + if (this._shouldSkipNode(node, copiedFolders)) + return; + if (PlacesUtils.nodeIsFolder(node)) + copiedFolders.push(node); + + let livemarkInfo = this.getCachedLivemarkInfo(node); + let feedURI = livemarkInfo && livemarkInfo.feedURI.spec; + + contents.forEach(function(content) { + content.entries.push( + PlacesUtils.wrapNode(node, content.type, feedURI) + ); + }); + }, this); + + function addData(type, data) { + xferable.addDataFlavor(type); + xferable.setTransferData(type, PlacesUtils.toISupportsString(data), + data.length * 2); + } + + let xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + xferable.init(null); + let hasData = false; + // This order matters here! It controls how this and other applications + // select data to be inserted based on type. + contents.forEach(function(content) { + if (content.entries.length > 0) { + hasData = true; + let glue = + content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl; + addData(content.type, content.entries.join(glue)); + } + }); + + // Track the exected action in the xferable. This must be the last flavor + // since it's the least preferred one. + // Enqueue a unique instance identifier to distinguish operations across + // concurrent instances of the application. + addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName); + + if (hasData) { + this.clipboard.setData(xferable, + this.cutNodes.length > 0 ? this : null, + Ci.nsIClipboard.kGlobalClipboard); + } + }, + + _cutNodes: [], + get cutNodes() this._cutNodes, + set cutNodes(aNodes) { + let self = this; + function updateCutNodes(aValue) { + self._cutNodes.forEach(function(aNode) { + self._view.toggleCutNode(aNode, aValue); + }); + } + + updateCutNodes(false); + this._cutNodes = aNodes; + updateCutNodes(true); + return aNodes; + }, + + /** + * Copy Bookmarks and Folders to the clipboard + */ + copy: function() { + let result = this._view.result; + let didSuppressNotifications = result.suppressNotifications; + if (!didSuppressNotifications) + result.suppressNotifications = true; + try { + this._populateClipboard(this._view.selectedNodes, "copy"); + } + finally { + if (!didSuppressNotifications) + result.suppressNotifications = false; + } + }, + + /** + * Cut Bookmarks and Folders to the clipboard + */ + cut: function() { + let result = this._view.result; + let didSuppressNotifications = result.suppressNotifications; + if (!didSuppressNotifications) + result.suppressNotifications = true; + try { + this._populateClipboard(this._view.selectedNodes, "cut"); + this.cutNodes = this._view.selectedNodes; + } + finally { + if (!didSuppressNotifications) + result.suppressNotifications = false; + } + }, + + /** + * Paste Bookmarks and Folders from the clipboard + */ + paste: function() { + // No reason to proceed if there isn't a valid insertion point. + let ip = this._view.insertionPoint; + if (!ip) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + let action = this.clipboardAction; + + let xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + xferable.init(null); + // This order matters here! It controls the preferred flavors for this + // paste operation. + [ PlacesUtils.TYPE_X_MOZ_PLACE, + PlacesUtils.TYPE_X_MOZ_URL, + PlacesUtils.TYPE_UNICODE, + ].forEach(function(type) xferable.addDataFlavor(type)); + + this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); + + // Now get the clipboard contents, in the best available flavor. + let data = {}, type = {}, items = []; + try { + xferable.getAnyTransferData(type, data, {}); + data = data.value.QueryInterface(Ci.nsISupportsString).data; + type = type.value; + items = PlacesUtils.unwrapNodes(data, type); + } catch(ex) { + // No supported data exists or nodes unwrap failed, just bail out. + return; + } + + let transactions = []; + let insertionIndex = ip.index; + for (let i = 0; i < items.length; ++i) { + if (ip.isTag) { + // Pasting into a tag container means tagging the item, regardless of + // the requested action. + let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri), + [ip.itemId]); + transactions.push(tagTxn); + continue; + } + + // Adjust index to make sure items are pasted in the correct position. + // If index is DEFAULT_INDEX, items are just appended. + if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX) + insertionIndex = ip.index + i; + + transactions.push( + PlacesUIUtils.makeTransaction(items[i], type, ip.itemId, + insertionIndex, action == "copy") + ); + } + + let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions); + PlacesUtils.transactionManager.doTransaction(aggregatedTxn); + + // Cut/past operations are not repeatable, so clear the clipboard. + if (action == "cut") { + this._clearClipboard(); + } + + // Select the pasted items, they should be consecutive. + let insertedNodeIds = []; + for (let i = 0; i < transactions.length; ++i) { + insertedNodeIds.push( + PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i) + ); + } + if (insertedNodeIds.length > 0) + this._view.selectItems(insertedNodeIds, false); + }, + + /** + * Cache the livemark info for a node. This allows the controller and the + * views to treat the given node as a livemark. + * @param aNode + * a places result node. + * @param aLivemarkInfo + * a mozILivemarkInfo object. + */ + cacheLivemarkInfo: function(aNode, aLivemarkInfo) { + this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo); + }, + + /** + * Returns whether or not there's cached mozILivemarkInfo object for a node. + * @param aNode + * a places result node. + * @return true if there's a cached mozILivemarkInfo object for + * aNode, false otherwise. + */ + hasCachedLivemarkInfo: function(aNode) + this._cachedLivemarkInfoObjects.has(aNode), + + /** + * Returns the cached livemark info for a node, if set by cacheLivemarkInfo, + * null otherwise. + * @param aNode + * a places result node. + * @return the mozILivemarkInfo object for aNode, if set, null otherwise. + */ + getCachedLivemarkInfo: function(aNode) + this._cachedLivemarkInfoObjects.get(aNode, null) +}; + +/** + * Handles drag and drop operations for views. Note that this is view agnostic! + * You should not use PlacesController._view within these methods, since + * the view that the item(s) have been dropped on was not necessarily active. + * Drop functions are passed the view that is being dropped on. + */ +var PlacesControllerDragHelper = { + /** + * DOM Element currently being dragged over + */ + currentDropTarget: null, + + /** + * Determines if the mouse is currently being dragged over a child node of + * this menu. This is necessary so that the menu doesn't close while the + * mouse is dragging over one of its submenus + * @param node + * The container node + * @returns true if the user is dragging over a node within the hierarchy of + * the container, false otherwise. + */ + draggingOverChildNode: function(node) { + let currentNode = this.currentDropTarget; + while (currentNode) { + if (currentNode == node) + return true; + currentNode = currentNode.parentNode; + } + return false; + }, + + /** + * @returns The current active drag session. Returns null if there is none. + */ + getSession: function() { + return this.dragService.getCurrentSession(); + }, + + /** + * Extract the first accepted flavor from a list of flavors. + * @param aFlavors + * The flavors list of type nsIDOMDOMStringList. + */ + getFirstValidFlavor: function(aFlavors) { + for (let i = 0; i < aFlavors.length; i++) { + if (this.GENERIC_VIEW_DROP_TYPES.indexOf(aFlavors[i]) != -1) + return aFlavors[i]; + } + + // If no supported flavor is found, check if data includes text/plain + // contents. If so, request them as text/unicode, a conversion will happen + // automatically. + if (aFlavors.contains("text/plain")) { + return PlacesUtils.TYPE_UNICODE; + } + + return null; + }, + + /** + * Determines whether or not the data currently being dragged can be dropped + * on a places view. + * @param ip + * The insertion point where the items should be dropped. + */ + canDrop: function(ip, dt) { + let dropCount = dt.mozItemCount; + + // Check every dragged item. + for (let i = 0; i < dropCount; i++) { + let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i)); + if (!flavor) + return false; + + // Urls can be dropped on any insertionpoint. + // XXXmano: remember that this method is called for each dragover event! + // Thus we shouldn't use unwrapNodes here at all if possible. + // I think it would be OK to accept bogus data here (e.g. text which was + // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and + // will just case the actual drop to be a no-op), and only rule out valid + // expected cases, which are either unsupported flavors, or items which + // cannot be dropped in the current insertionpoint. The last case will + // likely force us to use unwrapNodes for the private data types of + // places. + if (flavor == TAB_DROP_TYPE) + continue; + + let data = dt.mozGetDataAt(flavor, i); + let dragged; + try { + dragged = PlacesUtils.unwrapNodes(data, flavor)[0]; + } + catch (e) { + return false; + } + + // Only bookmarks and urls can be dropped into tag containers. + if (ip.isTag && ip.orientation == Ci.nsITreeView.DROP_ON && + dragged.type != PlacesUtils.TYPE_X_MOZ_URL && + (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE || + (dragged.uri && dragged.uri.startsWith("place:")) )) + return false; + + // The following loop disallows the dropping of a folder on itself or + // on any of its descendants. + if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER || + (dragged.uri && dragged.uri.startsWith("place:")) ) { + let parentId = ip.itemId; + while (parentId != PlacesUtils.placesRootId) { + if (dragged.concreteId == parentId || dragged.id == parentId) + return false; + parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId); + } + } + } + return true; + }, + + + /** + * Determines if a node can be moved. + * + * @param aNode + * A nsINavHistoryResultNode node. + * @returns True if the node can be moved, false otherwise. + */ + canMoveNode: + function(aNode) { + // Only bookmark items are movable. + if (aNode.itemId == -1) + return false; + + // Once tags and bookmarked are divorced, the tag-query check should be + // removed. + let parentNode = aNode.parent; + return parentNode != null && + !(PlacesUtils.nodeIsFolder(parentNode) && + PlacesUIUtils.isContentsReadOnly(parentNode)) && + !PlacesUtils.nodeIsTagQuery(parentNode); + }, + + /** + * Handles the drop of one or more items onto a view. + * @param insertionPoint + * The insertion point where the items should be dropped + */ + onDrop: function(insertionPoint, dt) { + let doCopy = ["copy", "link"].indexOf(dt.dropEffect) != -1; + + let transactions = []; + let dropCount = dt.mozItemCount; + let movedCount = 0; + for (let i = 0; i < dropCount; ++i) { + let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i)); + if (!flavor) + return; + + let data = dt.mozGetDataAt(flavor, i); + let unwrapped; + if (flavor != TAB_DROP_TYPE) { + // There's only ever one in the D&D case. + unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0]; + } + else if (data instanceof XULElement && data.localName == "tab" && + data.ownerDocument.defaultView instanceof ChromeWindow) { + let uri = data.linkedBrowser.currentURI; + let spec = uri ? uri.spec : "about:blank"; + let title = data.label; + unwrapped = { uri: spec, + title: data.label, + type: PlacesUtils.TYPE_X_MOZ_URL}; + } + else + throw("bogus data was passed as a tab"); + + let index = insertionPoint.index; + + // Adjust insertion index to prevent reversal of dragged items. When you + // drag multiple elts upward: need to increment index or each successive + // elt will be inserted at the same index, each above the previous. + let dragginUp = insertionPoint.itemId == unwrapped.parent && + index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id); + if (index != -1 && dragginUp) + index += movedCount++; + + // If dragging over a tag container we should tag the item. + if (insertionPoint.isTag && + insertionPoint.orientation == Ci.nsITreeView.DROP_ON) { + let uri = NetUtil.newURI(unwrapped.uri); + let tagItemId = insertionPoint.itemId; + let tagTxn = new PlacesTagURITransaction(uri, [tagItemId]); + transactions.push(tagTxn); + } + else { + transactions.push(PlacesUIUtils.makeTransaction(unwrapped, + flavor, insertionPoint.itemId, + index, doCopy)); + } + } + + let txn = new PlacesAggregatedTransaction("DropItems", transactions); + PlacesUtils.transactionManager.doTransaction(txn); + }, + + /** + * Checks if we can insert into a container. + * @param aContainer + * The container were we are want to drop + */ + disallowInsertion: function(aContainer) { + NS_ASSERT(aContainer, "empty container"); + // Allow dropping into Tag containers and editable folders. + return !PlacesUtils.nodeIsTagQuery(aContainer) && + (!PlacesUtils.nodeIsFolder(aContainer) || + PlacesUIUtils.isContentsReadOnly(aContainer)); + }, + + placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, + PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, + PlacesUtils.TYPE_X_MOZ_PLACE], + + // The order matters. + GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, + PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, + PlacesUtils.TYPE_X_MOZ_PLACE, + PlacesUtils.TYPE_X_MOZ_URL, + TAB_DROP_TYPE, + PlacesUtils.TYPE_UNICODE], +}; + + +XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService", + "@mozilla.org/widget/dragservice;1", + "nsIDragService"); + +function goUpdatePlacesCommands() { + // Get the controller for one of the places commands. + var placesController = doGetPlacesControllerForCommand("placesCmd_open"); + function updatePlacesCommand(aCommand) { + goSetCommandEnabled(aCommand, placesController && + placesController.isCommandEnabled(aCommand)); + } + + updatePlacesCommand("placesCmd_open"); + updatePlacesCommand("placesCmd_open:window"); + updatePlacesCommand("placesCmd_open:privatewindow"); + updatePlacesCommand("placesCmd_open:tab"); + updatePlacesCommand("placesCmd_new:folder"); + updatePlacesCommand("placesCmd_new:bookmark"); + updatePlacesCommand("placesCmd_new:livemark"); + updatePlacesCommand("placesCmd_new:separator"); + updatePlacesCommand("placesCmd_show:info"); + updatePlacesCommand("placesCmd_moveBookmarks"); + updatePlacesCommand("placesCmd_reload"); + updatePlacesCommand("placesCmd_sortBy:name"); + updatePlacesCommand("placesCmd_openParentFolder"); + updatePlacesCommand("placesCmd_cut"); + updatePlacesCommand("placesCmd_copy"); + updatePlacesCommand("placesCmd_paste"); + updatePlacesCommand("placesCmd_delete"); +} + +function doGetPlacesControllerForCommand(aCommand) +{ + // A context menu may be built for non-focusable views. Thus, we first try + // to look for a view associated with document.popupNode + let popupNode; + try { + popupNode = document.popupNode; + } catch (e) { + // The document went away (bug 797307). + return null; + } + if (popupNode) { + let view = PlacesUIUtils.getViewForNode(popupNode); + if (view && view._contextMenuShown) + return view.controllers.getControllerForCommand(aCommand); + } + + // When we're not building a context menu, only focusable views + // are possible. Thus, we can safely use the command dispatcher. + let controller = top.document.commandDispatcher + .getControllerForCommand(aCommand); + if (controller) + return controller; + + return null; +} + +function goDoPlacesCommand(aCommand) +{ + let controller = doGetPlacesControllerForCommand(aCommand); + if (controller && controller.isCommandEnabled(aCommand)) + controller.doCommand(aCommand); +} + diff --git a/browser/components/places/content/downloadsViewOverlay.xul b/browser/components/places/content/downloadsViewOverlay.xul new file mode 100644 index 000000000..1a44dfdc0 --- /dev/null +++ b/browser/components/places/content/downloadsViewOverlay.xul @@ -0,0 +1,44 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xul-overlay href="chrome://browser/content/downloads/allDownloadsViewOverlay.xul"?> + +<!DOCTYPE overlay [ +<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> +%downloadsDTD; +]> + +<overlay id="downloadsViewOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript"><![CDATA[ + const DOWNLOADS_QUERY = "place:transition=" + + Components.interfaces.nsINavHistoryService.TRANSITION_DOWNLOAD + + "&sort=" + + Components.interfaces.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING; + + ContentArea.setContentViewForQueryString(DOWNLOADS_QUERY, + function() new DownloadsPlacesView(document.getElementById("downloadsRichListBox"), false), + { showDetailsPane: false, + toolbarSet: "back-button, forward-button, organizeButton, clearDownloadsButton, libraryToolbarSpacer, searchFilter" }); + ]]></script> + + <window id="places"> + <commandset id="downloadCommands"/> + <menupopup id="downloadsContextMenu"/> + </window> + + <deck id="placesViewsDeck"> + <richlistbox id="downloadsRichListBox"/> + </deck> + + <toolbar id="placesToolbar"> + <toolbarbutton id="clearDownloadsButton" + insertbefore="libraryToolbarSpacer" + label="&clearDownloadsButton.label;" + command="downloadsCmd_clearDownloads" + tooltiptext="&clearDownloadsButton.tooltip;"/> + </toolbar> + +</overlay> diff --git a/browser/components/places/content/editBookmarkOverlay.js b/browser/components/places/content/editBookmarkOverlay.js new file mode 100644 index 000000000..fcc5f5cae --- /dev/null +++ b/browser/components/places/content/editBookmarkOverlay.js @@ -0,0 +1,1063 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed"; +const MAX_FOLDER_ITEM_IN_MENU_LIST = 5; + +var gEditItemOverlay = { + _uri: null, + _itemId: -1, + _itemIds: [], + _uris: [], + _tags: [], + _allTags: [], + _keyword: null, + _multiEdit: false, + _itemType: -1, + _readOnly: false, + _hiddenRows: [], + _onPanelReady: false, + _observersAdded: false, + _staticFoldersListBuilt: false, + _initialized: false, + _titleOverride: "", + + // the first field which was edited after this panel was initialized for + // a certain item + _firstEditedField: "", + + get itemId() { + return this._itemId; + }, + + get uri() { + return this._uri; + }, + + get multiEdit() { + return this._multiEdit; + }, + + /** + * Determines the initial data for the item edited or added by this dialog + */ + _determineInfo: function(aInfo) { + // hidden rows + if (aInfo && aInfo.hiddenRows) + this._hiddenRows = aInfo.hiddenRows; + else + this._hiddenRows.splice(0, this._hiddenRows.length); + // force-read-only + this._readOnly = aInfo && aInfo.forceReadOnly; + this._titleOverride = aInfo && aInfo.titleOverride ? aInfo.titleOverride + : ""; + this._onPanelReady = aInfo && aInfo.onPanelReady; + }, + + _showHideRows: function() { + var isBookmark = this._itemId != -1 && + this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK; + var isQuery = false; + if (this._uri) + isQuery = this._uri.schemeIs("place"); + + this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1; + this._element("folderRow").collapsed = + this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly; + this._element("tagsRow").collapsed = !this._uri || + this._hiddenRows.indexOf("tags") != -1 || isQuery; + // Collapse the tag selector if the item does not accept tags. + if (!this._element("tagsSelectorRow").collapsed && + this._element("tagsRow").collapsed) + this.toggleTagsSelector(); + this._element("descriptionRow").collapsed = + this._hiddenRows.indexOf("description") != -1 || this._readOnly; + this._element("keywordRow").collapsed = !isBookmark || this._readOnly || + this._hiddenRows.indexOf("keyword") != -1 || isQuery; + this._element("locationRow").collapsed = !(this._uri && !isQuery) || + this._hiddenRows.indexOf("location") != -1; + this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery || + this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1; + this._element("feedLocationRow").collapsed = !this._isLivemark || + this._hiddenRows.indexOf("feedLocation") != -1; + this._element("siteLocationRow").collapsed = !this._isLivemark || + this._hiddenRows.indexOf("siteLocation") != -1; + this._element("selectionCount").hidden = !this._multiEdit; + }, + + /** + * Initialize the panel + * @param aFor + * Either a places-itemId (of a bookmark, folder or a live bookmark), + * an array of itemIds (used for bulk tagging), or a URI object (in + * which case, the panel would be initialized in read-only mode). + * @param [optional] aInfo + * JS object which stores additional info for the panel + * initialization. The following properties may bet set: + * * hiddenRows (Strings array): list of rows to be hidden regardless + * of the item edited. Possible values: "title", "location", + * "description", "keyword", "loadInSidebar", "feedLocation", + * "siteLocation", folderPicker" + * * forceReadOnly - set this flag to initialize the panel to its + * read-only (view) mode even if the given item is editable. + */ + initPanel: function(aFor, aInfo) { + // For sanity ensure that the implementer has uninited the panel before + // trying to init it again, or we could end up leaking due to observers. + if (this._initialized) + this.uninitPanel(false); + + var aItemIdList; + if (Array.isArray(aFor)) { + aItemIdList = aFor; + aFor = aItemIdList[0]; + } + else if (this._multiEdit) { + this._multiEdit = false; + this._tags = []; + this._uris = []; + this._allTags = []; + this._itemIds = []; + this._element("selectionCount").hidden = true; + } + + this._folderMenuList = this._element("folderMenuList"); + this._folderTree = this._element("folderTree"); + + this._determineInfo(aInfo); + if (aFor instanceof Ci.nsIURI) { + this._itemId = -1; + this._uri = aFor; + this._readOnly = true; + } + else { + this._itemId = aFor; + // We can't store information on invalid itemIds. + this._readOnly = this._readOnly || this._itemId == -1; + + var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); + this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId); + if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { + this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + this._keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId); + this._initTextField("keywordField", this._keyword); + this._element("loadInSidebarCheckbox").checked = + PlacesUtils.annotations.itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + } + else { + this._uri = null; + this._isLivemark = false; + PlacesUtils.livemarks.getLivemark({id: this._itemId }) + .then(aLivemark => { + this._isLivemark = true; + this._initTextField("feedLocationField", aLivemark.feedURI.spec, true); + this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true); + this._showHideRows(); + }, () => undefined); + } + + // folder picker + this._initFolderMenuList(containerId); + + // description field + this._initTextField("descriptionField", + PlacesUIUtils.getItemDescription(this._itemId)); + } + + if (this._itemId == -1 || + this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) { + this._isLivemark = false; + + this._initTextField("locationField", this._uri.spec); + if (!aItemIdList) { + var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + } + else { + this._multiEdit = true; + this._allTags = []; + this._itemIds = aItemIdList; + for (var i = 0; i < aItemIdList.length; i++) { + if (aItemIdList[i] instanceof Ci.nsIURI) { + this._uris[i] = aItemIdList[i]; + this._itemIds[i] = -1; + } + else + this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]); + this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); + } + this._allTags = this._getCommonTags(); + this._initTextField("tagsField", this._allTags.join(", "), false); + this._element("itemsCountText").value = + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", + this._itemIds.length, + [this._itemIds.length]); + } + + // tags selector + this._rebuildTagsSelectorList(); + } + + // name picker + this._initNamePicker(); + + this._showHideRows(); + + // observe changes + if (!this._observersAdded) { + // Single bookmarks observe any change. History entries and multiEdit + // observe only tags changes, through bookmarks. + if (this._itemId != -1 || this._uri || this._multiEdit) + PlacesUtils.bookmarks.addObserver(this, false); + + this._element("namePicker").addEventListener("blur", this); + this._element("locationField").addEventListener("blur", this); + this._element("tagsField").addEventListener("blur", this); + this._element("keywordField").addEventListener("blur", this); + this._element("descriptionField").addEventListener("blur", this); + window.addEventListener("unload", this, false); + this._observersAdded = true; + } + + let focusElement = () => { + this._initialized = true; + }; + + if (this._onPanelReady) { + this._onPanelReady(focusElement); + } else { + focusElement(); + } + }, + + /** + * Finds tags that are in common among this._tags entries that track tags + * for each selected uri. + * The tags arrays should be kept up-to-date for this to work properly. + * + * @return array of common tags for the selected uris. + */ + _getCommonTags: function() { + return this._tags[0].filter( + function(aTag) this._tags.every( + function(aTags) aTags.indexOf(aTag) != -1 + ), this + ); + }, + + _initTextField: function(aTextFieldId, aValue, aReadOnly) { + var field = this._element(aTextFieldId); + field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly; + + if (field.value != aValue) { + field.value = aValue; + this._editorTransactionManagerClear(field); + } + }, + + /** + * Appends a menu-item representing a bookmarks folder to a menu-popup. + * @param aMenupopup + * The popup to which the menu-item should be added. + * @param aFolderId + * The identifier of the bookmarks folder. + * @return the new menu item. + */ + _appendFolderItemToMenupopup: + function(aMenupopup, aFolderId) { + // First make sure the folders-separator is visible + this._element("foldersSeparator").hidden = false; + + var folderMenuItem = document.createElement("menuitem"); + var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId) + folderMenuItem.folderId = aFolderId; + folderMenuItem.setAttribute("label", folderTitle); + folderMenuItem.className = "menuitem-iconic folder-icon"; + aMenupopup.appendChild(folderMenuItem); + return folderMenuItem; + }, + + _initFolderMenuList: function(aSelectedFolder) { + // clean up first + var menupopup = this._folderMenuList.menupopup; + while (menupopup.childNodes.length > 6) + menupopup.removeChild(menupopup.lastChild); + + const bms = PlacesUtils.bookmarks; + const annos = PlacesUtils.annotations; + + // Build the static list + var unfiledItem = this._element("unfiledRootItem"); + if (!this._staticFoldersListBuilt) { + unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId); + unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId; + var bmMenuItem = this._element("bmRootItem"); + bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId); + bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId; + var toolbarItem = this._element("toolbarFolderItem"); + toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId); + toolbarItem.folderId = PlacesUtils.toolbarFolderId; + this._staticFoldersListBuilt = true; + } + + // List of recently used folders: + var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO); + + /** + * The value of the LAST_USED_ANNO annotation is the time (in the form of + * Date.getTime) at which the folder has been last used. + * + * First we build the annotated folders array, each item has both the + * folder identifier and the time at which it was last-used by this dialog + * set. Then we sort it descendingly based on the time field. + */ + this._recentFolders = []; + for (var i = 0; i < folderIds.length; i++) { + var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO); + this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed }); + } + this._recentFolders.sort(function(a, b) { + if (b.lastUsed < a.lastUsed) + return -1; + if (b.lastUsed > a.lastUsed) + return 1; + return 0; + }); + + var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST, + this._recentFolders.length); + for (var i = 0; i < numberOfItems; i++) { + this._appendFolderItemToMenupopup(menupopup, + this._recentFolders[i].folderId); + } + + var defaultItem = this._getFolderMenuItem(aSelectedFolder); + this._folderMenuList.selectedItem = defaultItem; + + // Set a selectedIndex attribute to show special icons + this._folderMenuList.setAttribute("selectedIndex", + this._folderMenuList.selectedIndex); + + // Hide the folders-separator if no folder is annotated as recently-used + this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6); + this._folderMenuList.disabled = this._readOnly; + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIDOMEventListener) || + aIID.equals(Ci.nsINavBookmarkObserver) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + _element: function(aID) { + return document.getElementById("editBMPanel_" + aID); + }, + + _editorTransactionManagerClear: function(aItem) { + // Clear the editor's undo stack + let transactionManager; + try { + transactionManager = aItem.editor.transactionManager; + } catch (e) { + // When retrieving the transaction manager, editor may be null resulting + // in a TypeError. Additionally, the transaction manager may not + // exist yet, which causes access to it to throw NS_ERROR_FAILURE. + // In either event, the transaction manager doesn't exist it, so we + // don't need to worry about clearing it. + if (!(e instanceof TypeError) && e.result != Cr.NS_ERROR_FAILURE) { + throw e; + } + } + if (transactionManager) { + transactionManager.clear(); + } + }, + + _getItemStaticTitle: function() { + if (this._titleOverride) + return this._titleOverride; + + let title = ""; + if (this._itemId == -1) { + title = PlacesUtils.history.getPageTitle(this._uri); + } + else { + title = PlacesUtils.bookmarks.getItemTitle(this._itemId); + } + return title; + }, + + _initNamePicker: function() { + var namePicker = this._element("namePicker"); + namePicker.value = this._getItemStaticTitle(); + namePicker.readOnly = this._readOnly; + this._editorTransactionManagerClear(namePicker); + }, + + uninitPanel: function(aHideCollapsibleElements) { + if (aHideCollapsibleElements) { + // hide the folder tree if it was previously visible + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) + this.toggleFolderTreeVisibility(); + + // hide the tag selector if it was previously visible + var tagsSelectorRow = this._element("tagsSelectorRow"); + if (!tagsSelectorRow.collapsed) + this.toggleTagsSelector(); + } + + if (this._observersAdded) { + if (this._itemId != -1 || this._uri || this._multiEdit) + PlacesUtils.bookmarks.removeObserver(this); + + this._element("namePicker").removeEventListener("blur", this); + this._element("locationField").removeEventListener("blur", this); + this._element("tagsField").removeEventListener("blur", this); + this._element("keywordField").removeEventListener("blur", this); + this._element("descriptionField").removeEventListener("blur", this); + + this._observersAdded = false; + } + + this._itemId = -1; + this._uri = null; + this._uris = []; + this._tags = []; + this._allTags = []; + this._itemIds = []; + this._multiEdit = false; + this._firstEditedField = ""; + this._initialized = false; + this._titleOverride = ""; + this._readOnly = false; + }, + + onTagsFieldBlur: function() { + if (this._updateTags()) // if anything has changed + this._mayUpdateFirstEditField("tagsField"); + }, + + _updateTags: function() { + if (this._multiEdit) + return this._updateMultipleTagsForItems(); + return this._updateSingleTagForItem(); + }, + + _updateSingleTagForItem: function() { + var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri); + var tags = this._getTagsArrayFromTagField(); + if (tags.length > 0 || currentTags.length > 0) { + var tagsToRemove = []; + var tagsToAdd = []; + var txns = []; + for (var i = 0; i < currentTags.length; i++) { + if (tags.indexOf(currentTags[i]) == -1) + tagsToRemove.push(currentTags[i]); + } + for (var i = 0; i < tags.length; i++) { + if (currentTags.indexOf(tags[i]) == -1) + tagsToAdd.push(tags[i]); + } + + if (tagsToRemove.length > 0) { + let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove); + txns.push(untagTxn); + } + if (tagsToAdd.length > 0) { + let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd); + txns.push(tagTxn); + } + + if (txns.length > 0) { + let aggregate = new PlacesAggregatedTransaction("Update tags", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + + // Ensure the tagsField is in sync, clean it up from empty tags + var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + return true; + } + } + return false; + }, + + /** + * Stores the first-edit field for this dialog, if the passed-in field + * is indeed the first edited field + * @param aNewField + * the id of the field that may be set (without the "editBMPanel_" + * prefix) + */ + _mayUpdateFirstEditField: function(aNewField) { + // * The first-edit-field behavior is not applied in the multi-edit case + // * if this._firstEditedField is already set, this is not the first field, + // so there's nothing to do + if (this._multiEdit || this._firstEditedField) + return; + + this._firstEditedField = aNewField; + + // set the pref + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField); + }, + + _updateMultipleTagsForItems: function() { + var tags = this._getTagsArrayFromTagField(); + if (tags.length > 0 || this._allTags.length > 0) { + var tagsToRemove = []; + var tagsToAdd = []; + var txns = []; + for (var i = 0; i < this._allTags.length; i++) { + if (tags.indexOf(this._allTags[i]) == -1) + tagsToRemove.push(this._allTags[i]); + } + for (var i = 0; i < this._tags.length; i++) { + tagsToAdd[i] = []; + for (var j = 0; j < tags.length; j++) { + if (this._tags[i].indexOf(tags[j]) == -1) + tagsToAdd[i].push(tags[j]); + } + } + + if (tagsToAdd.length > 0) { + for (let i = 0; i < this._uris.length; i++) { + if (tagsToAdd[i].length > 0) { + let tagTxn = new PlacesTagURITransaction(this._uris[i], + tagsToAdd[i]); + txns.push(tagTxn); + } + } + } + if (tagsToRemove.length > 0) { + for (let i = 0; i < this._uris.length; i++) { + let untagTxn = new PlacesUntagURITransaction(this._uris[i], + tagsToRemove); + txns.push(untagTxn); + } + } + + if (txns.length > 0) { + let aggregate = new PlacesAggregatedTransaction("Update tags", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + + this._allTags = tags; + this._tags = []; + for (let i = 0; i < this._uris.length; i++) { + this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]); + } + + // Ensure the tagsField is in sync, clean it up from empty tags + this._initTextField("tagsField", tags, false); + return true; + } + } + return false; + }, + + onNamePickerBlur: function() { + if (this._itemId == -1) + return; + + var namePicker = this._element("namePicker") + + // Here we update either the item title or its cached static title + var newTitle = namePicker.value; + if (!newTitle && + PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) { + // We don't allow setting an empty title for a tag, restore the old one. + this._initNamePicker(); + } + else if (this._getItemStaticTitle() != newTitle) { + this._mayUpdateFirstEditField("namePicker"); + let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onDescriptionFieldBlur: function() { + var description = this._element("descriptionField").value; + if (description != PlacesUIUtils.getItemDescription(this._itemId)) { + var annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO, + type : Ci.nsIAnnotationService.TYPE_STRING, + flags : 0, + value : description, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onLocationFieldBlur: function() { + var uri; + try { + uri = PlacesUIUtils.createFixedURI(this._element("locationField").value); + } + catch(ex) { return; } + + if (!this._uri.equals(uri)) { + var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri); + PlacesUtils.transactionManager.doTransaction(txn); + this._uri = uri; + } + }, + + onKeywordFieldBlur: function() { + let oldKeyword = this._keyword; + let keyword = this._keyword = this._element("keywordField").value; + if (keyword != oldKeyword) { + let txn = new PlacesEditBookmarkKeywordTransaction(this._itemId, + keyword, + null, + oldKeyword); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + onLoadInSidebarCheckboxCommand: + function() { + let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO }; + if (this._element("loadInSidebarCheckbox").checked) + annoObj.value = true; + let txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj); + PlacesUtils.transactionManager.doTransaction(txn); + }, + + toggleFolderTreeVisibility: function() { + var expander = this._element("foldersExpander"); + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) { + expander.className = "expander-down"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextdown")); + folderTreeRow.collapsed = true; + this._element("chooseFolderSeparator").hidden = + this._element("chooseFolderMenuItem").hidden = false; + } + else { + expander.className = "expander-up" + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextup")); + folderTreeRow.collapsed = false; + + // XXXmano: Ideally we would only do this once, but for some odd reason, + // the editable mode set on this tree, together with its collapsed state + // breaks the view. + const FOLDER_TREE_PLACE_URI = + "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" + + PlacesUIUtils.allBookmarksFolderId; + this._folderTree.place = FOLDER_TREE_PLACE_URI; + + this._element("chooseFolderSeparator").hidden = + this._element("chooseFolderMenuItem").hidden = true; + var currentFolder = this._getFolderIdFromMenuList(); + this._folderTree.selectItems([currentFolder]); + this._folderTree.focus(); + } + }, + + _getFolderIdFromMenuList: + function() { + var selectedItem = this._folderMenuList.selectedItem; + NS_ASSERT("folderId" in selectedItem, + "Invalid menuitem in the folders-menulist"); + return selectedItem.folderId; + }, + + /** + * Get the corresponding menu-item in the folder-menu-list for a bookmarks + * folder if such an item exists. Otherwise, this creates a menu-item for the + * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached, + * the new item replaces the last menu-item. + * @param aFolderId + * The identifier of the bookmarks folder. + */ + _getFolderMenuItem: + function(aFolderId) { + var menupopup = this._folderMenuList.menupopup; + + for (let i = 0; i < menupopup.childNodes.length; i++) { + if ("folderId" in menupopup.childNodes[i] && + menupopup.childNodes[i].folderId == aFolderId) + return menupopup.childNodes[i]; + } + + // 3 special folders + separator + folder-items-count limit + if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST) + menupopup.removeChild(menupopup.lastChild); + + return this._appendFolderItemToMenupopup(menupopup, aFolderId); + }, + + onFolderMenuListCommand: function(aEvent) { + // Set a selectedIndex attribute to show special icons + this._folderMenuList.setAttribute("selectedIndex", + this._folderMenuList.selectedIndex); + + if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") { + // reset the selection back to where it was and expand the tree + // (this menu-item is hidden when the tree is already visible + var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId); + var item = this._getFolderMenuItem(container); + this._folderMenuList.selectedItem = item; + // XXXmano HACK: setTimeout 100, otherwise focus goes back to the + // menulist right away + setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this); + return; + } + + // Move the item + var container = this._getFolderIdFromMenuList(); + if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) { + var txn = new PlacesMoveItemTransaction(this._itemId, + container, + PlacesUtils.bookmarks.DEFAULT_INDEX); + PlacesUtils.transactionManager.doTransaction(txn); + + // Mark the containing folder as recently-used if it isn't in the + // static list + if (container != PlacesUtils.unfiledBookmarksFolderId && + container != PlacesUtils.toolbarFolderId && + container != PlacesUtils.bookmarksMenuFolderId) + this._markFolderAsRecentlyUsed(container); + } + + // Update folder-tree selection + var folderTreeRow = this._element("folderTreeRow"); + if (!folderTreeRow.collapsed) { + var selectedNode = this._folderTree.selectedNode; + if (!selectedNode || + PlacesUtils.getConcreteItemId(selectedNode) != container) + this._folderTree.selectItems([container]); + } + }, + + onFolderTreeSelect: function() { + var selectedNode = this._folderTree.selectedNode; + + // Disable the "New Folder" button if we cannot create a new folder + this._element("newFolderButton") + .disabled = !this._folderTree.insertionPoint || !selectedNode; + + if (!selectedNode) + return; + + var folderId = PlacesUtils.getConcreteItemId(selectedNode); + if (this._getFolderIdFromMenuList() == folderId) + return; + + var folderItem = this._getFolderMenuItem(folderId); + this._folderMenuList.selectedItem = folderItem; + folderItem.doCommand(); + }, + + _markFolderAsRecentlyUsed: + function(aFolderId) { + var txns = []; + + // Expire old unused recent folders + var anno = this._getLastUsedAnnotationObject(false); + while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) { + var folderId = this._recentFolders.pop().folderId; + let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno); + txns.push(annoTxn); + } + + // Mark folder as recently used + anno = this._getLastUsedAnnotationObject(true); + let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno); + txns.push(annoTxn); + + let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns); + PlacesUtils.transactionManager.doTransaction(aggregate); + }, + + /** + * Returns an object which could then be used to set/unset the + * LAST_USED_ANNO annotation for a folder. + * + * @param aLastUsed + * Whether to set or unset the LAST_USED_ANNO annotation. + * @returns an object representing the annotation which could then be used + * with the transaction manager. + */ + _getLastUsedAnnotationObject: + function(aLastUsed) { + var anno = { name: LAST_USED_ANNO, + type: Ci.nsIAnnotationService.TYPE_INT32, + flags: 0, + value: aLastUsed ? new Date().getTime() : null, + expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; + + return anno; + }, + + _rebuildTagsSelectorList: function() { + var tagsSelector = this._element("tagsSelector"); + var tagsSelectorRow = this._element("tagsSelectorRow"); + if (tagsSelectorRow.collapsed) + return; + + // Save the current scroll position and restore it after the rebuild. + let firstIndex = tagsSelector.getIndexOfFirstVisibleRow(); + let selectedIndex = tagsSelector.selectedIndex; + let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label + : null; + + while (tagsSelector.hasChildNodes()) + tagsSelector.removeChild(tagsSelector.lastChild); + + var tagsInField = this._getTagsArrayFromTagField(); + var allTags = PlacesUtils.tagging.allTags; + for (var i = 0; i < allTags.length; i++) { + var tag = allTags[i]; + var elt = document.createElement("listitem"); + elt.setAttribute("type", "checkbox"); + elt.setAttribute("label", tag); + if (tagsInField.indexOf(tag) != -1) + elt.setAttribute("checked", "true"); + tagsSelector.appendChild(elt); + if (selectedTag === tag) + selectedIndex = tagsSelector.getIndexOfItem(elt); + } + + // Restore position. + // The listbox allows to scroll only if the required offset doesn't + // overflow its capacity, thus need to adjust the index for removals. + firstIndex = + Math.min(firstIndex, + tagsSelector.itemCount - tagsSelector.getNumberOfVisibleRows()); + tagsSelector.scrollToIndex(firstIndex); + if (selectedIndex >= 0 && tagsSelector.itemCount > 0) { + selectedIndex = Math.min(selectedIndex, tagsSelector.itemCount - 1); + tagsSelector.selectedIndex = selectedIndex; + tagsSelector.ensureIndexIsVisible(selectedIndex); + } + }, + + toggleTagsSelector: function() { + var tagsSelector = this._element("tagsSelector"); + var tagsSelectorRow = this._element("tagsSelectorRow"); + var expander = this._element("tagsSelectorExpander"); + if (tagsSelectorRow.collapsed) { + expander.className = "expander-up"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextup")); + tagsSelectorRow.collapsed = false; + this._rebuildTagsSelectorList(); + + // This is a no-op if we've added the listener. + tagsSelector.addEventListener("CheckboxStateChange", this, false); + } + else { + expander.className = "expander-down"; + expander.setAttribute("tooltiptext", + expander.getAttribute("tooltiptextdown")); + tagsSelectorRow.collapsed = true; + } + }, + + /** + * Splits "tagsField" element value, returning an array of valid tag strings. + * + * @return Array of tag strings found in the field value. + */ + _getTagsArrayFromTagField: function() { + let tags = this._element("tagsField").value; + return tags.trim() + .split(/\s*,\s*/) // Split on commas and remove spaces. + .filter(function(tag) tag.length > 0); // Kill empty tags. + }, + + newFolder: function() { + var ip = this._folderTree.insertionPoint; + + // default to the bookmarks menu folder + if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) { + ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); + } + + // XXXmano: add a separate "New Folder" string at some point... + var defaultLabel = this._element("newFolderButton").label; + var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index); + PlacesUtils.transactionManager.doTransaction(txn); + this._folderTree.focus(); + this._folderTree.selectItems([this._lastNewItem]); + this._folderTree.startEditing(this._folderTree.view.selection.currentIndex, + this._folderTree.columns.getFirstColumn()); + }, + + // nsIDOMEventListener + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "CheckboxStateChange": + // Update the tags field when items are checked/unchecked in the listbox + var tags = this._getTagsArrayFromTagField(); + + if (aEvent.target.checked) { + if (tags.indexOf(aEvent.target.label) == -1) + tags.push(aEvent.target.label); + } + else { + var indexOfItem = tags.indexOf(aEvent.target.label); + if (indexOfItem != -1) + tags.splice(indexOfItem, 1); + } + this._element("tagsField").value = tags.join(", "); + this._updateTags(); + break; + case "blur": + let replaceFn = (str, firstLetter) => firstLetter.toUpperCase(); + let nodeName = aEvent.target.id.replace(/editBMPanel_(\w)/, replaceFn); + this["on" + nodeName + "Blur"](); + break; + case "unload": + this.uninitPanel(false); + break; + } + }, + + // nsINavBookmarkObserver + onItemChanged: function(aItemId, aProperty, + aIsAnnotationProperty, aValue, + aLastModified, aItemType) { + if (aProperty == "tags") { + // Tags case is special, since they should be updated if either: + // - the notification is for the edited bookmark + // - the notification is for the edited history entry + // - the notification is for one of edited uris + let shouldUpdateTagsField = this._itemId == aItemId; + if (this._itemId == -1 || this._multiEdit) { + // Check if the changed uri is part of the modified ones. + let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId); + let uris = this._multiEdit ? this._uris : [this._uri]; + uris.forEach(function(aURI, aIndex) { + if (aURI.equals(changedURI)) { + shouldUpdateTagsField = true; + if (this._multiEdit) { + this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]); + } + } + }, this); + } + + if (shouldUpdateTagsField) { + if (this._multiEdit) { + this._allTags = this._getCommonTags(); + this._initTextField("tagsField", this._allTags.join(", "), false); + } + else { + let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", "); + this._initTextField("tagsField", tags, false); + } + } + + // Any tags change should be reflected in the tags selector. + this._rebuildTagsSelectorList(); + return; + } + + if (this._itemId != aItemId) { + if (aProperty == "title") { + // If the title of a folder which is listed within the folders + // menulist has been changed, we need to update the label of its + // representing element. + var menupopup = this._folderMenuList.menupopup; + for (let i = 0; i < menupopup.childNodes.length; i++) { + if ("folderId" in menupopup.childNodes[i] && + menupopup.childNodes[i].folderId == aItemId) { + menupopup.childNodes[i].label = aValue; + break; + } + } + } + + return; + } + + switch (aProperty) { + case "title": + var namePicker = this._element("namePicker"); + if (namePicker.value != aValue) { + namePicker.value = aValue; + this._editorTransactionManagerClear(namePicker); + } + break; + case "uri": + var locationField = this._element("locationField"); + if (locationField.value != aValue) { + this._uri = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newURI(aValue, null, null); + this._initTextField("locationField", this._uri.spec); + this._initNamePicker(); + this._initTextField("tagsField", + PlacesUtils.tagging + .getTagsForURI(this._uri).join(", "), + false); + this._rebuildTagsSelectorList(); + } + break; + case "keyword": + this._keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId); + this._initTextField("keywordField", this._keyword); + break; + case PlacesUIUtils.DESCRIPTION_ANNO: + this._initTextField("descriptionField", + PlacesUIUtils.getItemDescription(this._itemId)); + break; + case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO: + this._element("loadInSidebarCheckbox").checked = + PlacesUtils.annotations.itemHasAnnotation(this._itemId, + PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO); + break; + case PlacesUtils.LMANNO_FEEDURI: + let feedURISpec = + PlacesUtils.annotations.getItemAnnotation(this._itemId, + PlacesUtils.LMANNO_FEEDURI); + this._initTextField("feedLocationField", feedURISpec, true); + break; + case PlacesUtils.LMANNO_SITEURI: + let siteURISpec = ""; + try { + siteURISpec = + PlacesUtils.annotations.getItemAnnotation(this._itemId, + PlacesUtils.LMANNO_SITEURI); + } catch (ex) {} + this._initTextField("siteLocationField", siteURISpec, true); + break; + } + }, + + onItemMoved: function(aItemId, aOldParent, aOldIndex, + aNewParent, aNewIndex, aItemType) { + if (aItemId != this._itemId || + aNewParent == this._getFolderIdFromMenuList()) + return; + + var folderItem = this._getFolderMenuItem(aNewParent); + + // just setting selectItem _does not_ trigger oncommand, so we don't + // recurse + this._folderMenuList.selectedItem = folderItem; + }, + + onItemAdded: function(aItemId, aParentId, aIndex, aItemType, + aURI) { + this._lastNewItem = aItemId; + }, + + onItemRemoved: function() { }, + onBeginUpdateBatch: function() { }, + onEndUpdateBatch: function() { }, + onItemVisited: function() { }, +}; diff --git a/browser/components/places/content/editBookmarkOverlay.xul b/browser/components/places/content/editBookmarkOverlay.xul new file mode 100644 index 000000000..196369dd2 --- /dev/null +++ b/browser/components/places/content/editBookmarkOverlay.xul @@ -0,0 +1,228 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd"> +%editBookmarkOverlayDTD; +]> + +<?xml-stylesheet href="chrome://browser/skin/places/editBookmarkOverlay.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> + +<overlay id="editBookmarkOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <vbox id="editBookmarkPanelContent" flex="1"> + <broadcaster id="paneElementsBroadcaster"/> + + <hbox id="editBMPanel_selectionCount" hidden="true" pack="center"> + <label id="editBMPanel_itemsCountText"/> + </hbox> + + <grid id="editBookmarkPanelGrid" flex="1"> + <columns id="editBMPanel_columns"> + <column id="editBMPanel_labelColumn" /> + <column flex="1" id="editBMPanel_editColumn" /> + </columns> + <rows id="editBMPanel_rows"> + <row id="editBMPanel_nameRow" + align="center" + collapsed="true"> + <label value="&editBookmarkOverlay.name.label;" + class="editBMPanel_rowLabel" + accesskey="&editBookmarkOverlay.name.accesskey;" + control="editBMPanel_namePicker" + observes="paneElementsBroadcaster"/> + <textbox id="editBMPanel_namePicker" + observes="paneElementsBroadcaster"/> + </row> + + <row id="editBMPanel_locationRow" + align="center" + collapsed="true"> + <label value="&editBookmarkOverlay.location.label;" + class="editBMPanel_rowLabel" + accesskey="&editBookmarkOverlay.location.accesskey;" + control="editBMPanel_locationField" + observes="paneElementsBroadcaster"/> + <textbox id="editBMPanel_locationField" + class="uri-element" + observes="paneElementsBroadcaster"/> + </row> + + <row id="editBMPanel_feedLocationRow" + align="center" + collapsed="true"> + <label value="&editBookmarkOverlay.feedLocation.label;" + class="editBMPanel_rowLabel" + accesskey="&editBookmarkOverlay.feedLocation.accesskey;" + control="editBMPanel_feedLocationField" + observes="paneElementsBroadcaster"/> + <textbox id="editBMPanel_feedLocationField" + class="uri-element" + observes="paneElementsBroadcaster"/> + </row> + + <row id="editBMPanel_siteLocationRow" + align="center" + collapsed="true"> + <label value="&editBookmarkOverlay.siteLocation.label;" + class="editBMPanel_rowLabel" + accesskey="&editBookmarkOverlay.siteLocation.accesskey;" + control="editBMPanel_siteLocationField" + observes="paneElementsBroadcaster"/> + <textbox id="editBMPanel_siteLocationField" + class="uri-element" + observes="paneElementsBroadcaster"/> + </row> + + <row id="editBMPanel_folderRow" + align="center" + collapsed="true"> + <label value="&editBookmarkOverlay.folder.label;" + class="editBMPanel_rowLabel" + control="editBMPanel_folderMenuList" + observes="paneElementsBroadcaster"/> + <hbox flex="1" align="center"> + <menulist id="editBMPanel_folderMenuList" + class="folder-icon" + flex="1" + oncommand="gEditItemOverlay.onFolderMenuListCommand(event);" + observes="paneElementsBroadcaster"> + <menupopup> + <!-- Static item for special folders --> + <menuitem id="editBMPanel_toolbarFolderItem" + class="menuitem-iconic folder-icon"/> + <menuitem id="editBMPanel_bmRootItem" + class="menuitem-iconic folder-icon"/> + <menuitem id="editBMPanel_unfiledRootItem" + class="menuitem-iconic folder-icon"/> + <menuseparator id="editBMPanel_chooseFolderSeparator"/> + <menuitem id="editBMPanel_chooseFolderMenuItem" + label="&editBookmarkOverlay.choose.label;" + class="menuitem-iconic folder-icon"/> + <menuseparator id="editBMPanel_foldersSeparator" hidden="true"/> + </menupopup> + </menulist> + <button id="editBMPanel_foldersExpander" + class="expander-down" + tooltiptext="&editBookmarkOverlay.foldersExpanderDown.tooltip;" + tooltiptextdown="&editBookmarkOverlay.foldersExpanderDown.tooltip;" + tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;" + oncommand="gEditItemOverlay.toggleFolderTreeVisibility();" + observes="paneElementsBroadcaster"/> + </hbox> + </row> + + <row id="editBMPanel_folderTreeRow" + collapsed="true" + flex="1"> + <spacer/> + <vbox flex="1"> + <tree id="editBMPanel_folderTree" + flex="1" + class="placesTree" + type="places" + height="150" + minheight="150" + editable="true" + onselect="gEditItemOverlay.onFolderTreeSelect();" + hidecolumnpicker="true" + observes="paneElementsBroadcaster"> + <treecols> + <treecol anonid="title" flex="1" primary="true" hideheader="true"/> + </treecols> + <treechildren flex="1"/> + </tree> + + <hbox id="editBMPanel_newFolderBox"> + <button label="&editBookmarkOverlay.newFolderButton.label;" + id="editBMPanel_newFolderButton" + accesskey="&editBookmarkOverlay.newFolderButton.accesskey;" + oncommand="gEditItemOverlay.newFolder();"/> + </hbox> + </vbox> + </row> + + <row id="editBMPanel_tagsRow" + align="center" + collapsed="true"> + <label value="&editBookmarkOverlay.tags.label;" + class="editBMPanel_rowLabel" + accesskey="&editBookmarkOverlay.tags.accesskey;" + control="editBMPanel_tagsField" + observes="paneElementsBroadcaster"/> + <hbox flex="1" align="center"> + <textbox id="editBMPanel_tagsField" + type="autocomplete" + class="padded" + flex="1" + autocompletesearch="places-tag-autocomplete" + completedefaultindex="true" + tabscrolling="true" + showcommentcolumn="true" + observes="paneElementsBroadcaster" + placeholder="&editBookmarkOverlay.tagsEmptyDesc.label;"/> + <button id="editBMPanel_tagsSelectorExpander" + class="expander-down" + tooltiptext="&editBookmarkOverlay.tagsExpanderDown.tooltip;" + tooltiptextdown="&editBookmarkOverlay.tagsExpanderDown.tooltip;" + tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;" + oncommand="gEditItemOverlay.toggleTagsSelector();" + observes="paneElementsBroadcaster"/> + </hbox> + </row> + + <row id="editBMPanel_tagsSelectorRow" + align="center" + collapsed="true"> + <spacer/> + <listbox id="editBMPanel_tagsSelector" + height="150" + observes="paneElementsBroadcaster"/> + </row> + + <row id="editBMPanel_keywordRow" + align="center" + collapsed="true"> + <observes element="additionalInfoBroadcaster" attribute="hidden"/> + <label value="&editBookmarkOverlay.keyword.label;" + class="editBMPanel_rowLabel" + accesskey="&editBookmarkOverlay.keyword.accesskey;" + control="editBMPanel_keywordField" + observes="paneElementsBroadcaster"/> + <textbox id="editBMPanel_keywordField" + observes="paneElementsBroadcaster"/> + </row> + + <row id="editBMPanel_descriptionRow" + collapsed="true"> + <observes element="additionalInfoBroadcaster" attribute="hidden"/> + <label value="&editBookmarkOverlay.description.label;" + class="editBMPanel_rowLabel" + accesskey="&editBookmarkOverlay.description.accesskey;" + control="editBMPanel_descriptionField" + observes="paneElementsBroadcaster"/> + <textbox id="editBMPanel_descriptionField" + multiline="true" + observes="paneElementsBroadcaster"/> + </row> + </rows> + </grid> + + <checkbox id="editBMPanel_loadInSidebarCheckbox" + collapsed="true" + label="&editBookmarkOverlay.loadInSidebar.label;" + accesskey="&editBookmarkOverlay.loadInSidebar.accesskey;" + oncommand="gEditItemOverlay.onLoadInSidebarCheckboxCommand();" + observes="paneElementsBroadcaster"> + <observes element="additionalInfoBroadcaster" attribute="hidden"/> + </checkbox> + + <!-- If the ids are changing or additional fields are being added, be sure + to sync the values in places.js --> + <broadcaster id="additionalInfoBroadcaster"/> + + </vbox> +</overlay> diff --git a/browser/components/places/content/history-panel.js b/browser/components/places/content/history-panel.js new file mode 100644 index 000000000..cda39dd26 --- /dev/null +++ b/browser/components/places/content/history-panel.js @@ -0,0 +1,91 @@ +/* -*- 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/. */ + +var gHistoryTree; +var gSearchBox; +var gHistoryGrouping = ""; +var gSearching = false; + +function HistorySidebarInit() +{ + gHistoryTree = document.getElementById("historyTree"); + gSearchBox = document.getElementById("search-box"); + + gHistoryGrouping = document.getElementById("viewButton"). + getAttribute("selectedsort"); + + if (gHistoryGrouping == "site") + document.getElementById("bysite").setAttribute("checked", "true"); + else if (gHistoryGrouping == "visited") + document.getElementById("byvisited").setAttribute("checked", "true"); + else if (gHistoryGrouping == "lastvisited") + document.getElementById("bylastvisited").setAttribute("checked", "true"); + else if (gHistoryGrouping == "dayandsite") + document.getElementById("bydayandsite").setAttribute("checked", "true"); + else + document.getElementById("byday").setAttribute("checked", "true"); + + searchHistory(""); +} + +function GroupBy(groupingType) +{ + gHistoryGrouping = groupingType; + searchHistory(gSearchBox.value); +} + +function searchHistory(aInput) +{ + var query = PlacesUtils.history.getNewQuery(); + var options = PlacesUtils.history.getNewQueryOptions(); + + const NHQO = Ci.nsINavHistoryQueryOptions; + var sortingMode; + var resultType; + + switch (gHistoryGrouping) { + case "visited": + resultType = NHQO.RESULTS_AS_URI; + sortingMode = NHQO.SORT_BY_VISITCOUNT_DESCENDING; + break; + case "lastvisited": + resultType = NHQO.RESULTS_AS_URI; + sortingMode = NHQO.SORT_BY_DATE_DESCENDING; + break; + case "dayandsite": + resultType = NHQO.RESULTS_AS_DATE_SITE_QUERY; + break; + case "site": + resultType = NHQO.RESULTS_AS_SITE_QUERY; + sortingMode = NHQO.SORT_BY_TITLE_ASCENDING; + break; + case "day": + default: + resultType = NHQO.RESULTS_AS_DATE_QUERY; + break; + } + + if (aInput) { + query.searchTerms = aInput; + if (gHistoryGrouping != "visited" && gHistoryGrouping != "lastvisited") { + sortingMode = NHQO.SORT_BY_FRECENCY_DESCENDING; + resultType = NHQO.RESULTS_AS_URI; + } + } + + options.sortingMode = sortingMode; + options.resultType = resultType; + options.includeHidden = !!aInput; + + // call load() on the tree manually + // instead of setting the place attribute in history-panel.xul + // otherwise, we will end up calling load() twice + gHistoryTree.load([query], options); +} + +window.addEventListener("SidebarFocused", + function() + gSearchBox.focus(), + false); diff --git a/browser/components/places/content/history-panel.xul b/browser/components/places/content/history-panel.xul new file mode 100644 index 000000000..bcc581a60 --- /dev/null +++ b/browser/components/places/content/history-panel.xul @@ -0,0 +1,92 @@ +<?xml version="1.0"?> <!-- -*- Mode: xml; 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/. --> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> + +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +<!DOCTYPE page [ +<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd"> +%placesDTD; +]> + +<!-- we need to keep id="history-panel" for upgrade and switching + between versions of the browser --> + +<page id="history-panel" orient="vertical" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="HistorySidebarInit();" + onunload="SidebarUtils.setMouseoverURL('');"> + + <script type="application/javascript" + src="chrome://browser/content/bookmarks/sidebarUtils.js"/> + <script type="application/javascript" + src="chrome://browser/content/places/history-panel.js"/> + + <commandset id="editMenuCommands"/> + <commandset id="placesCommands"/> + + <keyset id="editMenuKeys"> + </keyset> + + <!-- required to overlay the context menu --> + <menupopup id="placesContext"/> + + <!-- Bookmarks and history tooltip --> + <tooltip id="bhTooltip"/> + + <hbox id="sidebar-search-container" align="center"> + <label id="sidebar-search-label" + value="&find.label;" accesskey="&find.accesskey;" + control="search-box"/> + <textbox id="search-box" flex="1" type="search" class="compact" + aria-controls="historyTree" + oncommand="searchHistory(this.value);"/> + <button id="viewButton" style="min-width:0px !important;" type="menu" + label="&view.label;" accesskey="&view.accesskey;" selectedsort="day" + persist="selectedsort"> + <menupopup> + <menuitem id="bydayandsite" label="&byDayAndSite.label;" + accesskey="&byDayAndSite.accesskey;" type="radio" + oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'dayandsite'); GroupBy('dayandsite');"/> + <menuitem id="bysite" label="&bySite.label;" + accesskey="&bySite.accesskey;" type="radio" + oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'site'); GroupBy('site');"/> + <menuitem id="byday" label="&byDate.label;" + accesskey="&byDate.accesskey;" + type="radio" + oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'day'); GroupBy('day');"/> + <menuitem id="byvisited" label="&byMostVisited.label;" + accesskey="&byMostVisited.accesskey;" + type="radio" + oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'visited'); GroupBy('visited');"/> + <menuitem id="bylastvisited" label="&byLastVisited.label;" + accesskey="&byLastVisited.accesskey;" + type="radio" + oncommand="this.parentNode.parentNode.setAttribute('selectedsort', 'lastvisited'); GroupBy('lastvisited');"/> + </menupopup> + </button> + </hbox> + + <tree id="historyTree" + class="sidebar-placesTree" + flex="1" + type="places" + context="placesContext" + hidecolumnpicker="true" + onkeypress="SidebarUtils.handleTreeKeyPress(event);" + onclick="SidebarUtils.handleTreeClick(this, event, true);" + onmousemove="SidebarUtils.handleTreeMouseMove(event);" + onmouseout="SidebarUtils.setMouseoverURL('');"> + <treecols> + <treecol id="title" flex="1" primary="true" hideheader="true"/> + </treecols> + <treechildren class="sidebar-placesTreechildren" flex="1" tooltip="bhTooltip"/> + </tree> +</page> diff --git a/browser/components/places/content/menu.xml b/browser/components/places/content/menu.xml new file mode 100644 index 000000000..0fed40966 --- /dev/null +++ b/browser/components/places/content/menu.xml @@ -0,0 +1,475 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<bindings id="placesMenuBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="places-popup-base" + extends="chrome://global/content/bindings/popup.xml#popup"> + <content> + <xul:hbox flex="1"> + <xul:vbox class="menupopup-drop-indicator-bar" hidden="true"> + <xul:image class="menupopup-drop-indicator" mousethrough="always"/> + </xul:vbox> + <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical" + smoothscroll="false"> + <children/> + </xul:arrowscrollbox> + </xul:hbox> + </content> + + <implementation> + + <field name="_indicatorBar"> + document.getAnonymousElementByAttribute(this, "class", + "menupopup-drop-indicator-bar"); + </field> + + <field name="_scrollBox"> + document.getAnonymousElementByAttribute(this, "class", + "popup-internal-box"); + </field> + + <!-- This is the view that manage the popup --> + <field name="_rootView">PlacesUIUtils.getViewForNode(this);</field> + + <!-- Check if we should hide the drop indicator for the target --> + <method name="_hideDropIndicator"> + <parameter name="aEvent"/> + <body><![CDATA[ + let target = aEvent.target; + + // Don't draw the drop indicator outside of markers. + // The markers are hidden, since otherwise sometimes popups acquire + // scrollboxes on OS X, so we can't use them directly. + let firstChildTop = this._startMarker.nextSibling.boxObject.y; + let lastChildBottom = this._endMarker.previousSibling.boxObject.y + + this._endMarker.previousSibling.boxObject.height; + let betweenMarkers = target.boxObject.y >= firstChildTop || + target.boxObject.y <= lastChildBottom; + + // Hide the dropmarker if current node is not a Places node. + return !(target && target._placesNode && betweenMarkers); + ]]></body> + </method> + + <!-- This function returns information about where to drop when + dragging over this popup insertion point --> + <method name="_getDropPoint"> + <parameter name="aEvent"/> + <body><![CDATA[ + // Can't drop if the menu isn't a folder + let resultNode = this._placesNode; + + if (!PlacesUtils.nodeIsFolder(resultNode) || + PlacesControllerDragHelper.disallowInsertion(resultNode)) { + return null; + } + + var dropPoint = { ip: null, folderElt: null }; + + // The element we are dragging over + let elt = aEvent.target; + if (elt.localName == "menupopup") + elt = elt.parentNode; + + // Calculate positions taking care of arrowscrollbox + let eventY = aEvent.layerY; + let scrollbox = this._scrollBox; + let scrollboxOffset = scrollbox.scrollBoxObject.y - + (scrollbox.boxObject.y - this.boxObject.y); + let eltY = elt.boxObject.y - scrollboxOffset; + let eltHeight = elt.boxObject.height; + + if (!elt._placesNode) { + // If we are dragging over a non places node drop at the end. + dropPoint.ip = new InsertionPoint( + PlacesUtils.getConcreteItemId(resultNode), + -1, + Ci.nsITreeView.DROP_ON); + // We can set folderElt if we are dropping over a static menu that + // has an internal placespopup. + let isMenu = elt.localName == "menu" || + (elt.localName == "toolbarbutton" && + elt.getAttribute("type") == "menu"); + if (isMenu && elt.lastChild && + elt.lastChild.hasAttribute("placespopup")) + dropPoint.folderElt = elt; + return dropPoint; + } + if ((PlacesUtils.nodeIsFolder(elt._placesNode) && + !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) || + PlacesUtils.nodeIsTagQuery(elt._placesNode)) { + // This is a folder or a tag container. + if (eventY - eltY < eltHeight * 0.20) { + // If mouse is in the top part of the element, drop above folder. + dropPoint.ip = new InsertionPoint( + PlacesUtils.getConcreteItemId(resultNode), + -1, + Ci.nsITreeView.DROP_BEFORE, + PlacesUtils.nodeIsTagQuery(elt._placesNode), + elt._placesNode.itemId); + return dropPoint; + } + else if (eventY - eltY < eltHeight * 0.80) { + // If mouse is in the middle of the element, drop inside folder. + dropPoint.ip = new InsertionPoint( + PlacesUtils.getConcreteItemId(elt._placesNode), + -1, + Ci.nsITreeView.DROP_ON, + PlacesUtils.nodeIsTagQuery(elt._placesNode)); + dropPoint.folderElt = elt; + return dropPoint; + } + } + else if (eventY - eltY <= eltHeight / 2) { + // This is a non-folder node or a readonly folder. + // If the mouse is above the middle, drop above this item. + dropPoint.ip = new InsertionPoint( + PlacesUtils.getConcreteItemId(resultNode), + -1, + Ci.nsITreeView.DROP_BEFORE, + PlacesUtils.nodeIsTagQuery(elt._placesNode), + elt._placesNode.itemId); + return dropPoint; + } + + // Drop below the item. + dropPoint.ip = new InsertionPoint( + PlacesUtils.getConcreteItemId(resultNode), + -1, + Ci.nsITreeView.DROP_AFTER, + PlacesUtils.nodeIsTagQuery(elt._placesNode), + elt._placesNode.itemId); + return dropPoint; + ]]></body> + </method> + + <!-- Sub-menus should be opened when the mouse drags over them, and closed + when the mouse drags off. The overFolder object manages opening and + closing of folders when the mouse hovers. --> + <field name="_overFolder"><![CDATA[({ + _self: this, + _folder: {elt: null, + openTimer: null, + hoverTime: 350, + closeTimer: null}, + _closeMenuTimer: null, + + get elt() { + return this._folder.elt; + }, + set elt(val) { + return this._folder.elt = val; + }, + + get openTimer() { + return this._folder.openTimer; + }, + set openTimer(val) { + return this._folder.openTimer = val; + }, + + get hoverTime() { + return this._folder.hoverTime; + }, + set hoverTime(val) { + return this._folder.hoverTime = val; + }, + + get closeTimer() { + return this._folder.closeTimer; + }, + set closeTimer(val) { + return this._folder.closeTimer = val; + }, + + get closeMenuTimer() { + return this._closeMenuTimer; + }, + set closeMenuTimer(val) { + return this._closeMenuTimer = val; + }, + + setTimer: function OF__setTimer(aTime) { + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT); + return timer; + }, + + notify: function OF__notify(aTimer) { + // Function to process all timer notifications. + + if (aTimer == this._folder.openTimer) { + // Timer to open a submenu that's being dragged over. + this._folder.elt.lastChild.setAttribute("autoopened", "true"); + this._folder.elt.lastChild.showPopup(this._folder.elt); + this._folder.openTimer = null; + } + + else if (aTimer == this._folder.closeTimer) { + // Timer to close a submenu that's been dragged off of. + // Only close the submenu if the mouse isn't being dragged over any + // of its child menus. + var draggingOverChild = PlacesControllerDragHelper + .draggingOverChildNode(this._folder.elt); + if (draggingOverChild) + this._folder.elt = null; + this.clear(); + + // Close any parent folders which aren't being dragged over. + // (This is necessary because of the above code that keeps a folder + // open while its children are being dragged over.) + if (!draggingOverChild) + this.closeParentMenus(); + } + + else if (aTimer == this.closeMenuTimer) { + // Timer to close this menu after the drag exit. + var popup = this._self; + // if we are no more dragging we can leave the menu open to allow + // for better D&D bookmark organization + if (PlacesControllerDragHelper.getSession() && + !PlacesControllerDragHelper.draggingOverChildNode(popup.parentNode)) { + popup.hidePopup(); + // Close any parent menus that aren't being dragged over; + // otherwise they'll stay open because they couldn't close + // while this menu was being dragged over. + this.closeParentMenus(); + } + this._closeMenuTimer = null; + } + }, + + // Helper function to close all parent menus of this menu, + // as long as none of the parent's children are currently being + // dragged over. + closeParentMenus: function OF__closeParentMenus() { + var popup = this._self; + var parent = popup.parentNode; + while (parent) { + if (parent.localName == "menupopup" && parent._placesNode) { + if (PlacesControllerDragHelper.draggingOverChildNode(parent.parentNode)) + break; + parent.hidePopup(); + } + parent = parent.parentNode; + } + }, + + // The mouse is no longer dragging over the stored menubutton. + // Close the menubutton, clear out drag styles, and clear all + // timers for opening/closing it. + clear: function OF__clear() { + if (this._folder.elt && this._folder.elt.lastChild) { + if (!this._folder.elt.lastChild.hasAttribute("dragover")) + this._folder.elt.lastChild.hidePopup(); + // remove menuactive style + this._folder.elt.removeAttribute("_moz-menuactive"); + this._folder.elt = null; + } + if (this._folder.openTimer) { + this._folder.openTimer.cancel(); + this._folder.openTimer = null; + } + if (this._folder.closeTimer) { + this._folder.closeTimer.cancel(); + this._folder.closeTimer = null; + } + } + })]]></field> + + <method name="_cleanupDragDetails"> + <body><![CDATA[ + // Called on dragend and drop. + PlacesControllerDragHelper.currentDropTarget = null; + this._rootView._draggedElt = null; + this.removeAttribute("dragover"); + this.removeAttribute("dragstart"); + this._indicatorBar.hidden = true; + ]]></body> + </method> + + </implementation> + + <handlers> + <handler event="DOMMenuItemActive"><![CDATA[ + let elt = event.target; + if (elt.parentNode != this) + return; + + if (window.XULBrowserWindow) { + let elt = event.target; + let placesNode = elt._placesNode; + + var linkURI; + if (placesNode && PlacesUtils.nodeIsURI(placesNode)) + linkURI = placesNode.uri; + else if (elt.hasAttribute("targetURI")) + linkURI = elt.getAttribute("targetURI"); + + if (linkURI) + window.XULBrowserWindow.setOverLink(linkURI, null); + } + ]]></handler> + + <handler event="DOMMenuItemInactive"><![CDATA[ + let elt = event.target; + if (elt.parentNode != this) + return; + + if (window.XULBrowserWindow) + window.XULBrowserWindow.setOverLink("", null); + ]]></handler> + + <handler event="dragstart"><![CDATA[ + if (!event.target._placesNode) + return; + + let draggedElt = event.target._placesNode; + + // Force a copy action if parent node is a query or we are dragging a + // not-removable node. + if (!PlacesControllerDragHelper.canMoveNode(draggedElt)) + event.dataTransfer.effectAllowed = "copyLink"; + + // Activate the view and cache the dragged element. + this._rootView._draggedElt = draggedElt; + this._rootView.controller.setDataTransfer(event); + this.setAttribute("dragstart", "true"); + event.stopPropagation(); + ]]></handler> + + <handler event="drop"><![CDATA[ + PlacesControllerDragHelper.currentDropTarget = event.target; + + let dropPoint = this._getDropPoint(event); + if (dropPoint && dropPoint.ip) { + PlacesControllerDragHelper.onDrop(dropPoint.ip, event.dataTransfer); + event.preventDefault(); + } + + this._cleanupDragDetails(); + event.stopPropagation(); + ]]></handler> + + <handler event="dragover"><![CDATA[ + PlacesControllerDragHelper.currentDropTarget = event.target; + let dt = event.dataTransfer; + + let dropPoint = this._getDropPoint(event); + if (!dropPoint || !dropPoint.ip || + !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) { + this._indicatorBar.hidden = true; + event.stopPropagation(); + return; + } + + // Mark this popup as being dragged over. + this.setAttribute("dragover", "true"); + + if (dropPoint.folderElt) { + // We are dragging over a folder. + // _overFolder should take the care of opening it on a timer. + if (this._overFolder.elt && + this._overFolder.elt != dropPoint.folderElt) { + // We are dragging over a new folder, let's clear old values + this._overFolder.clear(); + } + if (!this._overFolder.elt) { + this._overFolder.elt = dropPoint.folderElt; + // Create the timer to open this folder. + this._overFolder.openTimer = this._overFolder + .setTimer(this._overFolder.hoverTime); + } + // Since we are dropping into a folder set the corresponding style. + dropPoint.folderElt.setAttribute("_moz-menuactive", true); + } + else { + // We are not dragging over a folder. + // Clear out old _overFolder information. + this._overFolder.clear(); + } + + // Autoscroll the popup strip if we drag over the scroll buttons. + let anonid = event.originalTarget.getAttribute('anonid'); + let scrollDir = anonid == "scrollbutton-up" ? -1 : + anonid == "scrollbutton-down" ? 1 : 0; + if (scrollDir != 0) { + this._scrollBox.scrollByIndex(scrollDir, false); + } + + // Check if we should hide the drop indicator for this target. + if (dropPoint.folderElt || this._hideDropIndicator(event)) { + this._indicatorBar.hidden = true; + event.preventDefault(); + event.stopPropagation(); + return; + } + + // We should display the drop indicator relative to the arrowscrollbox. + let sbo = this._scrollBox.scrollBoxObject; + let newMarginTop = 0; + if (scrollDir == 0) { + let elt = this.firstChild; + while (elt && event.screenY > elt.boxObject.screenY + + elt.boxObject.height / 2) + elt = elt.nextSibling; + newMarginTop = elt ? elt.boxObject.screenY - sbo.screenY : + sbo.height; + } + else if (scrollDir == 1) + newMarginTop = sbo.height; + + // Set the new marginTop based on arrowscrollbox. + newMarginTop += sbo.y - this._scrollBox.boxObject.y; + this._indicatorBar.firstChild.style.marginTop = newMarginTop + "px"; + this._indicatorBar.hidden = false; + + event.preventDefault(); + event.stopPropagation(); + ]]></handler> + + <handler event="dragexit"><![CDATA[ + PlacesControllerDragHelper.currentDropTarget = null; + this.removeAttribute("dragover"); + + // If we have not moved to a valid new target clear the drop indicator + // this happens when moving out of the popup. + let target = event.relatedTarget; + if (!target) + this._indicatorBar.hidden = true; + + // Close any folder being hovered over + if (this._overFolder.elt) { + this._overFolder.closeTimer = this._overFolder + .setTimer(this._overFolder.hoverTime); + } + + // The autoopened attribute is set when this folder was automatically + // opened after the user dragged over it. If this attribute is set, + // auto-close the folder on drag exit. + // We should also try to close this popup if the drag has started + // from here, the timer will check if we are dragging over a child. + if (this.hasAttribute("autoopened") || + this.hasAttribute("dragstart")) { + this._overFolder.closeMenuTimer = this._overFolder + .setTimer(this._overFolder.hoverTime); + } + + event.stopPropagation(); + ]]></handler> + + <handler event="dragend"><![CDATA[ + this._cleanupDragDetails(); + ]]></handler> + + </handlers> + </binding> +</bindings> diff --git a/browser/components/places/content/moveBookmarks.js b/browser/components/places/content/moveBookmarks.js new file mode 100644 index 000000000..6b1abd483 --- /dev/null +++ b/browser/components/places/content/moveBookmarks.js @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gMoveBookmarksDialog = { + _nodes: null, + + _foldersTree: null, + get foldersTree() { + if (!this._foldersTree) + this._foldersTree = document.getElementById("foldersTree"); + + return this._foldersTree; + }, + + init: function() { + this._nodes = window.arguments[0]; + + this.foldersTree.place = + "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" + + PlacesUIUtils.allBookmarksFolderId; + }, + + onOK: function(aEvent) { + var selectedNode = this.foldersTree.selectedNode; + NS_ASSERT(selectedNode, + "selectedNode must be set in a single-selection tree with initial selection set"); + var selectedFolderID = PlacesUtils.getConcreteItemId(selectedNode); + + var transactions = []; + for (var i=0; i < this._nodes.length; i++) { + // Nothing to do if the node is already under the selected folder + if (this._nodes[i].parent.itemId == selectedFolderID) + continue; + + let txn = new PlacesMoveItemTransaction(this._nodes[i].itemId, + selectedFolderID, + PlacesUtils.bookmarks.DEFAULT_INDEX); + transactions.push(txn); + } + + if (transactions.length != 0) { + let txn = new PlacesAggregatedTransaction("Move Items", transactions); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + newFolder: function() { + // The command is disabled when the tree is not focused + this.foldersTree.focus(); + goDoCommand("placesCmd_new:folder"); + } +}; diff --git a/browser/components/places/content/moveBookmarks.xul b/browser/components/places/content/moveBookmarks.xul new file mode 100644 index 000000000..b6e75f3da --- /dev/null +++ b/browser/components/places/content/moveBookmarks.xul @@ -0,0 +1,53 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> + +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +<!DOCTYPE window [ + <!ENTITY % moveBookmarksDTD SYSTEM "chrome://browser/locale/places/moveBookmarks.dtd"> + %moveBookmarksDTD; +]> + +<dialog id="moveBookmarkDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + ondialogaccept="return gMoveBookmarksDialog.onOK(event);" + title="&window.title;" + onload="gMoveBookmarksDialog.init();" + style="&window.style;" + screenX="24" + screenY="24" + persist="screenX screenY width height"> + + <script type="application/javascript" + src="chrome://browser/content/places/moveBookmarks.js"/> + + <hbox flex="1"> + <label id="movetolabel" value="&moveTo.label;" control="foldersTree"/> + <hbox flex="1"> + <tree id="foldersTree" + class="placesTree" + flex="1" + type="places" + seltype="single" + hidecolumnpicker="true"> + <treecols> + <treecol id="title" flex="1" primary="true" hideheader="true"/> + </treecols> + <treechildren id="placesListChildren" view="placesList" flex="1"/> + </tree> + <vbox> + <button id="newFolderButton" + label="&newFolderButton.label;" + accesskey="&newFolderButton.accesskey;" + oncommand="gMoveBookmarksDialog.newFolder();"/> + </vbox> + </hbox> + </hbox> +</dialog> diff --git a/browser/components/places/content/organizer.css b/browser/components/places/content/organizer.css new file mode 100644 index 000000000..47b1832c1 --- /dev/null +++ b/browser/components/places/content/organizer.css @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#searchFilter { + width: 23em; +} diff --git a/browser/components/places/content/places.css b/browser/components/places/content/places.css new file mode 100644 index 000000000..5151cca82 --- /dev/null +++ b/browser/components/places/content/places.css @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +tree[type="places"] { + -moz-binding: url("chrome://browser/content/places/tree.xml#places-tree"); +} + +.toolbar-drop-indicator { + position: relative; + z-index: 1; +} + +menupopup[placespopup="true"] { + -moz-binding: url("chrome://browser/content/places/menu.xml#places-popup-base"); +} diff --git a/browser/components/places/content/places.js b/browser/components/places/content/places.js new file mode 100644 index 000000000..a2339adfe --- /dev/null +++ b/browser/components/places/content/places.js @@ -0,0 +1,1532 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", + "resource://gre/modules/BookmarkJSONUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", + "resource://gre/modules/PlacesBackups.jsm"); + +const RESTORE_FILEPICKER_FILTER_EXT = "*.json;*.jsonlz4"; + +var PlacesOrganizer = { + _places: null, + + // IDs of fields from editBookmarkOverlay that should be hidden when infoBox + // is minimal. IDs should be kept in sync with the IDs of the elements + // observing additionalInfoBroadcaster. + _additionalInfoFields: [ + "editBMPanel_descriptionRow", + "editBMPanel_loadInSidebarCheckbox", + "editBMPanel_keywordRow", + ], + + _initFolderTree: function() { + var leftPaneRoot = PlacesUIUtils.leftPaneFolderId; + this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot; + }, + + selectLeftPaneQuery: function(aQueryName) { + var itemId = PlacesUIUtils.leftPaneQueries[aQueryName]; + this._places.selectItems([itemId]); + // Forcefully expand all-bookmarks + if (aQueryName == "AllBookmarks" || aQueryName == "History") + PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; + }, + + init: function() { + ContentArea.init(); + + this._places = document.getElementById("placesList"); + this._initFolderTree(); + + var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks + if (window.arguments && window.arguments[0]) + leftPaneSelection = window.arguments[0]; + + this.selectLeftPaneQuery(leftPaneSelection); + if (leftPaneSelection == "History") { + let historyNode = this._places.selectedNode; + if (historyNode.childCount > 0) + this._places.selectNode(historyNode.getChild(0)); + } + // clear the back-stack + this._backHistory.splice(0, this._backHistory.length); + document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); + + // Set up the search UI. + PlacesSearchBox.init(); + + window.addEventListener("AppCommand", this, true); + + // remove the "Properties" context-menu item, we've our own details pane + document.getElementById("placesContext") + .removeChild(document.getElementById("placesContext_show:info")); + + ContentArea.focus(); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Components.interfaces.nsIDOMEventListener) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + + throw new Components.Exception("", Components.results.NS_NOINTERFACE); + }, + + handleEvent: function(aEvent) { + if (aEvent.type != "AppCommand") + return; + + aEvent.stopPropagation(); + switch (aEvent.command) { + case "Back": + if (this._backHistory.length > 0) + this.back(); + break; + case "Forward": + if (this._forwardHistory.length > 0) + this.forward(); + break; + case "Search": + PlacesSearchBox.findAll(); + break; + } + }, + + destroy: function() { + }, + + _location: null, + get location() { + return this._location; + }, + + set location(aLocation) { + if (!aLocation || this._location == aLocation) + return aLocation; + + if (this.location) { + this._backHistory.unshift(this.location); + this._forwardHistory.splice(0, this._forwardHistory.length); + } + + this._location = aLocation; + this._places.selectPlaceURI(aLocation); + + if (!this._places.hasSelection) { + // If no node was found for the given place: uri, just load it directly + ContentArea.currentPlace = aLocation; + } + this.updateDetailsPane(); + + // update navigation commands + if (this._backHistory.length == 0) + document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); + else + document.getElementById("OrganizerCommand:Back").removeAttribute("disabled"); + if (this._forwardHistory.length == 0) + document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true); + else + document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled"); + + return aLocation; + }, + + _backHistory: [], + _forwardHistory: [], + + back: function() { + this._forwardHistory.unshift(this.location); + var historyEntry = this._backHistory.shift(); + this._location = null; + this.location = historyEntry; + }, + forward: function() { + this._backHistory.unshift(this.location); + var historyEntry = this._forwardHistory.shift(); + this._location = null; + this.location = historyEntry; + }, + + /** + * Called when a place folder is selected in the left pane. + * @param resetSearchBox + * true if the search box should also be reset, false otherwise. + * The search box should be reset when a new folder in the left + * pane is selected; the search scope and text need to be cleared in + * preparation for the new folder. Note that if the user manually + * resets the search box, either by clicking its reset button or by + * deleting its text, this will be false. + */ + _cachedLeftPaneSelectedURI: null, + onPlaceSelected: function(resetSearchBox) { + // Don't change the right-hand pane contents when there's no selection. + if (!this._places.hasSelection) + return; + + var node = this._places.selectedNode; + var queries = PlacesUtils.asQuery(node).getQueries(); + + // Items are only excluded on the left pane. + var options = node.queryOptions.clone(); + options.excludeItems = false; + var placeURI = PlacesUtils.history.queriesToQueryString(queries, + queries.length, + options); + + // If either the place of the content tree in the right pane has changed or + // the user cleared the search box, update the place, hide the search UI, + // and update the back/forward buttons by setting location. + if (ContentArea.currentPlace != placeURI || !resetSearchBox) { + ContentArea.currentPlace = placeURI; + PlacesSearchBox.hideSearchUI(); + this.location = node.uri; + } + + // Update the selected folder title where it appears in the UI: the folder + // scope button, and the search box emptytext. + // They must be updated even if the selection hasn't changed -- + // specifically when node's title changes. In that case a selection event + // is generated, this method is called, but the selection does not change. + var folderButton = document.getElementById("scopeBarFolder"); + var folderTitle = node.title || folderButton.getAttribute("emptytitle"); + folderButton.setAttribute("label", folderTitle); + if (PlacesSearchBox.filterCollection == "collection") + PlacesSearchBox.updateCollectionTitle(folderTitle); + + // When we invalidate a container we use suppressSelectionEvent, when it is + // unset a select event is fired, in many cases the selection did not really + // change, so we should check for it, and return early in such a case. Note + // that we cannot return any earlier than this point, because when + // !resetSearchBox, we need to update location and hide the UI as above, + // even though the selection has not changed. + if (node.uri == this._cachedLeftPaneSelectedURI) + return; + this._cachedLeftPaneSelectedURI = node.uri; + + // At this point, resetSearchBox is true, because the left pane selection + // has changed; otherwise we would have returned earlier. + + PlacesSearchBox.searchFilter.reset(); + this._setSearchScopeForNode(node); + this.updateDetailsPane(); + }, + + /** + * Sets the search scope based on aNode's properties. + * @param aNode + * the node to set up scope from + */ + _setSearchScopeForNode: function(aNode) { + let itemId = aNode.itemId; + + // Set default buttons status. + let bookmarksButton = document.getElementById("scopeBarAll"); + bookmarksButton.hidden = false; + let downloadsButton = document.getElementById("scopeBarDownloads"); + downloadsButton.hidden = true; + + if (PlacesUtils.nodeIsHistoryContainer(aNode) || + itemId == PlacesUIUtils.leftPaneQueries["History"]) { + PlacesQueryBuilder.setScope("history"); + } + else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) { + downloadsButton.hidden = false; + bookmarksButton.hidden = true; + PlacesQueryBuilder.setScope("downloads"); + } + else { + // Default to All Bookmarks for all other nodes, per bug 469437. + PlacesQueryBuilder.setScope("bookmarks"); + } + + // Enable or disable the folder scope button. + let folderButton = document.getElementById("scopeBarFolder"); + folderButton.hidden = !PlacesUtils.nodeIsFolder(aNode) || + itemId == PlacesUIUtils.allBookmarksFolderId; + }, + + /** + * Handle clicks on the places list. + * Single Left click, right click or modified click do not result in any + * special action, since they're related to selection. + * @param aEvent + * The mouse event. + */ + onPlacesListClick: function(aEvent) { + // Only handle clicks on tree children. + if (aEvent.target.localName != "treechildren") + return; + + let node = this._places.selectedNode; + if (node) { + let middleClick = aEvent.button == 1 && aEvent.detail == 1; + if (middleClick && PlacesUtils.nodeIsContainer(node)) { + // The command execution function will take care of seeing if the + // selection is a folder or a different container type, and will + // load its contents in tabs. + PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places); + } + } + }, + + /** + * Handle focus changes on the places list and the current content view. + */ + updateDetailsPane: function() { + if (!ContentArea.currentViewOptions.showDetailsPane) + return; + let view = PlacesUIUtils.getViewForNode(document.activeElement); + if (view) { + let selectedNodes = view.selectedNode ? + [view.selectedNode] : view.selectedNodes; + this._fillDetailsPane(selectedNodes); + } + }, + + openFlatContainer: function(aContainer) { + if (aContainer.itemId != -1) + this._places.selectItems([aContainer.itemId]); + else if (PlacesUtils.nodeIsQuery(aContainer)) + this._places.selectPlaceURI(aContainer.uri); + }, + + /** + * Returns the options associated with the query currently loaded in the + * main places pane. + */ + getCurrentOptions: function() { + return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions; + }, + + /** + * Returns the queries associated with the query currently loaded in the + * main places pane. + */ + getCurrentQueries: function() { + return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries(); + }, + + /** + * Open a file-picker and import the selected file into the bookmarks store + */ + importFromFile: function() { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) { + Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); + BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false) + .then(null, Components.utils.reportError); + } + }; + + fp.init(window, PlacesUIUtils.getString("SelectImport"), + Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterHTML); + fp.open(fpCallback); + }, + + /** + * Allows simple exporting of bookmarks. + */ + exportBookmarks: function() { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult != Ci.nsIFilePicker.returnCancel) { + Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); + BookmarkHTMLUtils.exportToFile(fp.file.path) + .then(null, Components.utils.reportError); + } + }; + + fp.init(window, PlacesUIUtils.getString("EnterExport"), + Ci.nsIFilePicker.modeSave); + fp.appendFilters(Ci.nsIFilePicker.filterHTML); + fp.defaultString = "bookmarks.html"; + fp.open(fpCallback); + }, + + /** + * Populates the restore menu with the dates of the backups available. + */ + populateRestoreMenu: function() { + let restorePopup = document.getElementById("fileRestorePopup"); + + let dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"]. + getService(Ci.nsIScriptableDateFormat); + + // Remove existing menu items. Last item is the restoreFromFile item. + while (restorePopup.childNodes.length > 1) + restorePopup.removeChild(restorePopup.firstChild); + + Task.spawn(function() { + let backupFiles = yield PlacesBackups.getBackupFiles(); + if (backupFiles.length == 0) + return; + + // Populate menu with backups. + for (let i = 0; i < backupFiles.length; i++) { + let fileSize = (yield OS.File.stat(backupFiles[i])).size; + let [size, unit] = DownloadUtils.convertByteUnits(fileSize); + let sizeString = PlacesUtils.getFormattedString("backupFileSizeText", + [size, unit]); + let sizeInfo; + let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]); + if (bookmarkCount != null) { + sizeInfo = " (" + sizeString + " - " + + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", + bookmarkCount, + [bookmarkCount]) + + ")"; + } else { + sizeInfo = " (" + sizeString + ")"; + } + + let backupDate = PlacesBackups.getDateForFile(backupFiles[i]); + let m = restorePopup.insertBefore(document.createElement("menuitem"), + document.getElementById("restoreFromFile")); + m.setAttribute("label", + dateSvc.FormatDate("", + Ci.nsIScriptableDateFormat.dateFormatLong, + backupDate.getFullYear(), + backupDate.getMonth() + 1, + backupDate.getDate()) + + sizeInfo); + m.setAttribute("value", OS.Path.basename(backupFiles[i])); + m.setAttribute("oncommand", + "PlacesOrganizer.onRestoreMenuItemClick(this);"); + } + + // Add the restoreFromFile item. + restorePopup.insertBefore(document.createElement("menuseparator"), + document.getElementById("restoreFromFile")); + }); + }, + + /** + * Called when a menuitem is selected from the restore menu. + */ + onRestoreMenuItemClick: function(aMenuItem) { + Task.spawn(function() { + let backupName = aMenuItem.getAttribute("value"); + let backupFilePaths = yield PlacesBackups.getBackupFiles(); + for (let backupFilePath of backupFilePaths) { + if (OS.Path.basename(backupFilePath) == backupName) { + PlacesOrganizer.restoreBookmarksFromFile(new FileUtils.File(backupFilePath)); + break; + } + } + }); + }, + + /** + * Called when 'Choose File...' is selected from the restore menu. + * Prompts for a file and restores bookmarks to those in the file. + */ + onRestoreBookmarksFromFile: function() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult != Ci.nsIFilePicker.returnCancel) { + this.restoreBookmarksFromFile(fp.file); + } + }.bind(this); + + fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"), + Ci.nsIFilePicker.modeOpen); + fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), + RESTORE_FILEPICKER_FILTER_EXT); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.displayDirectory = backupsDir; + fp.open(fpCallback); + }, + + /** + * Restores bookmarks from a JSON file. + */ + restoreBookmarksFromFile: function(aFile) { + // check file extension + let filePath = aFile.path; + if (!filePath.toLowerCase().endsWith("json") && + !filePath.toLowerCase().endsWith("jsonlz4")) { + this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError")); + return; + } + + // confirm ok to delete existing bookmarks + var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService); + if (!prompts.confirm(null, + PlacesUIUtils.getString("bookmarksRestoreAlertTitle"), + PlacesUIUtils.getString("bookmarksRestoreAlert"))) + return; + + Task.spawn(function() { + try { + yield BookmarkJSONUtils.importFromFile(aFile.path, true); + } catch(ex) { + PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError")); + } + }); + }, + + _showErrorAlert: function(aMsg) { + var brandShortName = document.getElementById("brandStrings"). + getString("brandShortName"); + + Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService). + alert(window, brandShortName, aMsg); + }, + + /** + * Backup bookmarks to desktop, auto-generate a filename with a date. + * The file is a JSON serialization of bookmarks, tags and any annotations + * of those items. + */ + backupBookmarks: function() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult != Ci.nsIFilePicker.returnCancel) { + BookmarkJSONUtils.exportToFile(fp.file.path); + } + }; + + fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"), + Ci.nsIFilePicker.modeSave); + fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), + RESTORE_FILEPICKER_FILTER_EXT); + fp.defaultString = PlacesBackups.getFilenameForDate(); + fp.displayDirectory = backupsDir; + fp.open(fpCallback); + }, + + _paneDisabled: false, + _setDetailsFieldsDisabledState: + function(aDisabled) { + if (aDisabled) { + document.getElementById("paneElementsBroadcaster") + .setAttribute("disabled", "true"); + } + else { + document.getElementById("paneElementsBroadcaster") + .removeAttribute("disabled"); + } + }, + + _detectAndSetDetailsPaneMinimalState: + function(aNode) { + /** + * The details of simple folder-items (as opposed to livemarks) or the + * of livemark-children are not likely to fill the infoBox anyway, + * thus we remove the "More/Less" button and show all details. + * + * the wasminimal attribute here is used to persist the "more/less" + * state in a bookmark->folder->bookmark scenario. + */ + var infoBox = document.getElementById("infoBox"); + var infoBoxExpander = document.getElementById("infoBoxExpander"); + var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper"); + var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster"); + + if (!aNode) { + infoBoxExpanderWrapper.hidden = true; + return; + } + if (aNode.itemId != -1 && + PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) { + if (infoBox.getAttribute("minimal") == "true") + infoBox.setAttribute("wasminimal", "true"); + infoBox.removeAttribute("minimal"); + infoBoxExpanderWrapper.hidden = true; + } + else { + if (infoBox.getAttribute("wasminimal") == "true") + infoBox.setAttribute("minimal", "true"); + infoBox.removeAttribute("wasminimal"); + infoBoxExpanderWrapper.hidden = + this._additionalInfoFields.every(function(id) + document.getElementById(id).collapsed); + } + additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true"; + }, + + // NOT YET USED + updateThumbnailProportions: function() { + var previewBox = document.getElementById("previewBox"); + var canvas = document.getElementById("itemThumbnail"); + var height = previewBox.boxObject.height; + var width = height * (screen.width / screen.height); + canvas.width = width; + canvas.height = height; + }, + + _fillDetailsPane: function(aNodeList) { + var infoBox = document.getElementById("infoBox"); + var detailsDeck = document.getElementById("detailsDeck"); + + // Make sure the infoBox UI is visible if we need to use it, we hide it + // below when we don't. + infoBox.hidden = false; + var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null; + // If a textbox within a panel is focused, force-blur it so its contents + // are saved + if (gEditItemOverlay.itemId != -1) { + var focusedElement = document.commandDispatcher.focusedElement; + if ((focusedElement instanceof HTMLInputElement || + focusedElement instanceof HTMLTextAreaElement) && + /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id)) + focusedElement.blur(); + + // don't update the panel if we are already editing this node unless we're + // in multi-edit mode + if (aSelectedNode) { + var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode); + var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId || + gEditItemOverlay.itemId == concreteId || + (aSelectedNode.itemId == -1 && gEditItemOverlay.uri && + gEditItemOverlay.uri == aSelectedNode.uri); + if (nodeIsSame && detailsDeck.selectedIndex == 1 && + !gEditItemOverlay.multiEdit) + return; + } + } + + // Clean up the panel before initing it again. + gEditItemOverlay.uninitPanel(false); + + if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) { + detailsDeck.selectedIndex = 1; + // Using the concrete itemId is arguably wrong. The bookmarks API + // does allow setting properties for folder shortcuts as well, but since + // the UI does not distinct between the couple, we better just show + // the concrete item properties for shortcuts to root nodes. + var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode); + var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId); + var readOnly = isRootItem || + aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId; + var useConcreteId = isRootItem || + PlacesUtils.nodeIsTagQuery(aSelectedNode); + var itemId = -1; + if (concreteId != -1 && useConcreteId) + itemId = concreteId; + else if (aSelectedNode.itemId != -1) + itemId = aSelectedNode.itemId; + else + itemId = PlacesUtils._uri(aSelectedNode.uri); + + gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"] + , forceReadOnly: readOnly + , titleOverride: aSelectedNode.title + }); + + // Dynamically generated queries, like history date containers, have + // itemId !=0 and do not exist in history. For them the panel is + // read-only, but empty, since it can't get a valid title for the object. + // In such a case we force the title using the selectedNode one, for UI + // polishness. + if (aSelectedNode.itemId == -1 && + (PlacesUtils.nodeIsDay(aSelectedNode) || + PlacesUtils.nodeIsHost(aSelectedNode))) + gEditItemOverlay._element("namePicker").value = aSelectedNode.title; + + this._detectAndSetDetailsPaneMinimalState(aSelectedNode); + } + else if (!aSelectedNode && aNodeList[0]) { + var itemIds = []; + for (var i = 0; i < aNodeList.length; i++) { + if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) && + !PlacesUtils.nodeIsURI(aNodeList[i])) { + detailsDeck.selectedIndex = 0; + var selectItemDesc = document.getElementById("selectItemDescription"); + var itemsCountLabel = document.getElementById("itemsCountText"); + selectItemDesc.hidden = false; + itemsCountLabel.value = + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", + aNodeList.length, [aNodeList.length]); + infoBox.hidden = true; + return; + } + itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId : + PlacesUtils._uri(aNodeList[i].uri); + } + detailsDeck.selectedIndex = 1; + gEditItemOverlay.initPanel(itemIds, + { hiddenRows: ["folderPicker", + "loadInSidebar", + "location", + "keyword", + "description", + "name"]}); + this._detectAndSetDetailsPaneMinimalState(aSelectedNode); + } + else { + detailsDeck.selectedIndex = 0; + infoBox.hidden = true; + let selectItemDesc = document.getElementById("selectItemDescription"); + let itemsCountLabel = document.getElementById("itemsCountText"); + let itemsCount = 0; + if (ContentArea.currentView.result) { + let rootNode = ContentArea.currentView.result.root; + if (rootNode.containerOpen) + itemsCount = rootNode.childCount; + } + if (itemsCount == 0) { + selectItemDesc.hidden = true; + itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems"); + } + else { + selectItemDesc.hidden = false; + itemsCountLabel.value = + PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", + itemsCount, [itemsCount]); + } + } + }, + + // NOT YET USED + _updateThumbnail: function() { + var bo = document.getElementById("previewBox").boxObject; + var width = bo.width; + var height = bo.height; + + var canvas = document.getElementById("itemThumbnail"); + var ctx = canvas.getContext('2d'); + var notAvailableText = canvas.getAttribute("notavailabletext"); + ctx.save(); + ctx.fillStyle = "-moz-Dialog"; + ctx.fillRect(0, 0, width, height); + ctx.translate(width/2, height/2); + + ctx.fillStyle = "GrayText"; + ctx.mozTextStyle = "12pt sans serif"; + var len = ctx.mozMeasureText(notAvailableText); + ctx.translate(-len/2,0); + ctx.mozDrawText(notAvailableText); + ctx.restore(); + }, + + toggleAdditionalInfoFields: function() { + var infoBox = document.getElementById("infoBox"); + var infoBoxExpander = document.getElementById("infoBoxExpander"); + var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel"); + var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster"); + + if (infoBox.getAttribute("minimal") == "true") { + infoBox.removeAttribute("minimal"); + infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel"); + infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey"); + infoBoxExpander.className = "expander-up"; + additionalInfoBroadcaster.removeAttribute("hidden"); + } + else { + infoBox.setAttribute("minimal", "true"); + infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel"); + infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey"); + infoBoxExpander.className = "expander-down"; + additionalInfoBroadcaster.setAttribute("hidden", "true"); + } + }, + + /** + * Save the current search (or advanced query) to the bookmarks root. + */ + saveSearch: function() { + // Get the place: uri for the query. + // If the advanced query builder is showing, use that. + var options = this.getCurrentOptions(); + var queries = this.getCurrentQueries(); + + var placeSpec = PlacesUtils.history.queriesToQueryString(queries, + queries.length, + options); + var placeURI = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newURI(placeSpec, null, null); + + // Prompt the user for a name for the query. + // XXX - using prompt service for now; will need to make + // a real dialog and localize when we're sure this is the UI we want. + var title = PlacesUIUtils.getString("saveSearch.title"); + var inputLabel = PlacesUIUtils.getString("saveSearch.inputLabel"); + var defaultText = PlacesUIUtils.getString("saveSearch.inputDefaultText"); + + var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService); + var check = {value: false}; + var input = {value: defaultText}; + var save = prompts.prompt(null, title, inputLabel, input, null, check); + + // Don't add the query if the user cancels or clears the seach name. + if (!save || input.value == "") + return; + + // Add the place: uri as a bookmark under the bookmarks root. + var txn = new PlacesCreateBookmarkTransaction(placeURI, + PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + input.value); + PlacesUtils.transactionManager.doTransaction(txn); + + // select and load the new query + this._places.selectPlaceURI(placeSpec); + } +}; + +/** + * A set of utilities relating to search within Bookmarks and History. + */ +var PlacesSearchBox = { + + /** + * The Search text field + */ + get searchFilter() { + return document.getElementById("searchFilter"); + }, + + /** + * Folders to include when searching. + */ + _folders: [], + get folders() { + if (this._folders.length == 0) { + this._folders.push(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.unfiledBookmarksFolderId, + PlacesUtils.toolbarFolderId); + } + return this._folders; + }, + set folders(aFolders) { + this._folders = aFolders; + return aFolders; + }, + + /** + * Run a search for the specified text, over the collection specified by + * the dropdown arrow. The default is all bookmarks, but can be + * localized to the active collection. + * @param filterString + * The text to search for. + */ + search: function(filterString) { + var PO = PlacesOrganizer; + // If the user empties the search box manually, reset it and load all + // contents of the current scope. + // XXX this might be to jumpy, maybe should search for "", so results + // are ungrouped, and search box not reset + if (filterString == "") { + PO.onPlaceSelected(false); + return; + } + + let currentView = ContentArea.currentView; + let currentOptions = PO.getCurrentOptions(); + + // Search according to the current scope and folders, which were set by + // PQB_setScope() + switch (PlacesSearchBox.filterCollection) { + case "collection": + currentView.applyFilter(filterString, this.folders); + break; + case "bookmarks": + currentView.applyFilter(filterString, this.folders); + break; + case "history": + if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { + var query = PlacesUtils.history.getNewQuery(); + query.searchTerms = filterString; + var options = currentOptions.clone(); + // Make sure we're getting uri results. + options.resultType = currentOptions.RESULTS_AS_URI; + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + options.includeHidden = true; + currentView.load([query], options); + } + else { + currentView.applyFilter(filterString, null, true); + } + break; + case "downloads": + if (currentView == ContentTree.view) { + let query = PlacesUtils.history.getNewQuery(); + query.searchTerms = filterString; + query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1); + let options = currentOptions.clone(); + // Make sure we're getting uri results. + options.resultType = currentOptions.RESULTS_AS_URI; + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + options.includeHidden = true; + currentView.load([query], options); + } + else { + // The new downloads view doesn't use places for searching downloads. + currentView.searchTerm = filterString; + } + break; + default: + throw new Components.Exception("Invalid filterCollection on search", + Components.results.NS_ERROR_INVALID_ARG); + } + + PlacesSearchBox.showSearchUI(); + + // Update the details panel + PlacesOrganizer.updateDetailsPane(); + }, + + /** + * Finds across all history, downloads or all bookmarks. + */ + findAll: function() { + switch (this.filterCollection) { + case "history": + PlacesQueryBuilder.setScope("history"); + break; + case "downloads": + PlacesQueryBuilder.setScope("downloads"); + break; + default: + PlacesQueryBuilder.setScope("bookmarks"); + break; + } + this.focus(); + }, + + /** + * Updates the display with the title of the current collection. + * @param aTitle + * The title of the current collection. + */ + updateCollectionTitle: function(aTitle) { + let title = ""; + // This is needed when a user performs a folder-specific search + // using the scope bar, removes the search-string, and unfocuses + // the search box, at least until the removal of the scope bar. + if (aTitle) { + title = PlacesUIUtils.getFormattedString("searchCurrentDefault", + [aTitle]); + } + else { + switch (this.filterCollection) { + case "history": + title = PlacesUIUtils.getString("searchHistory"); + break; + case "downloads": + title = PlacesUIUtils.getString("searchDownloads"); + break; + default: + title = PlacesUIUtils.getString("searchBookmarks"); + } + } + this.searchFilter.placeholder = title; + }, + + /** + * Gets/sets the active collection from the dropdown menu. + */ + get filterCollection() { + return this.searchFilter.getAttribute("collection"); + }, + set filterCollection(collectionName) { + if (collectionName == this.filterCollection) + return collectionName; + + this.searchFilter.setAttribute("collection", collectionName); + + var newGrayText = null; + if (collectionName == "collection") { + newGrayText = PlacesOrganizer._places.selectedNode.title || + document.getElementById("scopeBarFolder"). + getAttribute("emptytitle"); + } + this.updateCollectionTitle(newGrayText); + return collectionName; + }, + + /** + * Focus the search box + */ + focus: function() { + this.searchFilter.focus(); + }, + + /** + * Set up the gray text in the search bar as the Places View loads. + */ + init: function() { + this.updateCollectionTitle(); + }, + + /** + * Gets or sets the text shown in the Places Search Box + */ + get value() { + return this.searchFilter.value; + }, + set value(value) { + return this.searchFilter.value = value; + }, + + showSearchUI: function() { + // Hide the advanced search controls when the user hasn't searched + var searchModifiers = document.getElementById("searchModifiers"); + searchModifiers.hidden = false; + }, + + hideSearchUI: function() { + var searchModifiers = document.getElementById("searchModifiers"); + searchModifiers.hidden = true; + } +}; + +/** + * Functions and data for advanced query builder + */ +var PlacesQueryBuilder = { + + queries: [], + queryOptions: null, + + /** + * Called when a scope button in the scope bar is clicked. + * @param aButton + * the scope button that was selected + */ + onScopeSelected: function(aButton) { + switch (aButton.id) { + case "scopeBarHistory": + this.setScope("history"); + break; + case "scopeBarFolder": + this.setScope("collection"); + break; + case "scopeBarDownloads": + this.setScope("downloads"); + break; + case "scopeBarAll": + this.setScope("bookmarks"); + break; + default: + throw new Components.Exception("Invalid search scope button ID", + Components.results.NS_ERROR_INVALID_ARG); + break; + } + }, + + /** + * Sets the search scope. This can be called when no search is active, and + * in that case, when the user does begin a search aScope will be used (see + * PSB_search()). If there is an active search, it's performed again to + * update the content tree. + * @param aScope + * The search scope: "bookmarks", "collection", "downloads" or + * "history". + */ + setScope: function(aScope) { + // Determine filterCollection, folders, and scopeButtonId based on aScope. + var filterCollection; + var folders = []; + var scopeButtonId; + switch (aScope) { + case "history": + filterCollection = "history"; + scopeButtonId = "scopeBarHistory"; + break; + case "collection": + // The folder scope button can only become hidden upon selecting a new + // folder in the left pane, and the disabled state will remain unchanged + // until a new folder is selected. See PO__setScopeForNode(). + if (!document.getElementById("scopeBarFolder").hidden) { + filterCollection = "collection"; + scopeButtonId = "scopeBarFolder"; + folders.push(PlacesUtils.getConcreteItemId( + PlacesOrganizer._places.selectedNode)); + break; + } + // Fall through. If collection scope doesn't make sense for the + // selected node, choose bookmarks scope. + case "bookmarks": + filterCollection = "bookmarks"; + scopeButtonId = "scopeBarAll"; + folders.push(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.toolbarFolderId, + PlacesUtils.unfiledBookmarksFolderId); + break; + case "downloads": + filterCollection = "downloads"; + scopeButtonId = "scopeBarDownloads"; + break; + default: + throw new Components.Exception("Invalid search scope", + Components.results.NS_ERROR_INVALID_ARG); + break; + } + + // Check the appropriate scope button in the scope bar. + document.getElementById(scopeButtonId).checked = true; + + // Update the search box. Re-search if there's an active search. + PlacesSearchBox.filterCollection = filterCollection; + PlacesSearchBox.folders = folders; + var searchStr = PlacesSearchBox.searchFilter.value; + if (searchStr) + PlacesSearchBox.search(searchStr); + } +}; + +/** + * Population and commands for the View Menu. + */ +var ViewMenu = { + /** + * Removes content generated previously from a menupopup. + * @param popup + * The popup that contains the previously generated content. + * @param startID + * The id attribute of an element that is the start of the + * dynamically generated region - remove elements after this + * item only. + * Must be contained by popup. Can be null (in which case the + * contents of popup are removed). + * @param endID + * The id attribute of an element that is the end of the + * dynamically generated region - remove elements up to this + * item only. + * Must be contained by popup. Can be null (in which case all + * items until the end of the popup will be removed). Ignored + * if startID is null. + * @returns The element for the caller to insert new items before, + * null if the caller should just append to the popup. + */ + _clean: function(popup, startID, endID) { + if (endID) + NS_ASSERT(startID, "meaningless to have valid endID and null startID"); + if (startID) { + var startElement = document.getElementById(startID); + NS_ASSERT(startElement.parentNode == + popup, "startElement is not in popup"); + NS_ASSERT(startElement, + "startID does not correspond to an existing element"); + var endElement = null; + if (endID) { + endElement = document.getElementById(endID); + NS_ASSERT(endElement.parentNode == popup, + "endElement is not in popup"); + NS_ASSERT(endElement, + "endID does not correspond to an existing element"); + } + while (startElement.nextSibling != endElement) + popup.removeChild(startElement.nextSibling); + return endElement; + } + else { + while(popup.hasChildNodes()) + popup.removeChild(popup.firstChild); + } + return null; + }, + + /** + * Fills a menupopup with a list of columns + * @param event + * The popupshowing event that invoked this function. + * @param startID + * see _clean + * @param endID + * see _clean + * @param type + * the type of the menuitem, e.g. "radio" or "checkbox". + * Can be null (no-type). + * Checkboxes are checked if the column is visible. + * @param propertyPrefix + * If propertyPrefix is non-null: + * propertyPrefix + column ID + ".label" will be used to get the + * localized label string. + * propertyPrefix + column ID + ".accesskey" will be used to get the + * localized accesskey. + * If propertyPrefix is null, the column label is used as label and + * no accesskey is assigned. + */ + fillWithColumns: function(event, startID, endID, type, propertyPrefix) { + var popup = event.target; + var pivot = this._clean(popup, startID, endID); + + // If no column is "sort-active", the "Unsorted" item needs to be checked, + // so track whether or not we find a column that is sort-active. + var isSorted = false; + var content = document.getElementById("placeContent"); + var columns = content.columns; + for (var i = 0; i < columns.count; ++i) { + var column = columns.getColumnAt(i).element; + if (popup.parentNode && (popup.parentNode.id == "viewSort")) { + switch (column.id) { + case "placesContentParentFolder": + continue; + case "placesContentParentFolderPath": + continue; + } + } + var menuitem = document.createElement("menuitem"); + menuitem.id = "menucol_" + column.id; + menuitem.column = column; + var label = column.getAttribute("label"); + if (propertyPrefix) { + var menuitemPrefix = propertyPrefix; + // for string properties, use "name" as the id, instead of "title" + // see bug #386287 for details + var columnId = column.getAttribute("anonid"); + menuitemPrefix += columnId == "title" ? "name" : columnId; + label = PlacesUIUtils.getString(menuitemPrefix + ".label"); + var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey"); + menuitem.setAttribute("accesskey", accesskey); + } + menuitem.setAttribute("label", label); + if (type == "radio") { + menuitem.setAttribute("type", "radio"); + menuitem.setAttribute("name", "columns"); + // This column is the sort key. Its item is checked. + if (column.getAttribute("sortDirection") != "") { + menuitem.setAttribute("checked", "true"); + isSorted = true; + } + } + else if (type == "checkbox") { + menuitem.setAttribute("type", "checkbox"); + // Cannot uncheck the primary column. + if (column.getAttribute("primary") == "true") + menuitem.setAttribute("disabled", "true"); + // Items for visible columns are checked. + if (!column.hidden) + menuitem.setAttribute("checked", "true"); + } + if (pivot) + popup.insertBefore(menuitem, pivot); + else + popup.appendChild(menuitem); + } + event.stopPropagation(); + }, + + /** + * Set up the content of the view menu. + */ + populateSortMenu: function(event) { + this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy."); + + var sortColumn = this._getSortColumn(); + var viewSortAscending = document.getElementById("viewSortAscending"); + var viewSortDescending = document.getElementById("viewSortDescending"); + // We need to remove an existing checked attribute because the unsorted + // menu item is not rebuilt every time we open the menu like the others. + var viewUnsorted = document.getElementById("viewUnsorted"); + if (!sortColumn) { + viewSortAscending.removeAttribute("checked"); + viewSortDescending.removeAttribute("checked"); + viewUnsorted.setAttribute("checked", "true"); + } + else if (sortColumn.getAttribute("sortDirection") == "ascending") { + viewSortAscending.setAttribute("checked", "true"); + viewSortDescending.removeAttribute("checked"); + viewUnsorted.removeAttribute("checked"); + } + else if (sortColumn.getAttribute("sortDirection") == "descending") { + viewSortDescending.setAttribute("checked", "true"); + viewSortAscending.removeAttribute("checked"); + viewUnsorted.removeAttribute("checked"); + } + }, + + /** + * Shows/Hides a tree column. + * @param element + * The menuitem element for the column + */ + showHideColumn: function(element) { + var column = element.column; + + var splitter = column.nextSibling; + if (splitter && splitter.localName != "splitter") + splitter = null; + + if (element.getAttribute("checked") == "true") { + column.setAttribute("hidden", "false"); + if (splitter) + splitter.removeAttribute("hidden"); + } + else { + column.setAttribute("hidden", "true"); + if (splitter) + splitter.setAttribute("hidden", "true"); + } + }, + + /** + * Gets the last column that was sorted. + * @returns the currently sorted column, null if there is no sorted column. + */ + _getSortColumn: function() { + var content = document.getElementById("placeContent"); + var cols = content.columns; + for (var i = 0; i < cols.count; ++i) { + var column = cols.getColumnAt(i).element; + var sortDirection = column.getAttribute("sortDirection"); + if (sortDirection == "ascending" || sortDirection == "descending") + return column; + } + return null; + }, + + /** + * Sorts the view by the specified column. + * @param aColumn + * The colum that is the sort key. Can be null - the + * current sort column or the title column will be used. + * @param aDirection + * The direction to sort - "ascending" or "descending". + * Can be null - the last direction or descending will be used. + * + * If both aColumnID and aDirection are null, the view will be unsorted. + */ + setSortColumn: function(aColumn, aDirection) { + var result = document.getElementById("placeContent").result; + if (!aColumn && !aDirection) { + result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; + return; + } + + var columnId; + if (aColumn) { + columnId = aColumn.getAttribute("anonid"); + if (!aDirection) { + var sortColumn = this._getSortColumn(); + if (sortColumn) + aDirection = sortColumn.getAttribute("sortDirection"); + } + } + else { + var sortColumn = this._getSortColumn(); + columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title"; + } + + // This maps the possible values of columnId (i.e., anonid's of treecols in + // placeContent) to the default sortingMode and sortingAnnotation values for + // each column. + // key: Sort key in the name of one of the + // nsINavHistoryQueryOptions.SORT_BY_* constants + // dir: Default sort direction to use if none has been specified + // anno: The annotation to sort by, if key is "ANNOTATION" + var colLookupTable = { + title: { key: "TITLE", dir: "ascending" }, + tags: { key: "TAGS", dir: "ascending" }, + url: { key: "URI", dir: "ascending" }, + date: { key: "DATE", dir: "descending" }, + visitCount: { key: "VISITCOUNT", dir: "descending" }, + keyword: { key: "KEYWORD", dir: "ascending" }, + dateAdded: { key: "DATEADDED", dir: "descending" }, + lastModified: { key: "LASTMODIFIED", dir: "descending" }, + description: { key: "ANNOTATION", + dir: "ascending", + anno: PlacesUIUtils.DESCRIPTION_ANNO } + }; + + // Make sure we have a valid column. + if (!colLookupTable.hasOwnProperty(columnId)) + throw new Components.Exception("Invalid column", + Components.results.NS_ERROR_INVALID_ARG); + + // Use a default sort direction if none has been specified. If aDirection + // is invalid, result.sortingMode will be undefined, which has the effect + // of unsorting the tree. + aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase(); + + var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection; + result.sortingAnnotation = colLookupTable[columnId].anno || ""; + result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst]; + } +} + +var ContentArea = { + _specialViews: new Map(), + + init: function() { + this._deck = document.getElementById("placesViewsDeck"); + this._toolbar = document.getElementById("placesToolbar"); + ContentTree.init(); + this._setupView(); + }, + + /** + * Gets the content view to be used for loading the given query. + * If a custom view was set by setContentViewForQueryString, that + * view would be returned, else the default tree view is returned + * + * @param aQueryString + * a query string + * @return the view to be used for loading aQueryString. + */ + getContentViewForQueryString: + function(aQueryString) { + try { + if (this._specialViews.has(aQueryString)) { + let { view, options } = this._specialViews.get(aQueryString); + if (typeof view == "function") { + view = view(); + this._specialViews.set(aQueryString, { view: view, options: options }); + } + return view; + } + } + catch(ex) { + Components.utils.reportError(ex); + } + return ContentTree.view; + }, + + /** + * Sets a custom view to be used rather than the default places tree + * whenever the given query is selected in the left pane. + * @param aQueryString + * a query string + * @param aView + * Either the custom view or a function that will return the view + * the first (and only) time it's called. + * @param [optional] aOptions + * Object defining special options for the view. + * @see ContentTree.viewOptions for supported options and default values. + */ + setContentViewForQueryString: + function(aQueryString, aView, aOptions) { + if (!aQueryString || + typeof aView != "object" && typeof aView != "function") + throw new Components.Exception("Invalid arguments", + Components.results.NS_ERROR_INVALID_ARG); + + this._specialViews.set(aQueryString, { view: aView, + options: aOptions || new Object() }); + }, + + get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel), + set currentView(aNewView) { + let oldView = this.currentView; + if (oldView != aNewView) { + this._deck.selectedPanel = aNewView.associatedElement; + + // If the content area inactivated view was focused, move focus + // to the new view. + if (document.activeElement == oldView.associatedElement) + aNewView.associatedElement.focus(); + } + return aNewView; + }, + + get currentPlace() this.currentView.place, + set currentPlace(aQueryString) { + let oldView = this.currentView; + let newView = this.getContentViewForQueryString(aQueryString); + newView.place = aQueryString; + if (oldView != newView) { + oldView.active = false; + this.currentView = newView; + this._setupView(); + newView.active = true; + } + return aQueryString; + }, + + /** + * Applies view options. + */ + _setupView: function() { + let options = this.currentViewOptions; + + // showDetailsPane. + let detailsDeck = document.getElementById("detailsDeck"); + detailsDeck.hidden = !options.showDetailsPane; + + // toolbarSet. + for (let elt of this._toolbar.childNodes) { + // On Windows and Linux the menu buttons are menus wrapped in a menubar. + if (elt.id == "placesMenu") { + for (let menuElt of elt.childNodes) { + menuElt.hidden = options.toolbarSet.indexOf(menuElt.id) == -1; + } + } + else { + elt.hidden = options.toolbarSet.indexOf(elt.id) == -1; + } + } + }, + + /** + * Options for the current view. + * + * @see ContentTree.viewOptions for supported options and default values. + */ + get currentViewOptions() { + // Use ContentTree options as default. + let viewOptions = ContentTree.viewOptions; + if (this._specialViews.has(this.currentPlace)) { + let { view, options } = this._specialViews.get(this.currentPlace); + for (let option in options) { + viewOptions[option] = options[option]; + } + } + return viewOptions; + }, + + focus: function() { + this._deck.selectedPanel.focus(); + } +}; + +var ContentTree = { + init: function() { + this._view = document.getElementById("placeContent"); + }, + + get view() this._view, + + get viewOptions() Object.seal({ + showDetailsPane: true, + toolbarSet: "back-button, forward-button, organizeButton, viewMenu, maintenanceButton, libraryToolbarSpacer, searchFilter" + }), + + openSelectedNode: function(aEvent) { + let view = this.view; + PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view); + }, + + onClick: function(aEvent) { + let node = this.view.selectedNode; + if (node) { + let doubleClick = aEvent.button == 0 && aEvent.detail == 2; + let middleClick = aEvent.button == 1 && aEvent.detail == 1; + if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) { + // Open associated uri in the browser. + this.openSelectedNode(aEvent); + } + else if (middleClick && PlacesUtils.nodeIsContainer(node)) { + // The command execution function will take care of seeing if the + // selection is a folder or a different container type, and will + // load its contents in tabs. + PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view); + } + } + }, + + onKeyPress: function(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) + this.openSelectedNode(aEvent); + } +}; diff --git a/browser/components/places/content/places.xul b/browser/components/places/content/places.xul new file mode 100644 index 000000000..666937dde --- /dev/null +++ b/browser/components/places/content/places.xul @@ -0,0 +1,424 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> +<?xml-stylesheet href="chrome://browser/content/places/organizer.css"?> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> +<?xml-stylesheet href="chrome://browser/skin/places/organizer.css"?> + +<?xul-overlay href="chrome://browser/content/places/editBookmarkOverlay.xul"?> + +<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +<!DOCTYPE window [ +<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd"> +%placesDTD; +<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd"> +%editMenuOverlayDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> +%browserDTD; +]> + +<window id="places" + title="&places.library.title;" + windowtype="Places:Organizer" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="PlacesOrganizer.init();" + onunload="PlacesOrganizer.destroy();" + width="&places.library.width;" height="&places.library.height;" + screenX="10" screenY="10" + toggletoolbar="true" + persist="width height screenX screenY sizemode"> + + <script type="application/javascript" + src="chrome://browser/content/places/places.js"/> + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://browser/content/places/editBookmarkOverlay.js"/> + + <stringbundleset id="placesStringSet"> + <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/> + </stringbundleset> + + <commandset id="editMenuCommands"/> + <commandset id="placesCommands"/> + <keyset id="placesCommandKeys"/> + + <commandset id="organizerCommandSet"> + <command id="OrganizerCommand_find:all" + oncommand="PlacesSearchBox.findAll();"/> + <command id="OrganizerCommand_export" + oncommand="PlacesOrganizer.exportBookmarks();"/> + <command id="OrganizerCommand_import" + oncommand="PlacesOrganizer.importFromFile();"/> + <command id="OrganizerCommand_backup" + oncommand="PlacesOrganizer.backupBookmarks();"/> + <command id="OrganizerCommand_restoreFromFile" + oncommand="PlacesOrganizer.onRestoreBookmarksFromFile();"/> + <command id="OrganizerCommand_search:save" + oncommand="PlacesOrganizer.saveSearch();"/> + <command id="OrganizerCommand_search:moreCriteria" + oncommand="PlacesQueryBuilder.addRow();"/> + <command id="OrganizerCommand:Back" + oncommand="PlacesOrganizer.back();"/> + <command id="OrganizerCommand:Forward" + oncommand="PlacesOrganizer.forward();"/> + </commandset> + + <keyset id="placesOrganizerKeyset"> + <!-- Instantiation Keys --> + <key id="placesKey_close" key="&cmd.close.key;" modifiers="accel" + oncommand="close();"/> + + <!-- Command Keys --> + <key id="placesKey_find:all" + command="OrganizerCommand_find:all" + key="&cmd.find.key;" + modifiers="accel"/> + + <!-- Back/Forward Keys Support --> + <key id="placesKey_goBackKb" + keycode="VK_LEFT" + command="OrganizerCommand:Back" + modifiers="alt"/> + <key id="placesKey_goForwardKb" + keycode="VK_RIGHT" + command="OrganizerCommand:Forward" + modifiers="alt"/> +#ifdef XP_UNIX + <key id="placesKey_goBackKb2" + key="&goBackCmd.commandKey;" + command="OrganizerCommand:Back" + modifiers="accel"/> + <key id="placesKey_goForwardKb2" + key="&goForwardCmd.commandKey;" + command="OrganizerCommand:Forward" + modifiers="accel"/> +#endif + </keyset> + + <keyset id="editMenuKeys"> + </keyset> + + <popupset id="placesPopupset"> + <menupopup id="placesContext"/> + <menupopup id="placesColumnsContext" + onpopupshowing="ViewMenu.fillWithColumns(event, null, null, 'checkbox', null);" + oncommand="ViewMenu.showHideColumn(event.target); event.stopPropagation();"/> + </popupset> + + <toolbox id="placesToolbox"> + <toolbar class="chromeclass-toolbar" id="placesToolbar" align="center"> + <toolbarbutton id="back-button" + command="OrganizerCommand:Back" + tooltiptext="&backButton.tooltip;" + disabled="true"/> + + <toolbarbutton id="forward-button" + command="OrganizerCommand:Forward" + tooltiptext="&forwardButton.tooltip;" + disabled="true"/> + +#ifdef MOZ_WIDGET_GTK + <menubar id="placesMenu" _moz-menubarkeeplocal="true"> +#else + <menubar id="placesMenu"> +#endif + <menu accesskey="&organize.accesskey;" class="menu-iconic" + id="organizeButton" label="&organize.label;" + tooltiptext="&organize.tooltip;"> + <menupopup id="organizeButtonPopup"> + <menuitem id="newbookmark" + command="placesCmd_new:bookmark" + label="&cmd.new_bookmark.label;" + accesskey="&cmd.new_bookmark.accesskey;"/> + <menuitem id="newfolder" + command="placesCmd_new:folder" + label="&cmd.new_folder.label;" + accesskey="&cmd.new_folder.accesskey;"/> + <menuitem id="newseparator" + command="placesCmd_new:separator" + label="&cmd.new_separator.label;" + accesskey="&cmd.new_separator.accesskey;"/> + + <menuseparator id="orgUndoSeparator"/> + + <menuitem id="orgUndo" + command="cmd_undo" + label="&undoCmd.label;" + key="key_undo" + accesskey="&undoCmd.accesskey;"/> + <menuitem id="orgRedo" + command="cmd_redo" + label="&redoCmd.label;" + key="key_redo" + accesskey="&redoCmd.accesskey;"/> + + <menuseparator id="orgCutSeparator"/> + + <menuitem id="orgCut" + command="cmd_cut" + label="&cutCmd.label;" + key="key_cut" + accesskey="&cutCmd.accesskey;" + selection="separator|link|folder|mixed"/> + <menuitem id="orgCopy" + command="cmd_copy" + label="©Cmd.label;" + key="key_copy" + accesskey="©Cmd.accesskey;" + selection="separator|link|folder|mixed"/> + <menuitem id="orgPaste" + command="cmd_paste" + label="&pasteCmd.label;" + key="key_paste" + accesskey="&pasteCmd.accesskey;" + selection="mutable"/> + <menuitem id="orgDelete" + command="cmd_delete" + label="&deleteCmd.label;" + key="key_delete" + accesskey="&deleteCmd.accesskey;"/> + + <menuseparator id="selectAllSeparator"/> + + <menuitem id="orgSelectAll" + command="cmd_selectAll" + label="&selectAllCmd.label;" + key="key_selectAll" + accesskey="&selectAllCmd.accesskey;"/> + + <menuseparator id="orgMoveSeparator"/> + + <menuitem id="orgMoveBookmarks" + command="placesCmd_moveBookmarks" + label="&cmd.moveBookmarks.label;" + accesskey="&cmd.moveBookmarks.accesskey;"/> + <menuseparator id="orgCloseSeparator"/> + + <menuitem id="orgClose" + key="placesKey_close" + label="&file.close.label;" + accesskey="&file.close.accesskey;" + oncommand="close();"/> + </menupopup> + </menu> + <menu accesskey="&views.accesskey;" class="menu-iconic" + id="viewMenu" label="&views.label;" + tooltiptext="&views.tooltip;"> + <menupopup id="viewMenuPopup"> + + <menu id="viewColumns" + label="&view.columns.label;" accesskey="&view.columns.accesskey;"> + <menupopup onpopupshowing="ViewMenu.fillWithColumns(event, null, null, 'checkbox', null);" + oncommand="ViewMenu.showHideColumn(event.target); event.stopPropagation();"/> + </menu> + + <menu id="viewSort" label="&view.sort.label;" + accesskey="&view.sort.accesskey;"> + <menupopup onpopupshowing="ViewMenu.populateSortMenu(event);" + oncommand="ViewMenu.setSortColumn(event.target.column, null);"> + <menuitem id="viewUnsorted" type="radio" name="columns" + label="&view.unsorted.label;" accesskey="&view.unsorted.accesskey;" + oncommand="ViewMenu.setSortColumn(null, null);"/> + <menuseparator id="directionSeparator"/> + <menuitem id="viewSortAscending" type="radio" name="direction" + label="&view.sortAscending.label;" accesskey="&view.sortAscending.accesskey;" + oncommand="ViewMenu.setSortColumn(null, 'ascending'); event.stopPropagation();"/> + <menuitem id="viewSortDescending" type="radio" name="direction" + label="&view.sortDescending.label;" accesskey="&view.sortDescending.accesskey;" + oncommand="ViewMenu.setSortColumn(null, 'descending'); event.stopPropagation();"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu accesskey="&maintenance.accesskey;" class="menu-iconic" + id="maintenanceButton" label="&maintenance.label;" + tooltiptext="&maintenance.tooltip;"> + <menupopup id="maintenanceButtonPopup"> + <menuitem id="backupBookmarks" + command="OrganizerCommand_backup" + label="&cmd.backup.label;" + accesskey="&cmd.backup.accesskey;"/> + <menu id="fileRestoreMenu" label="&cmd.restore2.label;" + accesskey="&cmd.restore2.accesskey;"> + <menupopup id="fileRestorePopup" onpopupshowing="PlacesOrganizer.populateRestoreMenu();"> + <menuitem id="restoreFromFile" + command="OrganizerCommand_restoreFromFile" + label="&cmd.restoreFromFile.label;" + accesskey="&cmd.restoreFromFile.accesskey;"/> + </menupopup> + </menu> + <menuseparator/> + <menuitem id="fileImport" + command="OrganizerCommand_import" + label="&importBookmarksFromHTML.label;" + accesskey="&importBookmarksFromHTML.accesskey;"/> + <menuitem id="fileExport" + command="OrganizerCommand_export" + label="&exportBookmarksToHTML.label;" + accesskey="&exportBookmarksToHTML.accesskey;"/> + </menupopup> + </menu> + </menubar> + + <spacer id="libraryToolbarSpacer" flex="1"/> + + <textbox id="searchFilter" + clickSelectsAll="true" + type="search" + aria-controls="placeContent" + oncommand="PlacesSearchBox.search(this.value);" + collection="bookmarks"> + </textbox> + </toolbar> + </toolbox> + + <hbox flex="1" id="placesView"> + <tree id="placesList" + class="plain placesTree" + type="places" + hidecolumnpicker="true" context="placesContext" + onselect="PlacesOrganizer.onPlaceSelected(true);" + onclick="PlacesOrganizer.onPlacesListClick(event);" + onfocus="PlacesOrganizer.updateDetailsPane(event);" + seltype="single" + persist="width" + width="200" + minwidth="100" + maxwidth="400"> + <treecols> + <treecol anonid="title" flex="1" primary="true" hideheader="true"/> + </treecols> + <treechildren flex="1"/> + </tree> + <splitter collapse="none" persist="state"></splitter> + <vbox id="contentView" flex="4"> + <toolbox id="searchModifiers" hidden="true"> + <toolbar id="organizerScopeBar" class="chromeclass-toolbar" align="center"> + <label id="scopeBarTitle" value="&search.in.label;"/> + <toolbarbutton id="scopeBarAll" class="small-margin" + type="radio" group="scopeBar" + oncommand="PlacesQueryBuilder.onScopeSelected(this);" + label="&search.scopeBookmarks.label;" + accesskey="&search.scopeBookmarks.accesskey;"/> + <toolbarbutton id="scopeBarHistory" class="small-margin" + type="radio" group="scopeBar" + oncommand="PlacesQueryBuilder.onScopeSelected(this);" + label="&search.scopeHistory.label;" + accesskey="&search.scopeHistory.accesskey;"/> + <toolbarbutton id="scopeBarDownloads" class="small-margin" + type="radio" group="scopeBar" + oncommand="PlacesQueryBuilder.onScopeSelected(this);" + label="&search.scopeDownloads.label;" + accesskey="&search.scopeDownloads.accesskey;"/> + <toolbarbutton id="scopeBarFolder" class="small-margin" + type="radio" group="scopeBar" + oncommand="PlacesQueryBuilder.onScopeSelected(this);" + accesskey="&search.scopeFolder.accesskey;" + emptytitle="&search.scopeFolder.label;" flex="1"/> + <!-- The folder scope button should flex but not take up more room + than its label needs. The only simple way to do that is to + set a really big flex on the spacer, e.g., 2^31 - 1. --> + <spacer flex="2147483647"/> + <button id="saveSearch" class="small-margin" + label="&saveSearch.label;" accesskey="&saveSearch.accesskey;" + command="OrganizerCommand_search:save"/> + </toolbar> + </toolbox> + <deck id="placesViewsDeck" + selectedIndex="0" + flex="1"> + <tree id="placeContent" + class="plain placesTree" + context="placesContext" + hidecolumnpicker="true" + flex="1" + type="places" + flatList="true" + selectfirstnode="true" + enableColumnDrag="true" + onfocus="PlacesOrganizer.updateDetailsPane(event)" + onselect="PlacesOrganizer.updateDetailsPane(event)" + onkeypress="ContentTree.onKeyPress(event);" + onopenflatcontainer="PlacesOrganizer.openFlatContainer(aContainer);"> + <treecols id="placeContentColumns" context="placesColumnsContext"> + <treecol label="&col.name.label;" id="placesContentTitle" anonid="title" flex="5" primary="true" ordinal="1" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.tags.label;" id="placesContentTags" anonid="tags" flex="2" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.url.label;" id="placesContentUrl" anonid="url" flex="5" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.lastvisit.label;" id="placesContentDate" anonid="date" flex="1" hidden="true" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.visitcount.label;" id="placesContentVisitCount" anonid="visitCount" flex="1" hidden="true" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.keyword.label;" id="placesContentKeyword" anonid="keyword" flex="1" hidden="true" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.description.label;" id="placesContentDescription" anonid="description" flex="1" hidden="true" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.dateadded.label;" id="placesContentDateAdded" anonid="dateAdded" flex="1" hidden="true" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.lastmodified.label;" id="placesContentLastModified" anonid="lastModified" flex="1" hidden="true" + persist="width hidden ordinal sortActive sortDirection"/> + <splitter class="tree-splitter"/> + <treecol label="&col.parentfolder.label;" id="placesContentParentFolder" anonid="parentFolder" flex="1" hidden="true" + persist="width hidden ordinal"/> + <splitter class="tree-splitter"/> + <treecol label="&col.parentfolderpath.label;" id="placesContentParentFolderPath" anonid="parentFolderPath" flex="1" hidden="true" + persist="width hidden ordinal"/> + </treecols> + <treechildren flex="1" onclick="ContentTree.onClick(event);"/> + </tree> + </deck> + <deck id="detailsDeck" style="height: 11em;"> + <vbox id="itemsCountBox" align="center"> + <spacer flex="3"/> + <label id="itemsCountText"/> + <spacer flex="1"/> + <description id="selectItemDescription"> + &detailsPane.selectAnItemText.description; + </description> + <spacer flex="3"/> + </vbox> + <vbox id="infoBox" minimal="true"> + <vbox id="editBookmarkPanelContent" flex="1"/> + <hbox id="infoBoxExpanderWrapper" align="center"> + + <button type="image" id="infoBoxExpander" + class="expander-down" + oncommand="PlacesOrganizer.toggleAdditionalInfoFields();" + observes="paneElementsBroadcaster"/> + + <label id="infoBoxExpanderLabel" + lesslabel="&detailsPane.less.label;" + lessaccesskey="&detailsPane.less.accesskey;" + morelabel="&detailsPane.more.label;" + moreaccesskey="&detailsPane.more.accesskey;" + value="&detailsPane.more.label;" + accesskey="&detailsPane.more.accesskey;" + control="infoBoxExpander"/> + + </hbox> + </vbox> + </deck> + </vbox> + </hbox> +</window> diff --git a/browser/components/places/content/placesOverlay.xul b/browser/components/places/content/placesOverlay.xul new file mode 100644 index 000000000..59115a57f --- /dev/null +++ b/browser/components/places/content/placesOverlay.xul @@ -0,0 +1,247 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd"> +%placesDTD; +<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd"> +%editMenuOverlayDTD; +]> + +<overlay id="placesOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript"><![CDATA[ + // TODO: Bug 406371. + // A bunch of browser code depends on us defining these, sad but true :( + var Cc = Components.classes; + var Ci = Components.interfaces; + var Cr = Components.results; + + Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + Components.utils.import("resource:///modules/PlacesUIUtils.jsm"); + ]]></script> + <script type="application/javascript" + src="chrome://browser/content/places/controller.js"/> + <script type="application/javascript" + src="chrome://browser/content/places/treeView.js"/> + + <!-- Bookmarks and history tooltip --> + <tooltip id="bhTooltip" noautohide="true" + onpopupshowing="return window.top.BookmarksEventHandler.fillInBHTooltip(document, event)"> + <vbox id="bhTooltipTextBox" flex="1"> + <label id="bhtTitleText" class="tooltip-label" /> + <label id="bhtUrlText" crop="center" class="tooltip-label" /> + </vbox> + </tooltip> + + <commandset id="placesCommands" + commandupdater="true" + events="focus,sort,places" + oncommandupdate="goUpdatePlacesCommands();"> + <command id="placesCmd_open" + oncommand="goDoPlacesCommand('placesCmd_open');"/> + <command id="placesCmd_open:window" + oncommand="goDoPlacesCommand('placesCmd_open:window');"/> + <command id="placesCmd_open:privatewindow" + oncommand="goDoPlacesCommand('placesCmd_open:privatewindow');"/> + <command id="placesCmd_open:tab" + oncommand="goDoPlacesCommand('placesCmd_open:tab');"/> + + <command id="placesCmd_new:bookmark" + oncommand="goDoPlacesCommand('placesCmd_new:bookmark');"/> + <command id="placesCmd_new:livemark" + oncommand="goDoPlacesCommand('placesCmd_new:livemark');"/> + <command id="placesCmd_new:folder" + oncommand="goDoPlacesCommand('placesCmd_new:folder');"/> + <command id="placesCmd_new:separator" + oncommand="goDoPlacesCommand('placesCmd_new:separator');"/> + <command id="placesCmd_show:info" + oncommand="goDoPlacesCommand('placesCmd_show:info');"/> + <command id="placesCmd_rename" + oncommand="goDoPlacesCommand('placesCmd_show:info');" + observes="placesCmd_show:info"/> + <command id="placesCmd_reload" + oncommand="goDoPlacesCommand('placesCmd_reload');"/> + <command id="placesCmd_sortBy:name" + oncommand="goDoPlacesCommand('placesCmd_sortBy:name');"/> + <command id="placesCmd_moveBookmarks" + oncommand="goDoPlacesCommand('placesCmd_moveBookmarks');"/> + <command id="placesCmd_deleteDataHost" + oncommand="goDoPlacesCommand('placesCmd_deleteDataHost');"/> + <command id="placesCmd_createBookmark" + oncommand="goDoPlacesCommand('placesCmd_createBookmark');"/> + <command id="placesCmd_openParentFolder" + oncommand="goDoPlacesCommand('placesCmd_openParentFolder');"/> + + <!-- Special versions of cut/copy/paste/delete which check for an open context menu. --> + <command id="placesCmd_cut" + oncommand="goDoPlacesCommand('placesCmd_cut');"/> + <command id="placesCmd_copy" + oncommand="goDoPlacesCommand('placesCmd_copy');"/> + <command id="placesCmd_paste" + oncommand="goDoPlacesCommand('placesCmd_paste');"/> + <command id="placesCmd_delete" + oncommand="goDoPlacesCommand('placesCmd_delete');"/> + </commandset> + + <keyset id="placesCommandKeys"> + <key id="key_placesCmd_openParentFolder" + keycode="VK_F1" + command="placesCmd_openParentFolder" + modifiers="accel,shift"/> + </keyset> + + <menupopup id="placesContext" + onpopupshowing="this._view = PlacesUIUtils.getViewForNode(document.popupNode); + return this._view.buildContextMenu(this);" + onpopuphiding="this._view.destroyContextMenu();"> + <menuitem id="placesContext_open" + command="placesCmd_open" + label="&cmd.open.label;" + accesskey="&cmd.open.accesskey;" + default="true" + selectiontype="single" + selection="link"/> + <menuitem id="placesContext_open:newtab" + command="placesCmd_open:tab" + label="&cmd.open_tab.label;" + accesskey="&cmd.open_tab.accesskey;" + selectiontype="single" + selection="link"/> + <menuitem id="placesContext_openContainer:tabs" + oncommand="var view = PlacesUIUtils.getViewForNode(document.popupNode); + view.controller.openSelectionInTabs(event);" + onclick="checkForMiddleClick(this, event);" + label="&cmd.open_all_in_tabs.label;" + accesskey="&cmd.open_all_in_tabs.accesskey;" + selectiontype="single" + selection="folder|host|query"/> + <menuitem id="placesContext_openLinks:tabs" + oncommand="var view = PlacesUIUtils.getViewForNode(document.popupNode); + view.controller.openSelectionInTabs(event);" + onclick="checkForMiddleClick(this, event);" + label="&cmd.open_all_in_tabs.label;" + accesskey="&cmd.open_all_in_tabs.accesskey;" + selectiontype="multiple" + selection="link"/> + <menuitem id="placesContext_open:newwindow" + command="placesCmd_open:window" + label="&cmd.open_window.label;" + accesskey="&cmd.open_window.accesskey;" + selectiontype="single" + selection="link"/> + <menuitem id="placesContext_open:newprivatewindow" + command="placesCmd_open:privatewindow" + label="&cmd.open_private_window.label;" + accesskey="&cmd.open_private_window.accesskey;" + selectiontype="single" + selection="link" + hideifprivatebrowsing="true"/> + <menuseparator id="placesContext_openSeparator"/> + <menuitem id="placesContext_new:bookmark" + command="placesCmd_new:bookmark" + label="&cmd.new_bookmark.label;" + accesskey="&cmd.new_bookmark.accesskey;" + selectiontype="any" + hideifnoinsertionpoint="true"/> + <menuitem id="placesContext_new:folder" + command="placesCmd_new:folder" + label="&cmd.new_folder.label;" + accesskey="&cmd.context_new_folder.accesskey;" + selectiontype="any" + hideifnoinsertionpoint="true"/> + <menuitem id="placesContext_new:separator" + command="placesCmd_new:separator" + label="&cmd.new_separator.label;" + accesskey="&cmd.new_separator.accesskey;" + closemenu="single" + selectiontype="any" + hideifnoinsertionpoint="true"/> + <menuseparator id="placesContext_newSeparator"/> + <menuitem id="placesContext_createBookmark" + command="placesCmd_createBookmark" + label="&cmd.bookmarkLink.label;" + accesskey="&cmd.bookmarkLink.accesskey;" + selection="link" + forcehideselection="bookmark|tagChild"/> + <menuitem id="placesContext_cut" + command="placesCmd_cut" + label="&cutCmd.label;" + accesskey="&cutCmd.accesskey;" + closemenu="single" + selection="bookmark|folder|separator|query" + forcehideselection="tagChild|livemarkChild"/> + <menuitem id="placesContext_copy" + command="placesCmd_copy" + label="©Cmd.label;" + closemenu="single" + accesskey="©Cmd.accesskey;"/> + <menuitem id="placesContext_paste" + command="placesCmd_paste" + label="&pasteCmd.label;" + closemenu="single" + accesskey="&pasteCmd.accesskey;" + selectiontype="any" + hideifnoinsertionpoint="true"/> + <menuseparator id="placesContext_editSeparator"/> + <menuitem id="placesContext_delete" + command="placesCmd_delete" + label="&deleteCmd.label;" + accesskey="&deleteCmd.accesskey;" + closemenu="single" + selection="bookmark|tagChild|folder|query|dynamiccontainer|separator|host"/> + <menuitem id="placesContext_delete_history" + command="placesCmd_delete" + label="&cmd.delete.label;" + accesskey="&cmd.delete.accesskey;" + closemenu="single" + selection="link" + forcehideselection="bookmark|livemarkChild"/> + <menuitem id="placesContext_deleteHost" + command="placesCmd_deleteDataHost" + label="&cmd.deleteDomainData.label;" + accesskey="&cmd.deleteDomainData.accesskey;" + closemenu="single" + selection="link|host" + selectiontype="single" + hideifprivatebrowsing="true" + forcehideselection="bookmark|livemarkChild"/> + <menuseparator id="placesContext_deleteSeparator"/> + <menuitem id="placesContext_reload" + command="placesCmd_reload" + label="&cmd.reloadLivebookmark.label;" + accesskey="&cmd.reloadLivebookmark.accesskey;" + closemenu="single" + selection="livemark/feedURI"/> + <menuitem id="placesContext_sortBy:name" + command="placesCmd_sortBy:name" + label="&cmd.sortby_name.label;" + accesskey="&cmd.context_sortby_name.accesskey;" + closemenu="single" + selection="folder"/> + <menuseparator id="placesContext_sortSeparator"/> + <menuitem id="placesContext_openParentFolder" + command="placesCmd_openParentFolder" + label="&cmd.openParentFolder.label;" + key="key_placesCmd_openParentFolder" + accesskey="&cmd.openParentFolder.accesskey;" + selectiontype="single" + selection="bookmark" + forcehideselection="livemarkChild|livemark/feedURI|PlacesOrganizer/OrganizerQuery"/> + <menuseparator id="placesContext_parentFolderSeparator"/> + <menuitem id="placesContext_show:info" + command="placesCmd_show:info" + label="&cmd.properties.label;" + accesskey="&cmd.properties.accesskey;" + selection="bookmark|folder|query" + forcehideselection="livemarkChild"/> + </menupopup> + +</overlay> diff --git a/browser/components/places/content/sidebarUtils.js b/browser/components/places/content/sidebarUtils.js new file mode 100644 index 000000000..66ea10377 --- /dev/null +++ b/browser/components/places/content/sidebarUtils.js @@ -0,0 +1,104 @@ +// -*- Mode: Java; 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/. + +var SidebarUtils = { + handleTreeClick: function(aTree, aEvent, aGutterSelect) { + // right-clicks are not handled here + if (aEvent.button == 2) + return; + + var tbo = aTree.treeBoxObject; + var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY); + + if (cell.row == -1 || cell.childElt == "twisty") + return; + + var mouseInGutter = false; + if (aGutterSelect) { + var rect = tbo.getCoordsForCellItem(cell.row, cell.col, "image"); + // getCoordsForCellItem returns the x coordinate in logical coordinates + // (i.e., starting from the left and right sides in LTR and RTL modes, + // respectively.) Therefore, we make sure to exclude the blank area + // before the tree item icon (that is, to the left or right of it in + // LTR and RTL modes, respectively) from the click target area. + var isRTL = window.getComputedStyle(aTree, null).direction == "rtl"; + if (isRTL) + mouseInGutter = aEvent.clientX > rect.x; + else + mouseInGutter = aEvent.clientX < rect.x; + } + + var modifKey = aEvent.ctrlKey || aEvent.shiftKey; + + var isContainer = tbo.view.isContainer(cell.row); + var openInTabs = isContainer && + (aEvent.button == 1 || + (aEvent.button == 0 && modifKey)) && + PlacesUtils.hasChildURIs(tbo.view.nodeForTreeIndex(cell.row)); + + if (aEvent.button == 0 && isContainer && !openInTabs) { + tbo.view.toggleOpenState(cell.row); + return; + } + else if (!mouseInGutter && openInTabs && + aEvent.originalTarget.localName == "treechildren") { + tbo.view.selection.select(cell.row); + PlacesUIUtils.openContainerNodeInTabs(aTree.selectedNode, aEvent, aTree); + } + else if (!mouseInGutter && !isContainer && + aEvent.originalTarget.localName == "treechildren") { + // Clear all other selection since we're loading a link now. We must + // do this *before* attempting to load the link since openURL uses + // selection as an indication of which link to load. + tbo.view.selection.select(cell.row); + PlacesUIUtils.openNodeWithEvent(aTree.selectedNode, aEvent, aTree); + } + }, + + handleTreeKeyPress: function(aEvent) { + // XXX Bug 627901: Post Fx4, this method should take a tree parameter. + let tree = aEvent.target; + let node = tree.selectedNode; + if (node) { + if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) + PlacesUIUtils.openNodeWithEvent(node, aEvent, tree); + } + }, + + /** + * The following function displays the URL of a node that is being + * hovered over. + */ + handleTreeMouseMove: function(aEvent) { + if (aEvent.target.localName != "treechildren") + return; + + var tree = aEvent.target.parentNode; + var tbo = tree.treeBoxObject; + var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY); + + // cell.row is -1 when the mouse is hovering an empty area within the tree. + // To avoid showing a URL from a previously hovered node for a currently + // hovered non-url node, we must clear the moused-over URL in these cases. + if (cell.row != -1) { + var node = tree.view.nodeForTreeIndex(cell.row); + if (PlacesUtils.nodeIsURI(node)) + this.setMouseoverURL(node.uri); + else + this.setMouseoverURL(""); + } + else + this.setMouseoverURL(""); + }, + + setMouseoverURL: function(aURL) { + // When the browser window is closed with an open sidebar, the sidebar + // unload event happens after the browser's one. In this case + // top.XULBrowserWindow has been nullified already. + if (top.XULBrowserWindow) { + top.XULBrowserWindow.setOverLink(aURL, null); + } + } +}; diff --git a/browser/components/places/content/tree.xml b/browser/components/places/content/tree.xml new file mode 100644 index 000000000..05b016941 --- /dev/null +++ b/browser/components/places/content/tree.xml @@ -0,0 +1,789 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<bindings id="placesTreeBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="places-tree" extends="chrome://global/content/bindings/tree.xml#tree"> + <implementation> + <constructor><![CDATA[ + // Force an initial build. + if (this.place) + this.place = this.place; + ]]></constructor> + + <destructor><![CDATA[ + // Break the treeviewer->result->treeviewer cycle. + // Note: unsetting the result's viewer also unsets + // the viewer's reference to our treeBoxObject. + var result = this.result; + if (result) { + result.root.containerOpen = false; + } + + // Unregister the controllber before unlinking the view, otherwise it + // may still try to update commands on a view with a null result. + if (this._controller) { + this._controller.terminate(); + this.controllers.removeController(this._controller); + } + + this.view = null; + ]]></destructor> + + <property name="controller" + readonly="true" + onget="return this._controller"/> + + <!-- overriding --> + <property name="view"> + <getter><![CDATA[ + try { + return this.treeBoxObject.view.wrappedJSObject; + } + catch(e) { + return null; + } + ]]></getter> + <setter><![CDATA[ + return this.treeBoxObject.view = val; + ]]></setter> + </property> + + <property name="associatedElement" + readonly="true" + onget="return this"/> + + <method name="applyFilter"> + <parameter name="filterString"/> + <parameter name="folderRestrict"/> + <parameter name="includeHidden"/> + <body><![CDATA[ + // preserve grouping + var queryNode = PlacesUtils.asQuery(this.result.root); + var options = queryNode.queryOptions.clone(); + + // Make sure we're getting uri results. + // We do not yet support searching into grouped queries or into + // tag containers, so we must fall to the default case. + if (PlacesUtils.nodeIsHistoryContainer(queryNode) || + options.resultType == options.RESULTS_AS_TAG_QUERY || + options.resultType == options.RESULTS_AS_TAG_CONTENTS) + options.resultType = options.RESULTS_AS_URI; + + var query = PlacesUtils.history.getNewQuery(); + query.searchTerms = filterString; + + if (folderRestrict) { + query.setFolders(folderRestrict, folderRestrict.length); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + } + + options.includeHidden = !!includeHidden; + + this.load([query], options); + ]]></body> + </method> + + <method name="load"> + <parameter name="queries"/> + <parameter name="options"/> + <body><![CDATA[ + let result = PlacesUtils.history + .executeQueries(queries, queries.length, + options); + let callback; + if (this.flatList) { + let onOpenFlatContainer = this.onOpenFlatContainer; + if (onOpenFlatContainer) + callback = new Function("aContainer", onOpenFlatContainer); + } + + if (!this._controller) { + this._controller = new PlacesController(this); + this.controllers.appendController(this._controller); + } + + let treeView = new PlacesTreeView(this.flatList, callback, this._controller); + + // Observer removal is done within the view itself. When the tree + // goes away, treeboxobject calls view.setTree(null), which then + // calls removeObserver. + result.addObserver(treeView, false); + this.view = treeView; + + if (this.getAttribute("selectfirstnode") == "true" && treeView.rowCount > 0) { + treeView.selection.select(0); + } + + this._cachedInsertionPoint = undefined; + ]]></body> + </method> + + <property name="flatList"> + <getter><![CDATA[ + return this.getAttribute("flatList") == "true"; + ]]></getter> + <setter><![CDATA[ + if (this.flatList != val) { + this.setAttribute("flatList", val); + // reload with the last place set + if (this.place) + this.place = this.place; + } + return val; + ]]></setter> + </property> + + <property name="onOpenFlatContainer"> + <getter><![CDATA[ + return this.getAttribute("onopenflatcontainer"); + ]]></getter> + <setter><![CDATA[ + if (this.onOpenFlatContainer != val) { + this.setAttribute("onopenflatcontainer", val); + // reload with the last place set + if (this.place) + this.place = this.place; + } + return val; + ]]></setter> + </property> + + <!-- + Causes a particular node represented by the specified placeURI to be + selected in the tree. All containers above the node in the hierarchy + will be opened, so that the node is visible. + --> + <method name="selectPlaceURI"> + <parameter name="placeURI"/> + <body><![CDATA[ + // Do nothing if a node matching the given uri is already selected + if (this.hasSelection && this.selectedNode.uri == placeURI) + return; + + function findNode(container, placeURI, nodesURIChecked) { + var containerURI = container.uri; + if (containerURI == placeURI) + return container; + if (nodesURIChecked.indexOf(containerURI) != -1) + return null; + + // never check the contents of the same query + nodesURIChecked.push(containerURI); + + var wasOpen = container.containerOpen; + if (!wasOpen) + container.containerOpen = true; + for (var i = 0; i < container.childCount; ++i) { + var child = container.getChild(i); + var childURI = child.uri; + if (childURI == placeURI) + return child; + else if (PlacesUtils.nodeIsContainer(child)) { + var nested = findNode(PlacesUtils.asContainer(child), placeURI, nodesURIChecked); + if (nested) + return nested; + } + } + + if (!wasOpen) + container.containerOpen = false; + + return null; + } + + var container = this.result.root; + NS_ASSERT(container, "No result, cannot select place URI!"); + if (!container) + return; + + var child = findNode(container, placeURI, []); + if (child) + this.selectNode(child); + else { + // If the specified child could not be located, clear the selection + var selection = this.view.selection; + selection.clearSelection(); + } + ]]></body> + </method> + + <!-- + Causes a particular node to be selected in the tree, resulting in all + containers above the node in the hierarchy to be opened, so that the + node is visible. + --> + <method name="selectNode"> + <parameter name="node"/> + <body><![CDATA[ + var view = this.view; + + var parent = node.parent; + if (parent && !parent.containerOpen) { + // Build a list of all of the nodes that are the parent of this one + // in the result. + var parents = []; + var root = this.result.root; + while (parent && parent != root) { + parents.push(parent); + parent = parent.parent; + } + + // Walk the list backwards (opening from the root of the hierarchy) + // opening each folder as we go. + for (var i = parents.length - 1; i >= 0; --i) { + var index = view.treeIndexForNode(parents[i]); + if (index != Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE && + view.isContainer(index) && !view.isContainerOpen(index)) + view.toggleOpenState(index); + } + // Select the specified node... + } + + var index = view.treeIndexForNode(node); + if (index == Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE) + return; + + view.selection.select(index); + // ... and ensure it's visible, not scrolled off somewhere. + this.treeBoxObject.ensureRowIsVisible(index); + ]]></body> + </method> + + <!-- nsIPlacesView --> + <property name="result"> + <getter><![CDATA[ + try { + return this.view.QueryInterface(Ci.nsINavHistoryResultObserver).result; + } + catch (e) { + return null; + } + ]]></getter> + </property> + + <!-- nsIPlacesView --> + <property name="place"> + <getter><![CDATA[ + return this.getAttribute("place"); + ]]></getter> + <setter><![CDATA[ + this.setAttribute("place", val); + + var queriesRef = { }; + var queryCountRef = { }; + var optionsRef = { }; + PlacesUtils.history.queryStringToQueries(val, queriesRef, queryCountRef, optionsRef); + if (queryCountRef.value == 0) + queriesRef.value = [PlacesUtils.history.getNewQuery()]; + if (!optionsRef.value) + optionsRef.value = PlacesUtils.history.getNewQueryOptions(); + + this.load(queriesRef.value, optionsRef.value); + + return val; + ]]></setter> + </property> + + <!-- nsIPlacesView --> + <property name="hasSelection"> + <getter><![CDATA[ + return this.view && this.view.selection.count >= 1; + ]]></getter> + </property> + + <!-- nsIPlacesView --> + <property name="selectedNodes"> + <getter><![CDATA[ + let nodes = []; + if (!this.hasSelection) + return nodes; + + let selection = this.view.selection; + let rc = selection.getRangeCount(); + let resultview = this.view; + for (let i = 0; i < rc; ++i) { + let min = { }, max = { }; + selection.getRangeAt(i, min, max); + + for (let j = min.value; j <= max.value; ++j) + nodes.push(resultview.nodeForTreeIndex(j)); + } + return nodes; + ]]></getter> + </property> + + <method name="toggleCutNode"> + <parameter name="aNode"/> + <parameter name="aValue"/> + <body><![CDATA[ + this.view.toggleCutNode(aNode, aValue); + ]]></body> + </method> + + <!-- nsIPlacesView --> + <property name="removableSelectionRanges"> + <getter><![CDATA[ + // This property exists in addition to selectedNodes because it + // encodes selection ranges (which only occur in list views) into + // the return value. For each removed range, the index at which items + // will be re-inserted upon the remove transaction being performed is + // the first index of the range, so that the view updates correctly. + // + // For example, if we remove rows 2,3,4 and 7,8 from a list, when we + // undo that operation, if we insert what was at row 3 at row 3 again, + // it will show up _after_ the item that was at row 5. So we need to + // insert all items at row 2, and the tree view will update correctly. + // + // Also, this function collapses the selection to remove redundant + // data, e.g. when deleting this selection: + // + // http://www.foo.com/ + // (-) Some Folder + // http://www.bar.com/ + // + // ... returning http://www.bar.com/ as part of the selection is + // redundant because it is implied by removing "Some Folder". We + // filter out all such redundancies since some partial amount of + // the folder's children may be selected. + // + let nodes = []; + if (!this.hasSelection) + return nodes; + + var selection = this.view.selection; + var rc = selection.getRangeCount(); + var resultview = this.view; + // This list is kept independently of the range selected (i.e. OUTSIDE + // the for loop) since the row index of a container is unique for the + // entire view, and we could have some really wacky selection and we + // don't want to blow up. + var containers = { }; + for (var i = 0; i < rc; ++i) { + var range = []; + var min = { }, max = { }; + selection.getRangeAt(i, min, max); + + for (var j = min.value; j <= max.value; ++j) { + if (this.view.isContainer(j)) + containers[j] = true; + if (!(this.view.getParentIndex(j) in containers)) + range.push(resultview.nodeForTreeIndex(j)); + } + nodes.push(range); + } + return nodes; + ]]></getter> + </property> + + <!-- nsIPlacesView --> + <property name="draggableSelection" + onget="return this.selectedNodes"/> + + <!-- nsIPlacesView --> + <property name="selectedNode"> + <getter><![CDATA[ + var view = this.view; + if (!view || view.selection.count != 1) + return null; + + var selection = view.selection; + var min = { }, max = { }; + selection.getRangeAt(0, min, max); + + return this.view.nodeForTreeIndex(min.value); + ]]></getter> + </property> + + <!-- nsIPlacesView --> + <property name="insertionPoint"> + <getter><![CDATA[ + // invalidated on selection and focus changes + if (this._cachedInsertionPoint !== undefined) + return this._cachedInsertionPoint; + + // there is no insertion point for history queries + // so bail out now and save a lot of work when updating commands + var resultNode = this.result.root; + if (PlacesUtils.nodeIsQuery(resultNode) && + PlacesUtils.asQuery(resultNode).queryOptions.queryType == + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) + return this._cachedInsertionPoint = null; + + var orientation = Ci.nsITreeView.DROP_BEFORE; + // If there is no selection, insert at the end of the container. + if (!this.hasSelection) { + var index = this.view.rowCount - 1; + this._cachedInsertionPoint = + this._getInsertionPoint(index, orientation); + return this._cachedInsertionPoint; + } + + // This is a two-part process. The first part is determining the drop + // orientation. + // * The default orientation is to drop _before_ the selected item. + // * If the selected item is a container, the default orientation + // is to drop _into_ that container. + // + // Warning: It may be tempting to use tree indexes in this code, but + // you must not, since the tree is nested and as your tree + // index may change when folders before you are opened and + // closed. You must convert your tree index to a node, and + // then use getChildIndex to find your absolute index in + // the parent container instead. + // + var resultView = this.view; + var selection = resultView.selection; + var rc = selection.getRangeCount(); + var min = { }, max = { }; + selection.getRangeAt(rc - 1, min, max); + + // If the sole selection is a container, and we are not in + // a flatlist, insert into it. + // Note that this only applies to _single_ selections, + // if the last element within a multi-selection is a + // container, insert _adjacent_ to the selection. + // + // If the sole selection is the bookmarks toolbar folder, we insert + // into it even if it is not opened + var itemId = + PlacesUtils.getConcreteItemId(resultView.nodeForTreeIndex(max.value)); + if (selection.count == 1 && resultView.isContainer(max.value) && + !this.flatList) + orientation = Ci.nsITreeView.DROP_ON; + + this._cachedInsertionPoint = + this._getInsertionPoint(max.value, orientation); + return this._cachedInsertionPoint; + ]]></getter> + </property> + + <method name="_getInsertionPoint"> + <parameter name="index"/> + <parameter name="orientation"/> + <body><![CDATA[ + var result = this.result; + var resultview = this.view; + var container = result.root; + var dropNearItemId = -1; + NS_ASSERT(container, "null container"); + // When there's no selection, assume the container is the container + // the view is populated from (i.e. the result's itemId). + if (index != -1) { + var lastSelected = resultview.nodeForTreeIndex(index); + if (resultview.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) { + // If the last selected item is an open container, append _into_ + // it, rather than insert adjacent to it. + container = lastSelected; + index = -1; + } + else if (lastSelected.containerOpen && + orientation == Ci.nsITreeView.DROP_AFTER && + lastSelected.hasChildren) { + // If the last selected item is an open container and the user is + // trying to drag into it as a first item, really insert into it. + container = lastSelected; + orientation = Ci.nsITreeView.DROP_ON; + index = 0; + } + else { + // Use the last-selected node's container. + container = lastSelected.parent; + + // See comment in the treeView.js's copy of this method + if (!container || !container.containerOpen) + return null; + + // Avoid the potentially expensive call to getChildIndex + // if we know this container doesn't allow insertion + if (PlacesControllerDragHelper.disallowInsertion(container)) + return null; + + var queryOptions = PlacesUtils.asQuery(result.root).queryOptions; + if (queryOptions.sortingMode != + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) { + // If we are within a sorted view, insert at the end + index = -1; + } + else if (queryOptions.excludeItems || + queryOptions.excludeQueries || + queryOptions.excludeReadOnlyFolders) { + // Some item may be invisible, insert near last selected one. + // We don't replace index here to avoid requests to the db, + // instead it will be calculated later by the controller. + index = -1; + dropNearItemId = lastSelected.itemId; + } + else { + var lsi = container.getChildIndex(lastSelected); + index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1; + } + } + } + + if (PlacesControllerDragHelper.disallowInsertion(container)) + return null; + + return new InsertionPoint(PlacesUtils.getConcreteItemId(container), + index, orientation, + PlacesUtils.nodeIsTagQuery(container), + dropNearItemId); + ]]></body> + </method> + + <!-- nsIPlacesView --> + <method name="selectAll"> + <body><![CDATA[ + this.view.selection.selectAll(); + ]]></body> + </method> + + <!-- This method will select the first node in the tree that matches + each given item id. It will open any parent nodes that it needs + to in order to show the selected items. + --> + <method name="selectItems"> + <parameter name="aIDs"/> + <parameter name="aOpenContainers"/> + <body><![CDATA[ + // By default, we do search and select within containers which were + // closed (note that containers in which nodes were not found are + // closed). + if (aOpenContainers === undefined) + aOpenContainers = true; + + var ids = aIDs; // don't manipulate the caller's array + + // Array of nodes found by findNodes which are to be selected + var nodes = []; + + // Array of nodes found by findNodes which should be opened + var nodesToOpen = []; + + // A set of URIs of container-nodes that were previously searched, + // and thus shouldn't be searched again. This is empty at the initial + // start of the recursion and gets filled in as the recursion + // progresses. + var nodesURIChecked = []; + + /** + * Recursively search through a node's children for items + * with the given IDs. When a matching item is found, remove its ID + * from the IDs array, and add the found node to the nodes dictionary. + * + * NOTE: This method will leave open any node that had matching items + * in its subtree. + */ + function findNodes(node) { + var foundOne = false; + // See if node matches an ID we wanted; add to results. + // For simple folder queries, check both itemId and the concrete + // item id. + var index = ids.indexOf(node.itemId); + if (index == -1 && + node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) + index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId); + + if (index != -1) { + nodes.push(node); + foundOne = true; + ids.splice(index, 1); + } + + if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) || + nodesURIChecked.indexOf(node.uri) != -1) + return foundOne; + + PlacesUtils.asContainer(node); + if (!aOpenContainers && !node.containerOpen) + return foundOne; + + nodesURIChecked.push(node.uri); + + // Remember the beginning state so that we can re-close + // this node if we don't find any additional results here. + var previousOpenness = node.containerOpen; + node.containerOpen = true; + for (var child = 0; child < node.childCount && ids.length > 0; + child++) { + var childNode = node.getChild(child); + var found = findNodes(childNode); + if (!foundOne) + foundOne = found; + } + + // If we didn't find any additional matches in this node's + // subtree, revert the node to its previous openness. + if (foundOne) + nodesToOpen.unshift(node); + node.containerOpen = previousOpenness; + return foundOne; + } + + // Disable notifications while looking for nodes. + let result = this.result; + let didSuppressNotifications = result.suppressNotifications; + if (!didSuppressNotifications) + result.suppressNotifications = true + try { + findNodes(this.result.root); + } + finally { + if (!didSuppressNotifications) + result.suppressNotifications = false; + } + + // For all the nodes we've found, highlight the corresponding + // index in the tree. + var resultview = this.view; + var selection = this.view.selection; + selection.selectEventsSuppressed = true; + selection.clearSelection(); + // Open nodes containing found items + for (var i = 0; i < nodesToOpen.length; i++) { + nodesToOpen[i].containerOpen = true; + } + for (var i = 0; i < nodes.length; i++) { + var index = resultview.treeIndexForNode(nodes[i]); + if (index == Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE) + continue; + selection.rangedSelect(index, index, true); + } + selection.selectEventsSuppressed = false; + ]]></body> + </method> + + <field name="_contextMenuShown">false</field> + + <method name="buildContextMenu"> + <parameter name="aPopup"/> + <body><![CDATA[ + this._contextMenuShown = true; + return this.controller.buildContextMenu(aPopup); + ]]></body> + </method> + + <method name="destroyContextMenu"> + <parameter name="aPopup"/> + this._contextMenuShown = false; + <body/> + </method> + + <property name="ownerWindow" + readonly="true" + onget="return window;"/> + + <field name="_active">true</field> + <property name="active" + onget="return this._active" + onset="return this._active = val"/> + + </implementation> + <handlers> + <handler event="focus"><![CDATA[ + this._cachedInsertionPoint = undefined; + + // See select handler. We need the sidebar's places commandset to be + // updated as well + document.commandDispatcher.updateCommands("focus"); + ]]></handler> + <handler event="select"><![CDATA[ + this._cachedInsertionPoint = undefined; + + // This additional complexity is here for the sidebars + var win = window; + while (true) { + win.document.commandDispatcher.updateCommands("focus"); + if (win == window.top) + break; + + win = win.parent; + } + ]]></handler> + + <handler event="dragstart"><![CDATA[ + if (event.target.localName != "treechildren") + return; + + let nodes = this.selectedNodes; + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i]; + + // Disallow dragging the root node of a tree. + if (!node.parent) { + event.preventDefault(); + event.stopPropagation(); + return; + } + + // If this node is child of a readonly container (e.g. a livemark) + // or cannot be moved, we must force a copy. + if (!PlacesControllerDragHelper.canMoveNode(node)) { + event.dataTransfer.effectAllowed = "copyLink"; + break; + } + } + + this._controller.setDataTransfer(event); + event.stopPropagation(); + ]]></handler> + + <handler event="dragover"><![CDATA[ + if (event.target.localName != "treechildren") + return; + + let cell = this.treeBoxObject.getCellAt(event.clientX, event.clientY); + let node = cell.row != -1 ? + this.view.nodeForTreeIndex(cell.row) : + this.result.root; + // cache the dropTarget for the view + PlacesControllerDragHelper.currentDropTarget = node; + + // We have to calculate the orientation since view.canDrop will use + // it and we want to be consistent with the dropfeedback. + let tbo = this.treeBoxObject; + let rowHeight = tbo.rowHeight; + let eventY = event.clientY - tbo.treeBody.boxObject.y - + rowHeight * (cell.row - tbo.getFirstVisibleRow()); + + let orientation = Ci.nsITreeView.DROP_BEFORE; + + if (cell.row == -1) { + // If the row is not valid we try to insert inside the resultNode. + orientation = Ci.nsITreeView.DROP_ON; + } + else if (PlacesUtils.nodeIsContainer(node) && + eventY > rowHeight * 0.75) { + // If we are below the 75% of a container the treeview we try + // to drop after the node. + orientation = Ci.nsITreeView.DROP_AFTER; + } + else if (PlacesUtils.nodeIsContainer(node) && + eventY > rowHeight * 0.25) { + // If we are below the 25% of a container the treeview we try + // to drop inside the node. + orientation = Ci.nsITreeView.DROP_ON; + } + + if (!this.view.canDrop(cell.row, orientation, event.dataTransfer)) + return; + + event.preventDefault(); + event.stopPropagation(); + ]]></handler> + + <handler event="dragend"><![CDATA[ + PlacesControllerDragHelper.currentDropTarget = null; + ]]></handler> + + </handlers> + </binding> + +</bindings> diff --git a/browser/components/places/content/treeView.js b/browser/components/places/content/treeView.js new file mode 100644 index 000000000..db31ceebe --- /dev/null +++ b/browser/components/places/content/treeView.js @@ -0,0 +1,1770 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); + +const PTV_interfaces = [Ci.nsITreeView, + Ci.nsINavHistoryResultObserver, + Ci.nsINavHistoryResultTreeViewer, + Ci.nsISupportsWeakReference]; + +function PlacesTreeView(aFlatList, aOnOpenFlatContainer, aController) { + this._tree = null; + this._result = null; + this._selection = null; + this._rootNode = null; + this._rows = []; + this._flatList = aFlatList; + this._openContainerCallback = aOnOpenFlatContainer; + this._controller = aController; +} + +PlacesTreeView.prototype = { + get wrappedJSObject() this, + + __dateService: null, + get _dateService() { + if (!this.__dateService) { + this.__dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"]. + getService(Ci.nsIScriptableDateFormat); + } + return this.__dateService; + }, + + QueryInterface: XPCOMUtils.generateQI(PTV_interfaces), + + // Bug 761494: + // ---------- + // Some addons use methods from nsINavHistoryResultObserver and + // nsINavHistoryResultTreeViewer, without QIing to these interfaces first. + // That's not a problem when the view is retrieved through the + // <tree>.view getter (which returns the wrappedJSObject of this object), + // it raises an issue when the view retrieved through the treeBoxObject.view + // getter. Thus, to avoid breaking addons, the interfaces are prefetched. + classInfo: XPCOMUtils.generateCI({ interfaces: PTV_interfaces }), + + /** + * This is called once both the result and the tree are set. + */ + _finishInit: function() { + let selection = this.selection; + if (selection) + selection.selectEventsSuppressed = true; + + if (!this._rootNode.containerOpen) { + // This triggers containerStateChanged which then builds the visible + // section. + this._rootNode.containerOpen = true; + } + else + this.invalidateContainer(this._rootNode); + + // "Activate" the sorting column and update commands. + this.sortingChanged(this._result.sortingMode); + + if (selection) + selection.selectEventsSuppressed = false; + }, + + /** + * Plain Container: container result nodes which may never include sub + * hierarchies. + * + * When the rows array is constructed, we don't set the children of plain + * containers. Instead, we keep placeholders for these children. We then + * build these children lazily as the tree asks us for information about each + * row. Luckily, the tree doesn't ask about rows outside the visible area. + * + * @see _getNodeForRow and _getRowForNode for the actual magic. + * + * @note It's guaranteed that all containers are listed in the rows + * elements array. It's also guaranteed that separators (if they're not + * filtered, see below) are listed in the visible elements array, because + * bookmark folders are never built lazily, as described above. + * + * @param aContainer + * A container result node. + * + * @return true if aContainer is a plain container, false otherwise. + */ + _isPlainContainer: function(aContainer) { + // Livemarks are always plain containers. + if (this._controller.hasCachedLivemarkInfo(aContainer)) + return true; + + // We don't know enough about non-query containers. + if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode)) + return false; + + switch (aContainer.queryOptions.resultType) { + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY: + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY: + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY: + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY: + return false; + } + + // If it's a folder, it's not a plain container. + let nodeType = aContainer.type; + return nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER && + nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT; + }, + + /** + * Gets the row number for a given node. Assumes that the given node is + * visible (i.e. it's not an obsolete node). + * + * @param aNode + * A result node. Do not pass an obsolete node, or any + * node which isn't supposed to be in the tree (e.g. separators in + * sorted trees). + * @param [optional] aForceBuild + * @see _isPlainContainer. + * If true, the row will be computed even if the node still isn't set + * in our rows array. + * @param [optional] aParentRow + * The row of aNode's parent. Ignored for the root node. + * @param [optional] aNodeIndex + * The index of aNode in its parent. Only used if aParentRow is + * set too. + * + * @throws if aNode is invisible. + * @note If aParentRow and aNodeIndex are passed and parent is a plain + * container, this method will just return a calculated row value, without + * making assumptions on existence of the node at that position. + * @return aNode's row if it's in the rows list or if aForceBuild is set, -1 + * otherwise. + */ + _getRowForNode: + function(aNode, aForceBuild, aParentRow, aNodeIndex) { + if (aNode == this._rootNode) + throw new Error("The root node is never visible"); + + // A node is removed form the view either if it has no parent or if its + // root-ancestor is not the root node (in which case that's the node + // for which nodeRemoved was called). + // Tycho: let ancestors = [x for (x of PlacesUtils.nodeAncestors(aNode))]; + let ancestors = []; + for (let x of PlacesUtils.nodeAncestors(aNode)) { + ancestors.push(x); + } + + if (ancestors.length == 0 || + ancestors[ancestors.length - 1] != this._rootNode) { + throw new Error("Removed node passed to _getRowForNode"); + } + + // Ensure that the entire chain is open, otherwise that node is invisible. + for (let ancestor of ancestors) { + if (!ancestor.containerOpen) + throw new Error("Invisible node passed to _getRowForNode"); + } + + // Non-plain containers are initially built with their contents. + let parent = aNode.parent; + let parentIsPlain = this._isPlainContainer(parent); + if (!parentIsPlain) { + if (parent == this._rootNode) + return this._rows.indexOf(aNode); + + return this._rows.indexOf(aNode, aParentRow); + } + + let row = -1; + let useNodeIndex = typeof(aNodeIndex) == "number"; + if (parent == this._rootNode) { + if (aNode instanceof Ci.nsINavHistoryResultNode) { + row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode); + } + } else if (useNodeIndex && typeof(aParentRow) == "number") { + // If we have both the row of the parent node, and the node's index, we + // can avoid searching the rows array if the parent is a plain container. + row = aParentRow + aNodeIndex + 1; + } else { + // Look for the node in the nodes array. Start the search at the parent + // row. If the parent row isn't passed, we'll pass undefined to indexOf, + // which is fine. + row = this._rows.indexOf(aNode, aParentRow); + if (row == -1 && aForceBuild) { + let parentRow = typeof(aParentRow) == "number" ? aParentRow + : this._getRowForNode(parent); + row = parentRow + parent.getChildIndex(aNode) + 1; + } + } + + if (row != -1) + this._rows[row] = aNode; + + return row; + }, + + /** + * Given a row, finds and returns the parent details of the associated node. + * + * @param aChildRow + * Row number. + * @return [parentNode, parentRow] + */ + _getParentByChildRow: function(aChildRow) { + let node = this._getNodeForRow(aChildRow); + let parent = (node === null) ? this._rootNode : node.parent; + + // The root node is never visible + if (parent == this._rootNode) + return [this._rootNode, -1]; + + let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1); + return [parent, parentRow]; + }, + + /** + * Gets the node at a given row. + */ + _getNodeForRow: function(aRow) { + if (aRow < 0) { + return null; + } + + let node = this._rows[aRow]; + if (node !== undefined) + return node; + + // Find the nearest node. + let rowNode, row; + for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) { + rowNode = this._rows[i]; + row = i; + } + + // If there's no container prior to the given row, it's a child of + // the root node (remember: all containers are listed in the rows array). + if (!rowNode) + return this._rows[aRow] = this._rootNode.getChild(aRow); + + // Unset elements may exist only in plain containers. Thus, if the nearest + // node is a container, it's the row's parent, otherwise, it's a sibling. + if (rowNode instanceof Ci.nsINavHistoryContainerResultNode) + return this._rows[aRow] = rowNode.getChild(aRow - row - 1); + + let [parent, parentRow] = this._getParentByChildRow(row); + return this._rows[aRow] = parent.getChild(aRow - parentRow - 1); + }, + + /** + * This takes a container and recursively appends our rows array per its + * contents. Assumes that the rows arrays has no rows for the given + * container. + * + * @param [in] aContainer + * A container result node. + * @param [in] aFirstChildRow + * The first row at which nodes may be inserted to the row array. + * In other words, that's aContainer's row + 1. + * @param [out] aToOpen + * An array of containers to open once the build is done. + * + * @return the number of rows which were inserted. + */ + _buildVisibleSection: + function(aContainer, aFirstChildRow, aToOpen) + { + // There's nothing to do if the container is closed. + if (!aContainer.containerOpen) + return 0; + + // Inserting the new elements into the rows array in one shot (by + // Array.concat) is faster than resizing the array (by splice) on each loop + // iteration. + let cc = aContainer.childCount; + let newElements = new Array(cc); + this._rows = this._rows.splice(0, aFirstChildRow) + .concat(newElements, this._rows); + + if (this._isPlainContainer(aContainer)) + return cc; + + const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open"); + const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true"); + let sortingMode = this._result.sortingMode; + + let rowsInserted = 0; + for (let i = 0; i < cc; i++) { + let curChild = aContainer.getChild(i); + let curChildType = curChild.type; + + let row = aFirstChildRow + rowsInserted; + + // Don't display separators when sorted. + if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { + if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) { + // Remove the element for the filtered separator. + // Notice that the rows array was initially resized to include all + // children. + this._rows.splice(row, 1); + continue; + } + } + + this._rows[row] = curChild; + rowsInserted++; + + // Recursively do containers. + if (!this._flatList && + curChild instanceof Ci.nsINavHistoryContainerResultNode && + !this._controller.hasCachedLivemarkInfo(curChild)) { + let resource = this._getResourceForNode(curChild); + let isopen = resource != null && + PlacesUIUtils.localStore.HasAssertion(resource, + openLiteral, + trueLiteral, true); + if (isopen != curChild.containerOpen) + aToOpen.push(curChild); + else if (curChild.containerOpen && curChild.childCount > 0) + rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen); + } + } + + return rowsInserted; + }, + + /** + * This counts how many rows a node takes in the tree. For containers it + * will count the node itself plus any child node following it. + */ + _countVisibleRowsForNodeAtRow: + function(aNodeRow) { + let node = this._rows[aNodeRow]; + + // If it's not listed yet, we know that it's a leaf node (instanceof also + // null-checks). + if (!(node instanceof Ci.nsINavHistoryContainerResultNode)) + return 1; + + let outerLevel = node.indentLevel; + for (let i = aNodeRow + 1; i < this._rows.length; i++) { + let rowNode = this._rows[i]; + if (rowNode && rowNode.indentLevel <= outerLevel) + return i - aNodeRow; + } + + // This node plus its children take up the bottom of the list. + return this._rows.length - aNodeRow; + }, + + _getSelectedNodesInRange: + function(aFirstRow, aLastRow) { + let selection = this.selection; + let rc = selection.getRangeCount(); + if (rc == 0) + return []; + + // The visible-area borders are needed for checking whether a + // selected row is also visible. + let firstVisibleRow = this._tree.getFirstVisibleRow(); + let lastVisibleRow = this._tree.getLastVisibleRow(); + + let nodesInfo = []; + for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) { + let min = { }, max = { }; + selection.getRangeAt(rangeIndex, min, max); + + // If this range does not overlap the replaced chunk, we don't need to + // persist the selection. + if (max.value < aFirstRow || min.value > aLastRow) + continue; + + let firstRow = Math.max(min.value, aFirstRow); + let lastRow = Math.min(max.value, aLastRow); + for (let i = firstRow; i <= lastRow; i++) { + nodesInfo.push({ + node: this._rows[i], + oldRow: i, + wasVisible: i >= firstVisibleRow && i <= lastVisibleRow + }); + } + } + + return nodesInfo; + }, + + /** + * Tries to find an equivalent node for a node which was removed. We first + * look for the original node, in case it was just relocated. Then, if we + * that node was not found, we look for a node that has the same itemId, uri + * and time values. + * + * @param aUpdatedContainer + * An ancestor of the node which was removed. It does not have to be + * its direct parent. + * @param aOldNode + * The node which was removed. + * + * @return the row number of an equivalent node for aOldOne, if one was + * found, -1 otherwise. + */ + _getNewRowForRemovedNode: + function(aUpdatedContainer, aOldNode) { + if (aOldNode == undefined) { + return -1; + } + let parent = aOldNode.parent; + if (parent) { + // If the node's parent is still set, the node is not obsolete + // and we should just find out its new position. + // However, if any of the node's ancestor is closed, the node is + // invisible. + let ancestors = PlacesUtils.nodeAncestors(aOldNode); + for (let ancestor of ancestors) { + if (!ancestor.containerOpen) + return -1; + } + + return this._getRowForNode(aOldNode, true); + } + + // There's a broken edge case here. + // If a visit appears in two queries, and the second one was + // the old node, we'll select the first one after refresh. There's + // nothing we could do about that, because aOldNode.parent is + // gone by the time invalidateContainer is called. + let newNode = aUpdatedContainer.findNodeByDetails(aOldNode.uri, + aOldNode.time, + aOldNode.itemId, + true); + if (!newNode) + return -1; + + return this._getRowForNode(newNode, true); + }, + + /** + * Restores a given selection state as near as possible to the original + * selection state. + * + * @param aNodesInfo + * The persisted selection state as returned by + * _getSelectedNodesInRange. + * @param aUpdatedContainer + * The container which was updated. + */ + _restoreSelection: + function(aNodesInfo, aUpdatedContainer) { + if (aNodesInfo.length == 0) + return; + + let selection = this.selection; + + // Attempt to ensure that previously-visible selection will be visible + // if it's re-selected. However, we can only ensure that for one row. + let scrollToRow = -1; + for (let i = 0; i < aNodesInfo.length; i++) { + let nodeInfo = aNodesInfo[i]; + let row = this._getNewRowForRemovedNode(aUpdatedContainer, + nodeInfo.node); + // Select the found node, if any. + if (row != -1) { + selection.rangedSelect(row, row, true); + if (nodeInfo.wasVisible && scrollToRow == -1) + scrollToRow = row; + } + } + + // If only one node was previously selected and there's no selection now, + // select the node at its old row, if any. + if (aNodesInfo.length == 1 && selection.count == 0) { + let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1); + if (row != -1) { + selection.rangedSelect(row, row, true); + if (aNodesInfo[0].wasVisible && scrollToRow == -1) + scrollToRow = aNodesInfo[0].oldRow; + } + } + + if (scrollToRow != -1) + this._tree.ensureRowIsVisible(scrollToRow); + }, + + _convertPRTimeToString: function(aTime) { + const MS_PER_MINUTE = 60000; + const MS_PER_DAY = 86400000; + let timeMs = aTime / 1000; // PRTime is in microseconds + + // Date is calculated starting from midnight, so the modulo with a day are + // milliseconds from today's midnight. + // getTimezoneOffset corrects that based on local time, notice midnight + // can have a different offset during DST-change days. + let dateObj = new Date(); + let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE; + let midnight = now - (now % MS_PER_DAY); + midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE; + + let dateFormat = timeMs >= midnight ? + Ci.nsIScriptableDateFormat.dateFormatNone : + Ci.nsIScriptableDateFormat.dateFormatShort; + + let timeObj = new Date(timeMs); + return (this._dateService.FormatDateTime("", dateFormat, + Ci.nsIScriptableDateFormat.timeFormatNoSeconds, + timeObj.getFullYear(), timeObj.getMonth() + 1, + timeObj.getDate(), timeObj.getHours(), + timeObj.getMinutes(), timeObj.getSeconds())); + }, + + COLUMN_TYPE_UNKNOWN: 0, + COLUMN_TYPE_TITLE: 1, + COLUMN_TYPE_URI: 2, + COLUMN_TYPE_DATE: 3, + COLUMN_TYPE_VISITCOUNT: 4, + COLUMN_TYPE_KEYWORD: 5, + COLUMN_TYPE_DESCRIPTION: 6, + COLUMN_TYPE_DATEADDED: 7, + COLUMN_TYPE_LASTMODIFIED: 8, + COLUMN_TYPE_TAGS: 9, + COLUMN_TYPE_PARENTFOLDER: 10, + COLUMN_TYPE_PARENTFOLDERPATH: 11, + + _getColumnType: function(aColumn) { + let columnType = aColumn.element.getAttribute("anonid") || aColumn.id; + + switch (columnType) { + case "title": + return this.COLUMN_TYPE_TITLE; + case "url": + return this.COLUMN_TYPE_URI; + case "date": + return this.COLUMN_TYPE_DATE; + case "visitCount": + return this.COLUMN_TYPE_VISITCOUNT; + case "keyword": + return this.COLUMN_TYPE_KEYWORD; + case "description": + return this.COLUMN_TYPE_DESCRIPTION; + case "dateAdded": + return this.COLUMN_TYPE_DATEADDED; + case "lastModified": + return this.COLUMN_TYPE_LASTMODIFIED; + case "tags": + return this.COLUMN_TYPE_TAGS; + case "parentFolder": + return this.COLUMN_TYPE_PARENTFOLDER; + case "parentFolderPath": + return this.COLUMN_TYPE_PARENTFOLDERPATH; + } + return this.COLUMN_TYPE_UNKNOWN; + }, + + _sortTypeToColumnType: function(aSortType) { + switch (aSortType) { + case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING: + return [this.COLUMN_TYPE_TITLE, false]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING: + return [this.COLUMN_TYPE_TITLE, true]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING: + return [this.COLUMN_TYPE_DATE, false]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING: + return [this.COLUMN_TYPE_DATE, true]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING: + return [this.COLUMN_TYPE_URI, false]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING: + return [this.COLUMN_TYPE_URI, true]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING: + return [this.COLUMN_TYPE_VISITCOUNT, false]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING: + return [this.COLUMN_TYPE_VISITCOUNT, true]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_ASCENDING: + return [this.COLUMN_TYPE_KEYWORD, false]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_DESCENDING: + return [this.COLUMN_TYPE_KEYWORD, true]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING: + if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) + return [this.COLUMN_TYPE_DESCRIPTION, false]; + break; + case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING: + if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) + return [this.COLUMN_TYPE_DESCRIPTION, true]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING: + return [this.COLUMN_TYPE_DATEADDED, false]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING: + return [this.COLUMN_TYPE_DATEADDED, true]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING: + return [this.COLUMN_TYPE_LASTMODIFIED, false]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING: + return [this.COLUMN_TYPE_LASTMODIFIED, true]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING: + return [this.COLUMN_TYPE_TAGS, false]; + case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING: + return [this.COLUMN_TYPE_TAGS, true]; + } + return [this.COLUMN_TYPE_UNKNOWN, false]; + }, + + // nsINavHistoryResultObserver + nodeInserted: function(aParentNode, aNode, aNewIndex) { + NS_ASSERT(this._result, "Got a notification but have no result!"); + if (!this._tree || !this._result) + return; + + // Bail out for hidden separators. + if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) + return; + + let parentRow; + if (aParentNode != this._rootNode) { + parentRow = this._getRowForNode(aParentNode); + + // Update parent when inserting the first item, since twisty has changed. + if (aParentNode.childCount == 1) + this._tree.invalidateRow(parentRow); + } + + // Compute the new row number of the node. + let row = -1; + let cc = aParentNode.childCount; + if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) { + // We don't need to worry about sub hierarchies of the parent node + // if it's a plain container, or if the new node is its first child. + if (aParentNode == this._rootNode) + row = aNewIndex; + else + row = parentRow + aNewIndex + 1; + } + else { + // Here, we try to find the next visible element in the child list so we + // can set the new visible index to be right before that. Note that we + // have to search down instead of up, because some siblings could have + // children themselves that would be in the way. + let separatorsAreHidden = PlacesUtils.nodeIsSeparator(aNode) && + this.isSorted(); + for (let i = aNewIndex + 1; i < cc; i++) { + let node = aParentNode.getChild(i); + if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) { + // The children have not been shifted so the next item will have what + // should be our index. + row = this._getRowForNode(node, false, parentRow, i); + break; + } + } + if (row < 0) { + // At the end of the child list without finding a visible sibling. This + // is a little harder because we don't know how many rows the last item + // in our list takes up (it could be a container with many children). + let prevChild = aParentNode.getChild(aNewIndex - 1); + let prevIndex = this._getRowForNode(prevChild, false, parentRow, + aNewIndex - 1); + row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex); + } + } + + this._rows.splice(row, 0, aNode); + this._tree.rowCountChanged(row, 1); + + if (PlacesUtils.nodeIsContainer(aNode) && + PlacesUtils.asContainer(aNode).containerOpen) { + this.invalidateContainer(aNode); + } + }, + + /** + * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being + * removed but the node it is collapsed with is not being removed (this then + * just swap out the removee with its collapsing partner). The only time + * when we really remove things is when deleting URIs, which will apply to + * all collapsees. This function is called sometimes when resorting items. + * However, we won't do this when sorted by date because dates will never + * change for visits, and date sorting is the only time things are collapsed. + */ + nodeRemoved: function(aParentNode, aNode, aOldIndex) { + NS_ASSERT(this._result, "Got a notification but have no result!"); + if (!this._tree || !this._result) + return; + + // XXX bug 517701: We don't know what to do when the root node is removed. + if (aNode == this._rootNode) + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + + // Bail out for hidden separators. + if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) + return; + + let parentRow = aParentNode == this._rootNode ? + undefined : this._getRowForNode(aParentNode, true); + let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex); + if (oldRow < 0) + throw Cr.NS_ERROR_UNEXPECTED; + + // If the node was exclusively selected, the node next to it will be + // selected. + let selectNext = false; + let selection = this.selection; + if (selection.getRangeCount() == 1) { + let min = { }, max = { }; + selection.getRangeAt(0, min, max); + if (min.value == max.value && + this.nodeForTreeIndex(min.value) == aNode) + selectNext = true; + } + + // Remove the node and its children, if any. + let count = this._countVisibleRowsForNodeAtRow(oldRow); + this._rows.splice(oldRow, count); + this._tree.rowCountChanged(oldRow, -count); + + // Redraw the parent if its twisty state has changed. + if (aParentNode != this._rootNode && !aParentNode.hasChildren) { + let parentRow = oldRow - 1; + this._tree.invalidateRow(parentRow); + } + + // Restore selection if the node was exclusively selected. + if (!selectNext) + return; + + // Restore selection. + let rowToSelect = Math.min(oldRow, this._rows.length - 1); + if (rowToSelect != -1) + this.selection.rangedSelect(rowToSelect, rowToSelect, true); + }, + + nodeMoved: + function(aNode, aOldParent, aOldIndex, aNewParent, aNewIndex) { + NS_ASSERT(this._result, "Got a notification but have no result!"); + if (!this._tree || !this._result) + return; + + // Bail out for hidden separators. + if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) + return; + + // Note that at this point the node has already been moved by the backend, + // so we must give hints to _getRowForNode to get the old row position. + let oldParentRow = aOldParent == this._rootNode ? + undefined : this._getRowForNode(aOldParent, true); + let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex); + if (oldRow < 0) + throw Cr.NS_ERROR_UNEXPECTED; + + // If this node is a container it could take up more than one row. + let count = this._countVisibleRowsForNodeAtRow(oldRow); + + // Persist selection state. + let nodesToReselect = + this._getSelectedNodesInRange(oldRow, oldRow + count); + if (nodesToReselect.length > 0) + this.selection.selectEventsSuppressed = true; + + // Redraw the parent if its twisty state has changed. + if (aOldParent != this._rootNode && !aOldParent.hasChildren) { + let parentRow = oldRow - 1; + this._tree.invalidateRow(parentRow); + } + + // Remove node and its children, if any, from the old position. + this._rows.splice(oldRow, count); + this._tree.rowCountChanged(oldRow, -count); + + // Insert the node into the new position. + this.nodeInserted(aNewParent, aNode, aNewIndex); + + // Restore selection. + if (nodesToReselect.length > 0) { + this._restoreSelection(nodesToReselect, aNewParent); + this.selection.selectEventsSuppressed = false; + } + }, + + _invalidateCellValue: function(aNode, + aColumnType) { + NS_ASSERT(this._result, "Got a notification but have no result!"); + if (!this._tree || !this._result) + return; + + // Nothing to do for the root node. + if (aNode == this._rootNode) + return; + + let row = this._getRowForNode(aNode); + if (row == -1) + return; + + let column = this._findColumnByType(aColumnType); + if (column && !column.element.hidden) + this._tree.invalidateCell(row, column); + + // Last modified time is altered for almost all node changes. + if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) { + let lastModifiedColumn = + this._findColumnByType(this.COLUMN_TYPE_LASTMODIFIED); + if (lastModifiedColumn && !lastModifiedColumn.hidden) + this._tree.invalidateCell(row, lastModifiedColumn); + } + }, + + _populateLivemarkContainer: function(aNode) { + PlacesUtils.livemarks.getLivemark({ id: aNode.itemId }) + .then(aLivemark => { + let placesNode = aNode; + // Need to check containerOpen since getLivemark is async. + if (!placesNode.containerOpen) + return; + + let children = aLivemark.getNodesForContainer(placesNode); + for (let i = 0; i < children.length; i++) { + let child = children[i]; + this.nodeInserted(placesNode, child, i); + } + }, Components.utils.reportError); + }, + + nodeTitleChanged: function(aNode, aNewTitle) { + this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); + }, + + nodeURIChanged: function(aNode, aNewURI) { + this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI); + }, + + nodeIconChanged: function(aNode) { + this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); + }, + + nodeHistoryDetailsChanged: + function(aNode, aUpdatedVisitDate, + aUpdatedVisitCount) { + if (aNode.parent && this._controller.hasCachedLivemarkInfo(aNode.parent)) { + // Find the node in the parent. + let parentRow = this._flatList ? 0 : this._getRowForNode(aNode.parent); + for (let i = parentRow; i < this._rows.length; i++) { + let child = this.nodeForTreeIndex(i); + if (child.uri == aNode.uri) { + this._cellProperties.delete(child); + this._invalidateCellValue(child, this.COLUMN_TYPE_TITLE); + break; + } + } + return; + } + + this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE); + this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT); + }, + + nodeTagsChanged: function(aNode) { + this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS); + }, + + nodeKeywordChanged: function(aNode, aNewKeyword) { + this._invalidateCellValue(aNode, this.COLUMN_TYPE_KEYWORD); + }, + + nodeAnnotationChanged: function(aNode, aAnno) { + if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) { + this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION); + } + else if (aAnno == PlacesUtils.LMANNO_FEEDURI) { + PlacesUtils.livemarks.getLivemark({ id: aNode.itemId }) + .then(aLivemark => { + this._controller.cacheLivemarkInfo(aNode, aLivemark); + let properties = this._cellProperties.get(aNode); + this._cellProperties.set(aNode, properties += " livemark"); + // The livemark attribute is set as a cell property on the title cell. + this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); + }, Components.utils.reportError); + } + }, + + nodeDateAddedChanged: function(aNode, aNewValue) { + this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED); + }, + + nodeLastModifiedChanged: + function(aNode, aNewValue) { + this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED); + }, + + containerStateChanged: + function(aNode, aOldState, aNewState) { + this.invalidateContainer(aNode); + + if (PlacesUtils.nodeIsFolder(aNode) || + (this._flatList && aNode == this._rootNode)) { + let queryOptions = PlacesUtils.asQuery(this._rootNode).queryOptions; + if (queryOptions.excludeItems) { + return; + } + if (aNode.itemId != -1) { // run when there's a valid node id + PlacesUtils.livemarks.getLivemark({ id: aNode.itemId }) + .then(aLivemark => { + let shouldInvalidate = + !this._controller.hasCachedLivemarkInfo(aNode); + this._controller.cacheLivemarkInfo(aNode, aLivemark); + if (aNewState == Components.interfaces.nsINavHistoryContainerResultNode.STATE_OPENED) { + aLivemark.registerForUpdates(aNode, this); + // Prioritize the current livemark. + aLivemark.reload(); + PlacesUtils.livemarks.reloadLivemarks(); + if (shouldInvalidate) + this.invalidateContainer(aNode); + } + else { + aLivemark.unregisterForUpdates(aNode); + } + }, () => undefined); + } + } + }, + + invalidateContainer: function(aContainer) { + NS_ASSERT(this._result, "Need to have a result to update"); + if (!this._tree) + return; + + let startReplacement, replaceCount; + if (aContainer == this._rootNode) { + startReplacement = 0; + replaceCount = this._rows.length; + + // If the root node is now closed, the tree is empty. + if (!this._rootNode.containerOpen) { + this._rows = []; + if (replaceCount) + this._tree.rowCountChanged(startReplacement, -replaceCount); + + return; + } + } + else { + // Update the twisty state. + let row = this._getRowForNode(aContainer); + this._tree.invalidateRow(row); + + // We don't replace the container node itself, so we should decrease the + // replaceCount by 1. + startReplacement = row + 1; + replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1; + } + + // Persist selection state. + let nodesToReselect = + this._getSelectedNodesInRange(startReplacement, + startReplacement + replaceCount); + + // Now update the number of elements. + this.selection.selectEventsSuppressed = true; + + // First remove the old elements + this._rows.splice(startReplacement, replaceCount); + + // If the container is now closed, we're done. + if (!aContainer.containerOpen) { + let oldSelectionCount = this.selection.count; + if (replaceCount) + this._tree.rowCountChanged(startReplacement, -replaceCount); + + // Select the row next to the closed container if any of its + // children were selected, and nothing else is selected. + if (nodesToReselect.length > 0 && + nodesToReselect.length == oldSelectionCount) { + this.selection.rangedSelect(startReplacement, startReplacement, true); + this._tree.ensureRowIsVisible(startReplacement); + } + + this.selection.selectEventsSuppressed = false; + return; + } + + // Otherwise, start a batch first. + this._tree.beginUpdateBatch(); + if (replaceCount) + this._tree.rowCountChanged(startReplacement, -replaceCount); + + let toOpenElements = []; + let elementsAddedCount = this._buildVisibleSection(aContainer, + startReplacement, + toOpenElements); + if (elementsAddedCount) + this._tree.rowCountChanged(startReplacement, elementsAddedCount); + + if (!this._flatList) { + // Now, open any containers that were persisted. + for (let i = 0; i < toOpenElements.length; i++) { + let item = toOpenElements[i]; + let parent = item.parent; + + // Avoid recursively opening containers. + while (parent) { + if (parent.uri == item.uri) + break; + parent = parent.parent; + } + + // If we don't have a parent, we made it all the way to the root + // and didn't find a match, so we can open our item. + if (!parent && !item.containerOpen) + item.containerOpen = true; + } + } + + if (this._controller.hasCachedLivemarkInfo(aContainer)) { + let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; + if (!queryOptions.excludeItems) { + this._populateLivemarkContainer(aContainer); + } + } + + this._tree.endUpdateBatch(); + + // Restore selection. + this._restoreSelection(nodesToReselect, aContainer); + this.selection.selectEventsSuppressed = false; + }, + + _columns: [], + _findColumnByType: function(aColumnType) { + if (this._columns[aColumnType]) + return this._columns[aColumnType]; + + let columns = this._tree.columns; + let colCount = columns.count; + for (let i = 0; i < colCount; i++) { + let column = columns.getColumnAt(i); + let columnType = this._getColumnType(column); + this._columns[columnType] = column; + if (columnType == aColumnType) + return column; + } + + // That's completely valid. Most of our trees actually include just the + // title column. + return null; + }, + + sortingChanged: function(aSortingMode) { + if (!this._tree || !this._result) + return; + + // Depending on the sort mode, certain commands may be disabled. + window.updateCommands("sort"); + + let columns = this._tree.columns; + + // Clear old sorting indicator. + let sortedColumn = columns.getSortedColumn(); + if (sortedColumn) + sortedColumn.element.removeAttribute("sortDirection"); + + // Set new sorting indicator by looking through all columns for ours. + if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) + return; + + let [desiredColumn, desiredIsDescending] = + this._sortTypeToColumnType(aSortingMode); + let colCount = columns.count; + let column = this._findColumnByType(desiredColumn); + if (column) { + let sortDir = desiredIsDescending ? "descending" : "ascending"; + column.element.setAttribute("sortDirection", sortDir); + } + }, + + _inBatchMode: false, + batching: function(aToggleMode) { + if (this._inBatchMode != aToggleMode) { + this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode; + if (this._inBatchMode) { + this._tree.beginUpdateBatch(); + } + else { + this._tree.endUpdateBatch(); + } + } + }, + + get result() this._result, + set result(val) { + if (this._result) { + this._result.removeObserver(this); + this._rootNode.containerOpen = false; + } + + if (val) { + this._result = val; + this._rootNode = this._result.root; + this._cellProperties = new Map(); + this._cuttingNodes = new Set(); + } + else if (this._result) { + delete this._result; + delete this._rootNode; + delete this._cellProperties; + delete this._cuttingNodes; + } + + // If the tree is not set yet, setTree will call finishInit. + if (this._tree && val) + this._finishInit(); + + return val; + }, + + nodeForTreeIndex: function(aIndex) { + if (aIndex > this._rows.length) + throw Cr.NS_ERROR_INVALID_ARG; + + return this._getNodeForRow(aIndex); + }, + + treeIndexForNode: function(aNode) { + // The API allows passing invisible nodes. + try { + return this._getRowForNode(aNode, true); + } + catch(ex) { } + + return Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE; + }, + + _getResourceForNode: function(aNode) + { + let uri = aNode.uri; + NS_ASSERT(uri, "if there is no uri, we can't persist the open state"); + return uri ? PlacesUIUtils.RDF.GetResource(uri) : null; + }, + + // nsITreeView + get rowCount() this._rows.length, + get selection() this._selection, + set selection(val) this._selection = val, + + getRowProperties: function() { return ""; }, + + getCellProperties: + function(aRow, aColumn) { + // for anonid-trees, we need to add the column-type manually + var props = ""; + let columnType = aColumn.element.getAttribute("anonid"); + if (columnType) + props += columnType; + else + columnType = aColumn.id; + + // Set the "ltr" property on url cells + if (columnType == "url") + props += " ltr"; + + if (columnType != "title") + return props; + + let node = this._getNodeForRow(aRow); + + if (this._cuttingNodes.has(node)) { + props += " cutting"; + } + + let properties = this._cellProperties.get(node); + if (properties === undefined) { + properties = ""; + let itemId = node.itemId; + let nodeType = node.type; + if (PlacesUtils.containerTypes.indexOf(nodeType) != -1) { + if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) { + properties += " query"; + if (PlacesUtils.nodeIsTagQuery(node)) + properties += " tagContainer"; + else if (PlacesUtils.nodeIsDay(node)) + properties += " dayContainer"; + else if (PlacesUtils.nodeIsHost(node)) + properties += " hostContainer"; + } + else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER || + nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) { + if (this._controller.hasCachedLivemarkInfo(node)) { + properties += " livemark"; + } + else { + PlacesUtils.livemarks.getLivemark({ id: node.itemId }) + .then(aLivemark => { + this._controller.cacheLivemarkInfo(node, aLivemark); + let props = this._cellProperties.get(node); + this._cellProperties.set(node, props += " livemark"); + // The livemark attribute is set as a cell property on the title cell. + this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE); + }, () => undefined); + } + } + + if (itemId != -1) { + let queryName = PlacesUIUtils.getLeftPaneQueryNameFromId(itemId); + if (queryName) + properties += " OrganizerQuery_" + queryName; + } + } + else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) + properties += " separator"; + else if (PlacesUtils.nodeIsURI(node)) { + properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri); + + if (this._controller.hasCachedLivemarkInfo(node.parent)) { + properties += " livemarkItem"; + if (node.accessCount) { + properties += " visited"; + } + } + } + + this._cellProperties.set(node, properties); + } + + return props + " " + properties; + }, + + getColumnProperties: function(aColumn) { return ""; }, + + isContainer: function(aRow) { + // Only leaf nodes aren't listed in the rows array. + let node = this._rows[aRow]; + if (node === undefined) + return false; + + if (PlacesUtils.nodeIsContainer(node)) { + // Flat-lists may ignore expandQueries and other query options when + // they are asked to open a container. + if (this._flatList) + return true; + + // treat non-expandable childless queries as non-containers + if (PlacesUtils.nodeIsQuery(node)) { + let parent = node.parent; + if ((PlacesUtils.nodeIsQuery(parent) || + PlacesUtils.nodeIsFolder(parent)) && + !PlacesUtils.asQuery(node).hasChildren) + return PlacesUtils.asQuery(parent).queryOptions.expandQueries; + } + return true; + } + return false; + }, + + isContainerOpen: function(aRow) { + if (this._flatList) + return false; + + // All containers are listed in the rows array. + return this._rows[aRow].containerOpen; + }, + + isContainerEmpty: function(aRow) { + if (this._flatList) + return true; + + let node = this._rows[aRow]; + if (this._controller.hasCachedLivemarkInfo(node)) { + let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; + return queryOptions.excludeItems; + } + + // All containers are listed in the rows array. + return !node.hasChildren; + }, + + isSeparator: function(aRow) { + // All separators are listed in the rows array. + let node = this._rows[aRow]; + return node && PlacesUtils.nodeIsSeparator(node); + }, + + isSorted: function() { + return this._result.sortingMode != + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; + }, + + canDrop: function(aRow, aOrientation, aDataTransfer) { + if (!this._result) + throw Cr.NS_ERROR_UNEXPECTED; + + // Drop position into a sorted treeview would be wrong. + if (this.isSorted()) + return false; + + let ip = this._getInsertionPoint(aRow, aOrientation); + return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer); + }, + + _getInsertionPoint: function(index, orientation) { + let container = this._result.root; + let dropNearItemId = -1; + // When there's no selection, assume the container is the container + // the view is populated from (i.e. the result's itemId). + if (index != -1) { + let lastSelected = this.nodeForTreeIndex(index); + if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) { + // If the last selected item is an open container, append _into_ + // it, rather than insert adjacent to it. + container = lastSelected; + index = -1; + } + else if (lastSelected.containerOpen && + orientation == Ci.nsITreeView.DROP_AFTER && + lastSelected.hasChildren) { + // If the last selected node is an open container and the user is + // trying to drag into it as a first node, really insert into it. + container = lastSelected; + orientation = Ci.nsITreeView.DROP_ON; + index = 0; + } + else { + // Use the last-selected node's container. + container = lastSelected.parent; + + // During its Drag & Drop operation, the tree code closes-and-opens + // containers very often (part of the XUL "spring-loaded folders" + // implementation). And in certain cases, we may reach a closed + // container here. However, we can simply bail out when this happens, + // because we would then be back here in less than a millisecond, when + // the container had been reopened. + if (!container || !container.containerOpen) + return null; + + // Avoid the potentially expensive call to getChildIndex + // if we know this container doesn't allow insertion. + if (PlacesControllerDragHelper.disallowInsertion(container)) + return null; + + let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; + if (queryOptions.sortingMode != + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) { + // If we are within a sorted view, insert at the end. + index = -1; + } + else if (queryOptions.excludeItems || + queryOptions.excludeQueries || + queryOptions.excludeReadOnlyFolders) { + // Some item may be invisible, insert near last selected one. + // We don't replace index here to avoid requests to the db, + // instead it will be calculated later by the controller. + index = -1; + dropNearItemId = lastSelected.itemId; + } + else { + let lsi = container.getChildIndex(lastSelected); + index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1; + } + } + } + + if (PlacesControllerDragHelper.disallowInsertion(container)) + return null; + + return new InsertionPoint(PlacesUtils.getConcreteItemId(container), + index, orientation, + PlacesUtils.nodeIsTagQuery(container), + dropNearItemId); + }, + + drop: function(aRow, aOrientation, aDataTransfer) { + // We are responsible for translating the |index| and |orientation| + // parameters into a container id and index within the container, + // since this information is specific to the tree view. + let ip = this._getInsertionPoint(aRow, aOrientation); + if (ip) + PlacesControllerDragHelper.onDrop(ip, aDataTransfer); + + PlacesControllerDragHelper.currentDropTarget = null; + }, + + getParentIndex: function(aRow) { + let [parentNode, parentRow] = this._getParentByChildRow(aRow); + return parentRow; + }, + + hasNextSibling: function(aRow, aAfterIndex) { + if (aRow == this._rows.length - 1) { + // The last row has no sibling. + return false; + } + + let node = this._rows[aRow]; + if (node === undefined || this._isPlainContainer(node.parent)) { + // The node is a child of a plain container. + // If the next row is either unset or has the same parent, + // it's a sibling. + let nextNode = this._rows[aRow + 1]; + return (nextNode == undefined || nextNode.parent == node.parent); + } + + let thisLevel = node.indentLevel; + for (let i = aAfterIndex + 1; i < this._rows.length; ++i) { + let rowNode = this._getNodeForRow(i); + let nextLevel = rowNode.indentLevel; + if (nextLevel == thisLevel) + return true; + if (nextLevel < thisLevel) + break; + } + + return false; + }, + + getLevel: function(aRow) this._getNodeForRow(aRow).indentLevel, + + getImageSrc: function(aRow, aColumn) { + // Only the title column has an image. + if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE) + return ""; + + return this._getNodeForRow(aRow).icon; + }, + + getProgressMode: function(aRow, aColumn) { }, + getCellValue: function(aRow, aColumn) { }, + + getCellText: function(aRow, aColumn) { + let node = this._getNodeForRow(aRow); + switch (this._getColumnType(aColumn)) { + case this.COLUMN_TYPE_TITLE: + // normally, this is just the title, but we don't want empty items in + // the tree view so return a special string if the title is empty. + // Do it here so that callers can still get at the 0 length title + // if they go through the "result" API. + if (PlacesUtils.nodeIsSeparator(node)) + return ""; + return PlacesUIUtils.getBestTitle(node, true); + case this.COLUMN_TYPE_TAGS: + return node.tags; + case this.COLUMN_TYPE_URI: + if (PlacesUtils.nodeIsURI(node)) + return node.uri; + return ""; + case this.COLUMN_TYPE_DATE: + let nodeTime = node.time; + if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) { + // hosts and days shouldn't have a value for the date column. + // Actually, you could argue this point, but looking at the + // results, seeing the most recently visited date is not what + // I expect, and gives me no information I know how to use. + // Only show this for URI-based items. + return ""; + } + + return this._convertPRTimeToString(nodeTime); + case this.COLUMN_TYPE_VISITCOUNT: + return node.accessCount; + case this.COLUMN_TYPE_KEYWORD: + if (PlacesUtils.nodeIsBookmark(node)) + return PlacesUtils.bookmarks.getKeywordForBookmark(node.itemId); + return ""; + case this.COLUMN_TYPE_DESCRIPTION: + if (node.itemId != -1) { + try { + return PlacesUtils.annotations. + getItemAnnotation(node.itemId, PlacesUIUtils.DESCRIPTION_ANNO); + } + catch (ex) { /* has no description */ } + } + return ""; + case this.COLUMN_TYPE_DATEADDED: + if (node.dateAdded) + return this._convertPRTimeToString(node.dateAdded); + return ""; + case this.COLUMN_TYPE_LASTMODIFIED: + if (node.lastModified) + return this._convertPRTimeToString(node.lastModified); + return ""; + case this.COLUMN_TYPE_PARENTFOLDER: + if (PlacesUtils.nodeIsQuery(node.parent) && + PlacesUtils.asQuery(node.parent).queryOptions.queryType == + Components.interfaces.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY && node.uri) + return ""; + var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Components.interfaces.nsINavBookmarksService); + var rowId = node.itemId; + try { + var parentFolderId = bmsvc.getFolderIdForItem(rowId); + var folderTitle = bmsvc.getItemTitle(parentFolderId); + } catch(ex) { + var folderTitle = ""; + } + return folderTitle; + case this.COLUMN_TYPE_PARENTFOLDERPATH: + if (PlacesUtils.nodeIsQuery(node.parent) && + PlacesUtils.asQuery(node.parent).queryOptions.queryType == + Components.interfaces.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY && node.uri) + return ""; + var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Components.interfaces.nsINavBookmarksService); + var rowId = node.itemId; + try { + var FolderId; + var parentFolderId = bmsvc.getFolderIdForItem(rowId); + var folderTitle = bmsvc.getItemTitle(parentFolderId); + while ((FolderId = bmsvc.getFolderIdForItem(parentFolderId))) { + if (FolderId == parentFolderId) + break; + parentFolderId = FolderId; + var text = bmsvc.getItemTitle(parentFolderId); + if (!text) + break; + folderTitle = text + " /"+ folderTitle; + } + folderTitle = folderTitle.replace(/^\s/,""); + } catch(ex) { + var folderTitle = ""; + } + return folderTitle; + } + return ""; + }, + + setTree: function(aTree) { + // If we are replacing the tree during a batch, there is a concrete risk + // that the treeView goes out of sync, thus it's safer to end the batch now. + // This is a no-op if we are not batching. + this.batching(false); + + let hasOldTree = this._tree != null; + this._tree = aTree; + + if (this._result) { + if (hasOldTree) { + // detach from result when we are detaching from the tree. + // This breaks the reference cycle between us and the result. + if (!aTree) { + this._result.removeObserver(this); + this._rootNode.containerOpen = false; + } + } + if (aTree) + this._finishInit(); + } + }, + + toggleOpenState: function(aRow) { + if (!this._result) + throw Cr.NS_ERROR_UNEXPECTED; + + let node = this._rows[aRow]; + if (this._flatList && this._openContainerCallback) { + this._openContainerCallback(node); + return; + } + + // Persist containers open status, but never persist livemarks. + if (!this._controller.hasCachedLivemarkInfo(node)) { + let resource = this._getResourceForNode(node); + if (resource) { + const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open"); + const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true"); + + if (node.containerOpen) + PlacesUIUtils.localStore.Unassert(resource, openLiteral, trueLiteral); + else + PlacesUIUtils.localStore.Assert(resource, openLiteral, trueLiteral, true); + } + } + + node.containerOpen = !node.containerOpen; + }, + + cycleHeader: function(aColumn) { + if (!this._result) + throw Cr.NS_ERROR_UNEXPECTED; + + // Sometimes you want a tri-state sorting, and sometimes you don't. This + // rule allows tri-state sorting when the root node is a folder. This will + // catch the most common cases. When you are looking at folders, you want + // the third state to reset the sorting to the natural bookmark order. When + // you are looking at history, that third state has no meaning so we try + // to disallow it. + // + // The problem occurs when you have a query that results in bookmark + // folders. One example of this is the subscriptions view. In these cases, + // this rule doesn't allow you to sort those sub-folders by their natural + // order. + let allowTriState = PlacesUtils.nodeIsFolder(this._result.root); + + let oldSort = this._result.sortingMode; + let oldSortingAnnotation = this._result.sortingAnnotation; + let newSort; + let newSortingAnnotation = ""; + const NHQO = Ci.nsINavHistoryQueryOptions; + switch (this._getColumnType(aColumn)) { + case this.COLUMN_TYPE_TITLE: + if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING) + newSort = NHQO.SORT_BY_TITLE_DESCENDING; + else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING) + newSort = NHQO.SORT_BY_NONE; + else + newSort = NHQO.SORT_BY_TITLE_ASCENDING; + + break; + case this.COLUMN_TYPE_URI: + if (oldSort == NHQO.SORT_BY_URI_ASCENDING) + newSort = NHQO.SORT_BY_URI_DESCENDING; + else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING) + newSort = NHQO.SORT_BY_NONE; + else + newSort = NHQO.SORT_BY_URI_ASCENDING; + + break; + case this.COLUMN_TYPE_DATE: + if (oldSort == NHQO.SORT_BY_DATE_ASCENDING) + newSort = NHQO.SORT_BY_DATE_DESCENDING; + else if (allowTriState && + oldSort == NHQO.SORT_BY_DATE_DESCENDING) + newSort = NHQO.SORT_BY_NONE; + else + newSort = NHQO.SORT_BY_DATE_ASCENDING; + + break; + case this.COLUMN_TYPE_VISITCOUNT: + // visit count default is unusual because we sort by descending + // by default because you are most likely to be looking for + // highly visited sites when you click it + if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING) + newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING; + else if (allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING) + newSort = NHQO.SORT_BY_NONE; + else + newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING; + + break; + case this.COLUMN_TYPE_KEYWORD: + if (oldSort == NHQO.SORT_BY_KEYWORD_ASCENDING) + newSort = NHQO.SORT_BY_KEYWORD_DESCENDING; + else if (allowTriState && oldSort == NHQO.SORT_BY_KEYWORD_DESCENDING) + newSort = NHQO.SORT_BY_NONE; + else + newSort = NHQO.SORT_BY_KEYWORD_ASCENDING; + + break; + case this.COLUMN_TYPE_DESCRIPTION: + if (oldSort == NHQO.SORT_BY_ANNOTATION_ASCENDING && + oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) { + newSort = NHQO.SORT_BY_ANNOTATION_DESCENDING; + newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO; + } + else if (allowTriState && + oldSort == NHQO.SORT_BY_ANNOTATION_DESCENDING && + oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) + newSort = NHQO.SORT_BY_NONE; + else { + newSort = NHQO.SORT_BY_ANNOTATION_ASCENDING; + newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO; + } + + break; + case this.COLUMN_TYPE_DATEADDED: + if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING) + newSort = NHQO.SORT_BY_DATEADDED_DESCENDING; + else if (allowTriState && + oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING) + newSort = NHQO.SORT_BY_NONE; + else + newSort = NHQO.SORT_BY_DATEADDED_ASCENDING; + + break; + case this.COLUMN_TYPE_LASTMODIFIED: + if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING) + newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING; + else if (allowTriState && + oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING) + newSort = NHQO.SORT_BY_NONE; + else + newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING; + + break; + case this.COLUMN_TYPE_TAGS: + if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING) + newSort = NHQO.SORT_BY_TAGS_DESCENDING; + else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING) + newSort = NHQO.SORT_BY_NONE; + else + newSort = NHQO.SORT_BY_TAGS_ASCENDING; + + break; + case this.COLUMN_TYPE_PARENTFOLDER: + return; + + break; + case this.COLUMN_TYPE_PARENTFOLDERPATH: + return; + + break; + default: + throw Cr.NS_ERROR_INVALID_ARG; + } + this._result.sortingAnnotation = newSortingAnnotation; + this._result.sortingMode = newSort; + }, + + isEditable: function(aRow, aColumn) { + // At this point we only support editing the title field. + if (aColumn.index != 0) + return false; + + let node = this._rows[aRow]; + if (!node) { + Cu.reportError("isEditable called for an unbuilt row."); + return false; + } + let itemId = node.itemId; + + // Only bookmark-nodes are editable. Fortunately, this check also takes + // care of livemark children. + if (itemId == -1) + return false; + + // The following items are also not editable, even though they are bookmark + // items. + // * places-roots + // * the left pane special folders and queries (those are place: uri + // bookmarks) + // * separators + // + // Note that concrete itemIds aren't used intentionally. For example, we + // have no reason to disallow renaming a shortcut to the Bookmarks Toolbar, + // except for the one under All Bookmarks. + if (PlacesUtils.nodeIsSeparator(node) || PlacesUtils.isRootItem(itemId)) + return false; + + let parentId = PlacesUtils.getConcreteItemId(node.parent); + if (parentId == PlacesUIUtils.leftPaneFolderId || + parentId == PlacesUIUtils.allBookmarksFolderId) { + // Note that the for the time being this is the check that actually + // blocks renaming places "roots", and not the isRootItem check above. + // That's because places root are only exposed through folder shortcuts + // descendants of the left pane folder. + return false; + } + + return true; + }, + + setCellText: function(aRow, aColumn, aText) { + // We may only get here if the cell is editable. + let node = this._rows[aRow]; + if (node.title != aText) { + let txn = new PlacesEditItemTitleTransaction(node.itemId, aText); + PlacesUtils.transactionManager.doTransaction(txn); + } + }, + + toggleCutNode: function(aNode, aValue) { + let currentVal = this._cuttingNodes.has(aNode); + if (currentVal != aValue) { + if (aValue) + this._cuttingNodes.add(aNode); + else + this._cuttingNodes.delete(aNode); + + this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); + } + }, + + selectionChanged: function() { }, + cycleCell: function(aRow, aColumn) { }, + isSelectable: function(aRow, aColumn) { return false; }, + performAction: function(aAction) { }, + performActionOnRow: function(aAction, aRow) { }, + performActionOnCell: function(aAction, aRow, aColumn) { } +}; diff --git a/browser/components/places/jar.mn b/browser/components/places/jar.mn new file mode 100644 index 000000000..77d05663a --- /dev/null +++ b/browser/components/places/jar.mn @@ -0,0 +1,34 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: +% overlay chrome://browser/content/places/places.xul chrome://browser/content/places/downloadsViewOverlay.xul +# Provide another URI for the bookmarkProperties dialog so we can persist the +# attributes separately + content/browser/places/bookmarkProperties2.xul (content/bookmarkProperties.xul) +* content/browser/places/places.xul (content/places.xul) + content/browser/places/places.js (content/places.js) + content/browser/places/places.css (content/places.css) + content/browser/places/organizer.css (content/organizer.css) + content/browser/places/bookmarkProperties.xul (content/bookmarkProperties.xul) + content/browser/places/bookmarkProperties.js (content/bookmarkProperties.js) + content/browser/places/placesOverlay.xul (content/placesOverlay.xul) + content/browser/places/menu.xml (content/menu.xml) + content/browser/places/tree.xml (content/tree.xml) + content/browser/places/controller.js (content/controller.js) + content/browser/places/treeView.js (content/treeView.js) + content/browser/places/browserPlacesViews.js (content/browserPlacesViews.js) +# keep the Places version of the history sidebar at history/history-panel.xul +# to prevent having to worry about between versions of the browser + content/browser/history/history-panel.xul (content/history-panel.xul) + content/browser/places/history-panel.js (content/history-panel.js) +# ditto for the bookmarks sidebar + content/browser/bookmarks/bookmarksPanel.xul (content/bookmarksPanel.xul) + content/browser/bookmarks/bookmarksPanel.js (content/bookmarksPanel.js) + content/browser/bookmarks/sidebarUtils.js (content/sidebarUtils.js) + content/browser/places/moveBookmarks.xul (content/moveBookmarks.xul) + content/browser/places/moveBookmarks.js (content/moveBookmarks.js) + content/browser/places/editBookmarkOverlay.xul (content/editBookmarkOverlay.xul) + content/browser/places/editBookmarkOverlay.js (content/editBookmarkOverlay.js) + content/browser/places/downloadsViewOverlay.xul (content/downloadsViewOverlay.xul) diff --git a/browser/components/places/moz.build b/browser/components/places/moz.build new file mode 100644 index 000000000..8d85e2b76 --- /dev/null +++ b/browser/components/places/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] + +EXTRA_JS_MODULES += [ 'PlacesUIUtils.jsm' ] diff --git a/browser/components/preferences/advanced.js b/browser/components/preferences/advanced.js new file mode 100644 index 000000000..9fd7e9943 --- /dev/null +++ b/browser/components/preferences/advanced.js @@ -0,0 +1,726 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Load DownloadUtils module for convertByteUnits +Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); +Components.utils.import("resource://gre/modules/ctypes.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/LoadContextInfo.jsm"); +Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); + +var gAdvancedPane = { + _inited: false, + + /** + * Brings the appropriate tab to the front and initializes various bits of UI. + */ + init: function() + { + this._inited = true; + var advancedPrefs = document.getElementById("advancedPrefs"); + + var extraArgs = window.arguments[1]; + if (extraArgs && extraArgs["advancedTab"]){ + advancedPrefs.selectedTab = document.getElementById(extraArgs["advancedTab"]); + } else { + var preference = document.getElementById("browser.preferences.advanced.selectedTabIndex"); + if (preference.value !== null) + advancedPrefs.selectedIndex = preference.value; + } + +#ifdef MOZ_UPDATER + this.updateReadPrefs(); +#endif + this.updateOfflineAppsPermissions(); + this.updateOfflineApps(); + + this.updateActualCacheSize(); + this.updateActualAppCacheSize(); + + this.updateHWADisplay(); + + this.updateUAODisplay(); + + // Notify observers that the UI is now ready + Services.obs.notifyObservers(window, "advanced-pane-loaded", null); + }, + + /** + * Stores the identity of the current tab in preferences so that the selected + * tab can be persisted between openings of the preferences window. + */ + tabSelectionChanged: function() + { + if (!this._inited) + return; + var advancedPrefs = document.getElementById("advancedPrefs"); + var preference = document.getElementById("browser.preferences.advanced.selectedTabIndex"); + preference.valueFromPreferences = advancedPrefs.selectedIndex; + }, + + // GENERAL TAB + + /* + * Preferences: + * + * accessibility.browsewithcaret + * - true enables keyboard navigation and selection within web pages using a + * visible caret, false uses normal keyboard navigation with no caret + * accessibility.typeaheadfind + * - when set to true, typing outside text areas and input boxes will + * automatically start searching for what's typed within the current + * document; when set to false, no search action happens + * general.autoScroll + * - when set to true, clicking the scroll wheel on the mouse activates a + * mouse mode where moving the mouse down scrolls the document downward with + * speed correlated with the distance of the cursor from the original + * position at which the click occurred (and likewise with movement upward); + * if false, this behavior is disabled + * general.smoothScroll + * - set to true to enable finer page scrolling than line-by-line on page-up, + * page-down, and other such page movements + * layout.spellcheckDefault + * - an integer: + * 0 disables spellchecking + * 1 enables spellchecking, but only for multiline text fields + * 2 enables spellchecking for all text fields + */ + + /** + * Stores the original value of the spellchecking preference to enable proper + * restoration if unchanged (since we're mapping a tristate onto a checkbox). + */ + _storedSpellCheck: 0, + + /** + * Returns true if any spellchecking is enabled and false otherwise, caching + * the current value to enable proper pref restoration if the checkbox is + * never changed. + */ + readCheckSpelling: function() + { + var pref = document.getElementById("layout.spellcheckDefault"); + this._storedSpellCheck = pref.value; + + return (pref.value != 0); + }, + + /** + * Returns the value of the spellchecking preference represented by UI, + * preserving the preference's "hidden" value if the preference is + * unchanged and represents a value not strictly allowed in UI. + */ + writeCheckSpelling: function() + { + var checkbox = document.getElementById("checkSpelling"); + return checkbox.checked ? (this._storedSpellCheck == 2 ? 2 : 1) : 0; + }, + + /** + * security.OCSP.enabled is an integer value for legacy reasons. + * A value of 1 means OCSP is enabled. Any other value means it is disabled. + */ + readEnableOCSP: function() + { + var preference = document.getElementById("security.OCSP.enabled"); + // This is the case if the preference is the default value. + if (preference.value === undefined) { + return true; + } + return preference.value == 1; + }, + + /** + * See documentation for readEnableOCSP. + */ + writeEnableOCSP: function() + { + var checkbox = document.getElementById("enableOCSP"); + return checkbox.checked ? 1 : 0; + }, + + /** + * When the user toggles the layers.acceleration.disabled pref, + * sync its new value to the gfx.direct2d.disabled pref too. + */ + updateHardwareAcceleration: function() + { +#ifdef XP_WIN + var fromPref = document.getElementById("layers.acceleration.enabled"); + var toPref = document.getElementById("gfx.direct2d.enabled"); + toPref.value = fromPref.value; +#endif + this.updateHWADisplay(); + }, + + updateHWADisplay: function() + { +#ifdef XP_LINUX + let HWA = document.getElementById("layers.acceleration.enabled"); + document.getElementById("forceHWAccel").disabled = !HWA.value; +#endif + }, + + updateUAODisplay: function() + { + let GUAO = Services.prefs.getCharPref("network.http.useragent.global_override", ""); + let overridden = (GUAO != ""); + document.getElementById("UACompatGroup").hidden = overridden; + document.getElementById("GUAOwarning").hidden = !overridden; + }, + + GUAOReset: function() + { + Services.prefs.clearUserPref("network.http.useragent.global_override"); + this.updateUAODisplay(); + }, + + // DATA CHOICES TAB + + /** + * opening links behind a modal dialog is poor form. Work around flawed text-link handling here. + */ + openTextLink: function(evt) { + let where = Services.prefs.getBoolPref("browser.preferences.instantApply") ? "tab" : "window"; + openUILinkIn(evt.target.getAttribute("href"), where); + evt.preventDefault(); + }, + + /** + * Set up or hide the Learn More links for various data collection options + */ + _setupLearnMoreLink: function(pref, element) { + // set up the Learn More link with the correct URL + let url = Services.prefs.getCharPref(pref); + let el = document.getElementById(element); + + if (url) { + el.setAttribute("href", url); + } else { + el.setAttribute("hidden", "true"); + } + }, + + // NETWORK TAB + + /* + * Preferences: + * + * browser.cache.disk.capacity + * - the size of the browser cache in KB + * - Only used if browser.cache.disk.smart_size.enabled is disabled + */ + + /** + * Displays a dialog in which proxy settings may be changed. + */ + showConnections: function() + { + document.documentElement.openSubDialog("chrome://browser/content/preferences/connection.xul", + "", null); + }, + + // Retrieves the amount of space currently used by disk cache + updateActualCacheSize: function() + { + var sum = 0; + function updateUI(consumption) { + var actualSizeLabel = document.getElementById("actualDiskCacheSize"); + var sizeStrings = DownloadUtils.convertByteUnits(consumption); + var prefStrBundle = document.getElementById("bundlePreferences"); + var sizeStr = prefStrBundle.getFormattedString("actualDiskCacheSize", sizeStrings); + actualSizeLabel.value = sizeStr; + } + + Visitor.prototype = { + expected: 0, + sum: 0, + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsICacheStorageVisitor)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onCacheStorageInfo: function(num, consumption) + { + this.sum += consumption; + if (!--this.expected) + updateUI(this.sum); + } + }; + function Visitor(callbacksExpected) { + this.expected = callbacksExpected; + } + + var cacheService = + Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + // non-anonymous + var storage1 = cacheService.diskCacheStorage(LoadContextInfo.default, false); + // anonymous + var storage2 = cacheService.diskCacheStorage(LoadContextInfo.anonymous, false); + + // expect 2 callbacks + var visitor = new Visitor(2); + storage1.asyncVisitStorage(visitor, false /* Do not walk entries */); + storage2.asyncVisitStorage(visitor, false /* Do not walk entries */); + }, + + // Retrieves the amount of space currently used by offline cache + updateActualAppCacheSize: function() + { + var visitor = { + onCacheStorageInfo: function(aEntryCount, aConsumption, aCapacity, aDiskDirectory) + { + var actualSizeLabel = document.getElementById("actualAppCacheSize"); + var sizeStrings = DownloadUtils.convertByteUnits(aConsumption); + var prefStrBundle = document.getElementById("bundlePreferences"); + var sizeStr = prefStrBundle.getFormattedString("actualAppCacheSize", sizeStrings); + actualSizeLabel.value = sizeStr; + } + }; + + var cacheService = + Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + var storage = cacheService.appCacheStorage(LoadContextInfo.default, null); + try { + storage.asyncVisitStorage(visitor, false); + } catch(ex) { + // Service unavailable: user most likely crippled the cache. + } + }, + + updateCacheSizeUI: function(smartSizeEnabled) + { + document.getElementById("useCacheBefore").disabled = smartSizeEnabled; + document.getElementById("cacheSize").disabled = smartSizeEnabled; + document.getElementById("useCacheAfter").disabled = smartSizeEnabled; + }, + + readSmartSizeEnabled: function() + { + // The smart_size.enabled preference element is inverted="true", so its + // value is the opposite of the actual pref value + var disabled = document.getElementById("browser.cache.disk.smart_size.enabled").value; + this.updateCacheSizeUI(!disabled); + }, + + /** + * Converts the cache size from units of KB to units of MB and returns that + * value. + */ + readCacheSize: function() + { + var preference = document.getElementById("browser.cache.disk.capacity"); + return preference.value / 1024; + }, + + /** + * Converts the cache size as specified in UI (in MB) to KB and returns that + * value. + */ + writeCacheSize: function() + { + var cacheSize = document.getElementById("cacheSize"); + var intValue = parseInt(cacheSize.value, 10); + return isNaN(intValue) ? 0 : intValue * 1024; + }, + + /** + * Clears the cache. + */ + clearCache: function() + { + var cache = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Components.interfaces.nsICacheStorageService); + try { + cache.clear(); + } catch(ex) {} + this.updateActualCacheSize(); + }, + + /** + * Clears the application cache. + */ + clearOfflineAppCache: function() + { + Components.utils.import("resource:///modules/offlineAppCache.jsm"); + OfflineAppCacheHelper.clear(); + + this.updateActualAppCacheSize(); + this.updateOfflineApps(); + }, + + updateOfflineAppsPermissions: function() + { + var permPref = document.getElementById("offline-apps.permissions"); + var allowPref = document.getElementById("offline-apps.allow_by_default"); + var notifyPref = document.getElementById("browser.offline-apps.notify"); + switch (permPref.value) { + case 0: allowPref.value = false; + notifyPref.value = false; + break; + case 1: allowPref.value = false; + notifyPref.value = true; + break; + case 2: allowPref.value = true; + notifyPref.value = true; + break; + default: console.error("Preference error: Invalid value ",permPref.value," for offline app permissions - resetting to default."); + permPref.value = 2; + allowPref.value = true; + notifyPref.value = true; + } + // Set state of "Exceptions" button accordingly. + var button = document.getElementById("offlineNotifyExceptions"); + button.disabled = !allowPref.value && !notifyPref.value; + }, + + showOfflineExceptions: function() + { + var bundlePreferences = document.getElementById("bundlePreferences"); + var params = { blockVisible : false, + sessionVisible : false, + allowVisible : false, + prefilledHost : "", + permissionType : "offline-app", + manageCapability : Components.interfaces.nsIPermissionManager.DENY_ACTION, + windowTitle : bundlePreferences.getString("offlinepermissionstitle"), + introText : bundlePreferences.getString("offlinepermissionstext") }; + document.documentElement.openWindow("Browser:Permissions", + "chrome://browser/content/preferences/permissions.xul", + "", params); + }, + + // XXX: duplicated in browser.js + _getOfflineAppUsage: function(perm, groups) + { + var cacheService = Components.classes["@mozilla.org/network/application-cache-service;1"]. + getService(Components.interfaces.nsIApplicationCacheService); + if (!groups) { + try { + groups = cacheService.getGroups(); + } catch(ex) { + // Cache disabled. + return 0; + } + } + + var ios = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + + var usage = 0; + for (var i = 0; i < groups.length; i++) { + var uri = ios.newURI(groups[i], null, null); + if (perm.matchesURI(uri, true)) { + var cache = cacheService.getActiveCache(groups[i]); + usage += cache.usage; + } + } + + return usage; + }, + + /** + * Updates the list of offline applications + */ + updateOfflineApps: function() + { + var pm = Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager); + + var list = document.getElementById("offlineAppsList"); + while (list.firstChild) { + list.removeChild(list.firstChild); + } + + var cacheService = Components.classes["@mozilla.org/network/application-cache-service;1"]. + getService(Components.interfaces.nsIApplicationCacheService); + + try { + var groups = cacheService.getGroups(); + + var bundle = document.getElementById("bundlePreferences"); + + var enumerator = pm.enumerator; + while (enumerator.hasMoreElements()) { + var perm = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission); + if (perm.type == "offline-app" && + perm.capability != Components.interfaces.nsIPermissionManager.DEFAULT_ACTION && + perm.capability != Components.interfaces.nsIPermissionManager.DENY_ACTION) { + var row = document.createElement("listitem"); + row.id = ""; + row.className = "offlineapp"; + row.setAttribute("origin", perm.principal.origin); + var converted = DownloadUtils. + convertByteUnits(this._getOfflineAppUsage(perm, groups)); + row.setAttribute("usage", + bundle.getFormattedString("offlineAppUsage", + converted)); + list.appendChild(row); + } + } + } catch(ex) { + // Cache service unavailable/errored, off-line app cache is disabled or 0 + // Do nothing, just leave the box blank. + } + }, + + offlineAppSelected: function() + { + var removeButton = document.getElementById("offlineAppsListRemove"); + var list = document.getElementById("offlineAppsList"); + if (list.selectedItem) { + removeButton.setAttribute("disabled", "false"); + } else { + removeButton.setAttribute("disabled", "true"); + } + }, + + removeOfflineApp: function() + { + var list = document.getElementById("offlineAppsList"); + var item = list.selectedItem; + var origin = item.getAttribute("origin"); + var principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin); + + var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + var flags = prompts.BUTTON_TITLE_IS_STRING * prompts.BUTTON_POS_0 + + prompts.BUTTON_TITLE_CANCEL * prompts.BUTTON_POS_1; + + var bundle = document.getElementById("bundlePreferences"); + var title = bundle.getString("offlineAppRemoveTitle"); + var prompt = bundle.getFormattedString("offlineAppRemovePrompt", [principal.URI.prePath]); + var confirm = bundle.getString("offlineAppRemoveConfirm"); + var result = prompts.confirmEx(window, title, prompt, flags, confirm, + null, null, null, {}); + if (result != 0) + return; + + // get the permission + var pm = Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager); + var perm = pm.getPermissionObject(principal, "offline-app", true); + if (perm) { + // clear offline cache entries + try { + var cacheService = Components.classes["@mozilla.org/network/application-cache-service;1"]. + getService(Components.interfaces.nsIApplicationCacheService); + var groups = cacheService.getGroups(); + for (var i = 0; i < groups.length; i++) { + var uri = Services.io.newURI(groups[i], null, null); + if (perm.matchesURI(uri, true)) { + var cache = cacheService.getActiveCache(groups[i]); + cache.discard(); + } + } + } catch (e) {} + + pm.removePermission(perm); + } + list.removeChild(item); + gAdvancedPane.offlineAppSelected(); + this.updateActualAppCacheSize(); + }, + + // UPDATE TAB + + /* + * Preferences: + * + * app.update.enabled + * - true if updates to the application are enabled, false otherwise + * extensions.update.enabled + * - true if updates to extensions and themes are enabled, false otherwise + * browser.search.update + * - true if updates to search engines are enabled, false otherwise + * app.update.auto + * - true if updates should be automatically downloaded and installed, + * possibly with a warning if incompatible extensions are installed (see + * app.update.mode); false if the user should be asked what he wants to do + * when an update is available + * app.update.mode + * - an integer: + * 0 do not warn if an update will disable extensions or themes + * 1 warn if an update will disable extensions or themes + * 2 warn if an update will disable extensions or themes *or* if the + * update is a major update + */ + +#ifdef MOZ_UPDATER + /** + * Selects the item of the radiogroup, and sets the warnIncompatible checkbox + * based on the pref values and locked states. + * + * UI state matrix for update preference conditions + * + * UI Components: Preferences + * Radiogroup i = app.update.enabled + * Warn before disabling extensions checkbox ii = app.update.auto + * iii = app.update.mode + * + * Disabled states: + * Element pref value locked disabled + * radiogroup i t/f f false + * i t/f *t* *true* + * ii t/f f false + * ii t/f *t* *true* + * iii 0/1/2 t/f false + * warnIncompatible i t f false + * i t *t* *true* + * i *f* t/f *true* + * ii t f false + * ii t *t* *true* + * ii *f* t/f *true* + * iii 0/1/2 f false + * iii 0/1/2 *t* *true* + */ + updateReadPrefs: function() + { + var enabledPref = document.getElementById("app.update.enabled"); + var autoPref = document.getElementById("app.update.auto"); + var radiogroup = document.getElementById("updateRadioGroup"); + + if (!enabledPref.value) // Don't care for autoPref.value in this case. + radiogroup.value="manual"; // 3. Never check for updates. + else if (autoPref.value) // enabledPref.value && autoPref.value + radiogroup.value="auto"; // 1. Automatically install updates for Desktop only + else // enabledPref.value && !autoPref.value + radiogroup.value="checkOnly"; // 2. Check, but let me choose + + var canCheck = Components.classes["@mozilla.org/updates/update-service;1"]. + getService(Components.interfaces.nsIApplicationUpdateService). + canCheckForUpdates; + // canCheck is false if the enabledPref is false and locked, + // or the binary platform or OS version is not known. + // A locked pref is sufficient to disable the radiogroup. + radiogroup.disabled = !canCheck || enabledPref.locked || autoPref.locked; + + var modePref = document.getElementById("app.update.mode"); + var warnIncompatible = document.getElementById("warnIncompatible"); + // the warnIncompatible checkbox value is set by readAddonWarn + warnIncompatible.disabled = radiogroup.disabled || modePref.locked || + !enabledPref.value || !autoPref.value; + }, + + /** + * Sets the pref values based on the selected item of the radiogroup, + * and sets the disabled state of the warnIncompatible checkbox accordingly. + */ + updateWritePrefs: function() + { + var enabledPref = document.getElementById("app.update.enabled"); + var autoPref = document.getElementById("app.update.auto"); + var radiogroup = document.getElementById("updateRadioGroup"); + switch (radiogroup.value) { + case "auto": // 1. Automatically install updates for Desktop only + enabledPref.value = true; + autoPref.value = true; + break; + case "checkOnly": // 2. Check, but let me choose + enabledPref.value = true; + autoPref.value = false; + break; + case "manual": // 3. Never check for updates. + enabledPref.value = false; + autoPref.value = false; + } + + var warnIncompatible = document.getElementById("warnIncompatible"); + var modePref = document.getElementById("app.update.mode"); + warnIncompatible.disabled = enabledPref.locked || !enabledPref.value || + autoPref.locked || !autoPref.value || + modePref.locked; + + }, + + /** + * Stores the value of the app.update.mode preference, which is a tristate + * integer preference. We store the value here so that we can properly + * restore the preference value if the UI reflecting the preference value + * is in a state which can represent either of two integer values (as + * opposed to only one possible value in the other UI state). + */ + _modePreference: -1, + + /** + * Reads the app.update.mode preference and converts its value into a + * true/false value for use in determining whether the "Warn me if this will + * disable extensions or themes" checkbox is checked. We also save the value + * of the preference so that the preference value can be properly restored if + * the user's preferences cannot adequately be expressed by a single checkbox. + * + * app.update.mode Checkbox State Meaning + * 0 Unchecked Do not warn + * 1 Checked Warn if there are incompatibilities + * 2 Checked Warn if there are incompatibilities, + * or the update is major. + */ + readAddonWarn: function() + { + var preference = document.getElementById("app.update.mode"); + var warn = preference.value != 0; + gAdvancedPane._modePreference = warn ? preference.value : 1; + return warn; + }, + + /** + * Converts the state of the "Warn me if this will disable extensions or + * themes" checkbox into the integer preference which represents it, + * returning that value. + */ + writeAddonWarn: function() + { + var warnIncompatible = document.getElementById("warnIncompatible"); + return !warnIncompatible.checked ? 0 : gAdvancedPane._modePreference; + }, + + /** + * Displays the history of installed updates. + */ + showUpdates: function() + { + var prompter = Components.classes["@mozilla.org/updates/update-prompt;1"] + .createInstance(Components.interfaces.nsIUpdatePrompt); + prompter.showUpdateHistory(window); + }, +#endif + + // CERTIFICATES TAB + + /* + * Preferences: + * + * security.default_personal_cert + * - a string: + * "Select Automatically" select a certificate automatically when a site + * requests one + * "Ask Every Time" present a dialog to the user so he can select + * the certificate to use on a site which + * requests one + */ + + /** + * Displays the user's certificates and associated options. + */ + showCertificates: function() + { + document.documentElement.openWindow("mozilla:certmanager", + "chrome://pippki/content/certManager.xul", + "", null); + }, + + /** + * Displays a dialog from which the user can manage his security devices. + */ + showSecurityDevices: function() + { + document.documentElement.openWindow("mozilla:devicemanager", + "chrome://pippki/content/device_manager.xul", + "", null); + } +}; diff --git a/browser/components/preferences/advanced.xul b/browser/components/preferences/advanced.xul new file mode 100644 index 000000000..cfc857aed --- /dev/null +++ b/browser/components/preferences/advanced.xul @@ -0,0 +1,448 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % advancedDTD SYSTEM "chrome://browser/locale/preferences/advanced.dtd"> +%advancedDTD; +<!ENTITY % privacyDTD SYSTEM "chrome://browser/locale/preferences/privacy.dtd"> +%privacyDTD; +]> + +<overlay id="AdvancedPaneOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <prefpane id="paneAdvanced" onpaneload="gAdvancedPane.init();"> + + <preferences id="advancedPreferences"> + <preference id="browser.preferences.advanced.selectedTabIndex" + name="browser.preferences.advanced.selectedTabIndex" + type="int"/> + + <!--XXX button prefs --> + + <!-- General tab --> + <preference id="accessibility.typeaheadfind" name="accessibility.typeaheadfind" type="bool"/> + + <preference id="general.autoScroll" name="general.autoScroll" type="bool"/> + <preference id="general.smoothScroll" name="general.smoothScroll" type="bool"/> + <preference id="layers.acceleration.enabled" name="layers.acceleration.enabled" type="bool" + onchange="gAdvancedPane.updateHardwareAcceleration()"/> + <preference id="layers.acceleration.force" name="layers.acceleration.force" type="bool"/> +#ifdef XP_WIN + <preference id="gfx.direct2d.enabled" name="gfx.direct2d.disabled" type="bool" inverted="true"/> +#endif + <preference id="layout.spellcheckDefault" name="layout.spellcheckDefault" type="int"/> + + <preference id="pref.general.compatmode" name="general.useragent.compatMode" type="int"/> + + <preference id="pref.general.captiveportal" name="network.captive-portal-service.enabled" type="bool"/> + + <!-- Network tab --> + <preference id="browser.cache.disk.capacity" name="browser.cache.disk.capacity" type="int"/> + + <preference id="browser.cache.disk.smart_size.enabled" + name="browser.cache.disk.smart_size.enabled" + inverted="true" + type="bool"/> + + <preference id="offline-apps.permissions" name="offline-apps.permissions" type="int" + onchange="gAdvancedPane.updateOfflineAppsPermissions()"/> + <preference id="browser.offline-apps.notify" name="browser.offline-apps.notify" type="bool"/> + <preference id="offline-apps.allow_by_default" name="offline-apps.allow_by_default" type="bool"/> + + <!-- Update tab --> +#ifdef MOZ_UPDATER + <preference id="app.update.enabled" name="app.update.enabled" type="bool"/> + <preference id="app.update.auto" name="app.update.auto" type="bool"/> + <preference id="app.update.mode" name="app.update.mode" type="int"/> + + <preference id="app.update.disable_button.showUpdateHistory" + name="app.update.disable_button.showUpdateHistory" + type="bool"/> +#endif + + <preference id="browser.search.update" name="browser.search.update" type="bool"/> + + <!-- Certificates tab --> + <preference id="security.default_personal_cert" name="security.default_personal_cert" type="string"/> + + <preference id="security.disable_button.openCertManager" + name="security.disable_button.openCertManager" + type="bool"/> + <preference id="security.disable_button.openDeviceManager" + name="security.disable_button.openDeviceManager" + type="bool"/> + <preference id="security.OCSP.enabled" + name="security.OCSP.enabled" + type="int"/> + <preference id="security.OCSP.require" + name="security.OCSP.require" + type="bool"/> + + <!-- Pale Moon: smooth scrolling tab --> + <preference id="general.smoothScroll.lines" name="general.smoothScroll.lines" type="bool"/> + <preference id="general.smoothScroll.lines.durationMinMS" name="general.smoothScroll.lines.durationMinMS" type="int"/> + <preference id="general.smoothScroll.lines.durationMaxMS" name="general.smoothScroll.lines.durationMaxMS" type="int"/> + <preference id="general.smoothScroll.pages" name="general.smoothScroll.pages" type="bool"/> + <preference id="general.smoothScroll.pages.durationMinMS" name="general.smoothScroll.pages.durationMinMS" type="int"/> + <preference id="general.smoothScroll.pages.durationMaxMS" name="general.smoothScroll.pages.durationMaxMS" type="int"/> + <preference id="general.smoothScroll.mouseWheel" name="general.smoothScroll.mouseWheel" type="bool"/> + <preference id="general.smoothScroll.mouseWheel.durationMinMS" name="general.smoothScroll.mouseWheel.durationMinMS" type="int"/> + <preference id="general.smoothScroll.mouseWheel.durationMaxMS" name="general.smoothScroll.mouseWheel.durationMaxMS" type="int"/> + <preference id="general.smoothScroll.scrollbars" name="general.smoothScroll.scrollbars" type="bool"/> + <preference id="general.smoothScroll.scrollbars.durationMinMS" name="general.smoothScroll.scrollbars.durationMinMS" type="int"/> + <preference id="general.smoothScroll.scrollbars.durationMaxMS" name="general.smoothScroll.scrollbars.durationMaxMS" type="int"/> + + <preference id="mousewheel.default.delta_multiplier_y" name="mousewheel.default.delta_multiplier_y" type="int"/> + </preferences> + + <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> + + <script type="application/javascript" src="chrome://browser/content/preferences/advanced.js"/> + + <tabbox id="advancedPrefs" flex="1" + onselect="gAdvancedPane.tabSelectionChanged();"> + + <tabs id="tabsElement"> + <tab id="generalTab" label="&generalTab.label;" helpTopic="prefs-advanced-general"/> + <tab id="networkTab" label="&networkTab.label;" helpTopic="prefs-advanced-network"/> + <tab id="updateTab" label="&updateTab.label;" helpTopic="prefs-advanced-update"/> + <tab id="encryptionTab" label="&certificateTab.label;" helpTopic="prefs-advanced-encryption"/> + <tab id="scrollparamTab" label="&scrollparamTab.label;" helpTopic="prefs-advanced-scrollparams"/> + </tabs> + + <tabpanels flex="1"> + + <!-- General --> + <tabpanel id="generalPanel" orient="vertical"> + + <!-- Accessibility --> + <groupbox id="accessibilityGroup" align="start"> + <caption label="&accessibility.label;"/> + + <checkbox id="searchStartTyping" + label="&searchStartTyping.label;" + accesskey="&searchStartTyping.accesskey;" + preference="accessibility.typeaheadfind"/> + </groupbox> + + <!-- Browsing --> + <groupbox id="browsingGroup" align="start"> + <caption label="&browsing.label;"/> + + <checkbox id="useAutoScroll" + label="&useAutoScroll.label;" + accesskey="&useAutoScroll.accesskey;" + preference="general.autoScroll"/> + <checkbox id="checkSpelling" + label="&checkSpelling.label;" + accesskey="&checkSpelling.accesskey;" + onsyncfrompreference="return gAdvancedPane.readCheckSpelling();" + onsynctopreference="return gAdvancedPane.writeCheckSpelling();" + preference="layout.spellcheckDefault"/> + </groupbox> + + <!-- Hardware Acceleration --> + <groupbox id="browsingGroup" align="start"> + <caption label="&HWAccel.label;"/> + <label>&restartRequired.label;</label> + <checkbox id="allowHWAccel" + label="&allowHWAccel.label;" + accesskey="&allowHWAccel.accesskey;" + preference="layers.acceleration.enabled"/> +#ifdef XP_LINUX + <checkbox id="forceHWAccel" class="indent" + label="&forceHWAccel.label;" + preference="layers.acceleration.force"/> +#endif + </groupbox> + + <!-- User Agent compatibility --> + <hbox id="GUAOwarning" align="center" hidden="true"> + <label style="color:red;" id="UAWarning">&UAWarning.label;</label> + <button label="&UAWarning.reset;" oncommand="gAdvancedPane.GUAOReset();" /> + </hbox> + <groupbox id="UACompatGroup" orient="vertical"> + <caption label="&UACompatGroup.label;"/> + <hbox align="center"> + <label id="UACompat" control="UACompat-menu">&UACompat.label;</label> + <menulist id="UACompat-menu" preference="pref.general.compatmode" sizetopopup="always"> + <menupopup> + <menuitem label="&UACompat.Native;" value="0" /> + <menuitem label="&UACompat.Gecko;" value="1" /> + <menuitem label="&UACompat.Firefox;" value="2" /> + </menupopup> + </menulist> + </hbox> + </groupbox> + + <!-- Captive portal detection --> + <groupbox id="captivePortalGroup" orient="vertical"> + <caption label="&captivePortalGroup.label;"/> + <checkbox id="captivePortalDetect" + label="&captivePortalDetect.label;" + preference="pref.general.captiveportal"/> + </groupbox> + + </tabpanel> + + <!-- Network --> + <tabpanel id="networkPanel" orient="vertical"> + + <!-- Connection --> + <groupbox id="connectionGroup"> + <caption label="&connection.label;"/> + + <hbox align="center"> + <description flex="1" control="connectionSettings">&connectionDesc.label;</description> + <button id="connectionSettings" icon="network" label="&connectionSettings.label;" + accesskey="&connectionSettings.accesskey;" + oncommand="gAdvancedPane.showConnections();"/> + </hbox> + </groupbox> + + <!-- Cache --> + <groupbox id="cacheGroup"> + <caption label="&httpCache.label;"/> + + <hbox align="center"> + <label id="actualDiskCacheSize" flex="1"/> + <button id="clearCacheButton" icon="clear" + label="&clearCacheNow.label;" accesskey="&clearCacheNow.accesskey;" + oncommand="gAdvancedPane.clearCache();"/> + </hbox> + <checkbox preference="browser.cache.disk.smart_size.enabled" + id="allowSmartSize" flex="1" + onsyncfrompreference="return gAdvancedPane.readSmartSizeEnabled();" + label="&overrideSmartCacheSize.label;" + accesskey="&overrideSmartCacheSize.accesskey;"/> + <hbox align="center" class="indent"> + <label id="useCacheBefore" control="cacheSize" + accesskey="&limitCacheSizeBefore.accesskey;" + value="&limitCacheSizeBefore.label;"/> + <textbox id="cacheSize" type="number" size="4" max="1024" + preference="browser.cache.disk.capacity" + onsyncfrompreference="return gAdvancedPane.readCacheSize();" + onsynctopreference="return gAdvancedPane.writeCacheSize();" + aria-labelledby="useCacheBefore cacheSize useCacheAfter"/> + <label id="useCacheAfter" flex="1">&limitCacheSizeAfter.label;</label> + </hbox> + </groupbox> + + <!-- Offline apps --> + <groupbox id="offlineGroup"> + <caption label="&offlineStorage2.label;"/> + + <hbox align="center"> + <label id="actualAppCacheSize" flex="1"/> + <button id="clearOfflineAppCacheButton" icon="clear" + label="&clearOfflineAppCacheNow.label;" accesskey="&clearOfflineAppCacheNow.accesskey;" + oncommand="gAdvancedPane.clearOfflineAppCache();"/> + </hbox> + <label id="offlineAppsPermsLabel">&offlineAppsPermissions.label;</label> + <hbox align="center"> + <menulist id="offlineAppsPerms-menu" preference="offline-apps.permissions" sizetopopup="always"> + <menupopup> + <menuitem label="&offlineAppsPermissions.Allow;" value="2" /> + <menuitem label="&offlineAppsPermissions.Ask;" value="1" /> + <menuitem label="&offlineAppsPermissions.Deny;" value="0" /> + </menupopup> + </menulist> + <spacer flex="1"/> + <button id="offlineNotifyExceptions" + label="&offlineNotifyExceptions.label;" + accesskey="&offlineNotifyExceptions.accesskey;" + oncommand="gAdvancedPane.showOfflineExceptions();"/> + </hbox> + <hbox> + <vbox flex="1"> + <label id="offlineAppsListLabel">&offlineAppsList2.label;</label> + <listbox id="offlineAppsList" + style="height: &offlineAppsList.height;;" + flex="1" + aria-labelledby="offlineAppsListLabel" + onselect="gAdvancedPane.offlineAppSelected(event);"> + </listbox> + </vbox> + <vbox pack="end"> + <button id="offlineAppsListRemove" + disabled="true" + label="&offlineAppsListRemove.label;" + accesskey="&offlineAppsListRemove.accesskey;" + oncommand="gAdvancedPane.removeOfflineApp();"/> + </vbox> + </hbox> + </groupbox> + </tabpanel> + + <!-- Update --> + <tabpanel id="updatePanel" orient="vertical"> +#ifdef MOZ_UPDATER + <groupbox id="updateApp"> + <caption label="&updateApp.label;"/> + <radiogroup id="updateRadioGroup" + oncommand="gAdvancedPane.updateWritePrefs();"> + <radio id="autoDesktop" + value="auto" + label="&updateAuto1.label;" + accesskey="&updateAuto1.accesskey;"/> + <hbox class="indent"> + <checkbox id="warnIncompatible" + label="&updateAutoAddonWarn.label;" + accesskey="&updateAutoAddonWarn.accesskey;" + preference="app.update.mode" + onsyncfrompreference="return gAdvancedPane.readAddonWarn();" + onsynctopreference="return gAdvancedPane.writeAddonWarn();"/> + </hbox> + <radio value="checkOnly" + label="&updateCheck.label;" + accesskey="&updateCheck.accesskey;"/> + <radio value="manual" + label="&updateManual.label;" + accesskey="&updateManual.accesskey;"/> + </radiogroup> + + <hbox> + <button id="showUpdateHistory" + label="&updateHistory.label;" + accesskey="&updateHistory.accesskey;" + preference="app.update.disable_button.showUpdateHistory" + oncommand="gAdvancedPane.showUpdates();"/> + </hbox> + </groupbox> +#endif + <groupbox id="updateOthers"> + <caption label="&updateOthers.label;"/> + <checkbox id="enableSearchUpdate" + label="&enableSearchUpdate.label;" + accesskey="&enableSearchUpdate.accesskey;" + preference="browser.search.update"/> + </groupbox> + </tabpanel> + + <!-- Certificates --> + <tabpanel id="encryptionPanel" orient="vertical"> + + <!-- + The values on these radio buttons may look like l12y issues, but + they're not - this preference uses *those strings* as its values. + I KID YOU NOT. + --> + + <groupbox> + <caption label="&certGroup.label;"/> + <description id="CertSelectionDesc" control="certSelection">&certSelection.description;</description> + <radiogroup id="certSelection" orient="horizontal" preftype="string" + preference="security.default_personal_cert" + aria-labelledby="CertSelectionDesc"> + <radio label="&certs.auto;" accesskey="&certs.auto.accesskey;" + value="Select Automatically"/> + <radio label="&certs.ask;" accesskey="&certs.ask.accesskey;" + value="Ask Every Time"/> + </radiogroup> + </groupbox> + <groupbox> + <caption label="&ocspGroup.label;"/> + <checkbox id="enableOCSP" + label="&enableOCSP.label;" + accesskey="&enableOCSP.accesskey;" + onsyncfrompreference="return gAdvancedPane.readEnableOCSP();" + onsynctopreference="return gAdvancedPane.writeEnableOCSP();" + preference="security.OCSP.enabled"/> + <checkbox id="requireOCSP" + label="&requireOCSP.label;" + accesskey="&requireOCSP.accesskey;" + preference="security.OCSP.require"/> + </groupbox> + + <separator/> + + <hbox> + <button id="viewCertificatesButton" + label="&viewCerts.label;" accesskey="&viewCerts.accesskey;" + oncommand="gAdvancedPane.showCertificates();" + preference="security.disable_button.openCertManager"/> + <button id="viewSecurityDevicesButton" + label="&viewSecurityDevices.label;" accesskey="&viewSecurityDevices.accesskey;" + oncommand="gAdvancedPane.showSecurityDevices();" + preference="security.disable_button.openDeviceManager"/> + </hbox> + </tabpanel> + + <!-- Pale Moon: Scrolling tab --> + <tabpanel id="scrollparamTab" orient="vertical"> + + <checkbox id="useSmoothScrolling" + label="&useSmoothScrolling.label;" + accesskey="&useSmoothScrolling.accesskey;" + preference="general.smoothScroll"/> + + <label>&smoothscroll.explain.label;</label> + + <groupbox> + <caption label="&smoothscroll.params.label;"/> + + <checkbox label="&smoothscroll.mousewheel.label;" preference="general.smoothScroll.mouseWheel"/> + <hbox align="center" class="indent"> + <label value="&smoothscroll.mousewheel.duration;"/> + <textbox type="number" size="3" max="500" + preference="general.smoothScroll.mouseWheel.durationMinMS"/> + <label>&smoothscroll.to;</label> + <textbox type="number" size="4" max="2000" + preference="general.smoothScroll.mouseWheel.durationMaxMS"/> + <label flex="1">ms.</label> + </hbox> + + <checkbox label="&smoothscroll.arrowkeys.label;" preference="general.smoothScroll.lines"/> + <hbox align="center" class="indent"> + <label value="&smoothscroll.arrowkeys.duration;"/> + <textbox type="number" size="3" max="500" + preference="general.smoothScroll.lines.durationMinMS"/> + <label>&smoothscroll.to;</label> + <textbox type="number" size="4" max="2000" + preference="general.smoothScroll.lines.durationMaxMS"/> + <label flex="1">ms.</label> + </hbox> + + <checkbox label="&smoothscroll.pagekeys.label;" preference="general.smoothScroll.pages"/> + <hbox align="center" class="indent"> + <label value="&smoothscroll.pagekeys.duration;"/> + <textbox type="number" size="3" max="500" + preference="general.smoothScroll.pages.durationMinMS"/> + <label>&smoothscroll.to;</label> + <textbox type="number" size="4" max="2000" + preference="general.smoothScroll.pages.durationMaxMS"/> + <label flex="1">ms.</label> + </hbox> + + <checkbox label="&smoothscroll.scrollbar.label;" preference="general.smoothScroll.scrollbars"/> + <hbox align="center" class="indent"> + <label value="&smoothscroll.scrollbar.duration;"/> + <textbox type="number" size="3" max="500" + preference="general.smoothScroll.scrollbars.durationMinMS"/> + <label>&smoothscroll.to;</label> + <textbox type="number" size="4" max="2000" + preference="general.smoothScroll.scrollbars.durationMaxMS"/> + <label flex="1">ms.</label> + </hbox> + + <hbox align="center"> + <label value="&smoothscroll.overall.yspeed.label;"/> + <textbox type="number" size="3" min="1" max="999" + preference="mousewheel.default.delta_multiplier_y"/> + <label flex="1">%.</label> + </hbox> + </groupbox> + </tabpanel> + <!-- end Smooth scrolling tab --> + + </tabpanels> + </tabbox> + </prefpane> + +</overlay> diff --git a/browser/components/preferences/applicationManager.js b/browser/components/preferences/applicationManager.js new file mode 100644 index 000000000..43558c156 --- /dev/null +++ b/browser/components/preferences/applicationManager.js @@ -0,0 +1,97 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +var gAppManagerDialog = { + _removed: [], + + init: function() { + this.handlerInfo = window.arguments[0]; + + var bundle = document.getElementById("appManagerBundle"); + var contentText; + if (this.handlerInfo.type == TYPE_MAYBE_FEED) + contentText = bundle.getString("handleWebFeeds"); + else { + var description = gApplicationsPane._describeType(this.handlerInfo); + var key = + (this.handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) ? "handleFile" + : "handleProtocol"; + contentText = bundle.getFormattedString(key, [description]); + } + contentText = bundle.getFormattedString("descriptionApplications", [contentText]); + document.getElementById("appDescription").textContent = contentText; + + var list = document.getElementById("appList"); + var apps = this.handlerInfo.possibleApplicationHandlers.enumerate(); + while (apps.hasMoreElements()) { + let app = apps.getNext(); + if (!gApplicationsPane.isValidHandlerApp(app)) + continue; + + app.QueryInterface(Ci.nsIHandlerApp); + var item = list.appendItem(app.name); + item.setAttribute("image", gApplicationsPane._getIconURLForHandlerApp(app)); + item.className = "listitem-iconic"; + item.app = app; + } + + list.selectedIndex = 0; + }, + + onOK: function() { + if (!this._removed.length) { + // return early to avoid calling the |store| method. + return; + } + + for (var i = 0; i < this._removed.length; ++i) + this.handlerInfo.removePossibleApplicationHandler(this._removed[i]); + + this.handlerInfo.store(); + }, + + onCancel: function() { + // do nothing + }, + + remove: function() { + var list = document.getElementById("appList"); + this._removed.push(list.selectedItem.app); + var index = list.selectedIndex; + list.removeItemAt(index); + if (list.getRowCount() == 0) { + // The list is now empty, make the bottom part disappear + document.getElementById("appDetails").hidden = true; + } + else { + // Select the item at the same index, if we removed the last + // item of the list, select the previous item + if (index == list.getRowCount()) + --index; + list.selectedIndex = index; + } + }, + + onSelect: function() { + var list = document.getElementById("appList"); + if (!list.selectedItem) { + document.getElementById("remove").disabled = true; + return; + } + document.getElementById("remove").disabled = false; + var app = list.selectedItem.app; + var address = ""; + if (app instanceof Ci.nsILocalHandlerApp) + address = app.executable.path; + else if (app instanceof Ci.nsIWebHandlerApp) + address = app.uriTemplate; + else if (app instanceof Ci.nsIWebContentHandlerInfo) + address = app.uri; + document.getElementById("appLocation").value = address; + var bundle = document.getElementById("appManagerBundle"); + var appType = app instanceof Ci.nsILocalHandlerApp ? "descriptionLocalApp" + : "descriptionWebApp"; + document.getElementById("appType").value = bundle.getString(appType); + } +}; diff --git a/browser/components/preferences/applicationManager.xul b/browser/components/preferences/applicationManager.xul new file mode 100644 index 000000000..b5605c290 --- /dev/null +++ b/browser/components/preferences/applicationManager.xul @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/applicationManager.dtd"> + +<dialog id="appManager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + buttons="accept,cancel" + onload="gAppManagerDialog.init();" + ondialogaccept="gAppManagerDialog.onOK();" + ondialogcancel="gAppManagerDialog.onCancel();" + title="&appManager.title;" + style="&appManager.style;" + persist="screenX screenY"> + + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://browser/content/preferences/applicationManager.js"/> + <script type="application/javascript" + src="chrome://browser/content/preferences/applications.js"/> + + <commandset id="appManagerCommandSet"> + <command id="cmd_remove" + oncommand="gAppManagerDialog.remove();" + disabled="true"/> + </commandset> + + <keyset id="appManagerKeyset"> + <key id="delete" keycode="VK_DELETE" command="cmd_remove"/> + </keyset> + + <stringbundleset id="appManagerBundleset"> + <stringbundle id="appManagerBundle" + src="chrome://browser/locale/preferences/applicationManager.properties"/> + </stringbundleset> + + <description id="appDescription"/> + <separator class="thin"/> + <hbox flex="1"> + <listbox id="appList" onselect="gAppManagerDialog.onSelect();" flex="1"/> + <vbox> + <button id="remove" + label="&remove.label;" + accesskey="&remove.accesskey;" + command="cmd_remove"/> + <spacer flex="1"/> + </vbox> + </hbox> + <vbox id="appDetails"> + <separator class="thin"/> + <label id="appType"/> + <textbox id="appLocation" readonly="true" class="plain"/> + </vbox> +</dialog> diff --git a/browser/components/preferences/applications.js b/browser/components/preferences/applications.js new file mode 100644 index 000000000..3751ee732 --- /dev/null +++ b/browser/components/preferences/applications.js @@ -0,0 +1,1876 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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 & Enumeration Values + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +Components.utils.import('resource://gre/modules/Services.jsm'); + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; + +const PREF_DISABLED_PLUGIN_TYPES = "plugin.disable_full_page_plugin_for_types"; + +// Preferences that affect which entries to show in the list. +const PREF_SHOW_PLUGINS_IN_LIST = "browser.download.show_plugins_in_list"; +const PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS = + "browser.download.hide_plugins_without_extensions"; + +/* + * Preferences where we store handling information about the feed type. + * + * browser.feeds.handler + * - "bookmarks", "reader" (clarified further using the .default preference), + * or "ask" -- indicates the default handler being used to process feeds; + * "bookmarks" is obsolete; to specify that the handler is bookmarks, + * set browser.feeds.handler.default to "bookmarks"; + * + * browser.feeds.handler.default + * - "bookmarks", "client" or "web" -- indicates the chosen feed reader used + * to display feeds, either transiently (i.e., when the "use as default" + * checkbox is unchecked, corresponds to when browser.feeds.handler=="ask") + * or more permanently (i.e., the item displayed in the dropdown in Feeds + * preferences) + * + * browser.feeds.handler.webservice + * - the URL of the currently selected web service used to read feeds + * + * browser.feeds.handlers.application + * - nsILocalFile, stores the current client-side feed reading app if one has + * been chosen + */ +const PREF_FEED_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_FEED_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_FEED_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_FEED_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_FEED_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_FEED_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_FEED_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_FEED_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_FEED_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_FEED_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_FEED_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_FEED_SELECTED_READER = "browser.audioFeeds.handler.default"; + +// The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify +// the actions the application can take with content of various types. +// But since nsIHandlerInfo doesn't support plugins, there's no value +// identifying the "use plugin" action, so we use this constant instead. +const kActionUsePlugin = 5; + +/* +#ifdef MOZ_WIDGET_GTK +*/ +const ICON_URL_APP = "moz-icon://dummy.exe?size=16"; +/* +#else +*/ +const ICON_URL_APP = "chrome://browser/skin/preferences/application.png"; +/* +#endif +*/ + +// For CSS. Can be one of "ask", "save", "plugin" or "feed". If absent, the icon URL +// was set by us to a custom handler icon and CSS should not try to override it. +const APP_ICON_ATTR_NAME = "appHandlerIcon"; + +//****************************************************************************// +// Utilities + +function getFileDisplayName(file) { +#ifdef XP_WIN + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } +#endif + return file.leafName; +} + +function getLocalHandlerApp(aFile) { + var localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + localHandlerApp.name = getFileDisplayName(aFile); + localHandlerApp.executable = aFile; + + return localHandlerApp; +} + +/** + * An enumeration of items in a JS array. + * + * FIXME: use ArrayConverter once it lands (bug 380839). + * + * @constructor + */ +function ArrayEnumerator(aItems) { + this._index = 0; + this._contents = aItems; +} + +ArrayEnumerator.prototype = { + _index: 0, + + hasMoreElements: function() { + return this._index < this._contents.length; + }, + + getNext: function() { + return this._contents[this._index++]; + } +}; + +function isFeedType(t) { + return t == TYPE_MAYBE_FEED || t == TYPE_MAYBE_VIDEO_FEED || t == TYPE_MAYBE_AUDIO_FEED; +} + +//****************************************************************************// +// HandlerInfoWrapper + +/** + * This object wraps nsIHandlerInfo with some additional functionality + * the Applications prefpane needs to display and allow modification of + * the list of handled types. + * + * We create an instance of this wrapper for each entry we might display + * in the prefpane, and we compose the instances from various sources, + * including plugins and the handler service. + * + * We don't implement all the original nsIHandlerInfo functionality, + * just the stuff that the prefpane needs. + * + * In theory, all of the custom functionality in this wrapper should get + * pushed down into nsIHandlerInfo eventually. + */ +function HandlerInfoWrapper(aType, aHandlerInfo) { + this._type = aType; + this.wrappedHandlerInfo = aHandlerInfo; +} + +HandlerInfoWrapper.prototype = { + // The wrapped nsIHandlerInfo object. In general, this object is private, + // but there are a couple cases where callers access it directly for things + // we haven't (yet?) implemented, so we make it a public property. + wrappedHandlerInfo: null, + + + //**************************************************************************// + // Convenience Utils + + _handlerSvc: Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService), + + _prefSvc: Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch), + + _categoryMgr: Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager), + + element: function(aID) { + return document.getElementById(aID); + }, + + + //**************************************************************************// + // nsIHandlerInfo + + // The MIME type or protocol scheme. + _type: null, + get type() { + return this._type; + }, + + get description() { + if (this.wrappedHandlerInfo.description) + return this.wrappedHandlerInfo.description; + + if (this.primaryExtension) { + var extension = this.primaryExtension.toUpperCase(); + return this.element("bundlePreferences").getFormattedString("fileEnding", + [extension]); + } + + return this.type; + }, + + get preferredApplicationHandler() { + return this.wrappedHandlerInfo.preferredApplicationHandler; + }, + + set preferredApplicationHandler(aNewValue) { + this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue; + + // Make sure the preferred handler is in the set of possible handlers. + if (aNewValue) + this.addPossibleApplicationHandler(aNewValue) + }, + + get possibleApplicationHandlers() { + return this.wrappedHandlerInfo.possibleApplicationHandlers; + }, + + addPossibleApplicationHandler: function(aNewHandler) { + var possibleApps = this.possibleApplicationHandlers.enumerate(); + while (possibleApps.hasMoreElements()) { + if (possibleApps.getNext().equals(aNewHandler)) + return; + } + this.possibleApplicationHandlers.appendElement(aNewHandler, false); + }, + + removePossibleApplicationHandler: function(aHandler) { + var defaultApp = this.preferredApplicationHandler; + if (defaultApp && aHandler.equals(defaultApp)) { + // If the app we remove was the default app, we must make sure + // it won't be used anymore + this.alwaysAskBeforeHandling = true; + this.preferredApplicationHandler = null; + } + + var handlers = this.possibleApplicationHandlers; + for (var i = 0; i < handlers.length; ++i) { + var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if (handler.equals(aHandler)) { + handlers.removeElementAt(i); + break; + } + } + }, + + get hasDefaultHandler() { + return this.wrappedHandlerInfo.hasDefaultHandler; + }, + + get defaultDescription() { + return this.wrappedHandlerInfo.defaultDescription; + }, + + // What to do with content of this type. + get preferredAction() { + // If we have an enabled plugin, then the action is to use that plugin. + if (this.pluginName && !this.isDisabledPluginType) + return kActionUsePlugin; + + // If the action is to use a helper app, but we don't have a preferred + // handler app, then switch to using the system default, if any; otherwise + // fall back to saving to disk, which is the default action in nsMIMEInfo. + // Note: "save to disk" is an invalid value for protocol info objects, + // but the alwaysAskBeforeHandling getter will detect that situation + // and always return true in that case to override this invalid value. + if (this.wrappedHandlerInfo.preferredAction == Ci.nsIHandlerInfo.useHelperApp && + !gApplicationsPane.isValidHandlerApp(this.preferredApplicationHandler)) { + if (this.wrappedHandlerInfo.hasDefaultHandler) + return Ci.nsIHandlerInfo.useSystemDefault; + else + return Ci.nsIHandlerInfo.saveToDisk; + } + + return this.wrappedHandlerInfo.preferredAction; + }, + + set preferredAction(aNewValue) { + // If the action is to use the plugin, + // we must set the preferred action to "save to disk". + // But only if it's not currently the preferred action. + if ((aNewValue == kActionUsePlugin) && + (this.preferredAction != Ci.nsIHandlerInfo.saveToDisk)) { + aNewValue = Ci.nsIHandlerInfo.saveToDisk; + } + + // We don't modify the preferred action if the new action is to use a plugin + // because handler info objects don't understand our custom "use plugin" + // value. Also, leaving it untouched means that we can automatically revert + // to the old setting if the user ever removes the plugin. + + if (aNewValue != kActionUsePlugin) + this.wrappedHandlerInfo.preferredAction = aNewValue; + }, + + get alwaysAskBeforeHandling() { + // If this type is handled only by a plugin, we can't trust the value + // in the handler info object, since it'll be a default based on the absence + // of any user configuration, and the default in that case is to always ask, + // even though we never ask for content handled by a plugin, so special case + // plugin-handled types by returning false here. + if (this.pluginName && this.handledOnlyByPlugin) + return false; + + // If this is a protocol type and the preferred action is "save to disk", + // which is invalid for such types, then return true here to override that + // action. This could happen when the preferred action is to use a helper + // app, but the preferredApplicationHandler is invalid, and there isn't + // a default handler, so the preferredAction getter returns save to disk + // instead. + if (!(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) && + this.preferredAction == Ci.nsIHandlerInfo.saveToDisk) + return true; + + return this.wrappedHandlerInfo.alwaysAskBeforeHandling; + }, + + set alwaysAskBeforeHandling(aNewValue) { + this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue; + }, + + + //**************************************************************************// + // nsIMIMEInfo + + // The primary file extension associated with this type, if any. + // + // XXX Plugin objects contain an array of MimeType objects with "suffixes" + // properties; if this object has an associated plugin, shouldn't we check + // those properties for an extension? + get primaryExtension() { + try { + if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + this.wrappedHandlerInfo.primaryExtension) + return this.wrappedHandlerInfo.primaryExtension + } catch(ex) {} + + return null; + }, + + + //**************************************************************************// + // Plugin Handling + + // A plugin that can handle this type, if any. + // + // Note: just because we have one doesn't mean it *will* handle the type. + // That depends on whether or not the type is in the list of types for which + // plugin handling is disabled. + plugin: null, + + // Whether or not this type is only handled by a plugin or is also handled + // by some user-configured action as specified in the handler info object. + // + // Note: we can't just check if there's a handler info object for this type, + // because OS and user configuration is mixed up in the handler info object, + // so we always need to retrieve it for the OS info and can't tell whether + // it represents only OS-default information or user-configured information. + // + // FIXME: once handler info records are broken up into OS-provided records + // and user-configured records, stop using this boolean flag and simply + // check for the presence of a user-configured record to determine whether + // or not this type is only handled by a plugin. Filed as bug 395142. + handledOnlyByPlugin: undefined, + + get isDisabledPluginType() { + return this._getDisabledPluginTypes().indexOf(this.type) != -1; + }, + + _getDisabledPluginTypes: function() { + var types = ""; + + if (this._prefSvc.prefHasUserValue(PREF_DISABLED_PLUGIN_TYPES)) + types = this._prefSvc.getCharPref(PREF_DISABLED_PLUGIN_TYPES); + + // Only split if the string isn't empty so we don't end up with an array + // containing a single empty string. + if (types != "") + return types.split(","); + + return []; + }, + + disablePluginType: function() { + var disabledPluginTypes = this._getDisabledPluginTypes(); + + if (disabledPluginTypes.indexOf(this.type) == -1) + disabledPluginTypes.push(this.type); + + this._prefSvc.setCharPref(PREF_DISABLED_PLUGIN_TYPES, + disabledPluginTypes.join(",")); + + // Update the category manager so existing browser windows update. + this._categoryMgr.deleteCategoryEntry("Goanna-Content-Viewers", + this.type, + false); + }, + + enablePluginType: function() { + var disabledPluginTypes = this._getDisabledPluginTypes(); + + var type = this.type; + disabledPluginTypes = disabledPluginTypes.filter(function(v) v != type); + + this._prefSvc.setCharPref(PREF_DISABLED_PLUGIN_TYPES, + disabledPluginTypes.join(",")); + + // Update the category manager so existing browser windows update. + this._categoryMgr. + addCategoryEntry("Goanna-Content-Viewers", + this.type, + "@mozilla.org/content/plugin/document-loader-factory;1", + false, + true); + }, + + + //**************************************************************************// + // Storage + + store: function() { + this._handlerSvc.store(this.wrappedHandlerInfo); + }, + + + //**************************************************************************// + // Icons + + get smallIcon() { + return this._getIcon(16); + }, + + _getIcon: function(aSize) { + if (this.primaryExtension) + return "moz-icon://goat." + this.primaryExtension + "?size=" + aSize; + + if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) + return "moz-icon://goat?size=" + aSize + "&contentType=" + this.type; + + // FIXME: consider returning some generic icon when we can't get a URL for + // one (for example in the case of protocol schemes). Filed as bug 395141. + return null; + } + +}; + + +//****************************************************************************// +// Feed Handler Info + +/** + * This object implements nsIHandlerInfo for the feed types. It's a separate + * object because we currently store handling information for the feed type + * in a set of preferences rather than the nsIHandlerService-managed datastore. + * + * This object inherits from HandlerInfoWrapper in order to get functionality + * that isn't special to the feed type. + * + * XXX Should we inherit from HandlerInfoWrapper? After all, we override + * most of that wrapper's properties and methods, and we have to dance around + * the fact that the wrapper expects to have a wrappedHandlerInfo, which we + * don't provide. + */ + +function FeedHandlerInfo(aMIMEType) { + HandlerInfoWrapper.call(this, aMIMEType, null); +} + +FeedHandlerInfo.prototype = { + __proto__: HandlerInfoWrapper.prototype, + + //**************************************************************************// + // Convenience Utils + + _converterSvc: + Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService), + + _shellSvc: +#ifdef HAVE_SHELL_SERVICE + getShellService(), +#else + null, +#endif + + + //**************************************************************************// + // nsIHandlerInfo + + get description() { + return this.element("bundlePreferences").getString(this._appPrefLabel); + }, + + get preferredApplicationHandler() { + switch (this.element(this._prefSelectedReader).value) { + case "client": + var file = this.element(this._prefSelectedApp).value; + if (file) + return getLocalHandlerApp(file); + + return null; + + case "web": + var uri = this.element(this._prefSelectedWeb).value; + if (!uri) + return null; + return this._converterSvc.getWebContentHandlerByURI(this.type, uri); + + case "bookmarks": + default: + // When the pref is set to bookmarks, we handle feeds internally, + // we don't forward them to a local or web handler app, so there is + // no preferred handler. + return null; + } + }, + + set preferredApplicationHandler(aNewValue) { + if (aNewValue instanceof Ci.nsILocalHandlerApp) { + this.element(this._prefSelectedApp).value = aNewValue.executable; + this.element(this._prefSelectedReader).value = "client"; + } + else if (aNewValue instanceof Ci.nsIWebContentHandlerInfo) { + this.element(this._prefSelectedWeb).value = aNewValue.uri; + this.element(this._prefSelectedReader).value = "web"; + // Make the web handler be the new "auto handler" for feeds. + // Note: we don't have to unregister the auto handler when the user picks + // a non-web handler (local app, Live Bookmarks, etc.) because the service + // only uses the "auto handler" when the selected reader is a web handler. + // We also don't have to unregister it when the user turns on "always ask" + // (i.e. preview in browser), since that also overrides the auto handler. + this._converterSvc.setAutoHandler(this.type, aNewValue); + } + }, + + _possibleApplicationHandlers: null, + + get possibleApplicationHandlers() { + if (this._possibleApplicationHandlers) + return this._possibleApplicationHandlers; + + // A minimal implementation of nsIMutableArray. It only supports the two + // methods its callers invoke, namely appendElement and nsIArray::enumerate. + this._possibleApplicationHandlers = { + _inner: [], + _removed: [], + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIMutableArray) || + aIID.equals(Ci.nsIArray) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + get length() { + return this._inner.length; + }, + + enumerate: function() { + return new ArrayEnumerator(this._inner); + }, + + appendElement: function(aHandlerApp, aWeak) { + this._inner.push(aHandlerApp); + }, + + removeElementAt: function(aIndex) { + this._removed.push(this._inner[aIndex]); + this._inner.splice(aIndex, 1); + }, + + queryElementAt: function(aIndex, aInterface) { + return this._inner[aIndex].QueryInterface(aInterface); + } + }; + + // Add the selected local app if it's different from the OS default handler. + // Unlike for other types, we can store only one local app at a time for the + // feed type, since we store it in a preference that historically stores + // only a single path. But we display all the local apps the user chooses + // while the prefpane is open, only dropping the list when the user closes + // the prefpane, for maximum usability and consistency with other types. + var preferredAppFile = this.element(this._prefSelectedApp).value; + if (preferredAppFile) { + let preferredApp = getLocalHandlerApp(preferredAppFile); + let defaultApp = this._defaultApplicationHandler; + if (!defaultApp || !defaultApp.equals(preferredApp)) + this._possibleApplicationHandlers.appendElement(preferredApp, false); + } + + // Add the registered web handlers. There can be any number of these. + var webHandlers = this._converterSvc.getContentHandlers(this.type); + for each (let webHandler in webHandlers) + this._possibleApplicationHandlers.appendElement(webHandler, false); + + return this._possibleApplicationHandlers; + }, + + __defaultApplicationHandler: undefined, + get _defaultApplicationHandler() { + if (typeof this.__defaultApplicationHandler != "undefined") + return this.__defaultApplicationHandler; + + var defaultFeedReader = null; +#ifdef HAVE_SHELL_SERVICE + try { + defaultFeedReader = this._shellSvc.defaultFeedReader; + } + catch(ex) { + // no default reader or _shellSvc is null + } +#endif + + if (defaultFeedReader) { + let handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsIHandlerApp); + handlerApp.name = getFileDisplayName(defaultFeedReader); + handlerApp.QueryInterface(Ci.nsILocalHandlerApp); + handlerApp.executable = defaultFeedReader; + + this.__defaultApplicationHandler = handlerApp; + } + else { + this.__defaultApplicationHandler = null; + } + + return this.__defaultApplicationHandler; + }, + + get hasDefaultHandler() { +#ifdef HAVE_SHELL_SERVICE + try { + if (this._shellSvc.defaultFeedReader) + return true; + } + catch(ex) { + // no default reader or _shellSvc is null + } +#endif + + return false; + }, + + get defaultDescription() { + if (this.hasDefaultHandler) + return this._defaultApplicationHandler.name; + + // Should we instead return null? + return ""; + }, + + // What to do with content of this type. + get preferredAction() { + switch (this.element(this._prefSelectedAction).value) { + + case "bookmarks": + return Ci.nsIHandlerInfo.handleInternally; + + case "reader": { + let preferredApp = this.preferredApplicationHandler; + let defaultApp = this._defaultApplicationHandler; + + // If we have a valid preferred app, return useSystemDefault if it's + // the default app; otherwise return useHelperApp. + if (gApplicationsPane.isValidHandlerApp(preferredApp)) { + if (defaultApp && defaultApp.equals(preferredApp)) + return Ci.nsIHandlerInfo.useSystemDefault; + + return Ci.nsIHandlerInfo.useHelperApp; + } + + // The pref is set to "reader", but we don't have a valid preferred app. + // What do we do now? Not sure this is the best option (perhaps we + // should direct the user to the default app, if any), but for now let's + // direct the user to live bookmarks. + return Ci.nsIHandlerInfo.handleInternally; + } + + // If the action is "ask", then alwaysAskBeforeHandling will override + // the action, so it doesn't matter what we say it is, it just has to be + // something that doesn't cause the controller to hide the type. + case "ask": + default: + return Ci.nsIHandlerInfo.handleInternally; + } + }, + + set preferredAction(aNewValue) { + switch (aNewValue) { + + case Ci.nsIHandlerInfo.handleInternally: + this.element(this._prefSelectedReader).value = "bookmarks"; + break; + + case Ci.nsIHandlerInfo.useHelperApp: + this.element(this._prefSelectedAction).value = "reader"; + // The controller has already set preferredApplicationHandler + // to the new helper app. + break; + + case Ci.nsIHandlerInfo.useSystemDefault: + this.element(this._prefSelectedAction).value = "reader"; + this.preferredApplicationHandler = this._defaultApplicationHandler; + break; + } + }, + + get alwaysAskBeforeHandling() { + return this.element(this._prefSelectedAction).value == "ask"; + }, + + set alwaysAskBeforeHandling(aNewValue) { + if (aNewValue == true) + this.element(this._prefSelectedAction).value = "ask"; + else + this.element(this._prefSelectedAction).value = "reader"; + }, + + // Whether or not we are currently storing the action selected by the user. + // We use this to suppress notification-triggered updates to the list when + // we make changes that may spawn such updates, specifically when we change + // the action for the feed type, which results in feed preference updates, + // which spawn "pref changed" notifications that would otherwise cause us + // to rebuild the view unnecessarily. + _storingAction: false, + + + //**************************************************************************// + // nsIMIMEInfo + + get primaryExtension() { + return "xml"; + }, + + + //**************************************************************************// + // Storage + + // Changes to the preferred action and handler take effect immediately + // (we write them out to the preferences right as they happen), + // so we when the controller calls store() after modifying the handlers, + // the only thing we need to store is the removal of possible handlers + // XXX Should we hold off on making the changes until this method gets called? + store: function() { + for each (let app in this._possibleApplicationHandlers._removed) { + if (app instanceof Ci.nsILocalHandlerApp) { + let pref = this.element(PREF_FEED_SELECTED_APP); + var preferredAppFile = pref.value; + if (preferredAppFile) { + let preferredApp = getLocalHandlerApp(preferredAppFile); + if (app.equals(preferredApp)) + pref.reset(); + } + } + else { + app.QueryInterface(Ci.nsIWebContentHandlerInfo); + this._converterSvc.removeContentHandler(app.contentType, app.uri); + } + } + this._possibleApplicationHandlers._removed = []; + }, + + + //**************************************************************************// + // Icons + + get smallIcon() { + return this._smallIcon; + } + +}; + +var feedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_FEED), + _prefSelectedApp: PREF_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/feedIcon16.png", + _appPrefLabel: "webFeed" +} + +var videoFeedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_VIDEO_FEED), + _prefSelectedApp: PREF_VIDEO_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_VIDEO_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_VIDEO_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_VIDEO_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/videoFeedIcon16.png", + _appPrefLabel: "videoPodcastFeed" +} + +var audioFeedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_AUDIO_FEED), + _prefSelectedApp: PREF_AUDIO_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_AUDIO_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_AUDIO_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_AUDIO_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/audioFeedIcon16.png", + _appPrefLabel: "audioPodcastFeed" +} + +/** + * InternalHandlerInfoWrapper provides a basic mechanism to create an internal + * mime type handler that can be enabled/disabled in the applications preference + * menu. + */ +function InternalHandlerInfoWrapper(aMIMEType) { + var mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + var handlerInfo = mimeSvc.getFromTypeAndExtension(aMIMEType, null); + + HandlerInfoWrapper.call(this, aMIMEType, handlerInfo); +} + +InternalHandlerInfoWrapper.prototype = { + __proto__: HandlerInfoWrapper.prototype, + + // Override store so we so we can notify any code listening for registration + // or unregistration of this handler. + store: function() { + HandlerInfoWrapper.prototype.store.call(this); + Services.obs.notifyObservers(null, this._handlerChanged, null); + }, + + get enabled() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + get description() { + return this.element("bundlePreferences").getString(this._appPrefLabel); + } +}; + +//****************************************************************************// +// Prefpane Controller + +var gApplicationsPane = { + // The set of types the app knows how to handle. A hash of HandlerInfoWrapper + // objects, indexed by type. + _handledTypes: {}, + + // The list of types we can show, sorted by the sort column/direction. + // An array of HandlerInfoWrapper objects. We build this list when we first + // load the data and then rebuild it when users change a pref that affects + // what types we can show or change the sort column/direction. + // Note: this isn't necessarily the list of types we *will* show; if the user + // provides a filter string, we'll only show the subset of types in this list + // that match that string. + _visibleTypes: [], + + // A count of the number of times each visible type description appears. + // We use these counts to determine whether or not to annotate descriptions + // with their types to distinguish duplicate descriptions from each other. + // A hash of integer counts, indexed by string description. + _visibleTypeDescriptionCount: {}, + + + //**************************************************************************// + // Convenience & Performance Shortcuts + + // These get defined by init(). + _brandShortName : null, + _prefsBundle : null, + _list : null, + _filter : null, + + _prefSvc : Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch), + + _mimeSvc : Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService), + + _helperAppSvc : Cc["@mozilla.org/uriloader/external-helper-app-service;1"]. + getService(Ci.nsIExternalHelperAppService), + + _handlerSvc : Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService), + + _ioSvc : Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService), + + + //**************************************************************************// + // Initialization & Destruction + + init: function() { + // Initialize shortcuts to some commonly accessed elements & values. + this._brandShortName = + document.getElementById("bundleBrand").getString("brandShortName"); + this._prefsBundle = document.getElementById("bundlePreferences"); + this._list = document.getElementById("handlersView"); + this._filter = document.getElementById("filter"); + + // Observe preferences that influence what we display so we can rebuild + // the view when they change. + this._prefSvc.addObserver(PREF_SHOW_PLUGINS_IN_LIST, this, false); + this._prefSvc.addObserver(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_READER, this, false); + + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_READER, this, false); + + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_READER, this, false); + + + // Listen for window unload so we can remove our preference observers. + window.addEventListener("unload", this, false); + + // Figure out how we should be sorting the list. We persist sort settings + // across sessions, so we can't assume the default sort column/direction. + // XXX should we be using the XUL sort service instead? + if (document.getElementById("actionColumn").hasAttribute("sortDirection")) { + this._sortColumn = document.getElementById("actionColumn"); + // The typeColumn element always has a sortDirection attribute, + // either because it was persisted or because the default value + // from the xul file was used. If we are sorting on the other + // column, we should remove it. + document.getElementById("typeColumn").removeAttribute("sortDirection"); + } + else + this._sortColumn = document.getElementById("typeColumn"); + + // Load the data and build the list of handlers. + // By doing this in a timeout, we let the preferences dialog resize itself + // to an appropriate size before we add a bunch of items to the list. + // Otherwise, if there are many items, and the Applications prefpane + // is the one that gets displayed when the user first opens the dialog, + // the dialog might stretch too much in an attempt to fit them all in. + // XXX Shouldn't we perhaps just set a max-height on the richlistbox? + var _delayedPaneLoad = function(self) { + self._loadData(); + self._rebuildVisibleTypes(); + self._sortVisibleTypes(); + self._rebuildView(); + + // Notify observers that the UI is now ready + Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService). + notifyObservers(window, "app-handler-pane-loaded", null); + } + setTimeout(_delayedPaneLoad, 0, this); + }, + + destroy: function() { + window.removeEventListener("unload", this, false); + this._prefSvc.removeObserver(PREF_SHOW_PLUGINS_IN_LIST, this); + this._prefSvc.removeObserver(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_READER, this); + + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_READER, this); + + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_READER, this); + }, + + + //**************************************************************************// + // nsISupports + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIObserver) || + aIID.equals(Ci.nsIDOMEventListener || + aIID.equals(Ci.nsISupports))) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + //**************************************************************************// + // nsIObserver + + observe: function (aSubject, aTopic, aData) { + // Rebuild the list when there are changes to preferences that influence + // whether or not to show certain entries in the list. + if (aTopic == "nsPref:changed" && !this._storingAction) { + // These two prefs alter the list of visible types, so we have to rebuild + // that list when they change. + if (aData == PREF_SHOW_PLUGINS_IN_LIST || + aData == PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS) { + this._rebuildVisibleTypes(); + this._sortVisibleTypes(); + } + + // All the prefs we observe can affect what we display, so we rebuild + // the view when any of them changes. + this._rebuildView(); + } + }, + + + //**************************************************************************// + // nsIDOMEventListener + + handleEvent: function(aEvent) { + if (aEvent.type == "unload") { + this.destroy(); + } + }, + + + //**************************************************************************// + // Composed Model Construction + + _loadData: function() { + this._loadFeedHandler(); + this._loadPluginHandlers(); + this._loadApplicationHandlers(); + }, + + _loadFeedHandler: function() { + this._handledTypes[TYPE_MAYBE_FEED] = feedHandlerInfo; + feedHandlerInfo.handledOnlyByPlugin = false; + + this._handledTypes[TYPE_MAYBE_VIDEO_FEED] = videoFeedHandlerInfo; + videoFeedHandlerInfo.handledOnlyByPlugin = false; + + this._handledTypes[TYPE_MAYBE_AUDIO_FEED] = audioFeedHandlerInfo; + audioFeedHandlerInfo.handledOnlyByPlugin = false; + }, + + /** + * Load the set of handlers defined by plugins. + * + * Note: if there's more than one plugin for a given MIME type, we assume + * the last one is the one that the application will use. That may not be + * correct, but it's how we've been doing it for years. + * + * Perhaps we should instead query navigator.mimeTypes for the set of types + * supported by the application and then get the plugin from each MIME type's + * enabledPlugin property. But if there's a plugin for a type, we need + * to know about it even if it isn't enabled, since we're going to give + * the user an option to enable it. + * + * Also note that enabledPlugin does not get updated when + * plugin.disable_full_page_plugin_for_types changes, so even if we could use + * enabledPlugin to get the plugin that would be used, we'd still need to + * check the pref ourselves to find out if it's enabled. + */ + _loadPluginHandlers: function() { + "use strict"; + + let mimeTypes = navigator.mimeTypes; + + for (let mimeType of mimeTypes) { + let handlerInfoWrapper; + if (mimeType.type in this._handledTypes) { + handlerInfoWrapper = this._handledTypes[mimeType.type]; + } else { + let wrappedHandlerInfo = + this._mimeSvc.getFromTypeAndExtension(mimeType.type, null); + handlerInfoWrapper = new HandlerInfoWrapper(mimeType.type, wrappedHandlerInfo); + handlerInfoWrapper.handledOnlyByPlugin = true; + this._handledTypes[mimeType.type] = handlerInfoWrapper; + } + handlerInfoWrapper.pluginName = mimeType.enabledPlugin.name; + } + }, + + /** + * Load the set of handlers defined by the application datastore. + */ + _loadApplicationHandlers: function() { + var wrappedHandlerInfos = this._handlerSvc.enumerate(); + while (wrappedHandlerInfos.hasMoreElements()) { + let wrappedHandlerInfo = + wrappedHandlerInfos.getNext().QueryInterface(Ci.nsIHandlerInfo); + let type = wrappedHandlerInfo.type; + + let handlerInfoWrapper; + if (type in this._handledTypes) + handlerInfoWrapper = this._handledTypes[type]; + else { + handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo); + this._handledTypes[type] = handlerInfoWrapper; + } + + handlerInfoWrapper.handledOnlyByPlugin = false; + } + }, + + + //**************************************************************************// + // View Construction + + _rebuildVisibleTypes: function() { + // Reset the list of visible types and the visible type description counts. + this._visibleTypes = []; + this._visibleTypeDescriptionCount = {}; + + // Get the preferences that help determine what types to show. + var showPlugins = this._prefSvc.getBoolPref(PREF_SHOW_PLUGINS_IN_LIST); + var hidePluginsWithoutExtensions = + this._prefSvc.getBoolPref(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS); + + for (let type in this._handledTypes) { + let handlerInfo = this._handledTypes[type]; + + // Hide plugins without associated extensions if so prefed so we don't + // show a whole bunch of obscure types handled by plugins on Mac. + // Note: though protocol types don't have extensions, we still show them; + // the pref is only meant to be applied to MIME types, since plugins are + // only associated with MIME types. + // FIXME: should we also check the "suffixes" property of the plugin? + // Filed as bug 395135. + if (hidePluginsWithoutExtensions && handlerInfo.handledOnlyByPlugin && + handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + !handlerInfo.primaryExtension) + continue; + + // Hide types handled only by plugins if so prefed. + if (handlerInfo.handledOnlyByPlugin && !showPlugins) + continue; + + // We couldn't find any reason to exclude the type, so include it. + this._visibleTypes.push(handlerInfo); + + if (handlerInfo.description in this._visibleTypeDescriptionCount) + this._visibleTypeDescriptionCount[handlerInfo.description]++; + else + this._visibleTypeDescriptionCount[handlerInfo.description] = 1; + } + }, + + _rebuildView: function() { + // Clear the list of entries. + while (this._list.childNodes.length > 1) + this._list.removeChild(this._list.lastChild); + + var visibleTypes = this._visibleTypes; + + // If the user is filtering the list, then only show matching types. + if (this._filter.value) + visibleTypes = visibleTypes.filter(this._matchesFilter, this); + + for each (let visibleType in visibleTypes) { + let item = document.createElement("richlistitem"); + item.setAttribute("type", visibleType.type); + item.setAttribute("typeDescription", this._describeType(visibleType)); + if (visibleType.smallIcon) + item.setAttribute("typeIcon", visibleType.smallIcon); + item.setAttribute("actionDescription", + this._describePreferredAction(visibleType)); + + if (!this._setIconClassForPreferredAction(visibleType, item)) { + item.setAttribute("actionIcon", + this._getIconURLForPreferredAction(visibleType)); + } + + this._list.appendChild(item); + } + + this._selectLastSelectedType(); + }, + + _matchesFilter: function(aType) { + var filterValue = this._filter.value.toLowerCase(); + return this._describeType(aType).toLowerCase().indexOf(filterValue) != -1 || + this._describePreferredAction(aType).toLowerCase().indexOf(filterValue) != -1; + }, + + /** + * Describe, in a human-readable fashion, the type represented by the given + * handler info object. Normally this is just the description provided by + * the info object, but if more than one object presents the same description, + * then we annotate the duplicate descriptions with the type itself to help + * users distinguish between those types. + * + * @param aHandlerInfo {nsIHandlerInfo} the type being described + * @returns {string} a description of the type + */ + _describeType: function(aHandlerInfo) { + if (this._visibleTypeDescriptionCount[aHandlerInfo.description] > 1) + return this._prefsBundle.getFormattedString("typeDescriptionWithType", + [aHandlerInfo.description, + aHandlerInfo.type]); + + return aHandlerInfo.description; + }, + + /** + * Describe, in a human-readable fashion, the preferred action to take on + * the type represented by the given handler info object. + * + * XXX Should this be part of the HandlerInfoWrapper interface? It would + * violate the separation of model and view, but it might make more sense + * nonetheless (f.e. it would make sortTypes easier). + * + * @param aHandlerInfo {nsIHandlerInfo} the type whose preferred action + * is being described + * @returns {string} a description of the action + */ + _describePreferredAction: function(aHandlerInfo) { + // alwaysAskBeforeHandling overrides the preferred action, so if that flag + // is set, then describe that behavior instead. For most types, this is + // the "alwaysAsk" string, but for the feed type we show something special. + if (aHandlerInfo.alwaysAskBeforeHandling) { + if (isFeedType(aHandlerInfo.type)) + return this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + else + return this._prefsBundle.getString("alwaysAsk"); + } + + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + return this._prefsBundle.getString("saveFile"); + + case Ci.nsIHandlerInfo.useHelperApp: + var preferredApp = aHandlerInfo.preferredApplicationHandler; + var name; + if (preferredApp instanceof Ci.nsILocalHandlerApp) + name = getFileDisplayName(preferredApp.executable); + else + name = preferredApp.name; + return this._prefsBundle.getFormattedString("useApp", [name]); + + case Ci.nsIHandlerInfo.handleInternally: + // For the feed type, handleInternally means live bookmarks. + if (isFeedType(aHandlerInfo.type)) { + return this._prefsBundle.getFormattedString("addLiveBookmarksInApp", + [this._brandShortName]); + } + + if (aHandlerInfo instanceof InternalHandlerInfoWrapper) { + return this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + } + + // For other types, handleInternally looks like either useHelperApp + // or useSystemDefault depending on whether or not there's a preferred + // handler app. + if (this.isValidHandlerApp(aHandlerInfo.preferredApplicationHandler)) + return aHandlerInfo.preferredApplicationHandler.name; + + return aHandlerInfo.defaultDescription; + + // XXX Why don't we say the app will handle the type internally? + // Is it because the app can't actually do that? But if that's true, + // then why would a preferredAction ever get set to this value + // in the first place? + + case Ci.nsIHandlerInfo.useSystemDefault: + return this._prefsBundle.getFormattedString("useDefault", + [aHandlerInfo.defaultDescription]); + + case kActionUsePlugin: + return this._prefsBundle.getFormattedString("usePluginIn", + [aHandlerInfo.pluginName, + this._brandShortName]); + } + }, + + _selectLastSelectedType: function() { + // If the list is disabled by the pref.downloads.disable_button.edit_actions + // preference being locked, then don't select the type, as that would cause + // it to appear selected, with a different background and an actions menu + // that makes it seem like you can choose an action for the type. + if (this._list.disabled) + return; + + var lastSelectedType = this._list.getAttribute("lastSelectedType"); + if (!lastSelectedType) + return; + + var item = this._list.getElementsByAttribute("type", lastSelectedType)[0]; + if (!item) + return; + + this._list.selectedItem = item; + }, + + /** + * Whether or not the given handler app is valid. + * + * @param aHandlerApp {nsIHandlerApp} the handler app in question + * + * @returns {boolean} whether or not it's valid + */ + isValidHandlerApp: function(aHandlerApp) { + if (!aHandlerApp) + return false; + + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) + return this._isValidHandlerExecutable(aHandlerApp.executable); + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) + return aHandlerApp.uriTemplate; + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo) + return aHandlerApp.uri; + + return false; + }, + + _isValidHandlerExecutable: function(aExecutable) { + return aExecutable && + aExecutable.exists() && + aExecutable.isExecutable() && +// XXXben - we need to compare this with the running instance executable +// just don't know how to do that via script... +// XXXmano TBD: can probably add this to nsIShellService +#ifdef XP_WIN +#expand aExecutable.leafName != "__MOZ_APP_NAME__.exe"; +#else +#expand aExecutable.leafName != "__MOZ_APP_NAME__-bin"; +#endif + }, + + /** + * Rebuild the actions menu for the selected entry. Gets called by + * the richlistitem constructor when an entry in the list gets selected. + */ + rebuildActionsMenu: function() { + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + var menu = + document.getAnonymousElementByAttribute(typeItem, "class", "actionsMenu"); + var menuPopup = menu.menupopup; + + // Clear out existing items. + while (menuPopup.hasChildNodes()) + menuPopup.removeChild(menuPopup.lastChild); + + // Add the "Preview in Firefox" option for optional internal handlers. + if (handlerInfo instanceof InternalHandlerInfoWrapper) { + var internalMenuItem = document.createElement("menuitem"); + internalMenuItem.setAttribute("action", Ci.nsIHandlerInfo.handleInternally); + let label = this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + internalMenuItem.setAttribute("label", label); + internalMenuItem.setAttribute("tooltiptext", label); + internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask"); + menuPopup.appendChild(internalMenuItem); + } + + { + var askMenuItem = document.createElement("menuitem"); + askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk); + let label; + if (isFeedType(handlerInfo.type)) + label = this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + else + label = this._prefsBundle.getString("alwaysAsk"); + askMenuItem.setAttribute("label", label); + askMenuItem.setAttribute("tooltiptext", label); + askMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask"); + menuPopup.appendChild(askMenuItem); + } + + // Create a menu item for saving to disk. + // Note: this option isn't available to protocol types, since we don't know + // what it means to save a URL having a certain scheme to disk, nor is it + // available to feeds, since the feed code doesn't implement the capability. + if ((handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) && + !isFeedType(handlerInfo.type)) { + var saveMenuItem = document.createElement("menuitem"); + saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk); + let label = this._prefsBundle.getString("saveFile"); + saveMenuItem.setAttribute("label", label); + saveMenuItem.setAttribute("tooltiptext", label); + saveMenuItem.setAttribute(APP_ICON_ATTR_NAME, "save"); + menuPopup.appendChild(saveMenuItem); + } + + // If this is the feed type, add a Live Bookmarks item. + if (isFeedType(handlerInfo.type)) { + var internalMenuItem = document.createElement("menuitem"); + internalMenuItem.setAttribute("action", Ci.nsIHandlerInfo.handleInternally); + let label = this._prefsBundle.getFormattedString("addLiveBookmarksInApp", + [this._brandShortName]); + internalMenuItem.setAttribute("label", label); + internalMenuItem.setAttribute("tooltiptext", label); + internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "feed"); + menuPopup.appendChild(internalMenuItem); + } + + // Add a separator to distinguish these items from the helper app items + // that follow them. + let menuItem = document.createElement("menuseparator"); + menuPopup.appendChild(menuItem); + + // Create a menu item for the OS default application, if any. + if (handlerInfo.hasDefaultHandler) { + var defaultMenuItem = document.createElement("menuitem"); + defaultMenuItem.setAttribute("action", Ci.nsIHandlerInfo.useSystemDefault); + let label = this._prefsBundle.getFormattedString("useDefault", + [handlerInfo.defaultDescription]); + defaultMenuItem.setAttribute("label", label); + defaultMenuItem.setAttribute("tooltiptext", handlerInfo.defaultDescription); + defaultMenuItem.setAttribute("image", this._getIconURLForSystemDefault(handlerInfo)); + + menuPopup.appendChild(defaultMenuItem); + } + + // Create menu items for possible handlers. + let preferredApp = handlerInfo.preferredApplicationHandler; + let possibleApps = handlerInfo.possibleApplicationHandlers.enumerate(); + var possibleAppMenuItems = []; + while (possibleApps.hasMoreElements()) { + let possibleApp = possibleApps.getNext(); + if (!this.isValidHandlerApp(possibleApp)) + continue; + + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp); + let label; + if (possibleApp instanceof Ci.nsILocalHandlerApp) + label = getFileDisplayName(possibleApp.executable); + else + label = possibleApp.name; + label = this._prefsBundle.getFormattedString("useApp", [label]); + menuItem.setAttribute("label", label); + menuItem.setAttribute("tooltiptext", label); + menuItem.setAttribute("image", this._getIconURLForHandlerApp(possibleApp)); + + // Attach the handler app object to the menu item so we can use it + // to make changes to the datastore when the user selects the item. + menuItem.handlerApp = possibleApp; + + menuPopup.appendChild(menuItem); + possibleAppMenuItems.push(menuItem); + } + + // Create a menu item for the plugin. + if (handlerInfo.pluginName) { + var pluginMenuItem = document.createElement("menuitem"); + pluginMenuItem.setAttribute("action", kActionUsePlugin); + let label = this._prefsBundle.getFormattedString("usePluginIn", + [handlerInfo.pluginName, + this._brandShortName]); + pluginMenuItem.setAttribute("label", label); + pluginMenuItem.setAttribute("tooltiptext", label); + pluginMenuItem.setAttribute(APP_ICON_ATTR_NAME, "plugin"); + menuPopup.appendChild(pluginMenuItem); + } + + // Create a menu item for selecting a local application. +#ifdef XP_WIN + // On Windows, selecting an application to open another application + // would be meaningless so we special case executables. + var executableType = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService) + .getTypeFromExtension("exe"); + if (handlerInfo.type != executableType) +#endif + { + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("oncommand", "gApplicationsPane.chooseApp(event)"); + let label = this._prefsBundle.getString("useOtherApp"); + menuItem.setAttribute("label", label); + menuItem.setAttribute("tooltiptext", label); + menuPopup.appendChild(menuItem); + } + + // Create a menu item for managing applications. + if (possibleAppMenuItems.length) { + let menuItem = document.createElement("menuseparator"); + menuPopup.appendChild(menuItem); + menuItem = document.createElement("menuitem"); + menuItem.setAttribute("oncommand", "gApplicationsPane.manageApp(event)"); + menuItem.setAttribute("label", this._prefsBundle.getString("manageApp")); + menuPopup.appendChild(menuItem); + } + + // Select the item corresponding to the preferred action. If the always + // ask flag is set, it overrides the preferred action. Otherwise we pick + // the item identified by the preferred action (when the preferred action + // is to use a helper app, we have to pick the specific helper app item). + if (handlerInfo.alwaysAskBeforeHandling) + menu.selectedItem = askMenuItem; + else switch (handlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.handleInternally: + menu.selectedItem = internalMenuItem; + break; + case Ci.nsIHandlerInfo.useSystemDefault: + menu.selectedItem = defaultMenuItem; + break; + case Ci.nsIHandlerInfo.useHelperApp: + if (preferredApp) + menu.selectedItem = + possibleAppMenuItems.filter(function(v) v.handlerApp.equals(preferredApp))[0]; + break; + case kActionUsePlugin: + menu.selectedItem = pluginMenuItem; + break; + case Ci.nsIHandlerInfo.saveToDisk: + menu.selectedItem = saveMenuItem; + break; + } + }, + + + //**************************************************************************// + // Sorting & Filtering + + _sortColumn: null, + + /** + * Sort the list when the user clicks on a column header. + */ + sort: function (event) { + var column = event.target; + + // If the user clicked on a new sort column, remove the direction indicator + // from the old column. + if (this._sortColumn && this._sortColumn != column) + this._sortColumn.removeAttribute("sortDirection"); + + this._sortColumn = column; + + // Set (or switch) the sort direction indicator. + if (column.getAttribute("sortDirection") == "ascending") + column.setAttribute("sortDirection", "descending"); + else + column.setAttribute("sortDirection", "ascending"); + + this._sortVisibleTypes(); + this._rebuildView(); + }, + + /** + * Sort the list of visible types by the current sort column/direction. + */ + _sortVisibleTypes: function() { + if (!this._sortColumn) + return; + + var t = this; + + function sortByType(a, b) { + return t._describeType(a).toLowerCase(). + localeCompare(t._describeType(b).toLowerCase()); + } + + function sortByAction(a, b) { + return t._describePreferredAction(a).toLowerCase(). + localeCompare(t._describePreferredAction(b).toLowerCase()); + } + + switch (this._sortColumn.getAttribute("value")) { + case "type": + this._visibleTypes.sort(sortByType); + break; + case "action": + this._visibleTypes.sort(sortByAction); + break; + } + + if (this._sortColumn.getAttribute("sortDirection") == "descending") + this._visibleTypes.reverse(); + }, + + /** + * Filter the list when the user enters a filter term into the filter field. + */ + filter: function() { + this._rebuildView(); + }, + + focusFilterBox: function() { + this._filter.focus(); + this._filter.select(); + }, + + + //**************************************************************************// + // Changes + + onSelectAction: function(aActionItem) { + this._storingAction = true; + + try { + this._storeAction(aActionItem); + } + finally { + this._storingAction = false; + } + }, + + _storeAction: function(aActionItem) { + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + + let action = parseInt(aActionItem.getAttribute("action")); + + // Set the plugin state if we're enabling or disabling a plugin. + if (action == kActionUsePlugin) + handlerInfo.enablePluginType(); + else if (handlerInfo.pluginName && !handlerInfo.isDisabledPluginType) + handlerInfo.disablePluginType(); + + // Set the preferred application handler. + // We leave the existing preferred app in the list when we set + // the preferred action to something other than useHelperApp so that + // legacy datastores that don't have the preferred app in the list + // of possible apps still include the preferred app in the list of apps + // the user can choose to handle the type. + if (action == Ci.nsIHandlerInfo.useHelperApp) + handlerInfo.preferredApplicationHandler = aActionItem.handlerApp; + + // Set the "always ask" flag. + if (action == Ci.nsIHandlerInfo.alwaysAsk) + handlerInfo.alwaysAskBeforeHandling = true; + else + handlerInfo.alwaysAskBeforeHandling = false; + + // Set the preferred action. + handlerInfo.preferredAction = action; + + handlerInfo.store(); + + // Make sure the handler info object is flagged to indicate that there is + // now some user configuration for the type. + handlerInfo.handledOnlyByPlugin = false; + + // Update the action label and image to reflect the new preferred action. + typeItem.setAttribute("actionDescription", + this._describePreferredAction(handlerInfo)); + if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) { + typeItem.setAttribute("actionIcon", + this._getIconURLForPreferredAction(handlerInfo)); + } + }, + + manageApp: function(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + + document.documentElement.openSubDialog("chrome://browser/content/preferences/applicationManager.xul", + "", handlerInfo); + + // Rebuild the actions menu so that we revert to the previous selection, + // or "Always ask" if the previous default application has been removed + this.rebuildActionsMenu(); + + // update the richlistitem too. Will be visible when selecting another row + typeItem.setAttribute("actionDescription", + this._describePreferredAction(handlerInfo)); + if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) { + typeItem.setAttribute("actionIcon", + this._getIconURLForPreferredAction(handlerInfo)); + } + }, + + chooseApp: function(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var handlerApp; + let chooseAppCallback = function(aHandlerApp) { + // Rebuild the actions menu whether the user picked an app or canceled. + // If they picked an app, we want to add the app to the menu and select it. + // If they canceled, we want to go back to their previous selection. + this.rebuildActionsMenu(); + + // If the user picked a new app from the menu, select it. + if (aHandlerApp) { + let typeItem = this._list.selectedItem; + let actionsMenu = + document.getAnonymousElementByAttribute(typeItem, "class", "actionsMenu"); + let menuItems = actionsMenu.menupopup.childNodes; + for (let i = 0; i < menuItems.length; i++) { + let menuItem = menuItems[i]; + if (menuItem.handlerApp && menuItem.handlerApp.equals(aHandlerApp)) { + actionsMenu.selectedIndex = i; + this.onSelectAction(menuItem); + break; + } + } + } + }.bind(this); + +#ifdef XP_WIN + var params = {}; + var handlerInfo = this._handledTypes[this._list.selectedItem.type]; + + if (isFeedType(handlerInfo.type)) { + // MIME info will be null, create a temp object. + params.mimeInfo = this._mimeSvc.getFromTypeAndExtension(handlerInfo.type, + handlerInfo.primaryExtension); + } else { + params.mimeInfo = handlerInfo.wrappedHandlerInfo; + } + + params.title = this._prefsBundle.getString("fpTitleChooseApp"); + params.description = handlerInfo.description; + params.filename = null; + params.handlerApp = null; + + window.openDialog("chrome://global/content/appPicker.xul", null, + "chrome,modal,centerscreen,titlebar,dialog=yes", + params); + + if (this.isValidHandlerApp(params.handlerApp)) { + handlerApp = params.handlerApp; + + // Add the app to the type's list of possible handlers. + handlerInfo.addPossibleApplicationHandler(handlerApp); + } + + chooseAppCallback(handlerApp); +#else + let winTitle = this._prefsBundle.getString("fpTitleChooseApp"); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == Ci.nsIFilePicker.returnOK && fp.file && + this._isValidHandlerExecutable(fp.file)) { + handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + handlerApp.name = getFileDisplayName(fp.file); + handlerApp.executable = fp.file; + + // Add the app to the type's list of possible handlers. + let handlerInfo = this._handledTypes[this._list.selectedItem.type]; + handlerInfo.addPossibleApplicationHandler(handlerApp); + + chooseAppCallback(handlerApp); + } + }.bind(this); + + // Prompt the user to pick an app. If they pick one, and it's a valid + // selection, then add it to the list of possible handlers. + fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + fp.open(fpCallback); +#endif + }, + + // Mark which item in the list was last selected so we can reselect it + // when we rebuild the list or when the user returns to the prefpane. + onSelectionChanged: function() { + if (this._list.selectedItem) + this._list.setAttribute("lastSelectedType", + this._list.selectedItem.getAttribute("type")); + }, + + _setIconClassForPreferredAction: function(aHandlerInfo, aElement) { + // If this returns true, the attribute that CSS sniffs for was set to something + // so you shouldn't manually set an icon URI. + // This removes the existing actionIcon attribute if any, even if returning false. + aElement.removeAttribute("actionIcon"); + + if (aHandlerInfo.alwaysAskBeforeHandling) { + aElement.setAttribute(APP_ICON_ATTR_NAME, "ask"); + return true; + } + + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + aElement.setAttribute(APP_ICON_ATTR_NAME, "save"); + return true; + + case Ci.nsIHandlerInfo.handleInternally: + if (isFeedType(aHandlerInfo.type)) { + aElement.setAttribute(APP_ICON_ATTR_NAME, "feed"); + return true; + } else if (aHandlerInfo instanceof InternalHandlerInfoWrapper) { + aElement.setAttribute(APP_ICON_ATTR_NAME, "ask"); + return true; + } + break; + + case kActionUsePlugin: + aElement.setAttribute(APP_ICON_ATTR_NAME, "plugin"); + return true; + } + aElement.removeAttribute(APP_ICON_ATTR_NAME); + return false; + }, + + _getIconURLForPreferredAction: function(aHandlerInfo) { + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.useSystemDefault: + return this._getIconURLForSystemDefault(aHandlerInfo); + + case Ci.nsIHandlerInfo.useHelperApp: + let preferredApp = aHandlerInfo.preferredApplicationHandler; + if (this.isValidHandlerApp(preferredApp)) + return this._getIconURLForHandlerApp(preferredApp); + break; + + // This should never happen, but if preferredAction is set to some weird + // value, then fall back to the generic application icon. + default: + return ICON_URL_APP; + } + }, + + _getIconURLForHandlerApp: function(aHandlerApp) { + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) + return this._getIconURLForFile(aHandlerApp.executable); + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) + return this._getIconURLForWebApp(aHandlerApp.uriTemplate); + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo) + return this._getIconURLForWebApp(aHandlerApp.uri) + + // We know nothing about other kinds of handler apps. + return ""; + }, + + _getIconURLForFile: function(aFile) { + var fph = this._ioSvc.getProtocolHandler("file"). + QueryInterface(Ci.nsIFileProtocolHandler); + var urlSpec = fph.getURLSpecFromFile(aFile); + + return "moz-icon://" + urlSpec + "?size=16"; + }, + + _getIconURLForWebApp: function(aWebAppURITemplate) { + var uri = this._ioSvc.newURI(aWebAppURITemplate, null, null); + + // Unfortunately we can't use the favicon service to get the favicon, + // because the service looks for a record with the exact URL we give it, and + // users won't have such records for URLs they don't visit, and users won't + // visit the handler's URL template, they'll only visit URLs derived from + // that template (i.e. with %s in the template replaced by the URL of the + // content being handled). + + if (/^https?$/.test(uri.scheme) && this._prefSvc.getBoolPref("browser.chrome.favicons")) + return uri.prePath + "/favicon.ico"; + + return ""; + }, + + _getIconURLForSystemDefault: function(aHandlerInfo) { + // Handler info objects for MIME types on some OSes implement a property bag + // interface from which we can get an icon for the default app, so if we're + // dealing with a MIME type on one of those OSes, then try to get the icon. + if ("wrappedHandlerInfo" in aHandlerInfo) { + let wrappedHandlerInfo = aHandlerInfo.wrappedHandlerInfo; + + if (wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + wrappedHandlerInfo instanceof Ci.nsIPropertyBag) { + try { + let url = wrappedHandlerInfo.getProperty("defaultApplicationIconURL"); + if (url) + return url + "?size=16"; + } + catch(ex) {} + } + } + + // If this isn't a MIME type object on an OS that supports retrieving + // the icon, or if we couldn't retrieve the icon for some other reason, + // then use a generic icon. + return ICON_URL_APP; + } + +}; diff --git a/browser/components/preferences/applications.xul b/browser/components/preferences/applications.xul new file mode 100644 index 000000000..2e6fa549e --- /dev/null +++ b/browser/components/preferences/applications.xul @@ -0,0 +1,99 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; 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/. --> + +<!DOCTYPE overlay [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % applicationsDTD SYSTEM "chrome://browser/locale/preferences/applications.dtd"> + %brandDTD; + %applicationsDTD; +]> + +<overlay id="ApplicationsPaneOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <prefpane id="paneApplications" + onpaneload="gApplicationsPane.init();" + flex="1" + helpTopic="prefs-applications"> + + <preferences id="feedsPreferences"> + <preference id="browser.feeds.handler" + name="browser.feeds.handler" + type="string"/> + <preference id="browser.feeds.handler.default" + name="browser.feeds.handler.default" + type="string"/> + <preference id="browser.feeds.handlers.application" + name="browser.feeds.handlers.application" + type="file"/> + <preference id="browser.feeds.handlers.webservice" + name="browser.feeds.handlers.webservice" + type="string"/> + + <preference id="browser.videoFeeds.handler" + name="browser.videoFeeds.handler" + type="string"/> + <preference id="browser.videoFeeds.handler.default" + name="browser.videoFeeds.handler.default" + type="string"/> + <preference id="browser.videoFeeds.handlers.application" + name="browser.videoFeeds.handlers.application" + type="file"/> + <preference id="browser.videoFeeds.handlers.webservice" + name="browser.videoFeeds.handlers.webservice" + type="string"/> + + <preference id="browser.audioFeeds.handler" + name="browser.audioFeeds.handler" + type="string"/> + <preference id="browser.audioFeeds.handler.default" + name="browser.audioFeeds.handler.default" + type="string"/> + <preference id="browser.audioFeeds.handlers.application" + name="browser.audioFeeds.handlers.application" + type="file"/> + <preference id="browser.audioFeeds.handlers.webservice" + name="browser.audioFeeds.handlers.webservice" + type="string"/> + + <preference id="pref.downloads.disable_button.edit_actions" + name="pref.downloads.disable_button.edit_actions" + type="bool"/> + </preferences> + + <script type="application/javascript" src="chrome://browser/content/preferences/applications.js"/> + + <keyset> + <key key="&focusSearch1.key;" modifiers="accel" oncommand="gApplicationsPane.focusFilterBox();"/> + <key key="&focusSearch2.key;" modifiers="accel" oncommand="gApplicationsPane.focusFilterBox();"/> + </keyset> + + <hbox> + <textbox id="filter" flex="1" + type="search" + placeholder="&filter.emptytext;" + aria-controls="handlersView" + oncommand="gApplicationsPane.filter();"/> + </hbox> + + <separator class="thin"/> + + <richlistbox id="handlersView" orient="vertical" persist="lastSelectedType" + preference="pref.downloads.disable_button.edit_actions" + onselect="gApplicationsPane.onSelectionChanged();"> + <listheader equalsize="always" style="border: 0; padding: 0; -moz-appearance: none;"> + <treecol id="typeColumn" label="&typeColumn.label;" value="type" + accesskey="&typeColumn.accesskey;" persist="sortDirection" + flex="1" onclick="gApplicationsPane.sort(event);" + sortDirection="ascending"/> + <treecol id="actionColumn" label="&actionColumn2.label;" value="action" + accesskey="&actionColumn2.accesskey;" persist="sortDirection" + flex="1" onclick="gApplicationsPane.sort(event);"/> + </listheader> + </richlistbox> + </prefpane> +</overlay> diff --git a/browser/components/preferences/colors.xul b/browser/components/preferences/colors.xul new file mode 100644 index 000000000..caf8c8c0e --- /dev/null +++ b/browser/components/preferences/colors.xul @@ -0,0 +1,114 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<!DOCTYPE prefwindow SYSTEM "chrome://browser/locale/preferences/colors.dtd" > + +<prefwindow id="ColorsDialog" type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&colorsDialog.title;" + dlgbuttons="accept,cancel,help" + ondialoghelp="openPrefsHelp()" + style="width: &window.width; !important;"> + + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + <prefpane id="ColorsDialogPane" + helpTopic="prefs-fonts-and-colors"> + + <preferences> + <preference id="browser.display.document_color_use" name="browser.display.document_color_use" type="int"/> + <preference id="browser.anchor_color" name="browser.anchor_color" type="string"/> + <preference id="browser.visited_color" name="browser.visited_color" type="string"/> + <preference id="browser.underline_anchors" name="browser.underline_anchors" type="bool"/> + <preference id="browser.display.foreground_color" name="browser.display.foreground_color" type="string"/> + <preference id="browser.display.background_color" name="browser.display.background_color" type="string"/> + <preference id="browser.display.use_system_colors" name="browser.display.use_system_colors" type="bool"/> + <preference id="browser.display.prefers_color_scheme" name="browser.display.prefers_color_scheme" type="int"/> + </preferences> + + <hbox> + <groupbox flex="1"> + <caption label="&color;"/> + <hbox align="center"> + <label value="&textColor.label;" accesskey="&textColor.accesskey;" control="foregroundtextmenu"/> + <spacer flex="1"/> + <colorpicker type="button" id="foregroundtextmenu" palettename="standard" + preference="browser.display.foreground_color"/> + </hbox> + <hbox align="center" style="margin-top: 5px"> + <label value="&backgroundColor.label;" accesskey="&backgroundColor.accesskey;" control="backgroundmenu"/> + <spacer flex="1"/> + <colorpicker type="button" id="backgroundmenu" palettename="standard" + preference="browser.display.background_color"/> + </hbox> + <separator class="thin"/> + <hbox align="center"> + <checkbox id="browserUseSystemColors" label="&useSystemColors.label;" accesskey="&useSystemColors.accesskey;" + preference="browser.display.use_system_colors"/> + </hbox> + </groupbox> + + <groupbox flex="1"> + <caption label="&links;"/> + <hbox align="center"> + <label value="&linkColor.label;" accesskey="&linkColor.accesskey;" control="unvisitedlinkmenu"/> + <spacer flex="1"/> + <colorpicker type="button" id="unvisitedlinkmenu" palettename="standard" + preference="browser.anchor_color"/> + </hbox> + <hbox align="center" style="margin-top: 5px"> + <label value="&visitedLinkColor.label;" accesskey="&visitedLinkColor.accesskey;" control="visitedlinkmenu"/> + <spacer flex="1"/> + <colorpicker type="button" id="visitedlinkmenu" palettename="standard" + preference="browser.visited_color"/> + </hbox> + <separator class="thin"/> + <hbox align="center"> + <checkbox id="browserUnderlineAnchors" label="&underlineLinks.label;" accesskey="&underlineLinks.accesskey;" + preference="browser.underline_anchors"/> + </hbox> + </groupbox> + </hbox> +#ifdef XP_WIN + <vbox align="start"> +#else + <vbox> +#endif + <label accesskey="&overridePageColors.accesskey;" + control="useDocumentColors">&overridePageColors.label;</label> + <menulist id="useDocumentColors" preference="browser.display.document_color_use"> + <menupopup> + <menuitem label="&overridePageColors.always.label;" + value="2" id="documentColorAlways"/> + <menuitem label="&overridePageColors.auto.label;" + value="0" id="documentColorAutomatic"/> + <menuitem label="&overridePageColors.never.label;" + value="1" id="documentColorNever"/> + </menupopup> + </menulist> + </vbox> + + <groupbox> + <caption label="&prefersColorScheme.caption;"/> + <label control="prefersColorSchemeSelection">&prefersColorScheme.label;</label> + <radiogroup id="prefersColorSchemeSelection" + preference="browser.display.prefers_color_scheme"> + <radio value="1" + label="&prefersColorSchemeLight.label;" + accesskey="&prefersColorSchemeLight.accesskey;"/> + <radio value="2" + label="&prefersColorSchemeDark.label;" + accesskey="&prefersColorSchemeDark.accesskey;"/> + <radio value="0" + label="&prefersColorSchemeDisabled.label;" + accesskey="&prefersColorSchemeDisabled.accesskey;"/> + </radiogroup> + <description>&prefersColorSchemeWarning;</description> + </groupbox> + + </prefpane> +</prefwindow> diff --git a/browser/components/preferences/connection.js b/browser/components/preferences/connection.js new file mode 100644 index 000000000..f94819d3f --- /dev/null +++ b/browser/components/preferences/connection.js @@ -0,0 +1,199 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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 gConnectionsDialog = { + beforeAccept: function () + { + var proxyTypePref = document.getElementById("network.proxy.type"); + if (proxyTypePref.value == 2) { + this.doAutoconfigURLFixup(); + return true; + } + + if (proxyTypePref.value != 1) + return true; + + var httpProxyURLPref = document.getElementById("network.proxy.http"); + var httpProxyPortPref = document.getElementById("network.proxy.http_port"); + var shareProxiesPref = document.getElementById("network.proxy.share_proxy_settings"); + if (shareProxiesPref.value) { + var proxyPrefs = ["ssl", "ftp", "socks"]; + for (var i = 0; i < proxyPrefs.length; ++i) { + var proxyServerURLPref = document.getElementById("network.proxy." + proxyPrefs[i]); + var proxyPortPref = document.getElementById("network.proxy." + proxyPrefs[i] + "_port"); + var backupServerURLPref = document.getElementById("network.proxy.backup." + proxyPrefs[i]); + var backupPortPref = document.getElementById("network.proxy.backup." + proxyPrefs[i] + "_port"); + backupServerURLPref.value = proxyServerURLPref.value; + backupPortPref.value = proxyPortPref.value; + proxyServerURLPref.value = httpProxyURLPref.value; + proxyPortPref.value = httpProxyPortPref.value; + } + } + + this.sanitizeNoProxiesPref(); + + return true; + }, + + checkForSystemProxy: function () + { + if ("@mozilla.org/system-proxy-settings;1" in Components.classes) + document.getElementById("systemPref").removeAttribute("hidden"); + }, + + proxyTypeChanged: function () + { + var proxyTypePref = document.getElementById("network.proxy.type"); + + // Update http + var httpProxyURLPref = document.getElementById("network.proxy.http"); + httpProxyURLPref.disabled = proxyTypePref.value != 1; + var httpProxyPortPref = document.getElementById("network.proxy.http_port"); + httpProxyPortPref.disabled = proxyTypePref.value != 1; + + // Now update the other protocols + this.updateProtocolPrefs(); + + var shareProxiesPref = document.getElementById("network.proxy.share_proxy_settings"); + shareProxiesPref.disabled = proxyTypePref.value != 1; + + var autologinProxyPref = document.getElementById("signon.autologin.proxy"); + autologinProxyPref.disabled = proxyTypePref.value == 0; + + var noProxiesPref = document.getElementById("network.proxy.no_proxies_on"); + noProxiesPref.disabled = proxyTypePref.value == 0; + + var autoconfigURLPref = document.getElementById("network.proxy.autoconfig_url"); + autoconfigURLPref.disabled = proxyTypePref.value != 2; + + this.updateReloadButton(); + }, + + updateDNSPref: function () + { + var socksVersionPref = document.getElementById("network.proxy.socks_version"); + var socksDNSPref = document.getElementById("network.proxy.socks_remote_dns"); + var proxyTypePref = document.getElementById("network.proxy.type"); + var isDefinitelySocks4 = !socksVersionPref.disabled && socksVersionPref.value == 4; + socksDNSPref.disabled = (isDefinitelySocks4 || proxyTypePref.value == 0); + return undefined; + }, + + updateReloadButton: function () + { + // Disable the "Reload PAC" button if the selected proxy type is not PAC or + // if the current value of the PAC textbox does not match the value stored + // in prefs. Likewise, disable the reload button if PAC is not configured + // in prefs. + + var typedURL = document.getElementById("networkProxyAutoconfigURL").value; + var proxyTypeCur = document.getElementById("network.proxy.type").value; + + var prefs = + Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch); + var pacURL = prefs.getCharPref("network.proxy.autoconfig_url"); + var proxyType = prefs.getIntPref("network.proxy.type"); + + var disableReloadPref = + document.getElementById("pref.advanced.proxies.disable_button.reload"); + disableReloadPref.disabled = + (proxyTypeCur != 2 || proxyType != 2 || typedURL != pacURL); + }, + + readProxyType: function () + { + this.proxyTypeChanged(); + return undefined; + }, + + updateProtocolPrefs: function () + { + var proxyTypePref = document.getElementById("network.proxy.type"); + var shareProxiesPref = document.getElementById("network.proxy.share_proxy_settings"); + var proxyPrefs = ["ssl", "ftp", "socks"]; + for (var i = 0; i < proxyPrefs.length; ++i) { + var proxyServerURLPref = document.getElementById("network.proxy." + proxyPrefs[i]); + var proxyPortPref = document.getElementById("network.proxy." + proxyPrefs[i] + "_port"); + + // Restore previous per-proxy custom settings, if present. + if (!shareProxiesPref.value) { + var backupServerURLPref = document.getElementById("network.proxy.backup." + proxyPrefs[i]); + var backupPortPref = document.getElementById("network.proxy.backup." + proxyPrefs[i] + "_port"); + if (backupServerURLPref.hasUserValue) { + proxyServerURLPref.value = backupServerURLPref.value; + backupServerURLPref.reset(); + } + if (backupPortPref.hasUserValue) { + proxyPortPref.value = backupPortPref.value; + backupPortPref.reset(); + } + } + + proxyServerURLPref.updateElements(); + proxyPortPref.updateElements(); + proxyServerURLPref.disabled = proxyTypePref.value != 1 || shareProxiesPref.value; + proxyPortPref.disabled = proxyServerURLPref.disabled; + } + var socksVersionPref = document.getElementById("network.proxy.socks_version"); + socksVersionPref.disabled = proxyTypePref.value != 1 || shareProxiesPref.value; + this.updateDNSPref(); + return undefined; + }, + + readProxyProtocolPref: function (aProtocol, aIsPort) + { + var shareProxiesPref = document.getElementById("network.proxy.share_proxy_settings"); + if (shareProxiesPref.value) { + var pref = document.getElementById("network.proxy.http" + (aIsPort ? "_port" : "")); + return pref.value; + } + + var backupPref = document.getElementById("network.proxy.backup." + aProtocol + (aIsPort ? "_port" : "")); + return backupPref.hasUserValue ? backupPref.value : undefined; + }, + + reloadPAC: function () + { + Components.classes["@mozilla.org/network/protocol-proxy-service;1"]. + getService().reloadPAC(); + }, + + doAutoconfigURLFixup: function () + { + var autoURL = document.getElementById("networkProxyAutoconfigURL"); + var autoURLPref = document.getElementById("network.proxy.autoconfig_url"); + var URIFixup = Components.classes["@mozilla.org/docshell/urifixup;1"] + .getService(Components.interfaces.nsIURIFixup); + try { + autoURLPref.value = autoURL.value = URIFixup.createFixupURI(autoURL.value, 0).spec; + } catch(ex) {} + }, + + sanitizeNoProxiesPref: function() + { + var noProxiesPref = document.getElementById("network.proxy.no_proxies_on"); + // replace substrings of ; and \n with commas if they're neither immediately + // preceded nor followed by a valid separator character + noProxiesPref.value = noProxiesPref.value.replace(/([^, \n;])[;\n]+(?![,\n;])/g, '$1,'); + // replace any remaining ; and \n since some may follow commas, etc. + noProxiesPref.value = noProxiesPref.value.replace(/[;\n]/g, ''); + }, + + readHTTPProxyServer: function () + { + var shareProxiesPref = document.getElementById("network.proxy.share_proxy_settings"); + if (shareProxiesPref.value) + this.updateProtocolPrefs(); + return undefined; + }, + + readHTTPProxyPort: function () + { + var shareProxiesPref = document.getElementById("network.proxy.share_proxy_settings"); + if (shareProxiesPref.value) + this.updateProtocolPrefs(); + return undefined; + } +}; diff --git a/browser/components/preferences/connection.xul b/browser/components/preferences/connection.xul new file mode 100644 index 000000000..e21168652 --- /dev/null +++ b/browser/components/preferences/connection.xul @@ -0,0 +1,159 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE prefwindow SYSTEM "chrome://browser/locale/preferences/connection.dtd"> + +<?xml-stylesheet href="chrome://global/skin/"?> + +<prefwindow id="ConnectionsDialog" type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&connectionsDialog.title;" + dlgbuttons="accept,cancel,help" + onbeforeaccept="return gConnectionsDialog.beforeAccept();" + onload="gConnectionsDialog.checkForSystemProxy();" + ondialoghelp="openPrefsHelp()" + style="width: &window.width; !important;"> + + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + + <prefpane id="ConnectionsDialogPane" + helpTopic="prefs-connection-settings"> + + <preferences> + <preference id="network.proxy.type" name="network.proxy.type" type="int" + onchange="gConnectionsDialog.proxyTypeChanged();"/> + <preference id="network.proxy.http" name="network.proxy.http" type="string"/> + <preference id="network.proxy.http_port" name="network.proxy.http_port" type="int"/> + <preference id="network.proxy.ftp" name="network.proxy.ftp" type="string"/> + <preference id="network.proxy.ftp_port" name="network.proxy.ftp_port" type="int"/> + <preference id="network.proxy.ssl" name="network.proxy.ssl" type="string"/> + <preference id="network.proxy.ssl_port" name="network.proxy.ssl_port" type="int"/> + <preference id="network.proxy.socks" name="network.proxy.socks" type="string"/> + <preference id="network.proxy.socks_port" name="network.proxy.socks_port" type="int"/> + <preference id="network.proxy.socks_version" name="network.proxy.socks_version" type="int" + onchange="gConnectionsDialog.updateDNSPref();"/> + <preference id="network.proxy.socks_remote_dns" name="network.proxy.socks_remote_dns" type="bool"/> + <preference id="network.proxy.no_proxies_on" name="network.proxy.no_proxies_on" type="string"/> + <preference id="network.proxy.autoconfig_url" name="network.proxy.autoconfig_url" type="string"/> + <preference id="network.proxy.share_proxy_settings" name="network.proxy.share_proxy_settings" type="bool"/> + <preference id="signon.autologin.proxy" name="signon.autologin.proxy" type="bool"/> + <preference id="pref.advanced.proxies.disable_button.reload" + name="pref.advanced.proxies.disable_button.reload" type="bool"/> + <preference id="network.proxy.backup.ftp" name="network.proxy.backup.ftp" type="string"/> + <preference id="network.proxy.backup.ftp_port" name="network.proxy.backup.ftp_port" type="int"/> + <preference id="network.proxy.backup.ssl" name="network.proxy.backup.ssl" type="string"/> + <preference id="network.proxy.backup.ssl_port" name="network.proxy.backup.ssl_port" type="int"/> + <preference id="network.proxy.backup.socks" name="network.proxy.backup.socks" type="string"/> + <preference id="network.proxy.backup.socks_port" name="network.proxy.backup.socks_port" type="int"/> + </preferences> + + <script type="application/javascript" src="chrome://browser/content/preferences/connection.js"/> + + <stringbundle id="preferencesBundle" src="chrome://browser/locale/preferences/preferences.properties"/> + + <groupbox> + <caption label="&proxyTitle.label;"/> + + <radiogroup id="networkProxyType" preference="network.proxy.type" + onsyncfrompreference="return gConnectionsDialog.readProxyType();"> + <radio value="0" label="&noProxyTypeRadio.label;" accesskey="&noProxyTypeRadio.accesskey;"/> + <radio value="4" label="&WPADTypeRadio.label;" accesskey="&WPADTypeRadio.accesskey;"/> + <radio value="5" label="&systemTypeRadio.label;" accesskey="&systemTypeRadio.accesskey;" id="systemPref" hidden="true"/> + <radio value="1" label="&manualTypeRadio.label;" accesskey="&manualTypeRadio.accesskey;"/> + <grid class="indent" flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <hbox pack="end"> + <label value="&http.label;" accesskey="&http.accesskey;" control="networkProxyHTTP"/> + </hbox> + <hbox align="center"> + <textbox id="networkProxyHTTP" flex="1" + preference="network.proxy.http" onsyncfrompreference="return gConnectionsDialog.readHTTPProxyServer();"/> + <label value="&port.label;" accesskey="&HTTPport.accesskey;" control="networkProxyHTTP_Port"/> + <textbox id="networkProxyHTTP_Port" type="number" max="65535" size="5" + preference="network.proxy.http_port" onsyncfrompreference="return gConnectionsDialog.readHTTPProxyPort();"/> + </hbox> + </row> + <row> + <hbox/> + <hbox> + <checkbox id="shareAllProxies" label="&shareproxy.label;" accesskey="&shareproxy.accesskey;" + preference="network.proxy.share_proxy_settings" + onsyncfrompreference="return gConnectionsDialog.updateProtocolPrefs();"/> + </hbox> + </row> + <row align="center"> + <hbox pack="end"> + <label value="&ssl.label;" accesskey="&ssl.accesskey;" control="networkProxySSL"/> + </hbox> + <hbox align="center"> + <textbox id="networkProxySSL" flex="1" preference="network.proxy.ssl" + onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ssl', false);"/> + <label value="&port.label;" accesskey="&SSLport.accesskey;" control="networkProxySSL_Port"/> + <textbox id="networkProxySSL_Port" type="number" max="65535" size="5" preference="network.proxy.ssl_port" + onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ssl', true);"/> + </hbox> + </row> + <row align="center"> + <hbox pack="end"> + <label value="&ftp.label;" accesskey="&ftp.accesskey;" control="networkProxyFTP"/> + </hbox> + <hbox align="center"> + <textbox id="networkProxyFTP" flex="1" preference="network.proxy.ftp" + onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ftp', false);"/> + <label value="&port.label;" accesskey="&FTPport.accesskey;" control="networkProxyFTP_Port"/> + <textbox id="networkProxyFTP_Port" type="number" max="65535" size="5" preference="network.proxy.ftp_port" + onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('ftp', true);"/> + </hbox> + </row> + <row align="center"> + <hbox pack="end"> + <label value="&socks.label;" accesskey="&socks.accesskey;" control="networkProxySOCKS"/> + </hbox> + <hbox align="center"> + <textbox id="networkProxySOCKS" flex="1" preference="network.proxy.socks" + onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('socks', false);"/> + <label value="&port.label;" accesskey="&SOCKSport.accesskey;" control="networkProxySOCKS_Port"/> + <textbox id="networkProxySOCKS_Port" type="number" max="65535" size="5" preference="network.proxy.socks_port" + onsyncfrompreference="return gConnectionsDialog.readProxyProtocolPref('socks', true);"/> + </hbox> + </row> + <row> + <spacer/> + <radiogroup id="networkProxySOCKSVersion" orient="horizontal" + preference="network.proxy.socks_version"> + <radio id="networkProxySOCKSVersion4" value="4" label="&socks4.label;" accesskey="&socks4.accesskey;"/> + <radio id="networkProxySOCKSVersion5" value="5" label="&socks5.label;" accesskey="&socks5.accesskey;"/> + </radiogroup> + </row> + </rows> + </grid> + <radio value="2" label="&autoTypeRadio.label;" accesskey="&autoTypeRadio.accesskey;"/> + <hbox class="indent" flex="1" align="center"> + <textbox id="networkProxyAutoconfigURL" flex="1" preference="network.proxy.autoconfig_url" + oninput="gConnectionsDialog.updateReloadButton();"/> + <button id="autoReload" icon="refresh" + label="&reload.label;" accesskey="&reload.accesskey;" + oncommand="gConnectionsDialog.reloadPAC();" + preference="pref.advanced.proxies.disable_button.reload"/> + </hbox> + </radiogroup> + <separator class="thin"/> + <label value="&noproxy.label;" accesskey="&noproxy.accesskey;" control="networkProxyNone"/> + <textbox id="networkProxyNone" preference="network.proxy.no_proxies_on" multiline="true" rows="2"/> + <label value="&noproxyExplain.label;" control="networkProxyNone"/> + <checkbox id="autologinProxy" preference="signon.autologin.proxy" + label="&autologinproxy.label;" accesskey="&autologinproxy.accesskey;" + tooltiptext="&autologinproxy.tooltip;"/> + <checkbox id="networkProxySOCKSRemoteDNS" preference="network.proxy.socks_remote_dns" + label="&socksRemoteDNS.label;" accesskey="&socksRemoteDNS.accesskey;"/> + </groupbox> + </prefpane> +</prefwindow> diff --git a/browser/components/preferences/content.js b/browser/components/preferences/content.js new file mode 100644 index 000000000..62a675c92 --- /dev/null +++ b/browser/components/preferences/content.js @@ -0,0 +1,186 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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 gContentPane = { + + /** + * Initializes the fonts dropdowns displayed in this pane. + */ + init: function () + { + this._rebuildFonts(); + var menulist = document.getElementById("defaultFont"); + if (menulist.selectedIndex == -1) { + menulist.insertItemAt(0, "", "", ""); + menulist.selectedIndex = 0; + } + }, + + // UTILITY FUNCTIONS + + /** + * Utility function to enable/disable the button specified by aButtonID based + * on the value of the Boolean preference specified by aPreferenceID. + */ + updateButtons: function (aButtonID, aPreferenceID) + { + var button = document.getElementById(aButtonID); + var preference = document.getElementById(aPreferenceID); + button.disabled = preference.value != true; + return undefined; + }, + + /** + * Utility function to enable/disable the checkboxes for MSE options depending + * on the value of media.mediasource.enabled. + */ + updateMSE: function () + { + var checkboxMSEMP4 = document.getElementById('videoMSEMP4'); + var checkboxMSEWebM = document.getElementById('videoMSEWebM'); + var preference = document.getElementById('media.mediasource.enabled'); + checkboxMSEMP4.disabled = preference.value != true; + checkboxMSEWebM.disabled = preference.value != true; + }, + + // BEGIN UI CODE + + /* + * Preferences: + * + * dom.disable_open_during_load + * - true if popups are blocked by default, false otherwise + */ + + // POP-UPS + + /** + * Displays the popup exceptions dialog where specific site popup preferences + * can be set. + */ + showPopupExceptions: function () + { + var bundlePreferences = document.getElementById("bundlePreferences"); + var params = { blockVisible: false, sessionVisible: false, allowVisible: true, prefilledHost: "", permissionType: "popup" }; + params.windowTitle = bundlePreferences.getString("popuppermissionstitle"); + params.introText = bundlePreferences.getString("popuppermissionstext"); + document.documentElement.openWindow("Browser:Permissions", + "chrome://browser/content/preferences/permissions.xul", + "", params); + }, + + + // FONTS + + /** + * Populates the default font list in UI. + */ + _rebuildFonts: function () + { + var langGroupPref = document.getElementById("font.language.group"); + this._selectDefaultLanguageGroup(langGroupPref.value, + this._readDefaultFontTypeForLanguage(langGroupPref.value) == "serif"); + }, + + /** + * + */ + _selectDefaultLanguageGroup: function (aLanguageGroup, aIsSerif) + { + const kFontNameFmtSerif = "font.name.serif.%LANG%"; + const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%"; + const kFontNameListFmtSerif = "font.name-list.serif.%LANG%"; + const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%"; + const kFontSizeFmtVariable = "font.size.variable.%LANG%"; + + var prefs = [{ format : aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif, + type : "fontname", + element : "defaultFont", + fonttype : aIsSerif ? "serif" : "sans-serif" }, + { format : aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif, + type : "unichar", + element : null, + fonttype : aIsSerif ? "serif" : "sans-serif" }, + { format : kFontSizeFmtVariable, + type : "int", + element : "defaultFontSize", + fonttype : null }]; + var preferences = document.getElementById("contentPreferences"); + for (var i = 0; i < prefs.length; ++i) { + var preference = document.getElementById(prefs[i].format.replace(/%LANG%/, aLanguageGroup)); + if (!preference) { + preference = document.createElement("preference"); + var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup); + preference.id = name; + preference.setAttribute("name", name); + preference.setAttribute("type", prefs[i].type); + preferences.appendChild(preference); + } + + if (!prefs[i].element) + continue; + + var element = document.getElementById(prefs[i].element); + if (element) { + element.setAttribute("preference", preference.id); + + if (prefs[i].fonttype) + FontBuilder.buildFontList(aLanguageGroup, prefs[i].fonttype, element); + + preference.setElementValue(element); + } + } + }, + + /** + * Returns the type of the current default font for the language denoted by + * aLanguageGroup. + */ + _readDefaultFontTypeForLanguage: function (aLanguageGroup) + { + const kDefaultFontType = "font.default.%LANG%"; + var defaultFontTypePref = kDefaultFontType.replace(/%LANG%/, aLanguageGroup); + var preference = document.getElementById(defaultFontTypePref); + if (!preference) { + preference = document.createElement("preference"); + preference.id = defaultFontTypePref; + preference.setAttribute("name", defaultFontTypePref); + preference.setAttribute("type", "string"); + preference.setAttribute("onchange", "gContentPane._rebuildFonts();"); + document.getElementById("contentPreferences").appendChild(preference); + } + return preference.value; + }, + + /** + * Displays the fonts dialog, where web page font names and sizes can be + * configured. + */ + configureFonts: function () + { + document.documentElement.openSubDialog("chrome://browser/content/preferences/fonts.xul", + "", null); + }, + + /** + * Displays the colors dialog, where default web page/link/etc. colors can be + * configured. + */ + configureColors: function () + { + document.documentElement.openSubDialog("chrome://browser/content/preferences/colors.xul", + "", null); + }, + + // LANGUAGES + + /** + * Shows a dialog in which the preferred language for web content may be set. + */ + showLanguages: function () + { + document.documentElement.openSubDialog("chrome://browser/content/preferences/languages.xul", + "", null); + } +}; diff --git a/browser/components/preferences/content.xul b/browser/components/preferences/content.xul new file mode 100644 index 000000000..21f9e5d81 --- /dev/null +++ b/browser/components/preferences/content.xul @@ -0,0 +1,209 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % contentDTD SYSTEM "chrome://browser/locale/preferences/content.dtd"> + %brandDTD; + %contentDTD; +]> + +<overlay id="ContentPaneOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <prefpane id="paneContent" + onpaneload="gContentPane.init();" + helpTopic="prefs-content"> + + <preferences id="contentPreferences"> + <!--XXX buttons prefs --> + + <!-- POPUPS, IMAGES --> + <preference id="dom.disable_open_during_load" name="dom.disable_open_during_load" type="bool"/> + <preference id="permissions.default.image" name="permissions.default.image" type="int"/> + + <!-- FONTS --> + <preference id="font.language.group" + name="font.language.group" + type="wstring" + onchange="gContentPane._rebuildFonts();"/> + + <!-- JavaScript --> + <preference id="javascript.options.wasm" name="javascript.options.wasm" type="bool"/> + + + <!-- VIDEO --> + <preference id="media.mediasource.enabled" name="media.mediasource.enabled" type="bool"/> + <preference id="media.mediasource.mp4.enabled" name="media.mediasource.mp4.enabled" type="bool"/> + <preference id="media.mediasource.webm.enabled" name="media.mediasource.webm.enabled" type="bool"/> + + <!-- Media formats --> + <preference id="media.av1.enabled" name="media.av1.enabled" type="bool"/> + <preference id="media.flac.enabled" name="media.flac.enabled" type="bool"/> + <preference id="media.mp4.enabled" name="media.mp4.enabled" type="bool"/> + <preference id="media.ogg.enabled" name="media.ogg.enabled" type="bool"/> + <preference id="media.opus.enabled" name="media.opus.enabled" type="bool"/> + <preference id="media.webm.enabled" name="media.webm.enabled" type="bool"/> + + </preferences> + + <script type="application/javascript" src="chrome://mozapps/content/preferences/fontbuilder.js"/> + <script type="application/javascript" src="chrome://browser/content/preferences/content.js"/> + + <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> + + <!-- various checkboxes, font-fu --> + <groupbox id="miscGroup"> + <grid id="contentGrid"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows id="contentRows-1"> + <row id="popupPolicyRow"> + <vbox align="start"> + <checkbox id="popupPolicy" preference="dom.disable_open_during_load" + label="&blockPopups.label;" accesskey="&blockPopups.accesskey;" + onsyncfrompreference="return gContentPane.updateButtons('popupPolicyButton', + 'dom.disable_open_during_load');"/> + </vbox> + <button id="popupPolicyButton" label="&popupExceptions.label;" + oncommand="gContentPane.showPopupExceptions();" + accesskey="&popupExceptions.accesskey;"/> + </row> + <row id="enableImagesRow"> + <hbox align="center"> + <label id="loadImages" control="loadImages-menu">&loadImages.label;</label> + <menulist id="loadImages-menu" preference="permissions.default.image" sizetopopup="always"> + <menupopup> + <menuitem label="&loadImages.always;" value="1" /> + <menuitem label="&loadImages.never;" value="2" /> + <menuitem label="&loadImages.no3rdparty;" value="3" /> + </menupopup> + </menulist> + </hbox> + </row> + </rows> + </grid> + </groupbox> + + <!-- Fonts and Colors --> + <groupbox id="fontsGroup"> + <caption label="&fontsAndColors.label;"/> + + <grid id="fontsGrid"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows id="fontsRows"> + <row id="fontRow"> + <hbox align="center"> + <label control="defaultFont" accesskey="&defaultFont.accesskey;">&defaultFont.label;</label> + <menulist id="defaultFont" flex="1"/> + <label control="defaultFontSize" accesskey="&defaultSize.accesskey;">&defaultSize.label;</label> + <menulist id="defaultFontSize"> + <menupopup> + <menuitem value="9" label="9"/> + <menuitem value="10" label="10"/> + <menuitem value="11" label="11"/> + <menuitem value="12" label="12"/> + <menuitem value="13" label="13"/> + <menuitem value="14" label="14"/> + <menuitem value="15" label="15"/> + <menuitem value="16" label="16"/> + <menuitem value="17" label="17"/> + <menuitem value="18" label="18"/> + <menuitem value="20" label="20"/> + <menuitem value="22" label="22"/> + <menuitem value="24" label="24"/> + <menuitem value="26" label="26"/> + <menuitem value="28" label="28"/> + <menuitem value="30" label="30"/> + <menuitem value="32" label="32"/> + <menuitem value="34" label="34"/> + <menuitem value="36" label="36"/> + <menuitem value="40" label="40"/> + <menuitem value="44" label="44"/> + <menuitem value="48" label="48"/> + <menuitem value="56" label="56"/> + <menuitem value="64" label="64"/> + <menuitem value="72" label="72"/> + </menupopup> + </menulist> + </hbox> + <button id="advancedFonts" icon="select-font" + label="&advancedFonts.label;" + accesskey="&advancedFonts.accesskey;" + oncommand="gContentPane.configureFonts();"/> + </row> + <row id="colorsRow"> + <hbox/> + <button id="colors" icon="select-color" + label="&colors.label;" + accesskey="&colors.accesskey;" + oncommand="gContentPane.configureColors();"/> + </row> + </rows> + </grid> + </groupbox> + + <!-- Languages --> + <groupbox id="languagesGroup"> + <caption label="&languages.label;"/> + + <hbox id="languagesBox" align="center"> + <description flex="1" control="chooseLanguage">&chooseLanguage.label;</description> + <button id="chooseLanguage" + label="&chooseButton.label;" + accesskey="&chooseButton.accesskey;" + oncommand="gContentPane.showLanguages();"/> + </hbox> + </groupbox> + + <!-- Javascript --> + <groupbox id="jsOptionsGroup"> + <caption label="&jsOptions.label;"/> + + <checkbox id="jsOptionsWasm" preference="javascript.options.wasm" + label="&jsOptionsWasm.label;" accesskey="&jsOptionsWasm.accesskey;"/> + </groupbox> + + <!-- Video --> + <groupbox id="videoGroup"> + <caption label="&video.label;"/> + + <checkbox id="videoMSE" preference="media.mediasource.enabled" + label="&videoMSE.label;" accesskey="&videoMSE.accesskey;" + onsyncfrompreference="gContentPane.updateMSE();"/> + <checkbox class="indent" id="videoMSEMP4" preference="media.mediasource.mp4.enabled" + label="&videoMSEMP4.label;" accesskey="&videoMSEMP4.accesskey;"/> + <checkbox class="indent" id="videoMSEWebM" preference="media.mediasource.webm.enabled" + label="&videoMSEWebM.label;" accesskey="&videoMSEWebM.accesskey;"/> + </groupbox> + + <!-- Media formats --> + <groupbox id="mediaSupport" align="start"> + <caption label="&mediaSupport.label;"/> + <hbox align="center"> + <label id="allowEnable" value="&allowEnable.label;"/> +#ifdef MOZ_FMP4 + <checkbox id="enableMP4" label="&enableMP4.label;" preference="media.mp4.enabled"/> +#endif + <checkbox id="enableWebM" label="&enableWebM.label;" preference="media.webm.enabled"/> +#ifdef MOZ_AV1 + <checkbox id="enableAV1" label="&enableAV1.label;" preference="media.av1.enabled"/> +#endif + <checkbox id="enableOGG" label="&enableOGG.label;" preference="media.ogg.enabled"/> + <checkbox id="enableOPUS" label="&enableOPUS.label;" preference="media.opus.enabled"/> + <checkbox id="enableFLAC" label="&enableFLAC.label;" preference="media.flac.enabled"/> + </hbox> + </groupbox> + + </prefpane> + +</overlay> diff --git a/browser/components/preferences/cookies.js b/browser/components/preferences/cookies.js new file mode 100644 index 000000000..dbc2b3ef6 --- /dev/null +++ b/browser/components/preferences/cookies.js @@ -0,0 +1,943 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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 nsICookie = Components.interfaces.nsICookie; + +Components.utils.import("resource://gre/modules/PluralForm.jsm"); + +var gCookiesWindow = { + _cm : Components.classes["@mozilla.org/cookiemanager;1"] + .getService(Components.interfaces.nsICookieManager), + _ds : Components.classes["@mozilla.org/intl/scriptabledateformat;1"] + .getService(Components.interfaces.nsIScriptableDateFormat), + _hosts : {}, + _hostOrder : [], + _tree : null, + _bundle : null, + + init: function() { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.addObserver(this, "cookie-changed", false); + os.addObserver(this, "perm-changed", false); + + this._bundle = document.getElementById("bundlePreferences"); + this._tree = document.getElementById("cookiesList"); + + let removeAllCookies = document.getElementById("removeAllCookies"); + removeAllCookies.setAttribute("accesskey", this._bundle.getString("removeAllCookies.accesskey")); + let removeSelectedCookies = document.getElementById("removeSelectedCookies"); + removeSelectedCookies.setAttribute("accesskey", this._bundle.getString("removeSelectedCookies.accesskey")); + + this._populateList(true); + + document.getElementById("filter").focus(); + }, + + uninit: function() { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.removeObserver(this, "cookie-changed"); + os.removeObserver(this, "perm-changed"); + }, + + _populateList: function(aInitialLoad) { + this._loadCookies(); + this._tree.view = this._view; + if (aInitialLoad) + this.sort("rawHost"); + if (this._view.rowCount > 0) + this._tree.view.selection.select(0); + + if (aInitialLoad) { + if ("arguments" in window && + window.arguments[0] && + window.arguments[0].filterString) + this.setFilter(window.arguments[0].filterString); + } + else { + if (document.getElementById("filter").value != "") + this.filter(); + } + + this._updateRemoveAllButton(); + + this._saveState(); + }, + + _cookieEquals: function(aCookieA, aCookieB, aStrippedHost) { + return aCookieA.rawHost == aStrippedHost && + aCookieA.name == aCookieB.name && + aCookieA.path == aCookieB.path && + ChromeUtils.isOriginAttributesEqual(aCookieA.originAttributes, + aCookieB.originAttributes); + }, + + observe: function(aCookie, aTopic, aData) { + if (aTopic != "cookie-changed") + return; + + if (aCookie instanceof Components.interfaces.nsICookie) { + var strippedHost = this._makeStrippedHost(aCookie.host); + if (aData == "changed") + this._handleCookieChanged(aCookie, strippedHost); + else if (aData == "added") + this._handleCookieAdded(aCookie, strippedHost); + } + else if (aData == "cleared") { + this._hosts = {}; + this._hostOrder = []; + + var oldRowCount = this._view._rowCount; + this._view._rowCount = 0; + this._tree.treeBoxObject.rowCountChanged(0, -oldRowCount); + this._view.selection.clearSelection(); + this._updateRemoveAllButton(); + } + else if (aData == "reload") { + // first, clear any existing entries + this.observe(aCookie, aTopic, "cleared"); + + // then, reload the list + this._populateList(false); + } + + // We don't yet handle aData == "deleted" - it's a less common case + // and is rather complicated as selection tracking is difficult + }, + + _handleCookieChanged: function(changedCookie, strippedHost) { + var rowIndex = 0; + var cookieItem = null; + if (!this._view._filtered) { + for (var i = 0; i < this._hostOrder.length; ++i) { // (var host in this._hosts) { + ++rowIndex; + var hostItem = this._hosts[this._hostOrder[i]]; // var hostItem = this._hosts[host]; + if (this._hostOrder[i] == strippedHost) { // host == strippedHost) { + // Host matches, look for the cookie within this Host collection + // and update its data + for (var j = 0; j < hostItem.cookies.length; ++j) { + ++rowIndex; + var currCookie = hostItem.cookies[j]; + if (this._cookieEquals(currCookie, changedCookie, strippedHost)) { + currCookie.value = changedCookie.value; + currCookie.isSecure = changedCookie.isSecure; + currCookie.isDomain = changedCookie.isDomain; + currCookie.expires = changedCookie.expires; + cookieItem = currCookie; + break; + } + } + } + else if (hostItem.open) + rowIndex += hostItem.cookies.length; + } + } + else { + // Just walk the filter list to find the item. It doesn't matter that + // we don't update the main Host collection when we do this, because + // when the filter is reset the Host collection is rebuilt anyway. + for (rowIndex = 0; rowIndex < this._view._filterSet.length; ++rowIndex) { + currCookie = this._view._filterSet[rowIndex]; + if (this._cookieEquals(currCookie, changedCookie, strippedHost)) { + currCookie.value = changedCookie.value; + currCookie.isSecure = changedCookie.isSecure; + currCookie.isDomain = changedCookie.isDomain; + currCookie.expires = changedCookie.expires; + cookieItem = currCookie; + break; + } + } + } + + // Make sure the tree display is up to date... + this._tree.treeBoxObject.invalidateRow(rowIndex); + // ... and if the cookie is selected, update the displayed metadata too + if (cookieItem != null && this._view.selection.currentIndex == rowIndex) + this._updateCookieData(cookieItem); + }, + + _handleCookieAdded: function(changedCookie, strippedHost) { + var rowCountImpact = 0; + var addedHost = { value: 0 }; + this._addCookie(strippedHost, changedCookie, addedHost); + if (!this._view._filtered) { + // The Host collection for this cookie already exists, and it's not open, + // so don't increment the rowCountImpact becaues the user is not going to + // see the additional rows as they're hidden. + if (addedHost.value || this._hosts[strippedHost].open) + ++rowCountImpact; + } + else { + // We're in search mode, and the cookie being added matches + // the search condition, so add it to the list. + var c = this._makeCookieObject(strippedHost, changedCookie); + if (this._cookieMatchesFilter(c)) { + this._view._filterSet.push(this._makeCookieObject(strippedHost, changedCookie)); + ++rowCountImpact; + } + } + // Now update the tree display at the end (we could/should re run the sort + // if any to get the position correct.) + var oldRowCount = this._rowCount; + this._view._rowCount += rowCountImpact; + this._tree.treeBoxObject.rowCountChanged(oldRowCount - 1, rowCountImpact); + + this._updateRemoveAllButton(); + }, + + _view: { + _filtered : false, + _filterSet : [], + _filterValue: "", + _rowCount : 0, + _cacheValid : 0, + _cacheItems : [], + get rowCount() { + return this._rowCount; + }, + + _getItemAtIndex: function(aIndex) { + if (this._filtered) + return this._filterSet[aIndex]; + + var start = 0; + var count = 0, hostIndex = 0; + + var cacheIndex = Math.min(this._cacheValid, aIndex); + if (cacheIndex > 0) { + var cacheItem = this._cacheItems[cacheIndex]; + start = cacheItem['start']; + count = hostIndex = cacheItem['count']; + } + + for (var i = start; i < gCookiesWindow._hostOrder.length; ++i) { // var host in gCookiesWindow._hosts) { + var currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]]; // gCookiesWindow._hosts[host]; + if (!currHost) continue; + if (count == aIndex) + return currHost; + hostIndex = count; + + var cacheEntry = { 'start' : i, 'count' : count }; + var cacheStart = count; + + if (currHost.open) { + if (count < aIndex && aIndex <= (count + currHost.cookies.length)) { + // We are looking for an entry within this host's children, + // enumerate them looking for the index. + ++count; + for (var i = 0; i < currHost.cookies.length; ++i) { + if (count == aIndex) { + var cookie = currHost.cookies[i]; + cookie.parentIndex = hostIndex; + return cookie; + } + ++count; + } + } + else { + // A host entry was open, but we weren't looking for an index + // within that host entry's children, so skip forward over the + // entry's children. We need to add one to increment for the + // host value too. + count += currHost.cookies.length + 1; + } + } + else + ++count; + + for (var j = cacheStart; j < count; j++) + this._cacheItems[j] = cacheEntry; + this._cacheValid = count - 1; + } + return null; + }, + + _removeItemAtIndex: function(aIndex, aCount) { + var removeCount = aCount === undefined ? 1 : aCount; + if (this._filtered) { + // remove the cookies from the unfiltered set so that they + // don't reappear when the filter is changed. See bug 410863. + for (var i = aIndex; i < aIndex + removeCount; ++i) { + var item = this._filterSet[i]; + var parent = gCookiesWindow._hosts[item.rawHost]; + for (var j = 0; j < parent.cookies.length; ++j) { + if (item == parent.cookies[j]) { + parent.cookies.splice(j, 1); + break; + } + } + } + this._filterSet.splice(aIndex, removeCount); + return; + } + + var item = this._getItemAtIndex(aIndex); + if (!item) return; + this._invalidateCache(aIndex - 1); + if (item.container) { + gCookiesWindow._hosts[item.rawHost] = null; + } else { + var parent = this._getItemAtIndex(item.parentIndex); + for (var i = 0; i < parent.cookies.length; ++i) { + var cookie = parent.cookies[i]; + if (item.rawHost == cookie.rawHost && + item.name == cookie.name && + item.path == cookie.path && + ChromeUtils.isOriginAttributesEqual(item.originAttributes, + cookie.originAttributes)) { + parent.cookies.splice(i, removeCount); + } + } + } + }, + + _invalidateCache: function(aIndex) { + this._cacheValid = Math.min(this._cacheValid, aIndex); + }, + + getCellText: function(aIndex, aColumn) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) + return ""; + if (aColumn.id == "domainCol") + return item.rawHost; + else if (aColumn.id == "nameCol") + return item.name; + } + else { + if (aColumn.id == "domainCol") + return this._filterSet[aIndex].rawHost; + else if (aColumn.id == "nameCol") + return this._filterSet[aIndex].name; + } + return ""; + }, + + _selection: null, + get selection () { return this._selection; }, + set selection (val) { this._selection = val; return val; }, + getRowProperties: function(aIndex) { return ""; }, + getCellProperties: function(aIndex, aColumn) { return ""; }, + getColumnProperties: function(aColumn) { return ""; }, + isContainer: function(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) return false; + return item.container; + } + return false; + }, + isContainerOpen: function(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) return false; + return item.open; + } + return false; + }, + isContainerEmpty: function(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) return false; + return item.cookies.length == 0; + } + return false; + }, + isSeparator: function(aIndex) { return false; }, + isSorted: function(aIndex) { return false; }, + canDrop: function(aIndex, aOrientation) { return false; }, + drop: function(aIndex, aOrientation) {}, + getParentIndex: function(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + // If an item has no parent index (i.e. it is at the top level) this + // function MUST return -1 otherwise we will go into an infinite loop. + // Containers are always top level items in the cookies tree, so make + // sure to return the appropriate value here. + if (!item || item.container) return -1; + return item.parentIndex; + } + return -1; + }, + hasNextSibling: function(aParentIndex, aIndex) { + if (!this._filtered) { + // |aParentIndex| appears to be bogus, but we can get the real + // parent index by getting the entry for |aIndex| and reading the + // parentIndex field. + // The index of the last item in this host collection is the + // index of the parent + the size of the host collection, and + // aIndex has a next sibling if it is less than this value. + var item = this._getItemAtIndex(aIndex); + if (item) { + if (item.container) { + for (var i = aIndex + 1; i < this.rowCount; ++i) { + var subsequent = this._getItemAtIndex(i); + if (subsequent.container) + return true; + } + return false; + } + else { + var parent = this._getItemAtIndex(item.parentIndex); + if (parent && parent.container) + return aIndex < item.parentIndex + parent.cookies.length; + } + } + } + return aIndex < this.rowCount - 1; + }, + hasPreviousSibling: function(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) return false; + var parent = this._getItemAtIndex(item.parentIndex); + if (parent && parent.container) + return aIndex > item.parentIndex + 1; + } + return aIndex > 0; + }, + getLevel: function(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) return 0; + return item.level; + } + return 0; + }, + getImageSrc: function(aIndex, aColumn) {}, + getProgressMode: function(aIndex, aColumn) {}, + getCellValue: function(aIndex, aColumn) {}, + setTree: function(aTree) {}, + toggleOpenState: function(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) return; + this._invalidateCache(aIndex); + var multiplier = item.open ? -1 : 1; + var delta = multiplier * item.cookies.length; + this._rowCount += delta; + item.open = !item.open; + gCookiesWindow._tree.treeBoxObject.rowCountChanged(aIndex + 1, delta); + gCookiesWindow._tree.treeBoxObject.invalidateRow(aIndex); + } + }, + cycleHeader: function(aColumn) {}, + selectionChanged: function() {}, + cycleCell: function(aIndex, aColumn) {}, + isEditable: function(aIndex, aColumn) { + return false; + }, + isSelectable: function(aIndex, aColumn) { + return false; + }, + setCellValue: function(aIndex, aColumn, aValue) {}, + setCellText: function(aIndex, aColumn, aValue) {}, + performAction: function(aAction) {}, + performActionOnRow: function(aAction, aIndex) {}, + performActionOnCell: function(aAction, aindex, aColumn) {} + }, + + _makeStrippedHost: function(aHost) { + var formattedHost = aHost.charAt(0) == "." ? aHost.substring(1, aHost.length) : aHost; + return formattedHost.substring(0, 4) == "www." ? formattedHost.substring(4, formattedHost.length) : formattedHost; + }, + + _addCookie: function(aStrippedHost, aCookie, aHostCount) { + if (!(aStrippedHost in this._hosts) || !this._hosts[aStrippedHost]) { + this._hosts[aStrippedHost] = { cookies : [], + rawHost : aStrippedHost, + level : 0, + open : false, + container : true }; + this._hostOrder.push(aStrippedHost); + ++aHostCount.value; + } + + var c = this._makeCookieObject(aStrippedHost, aCookie); + this._hosts[aStrippedHost].cookies.push(c); + }, + + _makeCookieObject: function(aStrippedHost, aCookie) { + var host = aCookie.host; + var formattedHost = host.charAt(0) == "." ? host.substring(1, host.length) : host; + var c = { name : aCookie.name, + value : aCookie.value, + isDomain : aCookie.isDomain, + host : aCookie.host, + rawHost : aStrippedHost, + path : aCookie.path, + isSecure : aCookie.isSecure, + expires : aCookie.expires, + level : 1, + container : false, + originAttributes: aCookie.originAttributes }; + return c; + }, + + _loadCookies: function() { + var e = this._cm.enumerator; + var hostCount = { value: 0 }; + this._hosts = {}; + this._hostOrder = []; + while (e.hasMoreElements()) { + var cookie = e.getNext(); + if (cookie && cookie instanceof Components.interfaces.nsICookie) { + var strippedHost = this._makeStrippedHost(cookie.host); + this._addCookie(strippedHost, cookie, hostCount); + } + else + break; + } + this._view._rowCount = hostCount.value; + }, + + formatExpiresString: function(aExpires) { + if (aExpires) { + var date = new Date(1000 * aExpires); + return this._ds.FormatDateTime("", this._ds.dateFormatLong, + this._ds.timeFormatSeconds, + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds()); + } + return this._bundle.getString("expireAtEndOfSession"); + }, + + _updateCookieData: function(aItem) { + var seln = this._view.selection; + var ids = ["name", "value", "host", "path", "isSecure", "expires"]; + var properties; + + if (aItem && !aItem.container && seln.count > 0) { + properties = { name: aItem.name, value: aItem.value, host: aItem.host, + path: aItem.path, expires: this.formatExpiresString(aItem.expires), + isDomain: aItem.isDomain ? this._bundle.getString("domainColon") + : this._bundle.getString("hostColon"), + isSecure: aItem.isSecure ? this._bundle.getString("forSecureOnly") + : this._bundle.getString("forAnyConnection") }; + for (var i = 0; i < ids.length; ++i) + document.getElementById(ids[i]).disabled = false; + } + else { + var noneSelected = this._bundle.getString("noCookieSelected"); + properties = { name: noneSelected, value: noneSelected, host: noneSelected, + path: noneSelected, expires: noneSelected, + isSecure: noneSelected }; + for (i = 0; i < ids.length; ++i) + document.getElementById(ids[i]).disabled = true; + } + for (var property in properties) + document.getElementById(property).value = properties[property]; + }, + + onCookieSelected: function() { + var properties, item; + var seln = this._tree.view.selection; + var hasRows = this._tree.view.rowCount > 0; + var hasSelection = seln.count > 0; + if (!this._view._filtered) + item = this._view._getItemAtIndex(seln.currentIndex); + else + item = this._view._filterSet[seln.currentIndex]; + + this._updateCookieData(item); + + var rangeCount = seln.getRangeCount(); + var selectedCookieCount = 0; + for (var i = 0; i < rangeCount; ++i) { + var min = {}; var max = {}; + seln.getRangeAt(i, min, max); + for (var j = min.value; j <= max.value; ++j) { + item = this._view._getItemAtIndex(j); + if (!item) continue; + if (item.container && !item.open) + selectedCookieCount += item.cookies.length; + else if (!item.container) + ++selectedCookieCount; + } + } + var item = this._view._getItemAtIndex(seln.currentIndex); + if (item && seln.count == 1 && item.container && item.open) + selectedCookieCount += 2; + + let buttonLabel = this._bundle.getString("removeSelectedCookies.label"); + let removeSelectedCookies = document.getElementById("removeSelectedCookies"); + removeSelectedCookies.label = PluralForm.get(selectedCookieCount, buttonLabel) + .replace("#1", selectedCookieCount); + + removeSelectedCookies.disabled = !hasRows || !hasSelection; + }, + + performDeletion: function(deleteItems) { + var psvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + var blockFutureCookies = false; + if (psvc.prefHasUserValue("network.cookie.blockFutureCookies")) + blockFutureCookies = psvc.getBoolPref("network.cookie.blockFutureCookies"); + for (var i = 0; i < deleteItems.length; ++i) { + var item = deleteItems[i]; + this._cm.remove(item.host, item.name, item.path, + blockFutureCookies, item.originAttributes); + } + }, + + deleteCookie: function() { + // Selection Notes + // - Selection always moves to *NEXT* adjacent item unless item + // is last child at a given level in which case it moves to *PREVIOUS* + // item + // + // Selection Cases (Somewhat Complicated) + // + // 1) Single cookie selected, host has single child + // v cnn.com + // //// cnn.com ///////////// goksdjf@ //// + // > atwola.com + // + // Before SelectedIndex: 1 Before RowCount: 3 + // After SelectedIndex: 0 After RowCount: 1 + // + // 2) Host selected, host open + // v goats.com //////////////////////////// + // goats.com sldkkfjl + // goat.scom flksj133 + // > atwola.com + // + // Before SelectedIndex: 0 Before RowCount: 4 + // After SelectedIndex: 0 After RowCount: 1 + // + // 3) Host selected, host closed + // > goats.com //////////////////////////// + // > atwola.com + // + // Before SelectedIndex: 0 Before RowCount: 2 + // After SelectedIndex: 0 After RowCount: 1 + // + // 4) Single cookie selected, host has many children + // v goats.com + // goats.com sldkkfjl + // //// goats.com /////////// flksjl33 //// + // > atwola.com + // + // Before SelectedIndex: 2 Before RowCount: 4 + // After SelectedIndex: 1 After RowCount: 3 + // + // 5) Single cookie selected, host has many children + // v goats.com + // //// goats.com /////////// flksjl33 //// + // goats.com sldkkfjl + // > atwola.com + // + // Before SelectedIndex: 1 Before RowCount: 4 + // After SelectedIndex: 1 After RowCount: 3 + var seln = this._view.selection; + var tbo = this._tree.treeBoxObject; + + if (seln.count < 1) return; + + var nextSelected = 0; + var rowCountImpact = 0; + var deleteItems = []; + if (!this._view._filtered) { + var ci = seln.currentIndex; + nextSelected = ci; + var invalidateRow = -1; + var item = this._view._getItemAtIndex(ci); + if (item.container) { + rowCountImpact -= (item.open ? item.cookies.length : 0) + 1; + deleteItems = deleteItems.concat(item.cookies); + if (!this._view.hasNextSibling(-1, ci)) + --nextSelected; + this._view._removeItemAtIndex(ci); + } + else { + var parent = this._view._getItemAtIndex(item.parentIndex); + --rowCountImpact; + if (parent.cookies.length == 1) { + --rowCountImpact; + deleteItems.push(item); + if (!this._view.hasNextSibling(-1, ci)) + --nextSelected; + if (!this._view.hasNextSibling(-1, item.parentIndex)) + --nextSelected; + this._view._removeItemAtIndex(item.parentIndex); + invalidateRow = item.parentIndex; + } + else { + deleteItems.push(item); + if (!this._view.hasNextSibling(-1, ci)) + --nextSelected; + this._view._removeItemAtIndex(ci); + } + } + this._view._rowCount += rowCountImpact; + tbo.rowCountChanged(ci, rowCountImpact); + if (invalidateRow != -1) + tbo.invalidateRow(invalidateRow); + } + else { + var rangeCount = seln.getRangeCount(); + // Traverse backwards through selections to avoid messing + // up the indices when they are deleted. + // See bug 388079. + for (var i = rangeCount - 1; i >= 0; --i) { + var min = {}; var max = {}; + seln.getRangeAt(i, min, max); + nextSelected = min.value; + for (var j = min.value; j <= max.value; ++j) { + deleteItems.push(this._view._getItemAtIndex(j)); + if (!this._view.hasNextSibling(-1, max.value)) + --nextSelected; + } + var delta = max.value - min.value + 1; + this._view._removeItemAtIndex(min.value, delta); + rowCountImpact = -1 * delta; + this._view._rowCount += rowCountImpact; + tbo.rowCountChanged(min.value, rowCountImpact); + } + } + + this.performDeletion(deleteItems); + + if (nextSelected < 0) + seln.clearSelection(); + else { + seln.select(nextSelected); + this._tree.focus(); + } + }, + + deleteAllCookies: function() { + if (this._view._filtered) { + var rowCount = this._view.rowCount; + var deleteItems = []; + for (var index = 0; index < rowCount; index++) { + deleteItems.push(this._view._getItemAtIndex(index)); + } + this._view._removeItemAtIndex(0, rowCount); + this._view._rowCount = 0; + this._tree.treeBoxObject.rowCountChanged(0, -rowCount); + this.performDeletion(deleteItems); + } + else { + this._cm.removeAll(); + } + this._updateRemoveAllButton(); + this.focusFilterBox(); + }, + + onCookieKeyPress: function(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.deleteCookie(); + } + }, + + _lastSortProperty : "", + _lastSortAscending: false, + sort: function(aProperty) { + var ascending = (aProperty == this._lastSortProperty) ? !this._lastSortAscending : true; + // Sort the Non-Filtered Host Collections + if (aProperty == "rawHost") { + function sortByHost(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + } + this._hostOrder.sort(sortByHost); + if (!ascending) + this._hostOrder.reverse(); + } + + function sortByProperty(a, b) { + return a[aProperty].toLowerCase().localeCompare(b[aProperty].toLowerCase()); + } + for (var host in this._hosts) { + var cookies = this._hosts[host].cookies; + cookies.sort(sortByProperty); + if (!ascending) + cookies.reverse(); + } + // Sort the Filtered List, if in Filtered mode + if (this._view._filtered) { + this._view._filterSet.sort(sortByProperty); + if (!ascending) + this._view._filterSet.reverse(); + } + + // Adjust the Sort Indicator + var domainCol = document.getElementById("domainCol"); + var nameCol = document.getElementById("nameCol"); + var sortOrderString = ascending ? "ascending" : "descending"; + if (aProperty == "rawHost") { + domainCol.setAttribute("sortDirection", sortOrderString); + nameCol.removeAttribute("sortDirection"); + } + else { + nameCol.setAttribute("sortDirection", sortOrderString); + domainCol.removeAttribute("sortDirection"); + } + + this._view._invalidateCache(0); + this._view.selection.clearSelection(); + if (this._view.rowCount > 0) { + this._view.selection.select(0); + } + this._tree.treeBoxObject.invalidate(); + this._tree.treeBoxObject.ensureRowIsVisible(0); + + this._lastSortAscending = ascending; + this._lastSortProperty = aProperty; + }, + + clearFilter: function() { + // Revert to single-select in the tree + this._tree.setAttribute("seltype", "single"); + + // Clear the Tree Display + this._view._filtered = false; + this._view._rowCount = 0; + this._tree.treeBoxObject.rowCountChanged(0, -this._view._filterSet.length); + this._view._filterSet = []; + + // Just reload the list to make sure deletions are respected + this._loadCookies(); + this._tree.view = this._view; + + // Restore sort order + var sortby = this._lastSortProperty; + if (sortby == "") { + this._lastSortAscending = false; + this.sort("rawHost"); + } + else { + this._lastSortAscending = !this._lastSortAscending; + this.sort(sortby); + } + + // Restore open state + for (var i = 0; i < this._openIndices.length; ++i) + this._view.toggleOpenState(this._openIndices[i]); + this._openIndices = []; + + // Restore selection + this._view.selection.clearSelection(); + for (i = 0; i < this._lastSelectedRanges.length; ++i) { + var range = this._lastSelectedRanges[i]; + this._view.selection.rangedSelect(range.min, range.max, true); + } + this._lastSelectedRanges = []; + + document.getElementById("cookiesIntro").value = this._bundle.getString("cookiesAll"); + this._updateRemoveAllButton(); + }, + + _cookieMatchesFilter: function(aCookie) { + return aCookie.rawHost.indexOf(this._view._filterValue) != -1 || + aCookie.name.indexOf(this._view._filterValue) != -1 || + aCookie.value.indexOf(this._view._filterValue) != -1; + }, + + _filterCookies: function(aFilterValue) { + this._view._filterValue = aFilterValue; + var cookies = []; + for (var i = 0; i < gCookiesWindow._hostOrder.length; ++i) { //var host in gCookiesWindow._hosts) { + var currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]]; // gCookiesWindow._hosts[host]; + if (!currHost) continue; + for (var j = 0; j < currHost.cookies.length; ++j) { + var cookie = currHost.cookies[j]; + if (this._cookieMatchesFilter(cookie)) + cookies.push(cookie); + } + } + return cookies; + }, + + _lastSelectedRanges: [], + _openIndices: [], + _saveState: function() { + // Save selection + var seln = this._view.selection; + this._lastSelectedRanges = []; + var rangeCount = seln.getRangeCount(); + for (var i = 0; i < rangeCount; ++i) { + var min = {}; var max = {}; + seln.getRangeAt(i, min, max); + this._lastSelectedRanges.push({ min: min.value, max: max.value }); + } + + // Save open states + this._openIndices = []; + for (i = 0; i < this._view.rowCount; ++i) { + var item = this._view._getItemAtIndex(i); + if (item && item.container && item.open) + this._openIndices.push(i); + } + }, + + _updateRemoveAllButton: function() { + let removeAllCookies = document.getElementById("removeAllCookies"); + removeAllCookies.disabled = this._view._rowCount == 0; + + let labelStringID = "removeAllCookies.label"; + let accessKeyStringID = "removeAllCookies.accesskey"; + if (this._view._filtered) { + labelStringID = "removeAllShownCookies.label"; + accessKeyStringID = "removeAllShownCookies.accesskey"; + } + removeAllCookies.setAttribute("label", this._bundle.getString(labelStringID)); + removeAllCookies.setAttribute("accesskey", this._bundle.getString(accessKeyStringID)); + }, + + filter: function() { + var filter = document.getElementById("filter").value; + if (filter == "") { + gCookiesWindow.clearFilter(); + return; + } + var view = gCookiesWindow._view; + view._filterSet = gCookiesWindow._filterCookies(filter); + if (!view._filtered) { + // Save Display Info for the Non-Filtered mode when we first + // enter Filtered mode. + gCookiesWindow._saveState(); + view._filtered = true; + } + // Move to multi-select in the tree + gCookiesWindow._tree.setAttribute("seltype", "multiple"); + + // Clear the display + var oldCount = view._rowCount; + view._rowCount = 0; + gCookiesWindow._tree.treeBoxObject.rowCountChanged(0, -oldCount); + // Set up the filtered display + view._rowCount = view._filterSet.length; + gCookiesWindow._tree.treeBoxObject.rowCountChanged(0, view.rowCount); + + // if the view is not empty then select the first item + if (view.rowCount > 0) + view.selection.select(0); + + document.getElementById("cookiesIntro").value = gCookiesWindow._bundle.getString("cookiesFiltered"); + this._updateRemoveAllButton(); + }, + + setFilter: function(aFilterString) { + document.getElementById("filter").value = aFilterString; + this.filter(); + }, + + focusFilterBox: function() { + var filter = document.getElementById("filter"); + filter.focus(); + filter.select(); + }, + + onWindowKeyPress: function(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) + window.close(); + } +}; diff --git a/browser/components/preferences/cookies.xul b/browser/components/preferences/cookies.xul new file mode 100644 index 000000000..8dd757fd0 --- /dev/null +++ b/browser/components/preferences/cookies.xul @@ -0,0 +1,103 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/cookies.dtd" > + +<window id="CookiesDialog" windowtype="Browser:Cookies" + class="windowDialog" title="&window.title;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + style="width: &window.width;;" + onload="gCookiesWindow.init();" + onunload="gCookiesWindow.uninit();" + persist="screenX screenY width height" + onkeypress="gCookiesWindow.onWindowKeyPress(event);"> + + <script src="chrome://browser/content/preferences/cookies.js"/> + + <stringbundle id="bundlePreferences" + src="chrome://browser/locale/preferences/preferences.properties"/> + + <keyset> + <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/> + <key key="&focusSearch1.key;" modifiers="accel" oncommand="gCookiesWindow.focusFilterBox();"/> + <key key="&focusSearch2.key;" modifiers="accel" oncommand="gCookiesWindow.focusFilterBox();"/> + </keyset> + + <vbox flex="1" class="contentPane"> + <hbox align="center"> + <label accesskey="&filter.accesskey;" control="filter">&filter.label;</label> + <textbox type="search" id="filter" flex="1" + aria-controls="cookiesList" + oncommand="gCookiesWindow.filter();"/> + </hbox> + <separator class="thin"/> + <label control="cookiesList" id="cookiesIntro" value="&cookiesonsystem.label;"/> + <separator class="thin"/> + <tree id="cookiesList" flex="1" style="height: 10em;" + onkeypress="gCookiesWindow.onCookieKeyPress(event)" + onselect="gCookiesWindow.onCookieSelected();" + hidecolumnpicker="true" seltype="single"> + <treecols> + <treecol id="domainCol" label="&cookiedomain.label;" flex="2" primary="true" + persist="width" onclick="gCookiesWindow.sort('rawHost');"/> + <splitter class="tree-splitter"/> + <treecol id="nameCol" label="&cookiename.label;" flex="1" + persist="width" + onclick="gCookiesWindow.sort('name');"/> + </treecols> + <treechildren id="cookiesChildren"/> + </tree> + <hbox id="cookieInfoBox"> + <grid flex="1" id="cookieInfoGrid"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <hbox pack="end"><label id="nameLabel" control="name" value="&props.name.label;"/></hbox> + <textbox id="name" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"><label id="valueLabel" control="value" value="&props.value.label;"/></hbox> + <textbox id="value" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"><label id="isDomain" control="host" value="&props.domain.label;"/></hbox> + <textbox id="host" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"><label id="pathLabel" control="path" value="&props.path.label;"/></hbox> + <textbox id="path" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"><label id="isSecureLabel" control="isSecure" value="&props.secure.label;"/></hbox> + <textbox id="isSecure" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"><label id="expiresLabel" control="expires" value="&props.expires.label;"/></hbox> + <textbox id="expires" readonly="true" class="plain"/> + </row> + </rows> + </grid> + </hbox> + </vbox> + <hbox align="end"> + <hbox class="actionButtons" flex="1"> + <button id="removeSelectedCookies" disabled="true" icon="clear" + oncommand="gCookiesWindow.deleteCookie();"/> + <button id="removeAllCookies" disabled="true" icon="clear" + oncommand="gCookiesWindow.deleteAllCookies();"/> + <spacer flex="1"/> + <button oncommand="close();" icon="close" + label="&button.close.label;" accesskey="&button.close.accesskey;"/> + </hbox> + <resizer type="window" dir="bottomend"/> + </hbox> +</window> diff --git a/browser/components/preferences/fonts.js b/browser/components/preferences/fonts.js new file mode 100644 index 000000000..975671a6e --- /dev/null +++ b/browser/components/preferences/fonts.js @@ -0,0 +1,143 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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.display.languageList LOCK ALL when LOCKED + +const kDefaultFontType = "font.default.%LANG%"; +const kFontNameFmtSerif = "font.name.serif.%LANG%"; +const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%"; +const kFontNameFmtMonospace = "font.name.monospace.%LANG%"; +const kFontNameListFmtSerif = "font.name-list.serif.%LANG%"; +const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%"; +const kFontNameListFmtMonospace = "font.name-list.monospace.%LANG%"; +const kFontSizeFmtVariable = "font.size.variable.%LANG%"; +const kFontSizeFmtFixed = "font.size.fixed.%LANG%"; +const kFontMinSizeFmt = "font.minimum-size.%LANG%"; + +var gFontsDialog = { + _selectLanguageGroup: function (aLanguageGroup) + { + var prefs = [{ format: kDefaultFontType, type: "string", element: "defaultFontType", fonttype: null}, + { format: kFontNameFmtSerif, type: "fontname", element: "serif", fonttype: "serif" }, + { format: kFontNameFmtSansSerif, type: "fontname", element: "sans-serif", fonttype: "sans-serif" }, + { format: kFontNameFmtMonospace, type: "fontname", element: "monospace", fonttype: "monospace" }, + { format: kFontNameListFmtSerif, type: "unichar", element: null, fonttype: "serif" }, + { format: kFontNameListFmtSansSerif, type: "unichar", element: null, fonttype: "sans-serif" }, + { format: kFontNameListFmtMonospace, type: "unichar", element: null, fonttype: "monospace" }, + { format: kFontSizeFmtVariable, type: "int", element: "sizeVar", fonttype: null }, + { format: kFontSizeFmtFixed, type: "int", element: "sizeMono", fonttype: null }, + { format: kFontMinSizeFmt, type: "int", element: "minSize", fonttype: null }]; + var preferences = document.getElementById("fontPreferences"); + for (var i = 0; i < prefs.length; ++i) { + var preference = document.getElementById(prefs[i].format.replace(/%LANG%/, aLanguageGroup)); + if (!preference) { + preference = document.createElement("preference"); + var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup); + preference.id = name; + preference.setAttribute("name", name); + preference.setAttribute("type", prefs[i].type); + preferences.appendChild(preference); + } + + if (!prefs[i].element) + continue; + + var element = document.getElementById(prefs[i].element); + if (element) { + element.setAttribute("preference", preference.id); + + if (prefs[i].fonttype) + FontBuilder.buildFontList(aLanguageGroup, prefs[i].fonttype, element); + + preference.setElementValue(element); + } + } + }, + + readFontLanguageGroup: function () + { + var languagePref = document.getElementById("font.language.group"); + this._selectLanguageGroup(languagePref.value); + return undefined; + }, + + readFontSelection: function (aElement) + { + // Determine the appropriate value to select, for the following cases: + // - there is no setting + // - the font selected by the user is no longer present (e.g. deleted from + // fonts folder) + var preference = document.getElementById(aElement.getAttribute("preference")); + if (preference.value) { + var fontItems = aElement.getElementsByAttribute("value", preference.value); + + // There is a setting that actually is in the list. Respect it. + if (fontItems.length > 0) + return undefined; + } + + var defaultValue = aElement.firstChild.firstChild.getAttribute("value"); + var languagePref = document.getElementById("font.language.group"); + preference = document.getElementById("font.name-list." + aElement.id + "." + languagePref.value); + if (!preference || !preference.hasUserValue) + return defaultValue; + + var fontNames = preference.value.split(","); + var stripWhitespace = /^\s*(.*)\s*$/; + + for (var i = 0; i < fontNames.length; ++i) { + var fontName = fontNames[i].replace(stripWhitespace, "$1"); + fontItems = aElement.getElementsByAttribute("value", fontName); + if (fontItems.length) + break; + } + if (fontItems.length) + return fontItems[0].getAttribute("value"); + return defaultValue; + }, + + readUseDocumentFonts: function () + { + var preference = document.getElementById("browser.display.use_document_fonts"); + return preference.value == 1; + }, + + writeUseDocumentFonts: function () + { + var useDocumentFonts = document.getElementById("useDocumentFonts"); + return useDocumentFonts.checked ? 1 : 0; + }, + + onBeforeAccept: function () + { + // Only care in in-content prefs + if (!window.frameElement) { + return true; + } + + let preferences = document.querySelectorAll("preference[id*='font.minimum-size']"); + // It would be good if we could avoid touching languages the pref pages won't use, but + // unfortunately the language group APIs (deducing language groups from language codes) + // are C++ - only. So we just check all the things the user touched: + // Don't care about anything up to 24px, or if this value is the same as set previously: + preferences = Array.filter(preferences, prefEl => { + return prefEl.value > 24 && prefEl.value != prefEl.valueFromPreferences; + }); + if (!preferences.length) { + return; + } + + let strings = document.getElementById("bundlePreferences"); + let title = strings.getString("veryLargeMinimumFontTitle"); + let confirmLabel = strings.getString("acceptVeryLargeMinimumFont"); + let warningMessage = strings.getString("veryLargeMinimumFontWarning"); + let {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {}); + let flags = Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL | + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING | + Services.prompt.BUTTON_POS_1_DEFAULT; + let buttonChosen = Services.prompt.confirmEx(window, title, warningMessage, flags, confirmLabel, null, "", "", {}); + return buttonChosen == 0; + }, +}; + diff --git a/browser/components/preferences/fonts.xul b/browser/components/preferences/fonts.xul new file mode 100644 index 000000000..1c14bcf91 --- /dev/null +++ b/browser/components/preferences/fonts.xul @@ -0,0 +1,275 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<!DOCTYPE prefwindow SYSTEM "chrome://browser/locale/preferences/fonts.dtd" > + +<prefwindow id="FontsDialog" type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&fontsDialog.title;" + dlgbuttons="accept,cancel,help" + ondialoghelp="openPrefsHelp()" + onbeforeaccept="return gFontsDialog.onBeforeAccept();" + style=""> + + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + + <prefpane id="FontsDialogPane" + class="largeDialogContainer" + helpTopic="prefs-fonts-and-colors"> + + <preferences id="fontPreferences"> + <preference id="font.language.group" name="font.language.group" type="wstring"/> + <preference id="browser.display.use_document_fonts" + name="browser.display.use_document_fonts" + type="int"/> + <preference id="intl.charset.fallback.override" name="intl.charset.fallback.override" type="string"/> + </preferences> + + <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> + <script type="application/javascript" src="chrome://mozapps/content/preferences/fontbuilder.js"/> + <script type="application/javascript" src="chrome://browser/content/preferences/fonts.js"/> + + <!-- Fonts for: [ Language ] --> + <groupbox> + <caption> + <hbox align="center"> + <label accesskey="&language.accesskey;" control="selectLangs">&language.label;</label> + </hbox> + <menulist id="selectLangs" preference="font.language.group" + onsyncfrompreference="return gFontsDialog.readFontLanguageGroup();"> + <menupopup> + <menuitem value="ar" label="&font.langGroup.arabic;"/> + <menuitem value="x-armn" label="&font.langGroup.armenian;"/> + <menuitem value="x-beng" label="&font.langGroup.bengali;"/> + <menuitem value="zh-CN" label="&font.langGroup.simpl-chinese;"/> + <menuitem value="zh-HK" label="&font.langGroup.trad-chinese-hk;"/> + <menuitem value="zh-TW" label="&font.langGroup.trad-chinese;"/> + <menuitem value="x-cyrillic" label="&font.langGroup.cyrillic;"/> + <menuitem value="x-devanagari" label="&font.langGroup.devanagari;"/> + <menuitem value="x-ethi" label="&font.langGroup.ethiopic;"/> + <menuitem value="x-geor" label="&font.langGroup.georgian;"/> + <menuitem value="el" label="&font.langGroup.el;"/> + <menuitem value="x-gujr" label="&font.langGroup.gujarati;"/> + <menuitem value="x-guru" label="&font.langGroup.gurmukhi;"/> + <menuitem value="he" label="&font.langGroup.hebrew;"/> + <menuitem value="ja" label="&font.langGroup.japanese;"/> + <menuitem value="x-knda" label="&font.langGroup.kannada;"/> + <menuitem value="x-khmr" label="&font.langGroup.khmer;"/> + <menuitem value="ko" label="&font.langGroup.korean;"/> + <menuitem value="x-western" label="&font.langGroup.latin;"/> + <menuitem value="x-mlym" label="&font.langGroup.malayalam;"/> + <menuitem value="x-orya" label="&font.langGroup.oriya;"/> + <menuitem value="x-sinh" label="&font.langGroup.sinhala;"/> + <menuitem value="x-tamil" label="&font.langGroup.tamil;"/> + <menuitem value="x-telu" label="&font.langGroup.telugu;"/> + <menuitem value="th" label="&font.langGroup.thai;"/> + <menuitem value="x-tibt" label="&font.langGroup.tibetan;"/> + <menuitem value="x-cans" label="&font.langGroup.canadian;"/> + <menuitem value="x-unicode" label="&font.langGroup.other;"/> + </menupopup> + </menulist> + </caption> + + <grid> + <columns> + <column/> + <column flex="1"/> + <column/> + <column/> + </columns> + + <rows> + <row> + <separator class="thin"/> + </row> + + <row align="center"> + <hbox align="center" pack="end"> + <label accesskey="&proportional.accesskey;" control="defaultFontType">&proportional.label;</label> + </hbox> + <menulist id="defaultFontType" flex="1" style="width: 0px;"> + <menupopup> + <menuitem value="serif" label="&useDefaultFontSerif.label;"/> + <menuitem value="sans-serif" label="&useDefaultFontSansSerif.label;"/> + </menupopup> + </menulist> + <hbox align="center" pack="end"> + <label value="&size.label;" + accesskey="&sizeProportional.accesskey;" + control="sizeVar"/> + </hbox> + <menulist id="sizeVar"> + <menupopup> + <menuitem value="9" label="9"/> + <menuitem value="10" label="10"/> + <menuitem value="11" label="11"/> + <menuitem value="12" label="12"/> + <menuitem value="13" label="13"/> + <menuitem value="14" label="14"/> + <menuitem value="15" label="15"/> + <menuitem value="16" label="16"/> + <menuitem value="17" label="17"/> + <menuitem value="18" label="18"/> + <menuitem value="20" label="20"/> + <menuitem value="22" label="22"/> + <menuitem value="24" label="24"/> + <menuitem value="26" label="26"/> + <menuitem value="28" label="28"/> + <menuitem value="30" label="30"/> + <menuitem value="32" label="32"/> + <menuitem value="34" label="34"/> + <menuitem value="36" label="36"/> + <menuitem value="40" label="40"/> + <menuitem value="44" label="44"/> + <menuitem value="48" label="48"/> + <menuitem value="56" label="56"/> + <menuitem value="64" label="64"/> + <menuitem value="72" label="72"/> + </menupopup> + </menulist> + </row> + <row align="center"> + <hbox align="center" pack="end"> + <label accesskey="&serif.accesskey;" control="serif">&serif.label;</label> + </hbox> + <menulist id="serif" flex="1" style="width: 0px;" + onsyncfrompreference="return gFontsDialog.readFontSelection(document.getElementById('serif'));"/> + <spacer/> + </row> + <row align="center"> + <hbox align="center" pack="end"> + <label accesskey="&sans-serif.accesskey;" control="sans-serif">&sans-serif.label;</label> + </hbox> + <menulist id="sans-serif" flex="1" style="width: 0px;" + onsyncfrompreference="return gFontsDialog.readFontSelection(document.getElementById('sans-serif'));"/> + <spacer/> + </row> + <row align="center"> + <hbox align="center" pack="end"> + <label accesskey="&monospace.accesskey;" control="monospace">&monospace.label;</label> + </hbox> + <menulist id="monospace" flex="1" style="width: 0px;" crop="right" + onsyncfrompreference="return gFontsDialog.readFontSelection(document.getElementById('monospace'));"/> + <hbox align="center" pack="end"> + <label value="&size.label;" + accesskey="&sizeMonospace.accesskey;" + control="sizeMono"/> + </hbox> + <menulist id="sizeMono"> + <menupopup> + <menuitem value="9" label="9"/> + <menuitem value="10" label="10"/> + <menuitem value="11" label="11"/> + <menuitem value="12" label="12"/> + <menuitem value="13" label="13"/> + <menuitem value="14" label="14"/> + <menuitem value="15" label="15"/> + <menuitem value="16" label="16"/> + <menuitem value="17" label="17"/> + <menuitem value="18" label="18"/> + <menuitem value="20" label="20"/> + <menuitem value="22" label="22"/> + <menuitem value="24" label="24"/> + <menuitem value="26" label="26"/> + <menuitem value="28" label="28"/> + <menuitem value="30" label="30"/> + <menuitem value="32" label="32"/> + <menuitem value="34" label="34"/> + <menuitem value="36" label="36"/> + <menuitem value="40" label="40"/> + <menuitem value="44" label="44"/> + <menuitem value="48" label="48"/> + <menuitem value="56" label="56"/> + <menuitem value="64" label="64"/> + <menuitem value="72" label="72"/> + </menupopup> + </menulist> + </row> + </rows> + </grid> + <separator class="thin"/> + <hbox flex="1"> + <spacer flex="1"/> + <hbox align="center" pack="end"> + <label accesskey="&minSize.accesskey;" control="minSize">&minSize.label;</label> + <menulist id="minSize"> + <menupopup> + <menuitem value="0" label="&minSize.none;"/> + <menuitem value="9" label="9"/> + <menuitem value="10" label="10"/> + <menuitem value="11" label="11"/> + <menuitem value="12" label="12"/> + <menuitem value="13" label="13"/> + <menuitem value="14" label="14"/> + <menuitem value="15" label="15"/> + <menuitem value="16" label="16"/> + <menuitem value="17" label="17"/> + <menuitem value="18" label="18"/> + <menuitem value="20" label="20"/> + <menuitem value="22" label="22"/> + <menuitem value="24" label="24"/> + <menuitem value="26" label="26"/> + <menuitem value="28" label="28"/> + <menuitem value="30" label="30"/> + <menuitem value="32" label="32"/> + <menuitem value="34" label="34"/> + <menuitem value="36" label="36"/> + <menuitem value="40" label="40"/> + <menuitem value="44" label="44"/> + <menuitem value="48" label="48"/> + <menuitem value="56" label="56"/> + <menuitem value="64" label="64"/> + <menuitem value="72" label="72"/> + </menupopup> + </menulist> + </hbox> + </hbox> + <separator/> + <separator class="groove"/> + <hbox> + <checkbox id="useDocumentFonts" + label="&allowPagesToUse.label;" accesskey="&allowPagesToUse.accesskey;" + preference="browser.display.use_document_fonts" + onsyncfrompreference="return gFontsDialog.readUseDocumentFonts();" + onsynctopreference="return gFontsDialog.writeUseDocumentFonts();"/> + </hbox> + </groupbox> + + <!-- Character Encoding --> + <groupbox> + <caption label="&languages.customize.Fallback.grouplabel;"/> + <description>&languages.customize.Fallback.desc;</description> + <hbox align="center"> + <label value="&languages.customize.Fallback.label;" + accesskey="&languages.customize.Fallback.accesskey;" + control="DefaultCharsetList"/> + <menulist id="DefaultCharsetList" preference="intl.charset.fallback.override"> + <menupopup> + <menuitem label="&languages.customize.Fallback.auto;" value="*"/> + <menuitem label="&languages.customize.Fallback.utf8;" value="UTF-8"/> + <menuitem label="&languages.customize.Fallback.arabic;" value="windows-1256"/> + <menuitem label="&languages.customize.Fallback.baltic;" value="windows-1257"/> + <menuitem label="&languages.customize.Fallback.ceiso;" value="ISO-8859-2"/> + <menuitem label="&languages.customize.Fallback.cewindows;" value="windows-1250"/> + <menuitem label="&languages.customize.Fallback.simplified;" value="gbk"/> + <menuitem label="&languages.customize.Fallback.traditional;" value="Big5"/> + <menuitem label="&languages.customize.Fallback.cyrillic;" value="windows-1251"/> + <menuitem label="&languages.customize.Fallback.greek;" value="ISO-8859-7"/> + <menuitem label="&languages.customize.Fallback.hebrew;" value="windows-1255"/> + <menuitem label="&languages.customize.Fallback.japanese;" value="Shift_JIS"/> + <menuitem label="&languages.customize.Fallback.korean;" value="EUC-KR"/> + <menuitem label="&languages.customize.Fallback.thai;" value="windows-874"/> + <menuitem label="&languages.customize.Fallback.turkish;" value="windows-1254"/> + <menuitem label="&languages.customize.Fallback.vietnamese;" value="windows-1258"/> + <menuitem label="&languages.customize.Fallback.other;" value="windows-1252"/> + </menupopup> + </menulist> + </hbox> + </groupbox> + </prefpane> +</prefwindow> diff --git a/browser/components/preferences/handlers.css b/browser/components/preferences/handlers.css new file mode 100644 index 000000000..9a1d47446 --- /dev/null +++ b/browser/components/preferences/handlers.css @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +richlistitem { + -moz-binding: url("chrome://browser/content/preferences/handlers.xml#handler"); +} + +richlistitem[selected="true"] { + -moz-binding: url("chrome://browser/content/preferences/handlers.xml#handler-selected"); +} + +/** + * Make the icons appear. + * Note: we display the icon box for every item whether or not it has an icon + * so the labels of all the items align vertically. + */ +.actionsMenu > menupopup > menuitem > .menu-iconic-left { + display: -moz-box; + min-width: 16px; +} + +listitem.offlineapp { + -moz-binding: url("chrome://browser/content/preferences/handlers.xml#offlineapp"); +} diff --git a/browser/components/preferences/handlers.xml b/browser/components/preferences/handlers.xml new file mode 100644 index 000000000..5fb915cee --- /dev/null +++ b/browser/components/preferences/handlers.xml @@ -0,0 +1,81 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % applicationsDTD SYSTEM "chrome://browser/locale/preferences/applications.dtd"> + %brandDTD; + %applicationsDTD; +]> + +<bindings id="handlerBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="handler-base" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <implementation> + <property name="type" readonly="true"> + <getter> + return this.getAttribute("type"); + </getter> + </property> + </implementation> + </binding> + + <binding id="handler" extends="chrome://browser/content/preferences/handlers.xml#handler-base"> + <content> + <xul:hbox flex="1" equalsize="always"> + <xul:hbox flex="1" align="center" xbl:inherits="tooltiptext=typeDescription"> + <xul:image src="moz-icon://goat?size=16" class="typeIcon" + xbl:inherits="src=typeIcon" height="16" width="16"/> + <xul:label flex="1" crop="end" xbl:inherits="value=typeDescription"/> + </xul:hbox> + <xul:hbox flex="1" align="center" xbl:inherits="tooltiptext=actionDescription"> + <xul:image xbl:inherits="src=actionIcon" height="16" width="16" class="actionIcon"/> + <xul:label flex="1" crop="end" xbl:inherits="value=actionDescription"/> + </xul:hbox> + </xul:hbox> + </content> + </binding> + + <binding id="handler-selected" extends="chrome://browser/content/preferences/handlers.xml#handler-base"> + <content> + <xul:hbox flex="1" equalsize="always"> + <xul:hbox flex="1" align="center" xbl:inherits="tooltiptext=typeDescription"> + <xul:image src="moz-icon://goat?size=16" class="typeIcon" + xbl:inherits="src=typeIcon" height="16" width="16"/> + <xul:label flex="1" crop="end" xbl:inherits="value=typeDescription"/> + </xul:hbox> + <xul:hbox flex="1"> + <xul:menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1" + xbl:inherits="tooltiptext=actionDescription" + oncommand="gApplicationsPane.onSelectAction(event.originalTarget)"> + <xul:menupopup/> + </xul:menulist> + </xul:hbox> + </xul:hbox> + </content> + + <implementation> + <constructor> + gApplicationsPane.rebuildActionsMenu(); + </constructor> + </implementation> + + </binding> + + <binding id="offlineapp" + extends="chrome://global/content/bindings/listbox.xml#listitem"> + <content> + <children> + <xul:listcell xbl:inherits="label=origin"/> + <xul:listcell xbl:inherits="label=usage"/> + </children> + </content> + </binding> + +</bindings> diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn new file mode 100644 index 000000000..9256e3927 --- /dev/null +++ b/browser/components/preferences/jar.mn @@ -0,0 +1,44 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: +* content/browser/preferences/advanced.xul +* content/browser/preferences/advanced.js + content/browser/preferences/applications.xul +* content/browser/preferences/applications.js + content/browser/preferences/applicationManager.xul + content/browser/preferences/applicationManager.js +* content/browser/preferences/colors.xul + content/browser/preferences/cookies.xul + content/browser/preferences/cookies.js +* content/browser/preferences/content.xul + content/browser/preferences/content.js + content/browser/preferences/connection.xul + content/browser/preferences/connection.js + content/browser/preferences/fonts.xul + content/browser/preferences/fonts.js + content/browser/preferences/handlers.xml + content/browser/preferences/handlers.css + content/browser/preferences/languages.xul + content/browser/preferences/languages.js +* content/browser/preferences/main.xul +* content/browser/preferences/main.js + content/browser/preferences/newtaburl.js + content/browser/preferences/permissions.xul + content/browser/preferences/permissions.js +* content/browser/preferences/preferences.xul + content/browser/preferences/privacy.xul + content/browser/preferences/privacy.js + content/browser/preferences/sanitize.xul + content/browser/preferences/sanitize.js + content/browser/preferences/security.xul + content/browser/preferences/security.js + content/browser/preferences/selectBookmark.xul + content/browser/preferences/selectBookmark.js +#ifdef MOZ_SERVICES_SYNC + content/browser/preferences/sync.xul + content/browser/preferences/sync.js +#endif +* content/browser/preferences/tabs.xul +* content/browser/preferences/tabs.js diff --git a/browser/components/preferences/languages.js b/browser/components/preferences/languages.js new file mode 100644 index 000000000..5b8ea38a6 --- /dev/null +++ b/browser/components/preferences/languages.js @@ -0,0 +1,303 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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 gLanguagesDialog = { + + _availableLanguagesList : [], + _acceptLanguages : { }, + + _selectedItemID : null, + + init: function () + { + if (!this._availableLanguagesList.length) + this._loadAvailableLanguages(); + }, + + get _activeLanguages() + { + return document.getElementById("activeLanguages"); + }, + + get _availableLanguages() + { + return document.getElementById("availableLanguages"); + }, + + _loadAvailableLanguages: function () + { + // This is a parser for: resource://gre/res/language.properties + // The file is formatted like so: + // ab[-cd].accept=true|false + // ab = language + // cd = region + var bundleAccepted = document.getElementById("bundleAccepted"); + var bundleRegions = document.getElementById("bundleRegions"); + var bundleLanguages = document.getElementById("bundleLanguages"); + var bundlePreferences = document.getElementById("bundlePreferences"); + + function LanguageInfo(aName, aABCD, aIsVisible) + { + this.name = aName; + this.abcd = aABCD; + this.isVisible = aIsVisible; + } + + // 1) Read the available languages out of language.properties + var strings = bundleAccepted.strings; + while (strings.hasMoreElements()) { + var currString = strings.getNext(); + if (!(currString instanceof Components.interfaces.nsIPropertyElement)) + break; + + var property = currString.key.split("."); // ab[-cd].accept + if (property[1] == "accept") { + var abCD = property[0]; + var abCDPairs = abCD.split("-"); // ab[-cd] + var useABCDFormat = abCDPairs.length > 1; + var ab = useABCDFormat ? abCDPairs[0] : abCD; + var cd = useABCDFormat ? abCDPairs[1] : ""; + if (ab) { + var language = ""; + try { + language = bundleLanguages.getString(ab); + } + catch (e) { continue; }; + + var region = ""; + if (useABCDFormat) { + try { + region = bundleRegions.getString(cd); + } + catch (e) { continue; } + } + + var name = ""; + if (useABCDFormat) + name = bundlePreferences.getFormattedString("languageRegionCodeFormat", + [language, region, abCD]); + else + name = bundlePreferences.getFormattedString("languageCodeFormat", + [language, abCD]); + + if (name && abCD) { + var isVisible = currString.value == "true" && + (!(abCD in this._acceptLanguages) || !this._acceptLanguages[abCD]); + var li = new LanguageInfo(name, abCD, isVisible); + this._availableLanguagesList.push(li); + } + } + } + } + this._buildAvailableLanguageList(); + }, + + _buildAvailableLanguageList: function () + { + var availableLanguagesPopup = document.getElementById("availableLanguagesPopup"); + while (availableLanguagesPopup.hasChildNodes()) + availableLanguagesPopup.removeChild(availableLanguagesPopup.firstChild); + + // Sort the list of languages by name + this._availableLanguagesList.sort(function (a, b) { + return a.name.localeCompare(b.name); + }); + + // Load the UI with the data + for (var i = 0; i < this._availableLanguagesList.length; ++i) { + var abCD = this._availableLanguagesList[i].abcd; + if (this._availableLanguagesList[i].isVisible && + (!(abCD in this._acceptLanguages) || !this._acceptLanguages[abCD])) { + var menuitem = document.createElement("menuitem"); + menuitem.id = this._availableLanguagesList[i].abcd; + availableLanguagesPopup.appendChild(menuitem); + menuitem.setAttribute("label", this._availableLanguagesList[i].name); + } + } + }, + + readAcceptLanguages: function () + { + while (this._activeLanguages.hasChildNodes()) + this._activeLanguages.removeChild(this._activeLanguages.firstChild); + + var selectedIndex = 0; + var preference = document.getElementById("intl.accept_languages"); + if (preference.value == "") + return undefined; + var languages = preference.value.toLowerCase().split(/\s*,\s*/); + for (var i = 0; i < languages.length; ++i) { + var name = this._getLanguageName(languages[i]); + if (!name) + name = "[" + languages[i] + "]"; + var listitem = document.createElement("listitem"); + listitem.id = languages[i]; + if (languages[i] == this._selectedItemID) + selectedIndex = i; + this._activeLanguages.appendChild(listitem); + listitem.setAttribute("label", name); + + // Hash this language as an "Active" language so we don't + // show it in the list that can be added. + this._acceptLanguages[languages[i]] = true; + } + + if (this._activeLanguages.childNodes.length > 0) { + this._activeLanguages.ensureIndexIsVisible(selectedIndex); + this._activeLanguages.selectedIndex = selectedIndex; + } + + return undefined; + }, + + writeAcceptLanguages: function () + { + return undefined; + }, + + onAvailableLanguageSelect: function () + { + var addButton = document.getElementById("addButton"); + addButton.disabled = false; + + this._availableLanguages.removeAttribute("accesskey"); + }, + + addLanguage: function () + { + var selectedID = this._availableLanguages.selectedItem.id; + var preference = document.getElementById("intl.accept_languages"); + var arrayOfPrefs = preference.value.toLowerCase().split(/\s*,\s*/); + for (var i = 0; i < arrayOfPrefs.length; ++i ){ + if (arrayOfPrefs[i] == selectedID) + return; + } + + this._selectedItemID = selectedID; + + if (preference.value == "") + preference.value = selectedID; + else { + arrayOfPrefs.unshift(selectedID); + preference.value = arrayOfPrefs.join(","); + } + + this._acceptLanguages[selectedID] = true; + this._availableLanguages.selectedItem = null; + + // Rebuild the available list with the added item removed... + this._buildAvailableLanguageList(); + + this._availableLanguages.setAttribute("label", this._availableLanguages.getAttribute("label2")); + }, + + removeLanguage: function () + { + // Build the new preference value string. + var languagesArray = []; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + if (!item.selected) + languagesArray.push(item.id); + else + this._acceptLanguages[item.id] = false; + } + var string = languagesArray.join(","); + + // Get the item to select after the remove operation completes. + var selection = this._activeLanguages.selectedItems; + var lastSelected = selection[selection.length-1]; + var selectItem = lastSelected.nextSibling || lastSelected.previousSibling; + selectItem = selectItem ? selectItem.id : null; + + this._selectedItemID = selectItem; + + // Update the preference and force a UI rebuild + var preference = document.getElementById("intl.accept_languages"); + preference.value = string; + + this._buildAvailableLanguageList(); + }, + + _getLanguageName: function (aABCD) + { + if (!this._availableLanguagesList.length) + this._loadAvailableLanguages(); + for (var i = 0; i < this._availableLanguagesList.length; ++i) { + if (aABCD == this._availableLanguagesList[i].abcd) + return this._availableLanguagesList[i].name; + } + return ""; + }, + + moveUp: function () + { + var selectedItem = this._activeLanguages.selectedItems[0]; + var previousItem = selectedItem.previousSibling; + + var string = ""; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + string += (i == 0 ? "" : ","); + if (item.id == previousItem.id) + string += selectedItem.id; + else if (item.id == selectedItem.id) + string += previousItem.id; + else + string += item.id; + } + + this._selectedItemID = selectedItem.id; + + // Update the preference and force a UI rebuild + var preference = document.getElementById("intl.accept_languages"); + preference.value = string; + }, + + moveDown: function () + { + var selectedItem = this._activeLanguages.selectedItems[0]; + var nextItem = selectedItem.nextSibling; + + var string = ""; + for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) { + var item = this._activeLanguages.childNodes[i]; + string += (i == 0 ? "" : ","); + if (item.id == nextItem.id) + string += selectedItem.id; + else if (item.id == selectedItem.id) + string += nextItem.id; + else + string += item.id; + } + + this._selectedItemID = selectedItem.id; + + // Update the preference and force a UI rebuild + var preference = document.getElementById("intl.accept_languages"); + preference.value = string; + }, + + onLanguageSelect: function () + { + var upButton = document.getElementById("up"); + var downButton = document.getElementById("down"); + var removeButton = document.getElementById("remove"); + switch (this._activeLanguages.selectedCount) { + case 0: + upButton.disabled = downButton.disabled = removeButton.disabled = true; + break; + case 1: + upButton.disabled = this._activeLanguages.selectedIndex == 0; + downButton.disabled = this._activeLanguages.selectedIndex == this._activeLanguages.childNodes.length - 1; + removeButton.disabled = false; + break; + default: + upButton.disabled = true; + downButton.disabled = true; + removeButton.disabled = false; + } + } +}; + diff --git a/browser/components/preferences/languages.xul b/browser/components/preferences/languages.xul new file mode 100644 index 000000000..bd74e11cf --- /dev/null +++ b/browser/components/preferences/languages.xul @@ -0,0 +1,94 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE prefwindow SYSTEM "chrome://browser/locale/preferences/languages.dtd"> + +<?xml-stylesheet href="chrome://global/skin/"?> + +<prefwindow id="LanguagesDialog" type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&languages.customize.Header;" + dlgbuttons="accept,cancel,help" + ondialoghelp="openPrefsHelp()" + style="width: &window.width;;"> + + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + + <prefpane id="LanguagesDialogPane" + onpaneload="gLanguagesDialog.init();" + helpTopic="prefs-languages"> + + <preferences> + <preference id="intl.accept_languages" name="intl.accept_languages" type="wstring"/> + <preference id="pref.browser.language.disable_button.up" + name="pref.browser.language.disable_button.up" + type="bool"/> + <preference id="pref.browser.language.disable_button.down" + name="pref.browser.language.disable_button.down" + type="bool"/> + <preference id="pref.browser.language.disable_button.remove" + name="pref.browser.language.disable_button.remove" + type="bool"/> + </preferences> + + <script type="application/javascript" src="chrome://browser/content/preferences/languages.js"/> + + <stringbundleset id="languageSet"> + <stringbundle id="bundleRegions" src="chrome://global/locale/regionNames.properties"/> + <stringbundle id="bundleLanguages" src="chrome://global/locale/languageNames.properties"/> + <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> + <stringbundle id="bundleAccepted" src="resource://gre/res/language.properties"/> + </stringbundleset> + + <description>&languages.customize.prefLangDescript;</description> + <label>&languages.customize.active.label;</label> + <grid flex="1"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows> + <row flex="1"> + <listbox id="activeLanguages" flex="1" rows="6" + seltype="multiple" onselect="gLanguagesDialog.onLanguageSelect();" + preference="intl.accept_languages" + onsyncfrompreference="return gLanguagesDialog.readAcceptLanguages();" + onsynctopreference="return gLanguagesDialog.writeAcceptLanguages();"/> + <vbox> + <button id="up" class="up" oncommand="gLanguagesDialog.moveUp();" disabled="true" + label="&languages.customize.moveUp.label;" + accesskey="&languages.customize.moveUp.accesskey;" + preference="pref.browser.language.disable_button.up"/> + <button id="down" class="down" oncommand="gLanguagesDialog.moveDown();" disabled="true" + label="&languages.customize.moveDown.label;" + accesskey="&languages.customize.moveDown.accesskey;" + preference="pref.browser.language.disable_button.down"/> + <button id="remove" oncommand="gLanguagesDialog.removeLanguage();" disabled="true" + label="&languages.customize.deleteButton.label;" + accesskey="&languages.customize.deleteButton.accesskey;" + preference="pref.browser.language.disable_button.remove"/> + </vbox> + </row> + <row> + <separator class="thin"/> + </row> + <row> + <menulist id="availableLanguages" oncommand="gLanguagesDialog.onAvailableLanguageSelect();" + label="&languages.customize.selectLanguage.label;" + label2="&languages.customize.selectLanguage.label;"> + <menupopup id="availableLanguagesPopup"/> + </menulist> + <button id="addButton" oncommand="gLanguagesDialog.addLanguage();" disabled="true" + label="&languages.customize.addButton.label;" + accesskey="&languages.customize.addButton.accesskey;"/> + </row> + </rows> + </grid> + <separator/> + <separator/> + </prefpane> +</prefwindow> + diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js new file mode 100644 index 000000000..d4daeeab3 --- /dev/null +++ b/browser/components/preferences/main.js @@ -0,0 +1,543 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); + +var gMainPane = { + _pane: null, + + /** + * Initialization of this. + */ + init: function () + { + this._pane = document.getElementById("paneMain"); + + // set up the "use current page" label-changing listener + this._updateUseCurrentButton(); + window.addEventListener("focus", this._updateUseCurrentButton.bind(this), false); + + this.updateBrowserStartupLastSession(); + + this.setupDownloadsWindowOptions(); + +#ifdef HAVE_SHELL_SERVICE + this.updateSetDefaultBrowser(); +#ifdef XP_WIN + // In Windows 8 we launch the control panel since it's the only + // way to get all file type association prefs. So we don't know + // when the user will select the default. We refresh here periodically + // in case the default changes. On other Windows OS's defaults can also + // be set while the prefs are open. + window.setInterval(this.updateSetDefaultBrowser, 1000); +#endif +#endif + + // Notify observers that the UI is now ready + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(window, "main-pane-loaded", null); + }, + + setupDownloadsWindowOptions: function () + { + let showWhenDownloading = document.getElementById("showWhenDownloading"); + let closeWhenDone = document.getElementById("closeWhenDone"); + + // These radio buttons should be hidden when the Downloads Panel is enabled. + let shouldHide = !DownloadsCommon.useToolkitUI; + showWhenDownloading.hidden = shouldHide; + closeWhenDone.hidden = shouldHide; + }, + + // HOME PAGE + + /* + * Preferences: + * + * browser.startup.homepage + * - the user's home page, as a string; if the home page is a set of tabs, + * this will be those URLs separated by the pipe character "|" + * browser.startup.page + * - what page(s) to show when the user starts the application, as an integer: + * + * 0: a blank page + * 1: the home page (as set by the browser.startup.homepage pref) + * 2: the last page the user visited (DEPRECATED) + * 3: windows and tabs from the last session (a.k.a. session restore) + * + * The deprecated option is not exposed in UI; however, if the user has it + * selected and doesn't change the UI for this preference, the deprecated + * option is preserved. + */ + + syncFromHomePref: function () + { + let homePref = document.getElementById("browser.startup.homepage"); + + // If the pref is set to about:home, set the value to "" to show the + // placeholder text (about:home title). + if (homePref.value.toLowerCase() == "about:home") + return ""; + + // If the pref is actually "", show a blank page. The actual home page + // loading code treats them the same, and we don't want the placeholder text + // to be shown. + if (homePref.value == "") + return "about:logopage"; + + // Otherwise, show the actual pref value. + return undefined; + }, + + syncToHomePref: function (value) + { + // If the value is "", use about:home. + if (value == "") + return "about:home"; + + // Otherwise, use the actual textbox value. + return undefined; + }, + + /** + * Sets the home page to the current displayed page (or frontmost tab, if the + * most recent browser window contains multiple tabs), updating preference + * window UI to reflect this. + */ + setHomePageToCurrent: function () + { + let homePage = document.getElementById("browser.startup.homepage"); + let tabs = this._getTabsForHomePage(); + function getTabURI(t) t.linkedBrowser.currentURI.spec; + + // FIXME Bug 244192: using dangerous "|" joiner! + if (tabs.length) + homePage.value = tabs.map(getTabURI).join("|"); + }, + + /** + * Displays a dialog in which the user can select a bookmark to use as home + * page. If the user selects a bookmark, that bookmark's name is displayed in + * UI and the bookmark's address is stored to the home page preference. + */ + setHomePageToBookmark: function () + { + var rv = { urls: null, names: null }; + document.documentElement.openSubDialog("chrome://browser/content/preferences/selectBookmark.xul", + "resizable", rv); + if (rv.urls && rv.names) { + var homePage = document.getElementById("browser.startup.homepage"); + + // XXX still using dangerous "|" joiner! + homePage.value = rv.urls.join("|"); + } + }, + + /** + * Switches the "Use Current Page" button between its singular and plural + * forms. + */ + _updateUseCurrentButton: function () { + let useCurrent = document.getElementById("useCurrent"); + + let tabs = this._getTabsForHomePage(); + if (tabs.length > 1) + useCurrent.label = useCurrent.getAttribute("label2"); + else + useCurrent.label = useCurrent.getAttribute("label1"); + + // In this case, the button's disabled state is set by preferences.xml. + if (document.getElementById + ("pref.browser.homepage.disable_button.current_page").locked) + return; + + useCurrent.disabled = !tabs.length + }, + + _getTabsForHomePage: function () + { + var win; + var tabs = []; + if (document.documentElement.instantApply) { + const Cc = Components.classes, Ci = Components.interfaces; + // If we're in instant-apply mode, use the most recent browser window + var wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + win = wm.getMostRecentWindow("navigator:browser"); + } + else { + win = window.opener; + } + + if (win && win.document.documentElement + .getAttribute("windowtype") == "navigator:browser") { + // We should only include visible & non-pinned tabs + tabs = win.gBrowser.visibleTabs.slice(win.gBrowser._numPinnedTabs); + } + + return tabs; + }, + + /** + * Restores the default home page as the user's home page. + */ + restoreDefaultHomePage: function () + { + var homePage = document.getElementById("browser.startup.homepage"); + homePage.value = homePage.defaultValue; + }, + + // DOWNLOADS + + /* + * Preferences: + * + * browser.download.showWhenStarting - bool + * True if the Download Manager should be opened when a download is + * started, false if it shouldn't be opened. + * browser.download.closeWhenDone - bool + * True if the Download Manager should be closed when all downloads + * complete, false if it should be left open. + * browser.download.useDownloadDir - bool + * True - Save files directly to the folder configured via the + * browser.download.folderList preference. + * False - Always ask the user where to save a file and default to + * browser.download.lastDir when displaying a folder picker dialog. + * browser.download.dir - local file handle + * A local folder the user may have selected for downloaded files to be + * saved. Migration of other browser settings may also set this path. + * This folder is enabled when folderList equals 2. + * browser.download.lastDir - local file handle + * May contain the last folder path accessed when the user browsed + * via the file save-as dialog. (see contentAreaUtils.js) + * browser.download.folderList - int + * Indicates the location users wish to save downloaded files too. + * It is also used to display special file labels when the default + * download location is either the Desktop or the Downloads folder. + * Values: + * 0 - The desktop is the default download location. + * 1 - The system's downloads folder is the default download location. + * 2 - The default download location is elsewhere as specified in + * browser.download.dir. + * browser.download.downloadDir + * deprecated. + * browser.download.defaultFolder + * deprecated. + */ + + /** + * Updates preferences which depend upon the value of the preference which + * determines whether the Downloads manager is opened at the start of a + * download. + */ + readShowDownloadsWhenStarting: function () + { + this.showDownloadsWhenStartingPrefChanged(); + + // don't override the preference's value in UI + return undefined; + }, + + /** + * Enables or disables the "close Downloads manager when downloads finished" + * preference element, consequently updating the associated UI. + */ + showDownloadsWhenStartingPrefChanged: function () + { + var showWhenStartingPref = document.getElementById("browser.download.manager.showWhenStarting"); + var closeWhenDonePref = document.getElementById("browser.download.manager.closeWhenDone"); + closeWhenDonePref.disabled = !showWhenStartingPref.value; + }, + + /** + * Enables/disables the folder field and Browse button based on whether a + * default download directory is being used. + */ + readUseDownloadDir: function () + { + var downloadFolder = document.getElementById("downloadFolder"); + var chooseFolder = document.getElementById("chooseFolder"); + var preference = document.getElementById("browser.download.useDownloadDir"); + downloadFolder.disabled = !preference.value; + chooseFolder.disabled = !preference.value; + + // don't override the preference's value in UI + return undefined; + }, + + /** + * Displays a file picker in which the user can choose the location where + * downloads are automatically saved, updating preferences and UI in + * response to the choice, if one is made. + */ + chooseFolder: function () + { + const nsIFilePicker = Components.interfaces.nsIFilePicker; + const nsILocalFile = Components.interfaces.nsILocalFile; + + let bundlePreferences = document.getElementById("bundlePreferences"); + let title = bundlePreferences.getString("chooseDownloadFolderTitle"); + let folderListPref = document.getElementById("browser.download.folderList"); + let currentDirPref = this._indexToFolder(folderListPref.value); // file + let defDownloads = this._indexToFolder(1); // file + let fp = Components.classes["@mozilla.org/filepicker;1"]. + createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + let file = fp.file.QueryInterface(nsILocalFile); + let downloadDirPref = document.getElementById("browser.download.dir"); + + downloadDirPref.value = file; + folderListPref.value = this._folderToIndex(file); + // Note, the real prefs will not be updated yet, so dnld manager's + // userDownloadsDirectory may not return the right folder after + // this code executes. displayDownloadDirPref will be called on + // the assignment above to update the UI. + } + }.bind(this); + + fp.init(window, title, nsIFilePicker.modeGetFolder); + fp.appendFilters(nsIFilePicker.filterAll); + // First try to open what's currently configured + if (currentDirPref && currentDirPref.exists()) { + fp.displayDirectory = currentDirPref; + } // Try the system's download dir + else if (defDownloads && defDownloads.exists()) { + fp.displayDirectory = defDownloads; + } // Fall back to Desktop + else { + fp.displayDirectory = this._indexToFolder(0); + } + fp.open(fpCallback); + }, + + /** + * Initializes the download folder display settings based on the user's + * preferences. + */ + displayDownloadDirPref: function () + { + var folderListPref = document.getElementById("browser.download.folderList"); + var bundlePreferences = document.getElementById("bundlePreferences"); + var downloadFolder = document.getElementById("downloadFolder"); + var currentDirPref = document.getElementById("browser.download.dir"); + + // Used in defining the correct path to the folder icon. + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var fph = ios.getProtocolHandler("file") + .QueryInterface(Components.interfaces.nsIFileProtocolHandler); + var iconUrlSpec; + + // Display a 'pretty' label or the path in the UI. + if (folderListPref.value == 2) { + // Custom path selected and is configured + downloadFolder.label = this._getDisplayNameOfFile(currentDirPref.value); + iconUrlSpec = fph.getURLSpecFromFile(currentDirPref.value); + } else if (folderListPref.value == 1) { + // 'Downloads' + // In 1.5, this pointed to a folder we created called 'My Downloads' + // and was available as an option in the 1.5 drop down. On XP this + // was in My Documents, on OSX it was in User Docs. In 2.0, we did + // away with the drop down option, although the special label was + // still supported for the folder if it existed. Because it was + // not exposed it was rarely used. + // With 3.0, a new desktop folder - 'Downloads' was introduced for + // platforms and versions that don't support a default system downloads + // folder. See nsDownloadManager for details. + downloadFolder.label = bundlePreferences.getString("downloadsFolderName"); + iconUrlSpec = fph.getURLSpecFromFile(this._indexToFolder(1)); + } else { + // 'Desktop' + downloadFolder.label = bundlePreferences.getString("desktopFolderName"); + iconUrlSpec = fph.getURLSpecFromFile(this._getDownloadsFolder("Desktop")); + } + downloadFolder.image = "moz-icon://" + iconUrlSpec + "?size=16"; + + // don't override the preference's value in UI + return undefined; + }, + + /** + * Returns the textual path of a folder in readable form. + */ + _getDisplayNameOfFile: function (aFolder) + { + // TODO: would like to add support for 'Downloads on Macintosh HD' + // for OS X users. + return aFolder ? aFolder.path : ""; + }, + + /** + * Returns the Downloads folder. If aFolder is "Desktop", then the Downloads + * folder returned is the desktop folder; otherwise, it is a folder whose name + * indicates that it is a download folder and whose path is as determined by + * the XPCOM directory service via the download manager's attribute + * defaultDownloadsDirectory. + * + * @throws if aFolder is not "Desktop" or "Downloads" + */ + _getDownloadsFolder: function (aFolder) + { + switch (aFolder) { + case "Desktop": + var fileLoc = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); + return fileLoc.get("Desk", Components.interfaces.nsILocalFile); + break; + case "Downloads": + var dnldMgr = Components.classes["@mozilla.org/download-manager;1"] + .getService(Components.interfaces.nsIDownloadManager); + return dnldMgr.defaultDownloadsDirectory; + break; + } + throw "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'"; + }, + + /** + * Determines the type of the given folder. + * + * @param aFolder + * the folder whose type is to be determined + * @returns integer + * 0 if aFolder is the Desktop or is unspecified, + * 1 if aFolder is the Downloads folder, + * 2 otherwise + */ + _folderToIndex: function (aFolder) + { + if (!aFolder || aFolder.equals(this._getDownloadsFolder("Desktop"))) + return 0; + else if (aFolder.equals(this._getDownloadsFolder("Downloads"))) + return 1; + return 2; + }, + + /** + * Converts an integer into the corresponding folder. + * + * @param aIndex + * an integer + * @returns the Desktop folder if aIndex == 0, + * the Downloads folder if aIndex == 1, + * the folder stored in browser.download.dir + */ + _indexToFolder: function (aIndex) + { + switch (aIndex) { + case 0: + return this._getDownloadsFolder("Desktop"); + case 1: + return this._getDownloadsFolder("Downloads"); + } + var currentDirPref = document.getElementById("browser.download.dir"); + return currentDirPref.value; + }, + + /** + * Returns the value for the browser.download.folderList preference. + */ + getFolderListPref: function () + { + var folderListPref = document.getElementById("browser.download.folderList"); + switch (folderListPref.value) { + case 0: // Desktop + case 1: // Downloads + return folderListPref.value; + break; + case 2: // Custom + var currentDirPref = document.getElementById("browser.download.dir"); + if (currentDirPref.value) { + // Resolve to a known location if possible. We are writing out + // to prefs on this call, so now would be a good time to do it. + return this._folderToIndex(currentDirPref.value); + } + return 0; + break; + } + }, + + /** + * Hide/show the "Show my windows and tabs from last time" option based + * on the value of the browser.privatebrowsing.autostart pref. + */ + updateBrowserStartupLastSession: function() + { + let pbAutoStartPref = document.getElementById("browser.privatebrowsing.autostart"); + let startupPref = document.getElementById("browser.startup.page"); + let menu = document.getElementById("browserStartupPage"); + let option = document.getElementById("browserStartupLastSession"); + if (pbAutoStartPref.value) { + option.setAttribute("disabled", "true"); + if (option.selected) { + menu.selectedItem = document.getElementById("browserStartupHomePage"); + } + } else { + option.removeAttribute("disabled"); + startupPref.updateElements(); // select the correct index in the startup menulist + } + } +#ifdef HAVE_SHELL_SERVICE + , + + // SYSTEM DEFAULTS + + /* + * Preferences: + * + * browser.shell.checkDefault + * - true if a default-browser check (and prompt to make it so if necessary) + * occurs at startup, false otherwise + */ + + /** + * Show button for setting browser as default browser or information that + * browser is already the default browser. + */ + updateSetDefaultBrowser: function() + { + let shellSvc = getShellService(); + let setDefaultPane = document.getElementById("setDefaultPane"); + if (!shellSvc) { + setDefaultPane.hidden = true; + document.getElementById("alwaysCheckDefault").disabled = true; + return; + } + let selectedIndex = + shellSvc.isDefaultBrowser(false, true) ? 1 : 0; + setDefaultPane.selectedIndex = selectedIndex; + }, + + /** + * Set browser as the operating system default browser. + */ + setDefaultBrowser: function() + { + let shellSvc = getShellService(); + if (!shellSvc) + return; + try { + let claimAllTypes = true; +#ifdef XP_WIN + // In Windows 8+, the UI for selecting default protocol is much + // nicer than the UI for setting file type associations. So we + // only show the protocol association screen on Windows 8+. + // Windows 8 is version 6.2. + let version = Services.sysinfo.getProperty("version"); + claimAllTypes = (parseFloat(version) < 6.2); +#endif + shellSvc.setDefaultBrowser(claimAllTypes, false); + } catch (ex) { + Cu.reportError(ex); + return; + } + let selectedIndex = + shellSvc.isDefaultBrowser(false, true) ? 1 : 0; + document.getElementById("setDefaultPane").selectedIndex = selectedIndex; + } +#endif +}; diff --git a/browser/components/preferences/main.xul b/browser/components/preferences/main.xul new file mode 100644 index 000000000..0943e5580 --- /dev/null +++ b/browser/components/preferences/main.xul @@ -0,0 +1,216 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % mainDTD SYSTEM "chrome://browser/locale/preferences/main.dtd"> + <!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> + %brandDTD; + %mainDTD; + %aboutHomeDTD; +]> + +<overlay id="MainPaneOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <prefpane id="paneMain" + onpaneload="gMainPane.init();" + helpTopic="prefs-main"> + + <script type="application/javascript" src="chrome://browser/content/preferences/main.js"/> + + <preferences id="mainPreferences"> + <!-- XXX Button preferences --> + + <!-- Startup --> + <preference id="browser.startup.page" + name="browser.startup.page" + type="int"/> + <preference id="browser.startup.homepage" + name="browser.startup.homepage" + type="wstring"/> + + <preference id="pref.browser.homepage.disable_button.current_page" + name="pref.browser.homepage.disable_button.current_page" + type="bool"/> + <preference id="pref.browser.homepage.disable_button.bookmark_page" + name="pref.browser.homepage.disable_button.bookmark_page" + type="bool"/> + <preference id="pref.browser.homepage.disable_button.restore_default" + name="pref.browser.homepage.disable_button.restore_default" + type="bool"/> + + <preference id="browser.privatebrowsing.autostart" + name="browser.privatebrowsing.autostart" + type="bool" + onchange="gMainPane.updateBrowserStartupLastSession();"/> + + <!-- Downloads --> + <preference id="browser.download.manager.showWhenStarting" + name="browser.download.manager.showWhenStarting" + type="bool" + onchange="gMainPane.showDownloadsWhenStartingPrefChanged();"/> + <preference id="browser.download.manager.closeWhenDone" + name="browser.download.manager.closeWhenDone" + type="bool"/> + <preference id="browser.download.useDownloadDir" + name="browser.download.useDownloadDir" + type="bool"/> + <preference id="browser.download.dir" + name="browser.download.dir" + type="file" + onchange="gMainPane.displayDownloadDirPref();"/> + <preference id="browser.download.folderList" name="browser.download.folderList" type="int"/> + <preference id="browser.download.useToolkitUI" name="browser.download.useToolkitUI" type="bool" /> +#ifdef XP_WIN + <preference id="browser.download.saveZoneInformation" name="browser.download.saveZoneInformation" type="int" /> +#endif + +#ifdef HAVE_SHELL_SERVICE + <!-- System Defaults --> + <preference id="browser.shell.checkDefaultBrowser" + name="browser.shell.checkDefaultBrowser" + type="bool"/> + + <preference id="pref.general.disable_button.default_browser" + name="pref.general.disable_button.default_browser" + type="bool"/> +#endif + </preferences> + +#ifdef HAVE_SHELL_SERVICE + <stringbundle id="bundleShell" src="chrome://browser/locale/shellservice.properties"/> + <stringbundle id="bundleBrand" src="chrome://branding/locale/brand.properties"/> +#endif + + <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> + + <!-- Startup --> + <groupbox id="startupGroup"> + <caption label="&startup.label;"/> + + <hbox align="center"> + <label value="&startupPage.label;" accesskey="&startupPage.accesskey;" + control="browserStartupPage"/> + <menulist id="browserStartupPage" preference="browser.startup.page"> + <menupopup> + <menuitem label="&startupHomePage.label;" value="1" id="browserStartupHomePage"/> + <menuitem label="&startupBlankPage.label;" value="0" id="browserStartupBlank"/> + <menuitem label="&startupLastSession.label;" value="3" id="browserStartupLastSession"/> + </menupopup> + </menulist> + </hbox> + <separator class="thin"/> + <hbox align="center"> + <label value="&homepage.label;" accesskey="&homepage.accesskey;" control="browserHomePage"/> + <textbox id="browserHomePage" class="padded uri-element" flex="1" + type="autocomplete" autocompletesearch="history" + onsyncfrompreference="return gMainPane.syncFromHomePref();" + onsynctopreference="return gMainPane.syncToHomePref(this.value);" + oninput="gNewtabUrl.writeNewtabUrl(null, this.value);" + placeholder="&abouthome.pageTitle;" + preference="browser.startup.homepage"/> + </hbox> + <hbox align="center" pack="end"> + <button label="" accesskey="&useCurrentPage.accesskey;" + label1="&useCurrentPage.label;" + label2="&useMultiple.label;" + oncommand="gMainPane.setHomePageToCurrent(); gNewtabUrl.writeNewtabUrl();" + id="useCurrent" + preference="pref.browser.homepage.disable_button.current_page"/> + <button label="&chooseBookmark.label;" accesskey="&chooseBookmark.accesskey;" + oncommand="gMainPane.setHomePageToBookmark(); gNewtabUrl.writeNewtabUrl();" + id="useBookmark" + preference="pref.browser.homepage.disable_button.bookmark_page"/> + <button label="&restoreDefault.label;" accesskey="&restoreDefault.accesskey;" + oncommand="gMainPane.restoreDefaultHomePage(); gNewtabUrl.writeNewtabUrl();" + id="restoreDefaultHomePage" + preference="pref.browser.homepage.disable_button.restore_default"/> + </hbox> + </groupbox> + + <!-- Downloads --> + <groupbox id="downloadsGroup"> + <caption label="&downloads.label;"/> + + <checkbox id="showWhenDownloading" label="&showWhenDownloading.label;" + accesskey="&showWhenDownloading.accesskey;" + preference="browser.download.manager.showWhenStarting" + onsyncfrompreference="return gMainPane.readShowDownloadsWhenStarting();"/> + <checkbox id="closeWhenDone" label="&closeWhenDone.label;" + accesskey="&closeWhenDone.accesskey;" class="indent" + preference="browser.download.manager.closeWhenDone"/> + + <separator class="thin"/> + + <radiogroup id="saveWhere" + preference="browser.download.useDownloadDir" + onsyncfrompreference="return gMainPane.readUseDownloadDir();"> + <hbox id="saveToRow"> + <radio id="saveTo" value="true" + label="&saveTo.label;" + accesskey="&saveTo.accesskey;" + aria-labelledby="saveTo downloadFolder"/> + <filefield id="downloadFolder" flex="1" + preference="browser.download.folderList" + preference-editable="true" + aria-labelledby="saveTo" + onsyncfrompreference="return gMainPane.displayDownloadDirPref();" + onsynctopreference="return gMainPane.getFolderListPref()"/> + <button id="chooseFolder" oncommand="gMainPane.chooseFolder();" + accesskey="&chooseFolderWin.accesskey;" + label="&chooseFolderWin.label;" + preference="browser.download.folderList" + onsynctopreference="return gMainPane.getFolderListPref();"/> + </hbox> + <radio id="alwaysAsk" value="false" + label="&alwaysAsk.label;" + accesskey="&alwaysAsk.accesskey;"/> + </radiogroup> +#if 0 +<!-- Disabled for now -- ToolkitUI DM is nonfunctional. --> + <checkbox id="classicDownloadWindow" + preference="browser.download.useToolkitUI" + label="&toolkit.classic.download.window.label;" /> +#endif +#ifdef XP_WIN + <hbox align="center"> + <label id="zoneInfoLabel" control="zoneInfo-menu">&zoneInfo.label;</label> + <menulist id="zoneInfo-menu" + preference="browser.download.saveZoneInformation" + sizetopopup="always"> + <menupopup> + <menuitem label="&zoneInfo.never;" value="0" /> + <menuitem label="&zoneInfo.always;" value="1" /> + <menuitem label="&zoneInfo.system;" value="2" /> + </menupopup> + </menulist> + </hbox> +#endif + </groupbox> + +#ifdef HAVE_SHELL_SERVICE + <!-- System Defaults --> + <groupbox id="systemDefaultsGroup" orient="vertical"> + <caption label="&systemDefaults.label;"/> + + <checkbox id="alwaysCheckDefault" preference="browser.shell.checkDefaultBrowser" + label="&alwaysCheckDefault.label;" accesskey="&alwaysCheckDefault.accesskey;" + flex="1"/> + <hbox class="indent"> + <deck id="setDefaultPane"> + <button id="setDefaultButton" + label="&setDefault.label;" accesskey="&setDefault.accesskey;" + oncommand="gMainPane.setDefaultBrowser();" + preference="pref.general.disable_button.default_browser"/> + <description>&isDefault.label;</description> + </deck> + </hbox> + </groupbox> +#endif + </prefpane> + +</overlay> diff --git a/browser/components/preferences/moz.build b/browser/components/preferences/moz.build new file mode 100644 index 000000000..c888607f0 --- /dev/null +++ b/browser/components/preferences/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + + +for var in ('MOZ_APP_NAME', 'MOZ_MACBUNDLE_NAME'): + DEFINES[var] = CONFIG[var] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'): + DEFINES['HAVE_SHELL_SERVICE'] = 1 + +JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file diff --git a/browser/components/preferences/newtaburl.js b/browser/components/preferences/newtaburl.js new file mode 100644 index 000000000..f9103f00d --- /dev/null +++ b/browser/components/preferences/newtaburl.js @@ -0,0 +1,102 @@ +// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.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 gNewtabUrl = {
+ /**
+ * Writes browser.newtab.url with the appropriate value.
+ * If the choice is "my home page", get and sanitize
+ * the browser home page URL to make it suitable for newtab use.
+ *
+ * Called from prefwindow ondialogaccept in preferences.xul,
+ * newtabPage oncommand in tabs.xul, browserHomePage oninput,
+ * useCurrent, useBookmark and restoreDefaultHomePage oncommand
+ * in main.xul to consider instantApply.
+ */
+ writeNewtabUrl: function(newtabUrlChoice, browserHomepageUrl) {
+ try {
+ if (newtabUrlChoice) {
+ if (Services.prefs.getBoolPref("browser.preferences.instantApply")) {
+ newtabUrlChoice = parseInt(newtabUrlChoice);
+ } else {
+ return;
+ }
+ } else {
+ if (this.newtabUrlChoiceIsSet) {
+ newtabUrlChoice = Services.prefs.getIntPref("browser.newtab.choice");
+ } else {
+ newtabUrlChoice = this.getNewtabChoice();
+ }
+ }
+ if (browserHomepageUrl || browserHomepageUrl == "") {
+ if (Services.prefs.getBoolPref("browser.preferences.instantApply")) {
+ if (browserHomepageUrl == "") {
+ browserHomepageUrl = "about:home";
+ }
+ } else {
+ return;
+ }
+ } else {
+ browserHomepageUrl = Services.prefs.getComplexValue("browser.startup.homepage",
+ Components.interfaces.nsIPrefLocalizedString).data;
+ }
+ let newtabUrlPref = Services.prefs.getCharPref("browser.newtab.url");
+ switch (newtabUrlChoice) {
+ case 1:
+ newtabUrlPref = "about:logopage";
+ break;
+ case 2:
+ newtabUrlPref = Services.prefs.getDefaultBranch("browser.")
+ .getComplexValue("startup.homepage",
+ Components.interfaces.nsIPrefLocalizedString).data;
+ break;
+ case 3:
+ // If url is a pipe-delimited set of pages, just take the first one.
+ let newtabUrlSanitizedPref=browserHomepageUrl.split("|")[0];
+ // XXX: do we need extra sanitation here, e.g. for invalid URLs?
+ Services.prefs.setCharPref("browser.newtab.myhome", newtabUrlSanitizedPref);
+ newtabUrlPref = newtabUrlSanitizedPref;
+ break;
+ case 4:
+ newtabUrlPref = "about:newtab";
+ break;
+ default:
+ // In case of any other value it's a custom URL, consider instantApply.
+ if (this.newtabPageCustom) {
+ newtabUrlPref = this.newtabPageCustom;
+ }
+ }
+ Services.prefs.setCharPref("browser.newtab.url",newtabUrlPref);
+ } catch(e) { console.error(e); }
+ },
+
+ /**
+ * Determines the value of browser.newtab.choice based
+ * on the value of browser.newtab.url
+ *
+ * @returns the value of browser.newtab.choice
+ */
+ getNewtabChoice: function() {
+ let newtabUrlPref = Services.prefs.getCharPref("browser.newtab.url");
+ let browserHomepageUrl = Services.prefs.getComplexValue("browser.startup.homepage",
+ Components.interfaces.nsIPrefLocalizedString).data;
+ let newtabUrlSanitizedPref = browserHomepageUrl.split("|")[0];
+ let defaultStartupHomepage = Services.prefs.getDefaultBranch("browser.")
+ .getComplexValue("startup.homepage",
+ Components.interfaces.nsIPrefLocalizedString).data;
+ switch (newtabUrlPref) {
+ case "about:logopage":
+ return 1;
+ case defaultStartupHomepage:
+ return 2;
+ case newtabUrlSanitizedPref:
+ return 3;
+ case "about:newtab":
+ return 4;
+ default: // Custom URL entered.
+ // We need this to consider instantApply.
+ this.newtabPageCustom = newtabUrlPref;
+ return 0;
+ }
+ }
+};
diff --git a/browser/components/preferences/permissions.js b/browser/components/preferences/permissions.js new file mode 100644 index 000000000..a3c7c1b48 --- /dev/null +++ b/browser/components/preferences/permissions.js @@ -0,0 +1,459 @@ +/* -*- 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); + +const nsIPermissionManager = Components.interfaces.nsIPermissionManager; +const nsICookiePermission = Components.interfaces.nsICookiePermission; + +const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; + +function Permission(principal, type, capability) +{ + this.principal = principal; + this.origin = principal.origin; + this.type = type; + this.capability = capability; +} + +var gPermissionManager = { + _type : "", + _permissions : [], + _permissionsToAdd : new Map(), + _permissionsToDelete : new Map(), + _bundle : null, + _tree : null, + _observerRemoved : false, + + _view: { + _rowCount: 0, + get rowCount() + { + return this._rowCount; + }, + getCellText: function (aRow, aColumn) + { + if (aColumn.id == "siteCol") + return gPermissionManager._permissions[aRow].origin; + else if (aColumn.id == "statusCol") + return gPermissionManager._permissions[aRow].capability; + return ""; + }, + + isSeparator: function(aIndex) { return false; }, + isSorted: function() { return false; }, + isContainer: function(aIndex) { return false; }, + setTree: function(aTree){}, + getImageSrc: function(aRow, aColumn) {}, + getProgressMode: function(aRow, aColumn) {}, + getCellValue: function(aRow, aColumn) {}, + cycleHeader: function(column) {}, + getRowProperties: function(row){ return ""; }, + getColumnProperties: function(column){ return ""; }, + getCellProperties: function(row,column){ + if (column.element.getAttribute("id") == "siteCol") + return "ltr"; + + return ""; + } + }, + + _getCapabilityString: function (aCapability) + { + var stringKey = null; + switch (aCapability) { + case nsIPermissionManager.ALLOW_ACTION: + stringKey = "can"; + break; + case nsIPermissionManager.DENY_ACTION: + stringKey = "cannot"; + break; + case nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY: + stringKey = "canAccessFirstParty"; + break; + case nsICookiePermission.ACCESS_SESSION: + stringKey = "canSession"; + break; + } + return this._bundle.getString(stringKey); + }, + + addPermission: function (aCapability) + { + var textbox = document.getElementById("url"); + var input_url = textbox.value.replace(/^\s*/, ""); // trim any leading space + let principal; + try { + // The origin accessor on the principal object will throw if the + // principal doesn't have a canonical origin representation. This will + // help catch cases where the URI parser parsed something like + // `localhost:8080` as having the scheme `localhost`, rather than being + // an invalid URI. A canonical origin representation is required by the + // permission manager for storage, so this won't prevent any valid + // permissions from being entered by the user. + let uri; + try { + uri = Services.io.newURI(input_url, null, null); + principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri); + // If we have ended up with an unknown scheme, the following will throw. + principal.origin; + } catch(ex) { + uri = Services.io.newURI("http://" + input_url, null, null); + principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri); + // If we have ended up with an unknown scheme, the following will throw. + principal.origin; + } + } catch(ex) { + var message = this._bundle.getString("invalidURI"); + var title = this._bundle.getString("invalidURITitle"); + Services.prompt.alert(window, title, message); + return; + } + + var capabilityString = this._getCapabilityString(aCapability); + + // check whether the permission already exists, if not, add it + let permissionExists = false; + let capabilityExists = false; + for (var i = 0; i < this._permissions.length; ++i) { + if (this._permissions[i].principal.equals(principal)) { + permissionExists = true; + capabilityExists = this._permissions[i].capability == capabilityString; + if (!capabilityExists) { + this._permissions[i].capability = capabilityString; + } + break; + } + } + + + let permissionParams = {principal: principal, type: this._type, capability: aCapability}; + if (!permissionExists) { + this._permissionsToAdd.set(principal.origin, permissionParams); + this._addPermission(permissionParams); + } + else if (!capabilityExists) { + this._permissionsToAdd.set(principal.origin, permissionParams); + this._handleCapabilityChange(); + } + + textbox.value = ""; + textbox.focus(); + + // covers a case where the site exists already, so the buttons don't disable + this.onHostInput(textbox); + + // enable "remove all" button as needed + document.getElementById("removeAllPermissions").disabled = this._permissions.length == 0; + }, + + _removePermission: function(aPermission) + { + this._removePermissionFromList(aPermission.principal); + + // If this permission was added during this session, let's remove + // it from the pending adds list to prevent calls to the + // permission manager. + let isNewPermission = this._permissionsToAdd.delete(aPermission.principal.origin); + + if (!isNewPermission) { + this._permissionsToDelete.set(aPermission.principal.origin, aPermission); + } + + }, + + _handleCapabilityChange: function () + { + // Re-do the sort, if the status changed from Block to Allow + // or vice versa, since if we're sorted on status, we may no + // longer be in order. + if (this._lastPermissionSortColumn == "statusCol") { + this._resortPermissions(); + } + this._tree.treeBoxObject.invalidate(); + }, + + _addPermission: function(aPermission) + { + this._addPermissionToList(aPermission); + ++this._view._rowCount; + this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, 1); + // Re-do the sort, since we inserted this new item at the end. + this._resortPermissions(); + }, + + _resortPermissions: function() + { + gTreeUtils.sort(this._tree, this._view, this._permissions, + this._lastPermissionSortColumn, + this._permissionsComparator, + this._lastPermissionSortColumn, + !this._lastPermissionSortAscending); // keep sort direction + }, + + onHostInput: function (aSiteField) + { + document.getElementById("btnSession").disabled = !aSiteField.value; + document.getElementById("btnBlock").disabled = !aSiteField.value; + document.getElementById("btnAllow").disabled = !aSiteField.value; + }, + + onWindowKeyPress: function (aEvent) + { + if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) + window.close(); + }, + + onHostKeyPress: function (aEvent) + { + if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) + document.getElementById("btnAllow").click(); + }, + + onLoad: function () + { + this._bundle = document.getElementById("bundlePreferences"); + var params = window.arguments[0]; + this.init(params); + }, + + init: function (aParams) + { + if (this._type) { + // reusing an open dialog, clear the old observer + this.uninit(); + } + + this._type = aParams.permissionType; + this._manageCapability = aParams.manageCapability; + + var permissionsText = document.getElementById("permissionsText"); + while (permissionsText.hasChildNodes()) + permissionsText.removeChild(permissionsText.firstChild); + permissionsText.appendChild(document.createTextNode(aParams.introText)); + + document.title = aParams.windowTitle; + + document.getElementById("btnBlock").hidden = !aParams.blockVisible; + document.getElementById("btnSession").hidden = !aParams.sessionVisible; + document.getElementById("btnAllow").hidden = !aParams.allowVisible; + + var urlFieldVisible = (aParams.blockVisible || aParams.sessionVisible || aParams.allowVisible); + + var urlField = document.getElementById("url"); + urlField.value = aParams.prefilledHost; + urlField.hidden = !urlFieldVisible; + + this.onHostInput(urlField); + + var urlLabel = document.getElementById("urlLabel"); + urlLabel.hidden = !urlFieldVisible; + + let treecols = document.getElementsByTagName("treecols")[0]; + treecols.addEventListener("click", event => { + if (event.target.nodeName != "treecol" || event.button != 0) { + return; + } + + let sortField = event.target.getAttribute("data-field-name"); + if (!sortField) { + return; + } + + gPermissionManager.onPermissionSort(sortField); + }); + + Services.obs.notifyObservers(null, NOTIFICATION_FLUSH_PERMISSIONS, this._type); + Services.obs.addObserver(this, "perm-changed", false); + + this._loadPermissions(); + + urlField.focus(); + }, + + uninit: function () + { + if (!this._observerRemoved) { + Services.obs.removeObserver(this, "perm-changed"); + + this._observerRemoved = true; + } + }, + + observe: function (aSubject, aTopic, aData) + { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Components.interfaces.nsIPermission); + + // Ignore unrelated permission types. + if (permission.type != this._type) + return; + + if (aData == "added") { + this._addPermission(permission); + } + else if (aData == "changed") { + for (var i = 0; i < this._permissions.length; ++i) { + if (permission.matches(this._permissions[i].principal, true)) { + this._permissions[i].capability = this._getCapabilityString(permission.capability); + break; + } + } + this._handleCapabilityChange(); + } + else if (aData == "deleted") { + this._removePermissionFromList(permission.principal); + } + } + }, + + onPermissionSelected: function () + { + var hasSelection = this._tree.view.selection.count > 0; + var hasRows = this._tree.view.rowCount > 0; + document.getElementById("removePermission").disabled = !hasRows || !hasSelection; + document.getElementById("removeAllPermissions").disabled = !hasRows; + }, + + onPermissionDeleted: function () + { + if (!this._view.rowCount) + return; + var removedPermissions = []; + gTreeUtils.deleteSelectedItems(this._tree, this._view, this._permissions, removedPermissions); + for (var i = 0; i < removedPermissions.length; ++i) { + var p = removedPermissions[i]; + this._removePermission(p); + } + document.getElementById("removePermission").disabled = !this._permissions.length; + document.getElementById("removeAllPermissions").disabled = !this._permissions.length; + }, + + onAllPermissionsDeleted: function () + { + if (!this._view.rowCount) + return; + var removedPermissions = []; + gTreeUtils.deleteAll(this._tree, this._view, this._permissions, removedPermissions); + for (var i = 0; i < removedPermissions.length; ++i) { + var p = removedPermissions[i]; + this._removePermission(p); + } + document.getElementById("removePermission").disabled = true; + document.getElementById("removeAllPermissions").disabled = true; + }, + + onPermissionKeyPress: function (aEvent) + { + if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) { + this.onPermissionDeleted(); + } + }, + + _lastPermissionSortColumn: "", + _lastPermissionSortAscending: false, + _permissionsComparator : function (a, b) + { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }, + + + onPermissionSort: function (aColumn) + { + this._lastPermissionSortAscending = gTreeUtils.sort(this._tree, + this._view, + this._permissions, + aColumn, + this._permissionsComparator, + this._lastPermissionSortColumn, + this._lastPermissionSortAscending); + this._lastPermissionSortColumn = aColumn; + }, + + onApplyChanges: function() + { + // Stop observing permission changes since we are about + // to write out the pending adds/deletes and don't need + // to update the UI + this.uninit(); + + for (let permissionParams of this._permissionsToAdd.values()) { + Services.perms.addFromPrincipal(permissionParams.principal, permissionParams.type, permissionParams.capability); + } + + for (let p of this._permissionsToDelete.values()) { + Services.perms.removeFromPrincipal(p.principal, p.type); + } + + window.close(); + }, + + _loadPermissions: function () + { + this._tree = document.getElementById("permissionsTree"); + this._permissions = []; + + // load permissions into a table + var count = 0; + var enumerator = Services.perms.enumerator; + while (enumerator.hasMoreElements()) { + var nextPermission = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission); + this._addPermissionToList(nextPermission); + } + + this._view._rowCount = this._permissions.length; + + // sort and display the table + this._tree.view = this._view; + this.onPermissionSort("origin"); + + // disable "remove all" button if there are none + document.getElementById("removeAllPermissions").disabled = this._permissions.length == 0; + }, + + _addPermissionToList: function (aPermission) + { + if (aPermission.type == this._type && + (!this._manageCapability || + (aPermission.capability == this._manageCapability))) { + + var principal = aPermission.principal; + var capabilityString = this._getCapabilityString(aPermission.capability); + var p = new Permission(principal, + aPermission.type, + capabilityString); + this._permissions.push(p); + } + }, + + _removePermissionFromList: function (aPrincipal) + { + for (let i = 0; i < this._permissions.length; ++i) { + if (this._permissions[i].principal.equals(aPrincipal)) { + this._permissions.splice(i, 1); + this._view._rowCount--; + this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, -1); + this._tree.treeBoxObject.invalidate(); + break; + } + } + }, + + setOrigin: function (aOrigin) + { + document.getElementById("url").value = aOrigin; + } +}; + +function setOrigin(aOrigin) +{ + gPermissionManager.setOrigin(aOrigin); +} + +function initWithParams(aParams) +{ + gPermissionManager.init(aParams); +} + diff --git a/browser/components/preferences/permissions.xul b/browser/components/preferences/permissions.xul new file mode 100644 index 000000000..33806cc27 --- /dev/null +++ b/browser/components/preferences/permissions.xul @@ -0,0 +1,85 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/permissions.dtd" > + +<window id="PermissionsDialog" class="windowDialog" + windowtype="Browser:Permissions" + title="&window.title;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + style="width: &window.width;;" + onload="gPermissionManager.onLoad();" + onunload="gPermissionManager.uninit();" + persist="screenX screenY width height" + onkeypress="gPermissionManager.onWindowKeyPress(event);"> + + <script src="chrome://global/content/treeUtils.js"/> + <script src="chrome://browser/content/preferences/permissions.js"/> + + <stringbundle id="bundlePreferences" + src="chrome://browser/locale/preferences/preferences.properties"/> + + <keyset> + <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/> + </keyset> + + <vbox class="contentPane" flex="1"> + <description id="permissionsText" control="url"/> + <separator class="thin"/> + <label id="urlLabel" control="url" value="&address.label;" accesskey="&address.accesskey;"/> + <hbox align="start"> + <textbox id="url" flex="1" + oninput="gPermissionManager.onHostInput(event.target);" + onkeypress="gPermissionManager.onHostKeyPress(event);"/> + </hbox> + <hbox pack="end"> + <button id="btnBlock" disabled="true" label="&block.label;" accesskey="&block.accesskey;" + oncommand="gPermissionManager.addPermission(nsIPermissionManager.DENY_ACTION);"/> + <button id="btnSession" disabled="true" label="&session.label;" accesskey="&session.accesskey;" + oncommand="gPermissionManager.addPermission(nsICookiePermission.ACCESS_SESSION);"/> + <button id="btnAllow" disabled="true" label="&allow.label;" default="true" accesskey="&allow.accesskey;" + oncommand="gPermissionManager.addPermission(nsIPermissionManager.ALLOW_ACTION);"/> + </hbox> + <separator class="thin"/> + <tree id="permissionsTree" flex="1" style="height: 18em;" + hidecolumnpicker="true" + onkeypress="gPermissionManager.onPermissionKeyPress(event)" + onselect="gPermissionManager.onPermissionSelected();"> + <treecols> + <treecol id="siteCol" label="&treehead.sitename.label;" flex="3" + data-field-name="origin" persist="width"/> + <splitter class="tree-splitter"/> + <treecol id="statusCol" label="&treehead.status.label;" flex="1" + data-field-name="capability" persist="width"/> + </treecols> + <treechildren/> + </tree> + </vbox> + <vbox> + <hbox class="actionButtons" align="left" flex="1"> + <button id="removePermission" disabled="true" + accesskey="&removepermission.accesskey;" + icon="remove" label="&removepermission.label;" + oncommand="gPermissionManager.onPermissionDeleted();"/> + <button id="removeAllPermissions" + icon="clear" label="&removeallpermissions.label;" + accesskey="&removeallpermissions.accesskey;" + oncommand="gPermissionManager.onAllPermissionsDeleted();"/> + </hbox> + <spacer flex="1"/> + <hbox class="actionButtons" align="right" flex="1"> + <button oncommand="close();" icon="close" + label="&button.cancel.label;" accesskey="&button.cancel.accesskey;" /> + <button id="btnApplyChanges" oncommand="gPermissionManager.onApplyChanges();" icon="save" + label="&button.ok.label;" accesskey="&button.ok.accesskey;"/> + </hbox> + <resizer type="window" dir="bottomend"/> + </vbox> +</window> diff --git a/browser/components/preferences/preferences.xul b/browser/components/preferences/preferences.xul new file mode 100644 index 000000000..b56b16ecc --- /dev/null +++ b/browser/components/preferences/preferences.xul @@ -0,0 +1,77 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://mozapps/content/preferences/preferences.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?> + +<!-- XXX This should be in applications.xul, but bug 393953 means putting it + - there causes the Applications pane not to work the first time you open + - the Preferences dialog in a browsing session, so we work around the problem + - by putting it here instead. + --> +<?xml-stylesheet href="chrome://browser/content/preferences/handlers.css"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?> + +<!DOCTYPE prefwindow [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % preferencesDTD SYSTEM "chrome://browser/locale/preferences/preferences.dtd"> +%brandDTD; +%preferencesDTD; +]> + +#ifdef XP_WIN +#define USE_WIN_TITLE_STYLE +#endif + +<prefwindow type="prefwindow" + id="BrowserPreferences" + windowtype="Browser:Preferences" + ondialoghelp="openPrefsHelp()" +#ifdef USE_WIN_TITLE_STYLE + title="&prefWindow.titleWin;" +#else +#ifdef XP_UNIX + title="&prefWindow.titleGNOME;" +#endif +#endif + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" +#ifdef USE_WIN_TITLE_STYLE + style="&prefWinMinSize.styleWin2;" +#else + style="&prefWinMinSize.styleGNOME;" +#endif + onunload="if (typeof gSecurityPane != 'undefined') gSecurityPane.syncAddonSecurityLevel();" + ondialogaccept="gNewtabUrl.writeNewtabUrl();"> + + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" src="chrome://browser/content/preferences/newtaburl.js"/> + + <stringbundle id="bundleBrand" src="chrome://branding/locale/brand.properties"/> + <stringbundle id="bundlePreferences" + src="chrome://browser/locale/preferences/preferences.properties"/> + + <prefpane id="paneMain" label="&paneGeneral.title;" + src="chrome://browser/content/preferences/main.xul"/> + <prefpane id="paneTabs" label="&paneTabs.title;" + src="chrome://browser/content/preferences/tabs.xul"/> + <prefpane id="paneContent" label="&paneContent.title;" + src="chrome://browser/content/preferences/content.xul"/> + <prefpane id="paneApplications" label="&paneApplications.title;" + src="chrome://browser/content/preferences/applications.xul"/> + <prefpane id="panePrivacy" label="&panePrivacy.title;" + src="chrome://browser/content/preferences/privacy.xul"/> + <prefpane id="paneSecurity" label="&paneSecurity.title;" + src="chrome://browser/content/preferences/security.xul"/> +#ifdef MOZ_SERVICES_SYNC + <prefpane id="paneSync" label="&paneSync.title;" + src="chrome://browser/content/preferences/sync.xul"/> +#endif + <prefpane id="paneAdvanced" label="&paneAdvanced.title;" + src="chrome://browser/content/preferences/advanced.xul"/> + +</prefwindow> + diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js new file mode 100644 index 000000000..05ed3bcdd --- /dev/null +++ b/browser/components/preferences/privacy.js @@ -0,0 +1,458 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +var gPrivacyPane = { + + /** + * Whether the use has selected the auto-start private browsing mode in the UI. + */ + _autoStartPrivateBrowsing: false, + + /** + * Whether the prompt to restart Firefox should appear when changing the autostart pref. + */ + _shouldPromptForRestart: true, + + /** + * Sets up the UI for the number of days of history to keep, and updates the + * label of the "Clear Now..." button. + */ + init: function() + { + this._updateSanitizeSettingsButton(); + this.initializeHistoryMode(); + this.updateHistoryModePane(); + this.updatePrivacyMicroControls(); + this.initAutoStartPrivateBrowsingReverter(); + }, + + // HISTORY MODE + + /** + * The list of preferences which affect the initial history mode settings. + * If the auto start private browsing mode pref is active, the initial + * history mode would be set to "Don't remember anything". + * If all of these preferences have their default values, and the auto-start + * private browsing mode is not active, the initial history mode would be + * set to "Remember everything". + * Otherwise, the initial history mode would be set to "Custom". + * + * Extensions adding their own preferences can append their IDs to this array if needed. + */ + prefsForDefault: [ + "places.history.enabled", + "browser.formfill.enable", + "network.cookie.cookieBehavior", + "network.cookie.lifetimePolicy", + "privacy.sanitize.sanitizeOnShutdown" + ], + + /** + * The list of control IDs which are dependent on the auto-start private + * browsing setting, such that in "Custom" mode they would be disabled if + * the auto-start private browsing checkbox is checked, and enabled otherwise. + * + * Extensions adding their own controls can append their IDs to this array if needed. + */ + dependentControls: [ + "rememberHistory", + "rememberForms", + "keepUntil", + "keepCookiesUntil", + "alwaysClear", + "clearDataSettings" + ], + + /** + * Check whether all the preferences values are set to their default values + * + * @param aPrefs an array of pref names to check for + * @returns boolean true if all of the prefs are set to their default values, + * false otherwise + */ + _checkDefaultValues: function(aPrefs) { + for (let i = 0; i < aPrefs.length; ++i) { + let pref = document.getElementById(aPrefs[i]); + if (pref.value != pref.defaultValue) + return false; + } + return true; + }, + + /** + * Initialize the history mode menulist based on the privacy preferences + */ + initializeHistoryMode: function() + { + let mode; + let getVal = function(aPref) + document.getElementById(aPref).value; + + if (this._checkDefaultValues(this.prefsForDefault)) { + if (getVal("browser.privatebrowsing.autostart")) + mode = "dontremember"; + else + mode = "remember"; + } + else + mode = "custom"; + + document.getElementById("historyMode").value = mode; + }, + + /** + * Update the selected pane based on the history mode menulist + */ + updateHistoryModePane: function() + { + let selectedIndex = -1; + switch (document.getElementById("historyMode").value) { + case "remember": + selectedIndex = 0; + break; + case "dontremember": + selectedIndex = 1; + break; + case "custom": + selectedIndex = 2; + break; + } + document.getElementById("historyPane").selectedIndex = selectedIndex; + }, + + /** + * Update the private browsing auto-start pref and the history mode + * micro-management prefs based on the history mode menulist + */ + updateHistoryModePrefs: function() + { + let pref = document.getElementById("browser.privatebrowsing.autostart"); + switch (document.getElementById("historyMode").value) { + case "remember": + if (pref.value) + pref.value = false; + + // select the remember history option + document.getElementById("places.history.enabled").value = true; + + // select the remember forms history option + document.getElementById("browser.formfill.enable").value = true; + + // select the accept cookies option + document.getElementById("network.cookie.cookieBehavior").value = 0; + // select the cookie lifetime policy option + document.getElementById("network.cookie.lifetimePolicy").value = 0; + + // select the clear on close option + document.getElementById("privacy.sanitize.sanitizeOnShutdown").value = false; + break; + case "dontremember": + if (!pref.value) + pref.value = true; + break; + } + }, + + /** + * Update the privacy micro-management controls based on the + * value of the private browsing auto-start checkbox. + */ + updatePrivacyMicroControls: function() + { + if (document.getElementById("historyMode").value == "custom") { + let disabled = this._autoStartPrivateBrowsing = + document.getElementById("privateBrowsingAutoStart").checked; + this.dependentControls + .forEach(function(aElement) + document.getElementById(aElement).disabled = disabled); + + const Ci = Components.interfaces; + // adjust the cookie controls status + this.readAcceptCookies(); + let lifetimePolicy = document.getElementById("network.cookie.lifetimePolicy").value; + if (lifetimePolicy != Ci.nsICookieService.ACCEPT_NORMALLY && + lifetimePolicy != Ci.nsICookieService.ACCEPT_SESSION && + lifetimePolicy != Ci.nsICookieService.ACCEPT_FOR_N_DAYS) { + lifetimePolicy = Ci.nsICookieService.ACCEPT_NORMALLY; + } + document.getElementById("keepCookiesUntil").value = disabled ? 2 : lifetimePolicy; + + // adjust the checked state of the sanitizeOnShutdown checkbox + document.getElementById("alwaysClear").checked = disabled ? false : + document.getElementById("privacy.sanitize.sanitizeOnShutdown").value; + + // adjust the checked state of the remember history checkboxes + document.getElementById("rememberHistory").checked = disabled ? false : + document.getElementById("places.history.enabled").value; + document.getElementById("rememberForms").checked = disabled ? false : + document.getElementById("browser.formfill.enable").value; + + if (!disabled) { + // adjust the Settings button for sanitizeOnShutdown + this._updateSanitizeSettingsButton(); + } + } + }, + + // PRIVATE BROWSING + + /** + * Initialize the starting state for the auto-start private browsing mode pref reverter. + */ + initAutoStartPrivateBrowsingReverter: function() + { + let mode = document.getElementById("historyMode"); + let autoStart = document.getElementById("privateBrowsingAutoStart"); + this._lastMode = mode.selectedIndex; + this._lastCheckState = autoStart.hasAttribute('checked'); + }, + + _lastMode: null, + _lasCheckState: null, + updateAutostart: function() { + let mode = document.getElementById("historyMode"); + let autoStart = document.getElementById("privateBrowsingAutoStart"); + let pref = document.getElementById("browser.privatebrowsing.autostart"); + if ((mode.value == "custom" && this._lastCheckState == autoStart.checked) || + (mode.value == "remember" && !this._lastCheckState) || + (mode.value == "dontremember" && this._lastCheckState)) { + // These are all no-op changes, so we don't need to prompt. + this._lastMode = mode.selectedIndex; + this._lastCheckState = autoStart.hasAttribute('checked'); + return; + } + + if (!this._shouldPromptForRestart) { + // We're performing a revert. Just let it happen. + return; + } + + const Cc = Components.classes, Ci = Components.interfaces; + let brandName = document.getElementById("bundleBrand").getString("brandShortName"); + let bundle = document.getElementById("bundlePreferences"); + let msg = bundle.getFormattedString(autoStart.checked ? + "featureEnableRequiresRestart" : "featureDisableRequiresRestart", + [brandName]); + let title = bundle.getFormattedString("shouldRestartTitle", [brandName]); + let prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService); + let shouldProceed = prompts.confirm(window, title, msg) + if (shouldProceed) { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", + "restart"); + shouldProceed = !cancelQuit.data; + + if (shouldProceed) { + pref.value = autoStart.hasAttribute('checked'); + document.documentElement.acceptDialog(); + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + return; + } + } + + this._shouldPromptForRestart = false; + + if (this._lastCheckState) { + autoStart.checked = "checked"; + } else { + autoStart.removeAttribute('checked'); + } + mode.selectedIndex = this._lastMode; + mode.doCommand(); + + this._shouldPromptForRestart = true; + }, + + // HISTORY + + /* + * Preferences: + * + * places.history.enabled + * - whether history is enabled or not + * browser.formfill.enable + * - true if entries in forms and the search bar should be saved, false + * otherwise + */ + + // COOKIES + + /* + * Preferences: + * + * network.cookie.cookieBehavior + * - determines how the browser should handle cookies: + * 0 means enable all cookies + * 1 means reject all third party cookies + * 2 means disable all cookies + * 3 means reject third party cookies unless at least one is already set for the eTLD + * see netwerk/cookie/src/nsCookieService.cpp for details + * network.cookie.lifetimePolicy + * - determines how long cookies are stored: + * 0 means keep cookies until they expire + * 2 means keep cookies until the browser is closed + */ + + /** + * Reads the network.cookie.cookieBehavior preference value and + * enables/disables the rest of the cookie UI accordingly, returning true + * if cookies are enabled. + */ + readAcceptCookies: function() + { + var pref = document.getElementById("network.cookie.cookieBehavior"); + var acceptThirdPartyLabel = document.getElementById("acceptThirdPartyLabel"); + var acceptThirdPartyMenu = document.getElementById("acceptThirdPartyMenu"); + var keepUntil = document.getElementById("keepUntil"); + var menu = document.getElementById("keepCookiesUntil"); + + // enable the rest of the UI for anything other than "disable all cookies" + var acceptCookies = (pref.value != 2); + + acceptThirdPartyLabel.disabled = acceptThirdPartyMenu.disabled = !acceptCookies; + keepUntil.disabled = menu.disabled = this._autoStartPrivateBrowsing || !acceptCookies; + + return acceptCookies; + }, + + /** + * Enables/disables the "keep until" label and menulist in response to the + * "accept cookies" checkbox being checked or unchecked. + */ + writeAcceptCookies: function() + { + var accept = document.getElementById("acceptCookies"); + var acceptThirdPartyMenu = document.getElementById("acceptThirdPartyMenu"); + + // if we're enabling cookies, automatically select 'accept third party always' + if (accept.checked) + acceptThirdPartyMenu.selectedIndex = 0; + + return accept.checked ? 0 : 2; + }, + + /** + * Converts between network.cookie.cookieBehavior and the third-party cookie UI + */ + readAcceptThirdPartyCookies: function() + { + var pref = document.getElementById("network.cookie.cookieBehavior"); + switch (pref.value) + { + case 0: + return "always"; + case 1: + return "never"; + case 2: + return "never"; + case 3: + return "visited"; + default: + return undefined; + } + }, + + writeAcceptThirdPartyCookies: function() + { + var accept = document.getElementById("acceptThirdPartyMenu").selectedItem; + switch (accept.value) + { + case "always": + return 0; + case "visited": + return 3; + case "never": + return 1; + default: + return undefined; + } + }, + + /** + * Displays fine-grained, per-site preferences for cookies. + */ + showCookieExceptions: function() + { + var bundlePreferences = document.getElementById("bundlePreferences"); + var params = { blockVisible : true, + sessionVisible : true, + allowVisible : true, + prefilledHost : "", + permissionType : "cookie", + windowTitle : bundlePreferences.getString("cookiepermissionstitle"), + introText : bundlePreferences.getString("cookiepermissionstext") }; + document.documentElement.openWindow("Browser:Permissions", + "chrome://browser/content/preferences/permissions.xul", + "", params); + }, + + /** + * Displays all the user's cookies in a dialog. + */ + showCookies: function(aCategory) + { + document.documentElement.openWindow("Browser:Cookies", + "chrome://browser/content/preferences/cookies.xul", + "", null); + }, + + // CLEAR PRIVATE DATA + + /* + * Preferences: + * + * privacy.sanitize.sanitizeOnShutdown + * - true if the user's private data is cleared on startup according to the + * Clear Private Data settings, false otherwise + */ + + /** + * Displays the Clear Private Data settings dialog. + */ + showClearPrivateDataSettings: function() + { + document.documentElement.openSubDialog("chrome://browser/content/preferences/sanitize.xul", + "", null); + }, + + + /** + * Displays a dialog from which individual parts of private data may be + * cleared. + */ + clearPrivateDataNow: function(aClearEverything) + { + var ts = document.getElementById("privacy.sanitize.timeSpan"); + var timeSpanOrig = ts.value; + if (aClearEverything) + ts.value = 0; + + const Cc = Components.classes, Ci = Components.interfaces; + var glue = Cc["@mozilla.org/browser/browserglue;1"] + .getService(Ci.nsIBrowserGlue); + glue.sanitize(window); + + // reset the timeSpan pref + if (aClearEverything) + ts.value = timeSpanOrig; + Services.obs.notifyObservers(null, "clear-private-data", null); + }, + + /** + * Enables or disables the "Settings..." button depending + * on the privacy.sanitize.sanitizeOnShutdown preference value + */ + _updateSanitizeSettingsButton: function() { + var settingsButton = document.getElementById("clearDataSettings"); + var sanitizeOnShutdownPref = document.getElementById("privacy.sanitize.sanitizeOnShutdown"); + + settingsButton.disabled = !sanitizeOnShutdownPref.value; + } + +}; diff --git a/browser/components/preferences/privacy.xul b/browser/components/preferences/privacy.xul new file mode 100644 index 000000000..e5175422d --- /dev/null +++ b/browser/components/preferences/privacy.xul @@ -0,0 +1,256 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: XML; 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/. --> + +<!DOCTYPE overlay [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % privacyDTD SYSTEM "chrome://browser/locale/preferences/privacy.dtd"> +%brandDTD; +%privacyDTD; +]> + +<overlay id="PrivacyPaneOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <prefpane id="panePrivacy" + onpaneload="gPrivacyPane.init();" + helpTopic="prefs-privacy"> + + <preferences id="privacyPreferences"> + + <!-- Global Privacy Control --> + <preference id="privacy.GPCheader.enabled" + name="privacy.GPCheader.enabled" + type="bool"/> + + <!-- XXX button prefs --> + <preference id="pref.privacy.disable_button.cookie_exceptions" + name="pref.privacy.disable_button.cookie_exceptions" + type="bool"/> + <preference id="pref.privacy.disable_button.view_cookies" + name="pref.privacy.disable_button.view_cookies" + type="bool"/> + + <!-- Location Bar --> + <preference id="browser.urlbar.autocomplete.enabled" + name="browser.urlbar.autocomplete.enabled" + type="bool"/> + <preference id="browser.urlbar.suggest.bookmark" + name="browser.urlbar.suggest.bookmark" + type="bool"/> + <preference id="browser.urlbar.suggest.history" + name="browser.urlbar.suggest.history" + type="bool"/> + <preference id="browser.urlbar.suggest.openpage" + name="browser.urlbar.suggest.openpage" + type="bool"/> + + <!-- History --> + <preference id="places.history.enabled" + name="places.history.enabled" + type="bool"/> + <preference id="browser.formfill.enable" + name="browser.formfill.enable" + type="bool"/> + + <!-- Cookies --> + <preference id="network.cookie.cookieBehavior" name="network.cookie.cookieBehavior" type="int"/> + <preference id="network.cookie.lifetimePolicy" name="network.cookie.lifetimePolicy" type="int"/> + <preference id="network.cookie.blockFutureCookies" name="network.cookie.blockFutureCookies" type="bool"/> + + <!-- Clear Private Data --> + <preference id="privacy.sanitize.sanitizeOnShutdown" + name="privacy.sanitize.sanitizeOnShutdown" + onchange="gPrivacyPane._updateSanitizeSettingsButton();" + type="bool"/> + <preference id="privacy.sanitize.timeSpan" + name="privacy.sanitize.timeSpan" + type="int"/> + + <!-- Private Browsing --> + <preference id="browser.privatebrowsing.autostart" + name="browser.privatebrowsing.autostart" + onchange="gPrivacyPane.updatePrivacyMicroControls();" + type="bool"/> + + </preferences> + + <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> + + <script type="application/javascript" src="chrome://browser/content/preferences/privacy.js"/> + + <!-- History --> + <groupbox id="historyPanel"> + <caption>&history.label;</caption> + <hbox align="center"> + <label id="historyModeLabel" + control="historyMode" + accesskey="&historyHeader.pre.accesskey;">&historyHeader.pre.label;</label> + <menulist id="historyMode" + oncommand="gPrivacyPane.updateHistoryModePane(); + gPrivacyPane.updateHistoryModePrefs(); + gPrivacyPane.updatePrivacyMicroControls(); + gPrivacyPane.updateAutostart();"> + <menupopup> + <menuitem label="&historyHeader.remember.label;" value="remember"/> + <menuitem label="&historyHeader.dontremember.label;" value="dontremember"/> + <menuitem label="&historyHeader.custom.label;" value="custom"/> + </menupopup> + </menulist> + <label>&historyHeader.post.label;</label> + </hbox> + + <deck id="historyPane"> + <vbox align="center" id="historyRememberPane"> + <hbox align="center" flex="1"> + <spacer flex="1" class="indent"/> + <vbox flex="2"> + <description>&rememberDescription.label;</description> + <separator/> + <description>&rememberActions.pre.label;<html:a + class="inline-link" href="#" + onclick="gPrivacyPane.clearPrivateDataNow(false); return false;" + >&rememberActions.clearHistory.label;</html:a>&rememberActions.middle.label;<html:a + class="inline-link" href="#" + onclick="gPrivacyPane.showCookies(); return false;" + >&rememberActions.removeCookies.label;</html:a>&rememberActions.post.label;</description> + </vbox> + <spacer flex="1" class="indent"/> + </hbox> + </vbox> + <vbox align="center" id="historyDontRememberPane"> + <hbox align="center" flex="1"> + <spacer flex="1" class="indent"/> + <vbox flex="2"> + <description>&dontrememberDescription.label;</description> + <separator/> + <description>&dontrememberActions.pre.label;<html:a + class="inline-link" href="#" + onclick="gPrivacyPane.clearPrivateDataNow(true); return false;" + >&dontrememberActions.clearHistory.label;</html:a>&dontrememberActions.post.label;</description> + </vbox> + <spacer flex="1" class="indent"/> + </hbox> + </vbox> + <vbox id="historyCustomPane"> + <separator class="thin"/> + <checkbox id="privateBrowsingAutoStart" class="indent" + label="&privateBrowsingPermanent2.label;" + accesskey="&privateBrowsingPermanent2.accesskey;" + preference="browser.privatebrowsing.autostart" + oncommand="gPrivacyPane.updateAutostart()"/> + + <vbox class="indent"> + <vbox class="indent"> + <checkbox id="rememberHistory" + label="&rememberHistory2.label;" + accesskey="&rememberHistory2.accesskey;" + preference="places.history.enabled"/> + <checkbox id="rememberForms" + label="&rememberSearchForm.label;" + accesskey="&rememberSearchForm.accesskey;" + preference="browser.formfill.enable"/> + <hbox id="cookiesBox"> + <checkbox id="acceptCookies" label="&acceptCookies.label;" flex="1" + preference="network.cookie.cookieBehavior" + accesskey="&acceptCookies.accesskey;" + onsyncfrompreference="return gPrivacyPane.readAcceptCookies();" + onsynctopreference="return gPrivacyPane.writeAcceptCookies();"/> + <button id="cookieExceptions" oncommand="gPrivacyPane.showCookieExceptions();" + label="&cookieExceptions.label;" accesskey="&cookieExceptions.accesskey;" + preference="pref.privacy.disable_button.cookie_exceptions"/> + </hbox> + <hbox id="acceptThirdPartyRow" class="indent"> + <hbox id="acceptThirdPartyBox" align="center"> + <label id="acceptThirdPartyLabel" control="acceptThirdPartyMenu" + accesskey="&acceptThirdParty.pre.accesskey;">&acceptThirdParty.pre.label;</label> + <menulist id="acceptThirdPartyMenu" preference="network.cookie.cookieBehavior" + onsyncfrompreference="return gPrivacyPane.readAcceptThirdPartyCookies();" + onsynctopreference="return gPrivacyPane.writeAcceptThirdPartyCookies();"> + <menupopup> + <menuitem label="&acceptThirdParty.always.label;" value="always"/> + <menuitem label="&acceptThirdParty.visited.label;" value="visited"/> + <menuitem label="&acceptThirdParty.never.label;" value="never"/> + </menupopup> + </menulist> + </hbox> + </hbox> + + <hbox id="keepRow" class="indent"> + <hbox id="keepBox" align="center"> + <label id="keepUntil" + control="keepCookiesUntil" + accesskey="&keepUntil.accesskey;">&keepUntil.label;</label> + <menulist id="keepCookiesUntil" + preference="network.cookie.lifetimePolicy"> + <menupopup> + <menuitem label="&expire.label;" value="0"/> + <menuitem label="&close.label;" value="2"/> + </menupopup> + </menulist> + </hbox> + <hbox flex="1"/> + <button id="showCookiesButton" + label="&showCookies.label;" accesskey="&showCookies.accesskey;" + oncommand="gPrivacyPane.showCookies();" + preference="pref.privacy.disable_button.view_cookies"/> + </hbox> + + <hbox id="clearDataBox" align="center"> + <checkbox id="alwaysClear" flex="1" + preference="privacy.sanitize.sanitizeOnShutdown" + label="&clearOnClose.label;" + accesskey="&clearOnClose.accesskey;"/> + <button id="clearDataSettings" label="&clearOnCloseSettings.label;" + accesskey="&clearOnCloseSettings.accesskey;" + oncommand="gPrivacyPane.showClearPrivateDataSettings();"/> + </hbox> + </vbox> + </vbox> + </vbox> + </deck> + + </groupbox> + + <!-- Global Privacy Control --> + <groupbox id="dataPrivacyPanel"> + <caption>&dataPrivacy.label;</caption> + <hbox align="center"> + <checkbox id="privacyGPCCheckbox" + label="&sendGPCheader.label;" + accesskey="&sendGPCheader.accesskey;" + preference="privacy.GPCheader.enabled"/> + <separator class="thin"/> + <label class="text-link" id="GPCInfo" + href="https://www.palemoon.org/support/global-privacy-control" + value="&GPCInfo.label;"/> + + </hbox> + </groupbox> + + <!-- Location Bar --> + <groupbox id="locatioBarPanel"> + <caption>&locationBar.label;</caption> + + <label id="locationBarSuggestionLabel">&locbar.suggest.label;</label> + <hbox id="tabPrefsBox" align="center" flex="1"> + <checkbox id="historySuggestion" label="&locbar.history.label;" + accesskey="&locbar.history.accesskey;" + preference="browser.urlbar.suggest.history"/> + <checkbox id="bookmarkSuggestion" label="&locbar.bookmarks.label;" + accesskey="&locbar.bookmarks.accesskey;" + preference="browser.urlbar.suggest.bookmark"/> + <checkbox id="openpageSuggestion" label="&locbar.openpage.label;" + accesskey="&locbar.openpage.accesskey;" + preference="browser.urlbar.suggest.openpage"/> + </hbox> + + </groupbox> + + </prefpane> + +</overlay> diff --git a/browser/components/preferences/sanitize.js b/browser/components/preferences/sanitize.js new file mode 100644 index 000000000..4383bee4f --- /dev/null +++ b/browser/components/preferences/sanitize.js @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +var gSanitizeDialog = Object.freeze({ + onClearHistoryChanged: function () { + let downloadsPref = document.getElementById("privacy.clearOnShutdown.downloads"); + let historyPref = document.getElementById("privacy.clearOnShutdown.history"); + downloadsPref.value = historyPref.value; + } +}); diff --git a/browser/components/preferences/sanitize.xul b/browser/components/preferences/sanitize.xul new file mode 100644 index 000000000..829b5dfc8 --- /dev/null +++ b/browser/components/preferences/sanitize.xul @@ -0,0 +1,108 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?> + +<!DOCTYPE dialog [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % sanitizeDTD SYSTEM "chrome://browser/locale/sanitize.dtd"> + %brandDTD; + %sanitizeDTD; +]> + +<prefwindow id="SanitizeDialog" type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + dlgbuttons="accept,cancel,help" + ondialoghelp="openPrefsHelp()" + style="width: &dialog.width2;;" + title="&sanitizePrefs2.title;" + onload="gSanitizeDialog.onClearHistoryChanged();"> + + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" src="chrome://browser/content/preferences/sanitize.js"/> + + <prefpane id="SanitizeDialogPane" + helpTopic="prefs-clear-private-data"> + + <preferences> + <preference id="privacy.clearOnShutdown.history" name="privacy.clearOnShutdown.history" type="bool" + onchange="return gSanitizeDialog.onClearHistoryChanged();"/> + <preference id="privacy.clearOnShutdown.formdata" name="privacy.clearOnShutdown.formdata" type="bool"/> + <preference id="privacy.clearOnShutdown.passwords" name="privacy.clearOnShutdown.passwords" type="bool"/> + <preference id="privacy.clearOnShutdown.downloads" name="privacy.clearOnShutdown.downloads" type="bool"/> + <preference id="privacy.clearOnShutdown.cookies" name="privacy.clearOnShutdown.cookies" type="bool"/> + <preference id="privacy.clearOnShutdown.cache" name="privacy.clearOnShutdown.cache" type="bool"/> + <preference id="privacy.clearOnShutdown.offlineApps" name="privacy.clearOnShutdown.offlineApps" type="bool"/> + <preference id="privacy.clearOnShutdown.sessions" name="privacy.clearOnShutdown.sessions" type="bool"/> + <preference id="privacy.clearOnShutdown.siteSettings" name="privacy.clearOnShutdown.siteSettings" type="bool"/> + <preference id="privacy.clearOnShutdown.connectivityData" name="privacy.clearOnShutdown.connectivityData" type="bool"/> + </preferences> + + <description>&clearDataSettings2.label;</description> + + <groupbox orient="horizontal"> + <caption label="&historySection.label;"/> + <grid flex="1"> + <columns> + <column style="width: &column.width2;"/> + <column flex="1"/> + </columns> + <rows> + <row> + <checkbox label="&itemHistoryAndDownloads.label;" + accesskey="&itemHistoryAndDownloads.accesskey;" + preference="privacy.clearOnShutdown.history"/> + <checkbox label="&itemCookies.label;" + accesskey="&itemCookies.accesskey;" + preference="privacy.clearOnShutdown.cookies"/> + </row> + <row> + <checkbox label="&itemActiveLogins.label;" + accesskey="&itemActiveLogins.accesskey;" + preference="privacy.clearOnShutdown.sessions"/> + <checkbox label="&itemCache.label;" + accesskey="&itemCache.accesskey;" + preference="privacy.clearOnShutdown.cache"/> + </row> + <row> + <checkbox label="&itemFormSearchHistory.label;" + accesskey="&itemFormSearchHistory.accesskey;" + preference="privacy.clearOnShutdown.formdata"/> + </row> + </rows> + </grid> + </groupbox> + <groupbox orient="horizontal"> + <caption label="&dataSection.label;"/> + <grid flex="1"> + <columns> + <column style="width: &column.width2;"/> + <column flex="1"/> + </columns> + <rows> + <row> + <checkbox label="&itemPasswords.label;" + accesskey="&itemPasswords.accesskey;" + preference="privacy.clearOnShutdown.passwords"/> + <checkbox label="&itemOfflineApps.label;" + accesskey="&itemOfflineApps.accesskey;" + preference="privacy.clearOnShutdown.offlineApps"/> + </row> + <row> + <checkbox label="&itemSitePreferences.label;" + accesskey="&itemSitePreferences.accesskey;" + preference="privacy.clearOnShutdown.siteSettings"/> + <checkbox label="&itemConnectivityData.label;" + accesskey="&itemConnectivityData.accesskey;" + preference="privacy.clearOnShutdown.connectivityData"/> + </row> + </rows> + </grid> + </groupbox> + </prefpane> +</prefwindow> diff --git a/browser/components/preferences/security.js b/browser/components/preferences/security.js new file mode 100644 index 000000000..d8f491b1c --- /dev/null +++ b/browser/components/preferences/security.js @@ -0,0 +1,235 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); + +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +var gSecurityPane = { + _pane: null, + + /** + * Initializes UI. + */ + init: function () + { + this._pane = document.getElementById("paneSecurity"); + this._initMasterPasswordUI(); + }, + + // ADD-ONS + + /* + * Preferences: + * + * xpinstall.whitelist.required + * - true if a site must be added to a site whitelist before extensions + * provided by the site may be installed from it, false if the extension + * may be directly installed after a confirmation dialog + */ + + /** + * Enables/disables the add-ons Exceptions button depending on whether + * or not add-on installation warnings are displayed. + */ + readWarnAddonInstall: function () + { + var warn = document.getElementById("xpinstall.whitelist.required"); + var exceptions = document.getElementById("addonExceptions"); + + exceptions.disabled = !warn.value; + + // don't override the preference value + return undefined; + }, + + /** + * Displays the exceptions lists for add-on installation warnings. + */ + showAddonExceptions: function () + { + var bundlePrefs = document.getElementById("bundlePreferences"); + + var params = this._addonParams; + if (!params.windowTitle || !params.introText) { + params.windowTitle = bundlePrefs.getString("addons_permissions_title"); + params.introText = bundlePrefs.getString("addonspermissionstext"); + } + + document.documentElement.openWindow("Browser:Permissions", + "chrome://browser/content/preferences/permissions.xul", + "", params); + }, + + /** + * Parameters for the add-on install permissions dialog. + */ + _addonParams: + { + blockVisible: false, + sessionVisible: false, + allowVisible: true, + prefilledHost: "", + permissionType: "install" + }, + + /** + * Ensures that the blocklist is enabled/disabled appropriately based on level + */ + addonLevelNeedsSync: function() + { + Services.prefs.setBoolPref("extensions.blocklist.level.updated", true); + }, + // called from preferences window onunload. + syncAddonSecurityLevel: function() + { + if (Services.prefs.getBoolPref("extensions.blocklist.level.updated") == true) { + Services.prefs.setBoolPref("extensions.blocklist.level.updated", false); + var secLevel = Services.prefs.getIntPref("extensions.blocklist.level"); + Services.prefs.setBoolPref("extensions.blocklist.enabled", + !(secLevel == 99)); + } + }, + + // PASSWORDS + + /* + * Preferences: + * + * signon.rememberSignons + * - true if passwords are remembered, false otherwise + */ + + /** + * Enables/disables the Exceptions button used to configure sites where + * passwords are never saved. When browser is set to start in Private + * Browsing mode, the "Remember passwords" UI is useless, so we disable it. + */ + readSavePasswords: function () + { + var pref = document.getElementById("signon.rememberSignons"); + var excepts = document.getElementById("passwordExceptions"); + + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + document.getElementById("savePasswords").disabled = true; + excepts.disabled = true; + return false; + } else { + excepts.disabled = !pref.value; + // don't override pref value in UI + return undefined; + } + }, + + /** + * Displays a dialog in which the user can view and modify the list of sites + * where passwords are never saved. + */ + showPasswordExceptions: function () + { + let bundlePrefs = document.getElementById("bundlePreferences"); + let params = { + blockVisible: true, + sessionVisible: false, + allowVisible: false, + hideStatusColumn: true, + prefilledHost: "", + permissionType: "login-saving", + windowTitle: bundlePrefs.getString("savedLoginsExceptions_title"), + introText: bundlePrefs.getString("savedLoginsExceptions_desc") + }; + + document.documentElement.openWindow("Toolkit:PasswordManagerExceptions", + "chrome://browser/content/preferences/permissions.xul", + null, params); + }, + + /** + * Initializes master password UI: the "use master password" checkbox, selects + * the master password button to show, and enables/disables it as necessary. + * The master password is controlled by various bits of NSS functionality, so + * the UI for it can't be controlled by the normal preference bindings. + */ + _initMasterPasswordUI: function () + { + var noMP = !LoginHelper.isMasterPasswordSet(); + + var button = document.getElementById("changeMasterPassword"); + button.disabled = noMP; + + var checkbox = document.getElementById("useMasterPassword"); + checkbox.checked = !noMP; + }, + + /** + * Enables/disables the master password button depending on the state of the + * "use master password" checkbox, and prompts for master password removal if + * one is set. + */ + updateMasterPasswordButton: function () + { + var checkbox = document.getElementById("useMasterPassword"); + var button = document.getElementById("changeMasterPassword"); + button.disabled = !checkbox.checked; + + // unchecking the checkbox should try to immediately remove the master + // password, because it's impossible to non-destructively remove the master + // password used to encrypt all the passwords without providing it (by + // design), and it would be extremely odd to pop up that dialog when the + // user closes the prefwindow and saves his settings + if (!checkbox.checked) + this._removeMasterPassword(); + else + this.changeMasterPassword(); + + this._initMasterPasswordUI(); + }, + + /** + * Displays the "remove master password" dialog to allow the user to remove + * the current master password. When the dialog is dismissed, master password + * UI is automatically updated. + */ + _removeMasterPassword: function () + { + const Cc = Components.classes, Ci = Components.interfaces; + var secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"]. + getService(Ci.nsIPKCS11ModuleDB); + if (secmodDB.isFIPSEnabled) { + var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService); + var bundle = document.getElementById("bundlePreferences"); + promptService.alert(window, + bundle.getString("pw_change_failed_title"), + bundle.getString("pw_change2empty_in_fips_mode")); + } + else { + document.documentElement.openSubDialog("chrome://mozapps/content/preferences/removemp.xul", + "", null); + } + this._initMasterPasswordUI(); + }, + + /** + * Displays a dialog in which the master password may be changed. + */ + changeMasterPassword: function () + { + document.documentElement.openSubDialog("chrome://mozapps/content/preferences/changemp.xul", + "", null); + this._initMasterPasswordUI(); + }, + + /** + * Shows the sites where the user has saved passwords and the associated login + * information. + */ + showPasswords: function () + { + document.documentElement.openWindow("Toolkit:PasswordManager", + "chrome://passwordmgr/content/passwordManager.xul", + "", null); + } +}; diff --git a/browser/components/preferences/security.xul b/browser/components/preferences/security.xul new file mode 100644 index 000000000..350eb0d79 --- /dev/null +++ b/browser/components/preferences/security.xul @@ -0,0 +1,177 @@ +<?xml version="1.0"?> + +<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- --> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % securityDTD SYSTEM "chrome://browser/locale/preferences/security.dtd"> + %brandDTD; + %securityDTD; +]> + +<overlay id="SecurityPaneOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <prefpane id="paneSecurity" + onpaneload="gSecurityPane.init();" + helpTopic="prefs-security"> + + <preferences id="securityPreferences"> + <!-- XXX buttons --> + <preference id="pref.privacy.disable_button.view_passwords" + name="pref.privacy.disable_button.view_passwords" + type="bool"/> + <preference id="pref.privacy.disable_button.view_passwords_exceptions" + name="pref.privacy.disable_button.view_passwords_exceptions" + type="bool"/> + + <!-- Add-ons, malware, phishing --> + <preference id="xpinstall.whitelist.required" + name="xpinstall.whitelist.required" + type="bool"/> + <preference id="extensions.blocklist.level" + name="extensions.blocklist.level" + onchange="gSecurityPane.addonLevelNeedsSync();" + type="int"/> + + <!-- Passwords --> + <preference id="signon.rememberSignons" name="signon.rememberSignons" type="bool"/> + <preference id="signon.autofillForms" name="signon.autofillForms" type="bool"/> + + <!-- Security Protocols --> + + <preference id="network.stricttransportsecurity.enabled" + name="network.stricttransportsecurity.enabled" + type="bool"/> + + <!-- Opportunistic Encryption --> + + <preference id="network.http.upgrade-insecure-requests" + name="network.http.upgrade-insecure-requests" + type="bool"/> + <preference id="network.http.altsvc.oe" + name="network.http.altsvc.oe" + type="bool"/> + + <!-- XSS Filter --> + <!-- + <preference id="security.xssfilter.enable" name="security.xssfilter.enable" type="bool"/> + --> + + </preferences> + + <script type="application/javascript" src="chrome://browser/content/preferences/security.js"/> + + <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/> + + <!-- addons, forgery (phishing) UI --> + <groupbox id="addonsSecurityGroup"> + <caption label="&addons.label;"/> + + <hbox id="addonInstallBox"> + <checkbox id="warnAddonInstall" flex="1" + label="&warnAddonInstall.label;" + accesskey="&warnAddonInstall.accesskey;" + preference="xpinstall.whitelist.required" + onsyncfrompreference="return gSecurityPane.readWarnAddonInstall();"/> + <button id="addonExceptions" + label="&addonExceptions.label;" + accesskey="&addonExceptions.accesskey;" + oncommand="gSecurityPane.showAddonExceptions();"/> + </hbox> + <hbox id="addonSecuritySettingsBox" flex="1"> + <vbox> + <label id="addonSecurity" control="addonsecurity-menu">&addonSecuritylevel;</label> + <menulist id="addonsecurity-menu" preference="extensions.blocklist.level" sizetopopup="always"> + <menupopup> + <menuitem label="&addonSecurityLevel_Off;" value="99" /> + <menuitem label="&addonSecurityLevel_Low;" value="3" /> + <menuitem label="&addonSecurityLevel_High;" value="2" /> + <menuitem label="&addonSecurityLevel_Extreme;" value="1" /> + </menupopup> + </menulist> + </vbox> + </hbox> + </groupbox> + + <!-- Passwords --> + <groupbox id="passwordsGroup" orient="vertical"> + <caption label="&passwords.label;"/> + + <hbox id="savePasswordsBox"> + <checkbox id="savePasswords" flex="1" + label="&rememberPasswords.label;" accesskey="&rememberPasswords.accesskey;" + preference="signon.rememberSignons" + onsyncfrompreference="return gSecurityPane.readSavePasswords();"/> + <button id="passwordExceptions" + label="&passwordExceptions.label;" + accesskey="&passwordExceptions.accesskey;" + oncommand="gSecurityPane.showPasswordExceptions();" + preference="pref.privacy.disable_button.view_passwords_exceptions"/> + </hbox> + <checkbox id="autofillPasswords" flex="1" + label="&autofillPasswords.label;" accesskey="&autofillPasswords.accesskey;" + preference="signon.autofillForms"/> + <hbox id="masterPasswordBox"> + <checkbox id="useMasterPassword" flex="1" + oncommand="gSecurityPane.updateMasterPasswordButton();" + label="&useMasterPassword.label;" + accesskey="&useMasterPassword.accesskey;"/> + <button id="changeMasterPassword" + label="&changeMasterPassword.label;" + accesskey="&changeMasterPassword.accesskey;" + oncommand="gSecurityPane.changeMasterPassword();"/> + </hbox> + + <hbox id="showPasswordsBox"> + <spacer flex="1"/> + <button id="showPasswords" + label="&savedPasswords.label;" accesskey="&savedPasswords.accesskey;" + oncommand="gSecurityPane.showPasswords();" + preference="pref.privacy.disable_button.view_passwords"/> + </hbox> + </groupbox> + + <!-- Security protocols --> + <groupbox id="SecProtoGroup"> + <caption label="&SecProto.label;"/> + + <vbox id="SecProtoBox" align="start" flex="1"> + <checkbox id="enableHSTS" + label="&enableHSTS.label;" + accesskey="&enableHSTS.accesskey;" + preference="network.stricttransportsecurity.enabled" /> + </vbox> + </groupbox> + + <groupbox id="OpportunisticEncryption"> + <caption label="&OpEnc.label;"/> + <checkbox id="enableUIROpEnc" + label="&enableUIROpEnc.label;" + preference="network.http.upgrade-insecure-requests" /> + <checkbox id="enableAltSvcOpEnc" + label="&enableAltSvcOpEnc.label;" + preference="network.http.altsvc.oe" /> + </groupbox> + + <!-- XSS Filter --> + <!-- + <groupbox id="XSSFiltGroup"> + <caption label="&XSSFilt.label;"/> + + <hbox id="XSSFiltBox"> + <checkbox id="enableXSSFilt" flex="1" + label="&enableXSSFilt.label;" + accesskey="&enableXSSFilt.accesskey;" + preference="security.xssfilter.enable" /> + </hbox> + + </groupbox> + --> + + </prefpane> + +</overlay> diff --git a/browser/components/preferences/selectBookmark.js b/browser/components/preferences/selectBookmark.js new file mode 100644 index 000000000..ba468646c --- /dev/null +++ b/browser/components/preferences/selectBookmark.js @@ -0,0 +1,82 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/** + * SelectBookmarkDialog controls the user interface for the "Use Bookmark for + * Home Page" dialog. + * + * The caller (gMainPane.setHomePageToBookmark in main.js) invokes this dialog + * with a single argument - a reference to an object with a .urls property and + * a .names property. This dialog is responsible for updating the contents of + * the .urls property with an array of URLs to use as home pages and for + * updating the .names property with an array of names for those URLs before it + * closes. + */ +var SelectBookmarkDialog = { + init: function() { + document.getElementById("bookmarks").place = + "place:queryType=1&folder=" + PlacesUIUtils.allBookmarksFolderId; + + // Initial update of the OK button. + this.selectionChanged(); + }, + + /** + * Update the disabled state of the OK button as the user changes the + * selection within the view. + */ + selectionChanged: function() { + var accept = document.documentElement.getButton("accept"); + var bookmarks = document.getElementById("bookmarks"); + var disableAcceptButton = true; + if (bookmarks.hasSelection) { + if (!PlacesUtils.nodeIsSeparator(bookmarks.selectedNode)) + disableAcceptButton = false; + } + accept.disabled = disableAcceptButton; + }, + + onItemDblClick: function() { + var bookmarks = document.getElementById("bookmarks"); + var selectedNode = bookmarks.selectedNode; + if (selectedNode && PlacesUtils.nodeIsURI(selectedNode)) { + /** + * The user has double clicked on a tree row that is a link. Take this to + * mean that they want that link to be their homepage, and close the dialog. + */ + document.documentElement.getButton("accept").click(); + } + }, + + /** + * User accepts their selection. Set all the selected URLs or the contents + * of the selected folder as the list of homepages. + */ + accept: function() { + var bookmarks = document.getElementById("bookmarks"); + NS_ASSERT(bookmarks.hasSelection, + "Should not be able to accept dialog if there is no selected URL!"); + var urls = []; + var names = []; + var selectedNode = bookmarks.selectedNode; + if (PlacesUtils.nodeIsFolder(selectedNode)) { + var contents = PlacesUtils.getFolderContents(selectedNode.itemId).root; + var cc = contents.childCount; + for (var i = 0; i < cc; ++i) { + var node = contents.getChild(i); + if (PlacesUtils.nodeIsURI(node)) { + urls.push(node.uri); + names.push(node.title); + } + } + contents.containerOpen = false; + } + else { + urls.push(selectedNode.uri); + names.push(selectedNode.title); + } + window.arguments[0].urls = urls; + window.arguments[0].names = names; + } +}; diff --git a/browser/components/preferences/selectBookmark.xul b/browser/components/preferences/selectBookmark.xul new file mode 100644 index 000000000..5547534b6 --- /dev/null +++ b/browser/components/preferences/selectBookmark.xul @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://browser/content/places/places.css"?> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> + +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/selectBookmark.dtd"> + +<dialog id="selectBookmarkDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&selectBookmark.title;" style="width: 32em;" + persist="screenX screenY width height" screenX="24" screenY="24" + onload="SelectBookmarkDialog.init();" + ondialogaccept="SelectBookmarkDialog.accept();"> + + <script type="application/javascript" + src="chrome://browser/content/preferences/selectBookmark.js"/> + + <description>&selectBookmark.label;</description> + + <separator class="thin"/> + + <tree id="bookmarks" flex="1" type="places" + style="height: 15em;" + hidecolumnpicker="true" + seltype="single" + ondblclick="SelectBookmarkDialog.onItemDblClick();" + onselect="SelectBookmarkDialog.selectionChanged();"> + <treecols> + <treecol id="title" flex="1" primary="true" hideheader="true"/> + </treecols> + <treechildren id="bookmarksChildren" flex="1"/> + </tree> + + <separator class="thin"/> + +</dialog> diff --git a/browser/components/preferences/sync.js b/browser/components/preferences/sync.js new file mode 100644 index 000000000..ecf4fe6ef --- /dev/null +++ b/browser/components/preferences/sync.js @@ -0,0 +1,192 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Components.utils.import("resource://services-sync/main.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const PAGE_NO_ACCOUNT = 0; +const PAGE_HAS_ACCOUNT = 1; +const PAGE_NEEDS_UPDATE = 2; + +var gSyncPane = { + _stringBundle: null, + prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs", + "engine.tabs", "engine.history"], + + get page() { + return document.getElementById("weavePrefsDeck").selectedIndex; + }, + + set page(val) { + document.getElementById("weavePrefsDeck").selectedIndex = val; + }, + + get _usingCustomServer() { + return Weave.Svc.Prefs.isSet("serverURL"); + }, + + needsUpdate: function () { + this.page = PAGE_NEEDS_UPDATE; + let label = document.getElementById("loginError"); + label.value = Weave.Utils.getErrorString(Weave.Status.login); + label.className = "error"; + }, + + init: function () { + // If the Service hasn't finished initializing, wait for it. + let xps = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + + if (xps.ready) { + this._init(); + return; + } + + let onUnload = function () { + window.removeEventListener("unload", onUnload, false); + try { + Services.obs.removeObserver(onReady, "weave:service:ready"); + } catch (e) {} + }; + + let onReady = function () { + Services.obs.removeObserver(onReady, "weave:service:ready"); + window.removeEventListener("unload", onUnload, false); + this._init(); + }.bind(this); + + Services.obs.addObserver(onReady, "weave:service:ready", false); + window.addEventListener("unload", onUnload, false); + + xps.ensureLoaded(); + }, + + _init: function () { + let topics = ["weave:service:login:error", + "weave:service:login:finish", + "weave:service:start-over", + "weave:service:setup-complete", + "weave:service:logout:finish"]; + + // Add the observers now and remove them on unload + //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling + // of `this`. Fix in a followup. (bug 583347) + topics.forEach(function (topic) { + Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this); + }, this); + window.addEventListener("unload", function() { + topics.forEach(function (topic) { + Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this); + }, gSyncPane); + }, false); + + this._stringBundle = + Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties"); + this.updateWeavePrefs(); + }, + + updateWeavePrefs: function () { + if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED || + Weave.Svc.Prefs.get("firstSync", "") == "notReady") { + this.page = PAGE_NO_ACCOUNT; + } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE || + Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) { + this.needsUpdate(); + } else { + this.page = PAGE_HAS_ACCOUNT; + document.getElementById("accountName").value = Weave.Service.identity.account; + document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName; + document.getElementById("tosPP").hidden = this._usingCustomServer; + } + }, + + startOver: function (showDialog) { + if (showDialog) { + let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + + Services.prompt.BUTTON_POS_1_DEFAULT; + let buttonChoice = + Services.prompt.confirmEx(window, + this._stringBundle.GetStringFromName("syncUnlink.title"), + this._stringBundle.GetStringFromName("syncUnlink.label"), + flags, + this._stringBundle.GetStringFromName("syncUnlinkConfirm.label"), + null, null, null, {}); + + // If the user selects cancel, just bail + if (buttonChoice == 1) { + return; + } + } + + Weave.Service.startOver(); + this.updateWeavePrefs(); + }, + + updatePass: function () { + if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) { + gSyncUtils.changePassword(); + } else { + gSyncUtils.updatePassphrase(); + } + }, + + resetPass: function () { + if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) { + gSyncUtils.resetPassword(); + } else { + gSyncUtils.resetPassphrase(); + } + }, + + /** + * Invoke the Sync setup wizard. + * + * @param wizardType + * Indicates type of wizard to launch: + * null -- regular set up wizard + * "pair" -- pair a device first + * "reset" -- reset sync + */ + openSetup: function (wizardType) { + let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); + if (win) { + win.focus(); + } else { + window.openDialog("chrome://weave/content/setup.xul", + "weaveSetup", "centerscreen,chrome,resizable=no", + wizardType); + } + }, + + openQuotaDialog: function () { + let win = Services.wm.getMostRecentWindow("Sync:ViewQuota"); + if (win) { + win.focus(); + } else { + window.openDialog("chrome://weave/content/quota.xul", "", + "centerscreen,chrome,dialog,modal"); + } + }, + + openAddDevice: function () { + if (!Weave.Utils.ensureMPUnlocked()) { + return; + } + + let win = Services.wm.getMostRecentWindow("Sync:AddDevice"); + if (win) { + win.focus(); + } else { + window.openDialog("chrome://weave/content/addDevice.xul", + "syncAddDevice", "centerscreen,chrome,resizable=no"); + } + }, + + resetSync: function () { + this.openSetup("reset"); + }, +}; + diff --git a/browser/components/preferences/sync.xul b/browser/components/preferences/sync.xul new file mode 100644 index 000000000..2c91e0cd5 --- /dev/null +++ b/browser/components/preferences/sync.xul @@ -0,0 +1,178 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://branding/locale/syncBrand.dtd"> +<!ENTITY % syncDTD SYSTEM "chrome://browser/locale/preferences/sync.dtd"> +%brandDTD; +%syncBrandDTD; +%syncDTD; +]> + +<overlay id="SyncPaneOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <prefpane id="paneSync" + helpTopic="prefs-weave" + onpaneload="gSyncPane.init()"> + + <preferences> +<!-- <preference id="engine.addons" name="services.sync.engine.addons" type="bool"/> --> + <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/> + <preference id="engine.history" name="services.sync.engine.history" type="bool"/> + <preference id="engine.tabs" name="services.sync.engine.tabs" type="bool"/> + <preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/> + <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/> + </preferences> + + + <script type="application/javascript" + src="chrome://browser/content/preferences/sync.js"/> + <script type="application/javascript" + src="chrome://weave/content/utils.js"/> + + + <deck id="weavePrefsDeck"> + <vbox id="noAccount" align="center"> + <spacer flex="1"/> + <description id="syncDesc"> + &weaveDesc.label; + </description> + <separator/> + <label class="text-link" + onclick="event.stopPropagation(); gSyncPane.openSetup(null);" + value="&setupButton.label;"/> + <separator/> + <label class="text-link" + onclick="event.stopPropagation(); gSyncPane.openSetup('pair');" + value="&pairDevice.label;"/> + <spacer flex="3"/> + </vbox> + + <vbox id="hasAccount"> + <groupbox class="syncGroupBox"> + <!-- label is set to account name --> + <caption id="accountCaption" align="center"> + <image id="accountCaptionImage"/> + <label id="accountName" value=""/> + </caption> + + <hbox> + <button type="menu" + label="&manageAccount.label;" + accesskey="&manageAccount.accesskey;"> + <menupopup> + <menuitem label="&viewQuota.label;" + oncommand="gSyncPane.openQuotaDialog();"/> + <menuseparator/> + <menuitem label="&changePassword2.label;" + oncommand="gSyncUtils.changePassword();"/> + <menuitem label="&myRecoveryKey.label;" + oncommand="gSyncUtils.resetPassphrase();"/> + <menuseparator/> + <menuitem label="&resetSync2.label;" + oncommand="gSyncPane.resetSync();"/> + </menupopup> + </button> + </hbox> + + <hbox> + <label id="syncAddDeviceLabel" + class="text-link" + onclick="gSyncPane.openAddDevice(); return false;" + value="&pairDevice.label;"/> + </hbox> + + <vbox> + <label value="&syncMy.label;" /> + <richlistbox id="syncEnginesList" + orient="vertical" + onselect="if (this.selectedCount) this.clearSelection();"> +<!-- <richlistitem> + <checkbox label="&engine.addons.label;" + accesskey="&engine.addons.accesskey;" + preference="engine.addons"/> + </richlistitem> --> + <richlistitem> + <checkbox label="&engine.bookmarks.label;" + accesskey="&engine.bookmarks.accesskey;" + preference="engine.bookmarks"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.passwords.label;" + accesskey="&engine.passwords.accesskey;" + preference="engine.passwords"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.prefs.label;" + accesskey="&engine.prefs.accesskey;" + preference="engine.prefs"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.history.label;" + accesskey="&engine.history.accesskey;" + preference="engine.history"/> + </richlistitem> + <richlistitem> + <checkbox label="&engine.tabs.label;" + accesskey="&engine.tabs.accesskey;" + preference="engine.tabs"/> + </richlistitem> + </richlistbox> + </vbox> + </groupbox> + + <groupbox class="syncGroupBox"> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label value="&syncDeviceName.label;" + accesskey="&syncDeviceName.accesskey;" + control="syncComputerName"/> + <textbox id="syncComputerName" + onchange="gSyncUtils.changeName(this)"/> + </row> + </rows> + </grid> + <hbox> + <label class="text-link" + onclick="gSyncPane.startOver(true); return false;" + value="&unlinkDevice.label;"/> + </hbox> + </groupbox> + <hbox id="tosPP" pack="center"> + <label class="text-link" + onclick="event.stopPropagation();gSyncUtils.openToS();" + value="&prefs.tosLink.label;"/> + <label class="text-link" + onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();" + value="&prefs.ppLink.label;"/> + </hbox> + </vbox> + + <vbox id="needsUpdate" align="center" pack="center"> + <hbox> + <label id="loginError" value=""/> + <label class="text-link" + onclick="gSyncPane.updatePass(); return false;" + value="&updatePass.label;"/> + <label class="text-link" + onclick="gSyncPane.resetPass(); return false;" + value="&resetPass.label;"/> + </hbox> + <label class="text-link" + onclick="gSyncPane.startOver(true); return false;" + value="&unlinkDevice.label;"/> + </vbox> + </deck> + </prefpane> +</overlay> diff --git a/browser/components/preferences/tabs.js b/browser/components/preferences/tabs.js new file mode 100644 index 000000000..811064291 --- /dev/null +++ b/browser/components/preferences/tabs.js @@ -0,0 +1,89 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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 gTabsPane = { + + /* + * Preferences: + * + * browser.link.open_newwindow + * - determines where pages which would open in a new window are opened: + * 1 opens such links in the most recent window or tab, + * 2 opens such links in a new window, + * 3 opens such links in a new tab + * browser.tabs.loadInBackground + * - true if display should switch to a new tab which has been opened from a + * link, false if display shouldn't switch + * browser.tabs.warnOnClose + * - true if when closing a window with multiple tabs the user is warned and + * allowed to cancel the action, false to just close the window + * browser.tabs.warnOnOpen + * - true if the user should be warned if he attempts to open a lot of tabs at + * once (e.g. a large folder of bookmarks), false otherwise + * browser.taskbar.previews.enable + * - true if tabs are to be shown in the Windows 7 taskbar + */ + + /** + * Initialize any platform-specific UI. + */ + init: function () { +#ifdef XP_WIN + const Cc = Components.classes; + const Ci = Components.interfaces; + try { + let sysInfo = Cc["@mozilla.org/system-info;1"]. + getService(Ci.nsIPropertyBag2); + let ver = parseFloat(sysInfo.getProperty("version")); + let showTabsInTaskbar = document.getElementById("showTabsInTaskbar"); + showTabsInTaskbar.hidden = ver < 6.1; + } catch (ex) {} +#endif + // Set the proper value in the newtab drop-down. + gTabsPane.readNewtabUrl(); + }, + + /** + * Pale Moon: synchronize warnOnClose and warnOnCloseOtherTabs + */ + syncWarnOnClose: function() { + var warnOnClosePref = document.getElementById("browser.tabs.warnOnClose"); + var warnOnCloseOtherPref = document.getElementById("browser.tabs.warnOnCloseOtherTabs"); + warnOnCloseOtherPref.value = warnOnClosePref.value; + }, + + /** + * Determines where a link which opens a new window will open. + * + * @returns |true| if such links should be opened in new tabs + */ + readLinkTarget: function() { + var openNewWindow = document.getElementById("browser.link.open_newwindow"); + return openNewWindow.value != 2; + }, + + /** + * Determines where a link which opens a new window will open. + * + * @returns 2 if such links should be opened in new windows, + * 3 if such links should be opened in new tabs + */ + writeLinkTarget: function() { + var linkTargeting = document.getElementById("linkTargeting"); + return linkTargeting.checked ? 3 : 2; + }, + + /** + * Determines the value of the New Tab display drop-down based + * on the value of browser.newtab.url. + */ + readNewtabUrl: function() { + let newtabUrlChoice = document.getElementById("browser.newtab.choice"); + newtabUrlChoice.value = gNewtabUrl.getNewtabChoice(); + if (newtabUrlChoice.value == 0) { + document.getElementById("newtabPageCustom").hidden = false; + } + gNewtabUrl.newtabUrlChoiceIsSet = true; + } +}; diff --git a/browser/components/preferences/tabs.xul b/browser/components/preferences/tabs.xul new file mode 100644 index 000000000..1f7a2a9e3 --- /dev/null +++ b/browser/components/preferences/tabs.xul @@ -0,0 +1,101 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % tabsDTD SYSTEM "chrome://browser/locale/preferences/tabs.dtd"> +%tabsDTD; +]> + +<overlay id="TabsPaneOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <prefpane id="paneTabs" + onpaneload="gTabsPane.init();" + helpTopic="prefs-tabs"> + + <preferences id="tabsPreferences"> + <preference id="browser.link.open_newwindow" name="browser.link.open_newwindow" type="int"/> + <preference id="browser.tabs.autoHide" name="browser.tabs.autoHide" type="bool" inverted="true"/> + <preference id="browser.tabs.loadInBackground" name="browser.tabs.loadInBackground" type="bool" inverted="true"/> + <preference id="browser.tabs.warnOnClose" name="browser.tabs.warnOnClose" type="bool" + onchange="gTabsPane.syncWarnOnClose();"/> + <preference id="browser.tabs.warnOnCloseOtherTabs" name="browser.tabs.warnOnCloseOtherTabs" type="bool"/> + <preference id="browser.tabs.warnOnOpen" name="browser.tabs.warnOnOpen" type="bool"/> + <preference id="browser.sessionstore.restore_on_demand" name="browser.sessionstore.restore_on_demand" type="bool"/> +#ifdef XP_WIN + <preference id="browser.taskbar.previews.enable" name="browser.taskbar.previews.enable" type="bool"/> +#endif + <preference id="browser.tabs.insertRelatedAfterCurrent" name="browser.tabs.insertRelatedAfterCurrent" type="bool"/> + <preference id="browser.search.context.loadInBackground" name="browser.search.context.loadInBackground" type="bool" inverted="true"/> + <preference id="browser.tabs.closeWindowWithLastTab" name="browser.tabs.closeWindowWithLastTab" type="bool"/> + <preference id="browser.ctrlTab.previews" name="browser.ctrlTab.previews" type="bool"/> + + <preference id="browser.newtab.url" name="browser.newtab.url" type="string"/> + <preference id="browser.newtab.myhome" name="browser.newtab.myhome" type="string"/> + <preference id="browser.newtab.choice" name="browser.newtab.choice" type="int"/> + </preferences> + + <script type="application/javascript" src="chrome://browser/content/preferences/tabs.js"/> + + <!-- XXX flex below is a hack because wrapping checkboxes don't reflow + properly; see bug 349098 --> + <vbox id="tabPrefsBox" align="start" flex="1"> + <checkbox id="linkTargeting" label="&newWindowsAsTabs.label;" + accesskey="&newWindowsAsTabs.accesskey;" + preference="browser.link.open_newwindow" + onsyncfrompreference="return gTabsPane.readLinkTarget();" + onsynctopreference="return gTabsPane.writeLinkTarget();"/> + <checkbox id="warnCloseMultiple" label="&warnCloseMultipleTabs.label;" + accesskey="&warnCloseMultipleTabs.accesskey;" + preference="browser.tabs.warnOnClose"/> + <checkbox id="warnOpenMany" label="&warnOpenManyTabs.label;" + accesskey="&warnOpenManyTabs.accesskey;" + preference="browser.tabs.warnOnOpen"/> + <checkbox id="showTabBar" label="&showTabBar.label;" + accesskey="&showTabBar.accesskey;" + preference="browser.tabs.autoHide"/> + <checkbox id="restoreOnDemand" label="&restoreTabsOnDemand.label;" + accesskey="&restoreTabsOnDemand.accesskey;" + preference="browser.sessionstore.restore_on_demand"/> + <checkbox id="switchToNewTabs" label="&switchToNewTabs.label;" + accesskey="&switchToNewTabs.accesskey;" + preference="browser.tabs.loadInBackground"/> +#ifdef XP_WIN + <checkbox id="showTabsInTaskbar" label="&showTabsInTaskbar.label;" + accesskey="&showTabsInTaskbar.accesskey;" + preference="browser.taskbar.previews.enable"/> +#endif +<!-- Pale Moon additions --> + <checkbox id="insertRelatedAfterCurrent" label="&insertRelatedAfterCurrent.label;" + preference="browser.tabs.insertRelatedAfterCurrent"/> + <checkbox id="contextLoadInBackground" label="&contextLoadInBackground.label;" + preference="browser.search.context.loadInBackground"/> + <checkbox id="closeWindowWithLastTab" label="&closeWindowWithLastTab.label;" + preference="browser.tabs.closeWindowWithLastTab"/> + <checkbox id="showTabPreviews" label="&showTabPreviews.label;" + preference="browser.ctrlTab.previews"/> + <hbox align="center"> + <label value="&newtabPage.label;"/> + <menulist + id="newtabPage" + preference="browser.newtab.choice" + oncommand="gNewtabUrl.writeNewtabUrl(event.target.value);"> + <menupopup> + <menuitem label="&newtabPage.custom.label;" value="0" id="newtabPageCustom" hidden="true" /> + <menuitem label="&newtabPage.blank.label;" value="1" /> + <menuitem label="&newtabPage.home.label;" value="2" /> + <menuitem label="&newtabPage.myhome.label;" value="3" /> + <menuitem label="&newtabPage.quickdial.label;" value="4" /> + </menupopup> + </menulist> + </hbox> + </vbox> + + </prefpane> + +</overlay> diff --git a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml new file mode 100644 index 000000000..03347d358 --- /dev/null +++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +--> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd"> + %netErrorDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> + %browserDTD; + <!ENTITY basePBMenu.label "<span class='appMenuButton'>&brandShortName;</span><span class='fileMenu'>&fileMenu.label;</span>"> + <!ENTITY % privatebrowsingpageDTD SYSTEM "chrome://browser/locale/aboutPrivateBrowsing.dtd"> + %privatebrowsingpageDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all"/> + <link rel="stylesheet" href="chrome://browser/skin/aboutPrivateBrowsing.css" type="text/css" media="all"/> + <style type="text/css"><![CDATA[ + body.normal .showPrivate, + body.private .showNormal { + display: none; + } + body.appMenuButtonVisible .fileMenu { + display: none; + } + body.appMenuButtonInvisible .appMenuButton { + display: none; + } + ]]></style> + <script type="application/javascript;version=1.7"><![CDATA[ + const Cc = Components.classes; + const Ci = Components.interfaces; + + Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + document.title = "]]>&privatebrowsingpage.title.normal;<![CDATA["; + setFavIcon("chrome://global/skin/icons/question-16.png"); + } else { + document.title = "]]>&privatebrowsingpage.title;<![CDATA["; + setFavIcon("chrome://browser/skin/Privacy-16.png"); + } + + var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + + // Focus the location bar + mainWindow.focusAndSelectUrlBar(); + + function setFavIcon(url) { + var icon = document.createElement("link"); + icon.setAttribute("rel", "icon"); + icon.setAttribute("type", "image/png"); + icon.setAttribute("href", url); + var head = document.getElementsByTagName("head")[0]; + head.insertBefore(icon, head.firstChild); + } + + document.addEventListener("DOMContentLoaded", function () { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + document.body.setAttribute("class", "normal"); + } + + // Set up the help link + let moreInfoURL = Cc["@mozilla.org/toolkit/URLFormatterService;1"]. + getService(Ci.nsIURLFormatter). + formatURLPref("app.support.baseURL"); + let moreInfoLink = document.getElementById("moreInfoLink"); + if (moreInfoLink) + moreInfoLink.setAttribute("href", moreInfoURL + "private-browsing"); + + // Show the correct menu structure based on whether the App Menu button is + // shown or not. + var menuBar = mainWindow.document.getElementById("toolbar-menubar"); + var appMenuButtonIsVisible = menuBar.getAttribute("autohide") == "true"; + document.body.classList.add(appMenuButtonIsVisible ? "appMenuButtonVisible" : + "appMenuButtonInvisible"); + }, false); + + function openPrivateWindow() { + mainWindow.OpenBrowserWindow({private: true}); + } + ]]></script> + </head> + + <body dir="&locale.dir;" + class="private"> + + <!-- PAGE CONTAINER (for styling purposes only) --> + <div id="errorPageContainer"> + + <!-- Error Title --> + <div id="errorTitle"> + <h1 id="errorTitleText" class="showPrivate">&privatebrowsingpage.title;</h1> + <h1 id="errorTitleTextNormal" class="showNormal">&privatebrowsingpage.title.normal;</h1> + </div> + + <!-- LONG CONTENT (the section most likely to require scrolling) --> + <div id="errorLongContent"> + + <!-- Short Description --> + <div id="errorShortDesc"> + <p id="errorShortDescText" class="showPrivate">&privatebrowsingpage.perwindow.issueDesc;</p> + <p id="errorShortDescTextNormal" class="showNormal">&privatebrowsingpage.perwindow.issueDesc.normal;</p> + </div> + + <!-- Long Description --> + <div id="errorLongDesc"> + <p id="errorLongDescText">&privatebrowsingpage.perwindow.description;</p> + </div> + + <!-- Start Private Browsing --> + <div id="startPrivateBrowsingDesc" class="showNormal"> + <button xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="startPrivateBrowsing" label="&privatebrowsingpage.openPrivateWindow.label;" + accesskey="&privatebrowsingpage.openPrivateWindow.accesskey;" + oncommand="openPrivateWindow();"/> + </div> + + <!-- Footer --> + <div id="footerDesc"> + <p id="footerText" class="showPrivate">&privatebrowsingpage.howToStop3;</p> + <p id="footerTextNormal" class="showNormal">&privatebrowsingpage.howToStart3;</p> + </div> + + <!-- More Info --> + <div id="moreInfo" class="showPrivate"> + <p id="moreInfoText"> + &privatebrowsingpage.moreInfo; + </p> + <p id="moreInfoLinkContainer"> + <a id="moreInfoLink" target="_blank">&privatebrowsingpage.learnMore;</a> + </p> + </div> + </div> + </div> + + </body> +</html> diff --git a/browser/components/privatebrowsing/jar.mn b/browser/components/privatebrowsing/jar.mn new file mode 100644 index 000000000..5667dc338 --- /dev/null +++ b/browser/components/privatebrowsing/jar.mn @@ -0,0 +1,6 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/aboutPrivateBrowsing.xhtml (content/aboutPrivateBrowsing.xhtml) diff --git a/browser/components/privatebrowsing/moz.build b/browser/components/privatebrowsing/moz.build new file mode 100644 index 000000000..ecb79e730 --- /dev/null +++ b/browser/components/privatebrowsing/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file diff --git a/browser/components/search/content/engineManager.js b/browser/components/search/content/engineManager.js new file mode 100644 index 000000000..b9ed17cbc --- /dev/null +++ b/browser/components/search/content/engineManager.js @@ -0,0 +1,492 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const Ci = Components.interfaces; +const Cc = Components.classes; + +const ENGINE_FLAVOR = "text/x-moz-search-engine"; + +const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled"; + +var gEngineView = null; + +var gEngineManagerDialog = { + init: function() { + gEngineView = new EngineView(new EngineStore()); + + var suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF); + document.getElementById("enableSuggest").checked = suggestEnabled; + + var tree = document.getElementById("engineList"); + tree.view = gEngineView; + + Services.obs.addObserver(this, "browser-search-engine-modified", false); + }, + + destroy: function() { + // Remove the observer + Services.obs.removeObserver(this, "browser-search-engine-modified"); + }, + + observe: function(aEngine, aTopic, aVerb) { + if (aTopic == "browser-search-engine-modified") { + aEngine.QueryInterface(Ci.nsISearchEngine); + switch (aVerb) { + case "engine-added": + gEngineView._engineStore.addEngine(aEngine); + gEngineView.rowCountChanged(gEngineView.lastIndex, 1); + break; + case "engine-changed": + gEngineView._engineStore.reloadIcons(); + gEngineView.invalidate(); + break; + case "engine-removed": + case "engine-current": + case "engine-default": + // Not relevant + break; + } + } + }, + + onOK: function() { + // Set the preference + var newSuggestEnabled = document.getElementById("enableSuggest").checked; + Services.prefs.setBoolPref(BROWSER_SUGGEST_PREF, newSuggestEnabled); + + // Commit the changes + gEngineView._engineStore.commit(); + }, + + onRestoreDefaults: function() { + var num = gEngineView._engineStore.restoreDefaultEngines(); + gEngineView.rowCountChanged(0, num); + gEngineView.invalidate(); + }, + + showRestoreDefaults: function(val) { + document.documentElement.getButton("extra2").disabled = !val; + }, + + loadAddEngines: function() { + this.onOK(); + window.opener.BrowserSearch.loadAddEngines(); + window.close(); + }, + + remove: function() { + gEngineView._engineStore.removeEngine(gEngineView.selectedEngine); + var index = gEngineView.selectedIndex; + gEngineView.rowCountChanged(index, -1); + gEngineView.invalidate(); + gEngineView.selection.select(Math.min(index, gEngineView.lastIndex)); + gEngineView.ensureRowIsVisible(gEngineView.currentIndex); + document.getElementById("engineList").focus(); + }, + + /** + * Moves the selected engine either up or down in the engine list + * @param aDir + * -1 to move the selected engine down, +1 to move it up. + */ + bump: function(aDir) { + var selectedEngine = gEngineView.selectedEngine; + var newIndex = gEngineView.selectedIndex - aDir; + + gEngineView._engineStore.moveEngine(selectedEngine, newIndex); + + gEngineView.invalidate(); + gEngineView.selection.select(newIndex); + gEngineView.ensureRowIsVisible(newIndex); + this.showRestoreDefaults(true); + document.getElementById("engineList").focus(); + }, + + editKeyword: Task.async(function* () { + var selectedEngine = gEngineView.selectedEngine; + if (!selectedEngine) + return; + + var alias = { value: selectedEngine.alias }; + var strings = document.getElementById("engineManagerBundle"); + var title = strings.getString("editTitle"); + var msg = strings.getFormattedString("editMsg", [selectedEngine.name]); + + while (Services.prompt.prompt(window, title, msg, alias, null, {})) { + var bduplicate = false; + var eduplicate = false; + var dupName = ""; + + if (alias.value != "") { + // Check for duplicates in Places keywords. + bduplicate = !!(yield PlacesUtils.keywords.fetch(alias.value)); + + // Check for duplicates in changes we haven't committed yet + let engines = gEngineView._engineStore.engines; + for each (let engine in engines) { + if (engine.alias == alias.value && + engine.name != selectedEngine.name) { + eduplicate = true; + dupName = engine.name; + break; + } + } + } + + // Notify the user if they have chosen an existing engine/bookmark keyword + if (eduplicate || bduplicate) { + var dtitle = strings.getString("duplicateTitle"); + var bmsg = strings.getString("duplicateBookmarkMsg"); + var emsg = strings.getFormattedString("duplicateEngineMsg", [dupName]); + + Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg); + } else { + gEngineView._engineStore.changeEngine(selectedEngine, "alias", + alias.value); + gEngineView.invalidate(); + break; + } + } + }), + + onSelect: function() { + // Buttons only work if an engine is selected and it's not the last engine, + // the latter is true when the selected is first and last at the same time. + var lastSelected = (gEngineView.selectedIndex == gEngineView.lastIndex); + var firstSelected = (gEngineView.selectedIndex == 0); + var noSelection = (gEngineView.selectedIndex == -1); + + document.getElementById("cmd_remove") + .setAttribute("disabled", noSelection || + (firstSelected && lastSelected)); + + document.getElementById("cmd_moveup") + .setAttribute("disabled", noSelection || firstSelected); + + document.getElementById("cmd_movedown") + .setAttribute("disabled", noSelection || lastSelected); + + document.getElementById("cmd_editkeyword") + .setAttribute("disabled", noSelection); + } +}; + +function onDragEngineStart(event) { + var selectedIndex = gEngineView.selectedIndex; + if (selectedIndex >= 0) { + event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString()); + event.dataTransfer.effectAllowed = "move"; + } +} + +// "Operation" objects +function EngineMoveOp(aEngineClone, aNewIndex) { + if (!aEngineClone) + throw new Error("bad args to new EngineMoveOp!"); + this._engine = aEngineClone.originalEngine; + this._newIndex = aNewIndex; +} +EngineMoveOp.prototype = { + _engine: null, + _newIndex: null, + commit: function() { + Services.search.moveEngine(this._engine, this._newIndex); + } +} + +function EngineRemoveOp(aEngineClone) { + if (!aEngineClone) + throw new Error("bad args to new EngineRemoveOp!"); + this._engine = aEngineClone.originalEngine; +} +EngineRemoveOp.prototype = { + _engine: null, + commit: function() { + Services.search.removeEngine(this._engine); + } +} + +function EngineUnhideOp(aEngineClone, aNewIndex) { + if (!aEngineClone) + throw new Error("bad args to new EngineUnhideOp!"); + this._engine = aEngineClone.originalEngine; + this._newIndex = aNewIndex; +} +EngineUnhideOp.prototype = { + _engine: null, + _newIndex: null, + commit: function() { + this._engine.hidden = false; + Services.search.moveEngine(this._engine, this._newIndex); + } +} + +function EngineChangeOp(aEngineClone, aProp, aValue) { + if (!aEngineClone) + throw new Error("bad args to new EngineChangeOp!"); + + this._engine = aEngineClone.originalEngine; + this._prop = aProp; + this._newValue = aValue; +} +EngineChangeOp.prototype = { + _engine: null, + _prop: null, + _newValue: null, + commit: function() { + this._engine[this._prop] = this._newValue; + } +} + +function EngineStore() { + this._engines = Services.search.getVisibleEngines().map(this._cloneEngine); + this._defaultEngines = Services.search.getDefaultEngines().map(this._cloneEngine); + + this._ops = []; + + // check if we need to disable the restore defaults button + var someHidden = this._defaultEngines.some(function(e) e.hidden); + gEngineManagerDialog.showRestoreDefaults(someHidden); +} +EngineStore.prototype = { + _engines: null, + _defaultEngines: null, + _ops: null, + + get engines() { + return this._engines; + }, + set engines(val) { + this._engines = val; + return val; + }, + + _getIndexForEngine: function(aEngine) { + return this._engines.indexOf(aEngine); + }, + + _getEngineByName: function(aName) { + for each (var engine in this._engines) + if (engine.name == aName) + return engine; + + return null; + }, + + _cloneEngine: function(aEngine) { + var clonedObj={}; + for (var i in aEngine) + clonedObj[i] = aEngine[i]; + clonedObj.originalEngine = aEngine; + return clonedObj; + }, + + // Callback for Array's some(). A thisObj must be passed to some() + _isSameEngine: function(aEngineClone) { + return aEngineClone.originalEngine == this.originalEngine; + }, + + commit: function() { + var currentEngine = this._cloneEngine(Services.search.currentEngine); + for (var i = 0; i < this._ops.length; i++) + this._ops[i].commit(); + + // Restore currentEngine if it is a default engine that is still visible. + // Needed if the user deletes currentEngine and then restores it. + if (this._defaultEngines.some(this._isSameEngine, currentEngine) && + !currentEngine.originalEngine.hidden) + Services.search.currentEngine = currentEngine.originalEngine; + }, + + addEngine: function(aEngine) { + this._engines.push(this._cloneEngine(aEngine)); + }, + + moveEngine: function(aEngine, aNewIndex) { + if (aNewIndex < 0 || aNewIndex > this._engines.length - 1) + throw new Error("ES_moveEngine: invalid aNewIndex!"); + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("ES_moveEngine: invalid engine?"); + + if (index == aNewIndex) + return; // nothing to do + + // Move the engine in our internal store + var removedEngine = this._engines.splice(index, 1)[0]; + this._engines.splice(aNewIndex, 0, removedEngine); + + this._ops.push(new EngineMoveOp(aEngine, aNewIndex)); + }, + + removeEngine: function(aEngine) { + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("invalid engine?"); + + this._engines.splice(index, 1); + this._ops.push(new EngineRemoveOp(aEngine)); + if (this._defaultEngines.some(this._isSameEngine, aEngine)) + gEngineManagerDialog.showRestoreDefaults(true); + }, + + restoreDefaultEngines: function() { + var added = 0; + + for (var i = 0; i < this._defaultEngines.length; ++i) { + var e = this._defaultEngines[i]; + + // If the engine is already in the list, just move it. + if (this._engines.some(this._isSameEngine, e)) { + this.moveEngine(this._getEngineByName(e.name), i); + } else { + // Otherwise, add it back to our internal store + this._engines.splice(i, 0, e); + this._ops.push(new EngineUnhideOp(e, i)); + added++; + } + } + gEngineManagerDialog.showRestoreDefaults(false); + return added; + }, + + changeEngine: function(aEngine, aProp, aNewValue) { + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("invalid engine?"); + + this._engines[index][aProp] = aNewValue; + this._ops.push(new EngineChangeOp(aEngine, aProp, aNewValue)); + }, + + reloadIcons: function() { + this._engines.forEach(function(e) { + e.uri = e.originalEngine.uri; + }); + } +} + +function EngineView(aEngineStore) { + this._engineStore = aEngineStore; +} +EngineView.prototype = { + _engineStore: null, + tree: null, + + get lastIndex() { + return this.rowCount - 1; + }, + get selectedIndex() { + var seln = this.selection; + if (seln.getRangeCount() > 0) { + var min = {}; + seln.getRangeAt(0, min, {}); + return min.value; + } + return -1; + }, + get selectedEngine() { + return this._engineStore.engines[this.selectedIndex]; + }, + + // Helpers + rowCountChanged: function(index, count) { + this.tree.rowCountChanged(index, count); + }, + + invalidate: function() { + this.tree.invalidate(); + }, + + ensureRowIsVisible: function(index) { + this.tree.ensureRowIsVisible(index); + }, + + getSourceIndexFromDrag: function(dataTransfer) { + return parseInt(dataTransfer.getData(ENGINE_FLAVOR)); + }, + + // nsITreeView + get rowCount() { + return this._engineStore.engines.length; + }, + + getImageSrc: function(index, column) { + if (column.id == "engineName" && this._engineStore.engines[index].iconURI) + return this._engineStore.engines[index].iconURI.spec; + return ""; + }, + + getCellText: function(index, column) { + if (column.id == "engineName") + return this._engineStore.engines[index].name; + else if (column.id == "engineKeyword") + return this._engineStore.engines[index].alias; + return ""; + }, + + setTree: function(tree) { + this.tree = tree; + }, + + canDrop: function(targetIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + return (sourceIndex != -1 && + sourceIndex != targetIndex && + sourceIndex != targetIndex + orientation); + }, + + drop: function(dropIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + var sourceEngine = this._engineStore.engines[sourceIndex]; + + if (dropIndex > sourceIndex) { + if (orientation == Ci.nsITreeView.DROP_BEFORE) + dropIndex--; + } else { + if (orientation == Ci.nsITreeView.DROP_AFTER) + dropIndex++; + } + + this._engineStore.moveEngine(sourceEngine, dropIndex); + gEngineManagerDialog.showRestoreDefaults(true); + + // Redraw, and adjust selection + this.invalidate(); + this.selection.select(dropIndex); + }, + + selection: null, + getRowProperties: function(index) { return ""; }, + getCellProperties: function(index, column) { return ""; }, + getColumnProperties: function(column) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isContainerEmpty: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function(index) { return false; }, + getParentIndex: function(index) { return -1; }, + hasNextSibling: function(parentIndex, index) { return false; }, + getLevel: function(index) { return 0; }, + getProgressMode: function(index, column) { }, + getCellValue: function(index, column) { }, + toggleOpenState: function(index) { }, + cycleHeader: function(column) { }, + selectionChanged: function() { }, + cycleCell: function(row, column) { }, + isEditable: function(index, column) { return false; }, + isSelectable: function(index, column) { return false; }, + setCellValue: function(index, column, value) { }, + setCellText: function(index, column, value) { }, + performAction: function(action) { }, + performActionOnRow: function(action, index) { }, + performActionOnCell: function(action, index, column) { } +}; diff --git a/browser/components/search/content/engineManager.xul b/browser/components/search/content/engineManager.xul new file mode 100644 index 000000000..1152ef8db --- /dev/null +++ b/browser/components/search/content/engineManager.xul @@ -0,0 +1,93 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/engineManager.css"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/engineManager.dtd"> + +<dialog id="engineManager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + buttons="accept,cancel,extra2" + buttonlabelextra2="&restoreDefaults.label;" + buttonaccesskeyextra2="&restoreDefaults.accesskey;" + onload="gEngineManagerDialog.init();" + onunload="gEngineManagerDialog.destroy();" + ondialogaccept="gEngineManagerDialog.onOK();" + ondialogextra2="gEngineManagerDialog.onRestoreDefaults();" + title="&engineManager.title;" + style="&engineManager.style;" + persist="screenX screenY width height" + windowtype="Browser:SearchManager"> + + <script type="application/javascript" + src="chrome://browser/content/search/engineManager.js"/> + + <commandset id="engineManagerCommandSet"> + <command id="cmd_remove" + oncommand="gEngineManagerDialog.remove();" + disabled="true"/> + <command id="cmd_moveup" + oncommand="gEngineManagerDialog.bump(1);" + disabled="true"/> + <command id="cmd_movedown" + oncommand="gEngineManagerDialog.bump(-1);" + disabled="true"/> + <command id="cmd_editkeyword" + oncommand="gEngineManagerDialog.editKeyword().catch(Components.utils.reportError);" + disabled="true"/> + </commandset> + + <keyset id="engineManagerKeyset"> + <key id="delete" keycode="VK_DELETE" command="cmd_remove"/> + </keyset> + + <stringbundleset id="engineManagerBundleset"> + <stringbundle id="engineManagerBundle" src="chrome://browser/locale/engineManager.properties"/> + </stringbundleset> + + <description>&engineManager.intro;</description> + <separator class="thin"/> + <hbox flex="1"> + <tree id="engineList" flex="1" rows="10" hidecolumnpicker="true" + seltype="single" onselect="gEngineManagerDialog.onSelect();"> + <treechildren id="engineChildren" flex="1" + ondragstart="onDragEngineStart(event);"/> + <treecols> + <treecol id="engineName" flex="4" label="&columnLabel.name;"/> + <treecol id="engineKeyword" flex="1" label="&columnLabel.keyword;"/> + </treecols> + </tree> + <vbox> + <spacer flex="1"/> + <button id="edit" + label="&edit.label;" + accesskey="&edit.accesskey;" + command="cmd_editkeyword"/> + <button id="up" + label="&up.label;" + accesskey="&up.accesskey;" + command="cmd_moveup"/> + <button id="down" + label="&dn.label;" + accesskey="&dn.accesskey;" + command="cmd_movedown"/> + <spacer flex="1"/> + <button id="remove" + label="&remove.label;" + accesskey="&remove.accesskey;" + command="cmd_remove"/> + </vbox> + </hbox> + <hbox> + <checkbox id="enableSuggest" + label="&enableSuggest.label;" + accesskey="&enableSuggest.accesskey;"/> + </hbox> + <hbox> + <label id="addEngines" class="text-link" value="&addEngine.label;" + onclick="if (event.button == 0) { gEngineManagerDialog.loadAddEngines(); }"/> + </hbox> +</dialog> diff --git a/browser/components/search/content/search.xml b/browser/components/search/content/search.xml new file mode 100644 index 000000000..eccaa072a --- /dev/null +++ b/browser/components/search/content/search.xml @@ -0,0 +1,834 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ +<!ENTITY % searchBarDTD SYSTEM "chrome://browser/locale/searchbar.dtd" > +%searchBarDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> +%browserDTD; +]> + +<bindings id="SearchBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="searchbar"> + <resources> + <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/> + <stylesheet src="chrome://browser/skin/searchbar.css"/> + </resources> + <content> + <xul:stringbundle src="chrome://browser/locale/search.properties" + anonid="searchbar-stringbundle"/> + + <xul:textbox class="searchbar-textbox" + anonid="searchbar-textbox" + type="autocomplete" + flex="1" + autocompletepopup="PopupAutoComplete" + autocompletesearch="search-autocomplete" + autocompletesearchparam="searchbar-history" + timeout="250" + maxrows="10" + completeselectedindex="true" + showcommentcolumn="true" + tabscrolling="true" + xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines"> + <xul:box> + <xul:button class="searchbar-engine-button" + type="menu" + anonid="searchbar-engine-button"> + <xul:image class="searchbar-engine-image" xbl:inherits="src"/> + <xul:image class="searchbar-dropmarker-image"/> + <xul:menupopup class="searchbar-popup" + anonid="searchbar-popup"> + <xul:menuseparator/> + <xul:menuitem class="open-engine-manager" + anonid="open-engine-manager" + label="&cmd_engineManager.label;" + oncommand="openManager(event);"/> + </xul:menupopup> + </xul:button> + </xul:box> + <xul:hbox class="search-go-container"> + <xul:image class="search-go-button" + anonid="search-go-button" + onclick="handleSearchCommand(event);" + tooltiptext="&searchEndCap.label;"/> + </xul:hbox> + </xul:textbox> + </content> + + <implementation implements="nsIObserver"> + <constructor><![CDATA[ + if (this.parentNode.parentNode.localName == "toolbarpaletteitem") + return; + // Make sure we rebuild the popup in onpopupshowing + this._needToBuildPopup = true; + + var os = + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.addObserver(this, "browser-search-engine-modified", false); + + this._initialized = true; + + this.searchService.init((function search_init_cb(aStatus) { + // Bail out if the binding has been destroyed + if (!this._initialized) + return; + + if (Components.isSuccessCode(aStatus)) { + // Refresh the display (updating icon, etc) + this.updateDisplay(); + } else { + Components.utils.reportError("Cannot initialize search service, bailing out: " + aStatus); + } + }).bind(this)); + ]]></constructor> + + <destructor><![CDATA[ + if (this._initialized) { + this._initialized = false; + + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.removeObserver(this, "browser-search-engine-modified"); + } + + // Make sure to break the cycle from _textbox to us. Otherwise we leak + // the world. But make sure it's actually pointing to us. + if (this._textbox.mController.input == this) + this._textbox.mController.input = null; + ]]></destructor> + + <field name="_stringBundle">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-stringbundle");</field> + <field name="_textbox">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-textbox");</field> + <field name="_popup">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-popup");</field> + <field name="_ss">null</field> + <field name="_engines">null</field> + <field name="FormHistory" readonly="true"> + (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory; + </field> + + <property name="engines" readonly="true"> + <getter><![CDATA[ + if (!this._engines) + this._engines = this.searchService.getVisibleEngines(); + return this._engines; + ]]></getter> + </property> + + <field name="searchButton">document.getAnonymousElementByAttribute(this, + "anonid", "searchbar-engine-button");</field> + + <property name="currentEngine"> + <setter><![CDATA[ + let ss = this.searchService; + ss.defaultEngine = ss.currentEngine = val; + return val; + ]]></setter> + <getter><![CDATA[ + var currentEngine = this.searchService.currentEngine; + // Return a dummy engine if there is no currentEngine + return currentEngine || {name: "", uri: null}; + ]]></getter> + </property> + + <!-- textbox is used by sanitize.js to clear the undo history when + clearing form information. --> + <property name="textbox" readonly="true" + onget="return this._textbox;"/> + + <property name="searchService" readonly="true"> + <getter><![CDATA[ + if (!this._ss) { + const nsIBSS = Components.interfaces.nsIBrowserSearchService; + this._ss = + Components.classes["@mozilla.org/browser/search-service;1"] + .getService(nsIBSS); + } + return this._ss; + ]]></getter> + </property> + + <property name="value" onget="return this._textbox.value;" + onset="return this._textbox.value = val;"/> + + <method name="focus"> + <body><![CDATA[ + this._textbox.focus(); + ]]></body> + </method> + + <method name="select"> + <body><![CDATA[ + this._textbox.select(); + ]]></body> + </method> + + <method name="observe"> + <parameter name="aEngine"/> + <parameter name="aTopic"/> + <parameter name="aVerb"/> + <body><![CDATA[ + if (aTopic == "browser-search-engine-modified") { + switch (aVerb) { + case "engine-removed": + this.offerNewEngine(aEngine); + break; + case "engine-added": + this.hideNewEngine(aEngine); + break; + case "engine-current": + // The current engine was changed. Rebuilding the menu appears to + // confuse its idea of whether it should be open when it's just + // been clicked, so we force it to close now. + this._popup.hidePopup(); + break; + case "engine-changed": + // An engine was removed (or hidden) or added, or an icon was + // changed. Do nothing special. + } + + // Make sure the engine list is refetched next time it's needed + this._engines = null; + + // Rebuild the popup and update the display after any modification. + this.rebuildPopup(); + this.updateDisplay(); + } + ]]></body> + </method> + + <!-- There are two seaprate lists of search engines, whose uses intersect + in this file. The search service (nsIBrowserSearchService and + nsSearchService.js) maintains a list of Engine objects which is used to + populate the searchbox list of available engines and to perform queries. + That list is accessed here via this.SearchService, and it's that sort of + Engine that is passed to this binding's observer as aEngine. + + In addition, browser.js fills two lists of autodetected search engines + (browser.engines and browser.hiddenEngines) as properties of + mCurrentBrowser. Those lists contain unnamed JS objects of the form + { uri:, title:, icon: }, and that's what the searchbar uses to determine + whether to show any "Add <EngineName>" menu items in the drop-down. + + The two types of engines are currently related by their identifying + titles (the Engine object's 'name'), although that may change; see bug + 335102. --> + + <!-- If the engine that was just removed from the searchbox list was + autodetected on this page, move it to each browser's active list so it + will be offered to be added again. --> + <method name="offerNewEngine"> + <parameter name="aEngine"/> + <body><![CDATA[ + var allbrowsers = getBrowser().mPanelContainer.childNodes; + for (var tab = 0; tab < allbrowsers.length; tab++) { + var browser = getBrowser().getBrowserAtIndex(tab); + if (browser.hiddenEngines) { + // XXX This will need to be changed when engines are identified by + // URL rather than title; see bug 335102. + var removeTitle = aEngine.wrappedJSObject.name; + for (var i = 0; i < browser.hiddenEngines.length; i++) { + if (browser.hiddenEngines[i].title == removeTitle) { + if (!browser.engines) + browser.engines = []; + browser.engines.push(browser.hiddenEngines[i]); + browser.hiddenEngines.splice(i, 1); + break; + } + } + } + } + ]]></body> + </method> + + <!-- If the engine that was just added to the searchbox list was + autodetected on this page, move it to each browser's hidden list so it is + no longer offered to be added. --> + <method name="hideNewEngine"> + <parameter name="aEngine"/> + <body><![CDATA[ + var allbrowsers = getBrowser().mPanelContainer.childNodes; + for (var tab = 0; tab < allbrowsers.length; tab++) { + var browser = getBrowser().getBrowserAtIndex(tab); + if (browser.engines) { + // XXX This will need to be changed when engines are identified by + // URL rather than title; see bug 335102. + var removeTitle = aEngine.wrappedJSObject.name; + for (var i = 0; i < browser.engines.length; i++) { + if (browser.engines[i].title == removeTitle) { + if (!browser.hiddenEngines) + browser.hiddenEngines = []; + browser.hiddenEngines.push(browser.engines[i]); + browser.engines.splice(i, 1); + break; + } + } + } + } + ]]></body> + </method> + + <method name="setIcon"> + <parameter name="element"/> + <parameter name="uri"/> + <body><![CDATA[ + element.setAttribute("src", uri); + ]]></body> + </method> + + <method name="updateDisplay"> + <body><![CDATA[ + var uri = this.currentEngine.iconURI; + this.setIcon(this, uri ? uri.spec : ""); + + var name = this.currentEngine.name; + var text = this._stringBundle.getFormattedString("searchtip", [name]); + this._textbox.placeholder = name; + this._textbox.label = text; + this._textbox.tooltipText = text; + ]]></body> + </method> + + <!-- Rebuilds the dynamic portion of the popup menu (i.e., the menu items + for new search engines that can be added to the available list). This + is called each time the popup is shown. + --> + <method name="rebuildPopupDynamic"> + <body><![CDATA[ + // We might not have added the main popup items yet, do that first + // if needed. + if (this._needToBuildPopup) + this.rebuildPopup(); + + var popup = this._popup; + // Clear any addengine menuitems, including addengine-item entries and + // the addengine-separator. Work backward to avoid invalidating the + // indexes as items are removed. + var items = popup.childNodes; + for (var i = items.length - 1; i >= 0; i--) { + if (items[i].classList.contains("addengine-item") || + items[i].classList.contains("addengine-separator")) + popup.removeChild(items[i]); + } + + var addengines = getBrowser().mCurrentBrowser.engines; + if (addengines && addengines.length > 0) { + const kXULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + // Find the (first) separator in the remaining menu, or the first item + // if no separators are present. + var insertLocation = popup.firstChild; + while (insertLocation.nextSibling && + insertLocation.localName != "menuseparator") { + insertLocation = insertLocation.nextSibling; + } + if (insertLocation.localName != "menuseparator") + insertLocation = popup.firstChild; + + var separator = document.createElementNS(kXULNS, "menuseparator"); + separator.setAttribute("class", "addengine-separator"); + popup.insertBefore(separator, insertLocation); + + // Insert the "add this engine" items. + for (var i = 0; i < addengines.length; i++) { + var menuitem = document.createElement("menuitem"); + var engineInfo = addengines[i]; + var labelStr = + this._stringBundle.getFormattedString("cmd_addFoundEngine", + [engineInfo.title]); + menuitem = document.createElementNS(kXULNS, "menuitem"); + menuitem.setAttribute("class", "menuitem-iconic addengine-item"); + menuitem.setAttribute("label", labelStr); + menuitem.setAttribute("tooltiptext", engineInfo.uri); + menuitem.setAttribute("uri", engineInfo.uri); + if (engineInfo.icon) + this.setIcon(menuitem, engineInfo.icon); + menuitem.setAttribute("title", engineInfo.title); + popup.insertBefore(menuitem, insertLocation); + } + } + ]]></body> + </method> + + <!-- Rebuilds the list of visible search engines in the menu. Does not remove + or update any dynamic entries (i.e., "Add this engine" items) nor the + Manage Engines item. This is called by the observer when the list of + visible engines, or the currently selected engine, has changed. + --> + <method name="rebuildPopup"> + <body><![CDATA[ + var popup = this._popup; + + // Clear the popup, down to the first separator + while (popup.firstChild && popup.firstChild.localName != "menuseparator") + popup.removeChild(popup.firstChild); + + const kXULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + var engines = this.engines; + for (var i = engines.length - 1; i >= 0; --i) { + var menuitem = document.createElementNS(kXULNS, "menuitem"); + var name = engines[i].name; + menuitem.setAttribute("label", name); + menuitem.setAttribute("id", name); + menuitem.setAttribute("class", "menuitem-iconic searchbar-engine-menuitem menuitem-with-favicon"); + // Since this menu is rebuilt by the observer method whenever a new + // engine is selected, the "selected" attribute does not need to be + // explicitly cleared anywhere. + if (engines[i] == this.currentEngine) + menuitem.setAttribute("selected", "true"); + var tooltip = this._stringBundle.getFormattedString("searchtip", [name]); + menuitem.setAttribute("tooltiptext", tooltip); + if (engines[i].iconURI) + this.setIcon(menuitem, engines[i].iconURI.spec); + popup.insertBefore(menuitem, popup.firstChild); + menuitem.engine = engines[i]; + } + + this._needToBuildPopup = false; + ]]></body> + </method> + + <method name="openManager"> + <parameter name="aEvent"/> + <body><![CDATA[ + var wm = + Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + + var window = wm.getMostRecentWindow("Browser:SearchManager"); + if (window) + window.focus() + else { + setTimeout(function () { + openDialog("chrome://browser/content/search/engineManager.xul", + "_blank", "chrome,dialog,modal,centerscreen,resizable"); + }, 0); + } + ]]></body> + </method> + + <method name="selectEngine"> + <parameter name="aEvent"/> + <parameter name="isNextEngine"/> + <body><![CDATA[ + // Find the new index + var newIndex = this.engines.indexOf(this.currentEngine); + newIndex += isNextEngine ? 1 : -1; + + if (newIndex >= 0 && newIndex < this.engines.length) { + this.currentEngine = this.engines[newIndex]; + } + + aEvent.preventDefault(); + aEvent.stopPropagation(); + ]]></body> + </method> + + <method name="handleSearchCommand"> + <parameter name="aEvent"/> + <body><![CDATA[ + var textBox = this._textbox; + var textValue = textBox.value; + + var where = "current"; + if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") { + if (aEvent.button == 2) + return; + where = whereToOpenLink(aEvent, false, true); + } + else { + var newTabPref = textBox._prefBranch.getBoolPref("browser.search.openintab"); + if ((aEvent && aEvent.altKey) ^ newTabPref) + where = "tab"; + } + + this.doSearch(textValue, where); + ]]></body> + </method> + + <method name="doSearch"> + <parameter name="aData"/> + <parameter name="aWhere"/> + <body><![CDATA[ + var textBox = this._textbox; + + // Save the current value in the form history + if (aData && !PrivateBrowsingUtils.isWindowPrivate(window)) { + this.FormHistory.update( + { op : "bump", + fieldname : textBox.getAttribute("autocompletesearchparam"), + value : aData }, + { handleError : function(aError) { + Components.utils.reportError("Saving search to form history failed: " + aError.message); + }}); + } + + // null parameter below specifies HTML response for search + var submission = this.currentEngine.getSubmission(aData); + openUILinkIn(submission.uri.spec, aWhere, null, submission.postData); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="command"><![CDATA[ + const target = event.originalTarget; + if (target.engine) { + this.currentEngine = target.engine; + } else if (target.classList.contains("addengine-item")) { + var searchService = + Components.classes["@mozilla.org/browser/search-service;1"] + .getService(Components.interfaces.nsIBrowserSearchService); + // We only detect OpenSearch files + var type = Components.interfaces.nsISearchEngine.DATA_XML; + // Select the installed engine if the installation succeeds + var installCallback = { + onSuccess: engine => this.currentEngine = engine + } + searchService.addEngine(target.getAttribute("uri"), type, + target.getAttribute("src"), false, + installCallback); + } + else + return; + + this.focus(); + this.select(); + ]]></handler> + + <handler event="popupshowing" action="this.rebuildPopupDynamic();"/> + + <handler event="DOMMouseScroll" + phase="capturing" + modifiers="accel" + action="this.selectEngine(event, (event.detail > 0));"/> + + <handler event="focus"> + <![CDATA[ + // Speculatively connect to the current engine's search URI (and + // suggest URI, if different) to reduce request latency + + const SUGGEST_TYPE = "application/x-suggestions+json"; + var engine = this.currentEngine; + var connector = + Services.io.QueryInterface(Components.interfaces.nsISpeculativeConnect); + var searchURI = engine.getSubmission("dummy").uri; + let callbacks = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsILoadContext); + connector.speculativeConnect(searchURI, callbacks); + + if (engine.supportsResponseType(SUGGEST_TYPE)) { + var suggestURI = engine.getSubmission("dummy", SUGGEST_TYPE).uri; + if (suggestURI.prePath != searchURI.prePath) + connector.speculativeConnect(suggestURI, callbacks); + } + ]]></handler> + </handlers> + </binding> + + <binding id="searchbar-textbox" + extends="chrome://global/content/bindings/autocomplete.xml#autocomplete"> + <implementation implements="nsIObserver"> + <constructor><![CDATA[ + const kXULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + if (document.getBindingParent(this).parentNode.parentNode.localName == + "toolbarpaletteitem") + return; + + // Initialize fields + this._stringBundle = document.getBindingParent(this)._stringBundle; + this._prefBranch = + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + this._suggestEnabled = + this._prefBranch.getBoolPref("browser.search.suggest.enabled"); + this._clickSelectsAll = + this._prefBranch.getBoolPref("browser.urlbar.clickSelectsAll"); + + this.setAttribute("clickSelectsAll", this._clickSelectsAll); + + // Add items to context menu and attach controller to handle them + var textBox = document.getAnonymousElementByAttribute(this, + "anonid", "textbox-input-box"); + var cxmenu = document.getAnonymousElementByAttribute(textBox, + "anonid", "input-box-contextmenu"); + var pasteAndSearch; + cxmenu.addEventListener("popupshowing", function() { + if (!pasteAndSearch) + return; + var controller = document.commandDispatcher.getControllerForCommand("cmd_paste"); + var enabled = controller.isCommandEnabled("cmd_paste"); + if (enabled) + pasteAndSearch.removeAttribute("disabled"); + else + pasteAndSearch.setAttribute("disabled", "true"); + }, false); + + var element, label, akey; + + element = document.createElementNS(kXULNS, "menuseparator"); + cxmenu.appendChild(element); + + var insertLocation = cxmenu.firstChild; + while (insertLocation.nextSibling && + insertLocation.getAttribute("cmd") != "cmd_paste") + insertLocation = insertLocation.nextSibling; + if (insertLocation) { + element = document.createElementNS(kXULNS, "menuitem"); + label = this._stringBundle.getString("cmd_pasteAndSearch"); + element.setAttribute("label", label); + element.setAttribute("anonid", "paste-and-search"); + element.setAttribute("oncommand", + "BrowserSearch.searchBar.select(); goDoCommand('cmd_paste'); BrowserSearch.searchBar.handleSearchCommand();"); + cxmenu.insertBefore(element, insertLocation.nextSibling); + pasteAndSearch = element; + } + + element = document.createElementNS(kXULNS, "menuitem"); + label = this._stringBundle.getString("cmd_clearHistory"); + akey = this._stringBundle.getString("cmd_clearHistory_accesskey"); + element.setAttribute("label", label); + element.setAttribute("accesskey", akey); + element.setAttribute("cmd", "cmd_clearhistory"); + cxmenu.appendChild(element); + + element = document.createElementNS(kXULNS, "menuitem"); + label = this._stringBundle.getString("cmd_showSuggestions"); + akey = this._stringBundle.getString("cmd_showSuggestions_accesskey"); + element.setAttribute("anonid", "toggle-suggest-item"); + element.setAttribute("label", label); + element.setAttribute("accesskey", akey); + element.setAttribute("cmd", "cmd_togglesuggest"); + element.setAttribute("type", "checkbox"); + element.setAttribute("checked", this._suggestEnabled); + element.setAttribute("autocheck", "false"); + this._suggestMenuItem = element; + cxmenu.appendChild(element); + + this.controllers.appendController(this.searchbarController); + + // Add observer for suggest preference + var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + prefs.addObserver("browser.search.suggest.enabled", this, false); + prefs.addObserver("browser.urlbar.clickSelectsAll", this, false); + ]]></constructor> + + <destructor><![CDATA[ + var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + prefs.removeObserver("browser.search.suggest.enabled", this); + prefs.removeObserver("browser.urlbar.clickSelectsAll", this); + + // Because XBL and the customize toolbar code interacts poorly, + // there may not be anything to remove here + try { + this.controllers.removeController(this.searchbarController); + } catch (ex) { } + ]]></destructor> + + <field name="_stringBundle"/> + <field name="_prefBranch"/> + <field name="_suggestMenuItem"/> + <field name="_suggestEnabled"/> + <field name="_clickSelectsAll"/> + + <!-- + This overrides the searchParam property in autocomplete.xml. We're + hijacking this property as a vehicle for delivering the privacy + information about the window into the guts of nsSearchSuggestions. + + Note that the setter is the same as the parent. We were not sure whether + we can override just the getter. If that proves to be the case, the setter + can be removed. + --> + <property name="searchParam" + onget="return this.getAttribute('autocompletesearchparam') + + (PrivateBrowsingUtils.isWindowPrivate(window) ? '|private' : '');" + onset="this.setAttribute('autocompletesearchparam', val); return val;"/> + + <!-- + This method overrides the autocomplete binding's openPopup (essentially + duplicating the logic from the autocomplete popup binding's + openAutocompletePopup method), modifying it so that the popup is aligned with + the inner textbox, but sized to not extend beyond the search bar border. + --> + <method name="openPopup"> + <body><![CDATA[ + var popup = this.popup; + if (!popup.mPopupOpen) { + // Initially the panel used for the searchbar (PopupAutoComplete + // in browser.xul) is hidden to avoid impacting startup / new + // window performance. The base binding's openPopup would normally + // call the overriden openAutocompletePopup in urlbarBindings.xml's + // browser-autocomplete-result-popup binding to unhide the popup, + // but since we're overriding openPopup we need to unhide the panel + // ourselves. + popup.hidden = false; + + popup.mInput = this; + popup.view = this.controller.QueryInterface(Components.interfaces.nsITreeView); + popup.invalidate(); + + popup.showCommentColumn = this.showCommentColumn; + popup.showImageColumn = this.showImageColumn; + + document.popupNode = null; + + const isRTL = getComputedStyle(this, "").direction == "rtl"; + + var outerRect = this.getBoundingClientRect(); + var innerRect = this.inputField.getBoundingClientRect(); + if (isRTL) { + var width = innerRect.right - outerRect.left; + } else { + var width = outerRect.right - innerRect.left; + } + popup.setAttribute("width", width > 100 ? width : 100); + + var yOffset = outerRect.bottom - innerRect.bottom; + popup.openPopup(this.inputField, "after_start", 0, yOffset, false, false); + } + ]]></body> + </method> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + if (aTopic == "nsPref:changed") { + switch (aData) { + case "browser.search.suggest.enabled": + this._suggestEnabled = this._prefBranch.getBoolPref(aData); + this._suggestMenuItem.setAttribute("checked", this._suggestEnabled); + break; + case "browser.urlbar.clickSelectsAll": + this._clickSelectsAll = this._prefBranch.getBoolPref(aData); + this.setAttribute("clickSelectsAll", this._clickSelectsAll); + break; + } + } + ]]></body> + </method> + + <method name="openSearch"> + <body> + <![CDATA[ + // Don't open search popup if history popup is open + if (!this.popupOpen) { + document.getBindingParent(this).searchButton.open = true; + return false; + } + return true; + ]]> + </body> + </method> + + <!-- override |onTextEntered| in autocomplete.xml --> + <method name="onTextEntered"> + <parameter name="aEvent"/> + <body><![CDATA[ + var evt = aEvent || this.mEnterEvent; + document.getBindingParent(this).handleSearchCommand(evt); + this.mEnterEvent = null; + ]]></body> + </method> + + <!-- nsIController --> + <field name="searchbarController" readonly="true"><![CDATA[({ + _self: this, + supportsCommand: function(aCommand) { + return aCommand == "cmd_clearhistory" || + aCommand == "cmd_togglesuggest"; + }, + + isCommandEnabled: function(aCommand) { + return true; + }, + + doCommand: function (aCommand) { + switch (aCommand) { + case "cmd_clearhistory": + var param = this._self.getAttribute("autocompletesearchparam"); + + let searchBar = this._self.parentNode; + + BrowserSearch.searchBar.FormHistory.update({ op : "remove", fieldname : param }, null); + this._self.value = ""; + break; + case "cmd_togglesuggest": + // The pref observer will update _suggestEnabled and the menu + // checkmark. + this._self._prefBranch.setBoolPref("browser.search.suggest.enabled", + !this._self._suggestEnabled); + break; + default: + // do nothing with unrecognized command + } + } + })]]></field> + </implementation> + + <handlers> + <handler event="keypress" keycode="VK_UP" modifiers="accel" + phase="capturing" + action="document.getBindingParent(this).selectEngine(event, false);"/> + + <handler event="keypress" keycode="VK_DOWN" modifiers="accel" + phase="capturing" + action="document.getBindingParent(this).selectEngine(event, true);"/> + + <handler event="keypress" keycode="VK_DOWN" modifiers="alt" + phase="capturing" + action="return this.openSearch();"/> + + <handler event="keypress" keycode="VK_UP" modifiers="alt" + phase="capturing" + action="return this.openSearch();"/> + + <handler event="keypress" keycode="VK_F4" + phase="capturing" + action="return this.openSearch();"/> + + <handler event="dragover"> + <![CDATA[ + var types = event.dataTransfer.types; + if (types.contains("text/plain") || types.contains("text/x-moz-text-internal")) + event.preventDefault(); + ]]> + </handler> + + <handler event="drop"> + <![CDATA[ + var dataTransfer = event.dataTransfer; + var data = dataTransfer.getData("text/plain"); + if (!data) + data = dataTransfer.getData("text/x-moz-text-internal"); + if (data) { + event.preventDefault(); + this.value = data; + this.onTextEntered(event); + } + ]]> + </handler> + + </handlers> + </binding> +</bindings> diff --git a/browser/components/search/content/searchbarBindings.css b/browser/components/search/content/searchbarBindings.css new file mode 100644 index 000000000..b20e2157a --- /dev/null +++ b/browser/components/search/content/searchbarBindings.css @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +.searchbar-textbox { + -moz-binding: url("chrome://browser/content/search/search.xml#searchbar-textbox"); +} + +.searchbar-engine-button { + -moz-user-focus: none; +} diff --git a/browser/components/search/jar.mn b/browser/components/search/jar.mn new file mode 100644 index 000000000..71f6ba45e --- /dev/null +++ b/browser/components/search/jar.mn @@ -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/. + +browser.jar: + content/browser/search/search.xml (content/search.xml) + content/browser/search/searchbarBindings.css (content/searchbarBindings.css) + content/browser/search/engineManager.xul (content/engineManager.xul) + content/browser/search/engineManager.js (content/engineManager.js) diff --git a/browser/components/search/moz.build b/browser/components/search/moz.build new file mode 100644 index 000000000..ecb79e730 --- /dev/null +++ b/browser/components/search/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file diff --git a/browser/components/sessionstore/DocumentUtils.jsm b/browser/components/sessionstore/DocumentUtils.jsm new file mode 100644 index 000000000..2d40a08fc --- /dev/null +++ b/browser/components/sessionstore/DocumentUtils.jsm @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 = [ "DocumentUtils" ]; + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm"); + +this.DocumentUtils = { + /** + * Obtain form data for a DOMDocument instance. + * + * The returned object has 2 keys, "id" and "xpath". Each key holds an object + * which further defines form data. + * + * The "id" object maps element IDs to values. The "xpath" object maps the + * XPath of an element to its value. + * + * @param aDocument + * DOMDocument instance to obtain form data for. + * @return object + * Form data encoded in an object. + */ + getFormData: function(aDocument) { + let formNodes = aDocument.evaluate( + XPathGenerator.restorableFormNodes, + aDocument, + XPathGenerator.resolveNS, + Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null + ); + + let node; + let ret = {id: {}, xpath: {}}; + + // Limit the number of XPath expressions for performance reasons. See + // bug 477564. + const MAX_TRAVERSED_XPATHS = 100; + let generatedCount = 0; + + while (node = formNodes.iterateNext()) { + let nId = node.id; + let hasDefaultValue = true; + let value; + + // Only generate a limited number of XPath expressions for perf reasons + // (cf. bug 477564) + if (!nId && generatedCount > MAX_TRAVERSED_XPATHS) { + continue; + } + + if (node instanceof Ci.nsIDOMHTMLInputElement || + node instanceof Ci.nsIDOMHTMLTextAreaElement) { + switch (node.type) { + case "checkbox": + case "radio": + value = node.checked; + hasDefaultValue = value == node.defaultChecked; + break; + case "file": + value = { type: "file", fileList: node.mozGetFileNameArray() }; + hasDefaultValue = !value.fileList.length; + break; + default: // text, textarea + value = node.value; + hasDefaultValue = value == node.defaultValue; + break; + } + } else if (!node.multiple) { + // <select>s without the multiple attribute are hard to determine the + // default value, so assume we don't have the default. + hasDefaultValue = false; + value = { selectedIndex: node.selectedIndex, value: node.value }; + } else { + // <select>s with the multiple attribute are easier to determine the + // default value since each <option> has a defaultSelected + let options = Array.map(node.options, function(aOpt, aIx) { + let oSelected = aOpt.selected; + hasDefaultValue = hasDefaultValue && (oSelected == aOpt.defaultSelected); + return oSelected ? aOpt.value : -1; + }); + value = options.filter(function(aIx) aIx !== -1); + } + + // In order to reduce XPath generation (which is slow), we only save data + // for form fields that have been changed. (cf. bug 537289) + if (!hasDefaultValue) { + if (nId) { + ret.id[nId] = value; + } else { + generatedCount++; + ret.xpath[XPathGenerator.generate(node)] = value; + } + } + } + + return ret; + }, + + /** + * Merges form data on a document from previously obtained data. + * + * This is the inverse of getFormData(). The data argument is the same object + * type which is returned by getFormData(): an object containing the keys + * "id" and "xpath" which are each objects mapping element identifiers to + * form values. + * + * Where the document has existing form data for an element, the value + * will be replaced. Where the document has a form element but no matching + * data in the passed object, the element is untouched. + * + * @param aDocument + * DOMDocument instance to which to restore form data. + * @param aData + * Object defining form data. + */ + mergeFormData: function(aDocument, aData) { + if ("xpath" in aData) { + for each (let [xpath, value] in Iterator(aData.xpath)) { + let node = XPathGenerator.resolve(aDocument, xpath); + + if (node) { + this.restoreFormValue(node, value, aDocument); + } + } + } + + if ("id" in aData) { + for each (let [id, value] in Iterator(aData.id)) { + let node = aDocument.getElementById(id); + + if (node) { + this.restoreFormValue(node, value, aDocument); + } + } + } + }, + + /** + * Low-level function to restore a form value to a DOMNode. + * + * If you want a higher-level interface, see mergeFormData(). + * + * When the value is changed, the function will fire the appropriate DOM + * events. + * + * @param aNode + * DOMNode to set form value on. + * @param aValue + * Value to set form element to. + * @param aDocument [optional] + * DOMDocument node belongs to. If not defined, node.ownerDocument + * is used. + */ + restoreFormValue: function(aNode, aValue, aDocument) { + aDocument = aDocument || aNode.ownerDocument; + + let eventType; + + if (typeof aValue == "string" && aNode.type != "file") { + // Don't dispatch an input event if there is no change. + if (aNode.value == aValue) { + return; + } + + aNode.value = aValue; + eventType = "input"; + } else if (typeof aValue == "boolean") { + // Don't dispatch a change event for no change. + if (aNode.checked == aValue) { + return; + } + + aNode.checked = aValue; + eventType = "change"; + } else if (typeof aValue == "number") { + // handle select backwards compatibility, example { "#id" : index } + // We saved the value blindly since selects take more work to determine + // default values. So now we should check to avoid unnecessary events. + if (aNode.selectedIndex == aValue) { + return; + } + + if (aValue < aNode.options.length) { + aNode.selectedIndex = aValue; + eventType = "change"; + } + } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) { + // handle select new format + + // Don't dispatch a change event for no change + if (aNode.options[aNode.selectedIndex].value == aValue.value) { + return; + } + + // find first option with matching aValue if possible + for (let i = 0; i < aNode.options.length; i++) { + if (aNode.options[i].value == aValue.value) { + aNode.selectedIndex = i; + break; + } + } + eventType = "change"; + } else if (aValue && aValue.fileList && aValue.type == "file" && + aNode.type == "file") { + aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length); + eventType = "input"; + } else if (aValue && typeof aValue.indexOf == "function" && aNode.options) { + Array.forEach(aNode.options, function(opt, index) { + // don't worry about malformed options with same values + opt.selected = aValue.indexOf(opt.value) > -1; + + // Only fire the event here if this wasn't selected by default + if (!opt.defaultSelected) { + eventType = "change"; + } + }); + } + + // Fire events for this node if applicable + if (eventType) { + let event = aDocument.createEvent("UIEvents"); + event.initUIEvent(eventType, true, true, aDocument.defaultView, 0); + aNode.dispatchEvent(event); + } + } +}; diff --git a/browser/components/sessionstore/SessionStorage.jsm b/browser/components/sessionstore/SessionStorage.jsm new file mode 100644 index 000000000..8016d34bc --- /dev/null +++ b/browser/components/sessionstore/SessionStorage.jsm @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this file, +* You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["SessionStorage"]; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", + "resource:///modules/sessionstore/SessionStore.jsm"); + +this.SessionStorage = { + /** + * Updates all sessionStorage "super cookies" + * @param aDocShell + * That tab's docshell (containing the sessionStorage) + * @param aFullData + * always return privacy sensitive data (use with care) + */ + serialize: function(aDocShell, aFullData) { + return DomStorage.read(aDocShell, aFullData); + }, + + /** + * Restores all sessionStorage "super cookies". + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + * @param aStorageData + * Storage data to be restored + */ + deserialize: function(aDocShell, aStorageData) { + DomStorage.write(aDocShell, aStorageData); + } +}; + +Object.freeze(SessionStorage); + +var DomStorage = { + /** + * Reads all session storage data from the given docShell. + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + * @param aFullData + * Always return privacy sensitive data (use with care) + */ + read: function(aDocShell, aFullData) { + let data = {}; + let isPinned = aDocShell.isAppTab; + let shistory = aDocShell.sessionHistory; + + for (let i = 0; i < shistory.count; i++) { + let principal = History.getPrincipalForEntry(shistory, i, aDocShell); + if (!principal) + continue; + + // Check if we're allowed to store sessionStorage data. + let isHTTPS = principal.URI && principal.URI.schemeIs("https"); + if (aFullData || SessionStore.checkPrivacyLevel(isHTTPS, isPinned)) { + let origin = principal.extendedOrigin; + + // Don't read a host twice. + if (!(origin in data)) { + let originData = this._readEntry(principal, aDocShell); + if (Object.keys(originData).length) { + data[origin] = originData; + } + } + } + } + + return data; + }, + + /** + * Writes session storage data to the given tab. + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + * @param aStorageData + * Storage data to be restored + */ + write: function(aDocShell, aStorageData) { + for (let [host, data] in Iterator(aStorageData)) { + let uri = Services.io.newURI(host, null, null); + let principal = Services.scriptSecurityManager.getDocShellCodebasePrincipal(uri, aDocShell); + let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager); + let window = aDocShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindow); + + // There is no need to pass documentURI, it's only used to fill documentURI property of + // domstorage event, which in this case has no consumer. Prevention of events in case + // of missing documentURI will be solved in a followup bug to bug 600307. + try { + let storage = storageManager.createStorage(window, principal, "", aDocShell.usePrivateBrowsing); + } catch(e) { + Cu.reportError(e); + } + + for (let [key, value] in Iterator(data)) { + try { + storage.setItem(key, value); + } catch (e) { + // throws e.g. for URIs that can't have sessionStorage + Cu.reportError(e); + } + } + } + }, + + /** + * Reads an entry in the session storage data contained in a tab's history. + * @param aURI + * That history entry uri + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + */ + _readEntry: function(aPrincipal, aDocShell) { + let hostData = {}; + let storage; + + try { + let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager); + storage = storageManager.getStorage(aPrincipal); + } catch (e) { + // sessionStorage might throw if it's turned off, see bug 458954 + } + + if (storage && storage.length) { + for (let i = 0; i < storage.length; i++) { + try { + let key = storage.key(i); + hostData[key] = storage.getItem(key); + } catch (e) { + // This currently throws for secured items (cf. bug 442048). + } + } + } + + return hostData; + } +}; + +var History = { + /** + * Returns a given history entry's URI. + * @param aHistory + * That tab's session history + * @param aIndex + * The history entry's index + * @param aDocShell + * That tab's docshell + */ + getPrincipalForEntry: function(aHistory, + aIndex, + aDocShell) { + try { + return Services.scriptSecurityManager.getDocShellCodebasePrincipal( + aHistory.getEntryAtIndex(aIndex, false).URI, aDocShell); + } catch (e) { + // This might throw for some reason. + } + }, +}; diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm new file mode 100644 index 000000000..654f9e879 --- /dev/null +++ b/browser/components/sessionstore/SessionStore.jsm @@ -0,0 +1,4779 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 = ["SessionStore"]; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +const STATE_STOPPED = 0; +const STATE_RUNNING = 1; +const STATE_QUITTING = -1; + +const STATE_STOPPED_STR = "stopped"; +const STATE_RUNNING_STR = "running"; + +const TAB_STATE_NEEDS_RESTORE = 1; +const TAB_STATE_RESTORING = 2; + +const PRIVACY_NONE = 0; +const PRIVACY_ENCRYPTED = 1; +const PRIVACY_FULL = 2; + +const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored"; +const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored"; + +// Default maximum number of tabs to restore simultaneously. Controlled by +// the browser.sessionstore.max_concurrent_tabs pref. +const DEFAULT_MAX_CONCURRENT_TAB_RESTORES = 3; + +// global notifications observed +const OBSERVING = [ + "domwindowopened", "domwindowclosed", + "quit-application-requested", "quit-application-granted", + "browser-lastwindow-close-granted", + "quit-application", "browser:purge-session-history", + "browser:purge-domain-data" +]; + +// XUL Window properties to (re)store +// Restored in restoreDimensions() +const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"]; + +// Hideable window features to (re)store +// Restored in restoreWindowFeatures() +const WINDOW_HIDEABLE_FEATURES = [ + "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars" +]; + +const MESSAGES = [ + // The content script tells us that its form data (or that of one of its + // subframes) might have changed. This can be the contents or values of + // standard form fields or of ContentEditables. + "SessionStore:input", + + // The content script has received a pageshow event. This happens when a + // page is loaded from bfcache without any network activity, i.e. when + // clicking the back or forward button. + "SessionStore:pageshow" +]; + +// These are tab events that we listen to. +const TAB_EVENTS = [ + "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned", + "TabUnpinned" +]; + +#ifndef XP_WIN +#define BROKEN_WM_Z_ORDER +#endif + +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +// debug.js adds NS_ASSERT. cf. bug 669196 +Cu.import("resource://gre/modules/debug.js", this); +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); + +XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup", + "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup"); +XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", + "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"); + +// List of docShell capabilities to (re)store. These are automatically +// retrieved from a given docShell if not already collected before. +// This is made so they're automatically in sync with all nsIDocShell.allow* +// properties. +var gDocShellCapabilities = (function() { + let caps; + + return docShell => { + if (!caps) { + let keys = Object.keys(docShell); + caps = keys.filter(k => k.startsWith("allow")).map(k => k.slice(5)); + } + + return caps; + }; +})(); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +#ifdef MOZ_DEVTOOLS +XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager", + "resource://devtools/client/scratchpad/scratchpad-manager.jsm"); + +Object.defineProperty(this, "HUDService", { + get: function() { + let devtools = Cu.import("resource://devtools/shared/Loader.jsm", {}).devtools; + return devtools.require("devtools/client/webconsole/hudservice").HUDService; + }, + configurable: true, + enumerable: true +}); +#endif + +XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils", + "resource:///modules/sessionstore/DocumentUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", + "resource:///modules/sessionstore/SessionStorage.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile", + "resource:///modules/sessionstore/_SessionFile.jsm"); + +function debug(aMsg) { + aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); + Services.console.logStringMessage(aMsg); +} + +this.SessionStore = { + get promiseInitialized() { + return SessionStoreInternal.promiseInitialized.promise; + }, + + get canRestoreLastSession() { + return SessionStoreInternal.canRestoreLastSession; + }, + + set canRestoreLastSession(val) { + SessionStoreInternal.canRestoreLastSession = val; + }, + + init: function(aWindow) { + return SessionStoreInternal.init(aWindow); + }, + + getBrowserState: function() { + return SessionStoreInternal.getBrowserState(); + }, + + setBrowserState: function(aState) { + SessionStoreInternal.setBrowserState(aState); + }, + + getWindowState: function(aWindow) { + return SessionStoreInternal.getWindowState(aWindow); + }, + + setWindowState: function(aWindow, aState, aOverwrite) { + SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite); + }, + + getTabState: function(aTab) { + return SessionStoreInternal.getTabState(aTab); + }, + + setTabState: function(aTab, aState) { + SessionStoreInternal.setTabState(aTab, aState); + }, + + duplicateTab: function(aWindow, aTab, aDelta) { + return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta); + }, + + getClosedTabCount: function(aWindow) { + return SessionStoreInternal.getClosedTabCount(aWindow); + }, + + getClosedTabData: function(aWindow) { + return SessionStoreInternal.getClosedTabData(aWindow); + }, + + undoCloseTab: function(aWindow, aIndex) { + return SessionStoreInternal.undoCloseTab(aWindow, aIndex); + }, + + forgetClosedTab: function(aWindow, aIndex) { + return SessionStoreInternal.forgetClosedTab(aWindow, aIndex); + }, + + getClosedWindowCount: function() { + return SessionStoreInternal.getClosedWindowCount(); + }, + + getClosedWindowData: function() { + return SessionStoreInternal.getClosedWindowData(); + }, + + undoCloseWindow: function(aIndex) { + return SessionStoreInternal.undoCloseWindow(aIndex); + }, + + forgetClosedWindow: function(aIndex) { + return SessionStoreInternal.forgetClosedWindow(aIndex); + }, + + getWindowValue: function(aWindow, aKey) { + return SessionStoreInternal.getWindowValue(aWindow, aKey); + }, + + setWindowValue: function(aWindow, aKey, aStringValue) { + SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue); + }, + + deleteWindowValue: function(aWindow, aKey) { + SessionStoreInternal.deleteWindowValue(aWindow, aKey); + }, + + getTabValue: function(aTab, aKey) { + return SessionStoreInternal.getTabValue(aTab, aKey); + }, + + setTabValue: function(aTab, aKey, aStringValue) { + SessionStoreInternal.setTabValue(aTab, aKey, aStringValue); + }, + + deleteTabValue: function(aTab, aKey) { + SessionStoreInternal.deleteTabValue(aTab, aKey); + }, + + persistTabAttribute: function(aName) { + SessionStoreInternal.persistTabAttribute(aName); + }, + + restoreLastSession: function() { + SessionStoreInternal.restoreLastSession(); + }, + + checkPrivacyLevel: function(aIsHTTPS, aUseDefaultPref) { + return SessionStoreInternal.checkPrivacyLevel(aIsHTTPS, aUseDefaultPref); + } +}; + +// Freeze the SessionStore object. We don't want anyone to modify it. +Object.freeze(SessionStore); + +var SessionStoreInternal = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIDOMEventListener, + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]), + + // set default load state + _loadState: STATE_STOPPED, + + // During the initial restore and setBrowserState calls tracks the number of + // windows yet to be restored + _restoreCount: -1, + + // whether a setBrowserState call is in progress + _browserSetState: false, + + // time in milliseconds (Date.now()) when the session was last written to file + _lastSaveTime: 0, + + // time in milliseconds when the session was started (saved across sessions), + // defaults to now if no session was restored or timestamp doesn't exist + _sessionStartTime: Date.now(), + + // states for all currently opened windows + _windows: {}, + + // internal states for all open windows (data we need to associate, + // but not write to disk) + _internalWindows: {}, + + // states for all recently closed windows + _closedWindows: [], + + // not-"dirty" windows usually don't need to have their data updated + _dirtyWindows: {}, + + // collection of session states yet to be restored + _statesToRestore: {}, + + // counts the number of crashes since the last clean start + _recentCrashes: 0, + + // whether the last window was closed and should be restored + _restoreLastWindow: false, + + // number of tabs currently restoring + _tabsRestoringCount: 0, + + // max number of tabs to restore concurrently + _maxConcurrentTabRestores: DEFAULT_MAX_CONCURRENT_TAB_RESTORES, + + // whether restored tabs load cached versions or force a reload + _cacheBehavior: 0, + + // The state from the previous session (after restoring pinned tabs). This + // state is persisted and passed through to the next session during an app + // restart to make the third party add-on warning not trash the deferred + // session + _lastSessionState: null, + + // When starting Firefox with a single private window, this is the place + // where we keep the session we actually wanted to restore in case the user + // decides to later open a non-private window as well. + _deferredInitialState: null, + + // A promise resolved once initialization is complete + _promiseInitialization: Promise.defer(), + + // Whether session has been initialized + _sessionInitialized: false, + + // True if session store is disabled by multi-process browsing. + // See bug 516755. + _disabledForMultiProcess: false, + + // The original "sessionstore.resume_session_once" preference value before it + // was modified by saveState. saveState will set the + // "sessionstore.resume_session_once" to true when the + // the "sessionstore.resume_from_crash" preference is false (crash recovery + // is disabled) so that pinned tabs will be restored in the case of a + // crash. This variable is used to restore the original value so the + // previous session is not always restored when + // "sessionstore.resume_from_crash" is true. + _resume_session_once_on_shutdown: null, + + /** + * A promise fulfilled once initialization is complete. + */ + get promiseInitialized() { + return this._promiseInitialization; + }, + + /* ........ Public Getters .............. */ + get canRestoreLastSession() { + return this._lastSessionState; + }, + + set canRestoreLastSession(val) { + this._lastSessionState = null; + }, + + /* ........ Global Event Handlers .............. */ + + /** + * Initialize the component + */ + initService: function() { + if (this._sessionInitialized) { + return; + } + OBSERVING.forEach(function(aTopic) { + Services.obs.addObserver(this, aTopic, true); + }, this); + + this._initPrefs(); + + this._disabledForMultiProcess = false; + + // this pref is only read at startup, so no need to observe it + this._sessionhistory_max_entries = + this._prefBranch.getIntPref("sessionhistory.max_entries"); + + gSessionStartup.onceInitialized.then( + this.initSession.bind(this) + ); + }, + + initSession: function() { + let ss = gSessionStartup; + try { + if (ss.doRestore() || + ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) + this._initialState = ss.state; + } + catch(ex) { dump(ex + "\n"); } // no state to restore, which is ok + + if (this._initialState) { + try { + // If we're doing a DEFERRED session, then we want to pull pinned tabs + // out so they can be restored. + if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) { + let [iniState, remainingState] = this._prepDataForDeferredRestore(this._initialState); + // If we have a iniState with windows, that means that we have windows + // with app tabs to restore. + if (iniState.windows.length) + this._initialState = iniState; + else + this._initialState = null; + if (remainingState.windows.length) + this._lastSessionState = remainingState; + } + else { + // Get the last deferred session in case the user still wants to + // restore it + this._lastSessionState = this._initialState.lastSessionState; + + let lastSessionCrashed = + this._initialState.session && this._initialState.session.state && + this._initialState.session.state == STATE_RUNNING_STR; + if (lastSessionCrashed) { + this._recentCrashes = (this._initialState.session && + this._initialState.session.recentCrashes || 0) + 1; + + if (this._needsRestorePage(this._initialState, this._recentCrashes)) { + // replace the crashed session with a restore-page-only session + let pageData = { + url: "about:sessionrestore", + formdata: { + id: { "sessionData": this._initialState }, + xpath: {} + } + }; + this._initialState = { windows: [{ tabs: [{ entries: [pageData] }] }] }; + } + } + + // Load the session start time from the previous state + this._sessionStartTime = this._initialState.session && + this._initialState.session.startTime || + this._sessionStartTime; + + // make sure that at least the first window doesn't have anything hidden + delete this._initialState.windows[0].hidden; + // Since nothing is hidden in the first window, it cannot be a popup + delete this._initialState.windows[0].isPopup; + // We don't want to minimize and then open a window at startup. + if (this._initialState.windows[0].sizemode == "minimized") + this._initialState.windows[0].sizemode = "normal"; + // clear any lastSessionWindowID attributes since those don't matter + // during normal restore + this._initialState.windows.forEach(function(aWindow) { + delete aWindow.__lastSessionWindowID; + }); + } + } + catch (ex) { debug("The session file is invalid: " + ex); } + } + + // A Lazy getter for the sessionstore.js backup promise. + XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function() { + return _SessionFile.createBackupCopy(); + }); + + // at this point, we've as good as resumed the session, so we can + // clear the resume_session_once flag, if it's set + if (this._loadState != STATE_QUITTING && + this._prefBranch.getBoolPref("sessionstore.resume_session_once")) + this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); + + this._initEncoding(); + + // Session is ready. + this._sessionInitialized = true; + this._promiseInitialization.resolve(); + }, + + _initEncoding : function() { + // The (UTF-8) encoder used to write to files. + XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function() { + return new TextEncoder(); + }); + }, + + _initPrefs : function() { + XPCOMUtils.defineLazyGetter(this, "_prefBranch", function() { + return Services.prefs.getBranch("browser."); + }); + + // minimal interval between two save operations (in milliseconds) + XPCOMUtils.defineLazyGetter(this, "_interval", function() { + // used often, so caching/observing instead of fetching on-demand + this._prefBranch.addObserver("sessionstore.interval", this, true); + return this._prefBranch.getIntPref("sessionstore.interval"); + }); + + // when crash recovery is disabled, session data is not written to disk + XPCOMUtils.defineLazyGetter(this, "_resume_from_crash", function() { + // get crash recovery state from prefs and allow for proper reaction to state changes + this._prefBranch.addObserver("sessionstore.resume_from_crash", this, true); + return this._prefBranch.getBoolPref("sessionstore.resume_from_crash"); + }); + + this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); + this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true); + + this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); + this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true); + + // Straight-up collect the following one-time prefs + this._maxConcurrentTabRestores = + Services.prefs.getIntPref("browser.sessionstore.max_concurrent_tabs"); + // ensure a sane value for concurrency, ignore and set default otherwise + if (this._maxConcurrentTabRestores < 1 || this._maxConcurrentTabRestores > 10) { + this._maxConcurrentTabRestores = DEFAULT_MAX_CONCURRENT_TAB_RESTORES; + } + this._cacheBehavior = + Services.prefs.getIntPref("browser.sessionstore.cache_behavior"); + + }, + + _initWindow: function(aWindow) { + if (aWindow) { + this.onLoad(aWindow); + } else if (this._loadState == STATE_STOPPED) { + // If init is being called with a null window, it's possible that we + // just want to tell sessionstore that a session is live (as is the case + // with starting Firefox with -private, for example; see bug 568816), + // so we should mark the load state as running to make sure that + // things like setBrowserState calls will succeed in restoring the session. + this._loadState = STATE_RUNNING; + } + }, + + /** + * Start tracking a window. + * + * This function also initializes the component if it is not + * initialized yet. + */ + init: function(aWindow) { + let self = this; + this.initService(); + return this._promiseInitialization.promise.then( + function onSuccess() { + self._initWindow(aWindow); + } + ); + }, + + /** + * Called on application shutdown, after notifications: + * quit-application-granted, quit-application + */ + _uninit: function() { + // save all data for session resuming + if (this._sessionInitialized) + this.saveState(true); + + // clear out priority queue in case it's still holding refs + TabRestoreQueue.reset(); + + // Make sure to break our cycle with the save timer + if (this._saveTimer) { + this._saveTimer.cancel(); + this._saveTimer = null; + } + }, + + /** + * Handle notifications + */ + observe: function(aSubject, aTopic, aData) { + if (this._disabledForMultiProcess) + return; + + switch (aTopic) { + case "domwindowopened": // catch new windows + this.onOpen(aSubject); + break; + case "domwindowclosed": // catch closed windows + this.onClose(aSubject); + break; + case "quit-application-requested": + this.onQuitApplicationRequested(); + break; + case "quit-application-granted": + this.onQuitApplicationGranted(); + break; + case "browser-lastwindow-close-granted": + this.onLastWindowCloseGranted(); + break; + case "quit-application": + this.onQuitApplication(aData); + break; + case "browser:purge-session-history": // catch sanitization + this.onPurgeSessionHistory(); + break; + case "browser:purge-domain-data": + this.onPurgeDomainData(aData); + break; + case "nsPref:changed": // catch pref changes + this.onPrefChange(aData); + break; + case "timer-callback": // timer call back for delayed saving + this.onTimerCallback(); + break; + } + }, + + /** + * This method handles incoming messages sent by the session store content + * script and thus enables communication with OOP tabs. + */ + receiveMessage: function(aMessage) { + var browser = aMessage.target; + var win = browser.ownerDocument.defaultView; + + switch (aMessage.name) { + case "SessionStore:pageshow": + this.onTabLoad(win, browser); + break; + case "SessionStore:input": + this.onTabInput(win, browser); + break; + default: + debug("received unknown message '" + aMessage.name + "'"); + break; + } + + this._clearRestoringWindows(); + }, + + /* ........ Window Event Handlers .............. */ + + /** + * Implement nsIDOMEventListener for handling various window and tab events + */ + handleEvent: function(aEvent) { + if (this._disabledForMultiProcess) + return; + + var win = aEvent.currentTarget.ownerDocument.defaultView; + switch (aEvent.type) { + case "load": + // If __SS_restore_data is set, then we need to restore the document + // (form data, scrolling, etc.). This will only happen when a tab is + // first restored. + let browser = aEvent.currentTarget; + if (browser.__SS_restore_data) + this.restoreDocument(win, browser, aEvent); + this.onTabLoad(win, browser); + break; + case "TabOpen": + this.onTabAdd(win, aEvent.originalTarget); + break; + case "TabClose": + // aEvent.detail determines if the tab was closed by moving to a different window + if (!aEvent.detail) + this.onTabClose(win, aEvent.originalTarget); + this.onTabRemove(win, aEvent.originalTarget); + break; + case "TabSelect": + this.onTabSelect(win); + break; + case "TabShow": + this.onTabShow(win, aEvent.originalTarget); + break; + case "TabHide": + this.onTabHide(win, aEvent.originalTarget); + break; + case "TabPinned": + case "TabUnpinned": + this.saveStateDelayed(win); + break; + } + + this._clearRestoringWindows(); + }, + + /** + * If it's the first window load since app start... + * - determine if we're reloading after a crash or a forced-restart + * - restore window state + * - restart downloads + * Set up event listeners for this window's tabs + * @param aWindow + * Window reference + */ + onLoad: function(aWindow) { + // return if window has already been initialized + if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi]) + 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) + aWindow.__SSi = "window" + Date.now(); + + // and create its data object + this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false }; + + // and create its internal data object + this._internalWindows[aWindow.__SSi] = { hosts: {} } + + let isPrivateWindow = false; + if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) + this._windows[aWindow.__SSi].isPrivate = isPrivateWindow = true; + if (!this._isWindowLoaded(aWindow)) + this._windows[aWindow.__SSi]._restoring = true; + if (!aWindow.toolbar.visible) + this._windows[aWindow.__SSi].isPopup = true; + + // perform additional initialization when the first window is loading + if (this._loadState == STATE_STOPPED) { + this._loadState = STATE_RUNNING; + this._lastSaveTime = Date.now(); + + // restore a crashed session resp. resume the last session if requested + if (this._initialState) { + if (isPrivateWindow) { + // We're starting with a single private window. Save the state we + // actually wanted to restore so that we can do it later in case + // the user opens another, non-private window. + this._deferredInitialState = gSessionStartup.state; + delete this._initialState; + + // Nothing to restore now, notify observers things are complete. + Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); + } else { + // make sure that the restored tabs are first in the window + this._initialState._firstTabs = true; + this._restoreCount = this._initialState.windows ? this._initialState.windows.length : 0; + this.restoreWindow(aWindow, this._initialState, + this._isCmdLineEmpty(aWindow, this._initialState)); + delete this._initialState; + + // _loadState changed from "stopped" to "running" + // force a save operation so that crashes happening during startup are correctly counted + this.saveState(true); + } + } + else { + // Nothing to restore, notify observers things are complete. + Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); + + // the next delayed save request should execute immediately + this._lastSaveTime -= this._interval; + } + } + // this window was opened by _openWindowWithState + else if (!this._isWindowLoaded(aWindow)) { + let followUp = this._statesToRestore[aWindow.__SS_restoreID].windows.length == 1; + this.restoreWindow(aWindow, this._statesToRestore[aWindow.__SS_restoreID], true, followUp); + } + // The user opened another, non-private window after starting up with + // a single private one. Let's restore the session we actually wanted to + // restore at startup. + else if (this._deferredInitialState && !isPrivateWindow && + aWindow.toolbar.visible) { + + this._deferredInitialState._firstTabs = true; + this._restoreCount = this._deferredInitialState.windows ? + this._deferredInitialState.windows.length : 0; + this.restoreWindow(aWindow, this._deferredInitialState, false); + this._deferredInitialState = null; + } + else if (this._restoreLastWindow && aWindow.toolbar.visible && + this._closedWindows.length && !isPrivateWindow) { + + // default to the most-recently closed window + // don't use popup windows + let closedWindowState = null; + let closedWindowIndex; + for (let i = 0; i < this._closedWindows.length; i++) { + // Take the first non-popup, point our object at it, and break out. + if (!this._closedWindows[i].isPopup) { + closedWindowState = this._closedWindows[i]; + closedWindowIndex = i; + break; + } + } + + if (closedWindowState) { + let newWindowState; + if (!this._doResumeSession()) { + // We want to split the window up into pinned tabs and unpinned tabs. + // Pinned tabs should be restored. If there are any remaining tabs, + // they should be added back to _closedWindows. + // We'll cheat a little bit and reuse _prepDataForDeferredRestore + // even though it wasn't built exactly for this. + let [appTabsState, normalTabsState] = + this._prepDataForDeferredRestore({ windows: [closedWindowState] }); + + // These are our pinned tabs, which we should restore + if (appTabsState.windows.length) { + newWindowState = appTabsState.windows[0]; + delete newWindowState.__lastSessionWindowID; + } + + // In case there were no unpinned tabs, remove the window from _closedWindows + if (!normalTabsState.windows.length) { + this._closedWindows.splice(closedWindowIndex, 1); + } + // Or update _closedWindows with the modified state + else { + delete normalTabsState.windows[0].__lastSessionWindowID; + this._closedWindows[closedWindowIndex] = normalTabsState.windows[0]; + } + } + else { + // If we're just restoring the window, make sure it gets removed from + // _closedWindows. + this._closedWindows.splice(closedWindowIndex, 1); + newWindowState = closedWindowState; + delete newWindowState.hidden; + } + if (newWindowState) { + // Ensure that the window state isn't hidden + this._restoreCount = 1; + let state = { windows: [newWindowState] }; + this.restoreWindow(aWindow, state, this._isCmdLineEmpty(aWindow, state)); + } + } + // we actually restored the session just now. + this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); + } + if (this._restoreLastWindow && aWindow.toolbar.visible) { + // always reset (if not a popup window) + // we don't want to restore a window directly after, for example, + // undoCloseWindow was executed. + this._restoreLastWindow = false; + } + + var tabbrowser = aWindow.gBrowser; + + // add tab change listeners to all already existing tabs + for (let i = 0; i < tabbrowser.tabs.length; i++) { + this.onTabAdd(aWindow, tabbrowser.tabs[i], true); + } + // notification of tab add/remove/selection/show/hide + TAB_EVENTS.forEach(function(aEvent) { + tabbrowser.tabContainer.addEventListener(aEvent, this, true); + }, this); + }, + + /** + * On window open + * @param aWindow + * Window reference + */ + onOpen: function(aWindow) { + var _this = this; + aWindow.addEventListener("load", function(aEvent) { + aEvent.currentTarget.removeEventListener("load", arguments.callee, false); + _this.onLoad(aEvent.currentTarget); + }, false); + return; + }, + + /** + * On window close... + * - remove event listeners from tabs + * - save all window data + * @param aWindow + * Window reference + */ + onClose: function(aWindow) { + // this window was about to be restored - conserve its original data, if any + let isFullyLoaded = this._isWindowLoaded(aWindow); + if (!isFullyLoaded) { + if (!aWindow.__SSi) + aWindow.__SSi = "window" + Date.now(); + this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID]; + delete this._statesToRestore[aWindow.__SS_restoreID]; + delete aWindow.__SS_restoreID; + } + + // ignore windows not tracked by SessionStore + if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) { + return; + } + + // notify that the session store will stop tracking this window so that + // extensions can store any data about this window in session store before + // that's not possible anymore + let event = aWindow.document.createEvent("Events"); + event.initEvent("SSWindowClosing", true, false); + aWindow.dispatchEvent(event); + + if (this.windowToFocus && this.windowToFocus == aWindow) { + delete this.windowToFocus; + } + + var tabbrowser = aWindow.gBrowser; + + TAB_EVENTS.forEach(function(aEvent) { + tabbrowser.tabContainer.removeEventListener(aEvent, this, true); + }, this); + + // remove the progress listener for this window + tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener); + + let winData = this._windows[aWindow.__SSi]; + if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down + // update all window data for a last time + this._collectWindowData(aWindow); + + if (isFullyLoaded) { + winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label; + winData.title = this._replaceLoadingTitle(winData.title, tabbrowser, + tabbrowser.selectedTab); + let windows = {}; + windows[aWindow.__SSi] = winData; + this._updateCookies(windows); + } + + // Until we decide otherwise elsewhere, this window is part of a series + // of closing windows to quit. + winData._shouldRestore = true; + + // Save the window if it has multiple tabs or a single saveable tab and + // it's not private. + if (!winData.isPrivate && (winData.tabs.length > 1 || + (winData.tabs.length == 1 && this._shouldSaveTabState(winData.tabs[0])))) { + // we don't want to save the busy state + delete winData.busy; + + this._closedWindows.unshift(winData); + this._capClosedWindows(); + } + + // clear this window from the list + delete this._windows[aWindow.__SSi]; + delete this._internalWindows[aWindow.__SSi]; + + // save the state without this window to disk + this.saveStateDelayed(); + } + + for (let i = 0; i < tabbrowser.tabs.length; i++) { + this.onTabRemove(aWindow, tabbrowser.tabs[i], true); + } + + // Cache the window state until it is completely gone. + DyingWindowCache.set(aWindow, winData); + + delete aWindow.__SSi; + }, + + /** + * On quit application requested + */ + onQuitApplicationRequested: function() { + // get a current snapshot of all windows + this._forEachBrowserWindow(function(aWindow) { + this._collectWindowData(aWindow); + }); + // we must cache this because _getMostRecentBrowserWindow will always + // return null by the time quit-application occurs + var activeWindow = this._getMostRecentBrowserWindow(); + if (activeWindow) + this.activeWindowSSiCache = activeWindow.__SSi || ""; + this._dirtyWindows = []; + }, + + /** + * On quit application granted + */ + onQuitApplicationGranted: function() { + // freeze the data at what we've got (ignoring closing windows) + this._loadState = STATE_QUITTING; + }, + + /** + * On last browser window close + */ + onLastWindowCloseGranted: function() { + // last browser window is quitting. + // remember to restore the last window when another browser window is opened + // do not account for pref(resume_session_once) at this point, as it might be + // set by another observer getting this notice after us + this._restoreLastWindow = true; + }, + + /** + * On quitting application + * @param aData + * String type of quitting + */ + onQuitApplication: function(aData) { + if (aData == "restart") { + this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); + // The browser:purge-session-history notification fires after the + // quit-application notification so unregister the + // browser:purge-session-history notification to prevent clearing + // session data on disk on a restart. It is also unnecessary to + // perform any other sanitization processing on a restart as the + // browser is about to exit anyway. + Services.obs.removeObserver(this, "browser:purge-session-history"); + } + else if (this._resume_session_once_on_shutdown != null) { + // if the sessionstore.resume_session_once preference was changed by + // saveState because crash recovery is disabled then restore the + // preference back to the value it was prior to that. This will prevent + // SessionStore from always restoring the session when crash recovery is + // disabled. + this._prefBranch.setBoolPref("sessionstore.resume_session_once", + this._resume_session_once_on_shutdown); + } + + if (aData != "restart") { + // Throw away the previous session on shutdown + this._lastSessionState = null; + } + + this._loadState = STATE_QUITTING; // just to be sure + this._uninit(); + }, + + /** + * On purge of session history + */ + onPurgeSessionHistory: function() { + var _this = this; + _SessionFile.wipe(); + // 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; + this._lastSessionState = null; + let openWindows = {}; + this._forEachBrowserWindow(function(aWindow) { + Array.forEach(aWindow.gBrowser.tabs, function(aTab) { + delete aTab.linkedBrowser.__SS_data; + delete aTab.linkedBrowser.__SS_tabStillLoading; + delete aTab.linkedBrowser.__SS_formDataSaved; + delete aTab.linkedBrowser.__SS_hostSchemeData; + if (aTab.linkedBrowser.__SS_restoreState) + this._resetTabRestoringState(aTab); + }, this); + openWindows[aWindow.__SSi] = true; + }); + // also clear all data about closed tabs and windows + for (let ix in this._windows) { + if (ix in openWindows) { + this._windows[ix]._closedTabs = []; + } + else { + delete this._windows[ix]; + delete this._internalWindows[ix]; + } + } + // also clear all data about closed windows + this._closedWindows = []; + // give the tabbrowsers a chance to clear their histories first + var win = this._getMostRecentBrowserWindow(); + if (win) + win.setTimeout(function() { _this.saveState(true); }, 0); + else if (this._loadState == STATE_RUNNING) + this.saveState(true); + // Delete the private browsing backed up state, if any + if ("_stateBackup" in this) + delete this._stateBackup; + + this._clearRestoringWindows(); + }, + + /** + * On purge of domain data + * @param aData + * String domain data + */ + onPurgeDomainData: function(aData) { + // does a session history entry contain a url for the given domain? + function containsDomain(aEntry) { + try { + if (this._getURIFromString(aEntry.url).host.hasRootDomain(aData)) + return true; + } + catch (ex) { /* url had no host at all */ } + return aEntry.children && aEntry.children.some(containsDomain, this); + } + // remove all closed tabs containing a reference to the given domain + for (let ix in this._windows) { + let closedTabs = this._windows[ix]._closedTabs; + for (let i = closedTabs.length - 1; i >= 0; i--) { + if (closedTabs[i].state.entries.some(containsDomain, this)) + closedTabs.splice(i, 1); + } + } + // remove all open & closed tabs containing a reference to the given + // domain in closed windows + for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) { + let closedTabs = this._closedWindows[ix]._closedTabs; + let openTabs = this._closedWindows[ix].tabs; + let openTabCount = openTabs.length; + for (let i = closedTabs.length - 1; i >= 0; i--) + if (closedTabs[i].state.entries.some(containsDomain, this)) + closedTabs.splice(i, 1); + for (let j = openTabs.length - 1; j >= 0; j--) { + if (openTabs[j].entries.some(containsDomain, this)) { + openTabs.splice(j, 1); + if (this._closedWindows[ix].selected > j) + this._closedWindows[ix].selected--; + } + } + if (openTabs.length == 0) { + this._closedWindows.splice(ix, 1); + } + else if (openTabs.length != openTabCount) { + // Adjust the window's title if we removed an open tab + let selectedTab = openTabs[this._closedWindows[ix].selected - 1]; + // some duplication from restoreHistory - make sure we get the correct title + let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; + if (activeIndex >= selectedTab.entries.length) + activeIndex = selectedTab.entries.length - 1; + this._closedWindows[ix].title = selectedTab.entries[activeIndex].title; + } + } + if (this._loadState == STATE_RUNNING) + this.saveState(true); + + this._clearRestoringWindows(); + }, + + /** + * On preference change + * @param aData + * String preference changed + */ + onPrefChange: function(aData) { + switch (aData) { + // if the user decreases the max number of closed tabs they want + // preserved update our internal states to match that max + case "sessionstore.max_tabs_undo": + this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); + for (let ix in this._windows) { + this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length); + } + break; + case "sessionstore.max_windows_undo": + this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); + this._capClosedWindows(); + break; + case "sessionstore.interval": + this._interval = this._prefBranch.getIntPref("sessionstore.interval"); + // reset timer and save + if (this._saveTimer) { + this._saveTimer.cancel(); + this._saveTimer = null; + } + this.saveStateDelayed(null, -1); + break; + case "sessionstore.resume_from_crash": + this._resume_from_crash = this._prefBranch.getBoolPref("sessionstore.resume_from_crash"); + // restore original resume_session_once preference if set in saveState + if (this._resume_session_once_on_shutdown != null) { + this._prefBranch.setBoolPref("sessionstore.resume_session_once", + this._resume_session_once_on_shutdown); + this._resume_session_once_on_shutdown = null; + } + // either create the file with crash recovery information or remove it + // (when _loadState is not STATE_RUNNING, that file is used for session resuming instead) + if (!this._resume_from_crash) + _SessionFile.wipe(); + this.saveState(true); + break; + } + }, + + /** + * On timer callback + */ + onTimerCallback: function() { + this._saveTimer = null; + this.saveState(); + }, + + /** + * set up listeners for a new tab + * @param aWindow + * Window reference + * @param aTab + * Tab reference + * @param aNoNotification + * bool Do not save state if we're updating an existing tab + */ + onTabAdd: function(aWindow, aTab, aNoNotification) { + let browser = aTab.linkedBrowser; + browser.addEventListener("load", this, true); + + let mm = browser.messageManager; + MESSAGES.forEach(msg => mm.addMessageListener(msg, this)); + + if (!aNoNotification) { + this.saveStateDelayed(aWindow); + } + }, + + /** + * remove listeners for a tab + * @param aWindow + * Window reference + * @param aTab + * Tab reference + * @param aNoNotification + * bool Do not save state if we're updating an existing tab + */ + onTabRemove: function(aWindow, aTab, aNoNotification) { + let browser = aTab.linkedBrowser; + browser.removeEventListener("load", this, true); + + let mm = browser.messageManager; + MESSAGES.forEach(msg => mm.removeMessageListener(msg, this)); + + delete browser.__SS_data; + delete browser.__SS_tabStillLoading; + delete browser.__SS_formDataSaved; + delete browser.__SS_hostSchemeData; + + // If this tab was in the middle of restoring or still needs to be restored, + // we need to reset that state. If the tab was restoring, we will attempt to + // restore the next tab. + let previousState = browser.__SS_restoreState; + if (previousState) { + this._resetTabRestoringState(aTab); + if (previousState == TAB_STATE_RESTORING) + this.restoreNextTab(); + } + + if (!aNoNotification) { + this.saveStateDelayed(aWindow); + } + }, + + /** + * When a tab closes, collect its properties + * @param aWindow + * Window reference + * @param aTab + * Tab reference + */ + onTabClose: function(aWindow, aTab) { + // notify the tabbrowser that the tab state will be retrieved for the last time + // (so that extension authors can easily set data on soon-to-be-closed tabs) + var event = aWindow.document.createEvent("Events"); + event.initEvent("SSTabClosing", true, false); + aTab.dispatchEvent(event); + + // don't update our internal state if we don't have to + if (this._max_tabs_undo == 0) { + return; + } + + // make sure that the tab related data is up-to-date + var tabState = this._collectTabData(aTab); + this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState); + + // store closed-tab data for undo + if (this._shouldSaveTabState(tabState)) { + let tabTitle = aTab.label; + let tabbrowser = aWindow.gBrowser; + tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab); + + this._windows[aWindow.__SSi]._closedTabs.unshift({ + state: tabState, + title: tabTitle, + image: tabbrowser.getIcon(aTab), + pos: aTab._tPos + }); + var length = this._windows[aWindow.__SSi]._closedTabs.length; + if (length > this._max_tabs_undo) + this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo); + } + }, + + /** + * When a tab loads, save state. + * @param aWindow + * Window reference + * @param aBrowser + * Browser reference + */ + onTabLoad: function(aWindow, aBrowser) { + // react on "load" and solitary "pageshow" events (the first "pageshow" + // following "load" is too late for deleting the data caches) + // It's possible to get a load event after calling stop on a browser (when + // overwriting tabs). We want to return early if the tab hasn't been restored yet. + if (aBrowser.__SS_restoreState && + aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + return; + } + + delete aBrowser.__SS_data; + delete aBrowser.__SS_tabStillLoading; + delete aBrowser.__SS_formDataSaved; + this.saveStateDelayed(aWindow); + + }, + + /** + * Called when a browser sends the "input" notification + * @param aWindow + * Window reference + * @param aBrowser + * Browser reference + */ + onTabInput: function(aWindow, aBrowser) { + // deleting __SS_formDataSaved will cause us to recollect form data + delete aBrowser.__SS_formDataSaved; + + this.saveStateDelayed(aWindow, 3000); + }, + + /** + * When a tab is selected, save session data + * @param aWindow + * Window reference + */ + onTabSelect: function(aWindow) { + if (this._loadState == STATE_RUNNING) { + this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex; + + let tab = aWindow.gBrowser.selectedTab; + // If __SS_restoreState is still on the browser and it is + // TAB_STATE_NEEDS_RESTORE, then then we haven't restored + // this tab yet. Explicitly call restoreTab to kick off the restore. + if (tab.linkedBrowser.__SS_restoreState && + tab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) + this.restoreTab(tab); + + } + }, + + onTabShow: function(aWindow, aTab) { + // If the tab hasn't been restored yet, move it into the right bucket + if (aTab.linkedBrowser.__SS_restoreState && + aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + TabRestoreQueue.hiddenToVisible(aTab); + + // let's kick off tab restoration again to ensure this tab gets restored + // with "restore_hidden_tabs" == false (now that it has become visible) + this.restoreNextTab(); + } + + // Default delay of 2 seconds gives enough time to catch multiple TabShow + // events due to changing groups in Panorama. + this.saveStateDelayed(aWindow); + }, + + onTabHide: function(aWindow, aTab) { + // If the tab hasn't been restored yet, move it into the right bucket + if (aTab.linkedBrowser.__SS_restoreState && + aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + TabRestoreQueue.visibleToHidden(aTab); + } + + // Default delay of 2 seconds gives enough time to catch multiple TabHide + // events due to changing groups in Panorama. + this.saveStateDelayed(aWindow); + }, + + /* ........ nsISessionStore API .............. */ + + getBrowserState: function() { + return this._toJSONString(this._getCurrentState()); + }, + + setBrowserState: function(aState) { + this._handleClosedWindows(); + + try { + var state = JSON.parse(aState); + } + catch (ex) { /* invalid state object - don't restore anything */ } + if (!state || !state.windows) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + this._browserSetState = true; + + // Make sure the priority queue is emptied out + this._resetRestoringState(); + + var window = this._getMostRecentBrowserWindow(); + if (!window) { + this._restoreCount = 1; + this._openWindowWithState(state); + return; + } + + // close all other browser windows + this._forEachBrowserWindow(function(aWindow) { + if (aWindow != window) { + aWindow.close(); + this.onClose(aWindow); + } + }); + + // make sure closed window data isn't kept + this._closedWindows = []; + + // determine how many windows are meant to be restored + this._restoreCount = state.windows ? state.windows.length : 0; + + // restore to the given state + this.restoreWindow(window, state, true); + }, + + getWindowState: function(aWindow) { + if ("__SSi" in aWindow) { + return this._toJSONString(this._getWindowState(aWindow)); + } + + if (DyingWindowCache.has(aWindow)) { + let data = DyingWindowCache.get(aWindow); + return this._toJSONString({ windows: [data] }); + } + + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + setWindowState: function(aWindow, aState, aOverwrite) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + this.restoreWindow(aWindow, aState, aOverwrite); + }, + + getTabState: function(aTab) { + if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var tabState = this._collectTabData(aTab); + + var window = aTab.ownerDocument.defaultView; + this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState); + + return this._toJSONString(tabState); + }, + + setTabState: function(aTab, aState) { + var tabState = JSON.parse(aState); + if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var window = aTab.ownerDocument.defaultView; + this._setWindowStateBusy(window); + this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0); + }, + + duplicateTab: function(aWindow, aTab, aDelta) { + if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi || + !aWindow.getBrowser) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var tabState = this._collectTabData(aTab, true); + var sourceWindow = aTab.ownerDocument.defaultView; + this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true); + tabState.index += aDelta; + tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); + tabState.pinned = false; + + this._setWindowStateBusy(aWindow); + let newTab = aTab == aWindow.gBrowser.selectedTab ? + aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) : + aWindow.gBrowser.addTab(); + + this.restoreHistoryPrecursor(aWindow, [newTab], [tabState], 0, 0, 0, + true /* Load this tab right away. */); + + return newTab; + }, + + getClosedTabCount: function(aWindow) { + if ("__SSi" in aWindow) { + return this._windows[aWindow.__SSi]._closedTabs.length; + } + + if (DyingWindowCache.has(aWindow)) { + return DyingWindowCache.get(aWindow)._closedTabs.length; + } + + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + getClosedTabData: function(aWindow) { + if ("__SSi" in aWindow) { + return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs); + } + + if (DyingWindowCache.has(aWindow)) { + let data = DyingWindowCache.get(aWindow); + return this._toJSONString(data._closedTabs); + } + + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + undoCloseTab: function(aWindow, aIndex) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var closedTabs = this._windows[aWindow.__SSi]._closedTabs; + + // 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(); + let closedTabState = closedTab.state; + + this._setWindowStateBusy(aWindow); + // create a new tab + let tabbrowser = aWindow.gBrowser; + let tab = tabbrowser.addTab(); + + // restore tab content + this.restoreHistoryPrecursor(aWindow, [tab], [closedTabState], 1, 0, 0); + + // restore the tab's position + tabbrowser.moveTabTo(tab, closedTab.pos); + + // focus the tab's content area (bug 342432) + tab.linkedBrowser.focus(); + + return tab; + }, + + forgetClosedTab: function(aWindow, aIndex) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var closedTabs = this._windows[aWindow.__SSi]._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); + }, + + getClosedWindowCount: function() { + return this._closedWindows.length; + }, + + getClosedWindowData: function() { + return this._toJSONString(this._closedWindows); + }, + + undoCloseWindow: function(aIndex) { + if (!(aIndex in this._closedWindows)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // reopen the window + let state = { windows: this._closedWindows.splice(aIndex, 1) }; + let window = this._openWindowWithState(state); + this.windowToFocus = window; + return window; + }, + + forgetClosedWindow: function(aIndex) { + // default to the most-recently closed window + aIndex = aIndex || 0; + if (!(aIndex in this._closedWindows)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // remove closed window from the array + this._closedWindows.splice(aIndex, 1); + }, + + getWindowValue: function(aWindow, aKey) { + if ("__SSi" in aWindow) { + var data = this._windows[aWindow.__SSi].extData || {}; + return data[aKey] || ""; + } + + if (DyingWindowCache.has(aWindow)) { + let data = DyingWindowCache.get(aWindow).extData || {}; + return data[aKey] || ""; + } + + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + setWindowValue: function(aWindow, aKey, aStringValue) { + if (aWindow.__SSi) { + if (!this._windows[aWindow.__SSi].extData) { + this._windows[aWindow.__SSi].extData = {}; + } + this._windows[aWindow.__SSi].extData[aKey] = aStringValue; + this.saveStateDelayed(aWindow); + } + else { + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + }, + + deleteWindowValue: function(aWindow, aKey) { + if (aWindow.__SSi && this._windows[aWindow.__SSi].extData && + this._windows[aWindow.__SSi].extData[aKey]) + delete this._windows[aWindow.__SSi].extData[aKey]; + }, + + getTabValue: function(aTab, aKey) { + let data = {}; + if (aTab.__SS_extdata) { + data = aTab.__SS_extdata; + } + else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { + // If the tab hasn't been fully restored, get the data from the to-be-restored data + data = aTab.linkedBrowser.__SS_data.extData; + } + return data[aKey] || ""; + }, + + setTabValue: function(aTab, aKey, aStringValue) { + // If the tab hasn't been restored, then set the data there, otherwise we + // could lose newly added data. + let saveTo; + if (aTab.__SS_extdata) { + saveTo = aTab.__SS_extdata; + } + else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { + saveTo = aTab.linkedBrowser.__SS_data.extData; + } + else { + aTab.__SS_extdata = {}; + saveTo = aTab.__SS_extdata; + } + saveTo[aKey] = aStringValue; + this.saveStateDelayed(aTab.ownerDocument.defaultView); + }, + + deleteTabValue: function(aTab, aKey) { + // We want to make sure that if data is accessed early, we attempt to delete + // that data from __SS_data as well. Otherwise we'll throw in cases where + // data can be set or read. + let deleteFrom; + if (aTab.__SS_extdata) { + deleteFrom = aTab.__SS_extdata; + } + else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { + deleteFrom = aTab.linkedBrowser.__SS_data.extData; + } + + if (deleteFrom && deleteFrom[aKey]) + delete deleteFrom[aKey]; + }, + + persistTabAttribute: function(aName) { + if (TabAttributes.persist(aName)) { + this.saveStateDelayed(); + } + }, + + /** + * Restores the session state stored in _lastSessionState. This will attempt + * to merge data into the current session. If a window was opened at startup + * with pinned tab(s), then the remaining data from the previous session for + * that window will be opened into that winddow. Otherwise new windows will + * be opened. + */ + restoreLastSession: function() { + // Use the public getter since it also checks PB mode + if (!this.canRestoreLastSession) + throw (Components.returnCode = Cr.NS_ERROR_FAILURE); + + // First collect each window with its id... + let windows = {}; + this._forEachBrowserWindow(function(aWindow) { + if (aWindow.__SS_lastSessionWindowID) + windows[aWindow.__SS_lastSessionWindowID] = aWindow; + }); + + let lastSessionState = this._lastSessionState; + + // This shouldn't ever be the case... + if (!lastSessionState.windows.length) + throw (Components.returnCode = Cr.NS_ERROR_UNEXPECTED); + + // We're technically doing a restore, so set things up so we send the + // notification when we're done. We want to send "sessionstore-browser-state-restored". + this._restoreCount = lastSessionState.windows.length; + this._browserSetState = true; + + // We want to re-use the last opened window instead of opening a new one in + // the case where it's "empty" and not associated with a window in the session. + // We will do more processing via _prepWindowToRestoreInto if we need to use + // the lastWindow. + let lastWindow = this._getMostRecentBrowserWindow(); + let canUseLastWindow = lastWindow && + !lastWindow.__SS_lastSessionWindowID; + + // Restore into windows or open new ones as needed. + for (let i = 0; i < lastSessionState.windows.length; i++) { + let winState = lastSessionState.windows[i]; + let lastSessionWindowID = winState.__lastSessionWindowID; + // delete lastSessionWindowID so we don't add that to the window again + delete winState.__lastSessionWindowID; + + // See if we can use an open window. First try one that is associated with + // the state we're trying to restore and then fallback to the last selected + // window. + let windowToUse = windows[lastSessionWindowID]; + if (!windowToUse && canUseLastWindow) { + windowToUse = lastWindow; + canUseLastWindow = false; + } + + let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse); + + // If there's a window already open that we can restore into, use that + if (canUseWindow) { + // Since we're not overwriting existing tabs, we want to merge _closedTabs, + // putting existing ones first. Then make sure we're respecting the max pref. + if (winState._closedTabs && winState._closedTabs.length) { + let curWinState = this._windows[windowToUse.__SSi]; + curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs); + curWinState._closedTabs.splice(this._prefBranch.getIntPref("sessionstore.max_tabs_undo"), curWinState._closedTabs.length); + } + + // Restore into that window - pretend it's a followup since we'll already + // have a focused window. + //XXXzpao This is going to merge extData together (taking what was in + // winState over what is in the window already. The hack we have + // in _preWindowToRestoreInto will prevent most (all?) Panorama + // weirdness but we will still merge other extData. + // Bug 588217 should make this go away by merging the group data. + this.restoreWindow(windowToUse, { windows: [winState] }, canOverwriteTabs, true); + } + else { + this._openWindowWithState({ windows: [winState] }); + } + } + + // Merge closed windows from this session with ones from last session + if (lastSessionState._closedWindows) { + this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows); + this._capClosedWindows(); + } + +#ifdef MOZ_DEVTOOLS + // Scratchpad + if (lastSessionState.scratchpads) { + ScratchpadManager.restoreSession(lastSessionState.scratchpads); + } + + // The Browser Console + if (lastSessionState.browserConsole) { + HUDService.restoreBrowserConsoleSession(); + } +#endif + + // Set data that persists between sessions + this._recentCrashes = lastSessionState.session && + lastSessionState.session.recentCrashes || 0; + this._sessionStartTime = lastSessionState.session && + lastSessionState.session.startTime || + this._sessionStartTime; + + this._lastSessionState = null; + }, + + /** + * See if aWindow is usable for use when restoring a previous session via + * restoreLastSession. If usable, prepare it for use. + * + * @param aWindow + * the window to inspect & prepare + * @returns [canUseWindow, canOverwriteTabs] + * canUseWindow: can the window be used to restore into + * canOverwriteTabs: all of the current tabs are home pages and we + * can overwrite them + */ + _prepWindowToRestoreInto: function(aWindow) { + if (!aWindow) + return [false, false]; + + let event = aWindow.document.createEvent("Events"); + event.initEvent("SSRestoreIntoWindow", true, true); + + // Check if we can use the window. + if (!aWindow.dispatchEvent(event)) + return [false, false]; + + // We might be able to overwrite the existing tabs instead of just adding + // the previous session's tabs to the end. This will be set if possible. + let canOverwriteTabs = false; + + // Look at the open tabs in comparison to home pages. If all the tabs are + // home pages then we'll end up overwriting all of them. Otherwise we'll + // just close the tabs that match home pages. Tabs with the about:blank + // URI will always be overwritten. + let homePages = ["about:blank"]; + let removableTabs = []; + let tabbrowser = aWindow.gBrowser; + let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs; + let startupPref = this._prefBranch.getIntPref("startup.page"); + if (startupPref == 1) + homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|")); + + for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) { + let tab = tabbrowser.tabs[i]; + if (homePages.indexOf(tab.linkedBrowser.currentURI.spec) != -1) { + removableTabs.push(tab); + } + } + + if (tabbrowser.tabs.length == removableTabs.length) { + canOverwriteTabs = true; + } + else { + // If we're not overwriting all of the tabs, then close the home tabs. + for (let i = removableTabs.length - 1; i >= 0; i--) { + tabbrowser.removeTab(removableTabs.pop(), { animate: false }); + } + } + + return [true, canOverwriteTabs]; + }, + + /* ........ Saving Functionality .............. */ + + /** + * Store all session data for a window + * @param aWindow + * Window reference + */ + _saveWindowHistory: function(aWindow) { + var tabbrowser = aWindow.gBrowser; + var tabs = tabbrowser.tabs; + var tabsData = this._windows[aWindow.__SSi].tabs = []; + + for (var i = 0; i < tabs.length; i++) + tabsData.push(this._collectTabData(tabs[i])); + + this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1; + }, + + /** + * Collect data related to a single tab + * @param aTab + * tabbrowser tab + * @param aFullData + * always return privacy sensitive data (use with care) + * @returns object + */ + _collectTabData: function(aTab, aFullData) { + var tabData = { entries: [], lastAccessed: aTab.lastAccessed }; + var browser = aTab.linkedBrowser; + + if (!browser || !browser.currentURI) + // can happen when calling this function right after .addTab() + return tabData; + else if (browser.__SS_data && browser.__SS_tabStillLoading) { + // use the data to be restored when the tab hasn't been completely loaded + tabData = browser.__SS_data; + if (aTab.pinned) + tabData.pinned = true; + else + delete tabData.pinned; + tabData.hidden = aTab.hidden; + + // If __SS_extdata is set then we'll use that since it might be newer. + if (aTab.__SS_extdata) + tabData.extData = aTab.__SS_extdata; + // If it exists but is empty then a key was likely deleted. In that case just + // delete extData. + if (tabData.extData && !Object.keys(tabData.extData).length) + delete tabData.extData; + return tabData; + } + + var history = null; + try { + history = browser.sessionHistory; + } + catch (ex) { } // this could happen if we catch a tab during (de)initialization + + // Limit number of back/forward button history entries to save + let oldest, newest; + let maxSerializeBack = this._prefBranch.getIntPref("sessionstore.max_serialize_back"); + if (maxSerializeBack >= 0) { + oldest = Math.max(0, history.index - maxSerializeBack); + } else { // History.getEntryAtIndex(0, ...) is the oldest. + oldest = 0; + } + let maxSerializeFwd = this._prefBranch.getIntPref("sessionstore.max_serialize_forward"); + if (maxSerializeFwd >= 0) { + newest = Math.min(history.count - 1, history.index + maxSerializeFwd); + } else { // History.getEntryAtIndex(history.count - 1, ...) is the newest. + newest = history.count - 1; + } + + // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse + // data even when we shouldn't (e.g. Back, different anchor) + // Warning: this is required to save form data and scrolling position! + if (history && browser.__SS_data && + browser.__SS_data.entries[history.index] && + browser.__SS_data.entries[history.index].url == browser.currentURI.spec && + history.index < this._sessionhistory_max_entries - 1 && !aFullData) { + try { + tabData.entries = browser.__SS_data.entries.slice(oldest, newest + 1); + } + catch (ex) { + // No errors are expected above, but we use try-catch to keep sessionstore.js safe + NS_ASSERT(false, "SessionStore failed to slice history from browser.__SS_data"); + } + + // Set the one-based index of the currently active tab, ensuring it isn't out of bounds + tabData.index = Math.min(history.index - oldest + 1, tabData.entries.length); + } + else if (history && history.count > 0) { + browser.__SS_hostSchemeData = []; + try { + for (var j = oldest; j <= newest; j++) { + let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false), + aFullData, aTab.pinned, browser.__SS_hostSchemeData); + tabData.entries.push(entry); + } + } + catch (ex) { + // In some cases, getEntryAtIndex will throw. This seems to be due to + // history.count being higher than it should be. By doing this in a + // try-catch, we'll update history to where it breaks, assert for + // non-release builds, and still save sessionstore.js. + NS_ASSERT(false, "SessionStore failed gathering complete history " + + "for the focused window/tab. See bug 669196."); + } + + // Set the one-based index of the currently active tab, ensuring it isn't out of bounds + tabData.index = Math.min(history.index - oldest + 1, tabData.entries.length); + + // make sure not to cache privacy sensitive data which shouldn't get out + if (!aFullData) + browser.__SS_data = tabData; + } + else if (browser.currentURI.spec != "about:blank" || + browser.contentDocument.body.hasChildNodes()) { + tabData.entries[0] = { url: browser.currentURI.spec }; + tabData.index = 1; + } + + // If there is a userTypedValue set, then either the user has typed something + // in the URL bar, or a new tab was opened with a URI to load. userTypedClear + // is used to indicate whether the tab was in some sort of loading state with + // userTypedValue. + if (browser.userTypedValue) { + tabData.userTypedValue = browser.userTypedValue; + // We always used to keep track of the loading state as an integer, where + // '0' indicated the user had typed since the last load (or no load was + // ongoing), and any positive value indicated we had started a load since + // the last time the user typed in the URL bar. Mimic this to keep the + // session store representation in sync, even though we now represent this + // more explicitly: + tabData.userTypedClear = browser.didStartLoadSinceLastUserTyping() ? 1 : 0; + } else { + delete tabData.userTypedValue; + delete tabData.userTypedClear; + } + + if (aTab.pinned) + tabData.pinned = true; + else + delete tabData.pinned; + tabData.hidden = aTab.hidden; + + var disallow = []; + for (let cap of gDocShellCapabilities(browser.docShell)) + if (!browser.docShell["allow" + cap]) + disallow.push(cap); + if (disallow.length > 0) + tabData.disallow = disallow.join(","); + else if (tabData.disallow) + delete tabData.disallow; + + // Save tab attributes. + tabData.attributes = TabAttributes.get(aTab); + + // Store the tab icon. + let tabbrowser = aTab.ownerDocument.defaultView.gBrowser; + tabData.image = tabbrowser.getIcon(aTab); + + if (aTab.__SS_extdata) + tabData.extData = aTab.__SS_extdata; + else if (tabData.extData) + delete tabData.extData; + + if (history && browser.docShell instanceof Ci.nsIDocShell) { + let storageData = SessionStorage.serialize(browser.docShell, aFullData) + if (Object.keys(storageData).length) + tabData.storage = storageData; + } + + return tabData; + }, + + /** + * Get an object that is a serialized representation of a History entry + * Used for data storage + * @param aEntry + * nsISHEntry instance + * @param aFullData + * always return privacy sensitive data (use with care) + * @param aIsPinned + * the tab is pinned and should be treated differently for privacy + * @param aHostSchemeData + * an array of objects with host & scheme keys + * @returns object + */ + _serializeHistoryEntry: + function(aEntry, aFullData, aIsPinned, aHostSchemeData) { + var entry = { url: aEntry.URI.spec }; + + try { + // throwing is expensive, we know that about: pages will throw + if (entry.url.indexOf("about:") != 0) + aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme }); + } + catch (ex) { + // We just won't attempt to get cookies for this entry. + } + + if (aEntry.title && aEntry.title != entry.url) { + entry.title = aEntry.title; + } + if (aEntry.isSubFrame) { + entry.subframe = true; + } + if (!(aEntry instanceof Ci.nsISHEntry)) { + return entry; + } + + var cacheKey = aEntry.cacheKey; + if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && + cacheKey.data != 0) { + // XXXbz would be better to have cache keys implement + // nsISerializable or something. + entry.cacheKey = cacheKey.data; + } + entry.ID = aEntry.ID; + entry.docshellID = aEntry.docshellID; + + if (aEntry.referrerURI) + entry.referrer = aEntry.referrerURI.spec; + + if (aEntry.srcdocData) + entry.srcdocData = aEntry.srcdocData; + + if (aEntry.isSrcdocEntry) + entry.isSrcdocEntry = aEntry.isSrcdocEntry; + + if (aEntry.contentType) + entry.contentType = aEntry.contentType; + + var x = {}, y = {}; + aEntry.getScrollPosition(x, y); + if (x.value != 0 || y.value != 0) + entry.scroll = x.value + "," + y.value; + + try { + var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata"); + if (aEntry.postData && (aFullData || prefPostdata && + this.checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) { + aEntry.postData.QueryInterface(Ci.nsISeekableStream). + seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + var stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(aEntry.postData); + var postBytes = stream.readByteArray(stream.available()); + var postdata = String.fromCharCode.apply(null, postBytes); + if (aFullData || prefPostdata == -1 || + postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <= + prefPostdata) { + // We can stop doing base64 encoding once our serialization into JSON + // is guaranteed to handle all chars in strings, including embedded + // nulls. + entry.postdata_b64 = btoa(postdata); + } + } + } + catch (ex) { debug(ex); } // POSTDATA is tricky - especially since some extensions don't get it right + + if (aEntry.triggeringPrincipal) { + // Not catching anything specific here, just possible errors + // from writeCompoundObject and the like. + try { + var binaryStream = Cc["@mozilla.org/binaryoutputstream;1"]. + createInstance(Ci.nsIObjectOutputStream); + var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + binaryStream.setOutputStream(pipe.outputStream); + binaryStream.writeCompoundObject(aEntry.triggeringPrincipal, Ci.nsIPrincipal, true); + binaryStream.close(); + + // Now we want to read the data from the pipe's input end and encode it. + var scriptableStream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + scriptableStream.setInputStream(pipe.inputStream); + var triggeringPrincipalBytes = + 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.triggeringPrincipal_b64 = btoa(String.fromCharCode.apply(null, triggeringPrincipalBytes)); + } + catch (ex) { debug(ex); } + } + + 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) { + let children = []; + for (var i = 0; i < aEntry.childCount; i++) { + var child = aEntry.GetChildAt(i); + + if (child) { + // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) + if (child.URI.schemeIs("wyciwyg")) { + children = []; + break; + } + + children.push(this._serializeHistoryEntry(child, aFullData, + aIsPinned, aHostSchemeData)); + } + } + + if (children.length) + entry.children = children; + } + + return entry; + }, + + /** + * go through all tabs and store the current scroll positions + * and innerHTML content of WYSIWYG editors + * @param aWindow + * Window reference + */ + _updateTextAndScrollData: function(aWindow) { + var browsers = aWindow.gBrowser.browsers; + this._windows[aWindow.__SSi].tabs.forEach(function(tabData, i) { + try { + this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData); + } + catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time) + }, this); + }, + + /** + * go through all frames and store the current scroll positions + * and innerHTML content of WYSIWYG editors + * @param aWindow + * Window reference + * @param aBrowser + * single browser reference + * @param aTabData + * tabData object to add the information to + * @param aFullData + * always return privacy sensitive data (use with care) + */ + _updateTextAndScrollDataForTab: + function(aWindow, aBrowser, aTabData, aFullData) { + // we shouldn't update data for incompletely initialized tabs + if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading) + return; + + var tabIndex = (aTabData.index || aTabData.entries.length) - 1; + // entry data needn't exist for tabs just initialized with an incomplete session state + if (!aTabData.entries[tabIndex]) + return; + + let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : + this._getSelectedPageStyle(aBrowser.contentWindow); + if (selectedPageStyle) + aTabData.pageStyle = selectedPageStyle; + else if (aTabData.pageStyle) + delete aTabData.pageStyle; + + this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow, + aTabData.entries[tabIndex], + !aBrowser.__SS_formDataSaved, aFullData, + !!aTabData.pinned); + aBrowser.__SS_formDataSaved = true; + if (aBrowser.currentURI.spec == "about:config") + aTabData.entries[tabIndex].formdata = { + id: { + "textbox": aBrowser.contentDocument.getElementById("textbox").value + }, + xpath: {} + }; + }, + + /** + * go through all subframes and store all form data, the current + * scroll positions and innerHTML content of WYSIWYG editors + * @param aWindow + * Window reference + * @param aContent + * frame reference + * @param aData + * part of a tabData object to add the information to + * @param aUpdateFormData + * update all form data for this tab + * @param aFullData + * always return privacy sensitive data (use with care) + * @param aIsPinned + * the tab is pinned and should be treated differently for privacy + */ + _updateTextAndScrollDataForFrame: + function(aWindow, aContent, aData, + aUpdateFormData, aFullData, aIsPinned) { + for (var i = 0; i < aContent.frames.length; i++) { + if (aData.children && aData.children[i]) + this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i], + aData.children[i], aUpdateFormData, + aFullData, aIsPinned); + } + var isHTTPS = this._getURIFromString((aContent.parent || aContent). + document.location.href).schemeIs("https"); + let isAboutSR = aContent.top.document.location.href == "about:sessionrestore"; + if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) { + if (aFullData || aUpdateFormData) { + let formData = DocumentUtils.getFormData(aContent.document); + + // We want to avoid saving data for about:sessionrestore as a string. + // Since it's stored in the form as stringified JSON, stringifying further + // causes an explosion of escape characters. cf. bug 467409 + if (formData && isAboutSR) { + formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]); + } + + if (Object.keys(formData.id).length || + Object.keys(formData.xpath).length) { + aData.formdata = formData; + } else if (aData.formdata) { + delete aData.formdata; + } + } + + // designMode is undefined e.g. for XUL documents (as about:config) + if ((aContent.document.designMode || "") == "on" && aContent.document.body) + aData.innerHTML = aContent.document.body.innerHTML; + } + + // get scroll position from nsIDOMWindowUtils, since it allows avoiding a + // flush of layout + let domWindowUtils = aContent.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let scrollX = {}, scrollY = {}; + domWindowUtils.getScrollXY(false, scrollX, scrollY); + aData.scroll = scrollX.value + "," + scrollY.value; + }, + + /** + * determine the title of the currently enabled style sheet (if any) + * and recurse through the frameset if necessary + * @param aContent is a frame reference + * @returns the title style sheet determined to be enabled (empty string if none) + */ + _getSelectedPageStyle: function(aContent) { + const forScreen = /(?:^|,)\s*(?:all|screen)\s*(?:,|$)/i; + for (let i = 0; i < aContent.document.styleSheets.length; i++) { + let ss = aContent.document.styleSheets[i]; + let media = ss.media.mediaText; + if (!ss.disabled && ss.title && (!media || forScreen.test(media))) + return ss.title + } + for (let i = 0; i < aContent.frames.length; i++) { + let selectedPageStyle = this._getSelectedPageStyle(aContent.frames[i]); + if (selectedPageStyle) + return selectedPageStyle; + } + return ""; + }, + + /** + * extract the base domain from a history entry and its children + * @param aEntry + * the history entry, serialized + * @param aHosts + * the hash that will be used to store hosts eg, { hostname: true } + * @param aCheckPrivacy + * should we check the privacy level for https + * @param aIsPinned + * is the entry we're evaluating for a pinned tab; used only if + * aCheckPrivacy + */ + _extractHostsForCookiesFromEntry: + function(aEntry, aHosts, aCheckPrivacy, aIsPinned) { + + let host = aEntry._host, + scheme = aEntry._scheme; + + // If host & scheme aren't defined, then we are likely here in the startup + // process via _splitCookiesFromWindow. In that case, we'll turn aEntry.url + // into an nsIURI and get host/scheme from that. This will throw for about: + // urls in which case we don't need to do anything. + if (!host && !scheme) { + try { + let uri = this._getURIFromString(aEntry.url); + host = uri.host; + scheme = uri.scheme; + this._extractHostsForCookiesFromHostScheme(host, scheme, aHosts, aCheckPrivacy, aIsPinned); + } + catch(ex) { } + } + + if (aEntry.children) { + aEntry.children.forEach(function(entry) { + this._extractHostsForCookiesFromEntry(entry, aHosts, aCheckPrivacy, aIsPinned); + }, this); + } + }, + + /** + * extract the base domain from a host & scheme + * @param aHost + * the host of a uri (usually via nsIURI.host) + * @param aScheme + * the scheme of a uri (usually via nsIURI.scheme) + * @param aHosts + * the hash that will be used to store hosts eg, { hostname: true } + * @param aCheckPrivacy + * should we check the privacy level for https + * @param aIsPinned + * is the entry we're evaluating for a pinned tab; used only if + * aCheckPrivacy + */ + _extractHostsForCookiesFromHostScheme: + function(aHost, aScheme, aHosts, aCheckPrivacy, aIsPinned) { + // host and scheme may not be set (for about: urls for example), in which + // case testing scheme will be sufficient. + if (/https?/.test(aScheme) && !aHosts[aHost] && + (!aCheckPrivacy || + this.checkPrivacyLevel(aScheme == "https", aIsPinned))) { + // By setting this to true or false, we can determine when looking at + // the host in _updateCookies if we should check for privacy. + aHosts[aHost] = aIsPinned; + } + else if (aScheme == "file") { + aHosts[aHost] = true; + } + }, + + /** + * store all hosts for a URL + * @param aWindow + * Window reference + */ + _updateCookieHosts: function(aWindow) { + var hosts = this._internalWindows[aWindow.__SSi].hosts = {}; + + // Since _updateCookiesHosts is only ever called for open windows during a + // session, we can call into _extractHostsForCookiesFromHostScheme directly + // using data that is attached to each browser. + for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) { + let tab = aWindow.gBrowser.tabs[i]; + let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || []; + for (let j = 0; j < hostSchemeData.length; j++) { + this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host, + hostSchemeData[j].scheme, + hosts, true, tab.pinned); + } + } + }, + + /** + * Serialize cookie data + * @param aWindows + * JS object containing window data references + * { id: winData, etc. } + */ + _updateCookies: function(aWindows) { + function addCookieToHash(aHash, aHost, aPath, aName, aCookie) { + // lazily build up a 3-dimensional hash, with + // aHost, aPath, and aName as keys + if (!aHash[aHost]) + aHash[aHost] = {}; + if (!aHash[aHost][aPath]) + aHash[aHost][aPath] = {}; + aHash[aHost][aPath][aName] = aCookie; + } + + var jscookies = {}; + var _this = this; + // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision + var MAX_EXPIRY = Math.pow(2, 62); + + for (let [id, window] in Iterator(aWindows)) { + window.cookies = []; + let internalWindow = this._internalWindows[id]; + if (!internalWindow.hosts) + return; + for (var [host, isPinned] in Iterator(internalWindow.hosts)) { + let list; + try { + list = Services.cookies.getCookiesFromHost(host, {}); + } + catch (ex) { + debug("getCookiesFromHost failed. Host: " + host); + } + while (list && list.hasMoreElements()) { + var cookie = list.getNext().QueryInterface(Ci.nsICookie2); + // window._hosts will only have hosts with the right privacy rules, + // so there is no need to do anything special with this call to + // checkPrivacyLevel. + if (cookie.isSession && _this.checkPrivacyLevel(cookie.isSecure, isPinned)) { + // use the cookie's host, path, and name as keys into a hash, + // to make sure we serialize each cookie only once + if (!(cookie.host in jscookies && + cookie.path in jscookies[cookie.host] && + cookie.name in jscookies[cookie.host][cookie.path])) { + var jscookie = { "host": cookie.host, "value": cookie.value }; + // only add attributes with non-default values (saving a few bits) + if (cookie.path) jscookie.path = cookie.path; + if (cookie.name) jscookie.name = cookie.name; + if (cookie.isSecure) jscookie.secure = true; + if (cookie.isHttpOnly) jscookie.httponly = true; + if (cookie.expiry < MAX_EXPIRY) jscookie.expiry = cookie.expiry; + + addCookieToHash(jscookies, cookie.host, cookie.path, cookie.name, jscookie); + } + window.cookies.push(jscookies[cookie.host][cookie.path][cookie.name]); + } + } + } + + // don't include empty cookie sections + if (!window.cookies.length) + delete window.cookies; + } + }, + + /** + * Store window dimensions, visibility, sidebar + * @param aWindow + * Window reference + */ + _updateWindowFeatures: function(aWindow) { + var winData = this._windows[aWindow.__SSi]; + + WINDOW_ATTRIBUTES.forEach(function(aAttr) { + winData[aAttr] = this._getWindowDimension(aWindow, aAttr); + }, this); + + var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) { + return aWindow[aItem] && !aWindow[aItem].visible; + }); + if (hidden.length != 0) + winData.hidden = hidden.join(","); + else if (winData.hidden) + delete winData.hidden; + + var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand"); + if (sidebar) + winData.sidebar = sidebar; + else if (winData.sidebar) + delete winData.sidebar; + }, + + /** + * gather session data as object + * @param aUpdateAll + * Bool update all windows + * @param aPinnedOnly + * Bool collect pinned tabs only + * @returns object + */ + _getCurrentState: function(aUpdateAll, aPinnedOnly) { + this._handleClosedWindows(); + + var activeWindow = this._getMostRecentBrowserWindow(); + + if (this._loadState == STATE_RUNNING) { + // update the data for all windows with activities since the last save operation + this._forEachBrowserWindow(function(aWindow) { + if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore + return; + if (aUpdateAll || this._dirtyWindows[aWindow.__SSi] || aWindow == activeWindow) { + this._collectWindowData(aWindow); + } + else { // always update the window features (whose change alone never triggers a save operation) + this._updateWindowFeatures(aWindow); + } + }); + this._dirtyWindows = []; + } + + // collect the data for all windows + var total = [], windows = {}, ids = []; + var nonPopupCount = 0; + var ix; + for (ix in this._windows) { + if (this._windows[ix]._restoring) // window data is still in _statesToRestore + continue; + total.push(this._windows[ix]); + ids.push(ix); + windows[ix] = this._windows[ix]; + if (!this._windows[ix].isPopup) + nonPopupCount++; + } + this._updateCookies(windows); + + // collect the data for all windows yet to be restored + for (ix in this._statesToRestore) { + for each (let winData in this._statesToRestore[ix].windows) { + total.push(winData); + if (!winData.isPopup) + nonPopupCount++; + } + } + + // shallow copy this._closedWindows to preserve current state + let lastClosedWindowsCopy = this._closedWindows.slice(); + + // If no non-popup browser window remains open, return the state of the last + // closed window(s). We only want to do this when we're actually "ending" + // the session. + //XXXzpao We should do this for _restoreLastWindow == true, but that has + // its own check for popups. c.f. bug 597619 + if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 && + this._loadState == STATE_QUITTING) { + // prepend the last non-popup browser window, so that if the user loads more tabs + // at startup we don't accidentally add them to a popup window + do { + total.unshift(lastClosedWindowsCopy.shift()) + } while (total[0].isPopup && lastClosedWindowsCopy.length > 0) + } + + if (aPinnedOnly) { + // perform a deep copy so that existing session variables are not changed. + total = JSON.parse(this._toJSONString(total)); + total = total.filter(function(win) { + win.tabs = win.tabs.filter(function(tab) tab.pinned); + // remove closed tabs + win._closedTabs = []; + // correct selected tab index if it was stripped out + if (win.selected > win.tabs.length) + win.selected = 1; + return win.tabs.length > 0; + }); + if (total.length == 0) + return null; + + lastClosedWindowsCopy = []; + } + + if (activeWindow) { + this.activeWindowSSiCache = activeWindow.__SSi || ""; + } + ix = ids.indexOf(this.activeWindowSSiCache); + // We don't want to restore focus to a minimized window or a window which had all its + // tabs stripped out (doesn't exist). + if (ix != -1 && total[ix] && total[ix].sizemode == "minimized") + ix = -1; + + let session = { + state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR, + lastUpdate: Date.now(), + startTime: this._sessionStartTime, + recentCrashes: this._recentCrashes + }; + + var scratchpads = null; + var browserConsole = null; +#ifdef MOZ_DEVTOOLS + // Scratchpad + // get open Scratchpad window states too + scratchpads = ScratchpadManager.getSessionState(); + + // The Browser Console + browserConsole = HUDService.getBrowserConsoleSessionState(); +#endif + + return { + windows: total, + selectedWindow: ix + 1, + _closedWindows: lastClosedWindowsCopy, +#ifdef MOZ_DEVTOOLS + session: session, + scratchpads: scratchpads, + browserConsole: browserConsole +#else + session: session +#endif + }; + }, + + /** + * serialize session data for a window + * @param aWindow + * Window reference + * @returns string + */ + _getWindowState: function(aWindow) { + if (!this._isWindowLoaded(aWindow)) + return this._statesToRestore[aWindow.__SS_restoreID]; + + if (this._loadState == STATE_RUNNING) { + this._collectWindowData(aWindow); + } + + var winData = this._windows[aWindow.__SSi]; + let windows = {}; + windows[aWindow.__SSi] = winData; + this._updateCookies(windows); + + return { windows: [winData] }; + }, + + _collectWindowData: function(aWindow) { + if (!this._isWindowLoaded(aWindow)) + return; + + // update the internal state data for this window + this._saveWindowHistory(aWindow); + this._updateTextAndScrollData(aWindow); + this._updateCookieHosts(aWindow); + this._updateWindowFeatures(aWindow); + + // Make sure we keep __SS_lastSessionWindowID around for cases like entering + // or leaving PB mode. + if (aWindow.__SS_lastSessionWindowID) + this._windows[aWindow.__SSi].__lastSessionWindowID = + aWindow.__SS_lastSessionWindowID; + + this._dirtyWindows[aWindow.__SSi] = false; + }, + + /* ........ Restoring Functionality .............. */ + + /** + * restore features to a single window + * @param aWindow + * Window reference + * @param aState + * JS object or its eval'able source + * @param aOverwriteTabs + * bool overwrite existing tabs w/ new ones + * @param aFollowUp + * bool this isn't the restoration of the first window + */ + restoreWindow: function(aWindow, aState, aOverwriteTabs, aFollowUp) { + if (!aFollowUp) { + this.windowToFocus = aWindow; + } + // initialize window if necessary + if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) + this.onLoad(aWindow); + + try { + var root = typeof aState == "string" ? JSON.parse(aState) : aState; + if (!root.windows[0]) { + this._sendRestoreCompletedNotifications(); + return; // nothing to restore + } + } + catch (ex) { // invalid state object - don't restore anything + debug(ex); + this._sendRestoreCompletedNotifications(); + return; + } + + // We're not returning from this before we end up calling restoreHistoryPrecursor + // for this window, so make sure we send the SSWindowStateBusy event. + this._setWindowStateBusy(aWindow); + + if (root._closedWindows) + this._closedWindows = root._closedWindows; + + var winData; + if (!root.selectedWindow || root.selectedWindow > root.windows.length) { + root.selectedWindow = 0; + } + + // open new windows for all further window entries of a multi-window session + // (unless they don't contain any tab data) + for (var w = 1; w < root.windows.length; w++) { + winData = root.windows[w]; + if (winData && winData.tabs && winData.tabs[0]) { + var window = this._openWindowWithState({ windows: [winData] }); + if (w == root.selectedWindow - 1) { + this.windowToFocus = window; + } + } + } + winData = root.windows[0]; + if (!winData.tabs) { + winData.tabs = []; + } + // don't restore a single blank tab when we've had an external + // URL passed in for loading at startup (cf. bug 357419) + else if (root._firstTabs && !aOverwriteTabs && winData.tabs.length == 1 && + (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) { + winData.tabs = []; + } + + var tabbrowser = aWindow.gBrowser; + var openTabCount = aOverwriteTabs ? tabbrowser.browsers.length : -1; + var newTabCount = winData.tabs.length; + var tabs = []; + + // disable smooth scrolling while adding, moving, removing and selecting tabs + var tabstrip = tabbrowser.tabContainer.mTabstrip; + var smoothScroll = tabstrip.smoothScroll; + tabstrip.smoothScroll = false; + + // unpin all tabs to ensure they are not reordered in the next loop + if (aOverwriteTabs) { + for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--) + tabbrowser.unpinTab(tabbrowser.tabs[t]); + } + + // make sure that the selected tab won't be closed in order to + // prevent unnecessary flickering + if (aOverwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount) + tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1); + + let numVisibleTabs = 0; + + for (var t = 0; t < newTabCount; t++) { + tabs.push(t < openTabCount ? + tabbrowser.tabs[t] : + tabbrowser.addTab("about:blank", + {skipAnimation: true, + skipBackgroundNotify: true})); + // when resuming at startup: add additionally requested pages to the end + if (!aOverwriteTabs && root._firstTabs) { + tabbrowser.moveTabTo(tabs[t], t); + } + + if (winData.tabs[t].pinned) + tabbrowser.pinTab(tabs[t]); + + if (winData.tabs[t].hidden) { + tabbrowser.hideTab(tabs[t]); + } + else { + tabbrowser.showTab(tabs[t]); + numVisibleTabs++; + } + } + + // if all tabs to be restored are hidden, make the first one visible + if (!numVisibleTabs && winData.tabs.length) { + winData.tabs[0].hidden = false; + tabbrowser.showTab(tabs[0]); + } + + // If overwriting tabs, we want to reset each tab's "restoring" state. Since + // we're overwriting those tabs, they should no longer be restoring. The + // tabs will be rebuilt and marked if they need to be restored after loading + // state (in restoreHistoryPrecursor). + if (aOverwriteTabs) { + for (let i = 0; i < tabbrowser.tabs.length; i++) { + if (tabbrowser.browsers[i].__SS_restoreState) + this._resetTabRestoringState(tabbrowser.tabs[i]); + } + } + + // We want to set up a counter on the window that indicates how many tabs + // in this window are unrestored. This will be used in restoreNextTab to + // determine if gRestoreTabsProgressListener should be removed from the window. + // If we aren't overwriting existing tabs, then we want to add to the existing + // count in case there are still tabs restoring. + if (!aWindow.__SS_tabsToRestore) + aWindow.__SS_tabsToRestore = 0; + if (aOverwriteTabs) + aWindow.__SS_tabsToRestore = newTabCount; + else + aWindow.__SS_tabsToRestore += newTabCount; + + // We want to correlate the window with data from the last session, so + // assign another id if we have one. Otherwise clear so we don't do + // anything with it. + delete aWindow.__SS_lastSessionWindowID; + if (winData.__lastSessionWindowID) + aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID; + + // when overwriting tabs, remove all superflous ones + if (aOverwriteTabs && newTabCount < openTabCount) { + Array.slice(tabbrowser.tabs, newTabCount, openTabCount) + .forEach(tabbrowser.removeTab, tabbrowser); + } + + if (aOverwriteTabs) { + this.restoreWindowFeatures(aWindow, winData); + delete this._windows[aWindow.__SSi].extData; + } + if (winData.cookies) { + this.restoreCookies(winData.cookies); + } + if (winData.extData) { + if (!this._windows[aWindow.__SSi].extData) { + this._windows[aWindow.__SSi].extData = {}; + } + for (var key in winData.extData) { + this._windows[aWindow.__SSi].extData[key] = winData.extData[key]; + } + } + if (aOverwriteTabs || root._firstTabs) { + this._windows[aWindow.__SSi]._closedTabs = winData._closedTabs || []; + } + + this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs, + (aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0); + +#ifdef MOZ_DEVTOOLS + if (aState.scratchpads) { + ScratchpadManager.restoreSession(aState.scratchpads); + } + + // The Browser Console + if (aState.browserConsole) { + HUDService.restoreBrowserConsoleSession(); + } + +#endif + // set smoothScroll back to the original value + tabstrip.smoothScroll = smoothScroll; + + this._sendRestoreCompletedNotifications(); + }, + + /** + * Sets the tabs restoring order with the following priority: + * Selected tab, pinned tabs, optimized visible tabs, other visible tabs and + * hidden tabs. + * @param aTabBrowser + * Tab browser object + * @param aTabs + * Array of tab references + * @param aTabData + * Array of tab data + * @param aSelectedTab + * Index of selected tab (1 is first tab, 0 no selected tab) + */ + _setTabsRestoringOrder : function( + aTabBrowser, aTabs, aTabData, aSelectedTab) { + + // Store the selected tab. Need to substract one to get the index in aTabs. + let selectedTab; + if (aSelectedTab > 0 && aTabs[aSelectedTab - 1]) { + selectedTab = aTabs[aSelectedTab - 1]; + } + + // Store the pinned tabs and hidden tabs. + let pinnedTabs = []; + let pinnedTabsData = []; + let hiddenTabs = []; + let hiddenTabsData = []; + if (aTabs.length > 1) { + for (let t = aTabs.length - 1; t >= 0; t--) { + if (aTabData[t].pinned) { + pinnedTabs.unshift(aTabs.splice(t, 1)[0]); + pinnedTabsData.unshift(aTabData.splice(t, 1)[0]); + } else if (aTabData[t].hidden) { + hiddenTabs.unshift(aTabs.splice(t, 1)[0]); + hiddenTabsData.unshift(aTabData.splice(t, 1)[0]); + } + } + } + + // Optimize the visible tabs only if there is a selected tab. + if (selectedTab) { + let selectedTabIndex = aTabs.indexOf(selectedTab); + if (selectedTabIndex > 0) { + let scrollSize = aTabBrowser.tabContainer.mTabstrip.scrollClientSize; + let tabWidth = aTabs[0].getBoundingClientRect().width; + let maxVisibleTabs = Math.ceil(scrollSize / tabWidth); + if (maxVisibleTabs < aTabs.length) { + let firstVisibleTab = 0; + let nonVisibleTabsCount = aTabs.length - maxVisibleTabs; + if (nonVisibleTabsCount >= selectedTabIndex) { + // Selected tab is leftmost since we scroll to it when possible. + firstVisibleTab = selectedTabIndex; + } else { + // Selected tab is rightmost or no more room to scroll right. + firstVisibleTab = nonVisibleTabsCount; + } + aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs); + aTabData = + aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData); + } + } + } + + // Merge the stored tabs in order. + aTabs = pinnedTabs.concat(aTabs, hiddenTabs); + aTabData = pinnedTabsData.concat(aTabData, hiddenTabsData); + + // Load the selected tab to the first position and select it. + if (selectedTab) { + let selectedTabIndex = aTabs.indexOf(selectedTab); + if (selectedTabIndex > 0) { + aTabs = aTabs.splice(selectedTabIndex, 1).concat(aTabs); + aTabData = aTabData.splice(selectedTabIndex, 1).concat(aTabData); + } + aTabBrowser.selectedTab = selectedTab; + } + + return [aTabs, aTabData]; + }, + + /** + * Manage history restoration for a window + * @param aWindow + * Window to restore the tabs into + * @param aTabs + * Array of tab references + * @param aTabData + * Array of tab data + * @param aSelectTab + * Index of selected tab + * @param aIx + * Index of the next tab to check readyness for + * @param aCount + * Counter for number of times delaying b/c browser or history aren't ready + * @param aRestoreImmediately + * Flag to indicate whether the given set of tabs aTabs should be + * restored/loaded immediately even if restore_on_demand = true + */ + restoreHistoryPrecursor: + function(aWindow, aTabs, aTabData, aSelectTab, + aIx, aCount, aRestoreImmediately = false) { + var tabbrowser = aWindow.gBrowser; + + // make sure that all browsers and their histories are available + // - if one's not, resume this check in 100ms (repeat at most 10 times) + for (var t = aIx; t < aTabs.length; t++) { + try { + if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) { + throw new Error(); + } + } + catch (ex) { // in case browser or history aren't ready yet + if (aCount < 10) { + var restoreHistoryFunc = function(self) { + self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, + aIx, aCount + 1, aRestoreImmediately); + } + aWindow.setTimeout(restoreHistoryFunc, 100, this); + return; + } + } + } + + if (!this._isWindowLoaded(aWindow)) { + // from now on, the data will come from the actual window + delete this._statesToRestore[aWindow.__SS_restoreID]; + delete aWindow.__SS_restoreID; + delete this._windows[aWindow.__SSi]._restoring; + + // It's important to set the window state to dirty so that + // we collect their data for the first time when saving state. + this._dirtyWindows[aWindow.__SSi] = true; + } + + if (aTabs.length == 0) { + // this is normally done in restoreHistory() but as we're returning early + // here we need to take care of it. + this._setWindowStateReady(aWindow); + return; + } + + // Sets the tabs restoring order. + [aTabs, aTabData] = + this._setTabsRestoringOrder(tabbrowser, aTabs, aTabData, aSelectTab); + + // Prepare the tabs so that they can be properly restored. We'll pin/unpin + // and show/hide tabs as necessary. We'll also set the labels, user typed + // value, and attach a copy of the tab's data in case we close it before + // it's been restored. + for (t = 0; t < aTabs.length; t++) { + let tab = aTabs[t]; + let browser = tabbrowser.getBrowserForTab(tab); + let tabData = aTabData[t]; + + if (tabData.pinned) + tabbrowser.pinTab(tab); + else + tabbrowser.unpinTab(tab); + + if (tabData.hidden) + tabbrowser.hideTab(tab); + else + tabbrowser.showTab(tab); + + if ("attributes" in tabData) { + // Ensure that we persist tab attributes restored from previous sessions. + Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a)); + } + + browser.__SS_tabStillLoading = true; + + // keep the data around to prevent dataloss in case + // a tab gets closed before it's been properly restored + browser.__SS_data = tabData; + browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE; + browser.setAttribute("pending", "true"); + tab.setAttribute("pending", "true"); + + // Make sure that set/getTabValue will set/read the correct data by + // wiping out any current value in tab.__SS_extdata. + delete tab.__SS_extdata; + + if (!tabData.entries || tabData.entries.length == 0) { + // make sure to blank out this tab's content + // (just purging the tab's history won't be enough) + browser.contentDocument.location = "about:blank"; + continue; + } + + browser.stop(); // in case about:blank isn't done yet + + // wall-paper fix for bug 439675: make sure that the URL to be loaded + // is always visible in the address bar + let activeIndex = (tabData.index || tabData.entries.length) - 1; + let activePageData = tabData.entries[activeIndex] || null; + let uri = activePageData ? activePageData.url || null : null; + browser.userTypedValue = uri; + + // Also make sure currentURI is set so that switch-to-tab works before + // the tab is restored. We'll reset this to about:blank when we try to + // restore the tab to ensure that docshell doeesn't get confused. + if (uri) + browser.docShell.setCurrentURI(this._getURIFromString(uri)); + + // If the page has a title, set it. + if (activePageData) { + if (activePageData.title) { + tab.label = activePageData.title; + tab.crop = "end"; + } else if (activePageData.url != "about:blank") { + tab.label = activePageData.url; + tab.crop = "center"; + } + } + } + + // helper hashes for ensuring unique frame IDs and unique document + // identifiers. + var idMap = { used: {} }; + var docIdentMap = {}; + this.restoreHistory(aWindow, aTabs, aTabData, idMap, docIdentMap, + aRestoreImmediately); + }, + + /** + * Restore history for a window + * @param aWindow + * Window reference + * @param aTabs + * Array of tab references + * @param aTabData + * Array of tab data + * @param aIdMap + * Hash for ensuring unique frame IDs + * @param aRestoreImmediately + * Flag to indicate whether the given set of tabs aTabs should be + * restored/loaded immediately even if restore_on_demand = true + */ + restoreHistory: + function(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap, + aRestoreImmediately) { + var _this = this; + // if the tab got removed before being completely restored, then skip it + while (aTabs.length > 0 && !(this._canRestoreTabHistory(aTabs[0]))) { + aTabs.shift(); + aTabData.shift(); + } + if (aTabs.length == 0) { + // At this point we're essentially ready for consumers to read/write data + // via the sessionstore API so we'll send the SSWindowStateReady event. + this._setWindowStateReady(aWindow); + return; // no more tabs to restore + } + + var tab = aTabs.shift(); + var tabData = aTabData.shift(); + + var browser = aWindow.gBrowser.getBrowserForTab(tab); + var history = browser.webNavigation.sessionHistory; + + if (history.count > 0) { + history.PurgeHistory(history.count); + } + history.QueryInterface(Ci.nsISHistoryInternal); + + browser.__SS_shistoryListener = new SessionStoreSHistoryListener(tab); + history.addSHistoryListener(browser.__SS_shistoryListener); + + if (!tabData.entries) { + tabData.entries = []; + } + if (tabData.extData) { + tab.__SS_extdata = {}; + for (let key in tabData.extData) + tab.__SS_extdata[key] = tabData.extData[key]; + } + else + delete tab.__SS_extdata; + + for (var i = 0; i < tabData.entries.length; i++) { + //XXXzpao Wallpaper patch for bug 514751 + if (!tabData.entries[i].url) + continue; + history.addEntry(this._deserializeHistoryEntry(tabData.entries[i], + aIdMap, aDocIdentMap), true); + } + + // make sure to reset the capabilities and attributes, in case this tab gets reused + let disallow = new Set(tabData.disallow && tabData.disallow.split(",")); + for (let cap of gDocShellCapabilities(browser.docShell)) + browser.docShell["allow" + cap] = !disallow.has(cap); + + // Restore tab attributes. + if ("attributes" in tabData) { + TabAttributes.set(tab, tabData.attributes); + } + + // Restore the tab icon. + if ("image" in tabData) { + // Using null as the loadingPrincipal because serializing + // the principal would be overkill. Within SetIcon we + // default to the systemPrincipal if aLoadingPrincipal is + // null which will allow the favicon to load. + aWindow.gBrowser.setIcon(tab, tabData.image, null); + } + + if (tabData.storage && browser.docShell instanceof Ci.nsIDocShell) + SessionStorage.deserialize(browser.docShell, tabData.storage); + + // notify the tabbrowser that the tab chrome has been restored + var event = aWindow.document.createEvent("Events"); + event.initEvent("SSTabRestoring", true, false); + tab.dispatchEvent(event); + + // Restore the history in the next tab + aWindow.setTimeout(function(){ + _this.restoreHistory(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap, + aRestoreImmediately); + }, 0); + + // This could cause us to ignore max_concurrent_tabs pref a bit, but + // it ensures each window will have its selected tab loaded. + if (aRestoreImmediately || aWindow.gBrowser.selectedBrowser == browser) { + this.restoreTab(tab); + } + else { + TabRestoreQueue.add(tab); + this.restoreNextTab(); + } + }, + + /** + * Restores the specified tab. If the tab can't be restored (eg, no history or + * calling gotoIndex fails), then state changes will be rolled back. + * This method will check if gTabsProgressListener is attached to the tab's + * window, ensuring that we don't get caught without one. + * This method removes the session history listener right before starting to + * attempt a load. This will prevent cases of "stuck" listeners. + * If this method returns false, then it is up to the caller to decide what to + * do. In the common case (restoreNextTab), we will want to then attempt to + * restore the next tab. In the other case (selecting the tab, reloading the + * tab), the caller doesn't actually want to do anything if no page is loaded. + * + * @param aTab + * the tab to restore + * + * @returns true/false indicating whether or not a load actually happened + */ + restoreTab: function(aTab) { + let window = aTab.ownerDocument.defaultView; + let browser = aTab.linkedBrowser; + let tabData = browser.__SS_data; + + // There are cases within where we haven't actually started a load. In that + // that case we'll reset state changes we made and return false to the caller + // can handle appropriately. + let didStartLoad = false; + + // Make sure that the tabs progress listener is attached to this window + this._ensureTabsProgressListener(window); + + // Make sure that this tab is removed from the priority queue. + TabRestoreQueue.remove(aTab); + + // Increase our internal count. + this._tabsRestoringCount++; + + // Set this tab's state to restoring + browser.__SS_restoreState = TAB_STATE_RESTORING; + browser.removeAttribute("pending"); + aTab.removeAttribute("pending"); + + // Remove the history listener, since we no longer need it once we start restoring + this._removeSHistoryListener(aTab); + + let activeIndex = (tabData.index || tabData.entries.length) - 1; + if (activeIndex >= tabData.entries.length) + activeIndex = tabData.entries.length - 1; + // Reset currentURI. This creates a new session history entry with a new + // doc identifier, so we need to explicitly save and restore the old doc + // identifier (corresponding to the SHEntry at activeIndex) below. + browser.webNavigation.setCurrentURI(this._getURIFromString("about:blank")); + // Attach data that will be restored on "load" event, after tab is restored. + if (activeIndex > -1) { + // restore those aspects of the currently active documents which are not + // preserved in the plain history entries (mainly scroll state and text data) + browser.__SS_restore_data = tabData.entries[activeIndex] || {}; + browser.__SS_restore_pageStyle = tabData.pageStyle || ""; + browser.__SS_restore_tab = aTab; + didStartLoad = true; + try { + // In order to work around certain issues in session history, we need to + // force session history to update its internal index and call reload + // instead of gotoIndex. See bug 597315. + browser.webNavigation.sessionHistory.getEntryAtIndex(activeIndex, true); + browser.webNavigation.sessionHistory.reloadCurrentEntry(); + // If the user prefers it, bypass cache and always load from the network, + // but only if restoring on demand, to prevent request flooding (since + // reloading will override the max tabs to restore concurrently mechanism). + // See Issue #1772 + if (TabRestoreQueue.prefs.restoreOnDemand) { + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + switch (this._cacheBehavior) { + case 2: // hard refresh + flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + browser.webNavigation.reload(flags); + break; + case 1: // soft refresh + browser.webNavigation.reload(flags); + break; + default: // 0 or other: use cache, so do nothing. + break; + } + } + } + catch (ex) { + // ignore page load errors + aTab.removeAttribute("busy"); + didStartLoad = false; + } + } + + // Handle userTypedValue. Setting userTypedValue seems to update gURLbar + // as needed. Calling loadURI will cancel form filling in restoreDocument + if (tabData.userTypedValue) { + browser.userTypedValue = tabData.userTypedValue; + if (tabData.userTypedClear) { + // Make it so that we'll enter restoreDocument on page load. We will + // fire SSTabRestored from there. We don't have any form data to restore + // so we can just set the URL to null. + browser.__SS_restore_data = { url: null }; + browser.__SS_restore_tab = aTab; + if (didStartLoad) + browser.stop(); + didStartLoad = true; + browser.loadURIWithFlags(tabData.userTypedValue, + Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP); + } + } + + // If we didn't start a load, then we won't reset this tab through the usual + // channel (via the progress listener), so reset the tab ourselves. We will + // also send SSTabRestored since this tab has technically been restored. + if (!didStartLoad) { + this._sendTabRestoredNotification(aTab); + this._resetTabRestoringState(aTab); + } + + return didStartLoad; + }, + + /** + * This _attempts_ to restore the next available tab. If the restore fails, + * then we will attempt the next one. + * There are conditions where this won't do anything: + * if we're in the process of quitting + * if there are no tabs to restore + * if we have already reached the limit for number of tabs to restore + */ + restoreNextTab: function() { + // If we call in here while quitting, we don't actually want to do anything + if (this._loadState == STATE_QUITTING) + return; + + // Don't exceed the maximum number of concurrent tab restores. + if (this._tabsRestoringCount >= this._maxConcurrentTabRestores) + return; + + let tab = TabRestoreQueue.shift(); + if (tab) { + let didStartLoad = this.restoreTab(tab); + // If we don't start a load in the restored tab (eg, no entries) then we + // want to attempt to restore the next tab. + if (!didStartLoad) + this.restoreNextTab(); + } + }, + + /** + * expands serialized history data into a session-history-entry instance + * @param aEntry + * Object containing serialized history data for a URL + * @param aIdMap + * Hash for ensuring unique frame IDs + * @returns nsISHEntry + */ + _deserializeHistoryEntry: + function(aEntry, aIdMap, aDocIdentMap) { + + var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. + createInstance(Ci.nsISHEntry); + + shEntry.setURI(this._getURIFromString(aEntry.url)); + 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 = this._getURIFromString(aEntry.referrer); + if (aEntry.isSrcdocEntry) + shEntry.srcdocData = aEntry.srcdocData; + + if (aEntry.cacheKey) { + var 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) + var 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) { + var scrollPos = (aEntry.scroll || "0,0").split(","); + scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; + shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); + } + + if (aEntry.postdata_b64) { + var postdata = atob(aEntry.postdata_b64); + var stream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + stream.setData(postdata, postdata.length); + shEntry.postData = stream; + } + + 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.shEntry); + childDocIdents = matchingEntry.childDocIdents; + } + } + + // The field aEntry.owner_b64 got renamed to aEntry.triggeringPricipal_b64 in + // Bug 1286472. To remain backward compatible we still have to support that + // field for a few cycles before we can remove it within Bug 1289785. + if (aEntry.owner_b64) { + aEntry.triggeringPrincipal_b64 = aEntry.owner_b64; + delete aEntry.owner_b64; + } + + if (aEntry.triggeringPrincipal_b64) { + var triggeringPrincipalInput = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + var binaryData = atob(aEntry.triggeringPrincipal_b64); + triggeringPrincipalInput.setData(binaryData, binaryData.length); + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIObjectInputStream); + binaryStream.setInputStream(triggeringPrincipalInput); + try { // Catch possible deserialization exceptions + shEntry.triggeringPrincipal = binaryStream.readObject(true); + } catch (ex) { debug(ex); } + } + + if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { + for (var i = 0; i < aEntry.children.length; i++) { + //XXXzpao Wallpaper patch for bug 514751 + 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; + }, + + /** + * Restore properties to a loaded document + */ + restoreDocument: function(aWindow, aBrowser, aEvent) { + // wait for the top frame to be loaded completely + if (!aEvent || !aEvent.originalTarget || !aEvent.originalTarget.defaultView || + aEvent.originalTarget.defaultView != aEvent.originalTarget.defaultView.top) { + return; + } + + // always call this before injecting content into a document! + function hasExpectedURL(aDocument, aURL) + !aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, ""); + + let selectedPageStyle = aBrowser.__SS_restore_pageStyle; + function restoreTextDataAndScrolling(aContent, aData, aPrefix) { + if (aData.formdata && hasExpectedURL(aContent.document, aData.url)) { + let formdata = aData.formdata; + + // handle backwards compatibility + // this is a migration from pre-firefox 15. cf. bug 742051 + if (!("xpath" in formdata || "id" in formdata)) { + formdata = { xpath: {}, id: {} }; + + for each (let [key, value] in Iterator(aData.formdata)) { + if (key.charAt(0) == "#") { + formdata.id[key.slice(1)] = value; + } else { + formdata.xpath[key] = value; + } + } + } + + // for about:sessionrestore we saved the field as JSON to avoid + // nested instances causing humongous sessionstore.js files. + // cf. bug 467409 + if (aData.url == "about:sessionrestore" && + "sessionData" in formdata.id && + typeof formdata.id["sessionData"] == "object") { + formdata.id["sessionData"] = + JSON.stringify(formdata.id["sessionData"]); + } + + // update the formdata + aData.formdata = formdata; + // merge the formdata + DocumentUtils.mergeFormData(aContent.document, formdata); + } + + if (aData.innerHTML) { + aWindow.setTimeout(function() { + if (aContent.document.designMode == "on" && + hasExpectedURL(aContent.document, aData.url) && + aContent.document.body) { + aContent.document.body.innerHTML = aData.innerHTML; + } + }, 0); + } + var match; + if (aData.scroll && (match = /(\d+),(\d+)/.exec(aData.scroll)) != null) { + aContent.scrollTo(match[1], match[2]); + } + Array.forEach(aContent.document.styleSheets, function(aSS) { + aSS.disabled = aSS.title && aSS.title != selectedPageStyle; + }); + for (var i = 0; i < aContent.frames.length; i++) { + if (aData.children && aData.children[i] && + hasExpectedURL(aContent.document, aData.url)) { + restoreTextDataAndScrolling(aContent.frames[i], aData.children[i], aPrefix + i + "|"); + } + } + } + + // don't restore text data and scrolling state if the user has navigated + // away before the loading completed (except for in-page navigation) + if (hasExpectedURL(aEvent.originalTarget, aBrowser.__SS_restore_data.url)) { + var content = aEvent.originalTarget.defaultView; + restoreTextDataAndScrolling(content, aBrowser.__SS_restore_data, ""); + aBrowser.markupDocumentViewer.authorStyleDisabled = selectedPageStyle == "_nostyle"; + } + + // notify the tabbrowser that this document has been completely restored + this._sendTabRestoredNotification(aBrowser.__SS_restore_tab); + + delete aBrowser.__SS_restore_data; + delete aBrowser.__SS_restore_pageStyle; + delete aBrowser.__SS_restore_tab; + }, + + /** + * Restore visibility and dimension features to a window + * @param aWindow + * Window reference + * @param aWinData + * Object containing session data for the window + */ + restoreWindowFeatures: function(aWindow, aWinData) { + var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[]; + WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) { + aWindow[aItem].visible = hidden.indexOf(aItem) == -1; + }); + + if (aWinData.isPopup) { + this._windows[aWindow.__SSi].isPopup = true; + if (aWindow.gURLBar) { + aWindow.gURLBar.readOnly = true; + aWindow.gURLBar.setAttribute("enablehistory", "false"); + } + } + else { + delete this._windows[aWindow.__SSi].isPopup; + if (aWindow.gURLBar) { + aWindow.gURLBar.readOnly = false; + aWindow.gURLBar.setAttribute("enablehistory", "true"); + } + } + + var _this = this; + aWindow.setTimeout(function() { + _this.restoreDimensions.apply(_this, [aWindow, + +aWinData.width || 0, + +aWinData.height || 0, + "screenX" in aWinData ? +aWinData.screenX : NaN, + "screenY" in aWinData ? +aWinData.screenY : NaN, + aWinData.sizemode || "", aWinData.sidebar || ""]); + }, 0); + }, + + /** + * Restore a window's dimensions + * @param aWidth + * Window width + * @param aHeight + * Window height + * @param aLeft + * Window left + * @param aTop + * Window top + * @param aSizeMode + * Window size mode (eg: maximized) + * @param aSidebar + * Sidebar command + */ + restoreDimensions: function(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) { + var win = aWindow; + var _this = this; + function win_(aName) { return _this._getWindowDimension(win, aName); } + + // Find available space on the screen where this window is being placed + let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight); + if (screen && !this._prefBranch.getBoolPref("sessionstore.exactPos")) { + let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {}; + screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight); + + // Screen X/Y are based on the origin of the screen's desktop-pixel coordinate space + let screenLeftCss = screenLeft.value; + let screenTopCss = screenTop.value; + + // Convert the screen's device pixel dimensions to CSS px dimensions + screen.GetAvailRect(screenLeft, screenTop, screenWidth, screenHeight); + let cssToDevScale = screen.defaultCSSScaleFactor; + let screenRightCss = screenLeftCss + screenWidth.value / cssToDevScale; + let screenBottomCss = screenTopCss + screenHeight.value / cssToDevScale; + + // Pull the window within the screen's bounds. + // First, ensure the left edge is on-screen + if (aLeft < screenLeftCss) { + aLeft = screenLeftCss; + } + // Then check the resulting right edge, and reduce it if necessary. + let right = aLeft + aWidth; + if (right > screenRightCss) { + right = screenRightCss; + // See if we can move the left edge leftwards to maintain width. + if (aLeft > screenLeftCss) { + aLeft = Math.max(right - aWidth, screenLeftCss); + } + } + // Finally, update aWidth to account for the adjusted left and right edges. + aWidth = right - aLeft; + + // Do the same in the vertical dimension. + // First, ensure the top edge is on-screen + if (aTop < screenTopCss) { + aTop = screenTopCss; + } + // Then check the resulting right edge, and reduce it if necessary. + let bottom = aTop + aHeight; + if (bottom > screenBottomCss) { + bottom = screenBottomCss; + // See if we can move the top edge upwards to maintain height. + if (aTop > screenTopCss) { + aTop = Math.max(bottom - aHeight, screenTopCss); + } + } + // Finally, update aHeight to account for the adjusted top and bottom edges. + aHeight = bottom - aTop; + } + + // Only modify those aspects which aren't correct yet + if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) { + aWindow.moveTo(aLeft, aTop); + } + if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) { + // Don't resize the window if it's currently maximized and we would + // maximize it again shortly after. + if (aSizeMode != "maximized" || win_("sizemode") != "maximized") { + aWindow.resizeTo(aWidth, aHeight); + } + } + + // Restore window state + if (aSizeMode && win_("sizemode") != aSizeMode) + { + switch (aSizeMode) + { + case "maximized": + aWindow.maximize(); + break; + case "minimized": + aWindow.minimize(); + break; + case "normal": + aWindow.restore(); + break; + } + } + var sidebar = aWindow.document.getElementById("sidebar-box"); + if (sidebar.getAttribute("sidebarcommand") != aSidebar) { + aWindow.toggleSidebar(aSidebar); + } + // since resizing/moving a window brings it to the foreground, + // we might want to re-focus the last focused window + if (this.windowToFocus) { + this.windowToFocus.focus(); + } + }, + + /** + * Restores cookies + * @param aCookies + * Array of cookie objects + */ + restoreCookies: function(aCookies) { + // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision + var MAX_EXPIRY = Math.pow(2, 62); + for (let i = 0; i < aCookies.length; i++) { + var cookie = aCookies[i]; + try { + Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "", + cookie.value, !!cookie.secure, !!cookie.httponly, true, + "expiry" in cookie ? cookie.expiry : MAX_EXPIRY, {}); + } + catch (ex) { Cu.reportError(ex); } // don't let a single cookie stop recovering + } + }, + + /* ........ Disk Access .............. */ + + /** + * save state delayed by N ms + * marks window as dirty (i.e. data update can't be skipped) + * @param aWindow + * Window reference + * @param aDelay + * Milliseconds to delay + */ + saveStateDelayed: function(aWindow, aDelay) { + if (aWindow) { + this._dirtyWindows[aWindow.__SSi] = true; + } + + if (!this._saveTimer) { + // interval until the next disk operation is allowed + var minimalDelay = this._lastSaveTime + this._interval - Date.now(); + + // if we have to wait, set a timer, otherwise saveState directly + aDelay = Math.max(minimalDelay, aDelay || 2000); + if (aDelay > 0) { + this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT); + } + else { + this.saveState(); + } + } + }, + + /** + * save state to disk + * @param aUpdateAll + * Bool update all windows + */ + saveState: function(aUpdateAll) { + // If crash recovery is disabled, we only want to resume with pinned tabs + // if we crash. + let pinnedOnly = this._loadState == STATE_RUNNING && !this._resume_from_crash; + + var oState = this._getCurrentState(aUpdateAll, pinnedOnly); + if (!oState) { + return; + } + + // Forget about private windows. + for (let i = oState.windows.length - 1; i >= 0; i--) { + if (oState.windows[i].isPrivate) { + oState.windows.splice(i, 1); + if (oState.selectedWindow >= i) { + oState.selectedWindow--; + } + } + } + + for (let i = oState._closedWindows.length - 1; i >= 0; i--) { + if (oState._closedWindows[i].isPrivate) { + oState._closedWindows.splice(i, 1); + } + } + + // We want to restore closed windows that are marked with _shouldRestore. + // We're doing this here because we want to control this only when saving + // the file. + while (oState._closedWindows.length) { + let i = oState._closedWindows.length - 1; + if (oState._closedWindows[i]._shouldRestore) { + delete oState._closedWindows[i]._shouldRestore; + oState.windows.unshift(oState._closedWindows.pop()); + } + else { + // We only need to go until we hit !needsRestore since we're going in reverse + break; + } + } + + if (pinnedOnly) { + // Save original resume_session_once preference for when quiting browser, + // otherwise session will be restored next time browser starts and we + // only want it to be restored in the case of a crash. + if (this._resume_session_once_on_shutdown == null) { + this._resume_session_once_on_shutdown = + this._prefBranch.getBoolPref("sessionstore.resume_session_once"); + this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); + // flush the preference file so preference will be saved in case of a crash + Services.prefs.savePrefFile(null); + } + } + + // Persist the last session if we deferred restoring it + if (this._lastSessionState) + oState.lastSessionState = this._lastSessionState; + + // Make sure that we keep the previous session if we started with a single + // private window and no non-private windows have been opened, yet. + if (this._deferredInitialState) { + oState.windows = this._deferredInitialState.windows || []; + } + + this._saveStateObject(oState); + }, + + /** + * write a state object to disk + */ + _saveStateObject: function(aStateObj) { + let data = this._toJSONString(aStateObj); + + let stateString = this._createSupportsString(data); + Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); + data = stateString.data; + + // Don't touch the file if an observer has deleted all state data. + if (!data) { + return; + } + + let promise; + // If "sessionstore.resume_from_crash" is true, attempt to backup the + // session file first, before writing to it. + if (this._resume_from_crash) { + // Note that we do not have race conditions here as _SessionFile + // guarantees that any I/O operation is completed before proceeding to + // the next I/O operation. + // Note backup happens only once, on initial save. + promise = this._backupSessionFileOnce; + } else { + promise = Promise.resolve(); + } + + // Attempt to write to the session file (potentially, depending on + // "sessionstore.resume_from_crash" preference, after successful backup). + promise = promise.then(function onSuccess() { + // Write (atomically) to a session file, using a tmp file. + return _SessionFile.write(data); + }); + + // Once the session file is successfully updated, save the time stamp of the + // last save and notify the observers. + promise = promise.then(() => { + this._lastSaveTime = Date.now(); + Services.obs.notifyObservers(null, "sessionstore-state-write-complete", + ""); + }); + }, + + /* ........ Auxiliary Functions .............. */ + + // Wrap a string as a nsISupports + _createSupportsString: function(aData) { + let string = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + string.data = aData; + return string; + }, + + /** + * call a callback for all currently opened browser windows + * (might miss the most recent one) + * @param aFunc + * Callback each window is passed to + */ + _forEachBrowserWindow: function(aFunc) { + var windowsEnum = Services.wm.getEnumerator("navigator:browser"); + + while (windowsEnum.hasMoreElements()) { + var window = windowsEnum.getNext(); + if (window.__SSi && !window.closed) { + aFunc.call(this, window); + } + } + }, + + /** + * Returns most recent window + * @returns Window reference + */ + _getMostRecentBrowserWindow: function() { + var win = Services.wm.getMostRecentWindow("navigator:browser"); + if (!win) + return null; + if (!win.closed) + return win; + +#ifdef BROKEN_WM_Z_ORDER + win = null; + var windowsEnum = Services.wm.getEnumerator("navigator:browser"); + // this is oldest to newest, so this gets a bit ugly + while (windowsEnum.hasMoreElements()) { + let nextWin = windowsEnum.getNext(); + if (!nextWin.closed) + win = nextWin; + } + return win; +#else + var windowsEnum = + Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true); + while (windowsEnum.hasMoreElements()) { + win = windowsEnum.getNext(); + if (!win.closed) + return win; + } + return null; +#endif + }, + + /** + * Calls onClose for windows that are determined to be closed but aren't + * destroyed yet, which would otherwise cause getBrowserState and + * setBrowserState to treat them as open windows. + */ + _handleClosedWindows: function() { + var windowsEnum = Services.wm.getEnumerator("navigator:browser"); + + while (windowsEnum.hasMoreElements()) { + var window = windowsEnum.getNext(); + if (window.closed) { + this.onClose(window); + } + } + }, + + /** + * open a new browser window for a given session state + * called when restoring a multi-window session + * @param aState + * Object containing session data + */ + _openWindowWithState: function(aState) { + var argString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + argString.data = ""; + + // Build feature string + let features = "chrome,dialog=no,macsuppressanimation,all"; + let winState = aState.windows[0]; + WINDOW_ATTRIBUTES.forEach(function(aFeature) { + // Use !isNaN as an easy way to ignore sizemode and check for numbers + if (aFeature in winState && !isNaN(winState[aFeature])) + features += "," + aFeature + "=" + winState[aFeature]; + }); + + if (winState.isPrivate) { + features += ",private"; + } + + var window = + Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"), + "_blank", features, argString); + + do { + var ID = "window" + Math.random(); + } while (ID in this._statesToRestore); + this._statesToRestore[(window.__SS_restoreID = ID)] = aState; + + return window; + }, + + /** + * Whether or not to resume session, if not recovering from a crash. + * @returns bool + */ + _doResumeSession: function() { + return this._prefBranch.getIntPref("startup.page") == 3 || + this._prefBranch.getBoolPref("sessionstore.resume_session_once"); + }, + + /** + * whether the user wants to load any other page at startup + * (except the homepage) - needed for determining whether to overwrite the current tabs + * C.f.: nsBrowserContentHandler's defaultArgs implementation. + * @returns bool + */ + _isCmdLineEmpty: function(aWindow, aState) { + var pinnedOnly = aState.windows && + aState.windows.every(function(win) + win.tabs.every(function(tab) tab.pinned)); + + let hasFirstArgument = aWindow.arguments && aWindow.arguments[0]; + if (!pinnedOnly) { + let defaultArgs = Cc["@mozilla.org/browser/clh;1"]. + getService(Ci.nsIBrowserHandler).defaultArgs; + if (aWindow.arguments && + aWindow.arguments[0] && + aWindow.arguments[0] == defaultArgs) + hasFirstArgument = false; + } + + return !hasFirstArgument; + }, + + /** + * don't save sensitive data if the user doesn't want to + * (distinguishes between encrypted and non-encrypted sites) + * @param aIsHTTPS + * Bool is encrypted + * @param aUseDefaultPref + * don't do normal check for deferred + * @returns bool + */ + checkPrivacyLevel: function(aIsHTTPS, aUseDefaultPref) { + let pref = "sessionstore.privacy_level"; + // If we're in the process of quitting and we're not autoresuming the session + // then we should treat it as a deferred session. We have a different privacy + // pref for that case. + if (!aUseDefaultPref && this._loadState == STATE_QUITTING && !this._doResumeSession()) + pref = "sessionstore.privacy_level_deferred"; + return this._prefBranch.getIntPref(pref) < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL); + }, + + /** + * on popup windows, the XULWindow's attributes seem not to be set correctly + * we use thus JSDOMWindow attributes for sizemode and normal window attributes + * (and hope for reasonable values when maximized/minimized - since then + * outerWidth/outerHeight aren't the dimensions of the restored window) + * @param aWindow + * Window reference + * @param aAttribute + * String sizemode | width | height | other window attribute + * @returns string + */ + _getWindowDimension: function(aWindow, aAttribute) { + if (aAttribute == "sizemode") { + switch (aWindow.windowState) { + case aWindow.STATE_FULLSCREEN: + case aWindow.STATE_MAXIMIZED: + return "maximized"; + case aWindow.STATE_MINIMIZED: + return "minimized"; + default: + return "normal"; + } + } + + var dimension; + switch (aAttribute) { + case "width": + dimension = aWindow.outerWidth; + break; + case "height": + dimension = aWindow.outerHeight; + break; + default: + dimension = aAttribute in aWindow ? aWindow[aAttribute] : ""; + break; + } + + if (aWindow.windowState == aWindow.STATE_NORMAL) { + return dimension; + } + return aWindow.document.documentElement.getAttribute(aAttribute) || dimension; + }, + + /** + * Get nsIURI from string + * @param string + * @returns nsIURI + */ + _getURIFromString: function(aString) { + return Services.io.newURI(aString, null, null); + }, + + /** + * @param aState is a session state + * @param aRecentCrashes is the number of consecutive crashes + * @returns whether a restore page will be needed for the session state + */ + _needsRestorePage: function(aState, aRecentCrashes) { + const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000; + + // don't display the page when there's nothing to restore + let winData = aState.windows || null; + if (!winData || winData.length == 0) + return false; + + // don't wrap a single about:sessionrestore page + if (winData.length == 1 && winData[0].tabs && + winData[0].tabs.length == 1 && winData[0].tabs[0].entries && + winData[0].tabs[0].entries.length == 1 && + winData[0].tabs[0].entries[0].url == "about:sessionrestore") + return false; + + // don't automatically restore in Safe Mode + if (Services.appinfo.inSafeMode) + return true; + + let max_resumed_crashes = + this._prefBranch.getIntPref("sessionstore.max_resumed_crashes"); + let sessionAge = aState.session && aState.session.lastUpdate && + (Date.now() - aState.session.lastUpdate); + + return max_resumed_crashes != -1 && + (aRecentCrashes > max_resumed_crashes || + sessionAge && sessionAge >= SIX_HOURS_IN_MS); + }, + + /** + * Determine if the tab state we're passed is something we should save. This + * is used when closing a tab or closing a window with a single tab + * + * @param aTabState + * The current tab state + * @returns boolean + */ + _shouldSaveTabState: function(aTabState) { + // If the tab has only a transient about: history entry, no other + // session history, and no userTypedValue, then we don't actually want to + // store this tab's data. + return aTabState.entries.length && + !(aTabState.entries.length == 1 && + (aTabState.entries[0].url == "about:blank" || + aTabState.entries[0].url == "about:newtab") && + !aTabState.userTypedValue); + }, + + /** + * Determine if we can restore history into this tab. + * This will be false when a tab has been removed (usually between + * restoreHistoryPrecursor && restoreHistory) or if the tab is still marked + * as loading. + * + * @param aTab + * @returns boolean + */ + _canRestoreTabHistory: function(aTab) { + return aTab.parentNode && aTab.linkedBrowser && + aTab.linkedBrowser.__SS_tabStillLoading; + }, + + /** + * This is going to take a state as provided at startup (via + * nsISessionStartup.state) and split it into 2 parts. The first part + * (defaultState) will be a state that should still be restored at startup, + * while the second part (state) is a state that should be saved for later. + * defaultState will be comprised of windows with only pinned tabs, extracted + * from state. It will contain the cookies that go along with the history + * entries in those tabs. It will also contain window position information. + * + * defaultState will be restored at startup. state will be placed into + * this._lastSessionState and will be kept in case the user explicitly wants + * to restore the previous session (publicly exposed as restoreLastSession). + * + * @param state + * The state, presumably from nsISessionStartup.state + * @returns [defaultState, state] + */ + _prepDataForDeferredRestore: function(state) { + // Make sure that we don't modify the global state as provided by + // nsSessionStartup.state. Converting the object to a JSON string and + // parsing it again is the easiest way to do that, although not the most + // efficient one. Deferred sessions that don't have automatic session + // restore enabled tend to be a lot smaller though so that this shouldn't + // be a big perf hit. + state = JSON.parse(JSON.stringify(state)); + + let defaultState = { windows: [], selectedWindow: 1 }; + + state.selectedWindow = state.selectedWindow || 1; + + // Look at each window, remove pinned tabs, adjust selectedindex, + // remove window if necessary. + for (let wIndex = 0; wIndex < state.windows.length;) { + let window = state.windows[wIndex]; + window.selected = window.selected || 1; + // We're going to put the state of the window into this object + let pinnedWindowState = { tabs: [], cookies: []}; + for (let tIndex = 0; tIndex < window.tabs.length;) { + if (window.tabs[tIndex].pinned) { + // Adjust window.selected + if (tIndex + 1 < window.selected) + window.selected -= 1; + else if (tIndex + 1 == window.selected) + pinnedWindowState.selected = pinnedWindowState.tabs.length + 2; + // + 2 because the tab isn't actually in the array yet + + // Now add the pinned tab to our window + pinnedWindowState.tabs = + pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1)); + // We don't want to increment tIndex here. + continue; + } + tIndex++; + } + + // At this point the window in the state object has been modified (or not) + // We want to build the rest of this new window object if we have pinnedTabs. + if (pinnedWindowState.tabs.length) { + // First get the other attributes off the window + WINDOW_ATTRIBUTES.forEach(function(attr) { + if (attr in window) { + pinnedWindowState[attr] = window[attr]; + delete window[attr]; + } + }); + // We're just copying position data into the pinned window. + // Not copying over: + // - _closedTabs + // - extData + // - isPopup + // - hidden + + // Assign a unique ID to correlate the window to be opened with the + // remaining data + window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID + = "" + Date.now() + Math.random(); + + // Extract the cookies that belong with each pinned tab + this._splitCookiesFromWindow(window, pinnedWindowState); + + // Actually add this window to our defaultState + defaultState.windows.push(pinnedWindowState); + // Remove the window from the state if it doesn't have any tabs + if (!window.tabs.length) { + if (wIndex + 1 <= state.selectedWindow) + state.selectedWindow -= 1; + else if (wIndex + 1 == state.selectedWindow) + defaultState.selectedIndex = defaultState.windows.length + 1; + + state.windows.splice(wIndex, 1); + // We don't want to increment wIndex here. + continue; + } + + + } + wIndex++; + } + + return [defaultState, state]; + }, + + /** + * Splits out the cookies from aWinState into aTargetWinState based on the + * tabs that are in aTargetWinState. + * This alters the state of aWinState and aTargetWinState. + */ + _splitCookiesFromWindow: + function(aWinState, aTargetWinState) { + if (!aWinState.cookies || !aWinState.cookies.length) + return; + + // Get the hosts for history entries in aTargetWinState + let cookieHosts = {}; + aTargetWinState.tabs.forEach(function(tab) { + tab.entries.forEach(function(entry) { + this._extractHostsForCookiesFromEntry(entry, cookieHosts, false); + }, this); + }, this); + + // By creating a regex we reduce overhead and there is only one loop pass + // through either array (cookieHosts and aWinState.cookies). + let hosts = Object.keys(cookieHosts).join("|").replace("\\.", "\\.", "g"); + // If we don't actually have any hosts, then we don't want to do anything. + if (!hosts.length) + return; + let cookieRegex = new RegExp(".*(" + hosts + ")"); + for (let cIndex = 0; cIndex < aWinState.cookies.length;) { + if (cookieRegex.test(aWinState.cookies[cIndex].host)) { + aTargetWinState.cookies = + aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1)); + continue; + } + cIndex++; + } + }, + + /** + * Converts a JavaScript object into a JSON string + * (see http://www.json.org/ for more information). + * + * The inverse operation consists of JSON.parse(JSON_string). + * + * @param aJSObject is the object to be converted + * @returns the object's JSON representation + */ + _toJSONString: function(aJSObject) { + return JSON.stringify(aJSObject); + }, + + _sendRestoreCompletedNotifications: function() { + // not all windows restored, yet + if (this._restoreCount > 1) { + this._restoreCount--; + return; + } + + // observers were already notified + if (this._restoreCount == -1) + return; + + // This was the last window restored at startup, notify observers. + Services.obs.notifyObservers(null, + this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED, + ""); + + this._browserSetState = false; + this._restoreCount = -1; + }, + + /** + * Set the given window's busy state + * @param aWindow the window + * @param aValue the window's busy state + */ + _setWindowStateBusyValue: + function(aWindow, aValue) { + + this._windows[aWindow.__SSi].busy = aValue; + + // Keep the to-be-restored state in sync because that is returned by + // getWindowState() as long as the window isn't loaded, yet. + if (!this._isWindowLoaded(aWindow)) { + let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0]; + stateToRestore.busy = aValue; + } + }, + + /** + * Set the given window's state to 'not busy'. + * @param aWindow the window + */ + _setWindowStateReady: function(aWindow) { + this._setWindowStateBusyValue(aWindow, false); + this._sendWindowStateEvent(aWindow, "Ready"); + }, + + /** + * Set the given window's state to 'busy'. + * @param aWindow the window + */ + _setWindowStateBusy: function(aWindow) { + this._setWindowStateBusyValue(aWindow, true); + this._sendWindowStateEvent(aWindow, "Busy"); + }, + + /** + * Dispatch an SSWindowState_____ event for the given window. + * @param aWindow the window + * @param aType the type of event, SSWindowState will be prepended to this string + */ + _sendWindowStateEvent: function(aWindow, aType) { + let event = aWindow.document.createEvent("Events"); + event.initEvent("SSWindowState" + aType, true, false); + aWindow.dispatchEvent(event); + }, + + /** + * Dispatch the SSTabRestored event for the given tab. + * @param aTab the which has been restored + */ + _sendTabRestoredNotification: function(aTab) { + let event = aTab.ownerDocument.createEvent("Events"); + event.initEvent("SSTabRestored", true, false); + aTab.dispatchEvent(event); + }, + + /** + * @param aWindow + * Window reference + * @returns whether this window's data is still cached in _statesToRestore + * because it's not fully loaded yet + */ + _isWindowLoaded: function(aWindow) { + return !aWindow.__SS_restoreID; + }, + + /** + * Replace "Loading..." with the tab label (with minimal side-effects) + * @param aString is the string the title is stored in + * @param aTabbrowser is a tabbrowser object, containing aTab + * @param aTab is the tab whose title we're updating & using + * + * @returns aString that has been updated with the new title + */ + _replaceLoadingTitle : function(aString, aTabbrowser, aTab) { + if (aString == aTabbrowser.mStringBundle.getString("tabs.connecting")) { + aTabbrowser.setTabTitle(aTab); + [aString, aTab.label] = [aTab.label, aString]; + } + return aString; + }, + + /** + * Resize this._closedWindows to the value of the pref, except in the case + * where we don't have any non-popup windows on Windows and Linux. Then we must + * resize such that we have at least one non-popup window. + */ + _capClosedWindows : function() { + if (this._closedWindows.length <= this._max_windows_undo) + return; + let spliceTo = this._max_windows_undo; + let normalWindowIndex = 0; + // try to find a non-popup window in this._closedWindows + while (normalWindowIndex < this._closedWindows.length && + !!this._closedWindows[normalWindowIndex].isPopup) + normalWindowIndex++; + if (normalWindowIndex >= this._max_windows_undo) + spliceTo = normalWindowIndex + 1; + this._closedWindows.splice(spliceTo, this._closedWindows.length); + }, + + _clearRestoringWindows: function() { + for (let i = 0; i < this._closedWindows.length; i++) { + delete this._closedWindows[i]._shouldRestore; + } + }, + + /** + * Reset state to prepare for a new session state to be restored. + */ + _resetRestoringState: function() { + TabRestoreQueue.reset(); + this._tabsRestoringCount = 0; + }, + + /** + * Reset the restoring state for a particular tab. This will be called when + * removing a tab or when a tab needs to be reset (it's being overwritten). + * + * @param aTab + * The tab that will be "reset" + */ + _resetTabRestoringState: function(aTab) { + let window = aTab.ownerDocument.defaultView; + let browser = aTab.linkedBrowser; + + // Keep the tab's previous state for later in this method + let previousState = browser.__SS_restoreState; + + // The browser is no longer in any sort of restoring state. + delete browser.__SS_restoreState; + + aTab.removeAttribute("pending"); + browser.removeAttribute("pending"); + + // We want to decrement window.__SS_tabsToRestore here so that we always + // decrement it AFTER a tab is done restoring or when a tab gets "reset". + window.__SS_tabsToRestore--; + + // Remove the progress listener if we should. + this._removeTabsProgressListener(window); + + if (previousState == TAB_STATE_RESTORING) { + if (this._tabsRestoringCount) + this._tabsRestoringCount--; + } + else if (previousState == TAB_STATE_NEEDS_RESTORE) { + // Make sure the session history listener is removed. This is normally + // done in restoreTab, but this tab is being removed before that gets called. + this._removeSHistoryListener(aTab); + + // Make sure that the tab is removed from the list of tabs to restore. + // Again, this is normally done in restoreTab, but that isn't being called + // for this tab. + TabRestoreQueue.remove(aTab); + } + }, + + /** + * Add the tabs progress listener to the window if it isn't already + * + * @param aWindow + * The window to add our progress listener to + */ + _ensureTabsProgressListener: function(aWindow) { + let tabbrowser = aWindow.gBrowser; + if (tabbrowser.mTabsProgressListeners.indexOf(gRestoreTabsProgressListener) == -1) + tabbrowser.addTabsProgressListener(gRestoreTabsProgressListener); + }, + + /** + * Attempt to remove the tabs progress listener from the window. + * + * @param aWindow + * The window from which to remove our progress listener from + */ + _removeTabsProgressListener: function(aWindow) { + // If there are no tabs left to restore (or restoring) in this window, then + // we can safely remove the progress listener from this window. + if (!aWindow.__SS_tabsToRestore) + aWindow.gBrowser.removeTabsProgressListener(gRestoreTabsProgressListener); + }, + + /** + * Remove the session history listener from the tab's browser if there is one. + * + * @param aTab + * The tab who's browser to remove the listener + */ + _removeSHistoryListener: function(aTab) { + let browser = aTab.linkedBrowser; + if (browser.__SS_shistoryListener) { + browser.webNavigation.sessionHistory. + removeSHistoryListener(browser.__SS_shistoryListener); + delete browser.__SS_shistoryListener; + } + } +}; + +/** + * Priority queue that keeps track of a list of tabs to restore and returns + * the tab we should restore next, based on priority rules. We decide between + * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only + * restored with restore_hidden_tabs=true. + */ +var TabRestoreQueue = { + // The separate buckets used to store tabs. + tabs: {priority: [], visible: [], hidden: []}, + + // Preferences used by the TabRestoreQueue to determine which tabs + // are restored automatically and which tabs will be on-demand. + prefs: { + // Lazy getter that returns whether tabs are restored on demand. + get restoreOnDemand() { + let updateValue = () => { + let value = Services.prefs.getBoolPref(PREF); + let definition = {value: value, configurable: true}; + Object.defineProperty(this, "restoreOnDemand", definition); + return value; + } + + const PREF = "browser.sessionstore.restore_on_demand"; + Services.prefs.addObserver(PREF, updateValue, false); + return updateValue(); + }, + + // Lazy getter that returns whether pinned tabs are restored on demand. + get restorePinnedTabsOnDemand() { + let updateValue = () => { + let value = Services.prefs.getBoolPref(PREF); + let definition = {value: value, configurable: true}; + Object.defineProperty(this, "restorePinnedTabsOnDemand", definition); + return value; + } + + const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand"; + Services.prefs.addObserver(PREF, updateValue, false); + return updateValue(); + }, + + // Lazy getter that returns whether we should restore hidden tabs. + get restoreHiddenTabs() { + let updateValue = () => { + let value = Services.prefs.getBoolPref(PREF); + let definition = {value: value, configurable: true}; + Object.defineProperty(this, "restoreHiddenTabs", definition); + return value; + } + + const PREF = "browser.sessionstore.restore_hidden_tabs"; + Services.prefs.addObserver(PREF, updateValue, false); + return updateValue(); + } + }, + + // Resets the queue and removes all tabs. + reset: function() { + this.tabs = {priority: [], visible: [], hidden: []}; + }, + + // Adds a tab to the queue and determines its priority bucket. + add: function(tab) { + let {priority, hidden, visible} = this.tabs; + + if (tab.pinned) { + priority.push(tab); + } else if (tab.hidden) { + hidden.push(tab); + } else { + visible.push(tab); + } + }, + + // Removes a given tab from the queue, if it's in there. + remove: function(tab) { + let {priority, hidden, visible} = this.tabs; + + // We'll always check priority first since we don't + // have an indicator if a tab will be there or not. + let set = priority; + let index = set.indexOf(tab); + + if (index == -1) { + set = tab.hidden ? hidden : visible; + index = set.indexOf(tab); + } + + if (index > -1) { + set.splice(index, 1); + } + }, + + // Returns and removes the tab with the highest priority. + shift: function() { + let set; + let {priority, hidden, visible} = this.tabs; + + let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs; + let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand); + if (restorePinned && priority.length) { + set = priority; + } else if (!restoreOnDemand) { + if (visible.length) { + set = visible; + } else if (this.prefs.restoreHiddenTabs && hidden.length) { + set = hidden; + } + } + + return set && set.shift(); + }, + + // Moves a given tab from the 'hidden' to the 'visible' bucket. + hiddenToVisible: function(tab) { + let {hidden, visible} = this.tabs; + let index = hidden.indexOf(tab); + + if (index > -1) { + hidden.splice(index, 1); + visible.push(tab); + } else { + throw new Error("restore queue: hidden tab not found"); + } + }, + + // Moves a given tab from the 'visible' to the 'hidden' bucket. + visibleToHidden: function(tab) { + let {visible, hidden} = this.tabs; + let index = visible.indexOf(tab); + + if (index > -1) { + visible.splice(index, 1); + hidden.push(tab); + } else { + throw new Error("restore queue: visible tab not found"); + } + } +}; + +// A map storing a closed window's state data until it goes aways (is GC'ed). +// This ensures that API clients can still read (but not write) states of +// windows they still hold a reference to but we don't. +var DyingWindowCache = { + _data: new WeakMap(), + + has: function(window) { + return this._data.has(window); + }, + + get: function(window) { + return this._data.get(window); + }, + + set: function(window, data) { + this._data.set(window, data); + }, + + remove: function(window) { + this._data.delete(window); + } +}; + +// A set of tab attributes to persist. We will read a given list of tab +// attributes when collecting tab data and will re-set those attributes when +// the given tab data is restored to a new tab. +var TabAttributes = { + _attrs: new Set(), + + // We never want to directly read or write those attributes. + // 'image' should not be accessed directly but handled by using the + // gBrowser.getIcon()/setIcon() methods. + // 'pending' is used internal by sessionstore and managed accordingly. + // 'skipbackgroundnotify' is used internal by tabbrowser.xml. + _skipAttrs: new Set(["image", "pending", "skipbackgroundnotify"]), + + persist: function(name) { + if (this._attrs.has(name) || this._skipAttrs.has(name)) { + return false; + } + + this._attrs.add(name); + return true; + }, + + get: function(tab) { + let data = {}; + + for (let name of this._attrs) { + if (tab.hasAttribute(name)) { + data[name] = tab.getAttribute(name); + } + } + + return data; + }, + + set: function(tab, data = {}) { + // Clear attributes. + for (let name of this._attrs) { + tab.removeAttribute(name); + } + + // Set attributes. + for (let name in data) { + tab.setAttribute(name, data[name]); + } + } +}; + +// This is used to help meter the number of restoring tabs. This is the control +// point for telling the next tab to restore. It gets attached to each gBrowser +// via gBrowser.addTabsProgressListener +var gRestoreTabsProgressListener = { + onStateChange: function(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + // Ignore state changes on browsers that we've already restored and state + // changes that aren't applicable. + if (aBrowser.__SS_restoreState && + aBrowser.__SS_restoreState == TAB_STATE_RESTORING && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { + // We need to reset the tab before starting the next restore. + let win = aBrowser.ownerDocument.defaultView; + let tab = win.gBrowser.getTabForBrowser(aBrowser); + SessionStoreInternal._resetTabRestoringState(tab); + SessionStoreInternal.restoreNextTab(); + } + } +}; + +// A SessionStoreSHistoryListener will be attached to each browser before it is +// restored. We need to catch reloads that occur before the tab is restored +// because otherwise, docShell will reload an old URI (usually about:blank). +function SessionStoreSHistoryListener(aTab) { + this.tab = aTab; +} +SessionStoreSHistoryListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsISHistoryListener, + Ci.nsISupportsWeakReference + ]), + browser: null, + OnHistoryNewEntry: function(aNewURI) { }, + OnHistoryGoBack: function(aBackURI) { return true; }, + OnHistoryGoForward: function(aForwardURI) { return true; }, + OnHistoryGotoIndex: function(aIndex, aGotoURI) { return true; }, + OnHistoryPurge: function(aNumEntries) { return true; }, + OnHistoryReload: function(aReloadURI, aReloadFlags) { + // On reload, we want to make sure that session history loads the right + // URI. In order to do that, we will juet call restoreTab. That will remove + // the history listener and load the right URI. + SessionStoreInternal.restoreTab(this.tab); + // Returning false will stop the load that docshell is attempting. + return false; + } +} + +// See toolkit/forgetaboutsite/ForgetAboutSite.jsm +String.prototype.hasRootDomain = function hasRootDomain(aDomain) { + let index = this.indexOf(aDomain); + if (index == -1) + return false; + + if (this == aDomain) + return true; + + let prevChar = this[index - 1]; + return (index == (this.length - aDomain.length)) && + (prevChar == "." || prevChar == "/"); +} diff --git a/browser/components/sessionstore/XPathGenerator.jsm b/browser/components/sessionstore/XPathGenerator.jsm new file mode 100644 index 000000000..d0639ebb4 --- /dev/null +++ b/browser/components/sessionstore/XPathGenerator.jsm @@ -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/. */ + +this.EXPORTED_SYMBOLS = ["XPathGenerator"]; + +this.XPathGenerator = { + // these two hashes should be kept in sync + namespaceURIs: { "xhtml": "http://www.w3.org/1999/xhtml" }, + namespacePrefixes: { "http://www.w3.org/1999/xhtml": "xhtml" }, + + /** + * Generates an approximate XPath query to an (X)HTML node + */ + generate: function(aNode) { + // have we reached the document node already? + if (!aNode.parentNode) + return ""; + + // Access localName, namespaceURI just once per node since it's expensive. + let nNamespaceURI = aNode.namespaceURI; + let nLocalName = aNode.localName; + + let prefix = this.namespacePrefixes[nNamespaceURI] || null; + let tag = (prefix ? prefix + ":" : "") + this.escapeName(nLocalName); + + // stop once we've found a tag with an ID + if (aNode.id) + return "//" + tag + "[@id=" + this.quoteArgument(aNode.id) + "]"; + + // count the number of previous sibling nodes of the same tag + // (and possible also the same name) + let count = 0; + let nName = aNode.name || null; + for (let n = aNode; (n = n.previousSibling); ) + if (n.localName == nLocalName && n.namespaceURI == nNamespaceURI && + (!nName || n.name == nName)) + count++; + + // recurse until hitting either the document node or an ID'd node + return this.generate(aNode.parentNode) + "/" + tag + + (nName ? "[@name=" + this.quoteArgument(nName) + "]" : "") + + (count ? "[" + (count + 1) + "]" : ""); + }, + + /** + * Resolves an XPath query generated by XPathGenerator.generate + */ + resolve: function(aDocument, aQuery) { + let xptype = Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE; + return aDocument.evaluate(aQuery, aDocument, this.resolveNS, xptype, null).singleNodeValue; + }, + + /** + * Namespace resolver for the above XPath resolver + */ + resolveNS: function(aPrefix) { + return XPathGenerator.namespaceURIs[aPrefix] || null; + }, + + /** + * @returns valid XPath for the given node (usually just the local name itself) + */ + escapeName: function(aName) { + // we can't just use the node's local name, if it contains + // special characters (cf. bug 485482) + return /^\w+$/.test(aName) ? aName : + "*[local-name()=" + this.quoteArgument(aName) + "]"; + }, + + /** + * @returns a properly quoted string to insert into an XPath query + */ + quoteArgument: function(aArg) { + return !/'/.test(aArg) ? "'" + aArg + "'" : + !/"/.test(aArg) ? '"' + aArg + '"' : + "concat('" + aArg.replace(/'+/g, "',\"$&\",'") + "')"; + }, + + /** + * @returns an XPath query to all savable form field nodes + */ + get restorableFormNodes() { + // for a comprehensive list of all available <INPUT> types see + // http://mxr.mozilla.org/mozilla-central/search?string=kInputTypeTable + let ignoreTypes = ["password", "hidden", "button", "image", "submit", "reset"]; + // XXXzeniko work-around until lower-case has been implemented (bug 398389) + let toLowerCase = '"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"'; + let ignore = "not(translate(@type, " + toLowerCase + ")='" + + ignoreTypes.join("' or translate(@type, " + toLowerCase + ")='") + "')"; + let formNodesXPath = "//textarea|//select|//xhtml:textarea|//xhtml:select|" + + "//input[" + ignore + "]|//xhtml:input[" + ignore + "]"; + + delete this.restorableFormNodes; + return (this.restorableFormNodes = formNodesXPath); + } +}; diff --git a/browser/components/sessionstore/_SessionFile.jsm b/browser/components/sessionstore/_SessionFile.jsm new file mode 100644 index 000000000..173f6035d --- /dev/null +++ b/browser/components/sessionstore/_SessionFile.jsm @@ -0,0 +1,314 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["_SessionFile"]; + +/** + * Implementation of all the disk I/O required by the session store. + * This is a private API, meant to be used only by the session store. + * It will change. Do not use it for any other purpose. + * + * Note that this module implicitly depends on one of two things: + * 1. either the asynchronous file I/O system enqueues its requests + * and never attempts to simultaneously execute two I/O requests on + * the files used by this module from two distinct threads; or + * 2. the clients of this API are well-behaved and do not place + * concurrent requests to the files used by this module. + * + * Otherwise, we could encounter bugs, especially under Windows, + * e.g. if a request attempts to write sessionstore.js while + * another attempts to copy that file. + * + * This implementation uses OS.File, which guarantees property 1. + */ + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/Console.jsm"); + +// An encoder to UTF-8. +XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { + return new TextEncoder(); +}); +// A decoder. +XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { + return new TextDecoder(); +}); + +this._SessionFile = { + /** + * A promise fulfilled once initialization (either synchronous or + * asynchronous) is complete. + */ + promiseInitialized: function() { + return SessionFileInternal.promiseInitialized; + }, + /** + * Read the contents of the session file, asynchronously. + */ + read: function() { + return SessionFileInternal.read(); + }, + /** + * Read the contents of the session file, synchronously. + */ + syncRead: function() { + return SessionFileInternal.syncRead(); + }, + /** + * Write the contents of the session file, asynchronously. + */ + write: function(aData) { + return SessionFileInternal.write(aData); + }, + /** + * Create a backup copy, asynchronously. + */ + createBackupCopy: function() { + return SessionFileInternal.createBackupCopy(); + }, + /** + * Wipe the contents of the session file, asynchronously. + */ + wipe: function() { + return SessionFileInternal.wipe(); + } +}; + +Object.freeze(_SessionFile); + +/** + * Utilities for dealing with promises and Task.jsm + */ +const TaskUtils = { + /** + * Add logging to a promise. + * + * @param {Promise} promise + * @return {Promise} A promise behaving as |promise|, but with additional + * logging in case of uncaught error. + */ + captureErrors: function(promise) { + return promise.then( + null, + function onError(reason) { + console.error("Uncaught asynchronous error:", reason); + throw reason; + } + ); + }, + /** + * Spawn a new Task from a generator. + * + * This function behaves as |Task.spawn|, with the exception that it + * adds logging in case of uncaught error. For more information, see + * the documentation of |Task.jsm|. + * + * @param {generator} gen Some generator. + * @return {Promise} A promise built from |gen|, with the same semantics + * as |Task.spawn(gen)|. + */ + spawn: function spawn(gen) { + return this.captureErrors(Task.spawn(gen)); + } +}; + +var SessionFileInternal = { + /** + * A promise fulfilled once initialization is complete + */ + promiseInitialized: Promise.defer(), + + /** + * The path to sessionstore.js + */ + path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), + + /** + * The path to sessionstore.bak + */ + backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"), + + /** + * Utility function to safely read a file synchronously. + * @param aPath + * A path to read the file from. + * @returns string if successful, undefined otherwise. + */ + readAuxSync: function(aPath) { + let text; + try { + let file = new FileUtils.File(aPath); + let chan = NetUtil.newChannel({ + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true + }); + let stream = chan.open(); + text = NetUtil.readInputStreamToString(stream, stream.available(), + {charset: "utf-8"}); + } catch (e if e.result == Components.results.NS_ERROR_FILE_NOT_FOUND) { + // Ignore exceptions about non-existent files. + } catch (ex) { + // Any other error. + console.error("Uncaught error:", ex); + } finally { + return text; + } + }, + + /** + * Read the sessionstore file synchronously. + * + * This function is meant to serve as a fallback in case of race + * between a synchronous usage of the API and asynchronous + * initialization. + * + * In case if sessionstore.js file does not exist or is corrupted (something + * happened between backup and write), attempt to read the sessionstore.bak + * instead. + */ + syncRead: function() { + // First read the sessionstore.js. + let text = this.readAuxSync(this.path); + if (typeof text === "undefined") { + // If sessionstore.js does not exist or is corrupted, read sessionstore.bak. + text = this.readAuxSync(this.backupPath); + } + return text || ""; + }, + + /** + * Utility function to safely read a file asynchronously. + * @param aPath + * A path to read the file from. + * @param aReadOptions + * Read operation options. + * |outExecutionDuration| option will be reused and can be + * incrementally updated by the worker process. + * @returns string if successful, undefined otherwise. + */ + readAux: function(aPath, aReadOptions) { + let self = this; + return TaskUtils.spawn(function() { + let text; + try { + let bytes = yield OS.File.read(aPath, undefined, aReadOptions); + text = gDecoder.decode(bytes); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + // Any other error. + console.error("Uncaught error - with the file: " + self.path, ex); + } + throw new Task.Result(text); + }); + }, + + /** + * Read the sessionstore file asynchronously. + * + * In case sessionstore.js file does not exist or is corrupted (something + * happened between backup and write), attempt to read the sessionstore.bak + * instead. + */ + read: function() { + let self = this; + return TaskUtils.spawn(function task() { + // Specify |outExecutionDuration| option to hold the combined duration of + // the asynchronous reads off the main thread (of both sessionstore.js and + // sessionstore.bak, if necessary). If sessionstore.js does not exist or + // is corrupted, |outExecutionDuration| will register the time it took to + // attempt to read the file. It will then be subsequently incremented by + // the read time of sessionsore.bak. + let readOptions = { + outExecutionDuration: null + }; + // First read the sessionstore.js. + let text = yield self.readAux(self.path, readOptions); + if (typeof text === "undefined") { + // If sessionstore.js does not exist or is corrupted, read the + // sessionstore.bak. + text = yield self.readAux(self.backupPath, readOptions); + } + // Return either the content of the sessionstore.bak if it was read + // successfully or an empty string otherwise. + throw new Task.Result(text || ""); + }); + }, + + write: function(aData) { + let refObj = {}; + let self = this; + return TaskUtils.spawn(function task() { + let bytes = gEncoder.encode(aData); + + try { + let promise = OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"}); + yield promise; + } catch (ex) { + console.error("Could not write session state file: " + self.path, ex); + } + }); + }, + + createBackupCopy: function() { + let backupCopyOptions = { + outExecutionDuration: null + }; + let self = this; + return TaskUtils.spawn(function task() { + try { + yield OS.File.move(self.path, self.backupPath, backupCopyOptions); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not backup session state file: " + self.path, ex); + throw ex; + } + }); + }, + + wipe: function() { + let self = this; + return TaskUtils.spawn(function task() { + try { + yield OS.File.remove(self.path); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not remove session state file: " + self.path, ex); + throw ex; + } + + try { + yield OS.File.remove(self.backupPath); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not remove session state backup file: " + self.path, ex); + throw ex; + } + }); + }, + + _isNoSuchFile: function(aReason) { + return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; + } +}; diff --git a/browser/components/sessionstore/content/aboutSessionRestore.js b/browser/components/sessionstore/content/aboutSessionRestore.js new file mode 100644 index 000000000..7e21c97be --- /dev/null +++ b/browser/components/sessionstore/content/aboutSessionRestore.js @@ -0,0 +1,316 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +var gStateObject; +var gTreeData; + +// Page initialization + +window.onload = function() { + // the crashed session state is kept inside a textbox so that SessionStore picks it up + // (for when the tab is closed or the session crashes right again) + var sessionData = document.getElementById("sessionData"); + if (!sessionData.value) { + document.getElementById("errorTryAgain").disabled = true; + return; + } + + // remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0) + if (sessionData.value.charAt(0) == '(') + sessionData.value = sessionData.value.slice(1, -1); + try { + gStateObject = JSON.parse(sessionData.value); + } + catch (exJSON) { + var s = new Cu.Sandbox("about:blank", {sandboxName: 'aboutSessionRestore'}); + gStateObject = Cu.evalInSandbox("(" + sessionData.value + ")", s); + // If we couldn't parse the string with JSON.parse originally, make sure + // that the value in the textbox will be parsable. + sessionData.value = JSON.stringify(gStateObject); + } + + // make sure the data is tracked to be restored in case of a subsequent crash + var event = document.createEvent("UIEvents"); + event.initUIEvent("input", true, true, window, 0); + sessionData.dispatchEvent(event); + + initTreeView(); + + document.getElementById("errorTryAgain").focus(); +}; + +function initTreeView() { + var tabList = document.getElementById("tabList"); + var winLabel = tabList.getAttribute("_window_label"); + + gTreeData = []; + gStateObject.windows.forEach(function(aWinData, aIx) { + var winState = { + label: winLabel.replace("%S", (aIx + 1)), + open: true, + checked: true, + ix: aIx + }; + winState.tabs = aWinData.tabs.map(function(aTabData) { + var entry = aTabData.entries[aTabData.index - 1] || { url: "about:blank" }; + var iconURL = aTabData.attributes && aTabData.attributes.image || null; + // don't initiate a connection just to fetch a favicon (see bug 462863) + if (/^https?:/.test(iconURL)) + iconURL = "moz-anno:favicon:" + iconURL; + return { + label: entry.title || entry.url, + checked: true, + src: iconURL, + parent: winState + }; + }); + gTreeData.push(winState); + for (let tab of winState.tabs) + gTreeData.push(tab); + }, this); + + tabList.view = treeView; + tabList.view.selection.select(0); +} + +// User actions + +function restoreSession() { + document.getElementById("errorTryAgain").disabled = true; + + // remove all unselected tabs from the state before restoring it + var ix = gStateObject.windows.length - 1; + for (var t = gTreeData.length - 1; t >= 0; t--) { + if (treeView.isContainer(t)) { + if (gTreeData[t].checked === 0) + // this window will be restored partially + gStateObject.windows[ix].tabs = + gStateObject.windows[ix].tabs.filter(function(aTabData, aIx) + gTreeData[t].tabs[aIx].checked); + else if (!gTreeData[t].checked) + // this window won't be restored at all + gStateObject.windows.splice(ix, 1); + ix--; + } + } + var stateString = JSON.stringify(gStateObject); + + var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + var top = getBrowserWindow(); + + // if there's only this page open, reuse the window for restoring the session + if (top.gBrowser.tabs.length == 1) { + ss.setWindowState(top, stateString, true); + return; + } + + // restore the session into a new window and close the current tab + var newWindow = top.openDialog(top.location, "_blank", "chrome,dialog=no,all"); + newWindow.addEventListener("load", function() { + newWindow.removeEventListener("load", arguments.callee, true); + ss.setWindowState(newWindow, stateString, true); + + var tabbrowser = top.gBrowser; + var tabIndex = tabbrowser.getBrowserIndexForDocument(document); + tabbrowser.removeTab(tabbrowser.tabs[tabIndex]); + }, true); +} + +function startNewSession() { + var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + if (prefBranch.getIntPref("browser.startup.page") == 0) + getBrowserWindow().gBrowser.loadURI("about:logopage"); + else + getBrowserWindow().BrowserHome(); +} + +function onListClick(aEvent) { + // don't react to right-clicks + if (aEvent.button == 2) + return; + + if (!treeView.treeBox) { + return; + } + var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY); + if (cell.col) { + // Restore this specific tab in the same window for middle/double/accel clicking + // on a tab's title. + let accelKey = aEvent.ctrlKey; + if ((aEvent.button == 1 || aEvent.button == 0 && aEvent.detail == 2 || accelKey) && + cell.col.id == "title" && + !treeView.isContainer(cell.row)) { + restoreSingleTab(cell.row, aEvent.shiftKey); + aEvent.stopPropagation(); + } + else if (cell.col.id == "restore") + toggleRowChecked(cell.row); + } +} + +function onListKeyDown(aEvent) { + switch (aEvent.keyCode) + { + case KeyEvent.DOM_VK_SPACE: + toggleRowChecked(document.getElementById("tabList").currentIndex); + break; + case KeyEvent.DOM_VK_RETURN: + var ix = document.getElementById("tabList").currentIndex; + if (aEvent.ctrlKey && !treeView.isContainer(ix)) + restoreSingleTab(ix, aEvent.shiftKey); + break; + case KeyEvent.DOM_VK_UP: + case KeyEvent.DOM_VK_DOWN: + case KeyEvent.DOM_VK_PAGE_UP: + case KeyEvent.DOM_VK_PAGE_DOWN: + case KeyEvent.DOM_VK_HOME: + case KeyEvent.DOM_VK_END: + aEvent.preventDefault(); // else the page scrolls unwantedly + break; + } +} + +// Helper functions + +function getBrowserWindow() { + return window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); +} + +function toggleRowChecked(aIx) { + var item = gTreeData[aIx]; + item.checked = !item.checked; + treeView.treeBox.invalidateRow(aIx); + + function isChecked(aItem) aItem.checked; + + if (treeView.isContainer(aIx)) { + // (un)check all tabs of this window as well + for (let tab of item.tabs) { + tab.checked = item.checked; + treeView.treeBox.invalidateRow(gTreeData.indexOf(tab)); + } + } + else { + // update the window's checkmark as well (0 means "partially checked") + item.parent.checked = item.parent.tabs.every(isChecked) ? true : + item.parent.tabs.some(isChecked) ? 0 : false; + treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent)); + } + + document.getElementById("errorTryAgain").disabled = !gTreeData.some(isChecked); +} + +function restoreSingleTab(aIx, aShifted) { + var tabbrowser = getBrowserWindow().gBrowser; + var newTab = tabbrowser.addTab(); + var item = gTreeData[aIx]; + + var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + var tabState = gStateObject.windows[item.parent.ix] + .tabs[aIx - gTreeData.indexOf(item.parent) - 1]; + // ensure tab would be visible on the tabstrip. + tabState.hidden = false; + ss.setTabState(newTab, JSON.stringify(tabState)); + + // respect the preference as to whether to select the tab (the Shift key inverses) + var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + if (prefBranch.getBoolPref("browser.tabs.loadInBackground") != !aShifted) + tabbrowser.selectedTab = newTab; +} + +// Tree controller + +var treeView = { + treeBox: null, + selection: null, + + get rowCount() { return gTreeData.length; }, + setTree: function(treeBox) { this.treeBox = treeBox; }, + getCellText: function(idx, column) { return gTreeData[idx].label; }, + isContainer: function(idx) { return "open" in gTreeData[idx]; }, + getCellValue: function(idx, column){ return gTreeData[idx].checked; }, + isContainerOpen: function(idx) { return gTreeData[idx].open; }, + isContainerEmpty: function(idx) { return false; }, + isSeparator: function(idx) { return false; }, + isSorted: function() { return false; }, + isEditable: function(idx, column) { return false; }, + canDrop: function(idx, orientation, dt) { return false; }, + getLevel: function(idx) { return this.isContainer(idx) ? 0 : 1; }, + + getParentIndex: function(idx) { + if (!this.isContainer(idx)) + for (var t = idx - 1; t >= 0 ; t--) + if (this.isContainer(t)) + return t; + return -1; + }, + + hasNextSibling: function(idx, after) { + var thisLevel = this.getLevel(idx); + for (var t = after + 1; t < gTreeData.length; t++) + if (this.getLevel(t) <= thisLevel) + return this.getLevel(t) == thisLevel; + return false; + }, + + toggleOpenState: function(idx) { + if (!this.isContainer(idx)) + return; + var item = gTreeData[idx]; + if (item.open) { + // remove this window's tab rows from the view + var thisLevel = this.getLevel(idx); + for (var t = idx + 1; t < gTreeData.length && this.getLevel(t) > thisLevel; t++); + var deletecount = t - idx - 1; + gTreeData.splice(idx + 1, deletecount); + this.treeBox.rowCountChanged(idx + 1, -deletecount); + } + else { + // add this window's tab rows to the view + var toinsert = gTreeData[idx].tabs; + for (var i = 0; i < toinsert.length; i++) + gTreeData.splice(idx + i + 1, 0, toinsert[i]); + this.treeBox.rowCountChanged(idx + 1, toinsert.length); + } + item.open = !item.open; + this.treeBox.invalidateRow(idx); + }, + + getCellProperties: function(idx, column) { + if (column.id == "restore" && this.isContainer(idx) && gTreeData[idx].checked === 0) + return "partial"; + if (column.id == "title") + return this.getImageSrc(idx, column) ? "icon" : "noicon"; + + return ""; + }, + + getRowProperties: function(idx) { + var winState = gTreeData[idx].parent || gTreeData[idx]; + if (winState.ix % 2 != 0) + return "alternate"; + + return ""; + }, + + getImageSrc: function(idx, column) { + if (column.id == "title") + return gTreeData[idx].src || null; + return null; + }, + + getProgressMode : function(idx, column) { }, + cycleHeader: function(column) { }, + cycleCell: function(idx, column) { }, + selectionChanged: function() { }, + performAction: function(action) { }, + performActionOnCell: function(action, index, column) { }, + getColumnProperties: function(column) { return ""; } +}; diff --git a/browser/components/sessionstore/content/aboutSessionRestore.xhtml b/browser/components/sessionstore/content/aboutSessionRestore.xhtml new file mode 100644 index 000000000..6b22250d7 --- /dev/null +++ b/browser/components/sessionstore/content/aboutSessionRestore.xhtml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +--> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd"> + %netErrorDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % restorepageDTD SYSTEM "chrome://browser/locale/aboutSessionRestore.dtd"> + %restorepageDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&restorepage.tabtitle;</title> + <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all"/> + <link rel="stylesheet" href="chrome://browser/skin/aboutSessionRestore.css" type="text/css" media="all"/> + <link rel="icon" type="image/png" href="chrome://global/skin/icons/warning-16.png"/> + + <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutSessionRestore.js"/> + </head> + + <body dir="&locale.dir;"> + + <!-- PAGE CONTAINER (for styling purposes only) --> + <div id="errorPageContainer"> + + <!-- Error Title --> + <div id="errorTitle"> + <h1 id="errorTitleText">&restorepage.errorTitle;</h1> + </div> + + <!-- LONG CONTENT (the section most likely to require scrolling) --> + <div id="errorLongContent"> + + <!-- Short Description --> + <div id="errorShortDesc"> + <p id="errorShortDescText">&restorepage.problemDesc;</p> + </div> + + <!-- Long Description (Note: See netError.dtd for used XHTML tags) --> + <div id="errorLongDesc"> + <p>&restorepage.tryThis;</p> + <ul> + <li>&restorepage.restoreSome;</li> + <li>&restorepage.startNew;</li> + </ul> + </div> + + <!-- Short Description --> + <div id="errorTrailerDesc"> + <tree xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="tabList" flex="1" seltype="single" hidecolumnpicker="true" + onclick="onListClick(event);" onkeydown="onListKeyDown(event);" + _window_label="&restorepage.windowLabel;"> + <treecols> + <treecol cycler="true" id="restore" type="checkbox" label="&restorepage.restoreHeader;"/> + <splitter class="tree-splitter"/> + <treecol primary="true" id="title" label="&restorepage.listHeader;" flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + </div> + </div> + + <!-- Buttons --> + <hbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="buttons"> +#ifdef XP_UNIX + <button id="errorCancel" label="&restorepage.closeButton;" + accesskey="&restorepage.close.access;" + oncommand="startNewSession();"/> + <button id="errorTryAgain" label="&restorepage.tryagainButton;" + accesskey="&restorepage.restore.access;" + oncommand="restoreSession();"/> +#else + <button id="errorTryAgain" label="&restorepage.tryagainButton;" + accesskey="&restorepage.restore.access;" + oncommand="restoreSession();"/> + <button id="errorCancel" label="&restorepage.closeButton;" + accesskey="&restorepage.close.access;" + oncommand="startNewSession();"/> +#endif + </hbox> + <!-- holds the session data for when the tab is closed --> + <input type="text" id="sessionData" style="display: none;"/> + </div> + + </body> +</html> diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js new file mode 100644 index 000000000..e3e956ef2 --- /dev/null +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function debug(msg) { + Services.console.logStringMessage("SessionStoreContent: " + msg); +} + +/** + * Listens for and handles content events that we need for the + * session store service to be notified of state changes in content. + */ +var EventListener = { + + DOM_EVENTS: [ + "pageshow", "change", "input" + ], + + init: function () { + this.DOM_EVENTS.forEach(e => addEventListener(e, this, true)); + }, + + handleEvent: function (event) { + switch (event.type) { + case "pageshow": + if (event.persisted) + sendAsyncMessage("SessionStore:pageshow"); + break; + case "input": + case "change": + sendAsyncMessage("SessionStore:input"); + break; + default: + debug("received unknown event '" + event.type + "'"); + break; + } + } +}; + +EventListener.init(); diff --git a/browser/components/sessionstore/jar.mn b/browser/components/sessionstore/jar.mn new file mode 100644 index 000000000..7ad408e4c --- /dev/null +++ b/browser/components/sessionstore/jar.mn @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: +* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml) + content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js) + content/browser/content-sessionStore.js (content/content-sessionStore.js) diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build new file mode 100644 index 000000000..8167c7631 --- /dev/null +++ b/browser/components/sessionstore/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] + +XPIDL_SOURCES += [ + 'nsISessionStartup.idl', + 'nsISessionStore.idl', +] + +XPIDL_MODULE = 'sessionstore' + +EXTRA_COMPONENTS += [ + 'nsSessionStartup.js', + 'nsSessionStore.js', + 'nsSessionStore.manifest', +] + +EXTRA_JS_MODULES.sessionstore = [ + '_SessionFile.jsm', + 'DocumentUtils.jsm', + 'SessionStorage.jsm', + 'XPathGenerator.jsm', +] + +EXTRA_PP_JS_MODULES.sessionstore += ['SessionStore.jsm']
\ No newline at end of file diff --git a/browser/components/sessionstore/nsISessionStartup.idl b/browser/components/sessionstore/nsISessionStartup.idl new file mode 100644 index 000000000..a8e786d03 --- /dev/null +++ b/browser/components/sessionstore/nsISessionStartup.idl @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * nsISessionStore keeps track of the current browsing state - i.e. + * tab history, cookies, scroll state, form data, POSTDATA and window features + * - and allows to restore everything into one window. + */ + +[scriptable, uuid(51f4b9f0-f3d2-11e2-bb62-2c24dd830245)] +interface nsISessionStartup: nsISupports +{ + /** + * Return a promise that is resolved once initialization + * is complete. + */ + readonly attribute jsval onceInitialized; + + // Get session state + readonly attribute jsval state; + + /** + * Determines whether there is a pending session restore and makes sure that + * we're initialized before returning. If we're not yet this will read the + * session file synchronously. + */ + boolean doRestore(); + + /** + * Returns whether we will restore a session that ends up replacing the + * homepage. The browser uses this to not start loading the homepage if + * we're going to stop its load anyway shortly after. + * + * This is meant to be an optimization for the average case that loading the + * session file finishes before we may want to start loading the default + * homepage. Should this be called before the session file has been read it + * will just return false. + */ + readonly attribute bool willOverrideHomepage; + + /** + * What type of session we're restoring. + * NO_SESSION There is no data available from the previous session + * RECOVER_SESSION The last session crashed. It will either be restored or + * about:sessionrestore will be shown. + * RESUME_SESSION The previous session should be restored at startup + * DEFER_SESSION The previous session is fine, but it shouldn't be restored + * without explicit action (with the exception of pinned tabs) + */ + const unsigned long NO_SESSION = 0; + const unsigned long RECOVER_SESSION = 1; + const unsigned long RESUME_SESSION = 2; + const unsigned long DEFER_SESSION = 3; + + readonly attribute unsigned long sessionType; +}; diff --git a/browser/components/sessionstore/nsISessionStore.idl b/browser/components/sessionstore/nsISessionStore.idl new file mode 100644 index 000000000..0490772a4 --- /dev/null +++ b/browser/components/sessionstore/nsISessionStore.idl @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 - i.e. + * tab history, cookies, scroll state, form data, POSTDATA and window features + * - and allows to restore everything into one browser window. + * + * The nsISessionStore API operates mostly on browser windows and the tabbrowser + * tabs contained in them: + * + * * "Browser windows" are those DOM windows having loaded + * chrome://browser/content/browser.xul . From overlays you can just pass the + * global |window| object to the API, though (or |top| from a sidebar). + * From elsewhere you can get browser windows through the nsIWindowMediator + * by looking for "navigator:browser" windows. + * + * * "Tabbrowser tabs" are all the child nodes of a browser window's + * |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|. + */ + +[scriptable, uuid(43ec216b-f002-4424-bfc5-fc555c87dbc4)] +interface nsISessionStore : nsISupports +{ + /** + * Initialize the service + */ + jsval init(in nsIDOMWindow aWindow); + + /** + * Is it possible to restore the previous session. Will always be false when + * in Private Browsing mode. + */ + attribute boolean canRestoreLastSession; + + /** + * Restore the previous session if possible. This will not overwrite the + * current session. Instead the previous session will be merged into the + * current session. Current windows will be reused if they were windows that + * pinned tabs were previously restored into. New windows will be opened as + * needed. + * + * Note: This will throw if there is no previous state to restore. Check with + * canRestoreLastSession first to avoid thrown errors. + */ + void restoreLastSession(); + + /** + * Get the current browsing state. + * @returns a JSON string representing the session state. + */ + AString getBrowserState(); + + /** + * Set the browsing state. + * This will immediately restore the state of the whole application to the state + * passed in, *replacing* the current session. + * + * @param aState is a JSON string representing the session state. + */ + void setBrowserState(in AString aState); + + /** + * @param aWindow is the browser window whose state is to be returned. + * + * @returns a JSON string representing a session state with only one window. + */ + AString getWindowState(in nsIDOMWindow aWindow); + + /** + * @param aWindow is the browser window whose state is to be set. + * @param aState is a JSON string representing a session state. + * @param aOverwrite boolean overwrite existing tabs + */ + void setWindowState(in nsIDOMWindow aWindow, in AString aState, in boolean aOverwrite); + + /** + * @param aTab is the tabbrowser tab whose state is to be returned. + * + * @returns a JSON string representing the state of the tab + * (note: doesn't contain cookies - if you need them, use getWindowState instead). + */ + AString getTabState(in nsIDOMNode aTab); + + /** + * @param aTab is the tabbrowser tab whose state is to be set. + * @param aState is a JSON string representing a session state. + */ + void setTabState(in nsIDOMNode aTab, in AString aState); + + /** + * Duplicates a given tab as thoroughly as possible. + * + * @param aWindow is the browser window into which the tab will be duplicated. + * @param aTab is the tabbrowser tab to duplicate (can be from a different window). + * @param aDelta is the offset to the history entry to load in the duplicated tab. + * @returns a reference to the newly created tab. + */ + nsIDOMNode duplicateTab(in nsIDOMWindow aWindow, in nsIDOMNode aTab, + [optional] in long aDelta); + + /** + * 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); + + /** + * Get the number of restore-able windows + */ + unsigned long getClosedWindowCount(); + + /** + * Get closed windows data + * + * @returns a JSON string representing the list of closed windows. + */ + AString getClosedWindowData(); + + /** + * @param aIndex is the index of the windows to be restored (FIFO ordered). + * @returns the nsIDOMWindow object of the reopened window + */ + nsIDOMWindow undoCloseWindow(in unsigned long aIndex); + + /** + * @param aIndex is the index of the closed window to be removed (FIFO ordered). + * + * @throws NS_ERROR_INVALID_ARG + * when aIndex does not map to a closed window + */ + nsIDOMNode forgetClosedWindow(in unsigned long aIndex); + + /** + * @param aWindow is the window 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 getWindowValue(in nsIDOMWindow aWindow, in AString aKey); + + /** + * @param aWindow is the browser window 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 setWindowValue(in nsIDOMWindow aWindow, in AString aKey, in AString aStringValue); + + /** + * @param aWindow is the browser window to get the value for. + * @param aKey is the value's name. + */ + void deleteWindowValue(in nsIDOMWindow aWindow, in AString aKey); + + /** + * @param aTab is the tabbrowser 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 tabbrowser 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 tabbrowser tab to get the value for. + * @param aKey is the value's name. + */ + void deleteTabValue(in nsIDOMNode aTab, in AString aKey); + + /** + * @param aName is the name of the attribute to save/restore for all tabbrowser tabs. + */ + void persistTabAttribute(in AString aName); +}; diff --git a/browser/components/sessionstore/nsSessionStartup.js b/browser/components/sessionstore/nsSessionStartup.js new file mode 100644 index 000000000..13e13ecdb --- /dev/null +++ b/browser/components/sessionstore/nsSessionStartup.js @@ -0,0 +1,296 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Session Storage and Restoration + * + * Overview + * This service reads user's session file at startup, and makes a determination + * as to whether the session should be restored. It will restore the session + * under the circumstances described below. If the auto-start Private Browsing + * mode is active, however, the session is never restored. + * + * Crash Detection + * The session file stores a session.state property, that + * indicates whether the browser is currently running. When the browser shuts + * down, the field is changed to "stopped". At startup, this field is read, and + * if its value is "running", then it's assumed that the browser had previously + * crashed, or at the very least that something bad happened, and that we should + * restore the session. + * + * Forced Restarts + * In the event that a restart is required due to application update or extension + * installation, set the browser.sessionstore.resume_session_once pref to true, + * and the session will be restored the next time the browser starts. + * + * Always Resume + * This service will always resume the session if the integer pref + * browser.startup.page is set to 3. + */ + +/* :::::::: Constants and Helpers ::::::::::::::: */ + +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"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile", + "resource:///modules/sessionstore/_SessionFile.jsm"); + +const STATE_RUNNING_STR = "running"; + +function debug(aMsg) { + aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n"); + Services.console.logStringMessage(aMsg); +} + +var gOnceInitializedDeferred = Promise.defer(); + +/* :::::::: The Service ::::::::::::::: */ + +function SessionStartup() { +} + +SessionStartup.prototype = { + + // the state to restore at startup + _initialState: null, + _sessionType: Ci.nsISessionStartup.NO_SESSION, + _initialized: false, + +/* ........ Global Event Handlers .............. */ + + /** + * Initialize the component + */ + init: function() { + // do not need to initialize anything in auto-started private browsing sessions + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + this._initialized = true; + gOnceInitializedDeferred.resolve(); + return; + } + + if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") || + Services.prefs.getIntPref("browser.startup.page") == 3) { + this._ensureInitialized(); + } else { + _SessionFile.read().then( + this._onSessionFileRead.bind(this) + ); + } + }, + + // Wrap a string as a nsISupports + _createSupportsString: function(aData) { + let string = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + string.data = aData; + return string; + }, + + _onSessionFileRead: function(aStateString) { + if (this._initialized) { + // Initialization is complete, nothing else to do + return; + } + try { + this._initialized = true; + + // Let observers modify the state before it is used + let supportsStateString = this._createSupportsString(aStateString); + Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", ""); + aStateString = supportsStateString.data; + + // No valid session found. + if (!aStateString) { + this._sessionType = Ci.nsISessionStartup.NO_SESSION; + return; + } + + // parse the session state into a JS object + // remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0) + if (aStateString.charAt(0) == '(') + aStateString = aStateString.slice(1, -1); + let corruptFile = false; + try { + this._initialState = JSON.parse(aStateString); + } + catch (ex) { + debug("The session file contained un-parse-able JSON: " + ex); + // This is not valid JSON, but this might still be valid JavaScript, + // as used in FF2/FF3, so we need to eval. + // evalInSandbox will throw if aStateString is not parse-able. + try { + var s = new Cu.Sandbox("about:blank", {sandboxName: 'nsSessionStartup'}); + this._initialState = Cu.evalInSandbox("(" + aStateString + ")", s); + } catch(ex) { + debug("The session file contained un-eval-able JSON: " + ex); + corruptFile = true; + } + } + let doResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); + let doResumeSession = doResumeSessionOnce || + Services.prefs.getIntPref("browser.startup.page") == 3; + + // If this is a normal restore then throw away any previous session + if (!doResumeSessionOnce) + delete this._initialState.lastSessionState; + + let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"); + let lastSessionCrashed = + this._initialState && this._initialState.session && + this._initialState.session.state && + this._initialState.session.state == STATE_RUNNING_STR; + + // set the startup type + if (lastSessionCrashed && resumeFromCrash) + this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION; + else if (!lastSessionCrashed && doResumeSession) + this._sessionType = Ci.nsISessionStartup.RESUME_SESSION; + else if (this._initialState) + this._sessionType = Ci.nsISessionStartup.DEFER_SESSION; + else + this._initialState = null; // reset the state + + Services.obs.addObserver(this, "sessionstore-windows-restored", true); + + if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) + Services.obs.addObserver(this, "browser:purge-session-history", true); + + } finally { + // We're ready. Notify everyone else. + Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); + gOnceInitializedDeferred.resolve(); + } + }, + + /** + * Handle notifications + */ + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "app-startup": + Services.obs.addObserver(this, "final-ui-startup", true); + Services.obs.addObserver(this, "quit-application", true); + break; + case "final-ui-startup": + Services.obs.removeObserver(this, "final-ui-startup"); + Services.obs.removeObserver(this, "quit-application"); + this.init(); + break; + case "quit-application": + // no reason for initializing at this point (cf. bug 409115) + Services.obs.removeObserver(this, "final-ui-startup"); + Services.obs.removeObserver(this, "quit-application"); + if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) + Services.obs.removeObserver(this, "browser:purge-session-history"); + break; + case "sessionstore-windows-restored": + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + // free _initialState after nsSessionStore is done with it + this._initialState = null; + break; + case "browser:purge-session-history": + Services.obs.removeObserver(this, "browser:purge-session-history"); + // reset all state on sanitization + this._sessionType = Ci.nsISessionStartup.NO_SESSION; + break; + } + }, + +/* ........ Public API ................*/ + + get onceInitialized() { + return gOnceInitializedDeferred.promise; + }, + + /** + * Get the session state as a jsval + */ + get state() { + this._ensureInitialized(); + return this._initialState; + }, + + /** + * Determines whether there is a pending session restore and makes sure that + * we're initialized before returning. If we're not yet this will read the + * session file synchronously. + * @returns bool + */ + doRestore: function() { + this._ensureInitialized(); + return this._willRestore(); + }, + + /** + * Determines whether there is a pending session restore. + * @returns bool + */ + _willRestore: function() { + return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION || + this._sessionType == Ci.nsISessionStartup.RESUME_SESSION; + }, + + /** + * Returns whether we will restore a session that ends up replacing the + * homepage. The browser uses this to not start loading the homepage if + * we're going to stop its load anyway shortly after. + * + * This is meant to be an optimization for the average case that loading the + * session file finishes before we may want to start loading the default + * homepage. Should this be called before the session file has been read it + * will just return false. + * + * @returns bool + */ + get willOverrideHomepage() { + if (this._initialState && this._willRestore()) { + let windows = this._initialState.windows || null; + // If there are valid windows with not only pinned tabs, signal that we + // will override the default homepage by restoring a session. + return windows && windows.some(w => w.tabs.some(t => !t.pinned)); + } + return false; + }, + + /** + * Get the type of pending session store, if any. + */ + get sessionType() { + this._ensureInitialized(); + return this._sessionType; + }, + + // Ensure that initialization is complete. + // If initialization is not complete yet, fall back to a synchronous + // initialization and kill ongoing asynchronous initialization + _ensureInitialized: function() { + try { + if (this._initialized) { + // Initialization is complete, nothing else to do + return; + } + let contents = _SessionFile.syncRead(); + this._onSessionFileRead(contents); + } catch(ex) { + debug("ensureInitialized: could not read session " + ex + ", " + ex.stack); + throw ex; + } + }, + + /* ........ QueryInterface .............. */ + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference, + Ci.nsISessionStartup]), + classID: Components.ID("{ec7a6c20-e081-11da-8ad9-0800200c9a66}") +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]); diff --git a/browser/components/sessionstore/nsSessionStore.js b/browser/components/sessionstore/nsSessionStore.js new file mode 100644 index 000000000..38713d500 --- /dev/null +++ b/browser/components/sessionstore/nsSessionStore.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Session Storage and Restoration + * + * Overview + * This service keeps track of a user's session, storing the various bits + * required to return the browser to its current state. The relevant data is + * stored in memory, and is periodically saved to disk in a file in the + * profile directory. The service is started at first window load, in + * delayedStartup, and will restore the session from the data received from + * the nsSessionStartup service. + */ + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/sessionstore/SessionStore.jsm"); + +function SessionStoreService() {} + +// The SessionStore module's object is frozen. We need to modify our prototype +// and add some properties so let's just copy the SessionStore object. +Object.keys(SessionStore).forEach(function (aName) { + let desc = Object.getOwnPropertyDescriptor(SessionStore, aName); + Object.defineProperty(SessionStoreService.prototype, aName, desc); +}); + +SessionStoreService.prototype.classID = + Components.ID("{5280606b-2510-4fe0-97ef-9b5a22eafe6b}"); +SessionStoreService.prototype.QueryInterface = + XPCOMUtils.generateQI([Ci.nsISessionStore]); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStoreService]); diff --git a/browser/components/sessionstore/nsSessionStore.manifest b/browser/components/sessionstore/nsSessionStore.manifest new file mode 100644 index 000000000..0501afeb2 --- /dev/null +++ b/browser/components/sessionstore/nsSessionStore.manifest @@ -0,0 +1,5 @@ +component {5280606b-2510-4fe0-97ef-9b5a22eafe6b} nsSessionStore.js +contract @mozilla.org/browser/sessionstore;1 {5280606b-2510-4fe0-97ef-9b5a22eafe6b} +component {ec7a6c20-e081-11da-8ad9-0800200c9a66} nsSessionStartup.js +contract @mozilla.org/browser/sessionstartup;1 {ec7a6c20-e081-11da-8ad9-0800200c9a66} +category app-startup nsSessionStartup service,@mozilla.org/browser/sessionstartup;1 diff --git a/browser/components/shared/searchenginelogos.js b/browser/components/shared/searchenginelogos.js new file mode 100644 index 000000000..ce7e8c1d8 --- /dev/null +++ b/browser/components/shared/searchenginelogos.js @@ -0,0 +1,349 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 SEARCH_ENGINES = { + "DuckDuckGo": { + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAACT1BMVEXvISn/////9/fvUlr3ra3/" + + "zs7/7+/va2v/5+f/xsbvMTn/tbX/3t7/vb3vOUL3WmPvQkr/zgDvKTHvSlL3hIT3paX/1tbnISn3" + + "c3v3e3v3a3P3jIz3nJz/tb33c3PvKSn3lJT39/cAc73vSkr3e4Tv7+/3Yxj3pa3/tQj3jJT3nKX3" + + "Y2P/xs73hIzvQkL/vQjvQiHn5+f3hBD/ztbvMTH/vcb/3ucIc733lJz/pQilzufe7/fvMSHOzs73" + + "//cQrUpKvVprxmP3Y2vvShiUzmvWlJRzzmMYtUrvOTnn7/davVrWra3v9//nY2PvISGUxudztd7e" + + "3t7/76XvKSHea2v/xgDnOUK93vfW5/f/1t73Uhj/52ut3q2l3rXO784pjMZrrdb/rQjera3/5+/e" + + "paWMxufO79aEazkYrUr/nAj3jBD3axj3lBD///fehIRKpd7/1hCEYzk5vVL3//8ptVLW77UxtVLn" + + "SlLW1tZCvVp7vef/1gj/3invSkL//+fWtbXvpaX/3kr/97XvnJznWmMxjM5zvefOxsbWnKXWjIzG" + + "3u/ea3Pn997O5/fnQkqExuf3Whit1u/nUlrnxs7v5+d7zmuU1pT3exDOSjFjrVL/987/pUoQe8b/" + + "75T/3jFKxnO158bWKSl7zoRSxmtajEK1e0pzxlqcUjH/1iHOMSnOvb33cxDWnJx7td6EzmP/74xz" + + "azlrcznec3Pe771jxlpzczne78YpvVqEvWPn99YxvWOtSjHee3vG787OOTE5lEK1QjHv9+drzmve" + + "tbXO772q+r8wAAAFbUlEQVR4Xo2X84PzTBDHN3Zqu2fbemzbNl7atm3btvGHvTNJ2myuyd3NL2mT" + + "zmdnvjM76RImyGQlH5dCHBeSmscNmQkyfwBrZMLEY2aRF5cMSDYPEx+LZpUlAYRQbVEpnuc1je/M" + + "SbVwYoVFAbpE0IaLmiwqiVymmE3H84YuGs2mheCEhQH5qPUrje2ONxHKVIkXR2x2MxsMkDnLvftk" + + "2fSTQNCzSAgngwCCipkXxHiU+BsnCDFE8f6AQgnwaTGhkmDLymW8jPsBeIsth8iCpha618El1wgo" + + "4FOhWyWLWY+O8pbnAwTI29S1ElncJBmF4L0AGeJSdR4dUpt5w+DL0nAgoUuGGKKCBxDCOxrykaDb" + + "+yFQjhUylLlXpAB5jGnIqV6uvvWUcAAhLmDBXIAMrkXRdHQ+cerUiWefq1hRrAgg8LikUgdkQUAx" + + "6+2Ze0WLEO/1BQzrHCFNrAPAeDSD4q/Ln6R3p68MSYzDAUiwIEutJM0bHXE/gpEhJMxaAB3T6aT8" + + "mfkm+QBiMlwKFqAHvrHu9tvTOLrEdX4hFAkJWQB42qbVyam75ruv3zvF+wBCKJ0MAAV6SAy5+raA" + + "y+lb9tYBUw9sffKRJh+CDl2SAEAPquaC76swU1c+zlxbA9if/EIY78AcCBODDKjnVzDM0+sb57zq" + + "N14gdpbg4nraBaxm3NWpIDKNgJIIDTxEAKMyVM9/VrFcpijK52PbNhmk0RQORCA8dhGhIkDA+qPV" + + "Y/U8No2NHZsUfQCdzYTECSiRSRJKgxYAnK6+tnVrPYL7q2P7GNNnT0L3SQSS61AowK4BAExWq9XJ" + + "OmDT5D4GtUab7p92W1aD6AFBOjUKcONNKMG2o9vmScmhd+v5SCTS91StDLBwmHR5q0iiM4yv3X5g" + + "sD1i24tUHc0GQOrOihdw+ZV7drx+8I1IzfpaCQ1oSIGsbqEBdxy8KkLb8dYt7m7AFBpEJI8OUIAd" + + "Hve+wX509IqYgzLqxKMi5X+r6737wgHfMrZBKGwpQMWP0PN8/8qLn15cSRosEQeI3coxGrzRVfE2" + + "BEyTAMNpmbA3k2erPOyq+CUCPGvv3OmGykYBQhiYFbynDLu2uyW826qb7bSlv/VCe2R3vQqhIYQQ" + + "nLmSGKUAT1AqXn7V6p72iUsTThsNuhKUAeKMNFaiW2nG08H90IF1m6DywVdsHgA4bPgRGgAqUgBr" + + "DwxOtPcdv9RK6yklnaGKOXBMmN7RVCtJJMiUdG2s78dv9HbY7KrI9AQBOHwjaxaA6cKhRLXCHkpF" + + "PrAJYBz1su7LtSBQIjzozgI5AJDWsQ7gTJxETTHuEh5yW8kR5+1fvQBT5PDdWgPokE6GSuK3Aaby" + + "2KwNyGFIZ8/NfexVMAGXEfe8MA5QTVdrgGe2M9evev6FMwiAYr308nVzcx/SgHwSlswyLgDLHU0K" + + "tX5UZwCwZsM1b7516J1333v/g2UAuJoCNMsmZkEDZBXujCoOIfVJxQKsvXnDshvWfrEcAV9RAoqY" + + "rfdvHjY06R3tVmtjzQYsQ8ByC/C1O0dEzqkAGqELbiZ1W/RvBr51Ad9ZgO8dQCkh4/q5xvMC6hot" + + "sBl7rP1QT+HHQz9RGoSHhkyMgqEBdNPFWSWMY+1nBPxy+MjvZ2aZxB9n/zz3FwKiOTZfotb3AhhF" + + "xSUUNmGSjX+vWvPPYacVWJOkUilUT05ymEVb0JFHj9l/AVn+35b/jsx6YzNz8mja+iAEH7rYDntY" + + "Gaz3dizW080KWaeICx77kiG7lTKG6EEoPb0Wu0lZ9OA5whFH8GxHQjOMQls5HSs5t/glHX2FYtT/" + + "mGAs/fCtFU0vQJUSQYfvIBvVyukuLhbjuood/H6WCbD/AQSFvIO3JDxgAAAAAElFTkSuQmCC" + }, + "Yahoo": { + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAJjUlEQVR4Xs1aa3BU5Rl+nu/sZnPZ" + + "sLvZAMVECgoFokUUBbnkAjNqa6W2XlqsWAE1Ee0P+8NO2z+1Y2dsx5nO9I5aBERxrDo6Vp2OZQrE" + + "hEvRqqOFqVy8IRZCdjeBkOxmz/eUTLM7Q4BzTkOn3Wf257tnvue9PPt871lms1mUHATh6fsPtv+q" + + "TxBBj1CDEoSQPa4dG4/B7/QAQig9iNj+XCrXHSi5pgSbh2L7oynBF1KpEZAI2n27eg6+PQh/kCVX" + + "AVrK2bI6Y6wB5FcsWKq0ZoAyPV25t17oY4DMEqi/PFRiLUR1buh2TzDIrAhobo2XEAFJyJuONT2C" + + "/GNhKsbm59yYNCWkPsC7m1Ld+60J1D/u3GXxSKUpFQIUCWx+JEMQQRBCyx21ggxKBl0fZHdvGghW" + + "LkxbXPG5KRGSJUOA2LImbXJCMLS0JsQS8UIChMF+27khLQTSn/gkM/OaGGEBhATQKxoUwMBHARQs" + + "uPhYAYD++kKq/wgI+pIlsGBlzCkjYACENv/uaLGIxecVTWwoiqZbE4ADBlXCreuPugMQzAhmxTRQ" + + "EnXJtdVjJ5YDAEGh/bEMZYgA2Sq3TctrIIAEEPrDfV0gIBRpnAJjJ19SVT+zggiEA2/2bVx1xFjH" + + "wwhYMhLPL7w1CQgghQ/fPvHhzqyBA9BXamd9rXLM+DIJLNhpQqdEnQLX6Xiie+nD9WAgG9yxLuNY" + + "B4DHURzpyltikWozfCpiy+9TcA38QdA235WkoEILhuBXs50be2960IYr6BeIbK9949kewfGMk3Vs" + + "c2uSAghBJ9LY9cyxgHM2YaYzbX61KAa/D/R3880/piH/yB3Pp3O9Xs0DSOCUpvK66RXicP63b+x2" + + "j9EEIqDGO2tgQBBgUAIAO9f2iNanOy061mYowROWWtQaK6oN89i6phvBEIpr/tLkyCEVXV/Xuvf1" + + "/iP7ct5G4KN3T3z8Ro6ghxQKjNXp0utqUAja/Xpv1+6gEn3l0jGRMSN12tTNDAt+GDQdT6QhSDqb" + + "De5YmzIu4YeFK+NO2AxLrrD50Qxkg/gfOWpprQEo6hQCzW0JerYtIcDseCrt5l2ejWCfdg4Nog9M" + + "xF14exLDYObT7HsvH2cwrzplYVldQ+XpFTZXfiMZqc3DCyRs70G881rP6VohUcKuF9PZNDybR4Iu" + + "XlKVrIsUxsluXZtGLti1lra5NcEzJyXKebcl/ESABuhc22t5hvqIbufQ+HoVkjCiPdkD4rBu53Ps" + + "XJ9iMDMWncDLliTOKLVGxKK7kjY8CB9wz2t9mYO502frsz3Z/dsG4OkCBDu+ITSjsZoQAEhvvpI+" + + "9kmQm7sELFwed8pwxmBDceyF5Q1XVwrysYxZs31jCiNAdK5LG9fQr4ZNQxIugAAFtD+aBhTAftKU" + + "uY0ras8WaERRWrxqKMKzByCwY31a7ilB+QFt39gjvyyGY+6CWxIFkjq0p3/v1uzwU32gi64dUzMx" + + "RAE8YwVAkA2Lq8dNc7zzQSC9D3vaeymhgL+9nBroIrxgBTNnaaw8YQoSrpPmx7GGgL+AEs1tMcCA" + + "BHjW1aIxbGwbQ/988KRdKzYVpI7H05LPj5eMbbkzUfQvA73a+XSvgAAf1X6BF7VUA/Jf7i741tiX" + + "ftQ92OvdzXrnpWPHj+ajtWFCR/Zn923NAd4+j5Pnh+q/WAkSgKRdL3YPDshUWO/+EUGoqS3pLVSF" + + "9wOC6D5136cdq09Q3nqiG36euPqecQCff+Cfm37a42sBbn9y7LybasACSYvgd7zhQMK/ApRpaR3b" + + "8dgHypNed0a77fFj16wan3e5Y0PG9woSnWBnL0mIJAowUuEqU/AFpAR6sPJdrxMg62aUT22O0HOO" + + "Ceez9wb37up7+0+pY4coz9MTZv7yeDhCjsiqQAnD5ydhRWJUMCM8w6JVNdZX28iT2t+5pgfyVl66" + + "4XzzilqSwyMvQLavO/fnX3f94voPf9Dw/v0X/OPBy99f13bo769lYAlJECQExinvyATZPH948e6e" + + "D7zu14LCVXSzUp7wxMVfLbvn2ckEIQmU3L880v3SA125jITiYlkAXdrJ8yO3rz7vvKnlIgiOpgIA" + + "HAdNrQnBCwQH+6zN+1g3O6QhCQoAREruhu988tx3u3OZEBEyEIZBAUb8qDP3s8UHPnqrjxp1C4Eg" + + "mm6rDUddPw6G3hyh2ummoSUOCgClVx4+vP3xAYqERihL8Rct12V+882Dfen8aAgUUV0bnn1zFUCM" + + "FgQA03hH3HEoAMLhA/2vPpSC6F1YgL0fD1EVBGiUBEAtunsc6GL0kBN1Fy5LgoAMgE2/Sak/RPg+" + + "U4La1/W4/YQ4SgICJs2qnDwvIgijxRU3VVfVOIUOxzsvH1exNr4DljYH3uodfQUISmq+N0GY0XFw" + + "TWHzA1Doy9j0wbwJRIAEKR7eNzj6GQBA8vLraqrOzwP8zzfNmjSnfNJl0cJ32d+bM9YgOCCbA3gO" + + "BAA45WpcWTOqZblpao2RAgFARFV1xNIiOMDoeCOcGwECzSuSKHcthOAAK8a5c29IQiiiMs5kfTh4" + + "Aa0ZnHxpFOdIQDBjJpTN/nqUACAExrxvx0Ll5pR+MJh5fTRYCUSYCxsrknVlAM+JACAjtdydBIXA" + + "UDjfsjLBkYR11b3JcIVVAAmCca/7fq0g6lxbiCCnzInWzw4rGAkBDVdVjrugErRF2gQJ1k6KLPlJ" + + "jShBOlvvQAIa761qWBQDgw+xJ0i0tCUAAS78oKHlfQ0owYxoAAJX3zP+qu9FYexZyNNSc1ZGlj40" + + "EURwGPhh7s2x8vGgT6QEJCabmdfEz2hiRcLoxh+f3/ZsXe10WAyXovBhfIq7Yv345b+dxDAgIjBC" + + "8AZtqDxcNyO8/7CXdRZIoOnOGI2XOwIx6yuxmV+K7d12/P3O4+lPcgDi9eEvzK+eOj8aKmMx+r9H" + + "QObw3r59r2cJA3hSiNrGZbWkz5/cSDoOpjVVzWisUrGqHL1r8fnTn6Bn7v90yy99dsiCrlhWuXLN" + + "RIL438LAEwO97vYnewj5qndza2H5WlIEtj2TyqUc35PVX+pcMGcMpRIiIEgW7aszvlbCAk1tcQOV" + + "VgUo7GnPHNqdJ+h9+oqknXtjshBVQi3EzaszxvoskAnMvTVWXu1ABEuphY5+nH3v1RO+C2Q5+Za7" + + "aiCCKJUWEgBpy5pu5UhPtydoekvlhKkRUUDJtBClwQF1rs1ABLzfb/Pk+AoGKCUCIHY8l+o/Yvxe" + + "eyHxecz6coIAwBIiYMX2R9K+SSW4YOitNUAQ/zf8C4sZTcVG5HPrAAAAAElFTkSuQmCC" + }, + "Bing": { + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAACslBMVEX+sgf9vwj+uQj+wwn+uQn+" + + "wwj/rgf+xAn+sgb+rgb9wAj9wwn9wwj9vgj+uAj+swf9wAlQUFD+wAn+xAj+ughQUE/+wQn9wglQ" + + "T1D9vAj9sgf9sQf+sQdPUE/9uwj+twj9vwn9wQn/uQn9ugj9uQj+rgf+vAlPT1D9sgb9wQj+rQb9" + + "vQj+wAj+sAf+rwb9uAhPUFD9tAf9swf+ugn+rQf+tQf9vgn9sQb+tAf+wgn+vQn+uwj9twf9tgf+" + + "sAb9vQn+sQZQT09RUE/+wghPT0/+uwn+vAj9xAj9uwl8aj39tQf9wgj+uAlTUk7EkR//rgZ8bD10" + + "ZED+tghnXUZXVEz+vghVUk1cV0r6qwjuuQ+ugij+tQiogir9rQf+vwjlpBGHbzj7sAhbVUtkW0do" + + "XUX7rAhzY0HhohJdWUp8bT3anRZdWEp8aD18aT1dV0r2rQrKnh23iiT8vwnRmRnyuQ3ZqBeBazv3" + + "uwvqpw/6sAi2iSXSmRneoBSkhi3wqgxmXEbcnxSNcja+lyKVfDOdey/rpw5eWEn+vQjzqwt8az3o" + + "pg/+vgm7jCK2iST8sQfFkh5rX0RiW0iCbDr3rgl9aTxoYEblrhKthChvZEL+twdyZ0GGczn+tgfn" + + "pRCQdDRSUU9+ajzYnBaOczX0vA2agTFUUk5iWkiNdTf6tgrztg2khy1YVUyjfyz2rQmdeS/9sAf3" + + "sAquiCmDbDqvgyiogSq4iCRiWkfJlB29iyLCjSBpXUVXVE34rgn+uAd/bTyEbDmbeDBxYkGIdDno" + + "qxG0hiVrYUTOoRzRlRlkWkdZVEz9uQfPlxrzpwuMcDaNcTbspA7goROqgirAjCDfoBNvYULTlhic" + + "ey/pog98Zz2zhSZZVUxVUk6SczTvpQ30rAvdnBRtYEPYmRf9tgj9ugf+rwf/rQfADIlEAAAE5ElE" + + "QVR4Xo3XY7fkShgF4GKIpo1j22ds29a1bdu2bdu2bf+PW0ljJp0J9rdeq/dTb1VqpVcDrpMU9ZAo" + + "zwedAlVU/uYiMob5SjD2CkDYjow6Qk2juFZP9HkFav1+CSvV/gW9AHgDIEyQRQwg6BJMy22K1V6Q" + + "9AhAOMdYHyVGaXV4uEBgbW8AhFm9T/o7aChUBjomWN0rAOE81kdkqDr93MJNAHgHIAwXixpqivGV" + + "RDWj6BWAaj8pktkFWSmvPrPESt4BCFWkIbIFV45Oyfpahl2AYyW/+fqQMTlUeXIJn15xBvzLbjzt" + + "uKOl2vUh2ajMG5HbewFwB4LLArOmX3XOkSf4YTCfQOE8VvTdK/LqQZDyCOiJrLrlbHZ9xuTy8njL" + + "oPHgvAMsh2Rnx6i++xDumGyxNt0BP6Z6nUZfA/Z90GYLHGicvVxoAI4RHQG6sAQEZ6DLAcB43Afc" + + "krYH9HvjHq3PDMC9hzgCvISUhhCpARBKB1WBwwWQOmIYpF2AYjhWCKMKAGGrvC8Atl191HmCGyDx" + + "eAwRHZBaFV4xAacEAjOWf/VNSnACoIypP4yiPI7xPKYmILU8o1srvz/mJAeArcvPbIpSHvOUFi7a" + + "FxCWGEAmHl9795LfBZstQGy8pvmQjBeWkoftC4DtuwLVZNYcf9mJ+wWkVqowA+NR9r4BZgCcnAnU" + + "Eg/M2LPp1JRlC1DClAFyPgsEUA9cc74B1IxZkTO2mgDEzkCWeSq3N8gyHEnWA+DZeKAuB5sAqMZo" + + "CGN1sBuHZDpTswCnr3IGFiqUKrFu0NIxNyTzIbp6fh0gnHnW4kDcHqAKjTb1l4DQwTOAxXQPyrn4" + + "3J97bAGpMBURhHxCB7UDWC684f4eG2B2GKpNiOSANhSTQzaAsP3SndNtABSW9N+fRF8OoamjWNkP" + + "kLz8iisztlsoMoAFfpwrEg2xC20BUp8sj0QCrgDUGFBcRMarL5T4Lh0QknddG2HPwA1ggjqvQdPf" + + "LPMr/euu94Hh5JfbIgFzMosP2C8gxSAEvhwhZaD55vW09dbbbr8jUJfInUbfCvhjrf4pothVvonN" + + "r9+D6fp778uY2/GenUtSgh2A4RSO5VB9mQceJA89/Ii5zdg1jz7GKnYAX+isAJHHn3jyqbUBy/RP" + + "bx0GToBSBjKRZ3Y/93xzc127eeULKQAcAd4n6sDiF196uad+9VmvbHoVABcglG3kuIE33nxrunX6" + + "t9/xCUk3gJdSHCe+O8Paf+99KMFEt88NCDaInDjwwYfm7cc/2rAuPdEOWdobFoAWJwB2sk1wn34W" + + "2Qv0RD7/QuSWip0r5hmGWppMOgDdIgPEga931IBvv2tr4/SI6Y0o3KTqSHbCZwfALn0ErvGHH+PG" + + "8jt+2q3PVBa6wGA36p+jQj/MJ7p708kWCxCEk+XlBjZv+IX9LP76m/GxRkwDvTmkkfHqgbQIFkAV" + + "K4WBP/7c85fImdIoTgGgL4eKCIUTKszDodKkGWDCYFUQ121u4+rTJqaB4FuhIYKIYeTrgWC7uJRz" + + "ChuC5W/N+AuCxqdaANjnDLCznAbAtORGjegEsQIJ0RkwzlLPSE6fwgpA0OgqdBmEMJIjFoAJ/4ic" + + "W9oqQwDfCisQzKcbXQWRnWUlVgA26CO4byNtAzDhXwZ42YYdIE38xwCPQ1gBFtjFAI9D/A+btEBN" + + "Oyr4fwAAAABJRU5ErkJggg==" + }, + "Ecosia": { + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAN9klEQVR4XuVbCVQUV9Z+1SvVQAPN" + + "urQgiAiouCcY3BOjOCExjkuCGDWa/DHbjOPEjEuMSeY3GZ2YxTgmkkTNoAbJuBJ1EgUNUXFwRdki" + + "myzNYjcNNN1N71PfOXlnQIVuZmjhHMrTx+9offd+71a9V7fufcXYbDbSnw9ebzit17fJDpdXz+y3" + + "AfjgSsHqRadzvr2t0Qb2uwCYrFb+7uLy51vNZo+P84p/1+8CcE2pHtZkNPkCZ9xWPNEd7tGKmsdH" + + "pZ+8wNmI7nMBuHKncdifL+e/oWwzeHRFqNBoQyku07RGOHjX8JadubjtqZPZJ6+pmuJUbUafPheA" + + "W82tEW/l3tgc/e3xgpx65ejOCAz3h2KrzTH/L57N3fZ1UfmroIPny4rv9LkADPF0/wWAuwOCnjyR" + + "fUxtMLrfjzDIw62EYh8X+wM5UlE9g1szVlCOr4tYEeMlLe5zARjh7VnwiL9PJvAdLggphaXP348Q" + + "y503wFVSCjzax+uyPbsfXS9eSa88jleGDf5UwOPZ+lwAGIYhBx5/JClW5pELwrk6Zfz9CHzuvLWj" + + "Y94DThocurcrm2arlTlfr5xIucNkHpdXj4zeSnrwEPSksWBXSX3ub2eMT71VMV9vtkg6ndMxg/bo" + + "zGZxcuTAA13ZazWZWZPVJgGWu7KlR2ZOnM0K+Kae1OxoKoyrQbgzGSFuvwd0WKw2xnv3QUViaFDG" + + "lvEj/xQgYVX2OFzg+UIeY8U06dEAnFU0xE09mpnNLUA3lkcP2snNxZ1cMKyOcG1mE89Y9HOc8UbW" + + "VFPppbFmxS+RVnVtkM2gcwOBEUtaeV6BCkFQ5C/CQWMviYZPzRJFTchhBEKH7Ku4R+97l/PXppdW" + + "zVPo9AN/eGLKlOnygJ96NAAWm40ZnnbiSmFTy0gQpgX5HT82a9IciUBg6HQO15aEaI9ve6Ute98i" + + "a4uyW2kvT+pT6zIx6e+us17bLgiMqOyMUKPV+U04fDqbyy8iwRvs4Zafv2DWCO7iWHo0AADXVero" + + "R49lneYSkUAQXoqJ+HjHpLEr7wmWqsZPs2/du/qzqc8Tq0XY0SNj5vuGlvF9QioZibQFBJuuRWpR" + + "VoZY7twOJzaboGMk+CZ2cvLX7kn/v4HvHdxwt69nT53f9W1J5RLgUDfJre9nTf7NUJnHLYcHhQB0" + + "51ep0QYszczZ4f31PxTinWktJoulA0f745cLa5OkKsXTxEZ/tYtkdU2fr/hIf/n7aRZtk6Qz2/g/" + + "nINzwWlvAzZh+26O766DFeGpRwvXXby+rtlglHR3PP9TPUBvNvNYgcAKbDO2CZv/9sJ2/U+pL1AC" + + "z1te5jZv/buSKc/tY0RsN1Zv2NMLdWe+SWpN//MGq6o6nBLYSckpHi+nvMKIXExOfgo4Dqx6jUS9" + + "KfGgMf/sDIKD4Zlcn1r1vvuCjZsYscTQboUW/Vx3J+5SQ+O4spbW8EaD0Qt8mVikDpe6lY31k+VO" + + "CPDNYQV8I+VwC6VYk7ZxrfbIh2uIzSqEHdHQyf/0WntsDo911/V6AHDlG9+beYwOnnH3rvNalTZX" + + "HPvoOUrQthUPP1Z+4bXP8jXz6vSMZ6nWG67va89NIGiaEy4/8PvYIZ+M8vEqoDYMeafj1R8u+M6m" + + "UQXQIMjeOpmIO6FXA9D08aKd9LbnyYLKvd/JfFQQPKScVn4Kar/4q9SatphhCA8EuEv8eQmpbZPa" + + "TQOSIkJ3b40ftdqfdWkE11xTHKZ6e9ppa6MijE4Hz9///cVeS4V1p75aiMHTK08HT/OG2AMnrrfp" + + "M5Zi8G0WPils8SVv5iU4MngA/r6S28tgA7ZAgG34gC8Q4BsaeiUAeNS17PrDp3TO47angy9UXk2Y" + + "8X1WZoPeIE+viiWrryeQiZkrSPLFZ8nphsEd7Ih5JjLNr4SM9arixNyb98DG4xlnMrkaYgINAnzB" + + "J/jQAC0PfAo0bVvyuT5rz/8Bu85+413pc5vfBq7VFMRV1L2aueraDPaCKrRLG8M9asmWEceJr1hL" + + "cCgNEpJ9J4ybHu7EU9hGAlw0JIDVEH+xhrACRj/Af/u0UI8hOeC2fLP6He3hLRuA2amLv/B8bfdL" + + "D+xlCBkekhz6qMNqD6w1tsgOF29PP1U7ni3XenXKnx2UT3LVcrJk4GU6eBzER6wjT8vzO6Oxt+vX" + + "pXu47B7hKXZrhE999v5kPCKhxW3u+k3IGB/IFEB6SzM8POfpo+7Fn/I3v3xpovxgzXBS18U8Twgs" + + "IunjU8kk37Ju+XXjK+Q3FCmbgeETvoGhBZqcvgbQFxvk9sCMm6weSQ7wVaU6hlu0ltjjB7PNxFus" + + "I2K+hfCY7gvmmw4tqWypiQGGb2gAhiZoc3oA8FZHX2zY+AX7aYaHEjf02eOvicoiYa5qUsFNkUPV" + + "Q4nJ2j0JLnwzv1r9HXwR+IYGYGiCNqcHAK+0FIvHPXGMZngHy6rnO3L1R3opAEmoRE0KNX5k7vlk" + + "ckwRjQKpw4eh7cf5bWaziGpor83pAcD7PH2rE0XF5wAiveUaHZ72Bv/FmIOE5Zt/pRPyJ+5uWMwt" + + "hDeaA8hfiqY4rMFD0OhZ21oUBwwN0EK1OT0AKGYA4JWWJ/HQAV9XNoyzx3sz6gwJZDUdnTOEzJHn" + + "k7XRWWRN9Jlu6TAYC+CTQAO0UG1ODwAqOQB4n6eEJ/x/CH8r5hRhuyjXydnm7r9n2FCKY+4fAJMi" + + "HIBqodqcHgBaxkIx4z9Km2Wzgwu4K5nZKa9a79G9NNssJCuvJZKFXPZosd0bBCFplAFQLVRbr5TF" + + "eYwNgCQEFJN58rz7prSfl8YRJEdGB1b9ghY/knTxGZKtDCMlrT7kUqOc4PimYhR5++ZjpJULjpBn" + + "YXqlL4ACJi1jUQKfJ1UTurBxc3n/+H0kRKK+a1D+3Iq/iEzKXIGBdOljT8UYUqXzIvQ4WTeEVGg9" + + "yae3JpCM2hiyq3wcEfLdmkCgWqg2pwcA1VsA1PAoQSgILu3Q/nJtJDKR7r58k41Ptt2KJyoDe1/7" + + "p+ojyNmG8A6cDEUUefXKbGL7tYZQ1iojIkFQGTDVQrU5PQAoXQOggGnVNUuAJeKoS+0JuY1ycq0p" + + "uPOFlHNbrpXdY/tyYzBZkzcTQbr7/A71g0FuKuIqjsoFhgZoodqcHgDU7QFQvTUWnYsD9JIMv8Aw" + + "Li3AzSYxSSl/yK4dmUh/j+2M2igM1i63XDeg2Y0deR4YGqCFanN6ANC0oNiQm5EIwOeJjB6S+CPN" + + "RheyMOdZckUtt2snRNJ0V+eJIXlNgYTlG+1zPePS+DyxgWpor83pAUDHBk0LYP25tGdRvQX2kSam" + + "vFf4qMXBag/RmoUdBr8xfzoZ7K4kC0Ou2qNaUC+klWNooI0UaHN6ANCuQscG2Nba6I/SNbBU8lB2" + + "oHTabkft5KoHUEj2Vo4iJ+qiiM4sIodqhnXJQ52QFkvhGxqAoQnaHkgegHYVOjbAqNujdA2MAqYf" + + "K662bwF5wcOkWiclx2uHkB0l4wmOc6qBRGV07ZQD2/BBy+XwTbtH0NQnSmIoYKKGZ7RaWdKDh4jH" + + "03NNz2mTg/wcKYk5PxNEr45hpY3AaFqgbg8MgWnTH/ktBPfk4GGTDh6+4BMYGqDlgVeF0aiULt36" + + "OjA6NmhaoG4Pwuww+QlcLfvTwT6ADdiCTdobgC/aJYIGaOmVvoDksWV70ZwARscGTQsIpHdC3vyE" + + "Edyi9RVWbtL9wwIubMBW+8YIfNHGCDT06h4hNCrRpgJGx0a5Nv48nQ7o6Ox9bPzyK3NnxD4XOXAn" + + "2l727OEcnAsOuLBBb3vYhg/aGoPvzuw4rT0ObDBb+Gdr6uNSi8vngWPRtUiU6yefpK1sxRyesXnP" + + "G+9Y27Ti9jydySz6oap20qbL+auWZ13cPudk9j78gPFv+D+c054DG7AFm9Q+fMEnWvMpBSWLDpVV" + + "zVK3GVz/m7F0m/BVYWmS/+5DlWTHflts2vF/UY7VoBeqP0re2b6nX7dcXtr6z88XWw06YXf9gAMu" + + "bLS3CR/wRTlBew6XQYv7l+mqXYWlzzg1AB9eK1zBObPCIf7+9tbtp8DpzQ0Se4rK5rfXtOPmrcXO" + + "2CCBTq/XwNSjVW0Wq6uAYYybHo794xsjo7eB0JtbZAC23fhl+R/OX/3MbLOJ3YQCdVXykyGeYlFr" + + "jyZCOfXKUe9fKVwzVCbNfy4yLDXKS1pKCb25SYqCAnVzxJeFZcuK1C0xG8cN2/CQn/d1J2WCji+u" + + "DMM4cZucfdArAeCuQPiyM/9KmRkSePKtMUO39JTd9NLKJzdeurlxZeyQvy6LCt+H4Pa1DybwLUDw" + + "5KOnf+L2906zWK1CB3afMh9cLXjdaLHy7eYbImFzgbpl1Atnc/dyv0/65BcjS7MuftmgNwQDR3ra" + + "39K+Na/4tTUX8z7Zcq1wpT3bU4L8fx7gJikD/qqo7PWM2zXT+1QALtQpR59RNMz8NZtrTggJ/KEr" + + "gsFi4W++WrgG+G/5JS/bm4oiPs+yfcKYlzjBZnC4zZHP9KUAoD84keINY4eu9xKLNF0RztcpH1YZ" + + "jAHACp0+rFqrt7vNJXFg8I9pj8c/7eMiVlS16uR9KgAakxkruu314ZEfrBoR9Zk9wo3G5tiOW+NN" + + "no74mRs+ION2cmL41kdGrepT3wtMDPTNzkiYNP03oUGnHdzhLWvPZwUCjaO+sEF7jK/sZp8KgKPb" + + "0ynwZcXKdiu8klvg6vrNl6MAU4P8szBlgJ8Ok3/HZxhbvwoAtrTPHBBwhOXzNetGx7zfr74dpmDH" + + "pHGv7Hts/IIID/fK3tLQ7z+f/zdtBQxrg2hCXQAAAABJRU5ErkJggg==" + }, + "Ekoru": { + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAACplBMVEVCrUIApdYYrbUhrbVzvUJ1" + + "wUaBxj8Apc4AnNpHskcApd7O7/f///8praRovUc3s4dCtYRKuXuMyDxuwFWHyU1WvWtnv2lUvXUI" + + "pcYAnM4InM4Ipc4Qpb0QrcYQrb0IpdZKtUoYrb1dwNuUzjmczjmUzkKl3ufG7+9St0TO9/fn9/f3" + + "//8trZRjwWMYpa0InMYhraQQpcZzvTl7vTlGuIdKtVJSuVIMnNpLuGmMzkJauVZhwFghrb0ttb0Y" + + "n9Mcps4mq881scpBtsI8sNZHt9BOvc4xsKJoxdNrxt5zxt6E0uK11jE8sp95w1Jctz+czkKUzkqM" + + "zoyl1qXG58aM1udjvT2p4uu95+9avUrG7/fO7+9rxmd/x2Te7/f39/d4xXCDyYeMztYApcbv9+/W" + + "79YQpa9GtT0rq9ilzkqUzlKUzmsIrcat2JGczpyt2Jyt2qW93q233re93r3O58pSvWOIyliU1t6U" + + "1uec1udSrUpMut613ue15+dcv8zG5+fG5+8ArecIpd4QpdoIstt3zNfW587n7+fe79Z3zORjtUoQ" + + "nM4Qpc7v9/f3//elzjmUxkKc2toYra1SvUoYpd4Ard6t5++15+8YpcbW7+/e78a33qet1jnO571S" + + "tTne797v9+ec0I6MzpQIlM4Ypb2t1lKg3uJrvTkhta2UzlqUznuczmMprb295+ex1mcIrc4InL2U" + + "zpQYnN7O5++l1oiE0taUzpwAlNYQrc6czkrW9/etzjGt1jGt27re9/e93ojn9+cArdYAnOcKqrpf" + + "v46957XL57e13sa93sal1mul1jkApefW786t1krW796UzozW7/e11kqcznu11jlrxka11lLn79a9" + + "584hnL3G584AlN6t1kLG797v9/8htb2lzmeM1tb///eg1q3q6+s+AAAJxklEQVR4XoWXY5NsSxqF" + + "K7dVtG00bds2jm3bNi5t2zbGtuefzJt7V+OeqohZ1dHR/WE9+SpRqmi2RDGeQ+JkNIfuzwJM5rZj" + + "gpiLcQ9AXLKL45GRzsDhxsbG5sOBzoNHZWwOhCrX6mI00tlSf8WLOI5D8OG83vrmwIGoiBFZgKzc" + + "pyKBDVewFSGWZZEiIDkaOsZEOYjcgEnFHh1pOY29SFCcCoITBGRH3J1ABCNyApTlowdanEjgBMQi" + + "zs7abC6QzWZjZRJEJZwOjOOGZAOUGo0dvgzL2WFR27q8VEpn3Quq0OtT6nX5GCAAecNBcSVBtdI/" + + "Uq9E7XSpdebqiYlYjAepeD42sVendkFFcGKnO6IrCqFa9k9dLBfwGm/k6dLVfdjrKy0DlZYe4lXr" + + "fVXmtnVKPbiW+5cJquX1O8oF3DeXtlrTx6t8VQvbdg+vBZ0b3jU4X+Xj+TJ9ygW1AW1YJqiW/J0O" + + "XCR7gZme4PnShd1rpJVaM/yUBRC6PMQh+GmJxsUMINM/8QBeX3C2pak+3rew9j9SluaG53lfhdqG" + + "7E6OC0xlCKpMAPfV49a5eoxUTGXZc03KqWvDab5M68IxnOqCqZcBygBED0PvkT0Fft+bXyxbrs4e" + + "P378wURyEXH+TV9VyToBCPVjSjNVsl/sOi0ILCqopmJlQ3OL5lffba19pQb0z6azRxKZPHZXlenX" + + "4UI0K2VQyQmMb8Aj8oyZjvGL/tl3m0iCIEmSAPn9/qazJ5JKGmW39S6EkPeinIRK7sBFL/jzS4yX" + + "fE8p/qv7a8FHYj9J1PgxwV/TPiWj/3KoqoQAQsN9cQxYCgAVGGnVvNK8qWNuD4mlxnrhBUABw1O7" + + "/yom7C6d0NmgZwERCCo5gFMnEXJZoQBrZf/RJsJPki+0legtM6B5i7VETfoJwuM/NosJQ2XVKQdC" + + "dWMZQFQOIGXU8Ntk/6u1hJ8ge8wzA/3910OgYKhyxtpDEh6Pv/VBXIfB2N48xJ7CIWDAAe8ZhBxW" + + "+lLZeew/3gR29TcmbO0PZdT/4T4rROF3N2LC4+kJnROdrB+XAWJAuItQXjXFL8j1a4Xwe6YHwGsw" + + "MHRxMcMY+kPB/tANS5vf4/G04oZ+OmHO4wRvVzwOgGjDSQF5tcyobw8GrAK/dh8OnKF7eze/9NJM" + + "L00xBpyJUe0miMJ2vDUsE1qnHbWIGBA5DQCb2dCXxi18sJYgtTPXP+5nqN6tF9bgjNec295LMTeA" + + "kC7wEO7CI0DYdcnsgjKOY0C7AD0gjTdigxLoLFHTNh0MhRjqnSekJT2xvbfIEA5XWkmP3/1KAodQ" + + "XYDQqREAiC1QAlZNF/NDYEg0+knrQChEax5OSit1YYYyGMImnZvwuFbhVlZpEfoqAIBo3ckziNXS" + + "Gt85MBzxE21QAAP1zr078onfUpX9wTTpcRc+ACGsrdbbWKEZunD/5ZNfIqeOnjh0AWdAkiUDoTDV" + + "C/Hfo/MzVGXQpAOA7SbkkDa72K/qTqjiEQeUwKmnYqWPQw+bCPU0BKDZLmXrW01xMGy55XazLTDS" + + "C1V5SHCMq+IjXrkJRT48RrO1ZA9kwPQCDAQdOH9+bulY20wZwjMp9we2B2Cadkyk4M47qop3obsA" + + "2EsdkgGvECXhUKhoZk62X9j6k5c3P7kbyLL20IzBpHW73baDkjQ8oeW48ucw4AwCQFHMB6u+Td6y" + + "hkPXi7bKhu+efeSRjc9ufHHhT5kQoAo3rB4gBJLSeYvOznn34xQwwAyAtQoAZpjaiQ0P/eYXr33+" + + "2saNv/tw/pcK4clRU6UFAB4owpppHYu8EMH+8kXALgD4MSBIP4oNn//hoWvS3M9f6+7+MHPObB9l" + + "DBhgg1mam9evFjBgrPxnGED14UGarSH0YZgCGfDQH5UR2vjJT+d/kAE7MgD2gVnp2rzeJnjH8CAJ" + + "AgbE+HlcRA+MQZD6buV98K/uT/Y9Jc/lTg1jmJYBcLr9qsImOKGNkxswwEpN8JaklGzy40EsfvhH" + + "AMjhm7kVgELWOS5JgxU2dBkA8VbhLuvQ9VLrcR9XeQgLbP3Ncytm+MXuj//Wk5BT6GMGrMTXhTbX" + + "CRkgNEwCQD6SUxpNDJ8HJ2r8WlOwEqq4pJ3B7u5NPfJxuhVqUOJxnXHiSVqocAot4uJ5UFD9+1js" + + "PUlKtnsKpoNBw74LS1vgxf7r3ZZjSXkORhmmZ0vhGbZ+FnfBIcBuzJypLjOlWW+BHThVS2hNoWDl" + + "vm+VLC68HLp+fVPJTcx63EiZjAWsm2UbEnAi6GCOMEAMINgVOtr0mLyhb9aQ1k0QA7P54Uf37Hxn" + + "IAx+a1NCko+hYpOZeL3wdbYVJjGtFa4cxQCYRSSwKSMcim9CCFfP+tUWaGU4bDDduAFnU2jA2nZE" + + "bsdCH2PSuWwfOFe3J6Vze9XchkkAyDkglGehNHwV3jaJdpkQDikKz1jbVslTsNZIGYwFhZ4tbH5E" + + "kr7f+2euIy4DxA4OZlFP0zEcAo6hhiyZGcDmgU1wo9Sukltw7UkNw1hdq1ezuIb/eK+i8PQYBoAi" + + "dzh8MdCXeOVkTx5p8qv1lulpuNPayKbnkpISAG0wpraAH5dgdkHLNYsZgBjgOHyqUbHF2zXxbm0N" + + "3I5wM9eums3s5QUNY9K7PBgQkZJ/t+aV74+LMAdKCAKHnkkXaXjfECQhI179d3vrsbORxOLzZhuM" + + "cXUBuwX89Qnps+/1TvmJAQAlBMSilIa+zVftzv082nGJMdBaG8t+ybIXpavif1N1kbgCwBqrRxyb" + + "r9XQPlXZnlz+oZiRYfRfy8/3hkTyo+PfPNOFn7yLALHTLnCCUwc7gq8azvKvGYxRxcXmPCQDDkqf" + + "xafaR0SwKW8krGizwLHoDT1NXeJLt93zyNxliT0P/lssbiFqTCZgbRH7lWeeEsJYPYcEzqU30hBE" + + "escXS+5zQ5ZYn7GYtuax7GpYv/7tRFxRBjCZIXQ5IQmhUGekqNHH+KrBoR9Anw5afHxMw9BGPfhx" + + "/JcPXv1o2a+8VDOEDq8gQAwps+b55zW3fbwPxPPrY30UXZzWusBtRw5HV2LZv/KxDYoGyjmEkBNe" + + "+xoNVUSNjvaNauCvYli+wMauRo6n3y/v/LW46J/MAFYSHPi1JTjztPo0TVG0IrO2wAnR2x3vOy53" + + "isv+rC8cOIsrwldQCc7uIlPaCrPZatZr1eucCNntDke+o2Eky48BKwlddWDn7Ahxdnt+vi0/34EU" + + "99OgxgjY7/ED4MeESEs553DYQQ744F8OB2De/+vTdR3j8Sx/1tc+KERXcx3orbfuLAv+bzh2n5jL" + + "D4B7CdCesciBrs6OTvxzsbOza+TA2Pjksj33F08ljf+nbH/0f+C6zXUwzYSLAAAAAElFTkSuQmCC" + }, + "generic": { // generic search image to be used if no engine logo is present + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAATM0lEQVR4XuWbeXRUVbbGv3PvrUqq" + + "UhkqA2FIGBQZA2HQfoKi4IA0Dg2IQ4s2iIqNgsrQyCCjIA0oo0AghKYRCCKTMgYEZEqQKQ1CGAIk" + + "IYGEIfNYVffe/fZddXHV48Far/utssX+1vp5bpn8Ud93zt5nV0Xxa5QQQmIpsiwrEotf3/5zyCzz" + + "5zIgBH4DEoYZ5k5mJCbQxIo7SGYZwd2bzoWQfXc50Ga/r2PHTn8aOvQvCxOXLN3//fe7Mw8eTCtI" + + "S/vx2sGDqblbt6UcnzP3y3Vvv/PuqJZxrR4FEHBbEOKesS6x4JWlZVz8S3+dNiPl5E+n1KtXrtKJ" + + "9H/QhnXr6IsZn1dPHDe+eOyYMcWTJkwo/3LuXNq+dRudO3OW8vMLKDX10KVBgz+cGhlVqzFMGaXx" + + "a9/1n896o/se6LkwIfEf16/foGNHjtKoj0fmtmvbbmeQPSgJwDRmLDPcZCTzKTM/3Bm+ocvjnU/N" + + "njlTvXThImVnX64ZOWrMHD5B0bfK4ld75M2nyD/17b8qj3f7+LFj9MpLLx8JsAYsAPAR05t5lGkG" + + "iHoQgVGAEg4ggqnLNGceZ15nJtaKitoyfOjQkit5eXQ8/cSVhzt07A2WecLEr858kCOk7cxZczMr" + + "Kypp3CdjM9n4PABvMp3C6zSJfey5/gFDJiy0JK7eHfrtzvQGO1IzW2354dSD67bufXj+kuT4vgOG" + + "xDSLa28DhAVADPMkM6xBbP1dq1eupIrySnqz/ztTboUgWL8a86Fh4R1XJX9TfK2ggJ7o8sQOAB8y" + + "nds90qP2iC82i6OZ1VY3UQMieoj5PdOHeZ8Zw8xgFjPL3Sol7Np3bNDr/Qc3sdpCBYB6TFdm1rCP" + + "Pip0uVw0ctQnST7lIP6tNQ+W3e6IX/b3lcVXcnMprmXcagCv1Ylt1mzE51uUzacIu7NInL5BASU1" + + "al1N9bRTVbWzR9W6q6r2KvMuvx7h8ahTVNWTQEQbmcPMsfRTF2c83+v1GAAWIdDaKKM3Xnstx1Xj" + + "osEfDJn/b22Mwivuy4pzzNiJF4puFlKb+DbJAHq0/l33ektSCrHzAontZ1Vpb44qDhToOFJMuOwi" + + "m4cojoieYXowfZkPmHHMF7pOSRzKWg5jHxFdYc4tSFzRI8AWAgE0BtD/zwMG5FZVVlHXZ34/2CeE" + + "f0/dP9PtueSS4hLq8YceWw3zjz87MHrtUcKWUyTtOK+KtAISx0v4BFSTOK8yOodAJKqJGhPRs8zL" + + "zNvMMOZTZh6zgtmqadoBDiKbiNSUPUeG20OjwWrE/Hn+vHllWVk5NVFR0a2FEGDkX9x8reg6z6Wm" + + "HabFCYvOA+jX8sHn6yenkvjupGr5/oKmHLpGyokyUs64Sbmok5JDpPCWWq7ymk8EDuF+IurFvOHT" + + "D6YzicxaZjdz3O325BDrh0MZQ2o3aAdWk7DQsGlnTp+mWbPnHgQgGYIpAf9LAEJ+7/0P0j8eMax5" + + "mzatZ8iB0V9PSTqaWbuuXbLZgZBQiYw1MBCwKoAFgMJIjDnogwC3A2hqBeIB2JkQJpQJM3Ey4Uwo" + + "l0WkoshRe44X9nm11wvbr+ekPtD7xRdnLlqc2LFDx0dePX/uzNdGKei6rkl+3n0FAEVE1nq+X7++" + + "cYmLFx4tLi5Je2fU6oLouo5AxUIBQQ4pICAQgQZWhQECLYxirjIQoDD8HOICcjQgF155TFwmNUyV" + + "sXLDLyVdQ8sWEfcPHLusxhHerPzbb9cvyzx3Ths0aNBoM18d5oMfRTpYj3Z6bGB0VATmz//yhyd7" + + "jslp16GNpGuaPcgpU0AAYGBVGACKicyYE4wJBAEBbuBiIFBbADafENy3giCgQghEV6jywvRcbe7D" + + "HR8I6dl/ivrVF/1OJSUtTh09dkKnTz4Z83hZackuozwVf3Z+ItIVi7Ve9+7dO6em7s/T5IhTvfuN" + + "rNFUPSg4TILVCljZvMVqmoa5mggf4A1EIsCjcggWoJa5i5pPEFUCiHQRtl+qwXSHXQqPi6BpFX/o" + + "vuhYWp+Cnbt37x43TnTq0vmJN/hE7OIygD9LQGbgDI/sHN8qzsKf4n7q1mPANWeUwyrLcFgDhJ2N" + + "2xULIxjALvsgGMlEMCaBEhCqAdU6cN3MSTNxMVYPcCRfYD7JEM0ixYehgXgprnHg4526/lGDJexi" + + "Rsapqq5duz4DwMFoCvys2JiYDg6HHWXlNZc6Pt3TraoUbA+CsFhAigIosnncb9t13wFemPi81FUO" + + "wArUBxBgBmBVgbPFwHc64Glow2uhAr0JqKzrRJdmLeI3n3vgd6Vnz57PbNOufTwgWnATPOzPAHQG" + + "9erFtFA9LkREx16vHdvY4tJEgMUiSJYBA0li/ne9/yzpLtcKAaoOlErem8CiAZcqgEM6QJHA0yEC" + + "LxJQKiBkhxX1o6ND6jZq+pDrZlFWbmRkZHxIaGhz7gN+C0AQkW6stWvXqauqmrt+oybV1kDF5nKR" + + "RZIFSZJpXvgE4GNYmNxFQgCaDlQa+alATg1wgQAlFOhk4wAAVAtAIkK1JMgRHiJiasc0zRGleaVW" + + "qwWhoaENOQD4uwSsFqslRJalmrr1YomAYAmkSEIQhNeFr8Q/OZzogOQBLruBEgDBxpxgAeLNW0Hx" + + "6Q0OCYgKi6h9jdRQN0iHwxEcIYTwewCSpmmSlVt9rVo2pYjgAIRCAOH/L5DXoSazOSvQUALqAvCY" + + "5s0fQzXQCXZHUHCQ7HTKRAQhpEBe/R6Ap6a6pkqxWMKiIu2OkkJ4NIKi6yAiQGd8RT6I/1sAJAOK" + + "FYi51dUZKyOZAegmbiJYA23WYIceEsKbgqqqqprw8HC/BUCMYKmFhTcLQWjkDLE5rWVQtXJvALoG" + + "EKMTIxgf8/BZ71YS5FMGBAQIQIbPFG1i5ihcbhWSPUAKjQgMC6uoqERlRXlpeLgT/hyEJCLS8vPz" + + "s8orKh+MbVAvMtiG6jwNVtUDUjVAu4UC6PAi+ZgTvs93OSk6QAQownsdWm4fJIWA5NbgKquEHGHX" + + "nBG2kNp5eXkoLLpZcOPGNb+WgGBw48b1o1nZl3u3b9+6XmQolYKE1e0CcQjwMKpqBMCY71r3uQ3M" + + "Z9P0XcuFNMAmAYGmeYuJQgRJCMg3y1DGgQeEBakOZ3Bw3e0pp926puVZLFa/BkAMiouL9p84eVL0" + + "6vFswzrhuBoUCEt1NcjhNgLwYrEAsszcZQagO5wA+JwAj7cEAs0MFWEGoPNq4fViPtUE20VQWJA7" + + "UlaUkAOpqWcA5AB+bIJEpAEQrpqq48ePp2fnXcmvH1s/tlZMFHnO5QqppgawBpjvkJElRtyWoE8h" + + "053rHxpAvCrkbX4ymSEYqxCwVKjApXygZSOEhdmoQXb2VRxKTT0qhLgGluLvL0Pcbrfr3Nkzq/bt" + + "Tx39ep9XYprEUGFWvlDKy0E/B6B4AxDW/7m78m27L24zT4zKsCQjBEYiL7KmQgqQYPnpIqqtFmGP" + + "iagJDtSssSkpO12FN6/vlySpTFVVv1+DOoObNwqWbN2eMrT777vG1A4PoyaxhJ8uCtgCAUUGfP+8" + + "SRZGAIpPUxR3aYK6GQCZbUMnCF1j8x4IaJAKKyEys0l7pLUIdciukOy8awHJq5P3CiGOMzW6rvs3" + + "ACLSjVNQVlaadezI4SXr1n876J23+0W1aqi7C4qEKCr27r6QfGpaZ6yAJt3hPjPRb5tyNB0gDULz" + + "eFHdEMbrU2ehNawjlPrOakl3i7AVK1a5zp/L2CDLch6E0AAIBX4WsQCIrKzMyStXJb/aocN/Rca1" + + "bO55uIUudh+XUFjodUfE6LeuRp/SMEdm6fbdJ69xVfU2Uo1h4wZCN+bjPCDQCnqoiQYLgF2pP8pL" + + "lyZtEkAa73wREYFFAv4XOHGFj5saHBL2x2e6Pfu3hIXzLOHOMCm7UMf+ExLcGsBDGWxBjA2wBjJW" + + "wKIAEiObJSJM80RmUCqgegCXC/Awqse7Xr8O2KxAl7Y6nFYPnb+QLd56e8DFtNR9owHsYwpuu2n8" + + "2wg1TVOJCNWV5Zs8bteNseMmivLyCr1hhITO/CaDbd43XVIElJUB5aUm5UAlU3H7WualnCkz19IS" + + "Y+YA8q4AzmDgifYaIm26fiX/hhg3flIpm5/PG3GSx/Ib8JHwo3Fx62Nx27Zt7+MvQ21PPdU1of87" + + "Ax798VCanpmZKU2eNIGczjBRXK0h/YKMS1e9W+JwmCchwFsGkuRFmKWim6WiqoDbBZ7rwaMtoEhA" + + "84ZAfEMNNousX8rKlsaOm1C5ZvWqL3gPNgshTgGo5hLwbwASi+uMANCUz6a+N3rUyGn8d3zrhawc" + + "a+P7GlGd2rXE9M/n0EkekCaMG4PGje/TCZAuXddx9rLAtSIBlbx9IMBqzgnmlAQdUDVv3bvd3hKw" + + "KgDPF2hen1DPKREA/fCRY/Knkz8rTdm2ebbH405h82eJqBj+lqIoMljBwcHyipUr5xHr7Lnz9Pms" + + "ObRuw0atsqpazzibWfPkU09P556w6LkXXtTXrN1A/KlRIyLd+EfOTY0OnNVoQ5pOX+0mStpBlLjd" + + "JIVoKb9esYdo02GdDmdqlF+qEUtntOKiElq0eCm1afvQOUmShwLowOYj8EtIYYHVomXL6MOHj+wg" + + "1vYdO9V33xus/3nQhzR12gztxE8Zeq/evZMAPCwEou1BjndbtIy/MPD9D2nn93uosrLSMKIxuvFQ" + + "Uk2UW6TRhWsanS/Q6CKvV0t0KnPRz6YNiouLacPGTfTSK69XRURGrwPwRwG0ZfOh8LfMY6+A1a1b" + + "t/aXc/MyibVwUaLntTfepH5vDaCPR4+jlJ27tGvXb1Dbtu0+5t8P5aYkzHbRNswZPqdVfPusvv3e" + + "ocSkv1N6+gkqKSnxMXk7Ot28cZPSDh2m2XMXUI+eL1fExDTcJcnyMABd2HgTALZfwrhgZLD6vdn/" + + "Ra79cmM3Jk7+zNN/wEAa/OFQGjBwEK1M/lotLC6hYcOGbQTwNAdgNZslTDn58TGbLWh83Xr1d7Z/" + + "sMOVHr1e8Qz+aDhNmTqD5s5LoAULE2kOm5346VR6d+Bg6tb9hZJmzVudCA51rhBCGgzgSaaleeSl" + + "X8S8sTCYNn36cGKdO59JI0Z9og39y0gaO34SvcUhzJ47XyvgnZ82fdpeAC/wzt9/2xsUhgBYmDCm" + + "GdNNlpWBHMhn9qDgRWHOyOTwiFprHcGhywNtQfMUxTIWwFtMN+ZB5n42Hu5/4yZsQgIg+OtlrP76" + + "69nEOvTjYW34iNH6pMlT6a8zZtL7HwyhcRMna9k5lyk5efVJAC+z+Wbs1YK7yOfP1zYzjDrMfUxT" + + "piXTgmli/rsYY7eZQPyLEv+qeU3TdO70lvUbNq546sknXk7hZvfDvgNyndrRgusQeTyRGL/zp9f7" + + "SDdvXs994fnnx3ODOwLA6A+uf2KewB2+NdcNiHUHP+TvnZfBahkXF7J//4EdxFq3fqN7/KQptHTZ" + + "V8xymjx1Gg0Z/rG+d/9B4wosadig4RAA8VwyNv9ukP+vOQms1q3jI3IuXz5ArNVff+OZ9vks+mbd" + + "Blr9zTqaPW8+DRsxSt+0ZZt+Nb/A1a5d+08BPMS5hcD/8r/5Vq1aR7H548akt3xlsmfeggTalrKD" + + "2DAtWbqMRn0yTk9es1Yr4o7fs1fP+QAe4eCicC/LbHiIi2sVkZWVnW6YT+adT+Ljvu/AQdrzwz7j" + + "NU3gMli2fIVWXFJKAwa8uwpAFzZfB4C4583zdBfCV9xBYq1dv9GzavUaOpb+Dzp89Bh9+90WnvI+" + + "p8Sly7QiHl4mTZq0GcAzbD4WgHTPm2/cuHHw6YwzB4i18bvNnvUbv+PmlkkZZ87Rju9308zZX9Ki" + + "xCT1Gk9ms2bP3g3geTbfULBwr0piARB2u922Zeu2ncTavHW7Z8u2FMrNu0I5uXm0/2AazU9YTIuX" + + "LFXzC65RQkJCKoCeHNwD9+x/u+874dnsdvmbteu2EGv3nr3unbv2UGFRMd1kjh5LN5oez+1/U41A" + + "Fi1anAbgJc6tqTnI3Jsyj61kDwriCW/NGmLtP3DQs2fvfqqqrqEal5t+Op1By1esoqS//V3Nysml" + + "hQmLbpk3pjzlnjbPJuTg4BAsX7EyyRxvPQf4qHs8HjJ0PvMCreKOz92ezV+mBQsWHjLNN7+HzZuY" + + "R3f2nLkziXX0eLrnYNqPpKoaGcrKzjEGHvpqVbJ24eIlmjlr1v57fud9m57FYsHQYcNH60SUkXHW" + + "k+pjnj/jGzeAsftq5oVLNG3a9B8A9DDNW+5582A1bda8bVV1NfGXiSofe93jUYlldH3jBqA1a9dr" + + "59n8xImTtgP4A3f7pve6eQk+qqioKK+oqCwrLS0T7dq2gaLIYPM4dToDLrdbb9UqTkpakrh5/Phx" + + "X7L5DF3XLxKRB78Fmd/q4NnnnutLXqkXL2Xp/DWWvuG7TfqpjDP03vvvrwXQnX/3fvOq+22JM5DA" + + "6tu376e5eVdpN8/4mzZvNa496tOnz1cAusqK0giAhN+ohPlFJdo/+NAovupqfjxy1NWzZ68EAE8q" + + "itLA94PNbz4Ep9P5Ymxs7AgAXTiYGAAy/kMkzHHYUD1+jgIg8J8m8//Sxn+6BH7D+m9uoBi14hOM" + + "NwAAAABJRU5ErkJggg==" + } +}; diff --git a/browser/components/shell/ShellService.jsm b/browser/components/shell/ShellService.jsm new file mode 100644 index 000000000..bcbf0b6f6 --- /dev/null +++ b/browser/components/shell/ShellService.jsm @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this file, +* You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["ShellService"]; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry", + "resource://gre/modules/WindowsRegistry.jsm"); + +/** + * Internal functionality to save and restore the docShell.allow* properties. + */ +var ShellServiceInternal = { + /** + * Used to determine whether or not to offer "Set as desktop background" + * functionality. Even if shell service is available it is not + * guaranteed that it is able to set the background for every desktop + * which is especially true for Linux with its many different desktop + * environments. + */ + get canSetDesktopBackground() { +#ifdef XP_LINUX + if (this.shellService) { + let linuxShellService = this.shellService + .QueryInterface(Ci.nsIGNOMEShellService); + return linuxShellService.canSetDesktopBackground; + } +#elif defined(XP_WIN) + return true; +#else + return false; +#endif + }, + + /** + * Used to determine whether or not to show a "Set Default Browser" + * query dialog. This attribute is true if the application is starting + * up and "browser.shell.checkDefaultBrowser" is true, otherwise it + * is false. + */ + _checkedThisSession: false, + get shouldCheckDefaultBrowser() { + // If we've already checked, the browser has been started and this is a + // new window open, and we don't want to check again. + if (this._checkedThisSession) { + return false; + } + + if (!Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser")) { + return false; + } + +#ifdef XP_WIN + let optOutValue = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "Software\\Mozilla\\PaleMoon", + "DefaultBrowserOptOut"); + WindowsRegistry.removeRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "Software\\Mozilla\\PaleMoon", + "DefaultBrowserOptOut"); + if (optOutValue == "True") { + Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", false); + return false; + } +#endif + + return true; + }, + + set shouldCheckDefaultBrowser(shouldCheck) { + Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", !!shouldCheck); + }, + + isDefaultBrowser(startupCheck, forAllTypes) { + // If this is the first browser window, maintain internal state that we've + // checked this session (so that subsequent window opens don't show the + // default browser dialog). + if (startupCheck) { + this._checkedThisSession = true; + } + if (this.shellService) { + return this.shellService.isDefaultBrowser(startupCheck, forAllTypes); + } + return false; + } +}; + +XPCOMUtils.defineLazyServiceGetter(ShellServiceInternal, "shellService", + "@mozilla.org/browser/shell-service;1", Ci.nsIShellService); + +/** + * The external API exported by this module. + */ +this.ShellService = new Proxy(ShellServiceInternal, { + get(target, name) { + if (name in target) { + return target[name]; + } + if (target.shellService) { + return target.shellService[name]; + } + Services.console.logStringMessage(`${name} not found in ShellService: ${target.shellService}`); + return undefined; + } +}); diff --git a/browser/components/shell/content/setDesktopBackground.js b/browser/components/shell/content/setDesktopBackground.js new file mode 100644 index 000000000..011ca12fb --- /dev/null +++ b/browser/components/shell/content/setDesktopBackground.js @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; + +var gSetBackground = { + _position : "", + _backgroundColor : 0, + _screenWidth : 0, + _screenHeight : 0, + _image : null, + _canvas : null, + + get _shell() + { + return Components.classes["@mozilla.org/browser/shell-service;1"] + .getService(Ci.nsIShellService); + }, + + load: function () + { + this._canvas = document.getElementById("screen"); + this._screenWidth = screen.width; + this._screenHeight = screen.height; + if (this._screenWidth / this._screenHeight >= 1.6) + document.getElementById("monitor").setAttribute("aspectratio", "16:10"); + +#ifdef XP_WIN + // Hide fill + fit options if < Win7 since they don't work. + var version = Components.classes["@mozilla.org/system-info;1"] + .getService(Ci.nsIPropertyBag2) + .getProperty("version"); + var isWindows7OrHigher = (parseFloat(version) >= 6.1); + if (!isWindows7OrHigher) { + document.getElementById("fillPosition").hidden = true; + document.getElementById("fitPosition").hidden = true; + } +#endif + + // make sure that the correct dimensions will be used + setTimeout(function(self) { + self.init(window.arguments[0]); + }, 0, this); + }, + + init: function (aImage) + { + this._image = aImage; + + // set the size of the coordinate space + this._canvas.width = this._canvas.clientWidth; + this._canvas.height = this._canvas.clientHeight; + + var ctx = this._canvas.getContext("2d"); + ctx.scale(this._canvas.clientWidth / this._screenWidth, this._canvas.clientHeight / this._screenHeight); + + this._initColor(); + this.updatePosition(); + }, + + setDesktopBackground: function () + { + document.persist("menuPosition", "value"); + this._shell.desktopBackgroundColor = this._hexStringToLong(this._backgroundColor); + this._shell.setDesktopBackground(this._image, + Ci.nsIShellService["BACKGROUND_" + this._position]); + }, + + updatePosition: function () + { + var ctx = this._canvas.getContext("2d"); + ctx.clearRect(0, 0, this._screenWidth, this._screenHeight); + + this._position = document.getElementById("menuPosition").value; + + switch (this._position) { + case "TILE": + ctx.save(); + ctx.fillStyle = ctx.createPattern(this._image, "repeat"); + ctx.fillRect(0, 0, this._screenWidth, this._screenHeight); + ctx.restore(); + break; + case "STRETCH": + ctx.drawImage(this._image, 0, 0, this._screenWidth, this._screenHeight); + break; + case "CENTER": { + let x = (this._screenWidth - this._image.naturalWidth) / 2; + let y = (this._screenHeight - this._image.naturalHeight) / 2; + ctx.drawImage(this._image, x, y); + break; + } + case "FILL": { + // Try maxing width first, overflow height. + let widthRatio = this._screenWidth / this._image.naturalWidth; + let width = this._image.naturalWidth * widthRatio; + let height = this._image.naturalHeight * widthRatio; + if (height < this._screenHeight) { + // Height less than screen, max height and overflow width. + let heightRatio = this._screenHeight / this._image.naturalHeight; + width = this._image.naturalWidth * heightRatio; + height = this._image.naturalHeight * heightRatio; + } + let x = (this._screenWidth - width) / 2; + let y = (this._screenHeight - height) / 2; + ctx.drawImage(this._image, x, y, width, height); + break; + } + case "FIT": { + // Try maxing width first, top and bottom borders. + let widthRatio = this._screenWidth / this._image.naturalWidth; + let width = this._image.naturalWidth * widthRatio; + let height = this._image.naturalHeight * widthRatio; + let x = 0; + let y = (this._screenHeight - height) / 2; + if (height > this._screenHeight) { + // Height overflow, maximise height, side borders. + let heightRatio = this._screenHeight / this._image.naturalHeight; + width = this._image.naturalWidth * heightRatio; + height = this._image.naturalHeight * heightRatio; + x = (this._screenWidth - width) / 2; + y = 0; + } + ctx.drawImage(this._image, x, y, width, height); + break; + } + } + } +}; + +gSetBackground["_initColor"] = function () +{ + var color = this._shell.desktopBackgroundColor; + + const rMask = 4294901760; + const gMask = 65280; + const bMask = 255; + var r = (color & rMask) >> 16; + var g = (color & gMask) >> 8; + var b = (color & bMask); + this.updateColor(this._rgbToHex(r, g, b)); + + var colorpicker = document.getElementById("desktopColor"); + colorpicker.color = this._backgroundColor; +}; + +gSetBackground["updateColor"] = function (aColor) +{ + this._backgroundColor = aColor; + this._canvas.style.backgroundColor = aColor; +}; + +// Converts a color string in the format "#RRGGBB" to an integer. +gSetBackground["_hexStringToLong"] = function (aString) +{ + return parseInt(aString.substring(1, 3), 16) << 16 | + parseInt(aString.substring(3, 5), 16) << 8 | + parseInt(aString.substring(5, 7), 16); +}; + +gSetBackground["_rgbToHex"] = function (aR, aG, aB) +{ + return "#" + [aR, aG, aB].map(aInt => aInt.toString(16).replace(/^(.)$/, "0$1")) + .join("").toUpperCase(); +}; diff --git a/browser/components/shell/content/setDesktopBackground.xul b/browser/components/shell/content/setDesktopBackground.xul new file mode 100644 index 000000000..1bd781fea --- /dev/null +++ b/browser/components/shell/content/setDesktopBackground.xul @@ -0,0 +1,56 @@ +<?xml version="1.0"?> <!-- -*- Mode: HTML -*- --> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/setDesktopBackground.css" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/setDesktopBackground.dtd"> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + windowtype="Shell:SetDesktopBackground" + buttons="accept,cancel" + buttonlabelaccept="&setDesktopBackground.title;" + onload="gSetBackground.load();" + ondialogaccept="gSetBackground.setDesktopBackground();" + title="&setDesktopBackground.title;" + style="width: 30em;"> + + <stringbundle id="backgroundBundle" + src="chrome://browser/locale/shellservice.properties"/> + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" src="chrome://browser/content/setDesktopBackground.js"/> + <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + + <hbox align="center"> + <label value="&position.label;"/> + <menulist id="menuPosition" + label="&position.label;" + oncommand="gSetBackground.updatePosition();"> + <menupopup> + <menuitem label="¢er.label;" value="CENTER"/> + <menuitem label="&tile.label;" value="TILE"/> + <menuitem label="&stretch.label;" value="STRETCH"/> + <menuitem label="&fill.label;" value="FILL" id="fillPosition"/> + <menuitem label="&fit.label;" value="FIT" id="fitPosition"/> + </menupopup> + </menulist> + <spacer flex="1"/> + <label value="&color.label;"/> + <colorpicker id="desktopColor" + type="button" + onchange="gSetBackground.updateColor(this.color);"/> + </hbox> + <groupbox align="center"> + <caption label="&preview.label;"/> + <stack> + <!-- if width and height are not present, they default to 300x150 and stretch the stack --> + <html:canvas id="screen" width="1" height="1"/> + <image id="monitor"/> + </stack> + </groupbox> + +</dialog> diff --git a/browser/components/shell/jar.mn b/browser/components/shell/jar.mn new file mode 100644 index 000000000..4cff4da9e --- /dev/null +++ b/browser/components/shell/jar.mn @@ -0,0 +1,7 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/setDesktopBackground.xul (content/setDesktopBackground.xul) +* content/browser/setDesktopBackground.js (content/setDesktopBackground.js) diff --git a/browser/components/shell/moz.build b/browser/components/shell/moz.build new file mode 100644 index 000000000..de34f17fb --- /dev/null +++ b/browser/components/shell/moz.build @@ -0,0 +1,35 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += ['jar.mn'] + +XPIDL_SOURCES += ['nsIShellService.idl'] + +if CONFIG['OS_ARCH'] == 'WINNT': + XPIDL_SOURCES += ['nsIWindowsShellService.idl'] +elif 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + XPIDL_SOURCES += ['nsIGNOMEShellService.idl'] + +XPIDL_MODULE = 'shellservice' + +if CONFIG['OS_ARCH'] == 'WINNT': + SOURCES += ['nsWindowsShellService.cpp'] +elif 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + SOURCES += ['nsGNOMEShellService.cpp'] + +if SOURCES: + FINAL_LIBRARY = 'browsercomps' + +EXTRA_COMPONENTS += [ + 'nsSetDefaultBrowser.js', + 'nsSetDefaultBrowser.manifest', +] + +EXTRA_PP_JS_MODULES += ['ShellService.jsm'] + +for var in ('MOZ_APP_NAME', 'MOZ_APP_VERSION'): + DEFINES[var] = '"%s"' % CONFIG[var] + +CXXFLAGS += CONFIG['TK_CFLAGS'] diff --git a/browser/components/shell/nsGNOMEShellService.cpp b/browser/components/shell/nsGNOMEShellService.cpp new file mode 100644 index 000000000..9bc5f5913 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellService.cpp @@ -0,0 +1,637 @@ +/* -*- 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 "mozilla/ArrayUtils.h" + +#include "nsCOMPtr.h" +#include "nsGNOMEShellService.h" +#include "nsShellService.h" +#include "nsIServiceManager.h" +#include "nsIFile.h" +#include "nsIProperties.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIPrefService.h" +#include "prenv.h" +#include "nsStringAPI.h" +#include "nsIGConfService.h" +#include "nsIGIOService.h" +#include "nsIGSettingsService.h" +#include "nsIStringBundle.h" +#include "nsIOutputStream.h" +#include "nsIProcess.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsIDOMHTMLImageElement.h" +#include "nsIImageLoadingContent.h" +#include "imgIRequest.h" +#include "imgIContainer.h" +#include "mozilla/Sprintf.h" +#if defined(MOZ_WIDGET_GTK) +#include "nsIImageToPixbuf.h" +#endif +#include "nsXULAppAPI.h" + +#include <glib.h> +#include <glib-object.h> +#include <gtk/gtk.h> +#include <gdk/gdk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <limits.h> +#include <stdlib.h> + +using namespace mozilla; + +struct ProtocolAssociation +{ + const char *name; + bool essential; +}; + +struct MimeTypeAssociation +{ + const char *mimeType; + const char *extensions; +}; + +static const ProtocolAssociation appProtocols[] = { + { "http", true }, + { "https", true }, + { "ftp", false }, + { "chrome", false } +}; + +static const MimeTypeAssociation appTypes[] = { + { "text/html", "htm html shtml" }, + { "application/xhtml+xml", "xhtml xht" } +}; + +// GConf registry key constants +#define DG_BACKGROUND "/desktop/gnome/background" + +static const char kDesktopImageKey[] = DG_BACKGROUND "/picture_filename"; +static const char kDesktopOptionsKey[] = DG_BACKGROUND "/picture_options"; +static const char kDesktopDrawBGKey[] = DG_BACKGROUND "/draw_background"; +static const char kDesktopColorKey[] = DG_BACKGROUND "/primary_color"; + +static const char kDesktopBGSchema[] = "org.gnome.desktop.background"; +static const char kDesktopImageGSKey[] = "picture-uri"; +static const char kDesktopOptionGSKey[] = "picture-options"; +static const char kDesktopDrawBGGSKey[] = "draw-background"; +static const char kDesktopColorGSKey[] = "primary-color"; + +nsresult +nsGNOMEShellService::Init() +{ + nsresult rv; + + // GConf, GSettings or GIO _must_ be available, or we do not allow + // CreateInstance to succeed. + + nsCOMPtr<nsIGConfService> gconf = do_GetService(NS_GCONFSERVICE_CONTRACTID); + nsCOMPtr<nsIGIOService> giovfs = + do_GetService(NS_GIOSERVICE_CONTRACTID); + nsCOMPtr<nsIGSettingsService> gsettings = + do_GetService(NS_GSETTINGSSERVICE_CONTRACTID); + + if (!gconf && !giovfs && !gsettings) + return NS_ERROR_NOT_AVAILABLE; + + // Check G_BROKEN_FILENAMES. If it's set, then filenames in glib use + // the locale encoding. If it's not set, they use UTF-8. + mUseLocaleFilenames = PR_GetEnv("G_BROKEN_FILENAMES") != nullptr; + + if (GetAppPathFromLauncher()) + return NS_OK; + + nsCOMPtr<nsIProperties> dirSvc + (do_GetService("@mozilla.org/file/directory_service;1")); + NS_ENSURE_TRUE(dirSvc, NS_ERROR_NOT_AVAILABLE); + + nsCOMPtr<nsIFile> appPath; + rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile), + getter_AddRefs(appPath)); + NS_ENSURE_SUCCESS(rv, rv); + + return appPath->GetNativePath(mAppPath); +} + +NS_IMPL_ISUPPORTS(nsGNOMEShellService, nsIGNOMEShellService, nsIShellService) + +bool +nsGNOMEShellService::GetAppPathFromLauncher() +{ + gchar *tmp; + + const char *launcher = PR_GetEnv("MOZ_APP_LAUNCHER"); + if (!launcher) + return false; + + if (g_path_is_absolute(launcher)) { + mAppPath = launcher; + tmp = g_path_get_basename(launcher); + gchar *fullpath = g_find_program_in_path(tmp); + if (fullpath && mAppPath.Equals(fullpath)) + mAppIsInPath = true; + g_free(fullpath); + } else { + tmp = g_find_program_in_path(launcher); + if (!tmp) + return false; + mAppPath = tmp; + mAppIsInPath = true; + } + + g_free(tmp); + return true; +} + +bool +nsGNOMEShellService::KeyMatchesAppName(const char *aKeyValue) const +{ + + gchar *commandPath; + if (mUseLocaleFilenames) { + gchar *nativePath = g_filename_from_utf8(aKeyValue, -1, + nullptr, nullptr, nullptr); + if (!nativePath) { + NS_ERROR("Error converting path to filesystem encoding"); + return false; + } + + commandPath = g_find_program_in_path(nativePath); + g_free(nativePath); + } else { + commandPath = g_find_program_in_path(aKeyValue); + } + + if (!commandPath) + return false; + + bool matches = mAppPath.Equals(commandPath); + g_free(commandPath); + return matches; +} + +bool +nsGNOMEShellService::CheckHandlerMatchesAppName(const nsACString &handler) const +{ + gint argc; + gchar **argv; + nsAutoCString command(handler); + + // The string will be something of the form: [/path/to/]browser "%s" + // We want to remove all of the parameters and get just the binary name. + + if (g_shell_parse_argv(command.get(), &argc, &argv, nullptr) && argc > 0) { + command.Assign(argv[0]); + g_strfreev(argv); + } + + if (!KeyMatchesAppName(command.get())) + return false; // the handler is set to another app + + return true; +} + +NS_IMETHODIMP +nsGNOMEShellService::IsDefaultBrowser(bool aStartupCheck, + bool aForAllTypes, + bool* aIsDefaultBrowser) +{ + *aIsDefaultBrowser = false; + + nsCOMPtr<nsIGConfService> gconf = do_GetService(NS_GCONFSERVICE_CONTRACTID); + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + + bool enabled; + nsAutoCString handler; + nsCOMPtr<nsIGIOMimeApp> gioApp; + + for (unsigned int i = 0; i < ArrayLength(appProtocols); ++i) { + if (!appProtocols[i].essential) + continue; + + if (gconf) { + handler.Truncate(); + gconf->GetAppForProtocol(nsDependentCString(appProtocols[i].name), + &enabled, handler); + + if (!CheckHandlerMatchesAppName(handler) || !enabled) + return NS_OK; // the handler is disabled or set to another app + } + + if (giovfs) { + handler.Truncate(); + giovfs->GetAppForURIScheme(nsDependentCString(appProtocols[i].name), + getter_AddRefs(gioApp)); + if (!gioApp) + return NS_OK; + + gioApp->GetCommand(handler); + + if (!CheckHandlerMatchesAppName(handler)) + return NS_OK; // the handler is set to another app + } + } + + *aIsDefaultBrowser = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsGNOMEShellService::SetDefaultBrowser(bool aClaimAllTypes, + bool aForAllUsers) +{ +#ifdef DEBUG + if (aForAllUsers) + NS_WARNING("Setting the default browser for all users is not yet supported"); +#endif + + nsCOMPtr<nsIGConfService> gconf = do_GetService(NS_GCONFSERVICE_CONTRACTID); + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (gconf) { + nsAutoCString appKeyValue; + if (mAppIsInPath) { + // mAppPath is in the users path, so use only the basename as the launcher + gchar *tmp = g_path_get_basename(mAppPath.get()); + appKeyValue = tmp; + g_free(tmp); + } else { + appKeyValue = mAppPath; + } + + appKeyValue.AppendLiteral(" %s"); + + for (unsigned int i = 0; i < ArrayLength(appProtocols); ++i) { + if (appProtocols[i].essential || aClaimAllTypes) { + gconf->SetAppForProtocol(nsDependentCString(appProtocols[i].name), + appKeyValue); + } + } + } + + if (giovfs) { + nsresult rv; + nsCOMPtr<nsIStringBundleService> bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStringBundle> brandBundle; + rv = bundleService->CreateBundle(BRAND_PROPERTIES, getter_AddRefs(brandBundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString brandShortName; + brandBundle->GetStringFromName(u"brandShortName", + getter_Copies(brandShortName)); + + // use brandShortName as the application id. + NS_ConvertUTF16toUTF8 id(brandShortName); + nsCOMPtr<nsIGIOMimeApp> appInfo; + rv = giovfs->CreateAppFromCommand(mAppPath, + id, + getter_AddRefs(appInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + // set handler for the protocols + for (unsigned int i = 0; i < ArrayLength(appProtocols); ++i) { + if (appProtocols[i].essential || aClaimAllTypes) { + appInfo->SetAsDefaultForURIScheme(nsDependentCString(appProtocols[i].name)); + } + } + + // set handler for .html and xhtml files and MIME types: + if (aClaimAllTypes) { + // Add mime types for html, xhtml extension and set app to just created appinfo. + for (unsigned int i = 0; i < ArrayLength(appTypes); ++i) { + appInfo->SetAsDefaultForMimeType(nsDependentCString(appTypes[i].mimeType)); + appInfo->SetAsDefaultForFileExtensions(nsDependentCString(appTypes[i].extensions)); + } + } + } + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + (void) prefs->SetBoolPref(PREF_CHECKDEFAULTBROWSER, true); + // Reset the number of times the dialog should be shown + // before it is silenced. + (void) prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, 0); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsGNOMEShellService::GetCanSetDesktopBackground(bool* aResult) +{ + // setting desktop background is currently only supported + // for Gnome or desktops using the same GSettings and GConf keys + const char* gnomeSession = getenv("GNOME_DESKTOP_SESSION_ID"); + if (gnomeSession) { + *aResult = true; + } else { + *aResult = false; + } + + return NS_OK; +} + +static nsresult +WriteImage(const nsCString& aPath, imgIContainer* aImage) +{ +#if !defined(MOZ_WIDGET_GTK) + return NS_ERROR_NOT_AVAILABLE; +#else + nsCOMPtr<nsIImageToPixbuf> imgToPixbuf = + do_GetService("@mozilla.org/widget/image-to-gdk-pixbuf;1"); + if (!imgToPixbuf) + return NS_ERROR_NOT_AVAILABLE; + + GdkPixbuf* pixbuf = imgToPixbuf->ConvertImageToPixbuf(aImage); + if (!pixbuf) + return NS_ERROR_NOT_AVAILABLE; + + gboolean res = gdk_pixbuf_save(pixbuf, aPath.get(), "png", nullptr, nullptr); + + g_object_unref(pixbuf); + return res ? NS_OK : NS_ERROR_FAILURE; +#endif +} + +NS_IMETHODIMP +nsGNOMEShellService::SetDesktopBackground(nsIDOMElement* aElement, + int32_t aPosition) +{ + nsresult rv; + nsCOMPtr<nsIImageLoadingContent> imageContent = do_QueryInterface(aElement, &rv); + if (!imageContent) return rv; + + // get the image container + nsCOMPtr<imgIRequest> request; + rv = imageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(request)); + if (!request) return rv; + nsCOMPtr<imgIContainer> container; + rv = request->GetImage(getter_AddRefs(container)); + if (!container) return rv; + + // Set desktop wallpaper filling style + nsAutoCString options; + if (aPosition == BACKGROUND_TILE) + options.AssignLiteral("wallpaper"); + else if (aPosition == BACKGROUND_STRETCH) + options.AssignLiteral("stretched"); + else if (aPosition == BACKGROUND_FILL) + options.AssignLiteral("zoom"); + else if (aPosition == BACKGROUND_FIT) + options.AssignLiteral("scaled"); + else + options.AssignLiteral("centered"); + + // Write the background file to the home directory. + nsAutoCString filePath(PR_GetEnv("HOME")); + + // get the product brand name from localized strings + nsString brandName; + nsCID bundleCID = NS_STRINGBUNDLESERVICE_CID; + nsCOMPtr<nsIStringBundleService> bundleService(do_GetService(bundleCID)); + if (bundleService) { + nsCOMPtr<nsIStringBundle> brandBundle; + rv = bundleService->CreateBundle(BRAND_PROPERTIES, + getter_AddRefs(brandBundle)); + if (NS_SUCCEEDED(rv) && brandBundle) { + rv = brandBundle->GetStringFromName(u"brandShortName", + getter_Copies(brandName)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // build the file name + filePath.Append('/'); + filePath.Append(NS_ConvertUTF16toUTF8(brandName)); + filePath.AppendLiteral("_wallpaper.png"); + + // write the image to a file in the home dir + rv = WriteImage(filePath, container); + NS_ENSURE_SUCCESS(rv, rv); + + // Try GSettings first. If we don't have GSettings or the right schema, fall back + // to using GConf instead. Note that if GSettings works ok, the changes get + // mirrored to GConf by the gsettings->gconf bridge in gnome-settings-daemon + nsCOMPtr<nsIGSettingsService> gsettings = + do_GetService(NS_GSETTINGSSERVICE_CONTRACTID); + if (gsettings) { + nsCOMPtr<nsIGSettingsCollection> background_settings; + gsettings->GetCollectionForSchema( + NS_LITERAL_CSTRING(kDesktopBGSchema), getter_AddRefs(background_settings)); + if (background_settings) { + gchar *file_uri = g_filename_to_uri(filePath.get(), nullptr, nullptr); + if (!file_uri) + return NS_ERROR_FAILURE; + + background_settings->SetString(NS_LITERAL_CSTRING(kDesktopOptionGSKey), + options); + + background_settings->SetString(NS_LITERAL_CSTRING(kDesktopImageGSKey), + nsDependentCString(file_uri)); + g_free(file_uri); + background_settings->SetBoolean(NS_LITERAL_CSTRING(kDesktopDrawBGGSKey), + true); + return rv; + } + } + + // if the file was written successfully, set it as the system wallpaper + nsCOMPtr<nsIGConfService> gconf = do_GetService(NS_GCONFSERVICE_CONTRACTID); + + if (gconf) { + gconf->SetString(NS_LITERAL_CSTRING(kDesktopOptionsKey), options); + + // Set the image to an empty string first to force a refresh + // (since we could be writing a new image on top of an existing + // PaleMoon_wallpaper.png and nautilus doesn't monitor the file for changes) + gconf->SetString(NS_LITERAL_CSTRING(kDesktopImageKey), + EmptyCString()); + + gconf->SetString(NS_LITERAL_CSTRING(kDesktopImageKey), filePath); + gconf->SetBool(NS_LITERAL_CSTRING(kDesktopDrawBGKey), true); + } + + return rv; +} + +#define COLOR_16_TO_8_BIT(_c) ((_c) >> 8) +#define COLOR_8_TO_16_BIT(_c) ((_c) << 8 | (_c)) + +NS_IMETHODIMP +nsGNOMEShellService::GetDesktopBackgroundColor(uint32_t *aColor) +{ + nsCOMPtr<nsIGSettingsService> gsettings = + do_GetService(NS_GSETTINGSSERVICE_CONTRACTID); + nsCOMPtr<nsIGSettingsCollection> background_settings; + nsAutoCString background; + + if (gsettings) { + gsettings->GetCollectionForSchema( + NS_LITERAL_CSTRING(kDesktopBGSchema), getter_AddRefs(background_settings)); + if (background_settings) { + background_settings->GetString(NS_LITERAL_CSTRING(kDesktopColorGSKey), + background); + } + } + + if (!background_settings) { + nsCOMPtr<nsIGConfService> gconf = do_GetService(NS_GCONFSERVICE_CONTRACTID); + if (gconf) + gconf->GetString(NS_LITERAL_CSTRING(kDesktopColorKey), background); + } + + if (background.IsEmpty()) { + *aColor = 0; + return NS_OK; + } + + GdkColor color; + gboolean success = gdk_color_parse(background.get(), &color); + + NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); + + *aColor = COLOR_16_TO_8_BIT(color.red) << 16 | + COLOR_16_TO_8_BIT(color.green) << 8 | + COLOR_16_TO_8_BIT(color.blue); + return NS_OK; +} + +static void +ColorToCString(uint32_t aColor, nsCString& aResult) +{ + // The #rrrrggggbbbb format is used to match gdk_color_to_string() + char *buf = aResult.BeginWriting(13); + if (!buf) + return; + + uint16_t red = COLOR_8_TO_16_BIT((aColor >> 16) & 0xff); + uint16_t green = COLOR_8_TO_16_BIT((aColor >> 8) & 0xff); + uint16_t blue = COLOR_8_TO_16_BIT(aColor & 0xff); + + snprintf(buf, 14, "#%04x%04x%04x", red, green, blue); +} + +NS_IMETHODIMP +nsGNOMEShellService::SetDesktopBackgroundColor(uint32_t aColor) +{ + NS_ASSERTION(aColor <= 0xffffff, "aColor has extra bits"); + nsAutoCString colorString; + ColorToCString(aColor, colorString); + + nsCOMPtr<nsIGSettingsService> gsettings = + do_GetService(NS_GSETTINGSSERVICE_CONTRACTID); + if (gsettings) { + nsCOMPtr<nsIGSettingsCollection> background_settings; + gsettings->GetCollectionForSchema( + NS_LITERAL_CSTRING(kDesktopBGSchema), getter_AddRefs(background_settings)); + if (background_settings) { + background_settings->SetString(NS_LITERAL_CSTRING(kDesktopColorGSKey), + colorString); + return NS_OK; + } + } + + nsCOMPtr<nsIGConfService> gconf = do_GetService(NS_GCONFSERVICE_CONTRACTID); + + if (gconf) { + gconf->SetString(NS_LITERAL_CSTRING(kDesktopColorKey), colorString); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsGNOMEShellService::OpenApplication(int32_t aApplication) +{ + nsAutoCString scheme; + if (aApplication == APPLICATION_MAIL) + scheme.AssignLiteral("mailto"); + else if (aApplication == APPLICATION_NEWS) + scheme.AssignLiteral("news"); + else + return NS_ERROR_NOT_AVAILABLE; + + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (giovfs) { + nsCOMPtr<nsIGIOMimeApp> gioApp; + giovfs->GetAppForURIScheme(scheme, getter_AddRefs(gioApp)); + if (gioApp) + return gioApp->Launch(EmptyCString()); + } + + nsCOMPtr<nsIGConfService> gconf = do_GetService(NS_GCONFSERVICE_CONTRACTID); + if (!gconf) + return NS_ERROR_FAILURE; + + bool enabled; + nsAutoCString appCommand; + gconf->GetAppForProtocol(scheme, &enabled, appCommand); + + if (!enabled) + return NS_ERROR_FAILURE; + + // XXX we don't currently handle launching a terminal window. + // If the handler requires a terminal, bail. + bool requiresTerminal; + gconf->HandlerRequiresTerminal(scheme, &requiresTerminal); + if (requiresTerminal) + return NS_ERROR_FAILURE; + + // Perform shell argument expansion + int argc; + char **argv; + if (!g_shell_parse_argv(appCommand.get(), &argc, &argv, nullptr)) + return NS_ERROR_FAILURE; + + char **newArgv = new char*[argc + 1]; + int newArgc = 0; + + // Run through the list of arguments. Copy all of them to the new + // argv except for %s, which we skip. + for (int i = 0; i < argc; ++i) { + if (strcmp(argv[i], "%s") != 0) + newArgv[newArgc++] = argv[i]; + } + + newArgv[newArgc] = nullptr; + + gboolean err = g_spawn_async(nullptr, newArgv, nullptr, G_SPAWN_SEARCH_PATH, + nullptr, nullptr, nullptr, nullptr); + + g_strfreev(argv); + delete[] newArgv; + + return err ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsGNOMEShellService::OpenApplicationWithURI(nsIFile* aApplication, const nsACString& aURI) +{ + nsresult rv; + nsCOMPtr<nsIProcess> process = + do_CreateInstance("@mozilla.org/process/util;1", &rv); + if (NS_FAILED(rv)) + return rv; + + rv = process->Init(aApplication); + if (NS_FAILED(rv)) + return rv; + + const nsCString spec(aURI); + const char* specStr = spec.get(); + return process->Run(false, &specStr, 1); +} + +NS_IMETHODIMP +nsGNOMEShellService::GetDefaultFeedReader(nsIFile** _retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/browser/components/shell/nsGNOMEShellService.h b/browser/components/shell/nsGNOMEShellService.h new file mode 100644 index 000000000..a7b003802 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellService.h @@ -0,0 +1,36 @@ +/* -*- 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/. */ + +#ifndef nsgnomeshellservice_h____ +#define nsgnomeshellservice_h____ + +#include "nsIGNOMEShellService.h" +#include "nsStringAPI.h" +#include "mozilla/Attributes.h" + +class nsGNOMEShellService final : public nsIGNOMEShellService +{ +public: + nsGNOMEShellService() : mAppIsInPath(false) { } + + NS_DECL_ISUPPORTS + NS_DECL_NSISHELLSERVICE + NS_DECL_NSIGNOMESHELLSERVICE + + nsresult Init(); + +private: + ~nsGNOMEShellService() {} + + bool KeyMatchesAppName(const char *aKeyValue) const; + bool CheckHandlerMatchesAppName(const nsACString& handler) const; + + bool GetAppPathFromLauncher(); + bool mUseLocaleFilenames; + nsCString mAppPath; + bool mAppIsInPath; +}; + +#endif // nsgnomeshellservice_h____ diff --git a/browser/components/shell/nsIGNOMEShellService.idl b/browser/components/shell/nsIGNOMEShellService.idl new file mode 100644 index 000000000..842ce5e8a --- /dev/null +++ b/browser/components/shell/nsIGNOMEShellService.idl @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIShellService.idl" + +[scriptable, uuid(2ce5c803-edcd-443d-98eb-ceba86d02d13)] +interface nsIGNOMEShellService : nsIShellService +{ + /** + * Used to determine whether or not to offer "Set as desktop background" + * functionality. Even if shell service is available it is not + * guaranteed that it is able to set the background for every desktop + * which is especially true for Linux with its many different desktop + * environments. + */ + readonly attribute boolean canSetDesktopBackground; +}; + diff --git a/browser/components/shell/nsIShellService.idl b/browser/components/shell/nsIShellService.idl new file mode 100644 index 000000000..3e7e94b00 --- /dev/null +++ b/browser/components/shell/nsIShellService.idl @@ -0,0 +1,95 @@ +/* -*- 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 "nsISupports.idl" + +interface nsIDOMElement; +interface nsIFile; + +[scriptable, uuid(2d1a95e4-5bd8-4eeb-b0a8-c1455fd2a357)] +interface nsIShellService : nsISupports +{ + /** + * Determines whether or not Firefox is the "Default Browser." + * This is simply whether or not Firefox is registered to handle + * http links. + * + * @param aStartupCheck true if this is the check being performed + * by the first browser window at startup, + * false otherwise. + * @param aForAllTypes true if the check should be made for HTTP and HTML. + * false if the check should be made for HTTP only. + * This parameter may be ignored on some platforms. + */ + boolean isDefaultBrowser(in boolean aStartupCheck, + [optional] in boolean aForAllTypes); + + /** + * Registers Firefox as the "Default Browser." + * + * @param aClaimAllTypes Register Firefox as the handler for + * additional protocols (ftp, chrome etc) + * and web documents (.html, .xhtml etc). + * @param aForAllUsers Whether or not Firefox should attempt + * to become the default browser for all + * users on a multi-user system. + */ + void setDefaultBrowser(in boolean aClaimAllTypes, in boolean aForAllUsers); + + /** + * Flags for positioning/sizing of the Desktop Background image. + */ + const long BACKGROUND_TILE = 1; + const long BACKGROUND_STRETCH = 2; + const long BACKGROUND_CENTER = 3; + const long BACKGROUND_FILL = 4; + const long BACKGROUND_FIT = 5; + + /** + * Sets the desktop background image using either the HTML <IMG> + * element supplied or the background image of the element supplied. + * + * @param aImageElement Either a HTML <IMG> element or an element with + * a background image from which to source the + * background image. + * @param aPosition How to place the image on the desktop + */ + void setDesktopBackground(in nsIDOMElement aElement, in long aPosition); + + /** + * Constants identifying applications that can be opened with + * openApplication. + */ + const long APPLICATION_MAIL = 0; + const long APPLICATION_NEWS = 1; + + /** + * Opens the application specified. If more than one application of the + * given type is available on the system, the default or "preferred" + * application is used. + */ + void openApplication(in long aApplication); + + /** + * The desktop background color, visible when no background image is + * used, or if the background image is centered and does not fill the + * entire screen. A rgb value, where (r << 16 | g << 8 | b) + */ + attribute unsigned long desktopBackgroundColor; + + /** + * Opens an application with a specific URI to load. + * @param application + * The application file (or bundle directory, on OS X) + * @param uri + * The uri to be loaded by the application + */ + void openApplicationWithURI(in nsIFile aApplication, in ACString aURI); + + /** + * The default system handler for web feeds + */ + readonly attribute nsIFile defaultFeedReader; +}; diff --git a/browser/components/shell/nsIWindowsShellService.idl b/browser/components/shell/nsIWindowsShellService.idl new file mode 100644 index 000000000..57ed37055 --- /dev/null +++ b/browser/components/shell/nsIWindowsShellService.idl @@ -0,0 +1,17 @@ +/* -*- 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 "nsIShellService.idl" + +[scriptable, uuid(f8a26b94-49e5-4441-8fbc-315e0b4f22ef)] +interface nsIWindowsShellService : nsIShellService +{ + /** + * Provides the shell service an opportunity to do some Win7+ shortcut + * maintenance needed on initial startup of the browser. + */ + void shortcutMaintenance(); +}; + diff --git a/browser/components/shell/nsSetDefaultBrowser.js b/browser/components/shell/nsSetDefaultBrowser.js new file mode 100644 index 000000000..853d8d860 --- /dev/null +++ b/browser/components/shell/nsSetDefaultBrowser.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * --setDefaultBrowser commandline handler + * Makes the current executable the "default browser". + */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +Components.utils.import("resource:///modules/ShellService.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function nsSetDefaultBrowser() {} + +nsSetDefaultBrowser.prototype = { + handle: function(aCmdline) { + if (aCmdline.handleFlag("setDefaultBrowser", false)) { + ShellService.setDefaultBrowser(true, true); + } + }, + + helpInfo: " --setDefaultBrowser Set this app as the default browser.\n", + + classID: Components.ID("{F57899D0-4E2C-4ac6-9E29-50C736103B0C}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsSetDefaultBrowser]); diff --git a/browser/components/shell/nsSetDefaultBrowser.manifest b/browser/components/shell/nsSetDefaultBrowser.manifest new file mode 100644 index 000000000..bf3c0f04f --- /dev/null +++ b/browser/components/shell/nsSetDefaultBrowser.manifest @@ -0,0 +1,3 @@ +component {F57899D0-4E2C-4ac6-9E29-50C736103B0C} nsSetDefaultBrowser.js +contract @mozilla.org/browser/default-browser-clh;1 {F57899D0-4E2C-4ac6-9E29-50C736103B0C} +category command-line-handler m-setdefaultbrowser @mozilla.org/browser/default-browser-clh;1 diff --git a/browser/components/shell/nsShellService.h b/browser/components/shell/nsShellService.h new file mode 100644 index 000000000..516a8423a --- /dev/null +++ b/browser/components/shell/nsShellService.h @@ -0,0 +1,12 @@ +/* -*- 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/. */ + +#define PREF_CHECKDEFAULTBROWSER "browser.shell.checkDefaultBrowser" +#define PREF_SKIPDEFAULTBROWSERCHECK "browser.shell.skipDefaultBrowserCheck" +#define PREF_DEFAULTBROWSERCHECKCOUNT "browser.shell.defaultBrowserCheckCount" + +#define SHELLSERVICE_PROPERTIES "chrome://browser/locale/shellservice.properties" +#define BRAND_PROPERTIES "chrome://branding/locale/brand.properties" + diff --git a/browser/components/shell/nsWindowsShellService.cpp b/browser/components/shell/nsWindowsShellService.cpp new file mode 100644 index 000000000..c4039b95a --- /dev/null +++ b/browser/components/shell/nsWindowsShellService.cpp @@ -0,0 +1,1277 @@ +/* -*- 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 "nsWindowsShellService.h" + +#include "imgIContainer.h" +#include "imgIRequest.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/RefPtr.h" +#include "nsIDOMElement.h" +#include "nsIDOMHTMLImageElement.h" +#include "nsIImageLoadingContent.h" +#include "nsIPrefService.h" +#include "nsIPrefLocalizedString.h" +#include "nsIServiceManager.h" +#include "nsIStringBundle.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsShellService.h" +#include "nsIProcess.h" +#include "nsICategoryManager.h" +#include "nsBrowserCompsCID.h" +#include "nsDirectoryServiceUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIWindowsRegKey.h" +#include "nsUnicharUtils.h" +#include "nsIWinTaskbar.h" +#include "nsISupportsPrimitives.h" +#include "nsIURLFormatter.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "mozilla/WindowsVersion.h" + +#include "windows.h" +#include "shellapi.h" + +#ifdef _WIN32_WINNT +#undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 +#define INITGUID +#undef NTDDI_VERSION +#define NTDDI_VERSION NTDDI_WIN8 +// Needed for access to IApplicationActivationManager +#include <shlobj.h> + +#include <mbstring.h> +#include <shlwapi.h> + +#include <lm.h> +#undef ACCESS_READ + +#ifndef MAX_BUF +#define MAX_BUF 4096 +#endif + +#define REG_SUCCEEDED(val) \ + (val == ERROR_SUCCESS) + +#define REG_FAILED(val) \ + (val != ERROR_SUCCESS) + +#define NS_TASKBAR_CONTRACTID "@mozilla.org/windows-taskbar;1" + +using mozilla::IsWin8OrLater; +using namespace mozilla; +using namespace mozilla::gfx; + +NS_IMPL_ISUPPORTS(nsWindowsShellService, nsIWindowsShellService, nsIShellService) + +static nsresult +OpenKeyForReading(HKEY aKeyRoot, const nsAString& aKeyName, HKEY* aKey) +{ + const nsString &flatName = PromiseFlatString(aKeyName); + + DWORD res = ::RegOpenKeyExW(aKeyRoot, flatName.get(), 0, KEY_READ, aKey); + switch (res) { + case ERROR_SUCCESS: + break; + case ERROR_ACCESS_DENIED: + return NS_ERROR_FILE_ACCESS_DENIED; + case ERROR_FILE_NOT_FOUND: + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; +} + +/////////////////////////////////////////////////////////////////////////////// +// Default Browser Registry Settings +// +// The setting of these values are made by an external binary since writing +// these values may require elevation. +// +// - File Extension Mappings +// ----------------------- +// The following file extensions: +// .htm .html .shtml .xht .xhtml +// are mapped like so: +// +// HKCU\SOFTWARE\Classes\.<ext>\ (default) REG_SZ PaleMoonHTML +// +// as aliases to the class: +// +// HKCU\SOFTWARE\Classes\PaleMoonHTML\ +// DefaultIcon (default) REG_SZ <apppath>,1 +// shell\open\command (default) REG_SZ <apppath> -osint -url "%1" +// shell\open\ddeexec (default) REG_SZ <empty string> +// +// - Windows Vista and above Protocol Handler +// +// HKCU\SOFTWARE\Classes\PaleMoonURL\ (default) REG_SZ <appname> URL +// EditFlags REG_DWORD 2 +// FriendlyTypeName REG_SZ <appname> URL +// DefaultIcon (default) REG_SZ <apppath>,1 +// shell\open\command (default) REG_SZ <apppath> -osint -url "%1" +// shell\open\ddeexec (default) REG_SZ <empty string> +// +// - Protocol Mappings +// ----------------- +// The following protocols: +// HTTP, HTTPS, FTP +// are mapped like so: +// +// HKCU\SOFTWARE\Classes\<protocol>\ +// DefaultIcon (default) REG_SZ <apppath>,1 +// shell\open\command (default) REG_SZ <apppath> -osint -url "%1" +// shell\open\ddeexec (default) REG_SZ <empty string> +// +// - Windows Start Menu (XP SP1 and newer) +// ------------------------------------------------- +// The following keys are set to make PaleMoon appear in the Start Menu as the +// browser: +// +// HKCU\SOFTWARE\Clients\StartMenuInternet\PaleMoon.EXE\ +// (default) REG_SZ <appname> +// DefaultIcon (default) REG_SZ <apppath>,0 +// InstallInfo HideIconsCommand REG_SZ <uninstpath> /HideShortcuts +// InstallInfo IconsVisible REG_DWORD 1 +// InstallInfo ReinstallCommand REG_SZ <uninstpath> /SetAsDefaultAppGlobal +// InstallInfo ShowIconsCommand REG_SZ <uninstpath> /ShowShortcuts +// shell\open\command (default) REG_SZ <apppath> +// shell\properties (default) REG_SZ <appname> &Options +// shell\properties\command (default) REG_SZ <apppath> -preferences +// shell\safemode (default) REG_SZ <appname> &Safe Mode +// shell\safemode\command (default) REG_SZ <apppath> -safe-mode +// + +// The values checked are all default values so the value name is not needed. +typedef struct { + const char* keyName; + const char* valueData; + const char* oldValueData; +} SETTING; + +#define APP_REG_NAME L"Pale Moon" +#define VAL_FILE_ICON "%APPPATH%,1" +#define VAL_OPEN "\"%APPPATH%\" -osint -url \"%1\"" +#define OLD_VAL_OPEN "\"%APPPATH%\" -requestPending -osint -url \"%1\"" +#define DI "\\DefaultIcon" +#define SOC "\\shell\\open\\command" +#define SOD "\\shell\\open\\ddeexec" +// Used for updating the FTP protocol handler's shell open command under HKCU. +#define FTP_SOC L"Software\\Classes\\ftp\\shell\\open\\command" + +#define MAKE_KEY_NAME1(PREFIX, MID) \ + PREFIX MID + +// The DefaultIcon registry key value should never be used when checking if +// PaleMoon is the default browser for file handlers since other applications +// (e.g. MS Office) may modify the DefaultIcon registry key value to add Icon +// Handlers. see http://msdn2.microsoft.com/en-us/library/aa969357.aspx for +// more info. The FTP protocol is not checked so advanced users can set the FTP +// handler to another application and still have PaleMoon check if it is the +// default HTTP and HTTPS handler. +// *** Do not add additional checks here unless you skip them when aForAllTypes +// is false below***. +static SETTING gSettings[] = { + // File Handler Class + // ***keep this as the first entry because when aForAllTypes is not set below + // it will skip over this check.*** + { MAKE_KEY_NAME1("PaleMoonHTML", SOC), VAL_OPEN, OLD_VAL_OPEN }, + + // Protocol Handler Class - for Vista and above + { MAKE_KEY_NAME1("PaleMoonURL", SOC), VAL_OPEN, OLD_VAL_OPEN }, + + // Protocol Handlers + { MAKE_KEY_NAME1("HTTP", DI), VAL_FILE_ICON }, + { MAKE_KEY_NAME1("HTTP", SOC), VAL_OPEN, OLD_VAL_OPEN }, + { MAKE_KEY_NAME1("HTTPS", DI), VAL_FILE_ICON }, + { MAKE_KEY_NAME1("HTTPS", SOC), VAL_OPEN, OLD_VAL_OPEN } +}; + +// The settings to disable DDE are separate from the default browser settings +// since they are only checked when PaleMoon is the default browser and if they +// are incorrect they are fixed without notifying the user. +static SETTING gDDESettings[] = { + // File Handler Class + { MAKE_KEY_NAME1("Software\\Classes\\PaleMoonHTML", SOD) }, + + // Protocol Handler Class - for Vista and above + { MAKE_KEY_NAME1("Software\\Classes\\PaleMoonURL", SOD) }, + + // Protocol Handlers + { MAKE_KEY_NAME1("Software\\Classes\\FTP", SOD) }, + { MAKE_KEY_NAME1("Software\\Classes\\HTTP", SOD) }, + { MAKE_KEY_NAME1("Software\\Classes\\HTTPS", SOD) } +}; + +nsresult +GetHelperPath(nsAutoString& aPath) +{ + nsresult rv; + nsCOMPtr<nsIProperties> directoryService = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> appHelper; + rv = directoryService->Get(XRE_EXECUTABLE_FILE, + NS_GET_IID(nsIFile), + getter_AddRefs(appHelper)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appHelper->SetNativeLeafName(NS_LITERAL_CSTRING("uninstall")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appHelper->AppendNative(NS_LITERAL_CSTRING("helper.exe")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appHelper->GetPath(aPath); + + aPath.Insert(L'"', 0); + aPath.Append(L'"'); + return rv; +} + +nsresult +LaunchHelper(nsAutoString& aPath) +{ + STARTUPINFOW si = {sizeof(si), 0}; + PROCESS_INFORMATION pi = {0}; + + if (!CreateProcessW(nullptr, (LPWSTR)aPath.get(), nullptr, nullptr, FALSE, + 0, nullptr, nullptr, &si, &pi)) { + return NS_ERROR_FAILURE; + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return NS_OK; +} + +NS_IMETHODIMP +nsWindowsShellService::ShortcutMaintenance() +{ + nsresult rv; + + // XXX App ids were updated to a constant install path hash, + // XXX this code can be removed after a few upgrade cycles. + + // Launch helper.exe so it can update the application user model ids on + // shortcuts in the user's taskbar and start menu. This keeps older pinned + // shortcuts grouped correctly after major updates. Note, we also do this + // through the upgrade installer script, however, this is the only place we + // have a chance to trap links created by users who do control the install/ + // update process of the browser. + + nsCOMPtr<nsIWinTaskbar> taskbarInfo = + do_GetService(NS_TASKBAR_CONTRACTID); + if (!taskbarInfo) // If we haven't built with win7 sdk features, this fails. + return NS_OK; + + // Avoid if this isn't Win7+ + bool isSupported = false; + taskbarInfo->GetAvailable(&isSupported); + if (!isSupported) + return NS_OK; + + nsAutoString appId; + if (NS_FAILED(taskbarInfo->GetDefaultGroupId(appId))) + return NS_ERROR_UNEXPECTED; + + NS_NAMED_LITERAL_CSTRING(prefName, "browser.taskbar.lastgroupid"); + nsCOMPtr<nsIPrefBranch> prefs = + do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!prefs) + return NS_ERROR_UNEXPECTED; + + nsCOMPtr<nsISupportsString> prefString; + rv = prefs->GetComplexValue(prefName.get(), + NS_GET_IID(nsISupportsString), + getter_AddRefs(prefString)); + if (NS_SUCCEEDED(rv)) { + nsAutoString version; + prefString->GetData(version); + if (!version.IsEmpty() && version.Equals(appId)) { + // We're all good, get out of here. + return NS_OK; + } + } + // Update the version in prefs + prefString = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv); + if (NS_FAILED(rv)) + return rv; + + prefString->SetData(appId); + rv = prefs->SetComplexValue(prefName.get(), + NS_GET_IID(nsISupportsString), + prefString); + if (NS_FAILED(rv)) { + NS_WARNING("Couldn't set last user model id!"); + return NS_ERROR_UNEXPECTED; + } + + nsAutoString appHelperPath; + if (NS_FAILED(GetHelperPath(appHelperPath))) + return NS_ERROR_UNEXPECTED; + + appHelperPath.AppendLiteral(" /UpdateShortcutAppUserModelIds"); + + return LaunchHelper(appHelperPath); +} + +static bool +IsAARDefault(const RefPtr<IApplicationAssociationRegistration>& pAAR, + LPCWSTR aClassName) +{ + // Make sure the Prog ID matches what we have + LPWSTR registeredApp; + bool isProtocol = *aClassName != L'.'; + ASSOCIATIONTYPE queryType = isProtocol ? AT_URLPROTOCOL : AT_FILEEXTENSION; + HRESULT hr = pAAR->QueryCurrentDefault(aClassName, queryType, AL_EFFECTIVE, + ®isteredApp); + if (FAILED(hr)) { + return false; + } + + LPCWSTR progID = isProtocol ? L"PaleMoonURL" : L"PaleMoonHTML"; + bool isDefault = !wcsicmp(registeredApp, progID); + CoTaskMemFree(registeredApp); + + return isDefault; +} + +static void +IsDefaultBrowserWin8(bool aCheckAllTypes, bool* aIsDefaultBrowser) +{ + RefPtr<IApplicationAssociationRegistration> pAAR; + HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistration, + nullptr, + CLSCTX_INPROC, + IID_IApplicationAssociationRegistration, + getter_AddRefs(pAAR)); + if (FAILED(hr)) { + return; + } + + bool res = IsAARDefault(pAAR, L"http"); + if (*aIsDefaultBrowser) { + *aIsDefaultBrowser = res; + } + res = IsAARDefault(pAAR, L".html"); + if (*aIsDefaultBrowser && aCheckAllTypes) { + *aIsDefaultBrowser = res; + } +} + +/* + * Query's the AAR for the default status. + * This only checks for PaleMoonURL and if aCheckAllTypes is set, then + * it also checks for PaleMoonHTML. Note that those ProgIDs are shared + * by all PaleMoon browsers. +*/ +bool +nsWindowsShellService::IsDefaultBrowserVista(bool aCheckAllTypes, + bool* aIsDefaultBrowser) +{ + RefPtr<IApplicationAssociationRegistration> pAAR; + HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistration, + nullptr, + CLSCTX_INPROC, + IID_IApplicationAssociationRegistration, + getter_AddRefs(pAAR)); + if (FAILED(hr)) { + return false; + } + + if (aCheckAllTypes) { + BOOL res; + hr = pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE, + APP_REG_NAME, + &res); + *aIsDefaultBrowser = res; + } else if (!IsWin8OrLater()) { + *aIsDefaultBrowser = IsAARDefault(pAAR, L"http"); + } + + return true; +} + +NS_IMETHODIMP +nsWindowsShellService::IsDefaultBrowser(bool aStartupCheck, + bool aForAllTypes, + bool* aIsDefaultBrowser) +{ + // Assume we're the default unless one of the several checks below tell us + // otherwise. + *aIsDefaultBrowser = true; + + wchar_t exePath[MAX_BUF]; + if (!::GetModuleFileNameW(0, exePath, MAX_BUF)) + return NS_ERROR_FAILURE; + + // Convert the path to a long path since GetModuleFileNameW returns the path + // that was used to launch PaleMoon which is not necessarily a long path. + if (!::GetLongPathNameW(exePath, exePath, MAX_BUF)) + return NS_ERROR_FAILURE; + + nsAutoString appLongPath(exePath); + + HKEY theKey; + DWORD res; + nsresult rv; + wchar_t currValue[MAX_BUF]; + + SETTING* settings = gSettings; + if (!aForAllTypes && IsWin8OrLater()) { + // Skip over the file handler check + settings++; + } + + SETTING* end = gSettings + sizeof(gSettings) / sizeof(SETTING); + + for (; settings < end; ++settings) { + NS_ConvertUTF8toUTF16 keyName(settings->keyName); + NS_ConvertUTF8toUTF16 valueData(settings->valueData); + int32_t offset = valueData.Find("%APPPATH%"); + valueData.Replace(offset, 9, appLongPath); + + rv = OpenKeyForReading(HKEY_CLASSES_ROOT, keyName, &theKey); + if (NS_FAILED(rv)) { + *aIsDefaultBrowser = false; + return NS_OK; + } + + ::ZeroMemory(currValue, sizeof(currValue)); + DWORD len = sizeof currValue; + res = ::RegQueryValueExW(theKey, L"", nullptr, nullptr, + (LPBYTE)currValue, &len); + // Close the key that was opened. + ::RegCloseKey(theKey); + if (REG_FAILED(res) || + _wcsicmp(valueData.get(), currValue)) { + // Key wasn't set or was set to something other than our registry entry. + NS_ConvertUTF8toUTF16 oldValueData(settings->oldValueData); + offset = oldValueData.Find("%APPPATH%"); + oldValueData.Replace(offset, 9, appLongPath); + // The current registry value doesn't match the current or the old format. + if (_wcsicmp(oldValueData.get(), currValue)) { + *aIsDefaultBrowser = false; + return NS_OK; + } + + res = ::RegOpenKeyExW(HKEY_CLASSES_ROOT, PromiseFlatString(keyName).get(), + 0, KEY_SET_VALUE, &theKey); + if (REG_FAILED(res)) { + // If updating the open command fails try to update it using the helper + // application when setting PaleMoon as the default browser. + *aIsDefaultBrowser = false; + return NS_OK; + } + + const nsString &flatValue = PromiseFlatString(valueData); + res = ::RegSetValueExW(theKey, L"", 0, REG_SZ, + (const BYTE *) flatValue.get(), + (flatValue.Length() + 1) * sizeof(char16_t)); + // Close the key that was created. + ::RegCloseKey(theKey); + if (REG_FAILED(res)) { + // If updating the open command fails try to update it using the helper + // application when setting PaleMoon as the default browser. + *aIsDefaultBrowser = false; + return NS_OK; + } + } + } + + // Only check if PaleMoon is the default browser on Vista and above if the + // previous checks show that PaleMoon is the default browser. + if (*aIsDefaultBrowser) { + IsDefaultBrowserVista(aForAllTypes, aIsDefaultBrowser); + if (IsWin8OrLater()) { + IsDefaultBrowserWin8(aForAllTypes, aIsDefaultBrowser); + } + } + + // To handle the case where DDE isn't disabled due for a user because there + // account didn't perform a PaleMoon update this will check if PaleMoon is the + // default browser and if dde is disabled for each handler + // and if it isn't disable it. When PaleMoon is not the default browser the + // helper application will disable dde for each handler. + if (*aIsDefaultBrowser && aForAllTypes) { + // Check ftp settings + + end = gDDESettings + sizeof(gDDESettings) / sizeof(SETTING); + + for (settings = gDDESettings; settings < end; ++settings) { + NS_ConvertUTF8toUTF16 keyName(settings->keyName); + + rv = OpenKeyForReading(HKEY_CURRENT_USER, keyName, &theKey); + if (NS_FAILED(rv)) { + ::RegCloseKey(theKey); + // If disabling DDE fails try to disable it using the helper + // application when setting PaleMoon as the default browser. + *aIsDefaultBrowser = false; + return NS_OK; + } + + ::ZeroMemory(currValue, sizeof(currValue)); + DWORD len = sizeof currValue; + res = ::RegQueryValueExW(theKey, L"", nullptr, nullptr, + (LPBYTE)currValue, &len); + // Close the key that was opened. + ::RegCloseKey(theKey); + if (REG_FAILED(res) || char16_t('\0') != *currValue) { + // Key wasn't set or was set to something other than our registry entry. + // Delete the key along with all of its childrean and then recreate it. + const nsString &flatName = PromiseFlatString(keyName); + ::SHDeleteKeyW(HKEY_CURRENT_USER, flatName.get()); + res = ::RegCreateKeyExW(HKEY_CURRENT_USER, flatName.get(), 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, + nullptr, &theKey, nullptr); + if (REG_FAILED(res)) { + // If disabling DDE fails try to disable it using the helper + // application when setting PaleMoon as the default browser. + *aIsDefaultBrowser = false; + return NS_OK; + } + + res = ::RegSetValueExW(theKey, L"", 0, REG_SZ, (const BYTE *) L"", + sizeof(char16_t)); + // Close the key that was created. + ::RegCloseKey(theKey); + if (REG_FAILED(res)) { + // If disabling DDE fails try to disable it using the helper + // application when setting PaleMoon as the default browser. + *aIsDefaultBrowser = false; + return NS_OK; + } + } + } + + // Update the FTP protocol handler's shell open command if it is the old + // format. + res = ::RegOpenKeyExW(HKEY_CURRENT_USER, FTP_SOC, 0, KEY_ALL_ACCESS, + &theKey); + // Don't update the FTP protocol handler's shell open command when opening + // its registry key fails under HKCU since it most likely doesn't exist. + if (NS_FAILED(rv)) { + return NS_OK; + } + + NS_ConvertUTF8toUTF16 oldValueOpen(OLD_VAL_OPEN); + int32_t offset = oldValueOpen.Find("%APPPATH%"); + oldValueOpen.Replace(offset, 9, appLongPath); + + ::ZeroMemory(currValue, sizeof(currValue)); + DWORD len = sizeof currValue; + res = ::RegQueryValueExW(theKey, L"", nullptr, nullptr, (LPBYTE)currValue, + &len); + + // Don't update the FTP protocol handler's shell open command when the + // current registry value doesn't exist or matches the old format. + if (REG_FAILED(res) || + _wcsicmp(oldValueOpen.get(), currValue)) { + ::RegCloseKey(theKey); + return NS_OK; + } + + NS_ConvertUTF8toUTF16 valueData(VAL_OPEN); + valueData.Replace(offset, 9, appLongPath); + const nsString &flatValue = PromiseFlatString(valueData); + res = ::RegSetValueExW(theKey, L"", 0, REG_SZ, + (const BYTE *) flatValue.get(), + (flatValue.Length() + 1) * sizeof(char16_t)); + // Close the key that was created. + ::RegCloseKey(theKey); + // If updating the FTP protocol handlers shell open command fails try to + // update it using the helper application when setting PaleMoon as the + // default browser. + if (REG_FAILED(res)) { + *aIsDefaultBrowser = false; + } + } + + return NS_OK; +} + +static nsresult +DynSHOpenWithDialog(HWND hwndParent, const OPENASINFO *poainfo) +{ + // shell32.dll is in the knownDLLs list so will always be loaded from the + // system32 directory. + static const wchar_t kSehllLibraryName[] = L"shell32.dll"; + HMODULE shellDLL = ::LoadLibraryW(kSehllLibraryName); + if (!shellDLL) { + return NS_ERROR_FAILURE; + } + + decltype(SHOpenWithDialog)* SHOpenWithDialogFn = + (decltype(SHOpenWithDialog)*) GetProcAddress(shellDLL, "SHOpenWithDialog"); + + if (!SHOpenWithDialogFn) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + HRESULT hr = SHOpenWithDialogFn(hwndParent, poainfo); + if (SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED))) { + rv = NS_OK; + } else { + rv = NS_ERROR_FAILURE; + } + FreeLibrary(shellDLL); + return rv; +} + +nsresult +nsWindowsShellService::LaunchControlPanelDefaultsSelectionUI() +{ + IApplicationAssociationRegistrationUI* pAARUI; + HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistrationUI, + NULL, + CLSCTX_INPROC, + IID_IApplicationAssociationRegistrationUI, + (void**)&pAARUI); + if (SUCCEEDED(hr)) { + hr = pAARUI->LaunchAdvancedAssociationUI(APP_REG_NAME); + pAARUI->Release(); + } + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult +nsWindowsShellService::LaunchControlPanelDefaultPrograms() +{ + // Build the path control.exe path safely + WCHAR controlEXEPath[MAX_PATH + 1] = { '\0' }; + if (!GetSystemDirectoryW(controlEXEPath, MAX_PATH)) { + return NS_ERROR_FAILURE; + } + LPCWSTR controlEXE = L"control.exe"; + if (wcslen(controlEXEPath) + wcslen(controlEXE) >= MAX_PATH) { + return NS_ERROR_FAILURE; + } + if (!PathAppendW(controlEXEPath, controlEXE)) { + return NS_ERROR_FAILURE; + } + + WCHAR params[] = L"control.exe /name Microsoft.DefaultPrograms /page " + "pageDefaultProgram\\pageAdvancedSettings?pszAppName=" APP_REG_NAME; + STARTUPINFOW si = {sizeof(si), 0}; + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_SHOWDEFAULT; + PROCESS_INFORMATION pi = {0}; + if (!CreateProcessW(controlEXEPath, params, nullptr, nullptr, FALSE, + 0, nullptr, nullptr, &si, &pi)) { + return NS_ERROR_FAILURE; + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return NS_OK; +} + +static bool +IsWindowsLogonConnected() +{ + WCHAR userName[UNLEN + 1]; + DWORD size = ArrayLength(userName); + if (!GetUserNameW(userName, &size)) { + return false; + } + + LPUSER_INFO_24 info; + if (NetUserGetInfo(nullptr, userName, 24, (LPBYTE *)&info) + != NERR_Success) { + return false; + } + bool connected = info->usri24_internet_identity; + NetApiBufferFree(info); + + return connected; +} + +static bool +SettingsAppBelievesConnected() +{ + nsresult rv; + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + if (NS_FAILED(rv)) { + return false; + } + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + NS_LITERAL_STRING("SOFTWARE\\Microsoft\\Windows\\Shell\\Associations"), + nsIWindowsRegKey::ACCESS_READ); + if (NS_FAILED(rv)) { + return false; + } + + uint32_t value; + rv = regKey->ReadIntValue(NS_LITERAL_STRING("IsConnectedAtLogon"), &value); + if (NS_FAILED(rv)) { + return false; + } + + return !!value; +} + +nsresult +nsWindowsShellService::LaunchModernSettingsDialogDefaultApps() +{ + if (!IsWindowsBuildOrLater(14965) && + !IsWindowsLogonConnected() && SettingsAppBelievesConnected()) { + // Use the classic Control Panel to work around a bug of older + // builds of Windows 10. + return LaunchControlPanelDefaultPrograms(); + } + + IApplicationActivationManager* pActivator; + HRESULT hr = CoCreateInstance(CLSID_ApplicationActivationManager, + nullptr, + CLSCTX_INPROC, + IID_IApplicationActivationManager, + (void**)&pActivator); + + if (SUCCEEDED(hr)) { + DWORD pid; + hr = pActivator->ActivateApplication( + L"windows.immersivecontrolpanel_cw5n1h2txyewy" + L"!microsoft.windows.immersivecontrolpanel", + L"page=SettingsPageAppsDefaults", AO_NONE, &pid); + if (SUCCEEDED(hr)) { + // Do not check error because we could at least open + // the "Default apps" setting. + pActivator->ActivateApplication( + L"windows.immersivecontrolpanel_cw5n1h2txyewy" + L"!microsoft.windows.immersivecontrolpanel", + L"page=SettingsPageAppsDefaults" + L"&target=SystemSettings_DefaultApps_Browser", AO_NONE, &pid); + } + pActivator->Release(); + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult +nsWindowsShellService::InvokeHTTPOpenAsVerb() +{ + nsCOMPtr<nsIURLFormatter> formatter( + do_GetService("@mozilla.org/toolkit/URLFormatterService;1")); + if (!formatter) { + return NS_ERROR_UNEXPECTED; + } + + nsString urlStr; + nsresult rv = formatter->FormatURLPref( + NS_LITERAL_STRING("app.support.baseURL"), urlStr); + if (NS_FAILED(rv)) { + return rv; + } + if (!StringBeginsWith(urlStr, NS_LITERAL_STRING("https://"))) { + return NS_ERROR_FAILURE; + } + urlStr.AppendLiteral("win10-default-browser"); + + SHELLEXECUTEINFOW seinfo = { sizeof(SHELLEXECUTEINFOW) }; + seinfo.lpVerb = L"openas"; + seinfo.lpFile = urlStr.get(); + seinfo.nShow = SW_SHOWNORMAL; + if (!ShellExecuteExW(&seinfo)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult +nsWindowsShellService::LaunchHTTPHandlerPane() +{ + OPENASINFO info; + info.pcszFile = L"http"; + info.pcszClass = nullptr; + info.oaifInFlags = OAIF_FORCE_REGISTRATION | + OAIF_URL_PROTOCOL | + OAIF_REGISTER_EXT; + return DynSHOpenWithDialog(nullptr, &info); +} + +NS_IMETHODIMP +nsWindowsShellService::SetDefaultBrowser(bool aClaimAllTypes, bool aForAllUsers) +{ + nsAutoString appHelperPath; + if (NS_FAILED(GetHelperPath(appHelperPath))) + return NS_ERROR_FAILURE; + + if (aForAllUsers) { + appHelperPath.AppendLiteral(" /SetAsDefaultAppGlobal"); + } else { + appHelperPath.AppendLiteral(" /SetAsDefaultAppUser"); + } + + nsresult rv = LaunchHelper(appHelperPath); + if (NS_SUCCEEDED(rv) && IsWin8OrLater()) { + if (aClaimAllTypes) { + if (IsWin10OrLater()) { + rv = LaunchModernSettingsDialogDefaultApps(); + } else { + rv = LaunchControlPanelDefaultsSelectionUI(); + } + // The above call should never really fail, but just in case + // fall back to showing the HTTP association screen only. + if (NS_FAILED(rv)) { + if (IsWin10OrLater()) { + rv = InvokeHTTPOpenAsVerb(); + } else { + rv = LaunchHTTPHandlerPane(); + } + } + } else { + // Windows 10 blocks attempts to load the + // HTTP Handler association dialog. + if (IsWin10OrLater()) { + rv = LaunchModernSettingsDialogDefaultApps(); + } else { + rv = LaunchHTTPHandlerPane(); + } + + // The above call should never really fail, but just in case + // fall back to showing control panel for all defaults + if (NS_FAILED(rv)) { + rv = LaunchControlPanelDefaultsSelectionUI(); + } + } + } + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + (void) prefs->SetBoolPref(PREF_CHECKDEFAULTBROWSER, true); + // Reset the number of times the dialog should be shown + // before it is silenced. + (void) prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, 0); + } + + return rv; +} + +static nsresult +WriteBitmap(nsIFile* aFile, imgIContainer* aImage) +{ + nsresult rv; + + RefPtr<SourceSurface> surface = + aImage->GetFrame(imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE); + NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE); + + // For either of the following formats we want to set the biBitCount member + // of the BITMAPINFOHEADER struct to 32, below. For that value the bitmap + // format defines that the A8/X8 WORDs in the bitmap byte stream be ignored + // for the BI_RGB value we use for the biCompression member. + MOZ_ASSERT(surface->GetFormat() == SurfaceFormat::B8G8R8A8 || + surface->GetFormat() == SurfaceFormat::B8G8R8X8); + + RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface(); + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + + int32_t width = dataSurface->GetSize().width; + int32_t height = dataSurface->GetSize().height; + int32_t bytesPerPixel = 4 * sizeof(uint8_t); + uint32_t bytesPerRow = bytesPerPixel * width; + + // initialize these bitmap structs which we will later + // serialize directly to the head of the bitmap file + BITMAPINFOHEADER bmi; + bmi.biSize = sizeof(BITMAPINFOHEADER); + bmi.biWidth = width; + bmi.biHeight = height; + bmi.biPlanes = 1; + bmi.biBitCount = (WORD)bytesPerPixel*8; + bmi.biCompression = BI_RGB; + bmi.biSizeImage = bytesPerRow * height; + bmi.biXPelsPerMeter = 0; + bmi.biYPelsPerMeter = 0; + bmi.biClrUsed = 0; + bmi.biClrImportant = 0; + + BITMAPFILEHEADER bf; + bf.bfType = 0x4D42; // 'BM' + bf.bfReserved1 = 0; + bf.bfReserved2 = 0; + bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); + bf.bfSize = bf.bfOffBits + bmi.biSizeImage; + + // get a file output stream + nsCOMPtr<nsIOutputStream> stream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile); + NS_ENSURE_SUCCESS(rv, rv); + + DataSourceSurface::MappedSurface map; + if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) { + return NS_ERROR_FAILURE; + } + + // write the bitmap headers and rgb pixel data to the file + rv = NS_ERROR_FAILURE; + if (stream) { + uint32_t written; + stream->Write((const char*)&bf, sizeof(BITMAPFILEHEADER), &written); + if (written == sizeof(BITMAPFILEHEADER)) { + stream->Write((const char*)&bmi, sizeof(BITMAPINFOHEADER), &written); + if (written == sizeof(BITMAPINFOHEADER)) { + // write out the image data backwards because the desktop won't + // show bitmaps with negative heights for top-to-bottom + uint32_t i = map.mStride * height; + do { + i -= map.mStride; + stream->Write(((const char*)map.mData) + i, bytesPerRow, &written); + if (written == bytesPerRow) { + rv = NS_OK; + } else { + rv = NS_ERROR_FAILURE; + break; + } + } while (i != 0); + } + } + + stream->Close(); + } + + dataSurface->Unmap(); + + return rv; +} + +NS_IMETHODIMP +nsWindowsShellService::SetDesktopBackground(nsIDOMElement* aElement, + int32_t aPosition) +{ + nsresult rv; + + nsCOMPtr<imgIContainer> container; + nsCOMPtr<nsIDOMHTMLImageElement> imgElement(do_QueryInterface(aElement)); + if (!imgElement) { + // XXX write background loading stuff! + return NS_ERROR_NOT_AVAILABLE; + } + else { + nsCOMPtr<nsIImageLoadingContent> imageContent = + do_QueryInterface(aElement, &rv); + if (!imageContent) + return rv; + + // get the image container + nsCOMPtr<imgIRequest> request; + rv = imageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(request)); + if (!request) + return rv; + rv = request->GetImage(getter_AddRefs(container)); + if (!container) + return NS_ERROR_FAILURE; + } + + // get the file name from localized strings + nsCOMPtr<nsIStringBundleService> + bundleService(do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStringBundle> shellBundle; + rv = bundleService->CreateBundle(SHELLSERVICE_PROPERTIES, + getter_AddRefs(shellBundle)); + NS_ENSURE_SUCCESS(rv, rv); + + // e.g. "Desktop Background.bmp" + nsString fileLeafName; + rv = shellBundle->GetStringFromName + (u"desktopBackgroundLeafNameWin", + getter_Copies(fileLeafName)); + NS_ENSURE_SUCCESS(rv, rv); + + // get the profile root directory + nsCOMPtr<nsIFile> file; + rv = NS_GetSpecialDirectory(NS_APP_APPLICATION_REGISTRY_DIR, + getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + // eventually, the path is "%APPDATA%\Mozilla\PaleMoon\Desktop Background.bmp" + rv = file->Append(fileLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString path; + rv = file->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + // write the bitmap to a file in the profile directory + rv = WriteBitmap(file, container); + + // if the file was written successfully, set it as the system wallpaper + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + NS_LITERAL_STRING("Control Panel\\Desktop"), + nsIWindowsRegKey::ACCESS_SET_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString tile; + nsAutoString style; + switch (aPosition) { + case BACKGROUND_TILE: + style.Assign('0'); + tile.Assign('1'); + break; + case BACKGROUND_CENTER: + style.Assign('0'); + tile.Assign('0'); + break; + case BACKGROUND_STRETCH: + style.Assign('2'); + tile.Assign('0'); + break; + case BACKGROUND_FILL: + style.AssignLiteral("10"); + tile.Assign('0'); + break; + case BACKGROUND_FIT: + style.Assign('6'); + tile.Assign('0'); + break; + } + + rv = regKey->WriteStringValue(NS_LITERAL_STRING("TileWallpaper"), tile); + NS_ENSURE_SUCCESS(rv, rv); + rv = regKey->WriteStringValue(NS_LITERAL_STRING("WallpaperStyle"), style); + NS_ENSURE_SUCCESS(rv, rv); + rv = regKey->Close(); + NS_ENSURE_SUCCESS(rv, rv); + + ::SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, (PVOID)path.get(), + SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + } + return rv; +} + +NS_IMETHODIMP +nsWindowsShellService::OpenApplication(int32_t aApplication) +{ + nsAutoString application; + switch (aApplication) { + case nsIShellService::APPLICATION_MAIL: + application.AssignLiteral("Mail"); + break; + case nsIShellService::APPLICATION_NEWS: + application.AssignLiteral("News"); + break; + } + + // The Default Client section of the Windows Registry looks like this: + // + // Clients\aClient\ + // e.g. aClient = "Mail"... + // \Mail\(default) = Client Subkey Name + // \Client Subkey Name + // \Client Subkey Name\shell\open\command\ + // \Client Subkey Name\shell\open\command\(default) = path to exe + // + + // Find the default application for this class. + HKEY theKey; + nsresult rv = OpenKeyForReading(HKEY_CLASSES_ROOT, application, &theKey); + if (NS_FAILED(rv)) + return rv; + + wchar_t buf[MAX_BUF]; + DWORD type, len = sizeof buf; + DWORD res = ::RegQueryValueExW(theKey, EmptyString().get(), 0, + &type, (LPBYTE)&buf, &len); + + if (REG_FAILED(res) || !*buf) + return NS_OK; + + // Close the key we opened. + ::RegCloseKey(theKey); + + // Find the "open" command + application.Append('\\'); + application.Append(buf); + application.AppendLiteral("\\shell\\open\\command"); + + rv = OpenKeyForReading(HKEY_CLASSES_ROOT, application, &theKey); + if (NS_FAILED(rv)) + return rv; + + ::ZeroMemory(buf, sizeof(buf)); + len = sizeof buf; + res = ::RegQueryValueExW(theKey, EmptyString().get(), 0, + &type, (LPBYTE)&buf, &len); + if (REG_FAILED(res) || !*buf) + return NS_ERROR_FAILURE; + + // Close the key we opened. + ::RegCloseKey(theKey); + + // Look for any embedded environment variables and substitute their + // values, as |::CreateProcessW| is unable to do this. + nsAutoString path(buf); + int32_t end = path.Length(); + int32_t cursor = 0, temp = 0; + ::ZeroMemory(buf, sizeof(buf)); + do { + cursor = path.FindChar('%', cursor); + if (cursor < 0) + break; + + temp = path.FindChar('%', cursor + 1); + ++cursor; + + ::ZeroMemory(&buf, sizeof(buf)); + + ::GetEnvironmentVariableW(nsAutoString(Substring(path, cursor, temp - cursor)).get(), + buf, sizeof(buf)); + + // "+ 2" is to subtract the extra characters used to delimit the environment + // variable ('%'). + path.Replace((cursor - 1), temp - cursor + 2, nsDependentString(buf)); + + ++cursor; + } + while (cursor < end); + + STARTUPINFOW si; + PROCESS_INFORMATION pi; + + ::ZeroMemory(&si, sizeof(STARTUPINFOW)); + ::ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + + BOOL success = ::CreateProcessW(nullptr, (LPWSTR)path.get(), nullptr, + nullptr, FALSE, 0, nullptr, nullptr, + &si, &pi); + if (!success) + return NS_ERROR_FAILURE; + + return NS_OK; +} + +NS_IMETHODIMP +nsWindowsShellService::GetDesktopBackgroundColor(uint32_t* aColor) +{ + uint32_t color = ::GetSysColor(COLOR_DESKTOP); + *aColor = (GetRValue(color) << 16) | (GetGValue(color) << 8) | GetBValue(color); + return NS_OK; +} + +NS_IMETHODIMP +nsWindowsShellService::SetDesktopBackgroundColor(uint32_t aColor) +{ + int aParameters[2] = { COLOR_BACKGROUND, COLOR_DESKTOP }; + BYTE r = (aColor >> 16); + BYTE g = (aColor << 16) >> 24; + BYTE b = (aColor << 24) >> 24; + COLORREF colors[2] = { RGB(r,g,b), RGB(r,g,b) }; + + ::SetSysColors(sizeof(aParameters) / sizeof(int), aParameters, colors); + + nsresult rv; + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + NS_LITERAL_STRING("Control Panel\\Colors"), + nsIWindowsRegKey::ACCESS_SET_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + + wchar_t rgb[12]; + _snwprintf(rgb, 12, L"%u %u %u", r, g, b); + + rv = regKey->WriteStringValue(NS_LITERAL_STRING("Background"), + nsDependentString(rgb)); + NS_ENSURE_SUCCESS(rv, rv); + + return regKey->Close(); +} + +nsWindowsShellService::nsWindowsShellService() +{ +} + +nsWindowsShellService::~nsWindowsShellService() +{ +} + +NS_IMETHODIMP +nsWindowsShellService::OpenApplicationWithURI(nsIFile* aApplication, + const nsACString& aURI) +{ + nsresult rv; + nsCOMPtr<nsIProcess> process = + do_CreateInstance("@mozilla.org/process/util;1", &rv); + if (NS_FAILED(rv)) + return rv; + + rv = process->Init(aApplication); + if (NS_FAILED(rv)) + return rv; + + const nsCString spec(aURI); + const char* specStr = spec.get(); + return process->Run(false, &specStr, 1); +} + +NS_IMETHODIMP +nsWindowsShellService::GetDefaultFeedReader(nsIFile** _retval) +{ + *_retval = nullptr; + + nsresult rv; + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + NS_LITERAL_STRING("feed\\shell\\open\\command"), + nsIWindowsRegKey::ACCESS_READ); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString path; + rv = regKey->ReadStringValue(EmptyString(), path); + NS_ENSURE_SUCCESS(rv, rv); + if (path.IsEmpty()) + return NS_ERROR_FAILURE; + + if (path.First() == '"') { + // Everything inside the quotes + path = Substring(path, 1, path.FindChar('"', 1) - 1); + } + else { + // Everything up to the first space + path = Substring(path, 0, path.FindChar(' ')); + } + + nsCOMPtr<nsIFile> defaultReader = + do_CreateInstance("@mozilla.org/file/local;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = defaultReader->InitWithPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = defaultReader->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists) + return NS_ERROR_FAILURE; + + NS_ADDREF(*_retval = defaultReader); + return NS_OK; +} diff --git a/browser/components/shell/nsWindowsShellService.h b/browser/components/shell/nsWindowsShellService.h new file mode 100644 index 000000000..06c6c3c9b --- /dev/null +++ b/browser/components/shell/nsWindowsShellService.h @@ -0,0 +1,37 @@ +/* -*- 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/. */ + +#ifndef nswindowsshellservice_h____ +#define nswindowsshellservice_h____ + +#include "nscore.h" +#include "nsStringAPI.h" +#include "nsIWindowsShellService.h" +#include "nsITimer.h" + +#include <windows.h> +#include <ole2.h> + +class nsWindowsShellService : public nsIWindowsShellService +{ + virtual ~nsWindowsShellService(); + +public: + nsWindowsShellService(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISHELLSERVICE + NS_DECL_NSIWINDOWSSHELLSERVICE + +protected: + bool IsDefaultBrowserVista(bool aCheckAllTypes, bool* aIsDefaultBrowser); + nsresult LaunchControlPanelDefaultsSelectionUI(); + nsresult LaunchControlPanelDefaultPrograms(); + nsresult LaunchModernSettingsDialogDefaultApps(); + nsresult InvokeHTTPOpenAsVerb(); + nsresult LaunchHTTPHandlerPane(); +}; + +#endif // nswindowsshellservice_h____ diff --git a/browser/components/statusbar/Downloads.jsm b/browser/components/statusbar/Downloads.jsm new file mode 100644 index 000000000..091fdad2e --- /dev/null +++ b/browser/components/statusbar/Downloads.jsm @@ -0,0 +1,674 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["S4EDownloadService"]; + +const CC = Components.classes; +const CI = Components.interfaces; +const CU = Components.utils; + +CU.import("resource://gre/modules/Services.jsm"); +CU.import("resource://gre/modules/PluralForm.jsm"); +CU.import("resource://gre/modules/DownloadUtils.jsm"); +CU.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +CU.import("resource://gre/modules/XPCOMUtils.jsm"); + +function S4EDownloadService(window, gBrowser, service, getters) +{ + this._window = window; + this._gBrowser = gBrowser; + this._service = service; + this._getters = getters; + + this._handler = new JSTransferHandler(this._window, this); +} + +S4EDownloadService.prototype = +{ + _window: null, + _gBrowser: null, + _service: null, + _getters: null, + + _handler: null, + _listening: false, + + _binding: false, + _customizing: false, + + _lastTime: Infinity, + + _dlActive: false, + _dlPaused: false, + _dlFinished: false, + + _dlCountStr: null, + _dlTimeStr: null, + + _dlProgressAvg: 0, + _dlProgressMax: 0, + _dlProgressMin: 0, + _dlProgressType: "active", + + _dlNotifyTimer: 0, + _dlNotifyGlowTimer: 0, + + init: function() + { + if(!this._getters.downloadButton) + { + this.uninit(); + return; + } + + if(this._listening) + { + return; + } + + this._handler.start(); + this._listening = true; + + this._lastTime = Infinity; + + this.updateBinding(); + this.updateStatus(); + }, + + uninit: function() + { + if(!this._listening) + { + return; + } + + this._listening = false; + this._handler.stop(); + + this.releaseBinding(); + }, + + destroy: function() + { + this.uninit(); + this._handler.destroy(); + + ["_window", "_gBrowser", "_service", "_getters", "_handler"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + updateBinding: function() + { + if(!this._listening) + { + this.releaseBinding(); + return; + } + + switch(this._service.downloadButtonAction) + { + case 1: // Default + this.attachBinding(); + break; + default: + this.releaseBinding(); + break; + } + }, + + attachBinding: function() + { + if(this._binding) + { + return; + } + + let db = this._window.DownloadsButton; + + db._getAnchorS4EBackup = db.getAnchor; + db.getAnchor = this.getAnchor.bind(this); + + db._releaseAnchorS4EBackup = db.releaseAnchor; + db.releaseAnchor = function() {}; + + this._binding = true; + }, + + releaseBinding: function() + { + if(!this._binding) + { + return; + } + + let db = this._window.DownloadsButton; + + db.getAnchor = db._getAnchorS4EBackup; + db.releaseAnchor = db._releaseAnchorS4EBackup; + + this._binding = false; + }, + + customizing: function(val) + { + this._customizing = val; + }, + + updateStatus: function(lastFinished) + { + if(!this._getters.downloadButton) + { + this.uninit(); + return; + } + + let numActive = 0; + let numPaused = 0; + let activeTotalSize = 0; + let activeTransferred = 0; + let activeMaxProgress = -Infinity; + let activeMinProgress = Infinity; + let pausedTotalSize = 0; + let pausedTransferred = 0; + let pausedMaxProgress = -Infinity; + let pausedMinProgress = Infinity; + let maxTime = -Infinity; + + let dls = ((this.isPrivateWindow) ? this._handler.activePrivateEntries() : this._handler.activeEntries()); + for(let dl of dls) + { + if(dl.state == CI.nsIDownloadManager.DOWNLOAD_DOWNLOADING) + { + numActive++; + if(dl.size > 0) + { + if(dl.speed > 0) + { + maxTime = Math.max(maxTime, (dl.size - dl.transferred) / dl.speed); + } + + activeTotalSize += dl.size; + activeTransferred += dl.transferred; + + let currentProgress = ((dl.transferred * 100) / dl.size); + activeMaxProgress = Math.max(activeMaxProgress, currentProgress); + activeMinProgress = Math.min(activeMinProgress, currentProgress); + } + } + else if(dl.state == CI.nsIDownloadManager.DOWNLOAD_PAUSED) + { + numPaused++; + if(dl.size > 0) + { + pausedTotalSize += dl.size; + pausedTransferred += dl.transferred; + + let currentProgress = ((dl.transferred * 100) / dl.size); + pausedMaxProgress = Math.max(pausedMaxProgress, currentProgress); + pausedMinProgress = Math.min(pausedMinProgress, currentProgress); + } + } + } + + if((numActive + numPaused) == 0) + { + this._dlActive = false; + this._dlFinished = lastFinished; + this.updateButton(); + this._lastTime = Infinity; + return; + } + + let dlPaused = (numActive == 0); + let dlStatus = ((dlPaused) ? this._getters.strings.getString("pausedDownloads") + : this._getters.strings.getString("activeDownloads")); + let dlCount = ((dlPaused) ? numPaused : numActive); + let dlTotalSize = ((dlPaused) ? pausedTotalSize : activeTotalSize); + let dlTransferred = ((dlPaused) ? pausedTransferred : activeTransferred); + let dlMaxProgress = ((dlPaused) ? pausedMaxProgress : activeMaxProgress); + let dlMinProgress = ((dlPaused) ? pausedMinProgress : activeMinProgress); + let dlProgressType = ((dlPaused) ? "paused" : "active"); + + [this._dlTimeStr, this._lastTime] = DownloadUtils.getTimeLeft(maxTime, this._lastTime); + this._dlCountStr = PluralForm.get(dlCount, dlStatus).replace("#1", dlCount); + this._dlProgressAvg = ((dlTotalSize == 0) ? 100 : ((dlTransferred * 100) / dlTotalSize)); + this._dlProgressMax = ((dlTotalSize == 0) ? 100 : dlMaxProgress); + this._dlProgressMin = ((dlTotalSize == 0) ? 100 : dlMinProgress); + this._dlProgressType = dlProgressType + ((dlTotalSize == 0) ? "-unknown" : ""); + this._dlPaused = dlPaused; + this._dlActive = true; + this._dlFinished = false; + + this.updateButton(); + }, + + updateButton: function() + { + let download_button = this._getters.downloadButton; + let download_tooltip = this._getters.downloadButtonTooltip; + let download_progress = this._getters.downloadButtonProgress; + let download_label = this._getters.downloadButtonLabel; + if(!download_button) + { + return; + } + + if(!this._dlActive) + { + download_button.collapsed = true; + download_label.value = download_tooltip.label = this._getters.strings.getString("noDownloads"); + + download_progress.collapsed = true; + download_progress.value = 0; + + if(this._dlFinished && this._handler.hasPBAPI && !this.isUIShowing) + { + this.callAttention(download_button); + } + return; + } + + switch(this._service.downloadProgress) + { + case 2: + download_progress.value = this._dlProgressMax; + break; + case 3: + download_progress.value = this._dlProgressMin; + break; + default: + download_progress.value = this._dlProgressAvg; + break; + } + download_progress.setAttribute("pmType", this._dlProgressType); + download_progress.collapsed = (this._service.downloadProgress == 0); + + download_label.value = this.buildString(this._service.downloadLabel); + download_tooltip.label = this.buildString(this._service.downloadTooltip); + + this.clearAttention(download_button); + download_button.collapsed = false; + }, + + callAttention: function(download_button) + { + if(this._dlNotifyGlowTimer != 0) + { + this._window.clearTimeout(this._dlNotifyGlowTimer); + this._dlNotifyGlowTimer = 0; + } + + download_button.setAttribute("attention", "true"); + + if(this._service.downloadNotifyTimeout) + { + this._dlNotifyGlowTimer = this._window.setTimeout(function(self, button) + { + self._dlNotifyGlowTimer = 0; + button.removeAttribute("attention"); + }, this._service.downloadNotifyTimeout, this, download_button); + } + }, + + clearAttention: function(download_button) + { + if(this._dlNotifyGlowTimer != 0) + { + this._window.clearTimeout(this._dlNotifyGlowTimer); + this._dlNotifyGlowTimer = 0; + } + + download_button.removeAttribute("attention"); + }, + + notify: function() + { + if(this._dlNotifyTimer == 0 && this._service.downloadNotifyAnimate) + { + let download_button_anchor = this._getters.downloadButtonAnchor; + let download_notify_anchor = this._getters.downloadNotifyAnchor; + if(download_button_anchor) + { + if(!download_notify_anchor.style.transform) + { + let bAnchorRect = download_button_anchor.getBoundingClientRect(); + let nAnchorRect = download_notify_anchor.getBoundingClientRect(); + + let translateX = bAnchorRect.left - nAnchorRect.left; + translateX += .5 * (bAnchorRect.width - nAnchorRect.width); + + let translateY = bAnchorRect.top - nAnchorRect.top; + translateY += .5 * (bAnchorRect.height - nAnchorRect.height); + + download_notify_anchor.style.transform = "translate(" + translateX + "px, " + translateY + "px)"; + } + + download_notify_anchor.setAttribute("notification", "finish"); + this._dlNotifyTimer = this._window.setTimeout(function(self, anchor) + { + self._dlNotifyTimer = 0; + anchor.removeAttribute("notification"); + anchor.style.transform = ""; + }, 1000, this, download_notify_anchor); + } + } + }, + + clearFinished: function() + { + this._dlFinished = false; + let download_button = this._getters.downloadButton; + if(download_button) + { + this.clearAttention(download_button); + } + }, + + getAnchor: function(aCallback) + { + if(this._customizing) + { + aCallback(null); + return; + } + + aCallback(this._getters.downloadButtonAnchor); + }, + + openUI: function(aEvent) + { + this.clearFinished(); + + switch(this._service.downloadButtonAction) + { + case 1: // Firefox Default + this._handler.openUINative(); + break; + case 2: // Show Library + this._window.PlacesCommandHook.showPlacesOrganizer("Downloads"); + break; + case 3: // Show Tab + let found = this._gBrowser.browsers.some(function(browser, index) + { + if("about:downloads" == browser.currentURI.spec) + { + this._gBrowser.selectedTab = this._gBrowser.tabContainer.childNodes[index]; + return true; + } + }, this); + + if(!found) + { + this._window.openUILinkIn("about:downloads", "tab"); + } + break; + case 4: // External Command + let command = this._service.downloadButtonActionCommand; + if(commend) + { + this._window.goDoCommand(command); + } + break; + default: // Nothing + break; + } + + aEvent.stopPropagation(); + }, + + get isPrivateWindow() + { + return this._handler.hasPBAPI && PrivateBrowsingUtils.isWindowPrivate(this._window); + }, + + get isUIShowing() + { + switch(this._service.downloadButtonAction) + { + case 1: // Firefox Default + return this._handler.isUIShowingNative; + case 2: // Show Library + var organizer = Services.wm.getMostRecentWindow("Places:Organizer"); + if(organizer) + { + let selectedNode = organizer.PlacesOrganizer._places.selectedNode; + let downloadsItemId = organizer.PlacesUIUtils.leftPaneQueries["Downloads"]; + return selectedNode && selectedNode.itemId === downloadsItemId; + } + return false; + case 3: // Show tab + let currentURI = this._gBrowser.currentURI; + return currentURI && currentURI.spec == "about:downloads"; + default: // Nothing + return false; + } + }, + + buildString: function(mode) + { + switch(mode) + { + case 0: + return this._dlCountStr; + case 1: + return ((this._dlPaused) ? this._dlCountStr : this._dlTimeStr); + default: + let compStr = this._dlCountStr; + if(!this._dlPaused) + { + compStr += " (" + this._dlTimeStr + ")"; + } + return compStr; + } + } +}; + +function JSTransferHandler(window, downloadService) +{ + this._window = window; + + let api = CU.import("resource://gre/modules/Downloads.jsm", {}).Downloads; + + this._activePublic = new JSTransferListener(downloadService, api.getList(api.PUBLIC), false); + this._activePrivate = new JSTransferListener(downloadService, api.getList(api.PRIVATE), true); +} + +JSTransferHandler.prototype = +{ + _window: null, + _activePublic: null, + _activePrivate: null, + + destroy: function() + { + this._activePublic.destroy(); + this._activePrivate.destroy(); + + ["_window", "_activePublic", "_activePrivate"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + start: function() + { + this._activePublic.start(); + this._activePrivate.start(); + }, + + stop: function() + { + this._activePublic.stop(); + this._activePrivate.stop(); + }, + + get hasPBAPI() + { + return true; + }, + + openUINative: function() + { + this._window.DownloadsPanel.showPanel(); + }, + + get isUIShowingNative() + { + return this._window.DownloadsPanel.isPanelShowing; + }, + + activeEntries: function() + { + return this._activePublic.downloads(); + }, + + activePrivateEntries: function() + { + return this._activePrivate.downloads(); + } +}; + +function JSTransferListener(downloadService, listPromise, isPrivate) +{ + this._downloadService = downloadService; + this._isPrivate = isPrivate; + this._downloads = new Map(); + + listPromise.then(this.initList.bind(this)).then(null, CU.reportError); +} + +JSTransferListener.prototype = +{ + _downloadService: null, + _list: null, + _downloads: null, + _isPrivate: false, + _wantsStart: false, + + initList: function(list) + { + this._list = list; + if(this._wantsStart) { + this.start(); + } + + this._list.getAll().then(this.initDownloads.bind(this)).then(null, CU.reportError); + }, + + initDownloads: function(downloads) + { + downloads.forEach(function(download) + { + this.onDownloadAdded(download); + }, this); + }, + + destroy: function() + { + this._downloads.clear(); + + ["_downloadService", "_list", "_downloads"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + start: function() + { + if(!this._list) + { + this._wantsStart = true; + return; + } + + this._list.addView(this); + }, + + stop: function() + { + if(!this._list) + { + this._wantsStart = false; + return; + } + + this._list.removeView(this); + }, + + downloads: function() + { + return this._downloads.values(); + }, + + convertToState: function(dl) + { + if(dl.succeeded) + { + return CI.nsIDownloadManager.DOWNLOAD_FINISHED; + } + if(dl.error && dl.error.becauseBlockedByParentalControls) + { + return CI.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL; + } + if(dl.error) + { + return CI.nsIDownloadManager.DOWNLOAD_FAILED; + } + if(dl.canceled && dl.hasPartialData) + { + return CI.nsIDownloadManager.DOWNLOAD_PAUSED; + } + if(dl.canceled) + { + return CI.nsIDownloadManager.DOWNLOAD_CANCELED; + } + if(dl.stopped) + { + return CI.nsIDownloadManager.DOWNLOAD_NOTSTARTED; + } + return CI.nsIDownloadManager.DOWNLOAD_DOWNLOADING; + }, + + onDownloadAdded: function(aDownload) + { + let dl = this._downloads.get(aDownload); + if(!dl) + { + dl = {}; + this._downloads.set(aDownload, dl); + } + + dl.state = this.convertToState(aDownload); + dl.size = aDownload.totalBytes; + dl.speed = aDownload.speed; + dl.transferred = aDownload.currentBytes; + }, + + onDownloadChanged: function(aDownload) + { + this.onDownloadAdded(aDownload); + + if(this._isPrivate != this._downloadService.isPrivateWindow) + { + return; + } + + this._downloadService.updateStatus(aDownload.succeeded); + + if(aDownload.succeeded) + { + this._downloadService.notify() + } + }, + + onDownloadRemoved: function(aDownload) + { + this._downloads.delete(aDownload); + } +}; + diff --git a/browser/components/statusbar/Progress.jsm b/browser/components/statusbar/Progress.jsm new file mode 100644 index 000000000..69d55db49 --- /dev/null +++ b/browser/components/statusbar/Progress.jsm @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["S4EProgressService"]; + +const CI = Components.interfaces; +const CU = Components.utils; + +CU.import("resource://gre/modules/XPCOMUtils.jsm"); + +function S4EProgressService(gBrowser, service, getters, statusService) { + this._gBrowser = gBrowser; + this._service = service; + this._getters = getters; + this._statusService = statusService; + + this._gBrowser.addProgressListener(this); +} + +S4EProgressService.prototype = +{ + _gBrowser: null, + _service: null, + _getters: null, + _statusService: null, + + _busyUI: false, + + set value(val) + { + let toolbar_progress = this._getters.toolbarProgress; + if(toolbar_progress) + { + toolbar_progress.value = val; + } + + let throbber_progress = this._getters.throbberProgress; + if(throbber_progress) + { + if(val) + { + throbber_progress.setAttribute("progress", val); + } + else + { + throbber_progress.removeAttribute("progress"); + } + } + }, + + set collapsed(val) + { + let toolbar_progress = this._getters.toolbarProgress; + if(toolbar_progress) + { + toolbar_progress.collapsed = val; + } + + let throbber_progress = this._getters.throbberProgress; + if(throbber_progress) + { + if(val) + { + throbber_progress.removeAttribute("busy"); + } + else + { + throbber_progress.setAttribute("busy", true); + } + } + }, + + destroy: function() + { + this._gBrowser.removeProgressListener(this); + + ["_gBrowser", "_service", "_getters", "_statusService"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) + { + this._statusService.setNetworkStatus(aMessage, this._busyUI); + }, + + onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) + { + let nsIWPL = CI.nsIWebProgressListener; + + if(!this._busyUI + && aStateFlags & nsIWPL.STATE_START + && aStateFlags & nsIWPL.STATE_IS_NETWORK + && !(aStateFlags & nsIWPL.STATE_RESTORING)) + { + this._busyUI = true; + this.value = 0; + this.collapsed = false; + } + else if(aStateFlags & nsIWPL.STATE_STOP) + { + if(aRequest) + { + let msg = ""; + let location; + if(aRequest instanceof CI.nsIChannel || "URI" in aRequest) + { + location = aRequest.URI; + if(location.spec != "about:blank") + { + switch (aStatus) + { + case Components.results.NS_BINDING_ABORTED: + msg = this._getters.strings.getString("nv_stopped"); + break; + case Components.results.NS_ERROR_NET_TIMEOUT: + msg = this._getters.strings.getString("nv_timeout"); + break; + } + } + } + + if(!msg && (!location || location.spec != "about:blank")) + { + msg = this._getters.strings.getString("nv_done"); + } + + this._statusService.setDefaultStatus(msg); + this._statusService.setNetworkStatus("", this._busyUI); + } + + if(this._busyUI) + { + this._busyUI = false; + this.collapsed = true; + this.value = 0; + } + } + }, + + onUpdateCurrentBrowser: function(aStateFlags, aStatus, aMessage, aTotalProgress) + { + let nsIWPL = CI.nsIWebProgressListener; + let loadingDone = aStateFlags & nsIWPL.STATE_STOP; + + this.onStateChange( + this._gBrowser.webProgress, + { URI: this._gBrowser.currentURI }, + ((loadingDone ? nsIWPL.STATE_STOP : nsIWPL.STATE_START) | (aStateFlags & nsIWPL.STATE_IS_NETWORK)), + aStatus + ); + + if(!loadingDone) + { + this.onProgressChange(this._gBrowser.webProgress, null, 0, 0, aTotalProgress, 1); + this.onStatusChange(this._gBrowser.webProgress, null, 0, aMessage); + } + }, + + onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) + { + if (aMaxTotalProgress > 0 && this._busyUI) + { + // This is highly optimized. Don't touch this code unless + // you are intimately familiar with the cost of setting + // attrs on XUL elements. -- hyatt + let percentage = (aCurTotalProgress * 100) / aMaxTotalProgress; + this.value = percentage; + } + }, + + onProgressChange64: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) + { + return this.onProgressChange(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress); + }, + + QueryInterface: XPCOMUtils.generateQI([ CI.nsIWebProgressListener, CI.nsIWebProgressListener2 ]) +}; + diff --git a/browser/components/statusbar/Status.jsm b/browser/components/statusbar/Status.jsm new file mode 100644 index 000000000..dbdd1fc49 --- /dev/null +++ b/browser/components/statusbar/Status.jsm @@ -0,0 +1,492 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["S4EStatusService"]; + +const CU = Components.utils; + +CU.import("resource://gre/modules/Services.jsm"); +CU.import("resource://gre/modules/XPCOMUtils.jsm"); + +function S4EStatusService(window, service, getters) +{ + this._window = window; + this._service = service; + this._getters = getters; + + this._overLinkService = new S4EOverlinkService(this._window, this._service, this); +} + +S4EStatusService.prototype = +{ + _window: null, + _service: null, + _getters: null, + _overLinkService: null, + + _overLink: { val: "", type: "" }, + _network: { val: "", type: "" }, + _networkXHR: { val: "", type: "" }, + _status: { val: "", type: "" }, + _jsStatus: { val: "", type: "" }, + _defaultStatus: { val: "", type: "" }, + + _isFullScreen: false, + _isVideo: false, + + _statusText: { val: "", type: "" }, + _noUpdate: false, + _statusChromeTimeoutID: 0, + _statusContentTimeoutID: 0, + + getCompositeStatusText: function() + { + return this._statusText.val; + }, + + getStatusText: function() + { + return this._status.val; + }, + + setNetworkStatus: function(status, busy) + { + if(busy) + { + this._network = { val: status, type: "network" }; + this._networkXHR = { val: "", type: "network xhr" }; + } + else + { + this._networkXHR = { val: status, type: "network xhr" }; + } + this.updateStatusField(); + }, + + setStatusText: function(status) + { + this._status = { val: status, type: "status chrome" }; + this.updateStatusField(); + }, + + setJSStatus: function(status) + { + this._jsStatus = { val: status, type: "status content" }; + this.updateStatusField(); + }, + + setJSDefaultStatus: function(status) + { + // This was removed from Firefox in Bug 862917 + }, + + setDefaultStatus: function(status) + { + this._defaultStatus = { val: status, type: "status chrome default" }; + this.updateStatusField(); + }, + + setOverLink: function(link, aAnchor) + { + this._overLinkService.update(link, aAnchor); + }, + + setOverLinkInternal: function(link, aAnchor) + { + let status = this._service.status; + let statusLinkOver = this._service.statusLinkOver; + + if(statusLinkOver) + { + link = link.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g, encodeURIComponent); + if(status == statusLinkOver) + { + this._overLink = { val: link, type: "overLink", anchor: aAnchor }; + this.updateStatusField(); + } + else + { + this.setStatusField(statusLinkOver, { val: link, type: "overLink", anchor: aAnchor }, true); + } + } + }, + + setNoUpdate: function(nu) + { + this._noUpdate = nu; + }, + + buildBinding: function() { + + // Object.prototype.watch() shim, based on Eli Grey's polyfill + // object.watch + if (!this._window.XULBrowserWindow.watch) { + Object.defineProperty(this._window.XULBrowserWindow, "watch", { + enumerable: false, + configurable: true, + writable: false, + value: function (prop, handler) { + var oldval = this[prop], + newval = oldval, + getter = function () { + return newval; + }, + setter = function (val) { + oldval = newval; + return newval = handler.call(this, prop, oldval, val); + } + ; + + try { + if (delete this[prop]) { // can't watch constants + Object.defineProperty(this, prop, { + get: getter, + set: setter, + enumerable: true, + configurable: true + }); + } + } catch(e) { + // This fails fatally on non-configurable props, so just + // ignore errors if it does. + } + } + }); + } + + // object.unwatch + if (!this._window.XULBrowserWindow.unwatch) { + Object.defineProperty(this._window.XULBrowserWindow, "unwatch", { + enumerable: false, + configurable: true, + writable: false, + value: function (prop) { + var val = this[prop]; + delete this[prop]; // remove accessors + this[prop] = val; + } + }); + } + + let XULBWPropHandler = function(prop, oldval, newval) { + CU.reportError("Attempt to modify XULBrowserWindow." + prop); + return oldval; + }; + + ["updateStatusField", "onStatusChange"].forEach(function(prop) + { + this._window.XULBrowserWindow.unwatch(prop); + this._window.XULBrowserWindow[prop] = function() {}; + this._window.XULBrowserWindow.watch(prop, XULBWPropHandler); + }, this); + + ["getCompositeStatusText", "getStatusText", "setStatusText", "setJSStatus", + "setJSDefaultStatus", "setDefaultStatus", "setOverLink"].forEach(function(prop) + { + this._window.XULBrowserWindow.unwatch(prop); + this._window.XULBrowserWindow[prop] = this[prop].bind(this); + this._window.XULBrowserWindow.watch(prop, XULBWPropHandler); + }, this); + }, + + destroy: function() + { + // No need to unbind from the XULBrowserWindow, it's already null at this point + + this.clearTimer("_statusChromeTimeoutID"); + this.clearTimer("_statusContentTimeoutID"); + + this._overLinkService.destroy(); + + ["_overLink", "_network", "_networkXHR", "_status", "_jsStatus", "_defaultStatus", + "_statusText", "_window", "_service", "_getters", "_overLinkService"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + buildTextOrder: function() + { + this.__defineGetter__("_textOrder", function() + { + let textOrder = ["_overLink"]; + if(this._service.statusNetwork) + { + textOrder.push("_network"); + if(this._service.statusNetworkXHR) + { + textOrder.push("_networkXHR"); + } + } + textOrder.push("_status", "_jsStatus"); + if(this._service.statusDefault) + { + textOrder.push("_defaultStatus"); + } + + delete this._textOrder; + return this._textOrder = textOrder; + }); + }, + + updateStatusField: function(force) + { + let text = { val: "", type: "" }; + for(let i = 0; !text.val && i < this._textOrder.length; i++) + { + text = this[this._textOrder[i]]; + } + + if(this._statusText.val != text.val || force) + { + if(this._noUpdate) + { + return; + } + + this._statusText = text; + + this.setStatusField(this._service.status, text, false); + + if(text.val && this._service.statusTimeout) + { + this.setTimer(text.type); + } + } + }, + + setFullScreenState: function(isFullScreen, isVideo) + { + this._isFullScreen = isFullScreen; + this._isVideo = isFullScreen && isVideo; + + this.clearStatusField(); + this.updateStatusField(true); + }, + + setTimer: function(type) + { + let typeArgs = type.split(" ", 3); + + if(typeArgs.length < 2 || typeArgs[0] != "status") + { + return; + } + + if(typeArgs[1] == "chrome") + { + this.clearTimer("_statusChromeTimeoutID"); + this._statusChromeTimeoutID = this._window.setTimeout(function(self, isDefault) + { + self._statusChromeTimeoutID = 0; + if(isDefault) + { + self.setDefaultStatus(""); + } + else + { + self.setStatusText(""); + } + }, this._service.statusTimeout, this, (typeArgs.length == 3 && typeArgs[2] == "default")); + } + else + { + this.clearTimer("_statusContentTimeoutID"); + this._statusContentTimeoutID = this._window.setTimeout(function(self) + { + self._statusContentTimeoutID = 0; + self.setJSStatus(""); + }, this._service.statusTimeout, this); + } + }, + + clearTimer: function(timerName) + { + if(this[timerName] != 0) + { + this._window.clearTimeout(this[timerName]); + this[timerName] = 0; + } + }, + + clearStatusField: function() + { + this._getters.statusOverlay.value = ""; + + let status_label = this._getters.statusWidgetLabel; + if(status_label) + { + status_label.value = ""; + } + + }, + + setStatusField: function(location, text, allowTooltip) + { + if(!location) + { + return; + } + + let label = null; + + if(this._isFullScreen) + { + switch(location) + { + case 1: // Toolbar + location = 3 + break; + case 2: // URL bar + if(Services.prefs.getBoolPref("browser.fullscreen.autohide")) + { + location = 3 + } + break; + } + } + + switch(location) + { + case 1: // Toolbar + label = this._getters.statusWidgetLabel; + break; + case 2: // URL Bar + break; + case 3: // Popup + default: + if(this._isVideo) + { + return; + } + label = this._getters.statusOverlay; + break; + } + + if(label) + { + label.setAttribute("previoustype", label.getAttribute("type")); + label.setAttribute("type", text.type); + label.value = text.val; + label.setAttribute("crop", text.type == "overLink" ? "center" : "end"); + } + } +}; + +function S4EOverlinkService(window, service, statusService) { + this._window = window; + this._service = service; + this._statusService = statusService; +} + +S4EOverlinkService.prototype = +{ + _window: null, + _service: null, + _statusService: null, + + _timer: 0, + _currentLink: { link: "", anchor: null }, + _pendingLink: { link: "", anchor: null }, + _listening: false, + + update: function(aLink, aAnchor) + { + this.clearTimer(); + this.stopListen(); + this._pendingLink = { link: aLink, anchor: aAnchor }; + + if(!aLink) + { + if(this._window.XULBrowserWindow.hideOverLinkImmediately || !this._service.statusLinkOverDelayHide) + { + this._show(); + } + else + { + this._showDelayed(); + } + } + else if(this._currentLink.link || !this._service.statusLinkOverDelayShow) + { + this._show(); + } + else + { + this._showDelayed(); + this.startListen(); + } + }, + + destroy: function() + { + this.clearTimer(); + this.stopListen(); + + ["_currentLink", "_pendingLink", "_statusService", "_window"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + startListen: function() + { + if(!this._listening) + { + this._window.addEventListener("mousemove", this, true); + this._listening = true; + } + }, + + stopListen: function() + { + if(this._listening) + { + this._window.removeEventListener("mousemove", this, true); + this._listening = false; + } + }, + + clearTimer: function() + { + if(this._timer != 0) + { + this._window.clearTimeout(this._timer); + this._timer = 0; + } + }, + + handleEvent: function(event) + { + switch(event.type) + { + case "mousemove": + this.clearTimer(); + this._showDelayed(); + } + }, + + _showDelayed: function() + { + let delay = ((this._pendingLink.link) + ? this._service.statusLinkOverDelayShow + : this._service.statusLinkOverDelayHide); + + this._timer = this._window.setTimeout(function(self) + { + self._timer = 0; + self._show(); + self.stopListen(); + }, delay, this); + }, + + _show: function() + { + this._currentLink = this._pendingLink; + this._statusService.setOverLinkInternal(this._currentLink.link, this._currentLink.anchor); + } +}; + diff --git a/browser/components/statusbar/Status4Evar.jsm b/browser/components/statusbar/Status4Evar.jsm new file mode 100644 index 000000000..6400f2e2a --- /dev/null +++ b/browser/components/statusbar/Status4Evar.jsm @@ -0,0 +1,312 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["Status4Evar"]; + +const CC = Components.classes; +const CI = Components.interfaces; +const CU = Components.utils; + +const s4e_service = CC["@caligonstudios.com/status4evar;1"].getService(CI.nsIStatus4Evar); + +CU.import("resource://gre/modules/Services.jsm"); +CU.import("resource://gre/modules/XPCOMUtils.jsm"); +CU.import("resource://gre/modules/AddonManager.jsm"); + +CU.import("resource:///modules/statusbar/Status.jsm"); +CU.import("resource:///modules/statusbar/Progress.jsm"); +CU.import("resource:///modules/statusbar/Downloads.jsm"); +CU.import("resource:///modules/statusbar/Toolbars.jsm"); + +function Status4Evar(window, gBrowser, toolbox) +{ + this._window = window; + this._toolbox = toolbox; + + this.getters = new S4EWindowGetters(this._window); + this.toolbars = new S4EToolbars(this._window, gBrowser, this._toolbox, s4e_service, this.getters); + this.statusService = new S4EStatusService(this._window, s4e_service, this.getters); + this.progressMeter = new S4EProgressService(gBrowser, s4e_service, this.getters, this.statusService); + this.downloadStatus = new S4EDownloadService(this._window, gBrowser, s4e_service, this.getters); + this.sizeModeService = new SizeModeService(this._window, gBrowser, this); + + this._window.addEventListener("unload", this, false); +} + +Status4Evar.prototype = +{ + _window: null, + _toolbox: null, + + getters: null, + toolbars: null, + statusService: null, + progressMeter: null, + downloadStatus: null, + sizeModeService: null, + + setup: function() + { + this._toolbox.addEventListener("beforecustomization", this, false); + this._toolbox.addEventListener("aftercustomization", this, false); + + this.toolbars.setup(); + this.updateWindow(); + + // OMFG HAX! If a page is already loading, fake a network start event + if(this._window.XULBrowserWindow._busyUI) + { + let nsIWPL = CI.nsIWebProgressListener; + this.progressMeter.onStateChange(0, null, nsIWPL.STATE_START | nsIWPL.STATE_IS_NETWORK, 0); + } + }, + + destroy: function() + { + this._window.removeEventListener("unload", this, false); + this._toolbox.removeEventListener("aftercustomization", this, false); + this._toolbox.removeEventListener("beforecustomization", this, false); + + this.getters.destroy(); + this.statusService.destroy(); + this.downloadStatus.destroy(); + this.progressMeter.destroy(); + this.toolbars.destroy(); + this.sizeModeService.destroy(); + + ["_window", "_toolbox", "getters", "statusService", "downloadStatus", + "progressMeter", "toolbars", "sizeModeService"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + handleEvent: function(aEvent) + { + switch(aEvent.type) + { + case "unload": + this.destroy(); + break; + case "beforecustomization": + this.beforeCustomization(); + break; + case "aftercustomization": + this.updateWindow(); + break; + } + }, + + beforeCustomization: function() + { + this.toolbars.updateSplitters(false); + this.toolbars.updateWindowGripper(false); + + this.statusService.setNoUpdate(true); + let status_label = this.getters.statusWidgetLabel; + if(status_label) + { + status_label.value = this.getters.strings.getString("statusText"); + } + + this.downloadStatus.customizing(true); + }, + + updateWindow: function() + { + this.statusService.setNoUpdate(false); + this.getters.resetGetters(); + this.statusService.buildTextOrder(); + this.statusService.buildBinding(); + this.downloadStatus.init(); + this.downloadStatus.customizing(false); + this.toolbars.updateSplitters(true); + + s4e_service.updateWindow(this._window); + // This also handles the following: + // * buildTextOrder() + // * updateStatusField(true) + // * updateWindowGripper(true) + }, + + launchOptions: function(currentWindow) + { + let optionsURL = "chrome://browser/content/statusbar/prefs.xul"; + let windows = Services.wm.getEnumerator(null); + while (windows.hasMoreElements()) + { + let win = windows.getNext(); + if (win.document.documentURI == optionsURL) + { + win.focus(); + return; + } + } + + let features = "chrome,titlebar,toolbar,centerscreen"; + try + { + let instantApply = Services.prefs.getBoolPref("browser.preferences.instantApply"); + features += instantApply ? ",dialog=no" : ",modal"; + } + catch(e) + { + features += ",modal"; + } + currentWindow.openDialog(optionsURL, "", features); + } + +}; + +function S4EWindowGetters(window) +{ + this._window = window; +} + +S4EWindowGetters.prototype = +{ + _window: null, + _getterMap: + [ + ["addonbar", "addon-bar"], + ["addonbarCloseButton", "addonbar-closebutton"], + ["browserBottomBox", "browser-bottombox"], + ["downloadButton", "status4evar-download-button"], + ["downloadButtonTooltip", "status4evar-download-tooltip"], + ["downloadButtonProgress", "status4evar-download-progress-bar"], + ["downloadButtonLabel", "status4evar-download-label"], + ["downloadButtonAnchor", "status4evar-download-anchor"], + ["downloadNotifyAnchor", "status4evar-download-notification-anchor"], + ["statusBar", "status4evar-status-bar"], + ["statusWidget", "status4evar-status-widget"], + ["statusWidgetLabel", "status4evar-status-text"], + ["strings", "bundle_status4evar"], + ["throbberProgress", "status4evar-throbber-widget"], + ["toolbarProgress", "status4evar-progress-bar"] + ], + + resetGetters: function() + { + let document = this._window.document; + + this._getterMap.forEach(function(getter) + { + let [prop, id] = getter; + delete this[prop]; + this.__defineGetter__(prop, function() + { + delete this[prop]; + return this[prop] = document.getElementById(id); + }); + }, this); + + delete this.statusOverlay; + this.__defineGetter__("statusOverlay", function() + { + let so = this._window.XULBrowserWindow.statusTextField; + if(!so) + { + return null; + } + + delete this.statusOverlay; + return this.statusOverlay = so; + }); + }, + + destroy: function() + { + this._getterMap.forEach(function(getter) + { + let [prop, id] = getter; + delete this[prop]; + }, this); + + ["statusOverlay", "statusOverlay", "_window"].forEach(function(prop) + { + delete this[prop]; + }, this); + } +}; + +function SizeModeService(window, gBrowser, s4e) +{ + this._window = window; + this._gBrowser = gBrowser; + this._s4e = s4e; + this._mm = this._window.messageManager; + + this.lastFullScreen = this._window.fullScreen; + this.lastwindowState = this._window.windowState; + + if(s4e_service.advancedStatusDetectFullScreen) + { + this._mm.addMessageListener("status4evar@caligonstudios.com:video-detect-answer", this) + this._mm.loadFrameScript("resource:///modules/statusbar/content-thunk.js", true); + } + + this._window.addEventListener("sizemodechange", this, false); +} + +SizeModeService.prototype = +{ + _window: null, + _gBrowser: null, + _s4e: null, + _mm: null, + + lastFullScreen: null, + lastwindowState: null, + + destroy: function() + { + this._window.removeEventListener("sizemodechange", this, false); + + if(s4e_service.advancedStatusDetectFullScreen) + { + this._mm.removeDelayedFrameScript("resource:///modules/statusbar/content-thunk.js"); + this._mm.removeMessageListener("status4evar@caligonstudios.com:video-detect-answer", this); + } + + ["_window", "_gBrowser", "_s4e", "_mm"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + handleEvent: function(e) + { + if(this._window.fullScreen != this.lastFullScreen && s4e_service.advancedStatusDetectFullScreen) + { + this.lastFullScreen = this._window.fullScreen; + + if(this.lastFullScreen && s4e_service.advancedStatusDetectVideo) + { + this._gBrowser.selectedBrowser.messageManager.sendAsyncMessage("status4evar@caligonstudios.com:video-detect"); + } + else + { + this._s4e.statusService.setFullScreenState(this.lastFullScreen, false); + } + } + + if(this._window.windowState != this.lastwindowState) + { + this.lastwindowState = this._window.windowState; + this._s4e.toolbars.updateWindowGripper(true); + } + }, + + receiveMessage: function(message) + { + if(message.name == "status4evar@caligonstudios.com:video-detect-answer") + { + this._s4e.statusService.setFullScreenState(this.lastFullScreen, message.data.isVideo); + } + }, + + QueryInterface: XPCOMUtils.generateQI([ CI.nsIDOMEventListener, CI.nsIMessageListener ]) +}; diff --git a/browser/components/statusbar/Toolbars.jsm b/browser/components/statusbar/Toolbars.jsm new file mode 100644 index 000000000..321efd092 --- /dev/null +++ b/browser/components/statusbar/Toolbars.jsm @@ -0,0 +1,221 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["S4EToolbars"]; + +const CI = Components.interfaces; +const CU = Components.utils; + +CU.import("resource://gre/modules/Services.jsm"); + +function S4EToolbars(window, gBrowser, toolbox, service, getters) +{ + this._window = window; + this._toolbox = toolbox; + this._service = service; + this._getters = getters; + this._handler = new ClassicS4EToolbars(this._window, this._toolbox); +} + +S4EToolbars.prototype = +{ + _window: null, + _toolbox: null, + _service: null, + _getters: null, + + _handler: null, + + setup: function() + { + this.updateSplitters(false); + this.updateWindowGripper(false); + this._handler.setup(this._service.firstRun); + }, + + destroy: function() + { + this._handler.destroy(); + + ["_window", "_toolbox", "_service", "_getters", "_handler"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + updateSplitters: function(action) + { + let document = this._window.document; + + let splitter_before = document.getElementById("status4evar-status-splitter-before"); + if(splitter_before) + { + splitter_before.parentNode.removeChild(splitter_before); + } + + let splitter_after = document.getElementById("status4evar-status-splitter-after"); + if(splitter_after) + { + splitter_after.parentNode.removeChild(splitter_after); + } + + let status = this._getters.statusWidget; + if(!action || !status) + { + return; + } + + let urlbar = document.getElementById("urlbar-container"); + let stop = document.getElementById("stop-button"); + let fullscreenflex = document.getElementById("fullscreenflex"); + + let nextSibling = status.nextSibling; + let previousSibling = status.previousSibling; + + function getSplitter(splitter, suffix) + { + if(!splitter) + { + splitter = document.createElement("splitter"); + splitter.id = "status4evar-status-splitter-" + suffix; + splitter.setAttribute("resizebefore", "flex"); + splitter.setAttribute("resizeafter", "flex"); + splitter.className = "chromeclass-toolbar-additional status4evar-status-splitter"; + } + return splitter; + } + + if((previousSibling && previousSibling.flex > 0) + || (urlbar && stop && urlbar.getAttribute("combined") && stop == previousSibling)) + { + status.parentNode.insertBefore(getSplitter(splitter_before, "before"), status); + } + + if(nextSibling && nextSibling.flex > 0 && nextSibling != fullscreenflex) + { + status.parentNode.insertBefore(getSplitter(splitter_after, "after"), nextSibling); + } + }, + + updateWindowGripper: function(action) + { + let document = this._window.document; + + let gripper = document.getElementById("status4evar-window-gripper"); + let toolbar = this._getters.statusBar || this._getters.addonbar; + + if(!action || !toolbar || !this._service.addonbarWindowGripper + || this._window.windowState != CI.nsIDOMChromeWindow.STATE_NORMAL || toolbar.toolbox.customizing) + { + if(gripper) + { + gripper.parentNode.removeChild(gripper); + } + return; + } + + gripper = this._handler.buildGripper(toolbar, gripper, "status4evar-window-gripper"); + + toolbar.appendChild(gripper); + } +}; + +function ClassicS4EToolbars(window, toolbox) +{ + this._window = window; + this._toolbox = toolbox; +} + +ClassicS4EToolbars.prototype = +{ + _window: null, + _toolbox: null, + + setup: function(firstRun) + { + let document = this._window.document; + + let addon_bar = document.getElementById("addon-bar"); + if(addon_bar) + { + let baseSet = "addonbar-closebutton" + + ",status4evar-status-widget" + + ",status4evar-progress-widget"; + + // Update the defaultSet + let defaultSet = baseSet; + let defaultSetIgnore = ["addonbar-closebutton", "spring", "status-bar"]; + addon_bar.getAttribute("defaultset").split(",").forEach(function(item) + { + if(defaultSetIgnore.indexOf(item) == -1) + { + defaultSet += "," + item; + } + }); + defaultSet += ",status-bar" + addon_bar.setAttribute("defaultset", defaultSet); + + // Update the currentSet + if(firstRun) + { + let isCustomizableToolbar = function(aElt) + { + return aElt.localName == "toolbar" && aElt.getAttribute("customizable") == "true"; + } + + let isCustomizedAlready = false; + let toolbars = Array.filter(this._toolbox.childNodes, isCustomizableToolbar).concat( + Array.filter(this._toolbox.externalToolbars, isCustomizableToolbar)); + toolbars.forEach(function(toolbar) + { + if(toolbar.currentSet.indexOf("status4evar") > -1) + { + isCustomizedAlready = true; + } + }); + + if(!isCustomizedAlready) + { + let currentSet = baseSet; + let currentSetIgnore = ["addonbar-closebutton", "spring"]; + addon_bar.currentSet.split(",").forEach(function(item) + { + if(currentSetIgnore.indexOf(item) == -1) + { + currentSet += "," + item; + } + }); + addon_bar.currentSet = currentSet; + addon_bar.setAttribute("currentset", currentSet); + document.persist(addon_bar.id, "currentset"); + this._window.setToolbarVisibility(addon_bar, true); + } + } + } + }, + + destroy: function() + { + ["_window", "_toolbox"].forEach(function(prop) + { + delete this[prop]; + }, this); + }, + + buildGripper: function(toolbar, gripper, id) + { + if(!gripper) + { + let document = this._window.document; + + gripper = document.createElement("resizer"); + gripper.id = id; + gripper.dir = "bottomend"; + } + + return gripper; + } +}; diff --git a/browser/components/statusbar/content-thunk.js b/browser/components/statusbar/content-thunk.js new file mode 100644 index 000000000..fe1fbabad --- /dev/null +++ b/browser/components/statusbar/content-thunk.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleVideoDetect(message) +{ + let isVideo = false; + + let fsEl = content.document.mozFullScreenElement; + if(fsEl) + { + isVideo = ( + fsEl.nodeName == "VIDEO" + || (fsEl.nodeName == "IFRAME" && fsEl.contentDocument && fsEl.contentDocument.getElementsByTagName("VIDEO").length > 0) + || fsEl.getElementsByTagName("VIDEO").length > 0 + ); + } + + sendAsyncMessage("status4evar@caligonstudios.com:video-detect-answer", {isVideo: isVideo}); +} + +addMessageListener("status4evar@caligonstudios.com:video-detect", handleVideoDetect); + diff --git a/browser/components/statusbar/content/overlay.css b/browser/components/statusbar/content/overlay.css new file mode 100644 index 000000000..fd3452119 --- /dev/null +++ b/browser/components/statusbar/content/overlay.css @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +/* + * Status Popup + */ + +statuspanel { + -moz-binding: url("chrome://browser/content/statusbar/tabbrowser.xml#statuspanel"); +} + diff --git a/browser/components/statusbar/content/overlay.js b/browser/components/statusbar/content/overlay.js new file mode 100644 index 000000000..b868aaf0e --- /dev/null +++ b/browser/components/statusbar/content/overlay.js @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +if(!caligon) var caligon = {}; + +window.addEventListener("load", function buildS4E() +{ + window.removeEventListener("load", buildS4E, false); + + Components.utils.import("resource:///modules/statusbar/Status4Evar.jsm"); + + caligon.status4evar = new Status4Evar(window, gBrowser, gNavToolbox); + caligon.status4evar.setup(); +}, false); + diff --git a/browser/components/statusbar/content/overlay.xul b/browser/components/statusbar/content/overlay.xul new file mode 100644 index 000000000..b9934ee65 --- /dev/null +++ b/browser/components/statusbar/content/overlay.xul @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay SYSTEM "chrome://browser/locale/statusbar/statusbar-overlay.dtd"> + +<?xml-stylesheet href="chrome://browser/content/statusbar/overlay.css" type="text/css" ?> +<?xml-stylesheet href="chrome://browser/skin/statusbar/overlay.css" type="text/css" ?> +<?xml-stylesheet href="chrome://browser/skin/statusbar/dynamic.css" type="text/css" ?> + +<overlay id="status4evar-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_status4evar" src="chrome://browser/locale/statusbar/overlay.properties" /> + </stringbundleset> + + <script type="application/javascript" src="chrome://browser/content/statusbar/overlay.js" /> + + <commandset> + <command id="S4E:Options" oncommand="caligon.status4evar.launchOptions(window);"/> + </commandset> + + <popupset id="mainPopupSet"> + <hbox id="status4evar-download-notification-container" mousethrough="always"> + <vbox id="status4evar-download-notification-anchor"> + <vbox id="status4evar-download-notification-icon" /> + </vbox> + </hbox> + </popupset> + + <menupopup id="menu_ToolsPopup"> + <menuitem id="statusbar-options-fx" command="S4E:Options" + label="&status4evar.menu.options.label;"/> + </menupopup> + + <menupopup id="appmenu_customizeMenu"> + <menuitem id="statusbar-options-app" command="S4E:Options" + label="&status4evar.menu.options.label;"/> + </menupopup> + + <toolbarpalette id="BrowserToolbarPalette"> + <toolbaritem id="status4evar-status-widget" + title="&status4evar.status.widget.title;" + removable="true" flex="1" persist="width" width="100"> + <label id="status4evar-status-text" flex="1" crop="end" value="&status4evar.status.widget.title;" /> + </toolbaritem> + + <toolbarbutton id="status4evar-download-button" + title="&status4evar.download.widget.title;" + class="toolbarbutton-1 chromeclass-toolbar-additional" + removable="true" collapsed="true" tooltip="_child" + oncommand="caligon.status4evar.downloadStatus.openUI(event)"> + <stack id="status4evar-download-anchor" class="toolbarbutton-icon"> + <vbox id="status4evar-download-icon" /> + <vbox pack="end"> + <progressmeter id="status4evar-download-progress-bar" mode="normal" value="0" collapsed="true" min="0" max="100" /> + </vbox> + </stack> + <tooltip id="status4evar-download-tooltip" /> + <label id="status4evar-download-label" value="&status4evar.download.widget.title;" class="toolbarbutton-text" crop="right" flex="1" /> + </toolbarbutton> + + <toolbaritem id="status4evar-progress-widget" + title="&status4evar.progress.widget.title;" + removable="true"> + <progressmeter id="status4evar-progress-bar" class="progressmeter-statusbar" + mode="normal" value="0" collapsed="true" min="0" max="100" /> + </toolbaritem> + + <toolbarbutton id="status4evar-options-button" + title="&status4evar.options.widget.title;" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&status4evar.options.widget.label;" + removable="true" command="S4E:Options" tooltiptext="&status4evar.options.widget.title;" /> + </toolbarpalette> + + <statusbar id="status-bar" ordinal="1" /> +</overlay> + diff --git a/browser/components/statusbar/content/prefs.css b/browser/components/statusbar/content/prefs.css new file mode 100644 index 000000000..bafaa6129 --- /dev/null +++ b/browser/components/statusbar/content/prefs.css @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +.css-bg-editor { + -moz-binding: url("chrome://browser/content/statusbar/prefs.xml#css-bg-editor"); +} + diff --git a/browser/components/statusbar/content/prefs.js b/browser/components/statusbar/content/prefs.js new file mode 100644 index 000000000..47fd4b63d --- /dev/null +++ b/browser/components/statusbar/content/prefs.js @@ -0,0 +1,274 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); + +var status4evarPrefs = +{ + get dynamicProgressStyle() + { + let styleSheets = window.document.styleSheets; + for(let i = 0; i < styleSheets.length; i++) + { + let styleSheet = styleSheets[i]; + if(styleSheet.href == "chrome://browser/skin/statusbar/dynamic.css") + { + delete this.dynamicProgressStyle; + return this.dynamicProgressStyle = styleSheet; + } + } + + return null; + }, + +// +// Status timeout management +// + get statusTimeoutPref() + { + delete this.statusTimeoutPref; + return this.statusTimeoutPref = document.getElementById("status4evar-pref-status-timeout"); + }, + + get statusTimeoutCheckbox() + { + delete this.statusTimeoutCheckbox; + return this.statusTimeoutCheckbox = document.getElementById("status4evar-status-timeout-check"); + }, + + statusTimeoutChanged: function() + { + if(this.statusTimeoutPref.value > 0) + { + this.statusTimeoutPref.disabled = false; + this.statusTimeoutCheckbox.checked = true; + } + else + { + this.statusTimeoutPref.disabled = true; + this.statusTimeoutCheckbox.checked = false; + } + }, + + statusTimeoutSync: function() + { + this.statusTimeoutChanged(); + return undefined; + }, + + statusTimeoutToggle: function() + { + if(this.statusTimeoutPref.disabled == this.statusTimeoutCheckbox.checked) + { + if(this.statusTimeoutCheckbox.checked) + { + this.statusTimeoutPref.value = 10; + } + else + { + this.statusTimeoutPref.value = 0; + } + } + }, + +// +// Status network management +// + get statusNetworkPref() + { + delete this.statusNetworkPref; + return this.statusNetworkPref = document.getElementById("status4evar-pref-status-network"); + }, + + get statusNetworkXHRPref() + { + delete this.statusNetworkXHRPref; + return this.statusNetworkXHRPref = document.getElementById("status4evar-pref-status-network-xhr"); + }, + + statusNetworkChanged: function() + { + this.statusNetworkXHRPref.disabled = ! this.statusNetworkPref.value; + }, + + statusNetworkSync: function() + { + this.statusNetworkChanged(); + return undefined; + }, + +// +// Status Text langth managment +// + get textMaxLengthPref() + { + delete this.textMaxLengthPref; + return this.textMaxLengthPref = document.getElementById("status4evar-pref-status-toolbar-maxLength"); + }, + + get textMaxLengthCheckbox() + { + delete this.textMaxLengthCheckbox; + return this.textMaxLengthCheckbox = document.getElementById("status4evar-status-toolbar-maxLength-check"); + }, + + textLengthChanged: function() + { + if(this.textMaxLengthPref.value > 0) + { + this.textMaxLengthPref.disabled = false; + this.textMaxLengthCheckbox.checked = true; + } + else + { + this.textMaxLengthPref.disabled = true; + this.textMaxLengthCheckbox.checked = false; + } + }, + + textLengthSync: function() + { + this.textLengthChanged(); + return undefined; + }, + + textLengthToggle: function() + { + if(this.textMaxLengthPref.disabled == this.textMaxLengthCheckbox.checked) + { + if(this.textMaxLengthCheckbox.checked) + { + this.textMaxLengthPref.value = 800; + } + else + { + this.textMaxLengthPref.value = 0; + } + } + }, + +// +// Toolbar progress style management +// + get progressToolbarStylePref() + { + delete this.progressToolbarStylePref; + return this.progressToolbarStylePref = document.getElementById("status4evar-pref-progress-toolbar-style"); + }, + + get progressToolbarCSSPref() + { + delete this.progressToolbarCSSPref; + return this.progressToolbarCSSPref = document.getElementById("status4evar-pref-progress-toolbar-css"); + }, + + get progressToolbarProgress() + { + delete this.progressToolbarProgress; + return this.progressToolbarProgress = document.getElementById("status4evar-progress-bar"); + }, + + progressToolbarCSSChanged: function() + { + if(!this.progressToolbarCSSPref.value) + { + this.progressToolbarCSSPref.value = "#33FF33"; + } + this.dynamicProgressStyle.cssRules[1].style.background = this.progressToolbarCSSPref.value; + }, + + progressToolbarStyleChanged: function() + { + this.progressToolbarCSSChanged(); + this.progressToolbarCSSPref.disabled = !this.progressToolbarStylePref.value; + if(this.progressToolbarStylePref.value) + { + this.progressToolbarProgress.setAttribute("s4estyle", true); + } + else + { + this.progressToolbarProgress.removeAttribute("s4estyle"); + } + }, + + progressToolbarStyleSync: function() + { + this.progressToolbarStyleChanged(); + return undefined; + }, + +// +// Download progress management +// + get downloadProgressCheck() + { + delete this.downloadProgressCheck; + return this.downloadProgressCheck = document.getElementById("status4evar-download-progress-check"); + }, + + get downloadProgressPref() + { + delete this.downloadProgressPref; + return this.downloadProgressPref = document.getElementById("status4evar-pref-download-progress"); + }, + + get downloadProgressColorActivePref() + { + delete this.downloadProgressActiveColorPref; + return this.downloadProgressActiveColorPref = document.getElementById("status4evar-pref-download-color-active"); + }, + + get downloadProgressColorPausedPref() + { + delete this.downloadProgressPausedColorPref; + return this.downloadProgressPausedColorPref = document.getElementById("status4evar-pref-download-color-paused"); + }, + + downloadProgressSync: function() + { + let val = this.downloadProgressPref.value; + this.downloadProgressColorActivePref.disabled = (val == 0); + this.downloadProgressColorPausedPref.disabled = (val == 0); + this.downloadProgressPref.disabled = (val == 0); + this.downloadProgressCheck.checked = (val != 0); + return ((val == 0) ? 1 : val); + }, + + downloadProgressToggle: function() + { + let enabled = this.downloadProgressCheck.checked; + this.downloadProgressPref.value = ((enabled) ? 1 : 0); + }, + +// +// Pref Window load +// + get downloadButtonActionCommandPref() + { + delete this.downloadButtonActionCommandPref; + return this.downloadButtonActionCommandPref = document.getElementById("status4evar-pref-download-button-action-command"); + }, + + get downloadButtonActionThirdPartyItem() + { + delete this.downloadButtonActionThirdPartyItem; + return this.downloadButtonActionThirdPartyItem = document.getElementById("status4evar-download-button-action-menu-thirdparty"); + }, + + onPrefWindowLoad: function() + { + if(!this.downloadButtonActionCommandPref.value) + { + this.downloadButtonActionThirdPartyItem.disabled = true; + } + }, + + onPrefWindowUnLoad: function() + { + } +} + +var XULBrowserWindow = { +} + diff --git a/browser/components/statusbar/content/prefs.xml b/browser/components/statusbar/content/prefs.xml new file mode 100644 index 000000000..44baab18d --- /dev/null +++ b/browser/components/statusbar/content/prefs.xml @@ -0,0 +1,704 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings SYSTEM "chrome://browser/locale/statusbar/statusbar-prefs.dtd"> + +<bindings id="status4evar-prefs-bindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="css-bg-editor"> + <content sizetopopup="pref"> + <xul:vbox flex="1"> + <xul:deck anonid="css-bg-editor-deck" flex="1"> + <xul:vbox> + <xul:hbox align="center"> + <xul:label xbl:inherits="disabled">&status4evar.editor.css.color.label;</xul:label> + <xul:colorpicker anonid="css-bg-editor-color" type="button" onchange="this._editor._buildCSS();" xbl:inherits="disabled" /> + </xul:hbox> + + <xul:hbox align="center"> + <xul:label xbl:inherits="disabled">&status4evar.editor.css.image.label;</xul:label> + <xul:textbox anonid="css-bg-editor-image" readonly="true" flex="1" xbl:inherits="disabled" /> + <xul:button anonid="css-bg-editor-image-browse" label="&status4evar.option.browse;" oncommand="this._editor._imageBrowse();" xbl:inherits="disabled" /> + </xul:hbox> + <xul:hbox align="center" pack="end"> + <xul:button anonid="css-bg-editor-image-clear" label="&status4evar.option.clear;" oncommand="this._editor._imageClear();" xbl:inherits="disabled=no-image" /> + </xul:hbox> + + <xul:hbox> + <xul:groupbox pack="center"> + <xul:caption label="" /> + <xul:hbox flex="1" align="center"> + <xul:label>X</xul:label> + </xul:hbox> + <xul:separator class="groove" orient="horizontal" /> + <xul:hbox flex="1" align="center"> + <xul:label>Y</xul:label> + </xul:hbox> + </xul:groupbox> + + <xul:groupbox> + <xul:caption label="&status4evar.editor.css.image.repeat;" xbl:inherits="disabled=no-image" /> + <xul:menulist anonid="css-bg-editor-image-repeat-x" sizetopopup="always" onselect="this._editor._buildCSS();" xbl:inherits="disabled=no-image"> + <xul:menupopup> + <xul:menuitem label="&status4evar.option.no-repeat;" value="no-repeat" /> + <xul:menuitem label="&status4evar.option.repeat;" value="repeat" /> +<!-- + <xul:menuitem label="&status4evar.option.space;" value="space" /> + <xul:menuitem label="&status4evar.option.round;" value="round" /> +--> + </xul:menupopup> + </xul:menulist> + <xul:separator class="groove" orient="horizontal" /> + <xul:menulist anonid="css-bg-editor-image-repeat-y" sizetopopup="always" onselect="this._editor._buildCSS();" xbl:inherits="disabled=no-image"> + <xul:menupopup> + <xul:menuitem label="&status4evar.option.no-repeat;" value="no-repeat" /> + <xul:menuitem label="&status4evar.option.repeat;" value="repeat" /> +<!-- + <xul:menuitem label="&status4evar.option.space;" value="space" /> + <xul:menuitem label="&status4evar.option.round;" value="round" /> +--> + </xul:menupopup> + </xul:menulist> + </xul:groupbox> + + <xul:groupbox> + <xul:caption label="&status4evar.editor.css.image.position;" xbl:inherits="disabled=no-image" /> + <xul:menulist anonid="css-bg-editor-image-position-x" sizetopopup="always" onselect="this._editor._updatePositionX();" xbl:inherits="disabled=no-image"> + <xul:menupopup> + <xul:menuitem label="&status4evar.option.left;" value="left" /> + <xul:menuitem label="&status4evar.option.center;" value="center" /> + <xul:menuitem label="&status4evar.option.right;" value="right" /> + <xul:menuitem label="&status4evar.option.offset;" value="offset" /> + </xul:menupopup> + </xul:menulist> + <xul:separator class="groove" orient="horizontal" /> + <xul:menulist anonid="css-bg-editor-image-position-y" sizetopopup="always" onselect="this._editor._updatePositionY();" xbl:inherits="disabled=no-image"> + <xul:menupopup> + <xul:menuitem label="&status4evar.option.top;" value="top" /> + <xul:menuitem label="&status4evar.option.center;" value="center" /> + <xul:menuitem label="&status4evar.option.bottom;" value="bottom" /> + <xul:menuitem label="&status4evar.option.offset;" value="offset" /> + </xul:menupopup> + </xul:menulist> + </xul:groupbox> + + <xul:groupbox> + <xul:caption label="&status4evar.editor.css.image.offset;" xbl:inherits="disabled=no-image" /> + <xul:hbox> + <xul:textbox anonid="css-bg-editor-image-offset-x" type="number" size="4" min="-65535" onchange="this._editor._buildCSS();" /> + <xul:menulist anonid="css-bg-editor-image-offset-unit-x" sizetopopup="always" onselect="this._editor._buildCSS();"> + <xul:menupopup> + <xul:menuitem label="%" value="%" /> + <xul:menuitem label="px" value="px" /> + <xul:menuitem label="em" value="em" /> + <xul:menuitem label="in" value="in" /> + <xul:menuitem label="cm" value="cm" /> + <xul:menuitem label="mm" value="mm" /> + <xul:menuitem label="pt" value="pt" /> + <xul:menuitem label="pc" value="pc" /> + </xul:menupopup> + </xul:menulist> + </xul:hbox> + <xul:separator class="groove" orient="horizontal" /> + <xul:hbox> + <xul:textbox anonid="css-bg-editor-image-offset-y" type="number" size="4" min="-65535" onchange="this._editor._buildCSS();" /> + <xul:menulist anonid="css-bg-editor-image-offset-unit-y" sizetopopup="always" onselect="this._editor._buildCSS();"> + <xul:menupopup> + <xul:menuitem label="%" value="%" /> + <xul:menuitem label="px" value="px" /> + <xul:menuitem label="em" value="em" /> + <xul:menuitem label="in" value="in" /> + <xul:menuitem label="cm" value="cm" /> + <xul:menuitem label="mm" value="mm" /> + <xul:menuitem label="pt" value="pt" /> + <xul:menuitem label="pc" value="pc" /> + </xul:menupopup> + </xul:menulist> + </xul:hbox> + </xul:groupbox> + </xul:hbox> + </xul:vbox> + + <xul:textbox anonid="css-bg-editor-css-text" multiline="true" rows="6" xbl:inherits="disabled" /> + </xul:deck> + </xul:vbox> + + <xul:hbox align="center" pack="end"> + <children includes="progressmeter|toolbox" /> + <xul:label xbl:inherits="disabled">&status4evar.editor.label;</xul:label> + <xul:menulist anonid="css-bg-editor-mode-menu" sizetopopup="always" onselect="this._editor._updateMode();" xbl:inherits="disabled"> + <xul:menupopup> + <xul:menuitem label="&status4evar.option.simple;" /> + <xul:menuitem label="&status4evar.option.advanced;" /> + </xul:menupopup> + </xul:menulist> + </xul:hbox> + </content> + + <implementation> + <constructor><![CDATA[ + [ + "_editorColor", + "_editorImageBrowse", + "_editorImageClear", + "_editorImageRepeatX", + "_editorImageRepeatY", + "_editorImagePositionX", + "_editorImagePositionY", + "_editorImageOffsetX", + "_editorImageOffsetY", + "_editorImageOffsetUnitX", + "_editorImageOffsetUnitY", + "_editorMode" + ].forEach(function(prop) + { + this[prop]._editor = this; + }, this); + + this.setAdvanced(true, false); + ]]></constructor> + + <destructor><![CDATA[ + ]]></destructor> + + <field name="_disableBuildCSS"><![CDATA[ + true + ]]></field> + + <field name="_editorColor" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-color"); + ]]></field> + + <field name="_editorCSS" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-css-text"); + ]]></field> + + <field name="_editorDeck" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-deck"); + ]]></field> + + <field name="_editorImage" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image"); + ]]></field> + + <field name="_editorImageBrowse" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-browse"); + ]]></field> + + <field name="_editorImageClear" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-clear"); + ]]></field> + + <field name="_editorImageRepeatX" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-repeat-x"); + ]]></field> + + <field name="_editorImageRepeatY" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-repeat-y"); + ]]></field> + + <field name="_editorImagePositionX" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-position-x"); + ]]></field> + + <field name="_editorImagePositionY" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-position-y"); + ]]></field> + + <field name="_editorImageOffsetX" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-offset-x"); + ]]></field> + + <field name="_editorImageOffsetY" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-offset-y"); + ]]></field> + + <field name="_editorImageOffsetUnitX" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-offset-unit-x"); + ]]></field> + + <field name="_editorImageOffsetUnitY" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-image-offset-unit-y"); + ]]></field> + + <field name="_editorMode" readonly="true"><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "css-bg-editor-mode-menu"); + ]]></field> + + <field name="_initialized"><![CDATA[ + false + ]]></field> + + <field name="_reRGB" readonly="true"><![CDATA[ + /^rgb\((\d+), (\d+), (\d+)\)$/ + ]]></field> + + <field name="_reURL" readonly="true"><![CDATA[ + /^url\(\s*['"]?(.+?)['"]?\s*\)$/ + ]]></field> + + <field name="_reBgPosition" readonly="true"><![CDATA[ + /^(left|center|right)? ?(-?\d+[^\s\d]+)? ?(top|center|bottom)? ?(-?\d+[^\s\d]+)?$/ + ]]></field> + + <field name="_reCSSUnit" readonly="true"><![CDATA[ + /^(-?\d+)([^\s\d]+)$/ + ]]></field> + + <field name="_strings" readonly="true"><![CDATA[ + document.getElementById("bundle_status4evar"); + ]]></field> + + <property name="value"> + <getter><![CDATA[ + return this._editorCSS.value; + ]]></getter> + <setter><![CDATA[ + this._editorCSS.value = val; + + if(!this._initialized) + { + this.setAdvanced(false, false); + this._initialized = true; + } + + return val; + ]]></setter> + </property> + + <property name="disabled"> + <getter><![CDATA[ + return this.getAttribute("disabled") == "true"; + ]]></getter> + <setter><![CDATA[ + if(val) + { + this.setAttribute("disabled", "true"); + } + else + { + this.removeAttribute("disabled"); + } + + this._updateImageControllDisable(); + + return val; + ]]></setter> + </property> + + <method name="setAdvanced"> + <parameter name="aVal"/> + <parameter name="aPrompt"/> + <body><![CDATA[ + if(!aVal) + { + let success = this._parseCSS(); + if(!success) + { + let result = aPrompt && Services.prompt.confirm(window, + this._strings.getString("simpleEditorTitle"), + this._strings.getString("simpleEditorMessage")); + if(result) + { // Continue to simple mode + this._buildCSS(); + } + else + { // Stay on advanced mode + aVal = true; + } + } + } + + this._disableBuildCSS = aVal; + this._editorDeck.selectedIndex = ((aVal) ? 1 : 0); + this._editorMode.selectedIndex = ((aVal) ? 1 : 0); + ]]></body> + </method> + + <method name="_buildCSS"> + <body><![CDATA[ + if(this._disableBuildCSS) + { + return; + } + + let cssVal = this._editorColor.color; + let imgVal = this._editorImage.value; + if(imgVal) + { + cssVal += " url(\"" + imgVal + "\")"; + + // + // Print the background repeat + // + let bgRX = this._editorImageRepeatX.value; + let bgRY = this._editorImageRepeatY.value; + if(bgRX == "repeat" && bgRY == "no-repeat") + { + cssVal += " repeat-x"; + } + else if(bgRX == "no-repeat" && bgRY == "repeat") + { + cssVal += " repeat-y"; + } + else + { + cssVal += " " + bgRX; + if(bgRX != bgRY) + { + cssVal += " " + bgRY; + } + } + + // + // Print the background position + // + let bgPX = this._editorImagePositionX.value; + let bgPOX = this._editorImageOffsetX.value; + if(bgPX != "offset") + { + cssVal += " " + bgPX; + } + else + { + cssVal += " " + bgPOX + this._editorImageOffsetUnitX.value; + } + + let bgPY = this._editorImagePositionY.value; + let bgPOY = this._editorImageOffsetY.value; + if(bgPY != "offset") + { + cssVal += " " + bgPY; + } + else + { + cssVal += " " + bgPOY + this._editorImageOffsetUnitY.value; + } + } + + this._editorCSS.value = cssVal; + + let event = document.createEvent("Event"); + event.initEvent("change", true, true); + this._editorCSS.dispatchEvent(event); + ]]></body> + </method> + + <method name="_parseCSS"> + <body><![CDATA[ + let retVal = true; + + let cssParser = document.createElement("div"); + cssParser.style.background = this._editorCSS.value; + if(!cssParser.style.background) + { + Components.utils.reportError("Error parsing background CSS rule: " + this._editorCSS.value); + cssParser.style.background = "#33FF33"; + retVal = false; + } + + // + // Parse the background color + // + let bgC = cssParser.style.backgroundColor; + if(this._reRGB.test(bgC)) + { + let digits = this._reRGB.exec(bgC); + + let red = parseInt(digits[1]); + let green = parseInt(digits[2]); + let blue = parseInt(digits[3]); + + let rgb = blue | (green << 8) | (red << 16); + bgC = "#" + rgb.toString(16); + } + else + { + Components.utils.reportError("Error parsing background-color value: " + bgC); + bgC = "#33FF33"; + retVal = false; + } + + // + // Parse the background image + // + let bgI = cssParser.style.backgroundImage; + if(bgI != "none" && !this._reURL.test(bgI)) + { + Components.utils.reportError("Error parsing background-image value: " + bgI); + bgI = "none"; + retVal = false; + } + bgI = ((bgI != "none") ? this._reURL.exec(bgI)[1].trim() : ""); + + // + // Parse the background repeat + // + let bgR = cssParser.style.backgroundRepeat.split(" "); + let bgRX = bgR[0]; + if(bgRX == "repeat-x") + { + bgRX = "repeat"; + } + else if(bgRX == "repeat-y") + { + bgRX = "no-repeat"; + } + + let bgRY = bgR[bgR.length - 1]; + if(bgRY == "repeat-x") + { + bgRY = "no-repeat"; + } + else if(bgRY == "repeat-y") + { + bgRY = "repeat"; + } + + // + // Parse the background position + // + let bgP = cssParser.style.backgroundPosition; + let bgPParts = this._reBgPosition.exec(bgP); + let bgPValues = new Array(); + for(let i = 1; i <= 4; i++) + { + if(bgPParts[i]) + { + bgPValues.push({ + "value": bgPParts[i], + "group": i + }); + } + } + + if(bgPValues.length == 1) + { + bgPValues.splice(((bgPValues[0].group == 2) ? 0 : 1), 0, { + "value": "center", + "group": ((bgPValues[0].group == 2) ? 0 : 2) + }); + } + + if(bgPValues.length == 2 && bgPValues[1].group == 2) + { + bgPValues[1].group = 4; + } + + for(let i = 0; i < 4; i++) + { + let group = (i + 1); + if(bgPValues[i] != undefined && bgPValues[i].group == group) + { + continue; + } + + let tmp = "0px"; + switch(i) + { + case 0: + tmp = "offset"; + break; + case 2: + tmp = "offset"; + break; + } + + bgPValues.splice(i, 0, { + "value": tmp, + "group": group + }); + } + + let bgPOXParts = this._reCSSUnit.exec(bgPValues[1].value); + let bgPOYParts = this._reCSSUnit.exec(bgPValues[3].value); + + // + // Parse the background size + // + + // + // Initialize the UI + // + let disableBuildCSS = this._disableBuildCSS; + this._disableBuildCSS = true; + + this._editorColor.color = bgC; + this._editorImage.value = bgI; + this._editorImageOffsetX.value = bgPOXParts[1]; + this._editorImageOffsetY.value = bgPOYParts[1]; + + [ + [this._editorImageRepeatX, bgRX, "repeat", "repeat X"], + [this._editorImageRepeatY, bgRY, "repeat", "repeat Y"], + [this._editorImagePositionX, bgPValues[0].value, "left", "position X"], + [this._editorImagePositionY, bgPValues[2].value, "top", "position Y"], + [this._editorImageOffsetUnitX, bgPOXParts[2], "px", "offset X unit"], + [this._editorImageOffsetUnitY, bgPOYParts[2], "px", "offset Y unit"] + ].forEach(function(info) + { + if(!this._setSelectedItemSafe(info[0], info[1], info[2])) + { + Components.utils.reportError("Error setting " + info[3] + " to " + info[1]); + retVal = false; + } + }, this); + + this._updateImageControllDisable(); + + this._disableBuildCSS = disableBuildCSS; + + return retVal; + ]]></body> + </method> + + <method name="_imageBrowse"> + <body><![CDATA[ + let nsIFilePicker = Components.interfaces.nsIFilePicker; + let filePicker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + filePicker.init(window, this._strings.getString("imageSelectTitle"), nsIFilePicker.modeOpen); + filePicker.appendFilters(nsIFilePicker.filterImages); + + let res = filePicker.show(); + if(res == nsIFilePicker.returnOK) + { + this._editorImage.value = Services.io.newFileURI(filePicker.file).spec; + this._updateImageControllDisable(); + this._buildCSS(); + } + ]]></body> + </method> + + <method name="_imageClear"> + <body><![CDATA[ + this._editorImage.value = ""; + this._editorImageRepeatX.value = "repeat"; + this._editorImageRepeatY.value = "repeat"; + this._editorImagePositionX.value = "left"; + this._editorImagePositionY.value = "top"; + this._editorImageOffsetX.value = 0; + this._editorImageOffsetY.value = 0; + this._editorImageOffsetUnitX.value = "px"; + this._editorImageOffsetUnitY.value = "px"; + this._updateImageControllDisable(); + this._buildCSS(); + ]]></body> + </method> + + <method name="_processEvent"> + <parameter name="event"/> + <body><![CDATA[ + if(!("css-bg-editor-css-text" == event.originalTarget.getAttribute("anonid") + || "css-bg-editor-css-text" == document.getBindingParent(event.originalTarget).getAttribute("anonid"))) + { + event.stopPropagation(); + } + + //Components.utils.reportError("Editor event " + event.type + " on " + event.originalTarget.tagName + "::" + event.originalTarget.getAttribute("anonid")); + ]]></body> + </method> + + <method name="_setSelectedItemSafe"> + <parameter name="aElement"/> + <parameter name="aValue"/> + <parameter name="aDefault"/> + <body><![CDATA[ + aElement.value = aValue; + if(!aElement.selectedItem || aElement.selectedItem.value != aValue) + { + aElement.value = aDefault; + return false; + } + return true; + ]]></body> + </method> + + <method name="_updateImageControllDisable"> + <body><![CDATA[ + if(this.disabled || !this._editorImage.value) + { + this.setAttribute("no-image", "true"); + this._updatePositionOffsetXDisabled(true); + this._updatePositionOffsetYDisabled(true); + } + else + { + this.removeAttribute("no-image"); + this._updatePositionOffsetXDisabled(false); + this._updatePositionOffsetYDisabled(false); + } + ]]></body> + </method> + + <method name="_updateMode"> + <body><![CDATA[ + if(this._editorMode.selectedIndex == this._editorDeck.selectedIndex) + { + return; + } + + this.setAdvanced(((this._editorMode.selectedIndex == 1) ? true : false), true); + ]]></body> + </method> + + <method name="_updatePositionOffsetXDisabled"> + <parameter name="aVal"/> + <body><![CDATA[ + let bgPX = this._editorImagePositionX.value; + let disableOffsetX = aVal || (bgPX != "offset");// || bgPX == "center"); + this._editorImageOffsetX.disabled = disableOffsetX; + this._editorImageOffsetUnitX.disabled = disableOffsetX; + ]]></body> + </method> + + <method name="_updatePositionOffsetYDisabled"> + <parameter name="aVal"/> + <body><![CDATA[ + let bgPY = this._editorImagePositionY.value; + var disableOffsetY = aVal || (bgPY != "offset");// || bgPY == "center"); + this._editorImageOffsetY.disabled = disableOffsetY; + this._editorImageOffsetUnitY.disabled = disableOffsetY; + ]]></body> + </method> + + <method name="_updatePositionX"> + <body><![CDATA[ + this._updatePositionOffsetXDisabled(false); + this._buildCSS(); + ]]></body> + </method> + + <method name="_updatePositionY"> + <body><![CDATA[ + this._updatePositionOffsetYDisabled(false); + this._buildCSS(); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="command"><![CDATA[ + this._processEvent(event); + ]]></handler> + + <handler event="change"><![CDATA[ + this._processEvent(event); + ]]></handler> + + <handler event="input"><![CDATA[ + this._processEvent(event); + ]]></handler> + + <handler event="select"><![CDATA[ + this._processEvent(event); + ]]></handler> + </handlers> + </binding> +</bindings> + diff --git a/browser/components/statusbar/content/prefs.xul b/browser/components/statusbar/content/prefs.xul new file mode 100644 index 000000000..dd4158246 --- /dev/null +++ b/browser/components/statusbar/content/prefs.xul @@ -0,0 +1,297 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE prefwindow [ + <!ENTITY % prefsDTD SYSTEM "chrome://browser/locale/statusbar/statusbar-prefs.dtd"> + %prefsDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/config.css" type="text/css" ?> +<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css" ?> + +<?xml-stylesheet href="chrome://browser/content/statusbar/overlay.css" type="text/css" ?> +<?xml-stylesheet href="chrome://browser/skin/statusbar/overlay.css" type="text/css" ?> +<?xml-stylesheet href="chrome://browser/skin/statusbar/dynamic.css" type="text/css" ?> + +<?xml-stylesheet href="chrome://browser/content/statusbar/prefs.css" type="text/css" ?> +<?xml-stylesheet href="chrome://browser/skin/statusbar/prefs.css" type="text/css" ?> + +<prefwindow id="status4evar-prefs" title="&status4evar.window.title;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="status4evarPrefs.onPrefWindowLoad();" onunload="status4evarPrefs.onPrefWindowUnLoad();"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_status4evar" src="chrome://browser/locale/statusbar/prefs.properties" /> + </stringbundleset> + <script type="application/javascript" src="chrome://browser/content/statusbar/prefs.js" /> + + <prefpane id="status4evar-pane-status" label="&status4evar.pane.status;"> + <preferences> + <preference id="status4evar-pref-status" name="status4evar.status" type="int" /> + <preference id="status4evar-pref-status-default" name="status4evar.status.default" type="bool" /> + <preference id="status4evar-pref-status-network" name="status4evar.status.network" type="bool" + onchange="status4evarPrefs.statusNetworkChanged();" /> + <preference id="status4evar-pref-status-network-xhr" name="status4evar.status.network.xhr" type="bool" /> + <preference id="status4evar-pref-status-timeout" name="status4evar.status.timeout" type="int" + onchange="status4evarPrefs.statusTimeoutChanged();" /> + <preference id="status4evar-pref-status-linkOver" name="status4evar.status.linkOver" type="int" /> + <preference id="status4evar-pref-status-linkOver-delay-show" name="status4evar.status.linkOver.delay.show" type="int" /> + <preference id="status4evar-pref-status-linkOver-delay-hide" name="status4evar.status.linkOver.delay.hide" type="int" /> + <preference id="status4evar-pref-status-toolbar-maxLength" name="status4evar.status.toolbar.maxLength" type="int" + onchange="status4evarPrefs.textLengthChanged();" /> + <preference id="status4evar-pref-status-popup-invertMirror" name="status4evar.status.popup.invertMirror" type="bool" /> + <preference id="status4evar-pref-status-popup-mouseMirror" name="status4evar.status.popup.mouseMirror" type="bool" /> + <preference id="toolkit-pref-dom-status-change" name="dom.disable_window_status_change" type="bool" inverted="true" /> + </preferences> + + <commandset id="status4evar-commandset-status"> + <command id="status4evar-command-status-timeout" oncommand="status4evarPrefs.statusTimeoutToggle();" /> + <command id="status4evar-command-status-toolbar-maxLength" oncommand="status4evarPrefs.textLengthToggle();" /> + </commandset> + + <tabbox id="status4evar-tabbox-status" flex="1"> + <tabs id="status4evar-tabs-status"> + <tab id="status4evar-tab-status-general" label="&status4evar.tab.general;" /> + <tab id="status4evar-tab-status-toolbar" label="&status4evar.tab.toolbar;" /> + <tab id="status4evar-tab-status-popup" label="&status4evar.tab.popup;" /> + </tabs> + + <tabpanels id="status4evar-tabpanels-status" flex="1"> + <tabpanel id="status4evar-tabpanel-status-general" orient="vertical"> + <groupbox id="status4evar-status-general-status"> + <caption label="&status4evar.status.general.status.caption;" /> + + <hbox align="center"> + <label id="status4evar-status-label" control="status4evar-status-menu">&status4evar.status.label;</label> + <menulist id="status4evar-status-menu" preference="status4evar-pref-status" sizetopopup="always"> + <menupopup> + <menuitem label="&status4evar.option.none;" value="0" /> + <menuitem label="&status4evar.option.toolbar;" value="1" /> + <menuitem label="&status4evar.option.popup;" value="3" /> + </menupopup> + </menulist> + </hbox> + + <hbox align="center"> + <checkbox id="status4evar-status-timeout-check" label="&status4evar.status.timeout.label;" + command="status4evar-command-status-timeout" /> + <textbox id="status4evar-status-timeout-value" preference="status4evar-pref-status-timeout" type="number" size="4" + onsyncfrompreference="return status4evarPrefs.statusTimeoutSync();" /> + <label id="status4evar-status-timeout-unit">&status4evar.unit.seconds;</label> + </hbox> + + <checkbox id="status4evar-status-default-check" preference="status4evar-pref-status-default" label="&status4evar.status.default.label;" /> + + <checkbox id="status4evar-status-network-check" preference="status4evar-pref-status-network" label="&status4evar.status.network.label;" + onsyncfrompreference="return status4evarPrefs.statusNetworkSync();" /> + + <hbox align="center" class="indent"> + <checkbox id="status4evar-status-network-xhr-check" preference="status4evar-pref-status-network-xhr" label="&status4evar.status.network.xhr.label;" /> + </hbox> + + <checkbox id="toolkit-dom-status-change-check" preference="toolkit-pref-dom-status-change" label="&toolkit.dom.status.change.label;" /> + </groupbox> + + <groupbox id="status4evar-status-general-linkOver"> + <caption label="&status4evar.status.general.linkOver.caption;" /> + + <hbox align="center"> + <label id="status4evar-status-linkOver-label" control="status4evar-status-linkOver-menu">&status4evar.status.linkOver.label;</label> + <menulist id="status4evar-status-linkOver-menu" preference="status4evar-pref-status-linkOver" sizetopopup="always"> + <menupopup> + <menuitem label="&status4evar.option.none;" value="0" /> + <menuitem label="&status4evar.option.toolbar;" value="1" /> + <menuitem label="&status4evar.option.popup;" value="3" /> + </menupopup> + </menulist> + </hbox> + + <hbox align="center"> + <label id="status4evar-status-linkOver-delay-show-label" control="status4evar-status-linkOver-delay-show-value">&status4evar.status.linkOver.delay.show.label;</label> + <textbox id="status4evar-status-linkOver-delay-show-value" preference="status4evar-pref-status-linkOver-delay-show" type="number" size="5" /> + <label id="status4evar-status-linkOver-delay-show-unit">&status4evar.unit.milliseconds;</label> + </hbox> + + <hbox align="center"> + <label id="status4evar-status-linkOver-delay-hide-label" control="status4evar-status-linkOver-delay-hide-value">&status4evar.status.linkOver.delay.hide.label;</label> + <textbox id="status4evar-status-linkOver-delay-hide-value" preference="status4evar-pref-status-linkOver-delay-hide" type="number" size="5" /> + <label id="status4evar-status-linkOver-delay-hide-unit">&status4evar.unit.milliseconds;</label> + </hbox> + </groupbox> + + </tabpanel> + + <tabpanel id="status4evar-tabpanel-status-toolbar" orient="vertical"> + <hbox align="center"> + <checkbox id="status4evar-status-toolbar-maxLength-check" label="&status4evar.status.toolbar.maxLength.label;" + command="status4evar-command-status-toolbar-maxLength" /> + <textbox id="status4evar-status-toolbar-maxLength-value" preference="status4evar-pref-status-toolbar-maxLength" type="number" size="4" + onsyncfrompreference="return status4evarPrefs.textLengthSync();" /> + <label id="status4evar-status-toolbar-maxLength-unit">&status4evar.unit.px;</label> + </hbox> + </tabpanel> + + <tabpanel id="status4evar-tabpanel-status-popup" orient="vertical"> + <checkbox id="status4evar-status-popup-invertMirror-check" preference="status4evar-pref-status-popup-invertMirror" label="&status4evar.status.popup.invertMirror.label;" /> + + <checkbox id="status4evar-status-popup-mouseMirror-check" preference="status4evar-pref-status-popup-mouseMirror" label="&status4evar.status.popup.mouseMirror.label;" /> + </tabpanel> + + </tabpanels> + </tabbox> + </prefpane> + + <prefpane id="status4evar-pane-progress" label="&status4evar.pane.progress;"> + <preferences> + <preference id="status4evar-pref-progress-toolbar-force" name="status4evar.progress.toolbar.force" type="bool" /> + <preference id="status4evar-pref-progress-toolbar-style" name="status4evar.progress.toolbar.style" type="bool" + onchange="status4evarPrefs.progressToolbarStyleChanged();" /> + <preference id="status4evar-pref-progress-toolbar-css" name="status4evar.progress.toolbar.css" type="string" + onchange="status4evarPrefs.progressToolbarCSSChanged();" /> + </preferences> + + <commandset id="status4evar-commandset-status"> + </commandset> + + <checkbox id="status4evar-progress-toolbar-force-check" preference="status4evar-pref-progress-toolbar-force" label="&status4evar.progress.toolbar.force.label;" /> + + <checkbox id="status4evar-progress-toolbar-style-check" preference="status4evar-pref-progress-toolbar-style" label="&status4evar.progress.style.label;" + onsyncfrompreference="return status4evarPrefs.progressToolbarStyleSync();" /> + + <vbox class="css-bg-editor" preference="status4evar-pref-progress-toolbar-css" preference-editable="true" flex="1"> + <progressmeter id="status4evar-progress-bar" value="75" flex="1" /> + </vbox> + </prefpane> + + <prefpane id="status4evar-pane-download" label="&status4evar.pane.download;"> + <preferences> + <preference id="status4evar-pref-download-button-action" name="status4evar.download.button.action" type="int" /> + <preference id="status4evar-pref-download-color-active" name="status4evar.download.color.active" type="string" /> + <preference id="status4evar-pref-download-color-paused" name="status4evar.download.color.paused" type="string" /> + <preference id="status4evar-pref-download-force" name="status4evar.download.force" type="bool" /> + <preference id="status4evar-pref-download-label" name="status4evar.download.label" type="int" /> + <preference id="status4evar-pref-download-label-force" name="status4evar.download.label.force" type="bool" /> + <preference id="status4evar-pref-download-notify-animate" name="status4evar.download.notify.animate" type="bool" /> + <preference id="status4evar-pref-download-notify-timeout" name="status4evar.download.notify.timeout" type="int" /> + <preference id="status4evar-pref-download-progress" name="status4evar.download.progress" type="int" /> + <preference id="status4evar-pref-download-tooltip" name="status4evar.download.tooltip" type="int" /> + + <preference id="status4evar-pref-download-button-action-command" name="status4evar.download.button.action.command" type="string"/> + </preferences> + + <commandset id="status4evar-commandset-download"> + <command id="status4evar-command-download-progress" oncommand="status4evarPrefs.downloadProgressToggle();" /> + </commandset> + + <checkbox id="status4evar-download-force-check" preference="status4evar-pref-download-force" label="&status4evar.download.force.label;" /> + + <checkbox id="status4evar-download-label-force-check" preference="status4evar-pref-download-label-force" label="&status4evar.download.label.force.label;" /> + + <hbox align="center"> + <label id="status4evar-download-label-label" control="status4evar-download-label-menu">&status4evar.download.label.label;</label> + <menulist id="status4evar-download-label-menu" preference="status4evar-pref-download-label" sizetopopup="always"> + <menupopup> + <menuitem value="0" label="&status4evar.option.dlcount;" /> + <menuitem value="1" label="&status4evar.option.dltime;" /> + <menuitem value="2" label="&status4evar.option.both;" /> + </menupopup> + </menulist> + </hbox> + + <hbox align="center"> + <label id="status4evar-download-tooltip-label" control="status4evar-download-tooltip-menu">&status4evar.download.tooltip.label;</label> + <menulist id="status4evar-download-tooltip-menu" preference="status4evar-pref-download-tooltip" sizetopopup="always"> + <menupopup> + <menuitem value="0" label="&status4evar.option.dlcount;" /> + <menuitem value="1" label="&status4evar.option.dltime;" /> + <menuitem value="2" label="&status4evar.option.both;" /> + </menupopup> + </menulist> + </hbox> + + <hbox align="center"> + <label id="status4evar-download-button-action-label" control="status4evar-download-button-action-menu">&status4evar.download.button.action.label;</label> + <menulist id="status4evar-download-button-action-menu" preference="status4evar-pref-download-button-action" sizetopopup="always"> + <menupopup> + <menuitem value="0" label="&status4evar.option.nothing;" /> + <menuitem value="1" label="&status4evar.option.firefoxdefault;" /> + <menuitem value="2" label="&status4evar.option.download.library;" /> + <menuitem value="3" label="&status4evar.option.download.tab;" /> + <menuitem value="4" label="&status4evar.option.download.thirdparty;" id="status4evar-download-button-action-menu-thirdparty" /> + </menupopup> + </menulist> + </hbox> + + <hbox align="center"> + <label id="status4evar-download-notify-timeout-label" control="status4evar-download-notify-timeout-value">&status4evar.download.notify.timeout.label;</label> + <textbox id="status4evar-download-notify-timeout-value" preference="status4evar-pref-download-notify-timeout" type="number" size="3" /> + <label id="status4evar-download-notify-timeout-unit">&status4evar.unit.seconds;</label> + </hbox> + + <checkbox id="status4evar-download-notify-animate-check" preference="status4evar-pref-download-notify-animate" label="&status4evar.download.notify.animate.label;" /> + + <checkbox id="status4evar-download-progress-check" command="status4evar-command-download-progress" label="&status4evar.download.progress.label;" /> + + <vbox class="indent"> + <hbox align="center"> + <radiogroup id="status4evar-download-progress-radiogroup" preference="status4evar-pref-download-progress" + onsyncfrompreference="return status4evarPrefs.downloadProgressSync();"> + <radio value="1" label="&status4evar.download.progress.average.label;" /> + <radio value="2" label="&status4evar.download.progress.max.label;" /> + <radio value="3" label="&status4evar.download.progress.min.label;" /> + </radiogroup> + </hbox> + + <hbox align="center"> + <label id="status4evar-download-color-active-label" control="status4evar-download-color-active-picker">&status4evar.download.color.active.label;</label> + <colorpicker id="status4evar-download-color-active-picker" preference="status4evar-pref-download-color-active" type="button" /> + </hbox> + + <hbox align="center"> + <label id="status4evar-download-color-paused-label" control="status4evar-download-color-paused-picker">&status4evar.download.color.paused.label;</label> + <colorpicker id="status4evar-download-color-paused-picker" preference="status4evar-pref-download-color-paused" type="button" /> + </hbox> + </vbox> + </prefpane> + + <prefpane id="status4evar-pane-addonbar" label="&status4evar.pane.statusbar;"> + <preferences> + <preference id="status4evar-pref-addonbar-borderStyle" name="status4evar.addonbar.borderStyle" type="bool" /> + <preference id="status4evar-pref-addonbar-closeButton" name="status4evar.addonbar.closeButton" type="bool" /> + <preference id="status4evar-pref-addonbar-windowGripper" name="status4evar.addonbar.windowGripper" type="bool" /> + </preferences> + + <checkbox id="status4evar-addonbar-borderStyle-check" preference="status4evar-pref-addonbar-borderStyle" label="&status4evar.addonbar.borderStyle;" /> + + <checkbox id="status4evar-addonbar-closeButton-check" preference="status4evar-pref-addonbar-closeButton" label="&status4evar.addonbar.closeButton;" /> + + <checkbox id="status4evar-addonbar-windowGripper-check" preference="status4evar-pref-addonbar-windowGripper" label="&status4evar.addonbar.windowGripper;" /> + </prefpane> + + <prefpane id="status4evar-pane-advanced" label="&status4evar.pane.advanced;"> + <preferences> + <preference id="status4evar-pref-advanced-status-detectFullScreen" name="status4evar.advanced.status.detectFullScreen" type="bool" /> + <preference id="status4evar-pref-advanced-status-detectVideo" name="status4evar.advanced.status.detectVideo" type="bool" /> + <preference id="browser-pref-urlbar-trimming-enabled" name="browser.urlbar.trimURLs" type="bool" /> + </preferences> + + <vbox flex="1"> + <groupbox id="status4evar-status-urlbar-builtin"> + <caption label="&status4evar.status.urlbar.firefox.builtin.caption;" /> + + <checkbox id="browser-urlbar-trimming-enabled-ckeck" preference="browser-pref-urlbar-trimming-enabled" label="&browser.urlbar.trimming.enabled.label;" /> + </groupbox> + + <groupbox id="status4evar-advanced-status"> + <caption label="&status4evar.pane.status;" /> + + <checkbox id="status4evar-advanced-status-detectFullScreen-check" preference="status4evar-pref-advanced-status-detectFullScreen" label="&status4evar.advanced.status.detectFullScreen;" /> + <checkbox id="status4evar-advanced-status-detectVideo-check" preference="status4evar-pref-advanced-status-detectVideo" label="&status4evar.advanced.status.detectVideo;" /> + </groupbox> + </vbox> + </prefpane> +</prefwindow> + diff --git a/browser/components/statusbar/content/tabbrowser.xml b/browser/components/statusbar/content/tabbrowser.xml new file mode 100644 index 000000000..2f475771d --- /dev/null +++ b/browser/components/statusbar/content/tabbrowser.xml @@ -0,0 +1,218 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<bindings id="status4evar-bindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="statuspanel" display="xul:hbox" extends="chrome://browser/content/tabbrowser.xml#statuspanel"> + <implementation> + <!-- --> + <!-- Inverted mirror handling --> + <!-- --> + + <field name="_invertMirror"><![CDATA[ + false + ]]></field> + + <property name="invertMirror"> + <setter><![CDATA[ + this._invertMirror = val; + this.mirror = this._isMirrored; + return val; + ]]></setter> + <getter><![CDATA[ + return this._invertMirror; + ]]></getter> + </property> + + <!-- --> + <!-- Mouse mirror handling --> + <!-- --> + + <field name="_mouseMirror"><![CDATA[ + true + ]]></field> + + <field name="_mouseMirrorListen"><![CDATA[ + false + ]]></field> + + <property name="mouseMirror"> + <setter><![CDATA[ + this._mouseMirror = val; + this.setupMouseMirror(this.value); + return val; + ]]></setter> + <getter><![CDATA[ + return this._mouseMirror; + ]]></getter> + </property> + + <method name="setupMouseMirror"> + <parameter name="val"/> + <body><![CDATA[ + if(val && this._mouseMirror) + { + this._calcMouseTargetRect(); + if(!this._mouseMirrorListen) + { + MousePosTracker.addListener(this); + this._mouseMirrorListen = true; + } + } + else + { + this.mirror = false; + if(this._mouseMirrorListen) + { + MousePosTracker.removeListener(this); + this._mouseMirrorListen = false; + } + } + ]]></body> + </method> + + <method name="_calcMouseTargetRect"> + <body><![CDATA[ + let alignRight = false; + let isRTL = (getComputedStyle(document.documentElement).direction == "rtl"); + if((this._invertMirror && !isRTL) || (!this._invertMirror && isRTL)) + { + alignRight = true; + } + + let rect = this.getBoundingClientRect(); + this._mouseTargetRect = + { + top: rect.top, + bottom: rect.bottom, + left: ((alignRight) ? window.innerWidth - rect.width : 0), + right: ((alignRight) ? window.innerWidth : rect.width) + }; + ]]></body> + </method> + + <method name="onMouseEnter"> + <body><![CDATA[ + this.mirror = true; + ]]></body> + </method> + + <method name="onMouseLeave"> + <body><![CDATA[ + this.mirror = false; + ]]></body> + </method> + + <!-- --> + <!-- Mirror handling --> + <!-- --> + + <field name="_isMirrored"><![CDATA[ + false + ]]></field> + + <property name="mirror"> + <setter><![CDATA[ + this._isMirrored = val; + if(this._invertMirror) + { + val = !val; + } + + this.setBooleanAttr("mirror", val); + ]]></setter> + <getter><![CDATA[ + return this._isMirrored; + ]]></getter> + </property> + + <method name="_mirror"> + <body><![CDATA[ + this.mirror = !this._isMirrored; + ]]></body> + </method> + + <!-- --> + <!-- Value handling --> + <!-- --> + + <property name="label"> + <setter><![CDATA[ + if(window.caligon && window.caligon.status4evar) + { + window.caligon.status4evar.statusService.setStatusText(val); + } + return undefined; + ]]></setter> + <getter><![CDATA[ + if(window.caligon && window.caligon.status4evar) + { + return window.caligon.status4evar.statusService.getStatusText(); + } + return ""; + ]]></getter> + </property> + + <property name="value"> + <setter><![CDATA[ + this.setValue(val); + this.setupMouseMirror(val); + return val; + ]]></setter> + <getter><![CDATA[ + return ((this.hasAttribute("inactive")) ? "" : this.getAttribute("label")); + ]]></getter> + </property> + + <method name="setValue"> + <parameter name="val"/> + <body><![CDATA[ + if((this.getAttribute("type") || "").indexOf("network") > -1 && (this.getAttribute("previoustype") || "").indexOf("network") > -1) + { + this.style.minWidth = getComputedStyle(this).width; + } + else + { + this.style.minWidth = ""; + } + + if(val) + { + this.setAttribute("label", val); + this.setBooleanAttr("inactive", false); + } + else + { + this.setBooleanAttr("inactive", true); + } + ]]></body> + </method> + + <!-- --> + <!-- Helpers --> + <!-- --> + + <method name="setBooleanAttr"> + <parameter name="name"/> + <parameter name="val"/> + <body><![CDATA[ + if(val) + { + this.setAttribute(name, "true"); + } + else + { + this.removeAttribute(name); + } + ]]></body> + </method> + </implementation> + </binding> +</bindings> + diff --git a/browser/components/statusbar/jar.mn b/browser/components/statusbar/jar.mn new file mode 100644 index 000000000..b5a8d09b2 --- /dev/null +++ b/browser/components/statusbar/jar.mn @@ -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/. + +browser.jar: +% overlay chrome://browser/content/browser.xul chrome://browser/content/statusbar/overlay.xul +% style chrome://global/content/customizeToolbar.xul chrome://browser/skin/statusbar/overlay.css + content/browser/statusbar/overlay.js (content/overlay.js) + content/browser/statusbar/prefs.js (content/prefs.js) + content/browser/statusbar/prefs.xml (content/prefs.xml) + content/browser/statusbar/tabbrowser.xml (content/tabbrowser.xml) + content/browser/statusbar/overlay.xul (content/overlay.xul) + content/browser/statusbar/prefs.xul (content/prefs.xul) + content/browser/statusbar/overlay.css (content/overlay.css) + content/browser/statusbar/prefs.css (content/prefs.css)
\ No newline at end of file diff --git a/browser/components/statusbar/moz.build b/browser/components/statusbar/moz.build new file mode 100644 index 000000000..9d237181b --- /dev/null +++ b/browser/components/statusbar/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 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/. + +JAR_MANIFESTS += [ 'jar.mn' ] + +XPIDL_SOURCES += [ 'status4evar.idl' ] + +XPIDL_MODULE = 'status4evar' + +EXTRA_COMPONENTS += [ + 'status4evar.js', + 'status4evar.manifest', +] + +EXTRA_JS_MODULES.statusbar = [ + 'content-thunk.js', + 'Downloads.jsm', + 'Progress.jsm', + 'Status.jsm', + 'Status4Evar.jsm', + 'Toolbars.jsm', +]
\ No newline at end of file diff --git a/browser/components/statusbar/status4evar.idl b/browser/components/statusbar/status4evar.idl new file mode 100644 index 000000000..534dea31c --- /dev/null +++ b/browser/components/statusbar/status4evar.idl @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIDOMWindow; + +[scriptable, uuid(33d0433d-07be-4dc4-87fd-954057310efd)] +interface nsIStatus4Evar : nsISupports +{ + readonly attribute boolean addonbarBorderStyle; + readonly attribute boolean addonbarCloseButton; + readonly attribute boolean addonbarLegacyShim; + readonly attribute boolean addonbarWindowGripper; + + readonly attribute boolean advancedStatusDetectFullScreen; + readonly attribute boolean advancedStatusDetectVideo; + + readonly attribute long downloadButtonAction; + readonly attribute ACString downloadButtonActionCommand; + readonly attribute ACString downloadColorActive; + readonly attribute ACString downloadColorPaused; + readonly attribute boolean downloadForce; + readonly attribute long downloadLabel; + readonly attribute boolean downloadLabelForce; + readonly attribute boolean downloadNotifyAnimate; + readonly attribute long downloadNotifyTimeout; + readonly attribute long downloadProgress; + readonly attribute long downloadTooltip; + + readonly attribute boolean firstRun; + readonly attribute boolean firstRunAustralis; + + readonly attribute ACString progressToolbarCSS; + readonly attribute boolean progressToolbarForce; + readonly attribute boolean progressToolbarStyle; + readonly attribute boolean progressToolbarStyleAdvanced; + + readonly attribute long status; + readonly attribute boolean statusDefault; + readonly attribute boolean statusNetwork; + readonly attribute boolean statusNetworkXHR; + readonly attribute long statusTimeout; + readonly attribute long statusLinkOver; + readonly attribute long statusLinkOverDelayShow; + readonly attribute long statusLinkOverDelayHide; + + readonly attribute long statusToolbarMaxLength; + + readonly attribute boolean statusToolbarInvertMirror; + readonly attribute boolean statusToolbarMouseMirror; + + void resetPrefs(); + void updateWindow(in nsIDOMWindow win); +}; + diff --git a/browser/components/statusbar/status4evar.js b/browser/components/statusbar/status4evar.js new file mode 100644 index 000000000..4aa2e3e78 --- /dev/null +++ b/browser/components/statusbar/status4evar.js @@ -0,0 +1,695 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"; + +// Component constants +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"); + +const CURRENT_MIGRATION = 8; + +function Status_4_Evar(){} + +Status_4_Evar.prototype = +{ + classID: Components.ID("{33d0433d-07be-4dc4-87fd-954057310efd}"), + QueryInterface: XPCOMUtils.generateQI([ + CI.nsISupportsWeakReference, + CI.nsIObserver, + CI.nsIStatus4Evar + ]), + + prefs: null, + + addonbarBorderStyle: false, + addonbarCloseButton: false, + addonbarWindowGripper: true, + + advancedStatusDetectFullScreen: true, + advancedStatusDetectVideo: true, + + downloadButtonAction: 1, + downloadButtonActionCommand: "", + downloadColorActive: null, + downloadColorPaused: null, + downloadForce: false, + downloadLabel: 0, + downloadLabelForce: true, + downloadNotifyAnimate: true, + downloadNotifyTimeout: 60000, + downloadProgress: 1, + downloadTooltip: 1, + + firstRun: true, + + progressToolbarCSS: null, + progressToolbarForce: false, + progressToolbarStyle: false, + + status: 1, + statusDefault: true, + statusNetwork: true, + statusTimeout: 10000, + statusLinkOver: 1, + statusLinkOverDelayShow: 70, + statusLinkOverDelayHide: 150, + + statusToolbarMaxLength: 0, + + statusToolbarInvertMirror: false, + statusToolbarMouseMirror: true, + + pref_registry: + { + "addonbar.borderStyle": + { + update: function() + { + this.addonbarBorderStyle = this.prefs.getBoolPref("addonbar.borderStyle"); + }, + updateWindow: function(win) + { + let browser_bottom_box = win.caligon.status4evar.getters.browserBottomBox; + if(browser_bottom_box) + { + this.setBoolElementAttribute(browser_bottom_box, "s4eboarder", this.addonbarBorderStyle); + } + } + }, + + "addonbar.closeButton": + { + update: function() + { + this.addonbarCloseButton = this.prefs.getBoolPref("addonbar.closeButton"); + }, + updateWindow: function(win) + { + let addonbar_close_button = win.caligon.status4evar.getters.addonbarCloseButton; + if(addonbar_close_button) + { + addonbar_close_button.hidden = !this.addonbarCloseButton; + } + } + }, + + "addonbar.windowGripper": + { + update: function() + { + this.addonbarWindowGripper = this.prefs.getBoolPref("addonbar.windowGripper"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.toolbars.updateWindowGripper(true); + } + }, + + "advanced.status.detectFullScreen": + { + update: function() + { + this.advancedStatusDetectFullScreen = this.prefs.getBoolPref("advanced.status.detectFullScreen"); + } + }, + + "advanced.status.detectVideo": + { + update: function() + { + this.advancedStatusDetectVideo = this.prefs.getBoolPref("advanced.status.detectVideo"); + } + }, + + "download.button.action": + { + update: function() + { + this.downloadButtonAction = this.prefs.getIntPref("download.button.action"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.downloadStatus.updateBinding(); + } + }, + + "download.button.action.command": + { + update: function() + { + this.downloadButtonActionCommand = this.prefs.getCharPref("download.button.action.command"); + } + }, + + "download.color.active": + { + update: function() + { + this.downloadColorActive = this.prefs.getCharPref("download.color.active"); + }, + updateDynamicStyle: function(sheet) + { + sheet.cssRules[2].style.backgroundColor = this.downloadColorActive; + } + }, + + "download.color.paused": + { + update: function() + { + this.downloadColorPaused = this.prefs.getCharPref("download.color.paused"); + }, + updateDynamicStyle: function(sheet) + { + sheet.cssRules[3].style.backgroundColor = this.downloadColorPaused; + } + }, + + "download.force": + { + update: function() + { + this.downloadForce = this.prefs.getBoolPref("download.force"); + }, + updateWindow: function(win) + { + let download_button = win.caligon.status4evar.getters.downloadButton; + if(download_button) + { + this.setBoolElementAttribute(download_button, "forcevisible", this.downloadForce); + } + + let download_notify_anchor = win.caligon.status4evar.getters.downloadNotifyAnchor; + this.setBoolElementAttribute(download_notify_anchor, "forcevisible", this.downloadForce); + } + }, + + "download.label": + { + update: function() + { + this.downloadLabel = this.prefs.getIntPref("download.label"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.downloadStatus.updateButton(); + } + }, + + "download.label.force": + { + update: function() + { + this.downloadLabelForce = this.prefs.getBoolPref("download.label.force"); + }, + updateWindow: function(win) + { + let download_button = win.caligon.status4evar.getters.downloadButton; + if(download_button) + { + this.setBoolElementAttribute(download_button, "forcelabel", this.downloadLabelForce); + } + } + }, + + "download.notify.animate": + { + update: function() + { + this.downloadNotifyAnimate = this.prefs.getBoolPref("download.notify.animate"); + } + }, + + "download.notify.timeout": + { + update: function() + { + this.downloadNotifyTimeout = (this.prefs.getIntPref("download.notify.timeout") * 1000); + } + }, + + "download.progress": + { + update: function() + { + this.downloadProgress = this.prefs.getIntPref("download.progress"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.downloadStatus.updateButton(); + } + }, + + "download.tooltip": + { + update: function() + { + this.downloadTooltip = this.prefs.getIntPref("download.tooltip"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.downloadStatus.updateButton(); + } + }, + + "progress.toolbar.css": + { + update: function() + { + this.progressToolbarCSS = this.prefs.getCharPref("progress.toolbar.css"); + }, + updateDynamicStyle: function(sheet) + { + sheet.cssRules[1].style.background = this.progressToolbarCSS; + } + }, + + "progress.toolbar.force": + { + update: function() + { + this.progressToolbarForce = this.prefs.getBoolPref("progress.toolbar.force"); + }, + updateWindow: function(win) + { + let toolbar_progress = win.caligon.status4evar.getters.toolbarProgress; + if(toolbar_progress) + { + this.setBoolElementAttribute(toolbar_progress, "forcevisible", this.progressToolbarForce); + } + } + }, + + "progress.toolbar.style": + { + update: function() + { + this.progressToolbarStyle = this.prefs.getBoolPref("progress.toolbar.style"); + }, + updateWindow: function(win) + { + let toolbar_progress = win.caligon.status4evar.getters.toolbarProgress; + if(toolbar_progress) + { + this.setBoolElementAttribute(toolbar_progress, "s4estyle", this.progressToolbarStyle); + } + } + }, + + "status": + { + update: function() + { + this.status = this.prefs.getIntPref("status"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.statusService.clearStatusField(); + win.caligon.status4evar.statusService.updateStatusField(true); + } + }, + + "status.default": + { + update: function() + { + this.statusDefault = this.prefs.getBoolPref("status.default"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.statusService.buildTextOrder(); + win.caligon.status4evar.statusService.updateStatusField(true); + } + }, + + "status.linkOver": + { + update: function() + { + this.statusLinkOver = this.prefs.getIntPref("status.linkOver"); + } + }, + + "status.linkOver.delay.show": + { + update: function() + { + this.statusLinkOverDelayShow = this.prefs.getIntPref("status.linkOver.delay.show"); + } + }, + + "status.linkOver.delay.hide": + { + update: function() + { + this.statusLinkOverDelayHide = this.prefs.getIntPref("status.linkOver.delay.hide"); + } + }, + + "status.network": + { + update: function() + { + this.statusNetwork = this.prefs.getBoolPref("status.network"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.statusService.buildTextOrder(); + } + }, + + "status.network.xhr": + { + update: function() + { + this.statusNetworkXHR = this.prefs.getBoolPref("status.network.xhr"); + }, + updateWindow: function(win) + { + win.caligon.status4evar.statusService.buildTextOrder(); + } + }, + + "status.timeout": + { + update: function() + { + this.statusTimeout = (this.prefs.getIntPref("status.timeout") * 1000); + }, + updateWindow: function(win) + { + win.caligon.status4evar.statusService.updateStatusField(true); + } + }, + + "status.toolbar.maxLength": + { + update: function() + { + this.statusToolbarMaxLength = this.prefs.getIntPref("status.toolbar.maxLength"); + }, + updateWindow: function(win) + { + let status_widget = win.caligon.status4evar.getters.statusWidget; + if(status_widget) + { + status_widget.maxWidth = (this.statusToolbarMaxLength || ""); + } + } + }, + + "status.popup.invertMirror": + { + update: function() + { + this.statusToolbarInvertMirror = this.prefs.getBoolPref("status.popup.invertMirror"); + }, + updateWindow: function(win) + { + let statusOverlay = win.caligon.status4evar.getters.statusOverlay; + if(statusOverlay) + { + statusOverlay.invertMirror = this.statusToolbarInvertMirror; + } + } + }, + + "status.popup.mouseMirror": + { + update: function() + { + this.statusToolbarMouseMirror = this.prefs.getBoolPref("status.popup.mouseMirror"); + }, + updateWindow: function(win) + { + let statusOverlay = win.caligon.status4evar.getters.statusOverlay; + if(statusOverlay) + { + statusOverlay.mouseMirror = this.statusToolbarMouseMirror; + } + } + } + + }, + + // nsIObserver + observe: function(subject, topic, data) + { + try + { + switch(topic) + { + case "profile-after-change": + this.startup(); + break; + case "quit-application": + this.shutdown(); + break; + case "nsPref:changed": + this.updatePref(data, true); + break; + } + } + catch(e) + { + CU.reportError(e); + } + }, + + startup: function() + { + this.prefs = Services.prefs.getBranch("status4evar.").QueryInterface(CI.nsIPrefBranch2); + + this.firstRun = this.prefs.getBoolPref("firstRun"); + if(this.firstRun) + { + this.prefs.setBoolPref("firstRun", false); + } + + this.migrate(); + + for(let pref in this.pref_registry) + { + let pro = this.pref_registry[pref]; + + pro.update = pro.update.bind(this); + if(pro.updateWindow) + { + pro.updateWindow = pro.updateWindow.bind(this); + } + if(pro.updateDynamicStyle) + { + pro.updateDynamicStyle = pro.updateDynamicStyle.bind(this); + } + + this.prefs.addObserver(pref, this, true); + + this.updatePref(pref, false); + } + + Services.obs.addObserver(this, "quit-application", true); + }, + + shutdown: function() + { + Services.obs.removeObserver(this, "quit-application"); + + for(let pref in this.pref_registry) + { + this.prefs.removeObserver(pref, this); + } + + this.prefs = null; + }, + + migrate: function() + { + if(!this.firstRun) + { + let migration = 0; + try + { + migration = this.prefs.getIntPref("migration"); + } + catch(e) {} + + switch(migration) + { + case 5: + this.migrateBoolPref("status.detectFullScreen", "advanced.status.detectFullScreen"); + case 6: + let oldDownloadAction = this.prefs.getIntPref("download.button.action"); + let newDownloadAction = 1; + switch(oldDownloadAction) + { + case 2: + newDownloadAction = 1; + break; + case 3: + newDownloadAction = 2; + break; + case 4: + newDownloadAction = 1; + break; + } + this.prefs.setIntPref("download.button.action", newDownloadAction); + case 7: + let progressLocation = this.prefs.getIntPref("status"); + if (progressLocation == 2) + this.prefs.setIntPref("status", 1); + let linkOverLocation = this.prefs.getIntPref("status.linkOver"); + if (linkOverLocation == 2) + this.prefs.setIntPref("status.linkOver", 1); + break; + case CURRENT_MIGRATION: + break; + } + } + + this.prefs.setIntPref("migration", CURRENT_MIGRATION); + }, + + migrateBoolPref: function(oldPref, newPref) + { + if(this.prefs.prefHasUserValue(oldPref)) + { + this.prefs.setBoolPref(newPref, this.prefs.getBoolPref(oldPref)); + this.prefs.clearUserPref(oldPref); + } + }, + + migrateIntPref: function(oldPref, newPref) + { + if(this.prefs.prefHasUserValue(oldPref)) + { + this.prefs.setIntPref(newPref, this.prefs.getIntPref(oldPref)); + this.prefs.clearUserPref(oldPref); + } + }, + + migrateCharPref: function(oldPref, newPref) + { + if(this.prefs.prefHasUserValue(oldPref)) + { + this.prefs.setCharPref(newPref, this.prefs.getCharPref(oldPref)); + this.prefs.clearUserPref(oldPref); + } + }, + + updatePref: function(pref, updateWindows) + { + if(!(pref in this.pref_registry)) + { + return; + } + let pro = this.pref_registry[pref]; + + pro.update(); + + if(updateWindows) + { + let windowsEnum = Services.wm.getEnumerator("navigator:browser"); + while(windowsEnum.hasMoreElements()) + { + this.updateWindow(windowsEnum.getNext(), pro); + } + } + + if(pro.alsoUpdate) + { + pro.alsoUpdate.forEach(function (alsoPref) + { + this.updatePref(alsoPref); + }, this); + } + }, + + // Updtate a browser window + updateWindow: function(win, pro) + { + if(!(win instanceof CI.nsIDOMWindow) + || !(win.document.documentElement.getAttribute("windowtype") == "navigator:browser")) + { + return; + } + + if(pro) + { + this.handlePro(win, pro); + } + else + { + for(let pref in this.pref_registry) + { + this.handlePro(win, this.pref_registry[pref]); + } + } + }, + + handlePro: function(win, pro) + { + if(pro.updateWindow) + { + pro.updateWindow(win); + } + + if(pro.updateDynamicStyle) + { + let styleSheets = win.document.styleSheets; + for(let i = 0; i < styleSheets.length; i++) + { + let styleSheet = styleSheets[i]; + if(styleSheet.href == "chrome://browser/skin/statusbar/dynamic.css") + { + pro.updateDynamicStyle(styleSheet); + break; + } + } + } + }, + + setBoolElementAttribute: function(elem, attr, val) + { + if(val) + { + elem.setAttribute(attr, "true"); + } + else + { + elem.removeAttribute(attr); + } + }, + + setStringElementAttribute: function(elem, attr, val) + { + if(val) + { + elem.setAttribute(attr, val); + } + else + { + elem.removeAttribute(attr); + } + }, + + resetPrefs: function() + { + let childPrefs = this.prefs.getChildList(""); + childPrefs.forEach(function(pref) + { + if(this.prefs.prefHasUserValue(pref)) + { + this.prefs.clearUserPref(pref); + } + }, this); + } +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([Status_4_Evar]); + diff --git a/browser/components/statusbar/status4evar.manifest b/browser/components/statusbar/status4evar.manifest new file mode 100644 index 000000000..4bcf697d6 --- /dev/null +++ b/browser/components/statusbar/status4evar.manifest @@ -0,0 +1,3 @@ +component {33d0433d-07be-4dc4-87fd-954057310efd} status4evar.js +contract @caligonstudios.com/status4evar;1 {33d0433d-07be-4dc4-87fd-954057310efd} +category profile-after-change Status-4-Evar @caligonstudios.com/status4evar;1 |