From d13830aaa44daece600fc414b57d8d277d01f0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Qu=C3=A8ze?= Date: Thu, 19 Mar 2015 15:50:23 -0700 Subject: [PATCH 01/80] Bug 1144377 - hover state on reading list sidebar items remains after the mouse leaves the list, r=jaws. --- browser/components/readinglist/sidebar.js | 13 ++++++------- browser/themes/shared/readinglist/sidebar.inc.css | 3 ++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/browser/components/readinglist/sidebar.js b/browser/components/readinglist/sidebar.js index bc943110a295..e4e3a5c833b5 100644 --- a/browser/components/readinglist/sidebar.js +++ b/browser/components/readinglist/sidebar.js @@ -165,7 +165,7 @@ let RLSidebar = { }, /** - * The currently active element in the list. + * The list item displayed in the current tab. * @type {Element} */ get activeItem() { @@ -204,7 +204,7 @@ let RLSidebar = { }, /** - * The currently selected item in the list. + * The list item selected with the keyboard. * @type {Element} */ get selectedItem() { @@ -366,15 +366,14 @@ let RLSidebar = { }, /** - * Handle a mousemove event over the list box. + * Handle a mousemove event over the list box: + * If the hovered item isn't the selected one, clear the selection. * @param {Event} event - Triggering event. */ onListMouseMove(event) { let itemNode = this.findParentItemNode(event.target); - if (!itemNode) - return; - - this.selectedItem = itemNode; + if (itemNode != this.selectedItem) + this.selectedItem = null; }, /** diff --git a/browser/themes/shared/readinglist/sidebar.inc.css b/browser/themes/shared/readinglist/sidebar.inc.css index 108e323363fe..b198f909cc75 100644 --- a/browser/themes/shared/readinglist/sidebar.inc.css +++ b/browser/themes/shared/readinglist/sidebar.inc.css @@ -84,11 +84,12 @@ body { color: #008ACB; } -.item:not(:hover) .remove-button { +.item:not(:hover):not(.selected) .remove-button { display: none; } .remove-button { + padding: 0; width: 16px; height: 16px; background-size: contain; From 14ff6d0c0642f179bd784b40a022abc42f749081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Qu=C3=A8ze?= Date: Thu, 19 Mar 2015 15:50:23 -0700 Subject: [PATCH 02/80] Bug 1144675 - The Reading List button from the Location Bar should have a distinct icon for pages currently in the list, r=markh. --HG-- rename : browser/themes/shared/readinglist.inc.css => browser/themes/shared/readinglist/readinglist.inc.css --- browser/themes/linux/browser.css | 2 +- browser/themes/osx/browser.css | 2 +- browser/themes/shared/readinglist/icons.svg | 31 +++++++++---------- .../{ => readinglist}/readinglist.inc.css | 9 ++++-- browser/themes/windows/browser.css | 2 +- 5 files changed, 24 insertions(+), 22 deletions(-) rename browser/themes/shared/{ => readinglist}/readinglist.inc.css (85%) diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css index 899255777820..40d21867ffd5 100644 --- a/browser/themes/linux/browser.css +++ b/browser/themes/linux/browser.css @@ -1628,7 +1628,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action- list-style-image: url("chrome://browser/skin/Info.png"); } -%include ../shared/readinglist.inc.css +%include ../shared/readinglist/readinglist.inc.css /* Reader mode button */ diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index 648ccaf17ab7..237edea7da95 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -2527,7 +2527,7 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url- } } -%include ../shared/readinglist.inc.css +%include ../shared/readinglist/readinglist.inc.css /* Reader mode button */ diff --git a/browser/themes/shared/readinglist/icons.svg b/browser/themes/shared/readinglist/icons.svg index 364d25c83ca9..c859ae226dfd 100644 --- a/browser/themes/shared/readinglist/icons.svg +++ b/browser/themes/shared/readinglist/icons.svg @@ -13,25 +13,15 @@ display: none; } - #addpage { + #addpage, #alreadyadded { fill: #808080; } - #addpage-hover { + #addpage-hover, #alreadyadded-hover { fill: #555555; } - #addpage-active { + #addpage-active, #alreadyadded-active { fill: #0095DD; } - - #alreadyadded { - fill: #0095DD; - } - #alreadyadded-hover { - fill: #555555; - } - #alreadyadded-active { - fill: #808080; - } @@ -40,18 +30,27 @@ + + + + + + + + + - - - + + + diff --git a/browser/themes/shared/readinglist.inc.css b/browser/themes/shared/readinglist/readinglist.inc.css similarity index 85% rename from browser/themes/shared/readinglist.inc.css rename to browser/themes/shared/readinglist/readinglist.inc.css index 6bdf848a3188..7106352764a9 100644 --- a/browser/themes/shared/readinglist.inc.css +++ b/browser/themes/shared/readinglist/readinglist.inc.css @@ -1,5 +1,9 @@ /* Reading List button */ +#urlbar:not([focused]):not(:hover) #readinglist-addremove-button { + display: none; +} + #readinglist-addremove-button { -moz-appearance: none; border: none; @@ -16,11 +20,11 @@ height: 16px } -#readinglist-addremove-button:not([already-added="true"]):hover { +#readinglist-addremove-button:hover { list-style-image: url("chrome://browser/skin/readinglist/icons.svg#addpage-hover"); } -#readinglist-addremove-button:not([already-added="true"]):active { +#readinglist-addremove-button:active { list-style-image: url("chrome://browser/skin/readinglist/icons.svg#addpage-active"); } @@ -35,4 +39,3 @@ #readinglist-addremove-button[already-added="true"]:active { list-style-image: url("chrome://browser/skin/readinglist/icons.svg#alreadyadded-active"); } - diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index 3452ff91e37e..b02f0a5974c8 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -1576,7 +1576,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action- -moz-image-region: rect(0, 48px, 16px, 32px); } -%include ../shared/readinglist.inc.css +%include ../shared/readinglist/readinglist.inc.css /* Reader mode button */ From 4ba58a55edf4f23a5128137991eb7cd0983fa2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Qu=C3=A8ze?= Date: Thu, 19 Mar 2015 15:50:23 -0700 Subject: [PATCH 03/80] Bug 1144680 - The Reading List URLbar button should handle about:reader urls and filter out other non-http(s) urls, r=markh. --- browser/base/content/browser-readinglist.js | 27 ++++++++++++++++--- .../components/readinglist/ReadingList.jsm | 8 +++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/browser/base/content/browser-readinglist.js b/browser/base/content/browser-readinglist.js index 851fef307cb9..b3cda23a73ce 100644 --- a/browser/base/content/browser-readinglist.js +++ b/browser/base/content/browser-readinglist.js @@ -232,12 +232,22 @@ let ReadingListUI = { // nothing to do if we have no button. return; } - if (!this.enabled || state == "invalid") { + + let uri; + if (this.enabled && state == "valid") { + uri = gBrowser.currentURI; + if (uri.schemeIs("about")) + uri = ReaderParent.parseReaderUrl(uri.spec); + else if (!uri.schemeIs("http") && !uri.schemeIs("https")) + uri = null; + } + + if (!uri) { this.toolbarButton.setAttribute("hidden", true); return; } - let isInList = yield ReadingList.containsURL(gBrowser.currentURI); + let isInList = yield ReadingList.containsURL(uri); this.setToolbarButtonState(isInList); }), @@ -268,11 +278,17 @@ let ReadingListUI = { * @returns {Promise} Promise resolved when operation has completed. */ togglePageByBrowser: Task.async(function* (browser) { - let item = yield ReadingList.getItemForURL(browser.currentURI); + let uri = browser.currentURI; + if (uri.spec.startsWith("about:reader?")) + uri = ReaderParent.parseReaderUrl(uri.spec); + if (!uri) + return; + + let item = yield ReadingList.getItemForURL(uri); if (item) { yield item.delete(); } else { - yield ReadingList.addItemFromBrowser(browser); + yield ReadingList.addItemFromBrowser(browser, uri); } }), @@ -284,6 +300,9 @@ let ReadingListUI = { */ isItemForCurrentBrowser(item) { let currentURL = gBrowser.currentURI.spec; + if (currentURL.startsWith("about:reader?")) + currentURL = ReaderParent.parseReaderUrl(currentURL); + if (item.url == currentURL || item.resolvedURL == currentURL) { return true; } diff --git a/browser/components/readinglist/ReadingList.jsm b/browser/components/readinglist/ReadingList.jsm index ed2de5eb214c..9b6c785ebd74 100644 --- a/browser/components/readinglist/ReadingList.jsm +++ b/browser/components/readinglist/ReadingList.jsm @@ -289,13 +289,15 @@ ReadingListImpl.prototype = { /** * Add to the ReadingList the page that is loaded in a given browser. * - * @param {} browser - Browser element for the document. + * @param {} browser - Browser element for the document, + * used to get metadata about the article. + * @param {nsIURI/string} url - url to add to the reading list. * @return {Promise} Promise that is fullfilled with the added item. */ - addItemFromBrowser: Task.async(function* (browser) { + addItemFromBrowser: Task.async(function* (browser, url) { let metadata = yield getMetadataFromBrowser(browser); let itemData = { - url: browser.currentURI, + url: url, title: metadata.title, resolvedURL: metadata.url, excerpt: metadata.description, From ad54999ad089fa7322b7fa53cc25df3fcb4040de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Qu=C3=A8ze?= Date: Thu, 19 Mar 2015 15:50:23 -0700 Subject: [PATCH 04/80] Bug 1145372 - The add to reading list url bar button is larger than the other url bar buttons, r=markh. --- browser/themes/linux/browser.css | 4 ++++ browser/themes/osx/browser.css | 6 ++++++ browser/themes/shared/readinglist/readinglist.inc.css | 5 ++--- browser/themes/windows/browser.css | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css index 40d21867ffd5..98b1136f1e59 100644 --- a/browser/themes/linux/browser.css +++ b/browser/themes/linux/browser.css @@ -1630,6 +1630,10 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action- %include ../shared/readinglist/readinglist.inc.css +#readinglist-addremove-button { + padding: 0 2px; +} + /* Reader mode button */ #reader-mode-button { diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index 237edea7da95..b205a4098b71 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -2529,6 +2529,12 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url- %include ../shared/readinglist/readinglist.inc.css +#readinglist-addremove-button { + padding: 3px; + -moz-padding-start: 2px; + -moz-padding-end: 1px; +} + /* Reader mode button */ #reader-mode-button { diff --git a/browser/themes/shared/readinglist/readinglist.inc.css b/browser/themes/shared/readinglist/readinglist.inc.css index 7106352764a9..7840964a6b3b 100644 --- a/browser/themes/shared/readinglist/readinglist.inc.css +++ b/browser/themes/shared/readinglist/readinglist.inc.css @@ -8,7 +8,6 @@ -moz-appearance: none; border: none; list-style-image: url("chrome://browser/skin/readinglist/icons.svg#addpage"); - padding: 3px; } #readinglist-addremove-button:hover { @@ -16,8 +15,8 @@ } #readinglist-addremove-button > .toolbarbutton-icon { - width: 16px; - height: 16px + width: 14px; + height: 14px } #readinglist-addremove-button:hover { diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index b02f0a5974c8..098e01d02cc6 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -1578,6 +1578,10 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action- %include ../shared/readinglist/readinglist.inc.css +#readinglist-addremove-button { + padding: 0 2px; +} + /* Reader mode button */ #reader-mode-button { From ec5d5452f9c9ee50b1b23404df56a91894038dba Mon Sep 17 00:00:00 2001 From: Jared Wein Date: Thu, 19 Mar 2015 15:50:38 -0700 Subject: [PATCH 05/80] Bug 1133429 - [ReadingList] Store image URL in the ReadingList sqlite database. r=markh, a=KWierso. --- browser/components/readinglist/ReadingList.jsm | 9 +++------ browser/components/readinglist/SQLiteStore.jsm | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/browser/components/readinglist/ReadingList.jsm b/browser/components/readinglist/ReadingList.jsm index 9b6c785ebd74..259be3c795c9 100644 --- a/browser/components/readinglist/ReadingList.jsm +++ b/browser/components/readinglist/ReadingList.jsm @@ -42,6 +42,7 @@ const ITEM_BASIC_PROPERTY_NAMES = ` resolvedURL resolvedTitle excerpt + preview status favorite isArticle @@ -303,12 +304,8 @@ ReadingListImpl.prototype = { excerpt: metadata.description, }; - if (metadata.description) { - itemData.exerpt = metadata.description; - } - if (metadata.previews.length > 0) { - itemData.image = metadata.previews[0]; + itemData.preview = metadata.previews[0]; } let item = yield ReadingList.addItem(itemData); @@ -918,7 +915,7 @@ function getMetadataFromBrowser(browser) { Object.defineProperty(this, "ReadingList", { get() { if (!this._singleton) { - let store = new SQLiteStore("reading-list-temp.sqlite"); + let store = new SQLiteStore("reading-list-temp2.sqlite"); this._singleton = new ReadingListImpl(store); } return this._singleton; diff --git a/browser/components/readinglist/SQLiteStore.jsm b/browser/components/readinglist/SQLiteStore.jsm index 8777aa9ee72f..645bd21412cd 100644 --- a/browser/components/readinglist/SQLiteStore.jsm +++ b/browser/components/readinglist/SQLiteStore.jsm @@ -204,7 +204,8 @@ this.SQLiteStore.prototype = { storedOn INTEGER, markedReadBy TEXT, markedReadOn INTEGER, - readPosition INTEGER + readPosition INTEGER, + preview TEXT ); `); yield conn.execute(` From 2216b53c84cd858edd839f2f24725f510c9bbf5d Mon Sep 17 00:00:00 2001 From: Manraj Singh Date: Thu, 19 Mar 2015 20:23:56 -0300 Subject: [PATCH 06/80] Bug 1120408 - Show TelemetryLog in about:telemetry. r=felipe,gfritzsche --- toolkit/content/aboutTelemetry.js | 55 +++++++++++++++++++ toolkit/content/aboutTelemetry.xhtml | 9 +++ .../en-US/chrome/global/aboutTelemetry.dtd | 4 ++ .../chrome/global/aboutTelemetry.properties | 8 +++ 4 files changed, 76 insertions(+) diff --git a/toolkit/content/aboutTelemetry.js b/toolkit/content/aboutTelemetry.js index 87ac04d3fd85..90620ae49d79 100644 --- a/toolkit/content/aboutTelemetry.js +++ b/toolkit/content/aboutTelemetry.js @@ -12,6 +12,7 @@ Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/TelemetryTimestamps.jsm"); Cu.import("resource://gre/modules/TelemetryPing.jsm"); Cu.import("resource://gre/modules/TelemetrySession.jsm"); +Cu.import("resource://gre/modules/TelemetryLog.jsm"); const Telemetry = Services.telemetry; const bundle = Services.strings.createBundle( @@ -152,6 +153,57 @@ let GeneralData = { }, }; +let TelLog = { + /** + * Renders the telemetry log + */ + render: function() { + let entries = TelemetryLog.entries(); + + if(entries.length == 0) { + return; + } + setHasData("telemetry-log-section", true); + let table = document.createElement("table"); + + let caption = document.createElement("caption"); + let captionString = bundle.GetStringFromName("telemetryLogTitle"); + caption.appendChild(document.createTextNode(captionString + "\n")); + table.appendChild(caption); + + let headings = document.createElement("tr"); + this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingId") + "\t"); + this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingTimestamp") + "\t"); + this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingData") + "\t"); + table.appendChild(headings); + + for (let entry of entries) { + let row = document.createElement("tr"); + for (let elem of entry) { + this.appendColumn(row, "td", elem + "\t"); + } + table.appendChild(row); + } + + let dataDiv = document.getElementById("telemetry-log"); + dataDiv.appendChild(table); + }, + + /** + * Helper function for appending a column to the data table. + * + * @param aRowElement Parent row element + * @param aColType Column's tag name + * @param aColText Column contents + */ + appendColumn: function(aRowElement, aColType, aColText) { + let colElement = document.createElement(aColType); + let colTextElement = document.createTextNode(aColText); + colElement.appendChild(colTextElement); + aRowElement.appendChild(colElement); + }, +}; + let SlowSQL = { slowSqlHits: bundle.GetStringFromName("slowSqlHits"), @@ -993,6 +1045,9 @@ function onLoad() { // Show general data. GeneralData.render(); + // Show telemetry log. + TelLog.render(); + // Show slow SQL stats SlowSQL.render(); diff --git a/toolkit/content/aboutTelemetry.xhtml b/toolkit/content/aboutTelemetry.xhtml index 2c97312c1306..be9f64f9b1bf 100644 --- a/toolkit/content/aboutTelemetry.xhtml +++ b/toolkit/content/aboutTelemetry.xhtml @@ -44,6 +44,15 @@ +
+ +

&aboutTelemetry.telemetryLogSection;

+ &aboutTelemetry.toggle; + &aboutTelemetry.emptySection; +
+
+
+

&aboutTelemetry.slowSqlSection;

diff --git a/toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd b/toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd index 78ed6d21076e..843501055854 100644 --- a/toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd +++ b/toolkit/locales/en-US/chrome/global/aboutTelemetry.dtd @@ -16,6 +16,10 @@ General Data "> + + diff --git a/toolkit/locales/en-US/chrome/global/aboutTelemetry.properties b/toolkit/locales/en-US/chrome/global/aboutTelemetry.properties index 1604a1ba982b..90dff432035e 100644 --- a/toolkit/locales/en-US/chrome/global/aboutTelemetry.properties +++ b/toolkit/locales/en-US/chrome/global/aboutTelemetry.properties @@ -13,6 +13,14 @@ generalDataHeadingName = Name generalDataHeadingValue = Value +telemetryLogTitle = Telemetry Log + +telemetryLogHeadingId = Id + +telemetryLogHeadingTimestamp = Timestamp + +telemetryLogHeadingData = Data + slowSqlMain = Slow SQL Statements on Main Thread slowSqlOther = Slow SQL Statements on Helper Threads From 5e4ea3a14defd541a4874a9d682dbdfbf39561f3 Mon Sep 17 00:00:00 2001 From: Felipe Gomes Date: Thu, 19 Mar 2015 20:24:08 -0300 Subject: [PATCH 07/80] Bug 1143263 - WebApp Runtime makes Mac always use discrete graphics card. r=mstange --- toolkit/webapps/MacNativeApp.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/toolkit/webapps/MacNativeApp.js b/toolkit/webapps/MacNativeApp.js index dadd6c43ed45..9748c0e80352 100644 --- a/toolkit/webapps/MacNativeApp.js +++ b/toolkit/webapps/MacNativeApp.js @@ -269,6 +269,8 @@ NativeApp.prototype = { 0\n\ NSHighResolutionCapable\n\ \n\ + NSSupportsAutomaticGraphicsSwitching\n\ + \n\ NSPrincipalClass\n\ GeckoNSApplication\n\ FirefoxBinary\n\ From f333ec2ca3b3a7a3f9b605a0a695e34e975954cb Mon Sep 17 00:00:00 2001 From: Robert Strong Date: Thu, 19 Mar 2015 16:43:29 -0700 Subject: [PATCH 08/80] Bug 1081450 - Copy maintenance service binary into its install directory on Windows 8 when the installed binary is different. r=bbondy --- .../update/tests/data/xpcshellUtilsAUS.js | 36 ++++--------------- .../marStageSuccessCompleteSvc.js | 2 +- .../marStageSuccessPartialSvc.js | 2 +- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js index e0ed738b554f..465b3fc4b7e2 100644 --- a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js +++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js @@ -1572,15 +1572,9 @@ function stageUpdate() { * * @param aFirstTest * Whether this is the first test within the test. - * @param aSkipTest - * Whether to skip this test if the installed maintenance service - * isn't the same as the build's maintenance service. This is a - * temporary workaround until all build systems grant write access to - * the maintenance service install directory so the tests can copy the - * version of the maintenance service that should be tests. * @return true if the test should run and false if it shouldn't. */ -function shouldRunServiceTest(aFirstTest, aSkipTest) { +function shouldRunServiceTest(aFirstTest) { let binDir = getGREBinDir(); let updaterBin = binDir.clone(); updaterBin.append(FILE_UPDATER_BIN); @@ -1646,7 +1640,7 @@ function shouldRunServiceTest(aFirstTest, aSkipTest) { // In case the machine is running an old maintenance service or if it // is not installed, and permissions exist to install it. Then install // the newer bin that we have since all of the other checks passed. - return attemptServiceInstall(aSkipTest); + return attemptServiceInstall(); } /** @@ -1854,15 +1848,8 @@ function copyFileToTestAppDir(aFileRelPath, aInGreDir) { * This is useful for XP where we have permission to upgrade in case an * older service installer exists. Also if the user manually installed into * a unprivileged location. - * - * @param aSkipTest - * Whether to skip this test if the installed maintenance service - * isn't the same as the build's maintenance service. This is a - * temporary workaround until all build systems grant write access to - * the maintenance service install directory so the tests can copy the - * version of the maintenance service that should be tests. */ -function attemptServiceInstall(aSkipTest) { +function attemptServiceInstall() { const CSIDL_PROGRAM_FILES = 0x26; const CSIDL_PROGRAM_FILESX86 = 0x2A; // This will return an empty string on our Win XP build systems. @@ -1905,7 +1892,6 @@ function attemptServiceInstall(aSkipTest) { oldMaintSvcBin.moveTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN + ".backup"); buildMaintSvcBin.copyTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN); backupMaintSvcBin.remove(false); - return true; } catch (e) { // Restore the original file in case the moveTo was successful. if (backupMaintSvcBin.exists()) { @@ -1918,21 +1904,11 @@ function attemptServiceInstall(aSkipTest) { logTestInfo("unable to copy new maintenance service into the " + "maintenance service directory: " + maintSvcDir.path + ", " + "Exception: " + e); + do_throw("The account running the tests on the build systems should have " + + "write access to the maintenance service directory!"); } - let version = Cc["@mozilla.org/system-info;1"]. - getService(Ci.nsIPropertyBag2). - getProperty("version"); - // The account running the tests on Win XP and Win 7 build systems have write - // access to the maintenance service directory so throw if copying the - // maintenance service binary fails. This should always throw after write - // access is provided on all Windows build slaves in bug 1067756. - if ((parseFloat(version) <= 6.1)) { - do_throw("The account running the tests on Win 7 and below build systems " + - "should have write access to the maintenance service directory!"); - } - - return aSkipTest ? false : true; + return true; } /** diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js index 4f9610f26ecc..19dc39d67c77 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js @@ -6,7 +6,7 @@ /* General Complete MAR File Staged Patch Apply Test */ function run_test() { - if (!shouldRunServiceTest(false, true)) { + if (!shouldRunServiceTest()) { return; } diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js index a5af13db3ecb..110384831b23 100644 --- a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js @@ -9,7 +9,7 @@ function run_test() { // Set to true due to bug 1083653 DEBUG_AUS_TEST = true; - if (!shouldRunServiceTest(false, true)) { + if (!shouldRunServiceTest()) { return; } From 9d06161cb9123b6ca4a5752a161a8cbc7266d399 Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Wed, 11 Mar 2015 11:40:29 -0700 Subject: [PATCH 09/80] Bug 1131004 - Provide a more detailed description for pre-installed OpenH264 add-on. r=gfritzsche --HG-- extra : rebase_source : 9eb6390b093db42e82ccfdde2327aecc90dfd71d --- dom/locales/en-US/chrome/plugins.properties | 2 +- toolkit/mozapps/extensions/internal/GMPProvider.jsm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dom/locales/en-US/chrome/plugins.properties b/dom/locales/en-US/chrome/plugins.properties index df1df4a592f0..124768018d78 100644 --- a/dom/locales/en-US/chrome/plugins.properties +++ b/dom/locales/en-US/chrome/plugins.properties @@ -25,7 +25,7 @@ learn_more_label=Learn More gmp_license_info=License information openH264_name=OpenH264 Video Codec provided by Cisco Systems, Inc. -openH264_description=Play back web video and use video chats. +openH264_description2=This plugin is automatically installed by Mozilla to comply with the WebRTC specification and to enable WebRTC calls with devices that require the H.264 video codec. Visit http://www.openh264.org/ to view the codec source code and learn more about the implementation. eme-adobe_name=Primetime Content Decryption Module provided by Adobe Systems, Incorporated eme-adobe_description=Play back protected web video. diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm index 2bc7b778cc87..a21964d6a737 100644 --- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm +++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm @@ -52,7 +52,7 @@ const GMP_PLUGINS = [ { id: "gmp-gmpopenh264", name: "openH264_name", - description: "openH264_description", + description: "openH264_description2", // The following licenseURL is part of an awful hack to include the OpenH264 // license without having bug 624602 fixed yet, and intentionally ignores // localisation. From c0ede94dd61a142cf130461b5f12b4d7ebe735ee Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Wed, 18 Mar 2015 13:42:52 -0700 Subject: [PATCH 10/80] Bug 1143844 - Check document for readerable content to determine whether or not to show reader button. r=Gijs --HG-- extra : rebase_source : 7a849952243aa5ff8d4711a1f0308747558faa2f --- toolkit/components/reader/ReaderMode.jsm | 37 ++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/toolkit/components/reader/ReaderMode.jsm b/toolkit/components/reader/ReaderMode.jsm index dc1f01e37177..0ebbb80d26f4 100644 --- a/toolkit/components/reader/ReaderMode.jsm +++ b/toolkit/components/reader/ReaderMode.jsm @@ -63,14 +63,47 @@ this.ReaderMode = { /** * Decides whether or not a document is reader-able without parsing the whole thing. - * XXX: In the future, this should be smarter (bug 1143844). * * @param doc A document to parse. * @return boolean Whether or not we should show the reader mode button. */ isProbablyReaderable: function(doc) { let uri = Services.io.newURI(doc.documentURI, null, null); - return this._shouldCheckUri(uri); + + if (!this._shouldCheckUri(uri)) { + return false; + } + + let REGEXPS = { + unlikelyCandidates: /combx|comment|community|disqus|extra|foot|header|menu|remark|rss|shoutbox|sidebar|sponsor|ad-break|agegate|pagination|pager|popup|tweet|twitter/i, + okMaybeItsACandidate: /and|article|body|column|main|shadow/i, + }; + + let nodes = doc.getElementsByTagName("p"); + if (nodes.length < 5) { + return false; + } + + let possibleParagraphs = 0; + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i]; + let matchString = node.className + " " + node.id; + + if (REGEXPS.unlikelyCandidates.test(matchString) && + !REGEXPS.okMaybeItsACandidate.test(matchString)) { + continue; + } + + if (node.textContent.trim().length < 200) { + continue; + } + + possibleParagraphs++; + if (possibleParagraphs >= 5) { + return true; + } + } + return false; }, /** From a1c1a80388dc8f49029569611dfda19b8816cc92 Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Thu, 19 Mar 2015 13:17:01 -0700 Subject: [PATCH 11/80] Bug 1143844 - Update readerModeArticle.html test case to have more paragrpahs. r=Gijs --HG-- extra : rebase_source : 01d361f432ac4189274da01de61538a99761c225 --- browser/base/content/test/general/readerModeArticle.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/browser/base/content/test/general/readerModeArticle.html b/browser/base/content/test/general/readerModeArticle.html index f34cbece4e18..b533423ad66f 100644 --- a/browser/base/content/test/general/readerModeArticle.html +++ b/browser/base/content/test/general/readerModeArticle.html @@ -11,6 +11,9 @@

by Jane Doe

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.

Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.

+

Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.

+

Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.

+

Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.

From 4af71575a2cbbf0149c66d1aa8a3c3da0340bec2 Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Thu, 19 Mar 2015 11:44:07 -0700 Subject: [PATCH 12/80] Bug 1145259 - Add pageshow listener to check whether or not to show reader button when DOMContentLoaded doesn't fire. r=Gijs --HG-- extra : rebase_source : dc5b48a3fb01aa54d9c138deb1471370c35cd6c8 --- browser/base/content/content.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/browser/base/content/content.js b/browser/base/content/content.js index a82c4a1767b8..529be00a7d2d 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -486,6 +486,7 @@ let AboutReaderListener = { init: function() { addEventListener("AboutReaderContentLoaded", this, false, true); addEventListener("DOMContentLoaded", this, false); + addEventListener("pageshow", this, false); addEventListener("pagehide", this, false); addMessageListener("Reader:ParseDocument", this); }, @@ -525,6 +526,13 @@ let AboutReaderListener = { sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false }); break; + case "pageshow": + // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded" + // event, so we need to rely on "pageshow" in this case. + if (!aEvent.persisted) { + break; + } + // Fall through. case "DOMContentLoaded": if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader) { return; From 9fca7ec964778653955919f5d23df298119d1c19 Mon Sep 17 00:00:00 2001 From: Stephen Pohl Date: Thu, 19 Mar 2015 22:59:38 -0400 Subject: [PATCH 13/80] Bug 1140263: Enable Adobe EME on Windows Vista+ and add a pref to force-enable it on other platforms. r=mossop --- modules/libpref/init/all.js | 3 - toolkit/modules/GMPInstallManager.jsm | 109 ++-------- toolkit/modules/GMPUtils.jsm | 157 ++++++++++++++ toolkit/modules/moz.build | 1 + .../extensions/internal/GMPProvider.jsm | 202 +++++++----------- 5 files changed, 251 insertions(+), 221 deletions(-) create mode 100644 toolkit/modules/GMPUtils.jsm diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 57934ca0af65..062c5f04dd18 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4585,9 +4585,6 @@ pref("media.gmp-manager.certs.1.issuerName", "CN=DigiCert Secure Server CA,O=Dig pref("media.gmp-manager.certs.1.commonName", "aus4.mozilla.org"); pref("media.gmp-manager.certs.2.issuerName", "CN=Thawte SSL CA,O=\"Thawte, Inc.\",C=US"); pref("media.gmp-manager.certs.2.commonName", "aus4.mozilla.org"); - -// Adobe EME is currently pref'd off by default and hidden in the addon manager. -pref("media.gmp-eme-adobe.hidden", true); #endif // Whether or not to perform reader mode article parsing on page load. diff --git a/toolkit/modules/GMPInstallManager.jsm b/toolkit/modules/GMPInstallManager.jsm index d61bc1c46751..74edfe9c7657 100644 --- a/toolkit/modules/GMPInstallManager.jsm +++ b/toolkit/modules/GMPInstallManager.jsm @@ -15,15 +15,6 @@ const DOWNLOAD_INTERVAL = 0; // 1 day default const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24; -// Global pref to enable/disable all EME plugins -const EME_ENABLED = "media.eme.enabled"; - - -// GMP IDs -const OPEN_H264_ID = "gmp-gmpopenh264"; -const EME_ADOBE_ID = "gmp-eme-adobe"; -const GMP_ADDONS = [ OPEN_H264_ID, EME_ADOBE_ID ]; - Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); @@ -33,9 +24,10 @@ Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/ctypes.jsm"); +Cu.import("resource://gre/modules/GMPUtils.jsm"); this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader", - "GMPAddon", "GMPPrefs", "OPEN_H264_ID"]; + "GMPAddon"]; var gLocale = null; @@ -65,68 +57,6 @@ function getScopedLogger(prefix) { return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " "); } -/** - * Manages preferences for GMP addons - */ -let GMPPrefs = { - /** - * Obtains the specified preference in relation to the specified addon - * @param key The GMPPrefs key value to use - * @param defaultValue The default value if no preference exists - * @param addon The addon to scope the preference to - * @return The obtained preference value, or the defaultValue if none exists - */ - get: function(key, defaultValue, addon) { - if (key === GMPPrefs.KEY_APP_DISTRIBUTION || - key === GMPPrefs.KEY_APP_DISTRIBUTION_VERSION) { - let prefValue = "default"; - try { - prefValue = Services.prefs.getDefaultBranch(null).getCharPref(key); - } catch (e) { - // use default when pref not found - } - return prefValue; - } - - return Preferences.get(this._getPrefKey(key, addon), defaultValue); - }, - /** - * Sets the specified preference in relation to the specified addon - * @param key The GMPPrefs key value to use - * @param val The value to set - * @param addon The addon to scope the preference to - */ - set: function(key, val, addon) { - let log = getScopedLogger("GMPInstallManager.jsm GMPPrefs.set"); - log.info("Setting pref: " + this._getPrefKey(key, addon) + - " to value: " + val); - return Preferences.set(this._getPrefKey(key, addon), val); - }, - _getPrefKey: function(key, addon) { - return key.replace("{0}", addon || ""); - }, - - /** - * List of keys which can be used in get and set - */ - KEY_ADDON_ENABLED: "media.{0}.enabled", - KEY_ADDON_LAST_UPDATE: "media.{0}.lastUpdate", - KEY_ADDON_VERSION: "media.{0}.version", - KEY_ADDON_AUTOUPDATE: "media.{0}.autoupdate", - KEY_ADDON_HIDDEN: "media.{0}.hidden", - KEY_URL: "media.gmp-manager.url", - KEY_URL_OVERRIDE: "media.gmp-manager.url.override", - KEY_CERT_CHECKATTRS: "media.gmp-manager.cert.checkAttributes", - KEY_CERT_REQUIREBUILTIN: "media.gmp-manager.cert.requireBuiltIn", - KEY_UPDATE_LAST_CHECK: "media.gmp-manager.lastCheck", - KEY_UPDATE_SECONDS_BETWEEN_CHECKS: "media.gmp-manager.secondsBetweenChecks", - KEY_APP_DISTRIBUTION: "distribution.id", - KEY_APP_DISTRIBUTION_VERSION: "distribution.version", - KEY_BUILDID: "media.gmp-manager.buildID", - - CERTS_BRANCH: "media.gmp-manager.certs." -}; - // This is copied directly from nsUpdateService.js // It is used for calculating the URL string w/ var replacement. // TODO: refactor this out somewhere else @@ -417,11 +347,11 @@ GMPInstallManager.prototype = { return now - lastCheck; }, get _isEMEEnabled() { - return Preferences.get(EME_ENABLED, true); + return GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true); }, _isAddonUpdateEnabled: function(aAddon) { - return GMPPrefs.get(GMPPrefs.KEY_ADDON_ENABLED, true, aAddon) && - GMPPrefs.get(GMPPrefs.KEY_ADDON_AUTOUPDATE, true, aAddon); + return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon) && + GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon); }, _updateLastCheck: function() { let now = Math.round(Date.now() / 1000); @@ -452,7 +382,7 @@ GMPInstallManager.prototype = { "new or updated GMPs."); } else { let secondsBetweenChecks = - GMPPrefs.get(GMPPrefs.KEY_UPDATE_SECONDS_BETWEEN_CHECKS, + GMPPrefs.get(GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS, DEFAULT_SECONDS_BETWEEN_CHECKS) let secondsSinceLast = this._getTimeSinceLastCheck(); log.info("Last check was: " + secondsSinceLast + @@ -470,19 +400,14 @@ GMPInstallManager.prototype = { let addonsToInstall = gmpAddons.filter(function(gmpAddon) { log.info("Found addon: " + gmpAddon.toString()); - if (gmpAddon.isHidden || !gmpAddon.isValid || gmpAddon.isInstalled) { - log.info("Addon hidden, invalid or already installed."); - return false; - } - - // We're dealing with an EME GMP if the id starts with "gmp-eme-". - if (gmpAddon.id.indexOf("gmp-eme-") == 0 && !this._isEMEEnabled) { - log.info("Auto-update is off for all EME plugins, skipping check."); + if (!gmpAddon.isValid || GMPUtils.isPluginHidden(gmpAddon) || + gmpAddon.isInstalled) { + log.info("Addon invalid, hidden or already installed."); return false; } let addonUpdateEnabled = false; - if (GMP_ADDONS.indexOf(gmpAddon.id) >= 0) { + if (GMP_PLUGIN_IDS.indexOf(gmpAddon.id) >= 0) { addonUpdateEnabled = this._isAddonUpdateEnabled(gmpAddon.id); if (!addonUpdateEnabled) { log.info("Auto-update is off for " + gmpAddon.id + @@ -564,7 +489,7 @@ GMPInstallManager.prototype = { let certs = null; if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) && GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, true)) { - certs = gCertUtils.readCertPrefs(GMPPrefs.CERTS_BRANCH); + certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH); } let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN, @@ -751,11 +676,11 @@ GMPAddon.prototype = { }, get isInstalled() { return this.version && - GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, "", this.id) === this.version; + GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) === this.version; + }, + get isEME() { + return this.id.indexOf("gmp-eme-") == 0; }, - get isHidden() { - return GMPPrefs.get(GMPPrefs.KEY_ADDON_HIDDEN, false, this.id); - } }; /** * Constructs a GMPExtractor object which is used to extract a GMP zip @@ -959,10 +884,10 @@ GMPDownloader.prototype = { installPromise.then(extractedPaths => { // Success, set the prefs let now = Math.round(Date.now() / 1000); - GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, now, gmpAddon.id); + GMPPrefs.set(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id); // Setting the version pref signals installation completion to consumers, // if you need to set other prefs etc. do it before this. - GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, gmpAddon.version, + GMPPrefs.set(GMPPrefs.KEY_PLUGIN_VERSION, gmpAddon.version, gmpAddon.id); this._deferred.resolve(extractedPaths); }, err => { diff --git a/toolkit/modules/GMPUtils.jsm b/toolkit/modules/GMPUtils.jsm new file mode 100644 index 000000000000..f4e6caaade7c --- /dev/null +++ b/toolkit/modules/GMPUtils.jsm @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = + Components; + +this.EXPORTED_SYMBOLS = [ "EME_ADOBE_ID", + "GMP_PLUGIN_IDS", + "GMPPrefs", + "GMPUtils", + "OPEN_H264_ID" ]; + +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// GMP IDs +const OPEN_H264_ID = "gmp-gmpopenh264"; +const EME_ADOBE_ID = "gmp-eme-adobe"; +const GMP_PLUGIN_IDS = [ OPEN_H264_ID, EME_ADOBE_ID ]; + +this.GMPUtils = { + /** + * Checks whether or not a given plugin is hidden. Hidden plugins are neither + * downloaded nor displayed in the addons manager. + * @param aPlugin + * The plugin to check. + */ + isPluginHidden: function(aPlugin) { + if (aPlugin.isEME) { + if (this._isPluginSupported(aPlugin) || + this._isPluginForcedVisible(aPlugin)) { + return !GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true); + } else { + return true; + } + } + return false; + }, + + /** + * Checks whether or not a given plugin is supported by the current OS. + * @param aPlugin + * The plugin to check. + */ + _isPluginSupported: function(aPlugin) { + if (aPlugin.id == EME_ADOBE_ID) { + if (Services.appinfo.OS == "WINNT") { + return Services.sysinfo.getPropertyAsInt32("version") >= 6; + } else { + return false; + } + } + return true; + }, + + /** + * Checks whether or not a given plugin is forced visible. This can be used + * to test plugins that aren't yet supported by default on a particular OS. + * @param aPlugin + * The plugin to check. + */ + _isPluginForcedVisible: function(aPlugin) { + return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_FORCEVISIBLE, false, aPlugin.id); + }, +}; + +/** + * Manages preferences for GMP addons + */ +this.GMPPrefs = { + KEY_EME_ENABLED: "media.eme.enabled", + KEY_PLUGIN_ENABLED: "media.{0}.enabled", + KEY_PLUGIN_LAST_UPDATE: "media.{0}.lastUpdate", + KEY_PLUGIN_VERSION: "media.{0}.version", + KEY_PLUGIN_AUTOUPDATE: "media.{0}.autoupdate", + KEY_PLUGIN_FORCEVISIBLE: "media.{0}.forcevisible", + KEY_URL: "media.gmp-manager.url", + KEY_URL_OVERRIDE: "media.gmp-manager.url.override", + KEY_CERT_CHECKATTRS: "media.gmp-manager.cert.checkAttributes", + KEY_CERT_REQUIREBUILTIN: "media.gmp-manager.cert.requireBuiltIn", + KEY_UPDATE_LAST_CHECK: "media.gmp-manager.lastCheck", + KEY_SECONDS_BETWEEN_CHECKS: "media.gmp-manager.secondsBetweenChecks", + KEY_APP_DISTRIBUTION: "distribution.id", + KEY_APP_DISTRIBUTION_VERSION: "distribution.version", + KEY_BUILDID: "media.gmp-manager.buildID", + KEY_CERTS_BRANCH: "media.gmp-manager.certs.", + KEY_PROVIDER_ENABLED: "media.gmp-provider.enabled", + KEY_PROVIDER_LASTCHECK: "media.gmp-manager.lastCheck", + KEY_LOG_BASE: "media.gmp.log.", + KEY_LOGGING_LEVEL: this.KEY_LOG_BASE + "level", + KEY_LOGGING_DUMP: this.KEY_LOG_BASE + "dump", + + /** + * Obtains the specified preference in relation to the specified plugin. + * @param aKey The preference key value to use. + * @param aDefaultValue The default value if no preference exists. + * @param aPlugin The plugin to scope the preference to. + * @return The obtained preference value, or the defaultValue if none exists. + */ + get: function(aKey, aDefaultValue, aPlugin) { + if (aKey === this.KEY_APP_DISTRIBUTION || + aKey === this.KEY_APP_DISTRIBUTION_VERSION) { + let prefValue = "default"; + try { + prefValue = Services.prefs.getDefaultBranch(null).getCharPref(aKey); + } catch (e) { + // use default when pref not found + } + return prefValue; + } + return Preferences.get(this.getPrefKey(aKey, aPlugin), aDefaultValue); + }, + + /** + * Sets the specified preference in relation to the specified plugin. + * @param aKey The preference key value to use. + * @param aVal The value to set. + * @param aPlugin The plugin to scope the preference to. + */ + set: function(aKey, aVal, aPlugin) { + Preferences.set(this.getPrefKey(aKey, aPlugin), aVal); + }, + + /** + * Checks whether or not the specified preference is set in relation to the + * specified plugin. + * @param aKey The preference key value to use. + * @param aPlugin The plugin to scope the preference to. + * @return true if the preference is set, false otherwise. + */ + isSet: function(aKey, aPlugin) { + return Preferences.isSet(this.getPrefKey(aKey, aPlugin)); + }, + + /** + * Resets the specified preference in relation to the specified plugin to its + * default. + * @param aKey The preference key value to use. + * @param aPlugin The plugin to scope the preference to. + */ + reset: function(aKey, aPlugin) { + Preferences.reset(this.getPrefKey(aKey, aPlugin)); + }, + + /** + * Scopes the specified preference key to the specified plugin. + * @param aKey The preference key value to use. + * @param aPlugin The plugin to scope the preference to. + * @return A preference key scoped to the specified plugin. + */ + getPrefKey: function(aKey, aPlugin) { + return aKey.replace("{0}", aPlugin || ""); + }, +}; \ No newline at end of file diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index b84c4784f4ee..23c1d9e5185a 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -67,6 +67,7 @@ EXTRA_JS_MODULES += [ EXTRA_PP_JS_MODULES += [ 'CertUtils.jsm', 'GMPInstallManager.jsm', + 'GMPUtils.jsm', 'ResetProfile.jsm', 'secondscreen/RokuApp.jsm', 'Services.jsm', diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm index a21964d6a737..e7dd1688631d 100644 --- a/toolkit/mozapps/extensions/internal/GMPProvider.jsm +++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm @@ -17,6 +17,7 @@ Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/GMPUtils.jsm"); XPCOMUtils.defineLazyModuleGetter( this, "GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"); @@ -30,27 +31,12 @@ const NS_GRE_DIR = "GreD"; const CLEARKEY_PLUGIN_ID = "gmp-clearkey"; const CLEARKEY_VERSION = "0.1"; -/** - * Keys which can be used via GMPPrefs. - */ -const KEY_PROVIDER_ENABLED = "media.gmp-provider.enabled"; -const KEY_PROVIDER_LASTCHECK = "media.gmp-manager.lastCheck"; -const KEY_LOG_BASE = "media.gmp.log."; -const KEY_LOGGING_LEVEL = KEY_LOG_BASE + "level"; -const KEY_LOGGING_DUMP = KEY_LOG_BASE + "dump"; -const KEY_EME_ENABLED = "media.eme.enabled"; // Global pref to enable/disable all EME plugins -const KEY_PLUGIN_ENABLED = "media.{0}.enabled"; -const KEY_PLUGIN_LAST_UPDATE = "media.{0}.lastUpdate"; -const KEY_PLUGIN_VERSION = "media.{0}.version"; -const KEY_PLUGIN_AUTOUPDATE = "media.{0}.autoupdate"; -const KEY_PLUGIN_HIDDEN = "media.{0}.hidden"; - const GMP_LICENSE_INFO = "gmp_license_info"; const GMP_LEARN_MORE = "learn_more_label"; const GMP_PLUGINS = [ { - id: "gmp-gmpopenh264", + id: OPEN_H264_ID, name: "openH264_name", description: "openH264_description2", // The following licenseURL is part of an awful hack to include the OpenH264 @@ -61,7 +47,7 @@ const GMP_PLUGINS = [ optionsURL: "chrome://mozapps/content/extensions/gmpPrefs.xul" }, { - id: "gmp-eme-adobe", + id: EME_ADOBE_ID, name: "eme-adobe_name", description: "eme-adobe_description", // The following learnMoreURL is another hack to be able to support a SUMO page for this @@ -88,9 +74,9 @@ function configureLogging() { gLogger = Log.repository.getLogger("Toolkit.GMP"); gLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); } - gLogger.level = GMPPrefs.get(KEY_LOGGING_LEVEL, Log.Level.Warn); + gLogger.level = GMPPrefs.get(GMPPrefs.KEY_LOGGING_LEVEL, Log.Level.Warn); - let logDumping = GMPPrefs.get(KEY_LOGGING_DUMP, false); + let logDumping = GMPPrefs.get(GMPPrefs.KEY_LOGGING_DUMP, false); if (logDumping != !!gLogAppenderDump) { if (logDumping) { gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter()); @@ -102,64 +88,7 @@ function configureLogging() { } } -/** - * Manages preferences for GMP addons - */ -let GMPPrefs = { - /** - * Obtains the specified preference in relation to the specified plugin. - * @param aKey The preference key value to use. - * @param aDefaultValue The default value if no preference exists. - * @param aPlugin The plugin to scope the preference to. - * @return The obtained preference value, or the defaultValue if none exists. - */ - get: function(aKey, aDefaultValue, aPlugin) { - return Preferences.get(this.getPrefKey(aKey, aPlugin), aDefaultValue); - }, - /** - * Sets the specified preference in relation to the specified plugin. - * @param aKey The preference key value to use. - * @param aVal The value to set. - * @param aPlugin The plugin to scope the preference to. - */ - set: function(aKey, aVal, aPlugin) { - let log = - Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", - "GMPProvider.jsm " + - "GMPPrefs.set "); - log.trace("Setting pref: " + this.getPrefKey(aKey, aPlugin) + - " to value: " + aVal); - Preferences.set(this.getPrefKey(aKey, aPlugin), aVal); - }, - /** - * Checks whether or not the specified preference is set in relation to the - * specified plugin. - * @param aKey The preference key value to use. - * @param aPlugin The plugin to scope the preference to. - * @return true if the preference is set, false otherwise. - */ - isSet: function(aKey, aPlugin) { - return Preferences.isSet(GMPPrefs.getPrefKey(aKey, aPlugin)); - }, - /** - * Resets the specified preference in relation to the specified plugin to its - * default. - * @param aKey The preference key value to use. - * @param aPlugin The plugin to scope the preference to. - */ - reset: function(aKey, aPlugin) { - Preferences.reset(this.getPrefKey(aKey, aPlugin)); - }, - /** - * Scopes the specified preference key to the specified plugin. - * @param aKey The preference key value to use. - * @param aPlugin The plugin to scope the preference to. - * @return A preference key scoped to the specified plugin. - */ - getPrefKey: function(aKey, aPlugin) { - return aKey.replace("{0}", aPlugin || ""); - }, -}; + /** * The GMPWrapper provides the info for the various GMP plugins to public @@ -171,13 +100,15 @@ function GMPWrapper(aPluginInfo) { Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", "GMPWrapper(" + this._plugin.id + ") "); - Preferences.observe(GMPPrefs.getPrefKey(KEY_PLUGIN_ENABLED, this._plugin.id), + Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED, + this._plugin.id), this.onPrefEnabledChanged, this); - Preferences.observe(GMPPrefs.getPrefKey(KEY_PLUGIN_VERSION, this._plugin.id), + Preferences.observe(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION, + this._plugin.id), this.onPrefVersionChanged, this); if (this._plugin.isEME) { - Preferences.observe(KEY_EME_ENABLED, this.onPrefEMEGlobalEnabledChanged, - this); + Preferences.observe(GMPPrefs.KEY_EME_ENABLED, + this.onPrefEMEGlobalEnabledChanged, this); } } @@ -194,8 +125,8 @@ GMPWrapper.prototype = { if (!this._gmpPath && this.isInstalled) { this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir, this._plugin.id, - GMPPrefs.get(KEY_PLUGIN_VERSION, null, - this._plugin.id)); + GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, + null, this._plugin.id)); } return this._gmpPath; }, @@ -210,12 +141,12 @@ GMPWrapper.prototype = { get description() { return this._plugin.description; }, get fullDescription() { return this._plugin.fullDescription; }, - get version() { return GMPPrefs.get(KEY_PLUGIN_VERSION, null, + get version() { return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, null, this._plugin.id); }, get isActive() { return !this.appDisabled && !this.userDisabled; }, get appDisabled() { - if (this._plugin.isEME && !GMPPrefs.get(KEY_EME_ENABLED, true)) { + if (this._plugin.isEME && !GMPPrefs.get(GMPPrefs.KEY_EME_ENABLED, true)) { // If "media.eme.enabled" is false, all EME plugins are disabled. return true; } @@ -223,9 +154,10 @@ GMPWrapper.prototype = { }, get userDisabled() { - return !GMPPrefs.get(KEY_PLUGIN_ENABLED, true, this._plugin.id); + return !GMPPrefs.get(GMPPrefs.KEY_PLUGIN_ENABLED, true, this._plugin.id); }, - set userDisabled(aVal) { GMPPrefs.set(KEY_PLUGIN_ENABLED, aVal === false, + set userDisabled(aVal) { GMPPrefs.set(GMPPrefs.KEY_PLUGIN_ENABLED, + aVal === false, this._plugin.id); }, get blocklistState() { return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; }, @@ -246,7 +178,7 @@ GMPWrapper.prototype = { }, get updateDate() { - let time = Number(GMPPrefs.get(KEY_PLUGIN_LAST_UPDATE, null, + let time = Number(GMPPrefs.get(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, null, this._plugin.id)); if (time !== NaN && this.isInstalled) { return new Date(time * 1000) @@ -275,21 +207,21 @@ GMPWrapper.prototype = { }, get applyBackgroundUpdates() { - if (!GMPPrefs.isSet(KEY_PLUGIN_AUTOUPDATE, this._plugin.id)) { + if (!GMPPrefs.isSet(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id)) { return AddonManager.AUTOUPDATE_DEFAULT; } - return GMPPrefs.get(KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id) ? + return GMPPrefs.get(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id) ? AddonManager.AUTOUPDATE_ENABLE : AddonManager.AUTOUPDATE_DISABLE; }, set applyBackgroundUpdates(aVal) { if (aVal == AddonManager.AUTOUPDATE_DEFAULT) { - GMPPrefs.reset(KEY_PLUGIN_AUTOUPDATE, this._plugin.id); + GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id); } else if (aVal == AddonManager.AUTOUPDATE_ENABLE) { - GMPPrefs.set(KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id); + GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id); } else if (aVal == AddonManager.AUTOUPDATE_DISABLE) { - GMPPrefs.set(KEY_PLUGIN_AUTOUPDATE, false, this._plugin.id); + GMPPrefs.set(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, this._plugin.id); } }, @@ -307,7 +239,7 @@ GMPWrapper.prototype = { } let secSinceLastCheck = - Date.now() / 1000 - Preferences.get(KEY_PROVIDER_LASTCHECK, 0); + Date.now() / 1000 - Preferences.get(GMPPrefs.KEY_PROVIDER_LASTCHECK, 0); if (secSinceLastCheck <= SEC_IN_A_DAY) { this._log.trace("findUpdates() - " + this._plugin.id + " - last check was less then a day ago"); @@ -400,6 +332,25 @@ GMPWrapper.prototype = { onPrefEMEGlobalEnabledChanged: function() { AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["appDisabled"]); + if (this.appDisabled) { + AddonManagerPrivate.callAddonListeners("onUninstalling", this, false); + if (this._gmpPath) { + this._log.info("onPrefEMEGlobalEnabledChanged() - unregistering gmp " + + "directory " + this._gmpPath); + gmpService.removePluginDirectory(this._gmpPath); + } + AddonManagerPrivate.callAddonListeners("onUninstalled", this); + } else { + AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this, + null, false); + AddonManagerPrivate.callAddonListeners("onInstalling", this, false); + if (this._gmpPath && this.isActive) { + this._log.info("onPrefEMEGlobalEnabledChanged() - registering gmp " + + "directory " + this._gmpPath); + gmpService.addPluginDirectory(this._gmpPath); + } + AddonManagerPrivate.callAddonListeners("onInstalled", this); + } if (!this.userDisabled) { this._handleEnabledChanged(); } @@ -422,12 +373,13 @@ GMPWrapper.prototype = { AddonManagerPrivate.callInstallListeners("onExternalInstall", null, this, null, false); + AddonManagerPrivate.callAddonListeners("onInstalling", this, false); this._gmpPath = null; if (this.isInstalled) { this._gmpPath = OS.Path.join(OS.Constants.Path.profileDir, this._plugin.id, - GMPPrefs.get(KEY_PLUGIN_VERSION, null, - this._plugin.id)); + GMPPrefs.get(GMPPrefs.KEY_PLUGIN_VERSION, + null, this._plugin.id)); } if (this._gmpPath && this.isActive) { this._log.info("onPrefVersionChanged() - registering gmp directory " + @@ -438,13 +390,15 @@ GMPWrapper.prototype = { }, shutdown: function() { - Preferences.ignore(GMPPrefs.getPrefKey(KEY_PLUGIN_ENABLED, this._plugin.id), + Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED, + this._plugin.id), this.onPrefEnabledChanged, this); - Preferences.ignore(GMPPrefs.getPrefKey(KEY_PLUGIN_VERSION, this._plugin.id), + Preferences.ignore(GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION, + this._plugin.id), this.onPrefVersionChanged, this); if (this._plugin.isEME) { - Preferences.ignore(KEY_EME_ENABLED, this.onPrefEMEGlobalEnabledChanged, - this); + Preferences.ignore(GMPPrefs.KEY_EME_ENABLED, + this.onPrefEMEGlobalEnabledChanged, this); } return this._updateTask; }, @@ -462,7 +416,7 @@ let GMPProvider = { let telemetry = {}; this.buildPluginList(); - Preferences.observe(KEY_LOG_BASE, configureLogging); + Preferences.observe(GMPPrefs.KEY_LOG_BASE, configureLogging); for (let [id, plugin] of this._plugins) { let wrapper = plugin.wrapper; @@ -490,7 +444,7 @@ let GMPProvider = { } } - if (Preferences.get(KEY_EME_ENABLED, false)) { + if (Preferences.get(GMPPrefs.KEY_EME_ENABLED, false)) { try { let greDir = Services.dirsvc.get(NS_GRE_DIR, Ci.nsILocalFile); @@ -510,7 +464,7 @@ let GMPProvider = { shutdown: function() { this._log.trace("shutdown"); - Preferences.ignore(KEY_LOG_BASE, configureLogging); + Preferences.ignore(GMPPrefs.KEY_LOG_BASE, configureLogging); let shutdownTask = Task.spawn(function* GMPProvider_shutdownTask() { this._log.trace("shutdown - shutdownTask"); @@ -541,7 +495,7 @@ let GMPProvider = { } let plugin = this._plugins.get(aId); - if (plugin) { + if (plugin && !GMPUtils.isPluginHidden(plugin)) { aCallback(plugin.wrapper); } else { aCallback(null); @@ -555,12 +509,13 @@ let GMPProvider = { return; } - let results = [p.wrapper for ([id, p] of this._plugins)]; + let results = [p.wrapper for ([id, p] of this._plugins) + if (!GMPUtils.isPluginHidden(p))]; aCallback(results); }, get isEnabled() { - return GMPPrefs.get(KEY_PROVIDER_ENABLED, false); + return GMPPrefs.get(GMPPrefs.KEY_PROVIDER_ENABLED, false); }, generateFullDescription: function(aPlugin) { @@ -576,26 +531,21 @@ let GMPProvider = { }, buildPluginList: function() { - - let map = new Map(); - GMP_PLUGINS.forEach(aPlugin => { - // Only show GMPs in addon manager that aren't hidden. - if (!GMPPrefs.get(KEY_PLUGIN_HIDDEN, false, aPlugin.id)) { - let plugin = { - id: aPlugin.id, - name: pluginsBundle.GetStringFromName(aPlugin.name), - description: pluginsBundle.GetStringFromName(aPlugin.description), - homepageURL: aPlugin.homepageURL, - optionsURL: aPlugin.optionsURL, - wrapper: null, - isEME: aPlugin.isEME - }; - plugin.fullDescription = this.generateFullDescription(aPlugin); - plugin.wrapper = new GMPWrapper(plugin); - map.set(plugin.id, plugin); - } - }, this); - this._plugins = map; + this._plugins = new Map(); + for (let aPlugin of GMP_PLUGINS) { + let plugin = { + id: aPlugin.id, + name: pluginsBundle.GetStringFromName(aPlugin.name), + description: pluginsBundle.GetStringFromName(aPlugin.description), + homepageURL: aPlugin.homepageURL, + optionsURL: aPlugin.optionsURL, + wrapper: null, + isEME: aPlugin.isEME, + }; + plugin.fullDescription = this.generateFullDescription(aPlugin); + plugin.wrapper = new GMPWrapper(plugin); + this._plugins.set(plugin.id, plugin); + } }, }; From 90c1248342ef99f30b04f268bc2eb7a1bd62ac50 Mon Sep 17 00:00:00 2001 From: Stephen Pohl Date: Thu, 19 Mar 2015 22:59:45 -0400 Subject: [PATCH 14/80] Bug 1140263: Test updates. r=mossop --- .../tests/xpcshell/test_GMPInstallManager.js | 59 +++---- .../test/browser/browser_gmpProvider.js | 144 +++++++----------- .../test/xpcshell/test_gmpProvider.js | 51 ++++--- 3 files changed, 111 insertions(+), 143 deletions(-) diff --git a/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js index 1e257d78fcc2..b0938e0847d2 100644 --- a/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js +++ b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js @@ -4,7 +4,7 @@ const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components; const URL_HOST = "http://localhost"; -Cu.import("resource://gre/modules/GMPInstallManager.jsm"); +let GMPScope = Cu.import("resource://gre/modules/GMPInstallManager.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); @@ -26,26 +26,26 @@ function run_test() {Cu.import("resource://gre/modules/Preferences.jsm") add_task(function* test_prefs() { let addon1 = "addon1", addon2 = "addon2"; - GMPPrefs.set(GMPPrefs.KEY_URL, "http://not-really-used"); - GMPPrefs.set(GMPPrefs.KEY_URL_OVERRIDE, "http://not-really-used-2"); - GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, "1", addon1); - GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, "2", addon1); - GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, "3", addon2); - GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, "4", addon2); - GMPPrefs.set(GMPPrefs.KEY_ADDON_AUTOUPDATE, false, addon2); - GMPPrefs.set(GMPPrefs.KEY_CERT_CHECKATTRS, true); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_URL, "http://not-really-used"); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, "http://not-really-used-2"); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "1", addon1); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "2", addon1); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "3", addon2); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "4", addon2); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, addon2); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_CERT_CHECKATTRS, true); - do_check_eq(GMPPrefs.get(GMPPrefs.KEY_URL), "http://not-really-used"); - do_check_eq(GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE), + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_URL), "http://not-really-used"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_URL_OVERRIDE), "http://not-really-used-2"); - do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE, "", addon1), "1"); - do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, "", addon1), "2"); - do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE, "", addon2), "3"); - do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, "", addon2), "4"); - do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_AUTOUPDATE, undefined, addon2), + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon1), "1"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", addon1), "2"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon2), "3"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", addon2), "4"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, undefined, addon2), false); - do_check_true(GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS)); - GMPPrefs.set(GMPPrefs.KEY_ADDON_AUTOUPDATE, true, addon2); + do_check_true(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_CERT_CHECKATTRS)); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, addon2); }); /** @@ -184,10 +184,10 @@ add_test(function test_checkForAddons_bad_ssl() { // Add random stuff that cause CertUtil to require https. // let PREF_KEY_URL_OVERRIDE_BACKUP = - Preferences.get(GMPPrefs.KEY_URL_OVERRIDE, undefined); - Preferences.reset(GMPPrefs.KEY_URL_OVERRIDE); + Preferences.get(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, undefined); + Preferences.reset(GMPScope.GMPPrefs.KEY_URL_OVERRIDE); - let CERTS_BRANCH_DOT_ONE = GMPPrefs.CERTS_BRANCH + ".1"; + let CERTS_BRANCH_DOT_ONE = GMPScope.GMPPrefs.KEY_CERTS_BRANCH + ".1"; let PREF_CERTS_BRANCH_DOT_ONE_BACKUP = Preferences.get(CERTS_BRANCH_DOT_ONE, undefined); Services.prefs.setCharPref(CERTS_BRANCH_DOT_ONE, "funky value"); @@ -203,7 +203,7 @@ add_test(function test_checkForAddons_bad_ssl() { "not https.")); installManager.uninit(); if (PREF_KEY_URL_OVERRIDE_BACKUP) { - Preferences.set(GMPPrefs.KEY_URL_OVERRIDE, + Preferences.set(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, PREF_KEY_URL_OVERRIDE_BACKUP); } if (PREF_CERTS_BRANCH_DOT_ONE_BACKUP) { @@ -483,10 +483,11 @@ function* test_checkForAddons_installAddon(id, includeSize, wantInstallReject) { do_check_true(compareBinaryData(downloadedBytes, sourceBytes)); // Make sure the prefs are set correctly - do_check_true(!!GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE, "", - gmpAddon.id)); - do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, "", gmpAddon.id), - "1.1"); + do_check_true(!!GMPScope.GMPPrefs.get( + GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", gmpAddon.id)); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", + gmpAddon.id), + "1.1"); // Make sure it reports as being installed do_check_true(gmpAddon.isInstalled); @@ -518,7 +519,7 @@ add_task(test_checkForAddons_installAddon.bind(null, "3", true, true)); * Tests simpleCheckAndInstall when autoupdate is disabled for a GMP */ add_task(function* test_simpleCheckAndInstall_autoUpdateDisabled() { - GMPPrefs.set(GMPPrefs.KEY_ADDON_AUTOUPDATE, false, OPEN_H264_ID); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, GMPScope.OPEN_H264_ID); let responseXML = "" + "" + @@ -536,8 +537,8 @@ add_task(function* test_simpleCheckAndInstall_autoUpdateDisabled() { let installManager = new GMPInstallManager(); let result = yield installManager.simpleCheckAndInstall(); do_check_eq(result.status, "nothing-new-to-install"); - Preferences.reset(GMPPrefs.KEY_UPDATE_LAST_CHECK); - GMPPrefs.set(GMPPrefs.KEY_ADDON_AUTOUPDATE, true, OPEN_H264_ID); + Preferences.reset(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, GMPScope.OPEN_H264_ID); }); /** diff --git a/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js index 550ea938cdb5..112014aefdc6 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js +++ b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js @@ -15,18 +15,15 @@ let gCategoryUtilities; let gIsEnUsLocale; let gMockAddons = []; -let gMockEmeAddons = []; for (let plugin of GMPScope.GMP_PLUGINS) { let mockAddon = Object.freeze({ id: plugin.id, isValid: true, isInstalled: false, + isEME: plugin.id.indexOf("gmp-eme-") == 0 ? true : false, }); gMockAddons.push(mockAddon); - if (mockAddon.id.indexOf("gmp-eme-") == 0) { - gMockEmeAddons.push(mockAddon); - } } let gInstalledAddonId = ""; @@ -47,7 +44,6 @@ MockGMPInstallManager.prototype = { }, }; - let gOptionsObserver = { lastDisplayed: null, observe: function(aSubject, aTopic, aData) { @@ -86,8 +82,8 @@ function openDetailsView(aId) { } add_task(function* initializeState() { - gPrefs.setBoolPref(GMPScope.KEY_LOGGING_DUMP, true); - gPrefs.setIntPref(GMPScope.KEY_LOGGING_LEVEL, 0); + gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_LOGGING_DUMP, true); + gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_LOGGING_LEVEL, 0); gManagerWindow = yield open_manager(); gCategoryUtilities = new CategoryUtilities(gManagerWindow); @@ -96,16 +92,16 @@ add_task(function* initializeState() { Services.obs.removeObserver(gOptionsObserver, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED); for (let addon of gMockAddons) { - gPrefs.clearUserPref(getKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id)); - gPrefs.clearUserPref(getKey(GMPScope.KEY_PLUGIN_LAST_UPDATE, addon.id)); - gPrefs.clearUserPref(getKey(GMPScope.KEY_PLUGIN_AUTOUPDATE, addon.id)); - gPrefs.clearUserPref(getKey(GMPScope.KEY_PLUGIN_VERSION, addon.id)); - gPrefs.clearUserPref(getKey(GMPScope.KEY_PLUGIN_HIDDEN, addon.id)); + gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id)); + gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id)); + gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id)); + gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id)); + gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCEVISIBLE, addon.id)); } - gPrefs.clearUserPref(GMPScope.KEY_LOGGING_DUMP); - gPrefs.clearUserPref(GMPScope.KEY_LOGGING_LEVEL); - gPrefs.clearUserPref(GMPScope.KEY_PROVIDER_LASTCHECK); - gPrefs.clearUserPref(GMPScope.KEY_EME_ENABLED); + gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_LOGGING_DUMP); + gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_LOGGING_LEVEL); + gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_PROVIDER_LASTCHECK); + gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_EME_ENABLED); yield GMPScope.GMPProvider.shutdown(); GMPScope.GMPProvider.startup(); })); @@ -117,13 +113,14 @@ add_task(function* initializeState() { // Start out with plugins not being installed, disabled and automatic updates // disabled. - gPrefs.setBoolPref(GMPScope.KEY_EME_ENABLED, true); + gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, true); for (let addon of gMockAddons) { - gPrefs.setBoolPref(getKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id), false); - gPrefs.setIntPref(getKey(GMPScope.KEY_PLUGIN_LAST_UPDATE, addon.id), 0); - gPrefs.setBoolPref(getKey(GMPScope.KEY_PLUGIN_AUTOUPDATE, addon.id), false); - gPrefs.setCharPref(getKey(GMPScope.KEY_PLUGIN_VERSION, addon.id), ""); - gPrefs.setBoolPref(getKey(GMPScope.KEY_PLUGIN_HIDDEN, addon.id), false); + gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false); + gPrefs.setIntPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id), 0); + gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id), false); + gPrefs.setCharPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), ""); + gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCEVISIBLE, addon.id), + true); } yield GMPScope.GMPProvider.shutdown(); GMPScope.GMPProvider.startup(); @@ -176,7 +173,7 @@ add_task(function* testNotInstalled() { yield gCategoryUtilities.openType("plugin"); for (let addon of gMockAddons) { - gPrefs.setBoolPref(getKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id), true); + gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true); let item = get_addon_element(gManagerWindow, addon.id); Assert.ok(item, "Got add-on element:" + addon.id); item.parentNode.ensureElementIsVisible(item); @@ -216,10 +213,10 @@ add_task(function* testNotInstalledDetails() { add_task(function* testInstalled() { for (let addon of gMockAddons) { - gPrefs.setIntPref(getKey(GMPScope.KEY_PLUGIN_LAST_UPDATE, addon.id), + gPrefs.setIntPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id), TEST_DATE.getTime()); - gPrefs.setBoolPref(getKey(GMPScope.KEY_PLUGIN_AUTOUPDATE, addon.id), false); - gPrefs.setCharPref(getKey(GMPScope.KEY_PLUGIN_VERSION, addon.id), "1.2.3.4"); + gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id), false); + gPrefs.setCharPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), "1.2.3.4"); yield gCategoryUtilities.openType("plugin"); @@ -276,63 +273,18 @@ add_task(function* testInstalledDetails() { }); add_task(function* testInstalledGlobalEmeDisabled() { - gPrefs.setBoolPref(GMPScope.KEY_EME_ENABLED, false); - for (let addon of gMockEmeAddons) { + gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, false); + for (let addon of gMockAddons) { yield gCategoryUtilities.openType("plugin"); let item = get_addon_element(gManagerWindow, addon.id); - Assert.ok(item, "Got add-on element."); - item.parentNode.ensureElementIsVisible(item); - is(item.getAttribute("active"), "false"); - - let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning"); - is_element_hidden(el, "Warning notification is hidden."); - el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix"); - is_element_visible(el, "disabled-postfix is visible."); - el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn"); - is_element_hidden(el, "Disable button not visible."); - el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "enable-btn"); - is_element_hidden(el, "Enable button not visible."); - - let menu = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "state-menulist"); - is_element_visible(menu, "State menu should be visible."); - - let neverActivate = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "never-activate-menuitem"); - is(menu.selectedItem, neverActivate, "Plugin state should be never-activate."); + if (addon.isEME) { + Assert.ok(!item, "Couldn't get add-on element."); + } else { + Assert.ok(item, "Got add-on element."); + } } - gPrefs.setBoolPref(GMPScope.KEY_EME_ENABLED, true); -}); - -add_task(function* testInstalledGlobalEmeDisabledDetails() { - gPrefs.setBoolPref(GMPScope.KEY_EME_ENABLED, false); - for (let addon of gMockEmeAddons) { - yield openDetailsView(addon.id); - let doc = gManagerWindow.document; - - let el = doc.getElementsByClassName("disabled-postfix")[0]; - is_element_visible(el, "disabled-postfix is visible."); - el = doc.getElementById("detail-findUpdates-btn"); - is_element_hidden(el, "Find updates link is hidden."); - el = doc.getElementById("detail-warning"); - is_element_hidden(el, "Warning notification is hidden."); - el = doc.getElementsByTagName("setting")[0]; - - let contextMenu = doc.getElementById("addonitem-popup"); - let deferred = Promise.defer(); - let listener = () => { - contextMenu.removeEventListener("popupshown", listener, false); - deferred.resolve(); - }; - contextMenu.addEventListener("popupshown", listener, false); - el = doc.getElementsByClassName("detail-view-container")[0]; - EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); - EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); - yield deferred.promise; - let menuSep = doc.getElementById("addonitem-menuseparator"); - is_element_hidden(menuSep, "Menu separator is hidden."); - contextMenu.hidePopup(); - } - gPrefs.setBoolPref(GMPScope.KEY_EME_ENABLED, true); + gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, true); }); add_task(function* testPreferencesButton() { @@ -351,9 +303,9 @@ add_task(function* testPreferencesButton() { yield close_manager(gManagerWindow); gManagerWindow = yield open_manager(); gCategoryUtilities = new CategoryUtilities(gManagerWindow); - gPrefs.setCharPref(getKey(GMPScope.KEY_PLUGIN_VERSION, addon.id), + gPrefs.setCharPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), preferences.version); - gPrefs.setBoolPref(getKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id), + gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), preferences.enabled); yield gCategoryUtilities.openType("plugin"); @@ -372,7 +324,7 @@ add_task(function* testPreferencesButton() { }); add_task(function* testUpdateButton() { - gPrefs.clearUserPref(GMPScope.KEY_PROVIDER_LASTCHECK); + gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_PROVIDER_LASTCHECK); let originalInstallManager = GMPScope.GMPInstallManager; Object.defineProperty(GMPScope, "GMPInstallManager", { @@ -411,14 +363,10 @@ add_task(function* testUpdateButton() { }); }); -add_task(function* testHidden() { - gPrefs.clearUserPref(GMPScope.KEY_PROVIDER_LASTCHECK); - +add_task(function* testEmeSupport() { for (let addon of gMockAddons) { - gPrefs.setBoolPref(getKey(GMPScope.KEY_PLUGIN_HIDDEN, addon.id), true); + gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCEVISIBLE, addon.id)); } - - // Hiding of plugins requires a restart of the GMP provider. yield GMPScope.GMPProvider.shutdown(); GMPScope.GMPProvider.startup(); @@ -426,8 +374,26 @@ add_task(function* testHidden() { yield gCategoryUtilities.openType("plugin"); let doc = gManagerWindow.document; let item = get_addon_element(gManagerWindow, addon.id); - Assert.equal(item, null); + if (addon.id == GMPScope.EME_ADOBE_ID) { + if (Services.appinfo.OS == "WINNT" && + Services.sysinfo.getPropertyAsInt32("version") >= 6) { + Assert.ok(item, "Adobe EME supported, found add-on element."); + } else { + Assert.ok(!item, + "Adobe EME not supported, couldn't find add-on element."); + } + } else { + Assert.ok(item, "Found add-on element."); + } } + + for (let addon of gMockAddons) { + gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCEVISIBLE, addon.id), + true); + } + yield GMPScope.GMPProvider.shutdown(); + GMPScope.GMPProvider.startup(); + }); add_task(function* test_cleanup() { diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js b/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js index 224bd51943ea..4353f8156c22 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js @@ -47,11 +47,12 @@ function run_test() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); startupManager(); - gPrefs.setBoolPref(GMPScope.KEY_LOGGING_DUMP, true); - gPrefs.setIntPref(GMPScope.KEY_LOGGING_LEVEL, 0); - gPrefs.setBoolPref(GMPScope.KEY_EME_ENABLED, true); + gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_LOGGING_DUMP, true); + gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_LOGGING_LEVEL, 0); + gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, true); for (let addon of gMockAddons.values()) { - gPrefs.setBoolPref(gGetKey(GMPScope.KEY_PLUGIN_HIDDEN, addon.id), false); + gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCEVISIBLE, addon.id), + true); } GMPScope.GMPProvider.shutdown(); GMPScope.GMPProvider.startup(); @@ -61,8 +62,8 @@ function run_test() { add_task(function* test_notInstalled() { for (let addon of gMockAddons.values()) { - gPrefs.setCharPref(gGetKey(GMPScope.KEY_PLUGIN_VERSION, addon.id), ""); - gPrefs.setBoolPref(gGetKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id), false); + gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), ""); + gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false); } let addons = yield promiseAddonsByIDs([...gMockAddons.keys()]); @@ -126,10 +127,10 @@ add_task(function* test_installed() { let file = Services.dirsvc.get("ProfD", Ci.nsIFile); file.append(addon.id); file.append(TEST_VERSION); - gPrefs.setBoolPref(gGetKey(GMPScope.KEY_PLUGIN_ENABLED, mockAddon.id), false); - gPrefs.setCharPref(gGetKey(GMPScope.KEY_PLUGIN_LAST_UPDATE, mockAddon.id), + gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, mockAddon.id), false); + gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, mockAddon.id), "" + TEST_TIME_SEC); - gPrefs.setCharPref(gGetKey(GMPScope.KEY_PLUGIN_VERSION, mockAddon.id), + gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, mockAddon.id), TEST_VERSION); Assert.ok(addon.isInstalled); @@ -165,7 +166,7 @@ add_task(function* test_enable() { Assert.equal(addons.length, gMockAddons.size); for (let addon of addons) { - gPrefs.setBoolPref(gGetKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id), true); + gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true); Assert.ok(addon.isActive); Assert.ok(!addon.appDisabled); @@ -180,7 +181,7 @@ add_task(function* test_globalEmeDisabled() { let addons = yield promiseAddonsByIDs([...gMockEmeAddons.keys()]); Assert.equal(addons.length, gMockEmeAddons.size); - gPrefs.setBoolPref(GMPScope.KEY_EME_ENABLED, false); + gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, false); GMPScope.GMPProvider.shutdown(); GMPScope.GMPProvider.startup(); for (let addon of addons) { @@ -190,7 +191,7 @@ add_task(function* test_globalEmeDisabled() { Assert.equal(addon.permissions, 0); } - gPrefs.setBoolPref(GMPScope.KEY_EME_ENABLED, true); + gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, true); GMPScope.GMPProvider.shutdown(); GMPScope.GMPProvider.startup(); }); @@ -200,7 +201,7 @@ add_task(function* test_autoUpdatePrefPersistance() { Assert.equal(addons.length, gMockAddons.size); for (let addon of addons) { - let autoupdateKey = gGetKey(GMPScope.KEY_PLUGIN_AUTOUPDATE, addon.id); + let autoupdateKey = gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id); gPrefs.clearUserPref(autoupdateKey); addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; @@ -233,10 +234,10 @@ add_task(function* test_pluginRegistration() { }; GMPScope.gmpService = MockGMPService; - gPrefs.setBoolPref(gGetKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id), true); + gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true); // Check that the plugin gets registered after startup. - gPrefs.setCharPref(gGetKey(GMPScope.KEY_PLUGIN_VERSION, addon.id), + gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), TEST_VERSION); clearPaths(); yield promiseRestartManager(); @@ -245,7 +246,7 @@ add_task(function* test_pluginRegistration() { // Check that clearing the version doesn't trigger registration. clearPaths(); - gPrefs.clearUserPref(gGetKey(GMPScope.KEY_PLUGIN_VERSION, addon.id)); + gPrefs.clearUserPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id)); Assert.deepEqual(addedPaths, []); Assert.deepEqual(removedPaths, [file.path]); @@ -256,23 +257,23 @@ add_task(function* test_pluginRegistration() { Assert.equal(removedPaths.indexOf(file.path), -1); // Changing the pref mid-session should cause unregistration and registration. - gPrefs.setCharPref(gGetKey(GMPScope.KEY_PLUGIN_VERSION, addon.id), + gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), TEST_VERSION); clearPaths(); const TEST_VERSION_2 = "5.6.7.8"; let file2 = Services.dirsvc.get("ProfD", Ci.nsIFile); file2.append(addon.id); file2.append(TEST_VERSION_2); - gPrefs.setCharPref(gGetKey(GMPScope.KEY_PLUGIN_VERSION, addon.id), + gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), TEST_VERSION_2); Assert.deepEqual(addedPaths, [file2.path]); Assert.deepEqual(removedPaths, [file.path]); // Disabling the plugin should cause unregistration. - gPrefs.setCharPref(gGetKey(GMPScope.KEY_PLUGIN_VERSION, addon.id), + gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), TEST_VERSION); clearPaths(); - gPrefs.setBoolPref(gGetKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id), false); + gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false); Assert.deepEqual(addedPaths, []); Assert.deepEqual(removedPaths, [file.path]); @@ -284,7 +285,7 @@ add_task(function* test_pluginRegistration() { // Re-enabling the plugin should cause registration. clearPaths(); - gPrefs.setBoolPref(gGetKey(GMPScope.KEY_PLUGIN_ENABLED, addon.id), true); + gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true); Assert.deepEqual(addedPaths, [file.path]); Assert.deepEqual(removedPaths, []); GMPScope = Cu.import("resource://gre/modules/addons/GMPProvider.jsm"); @@ -303,21 +304,21 @@ add_task(function* test_periodicUpdate() { Assert.equal(addons.length, gMockAddons.size); for (let addon of addons) { - gPrefs.clearUserPref(gGetKey(GMPScope.KEY_PLUGIN_AUTOUPDATE, addon.id)); + gPrefs.clearUserPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id)); addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; - gPrefs.setIntPref(GMPScope.KEY_PROVIDER_LASTCHECK, 0); + gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_PROVIDER_LASTCHECK, 0); let result = yield addon.findUpdates({}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE); Assert.strictEqual(result, false); addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE; - gPrefs.setIntPref(GMPScope.KEY_PROVIDER_LASTCHECK, Date.now() / 1000 - 60); + gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_PROVIDER_LASTCHECK, Date.now() / 1000 - 60); result = yield addon.findUpdates({}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE); Assert.strictEqual(result, false); - gPrefs.setIntPref(GMPScope.KEY_PROVIDER_LASTCHECK, + gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_PROVIDER_LASTCHECK, Date.now() / 1000 - 2 * GMPScope.SEC_IN_A_DAY); gInstalledAddonId = ""; result = From b8c729938b86e201889792db87075591f1a8cc91 Mon Sep 17 00:00:00 2001 From: Stephen Pohl Date: Thu, 19 Mar 2015 22:59:49 -0400 Subject: [PATCH 15/80] Bug 1140263: Disable Adobe EME on all platforms for now. r=cpearce --- toolkit/modules/GMPUtils.jsm | 6 +----- .../extensions/test/browser/browser_gmpProvider.js | 9 ++------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/toolkit/modules/GMPUtils.jsm b/toolkit/modules/GMPUtils.jsm index f4e6caaade7c..9bd1b673b146 100644 --- a/toolkit/modules/GMPUtils.jsm +++ b/toolkit/modules/GMPUtils.jsm @@ -47,11 +47,7 @@ this.GMPUtils = { */ _isPluginSupported: function(aPlugin) { if (aPlugin.id == EME_ADOBE_ID) { - if (Services.appinfo.OS == "WINNT") { - return Services.sysinfo.getPropertyAsInt32("version") >= 6; - } else { - return false; - } + return false; } return true; }, diff --git a/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js index 112014aefdc6..2a86664aca91 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js +++ b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js @@ -375,13 +375,8 @@ add_task(function* testEmeSupport() { let doc = gManagerWindow.document; let item = get_addon_element(gManagerWindow, addon.id); if (addon.id == GMPScope.EME_ADOBE_ID) { - if (Services.appinfo.OS == "WINNT" && - Services.sysinfo.getPropertyAsInt32("version") >= 6) { - Assert.ok(item, "Adobe EME supported, found add-on element."); - } else { - Assert.ok(!item, - "Adobe EME not supported, couldn't find add-on element."); - } + Assert.ok(!item, + "Adobe EME not supported, couldn't find add-on element."); } else { Assert.ok(item, "Found add-on element."); } From 941b1d7ea23e6c2e28407591788e659b1be128d5 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Fri, 20 Mar 2015 09:39:20 +0100 Subject: [PATCH 16/80] Bug 1125113 - Change the keywords schema associating them with uris. r=ttaubert --- toolkit/components/places/Database.cpp | 82 +++- toolkit/components/places/Database.h | 3 +- toolkit/components/places/PlacesDBUtils.jsm | 19 +- toolkit/components/places/PlacesUtils.jsm | 82 ++-- toolkit/components/places/UnifiedComplete.js | 27 +- .../places/nsINavBookmarksService.idl | 8 +- toolkit/components/places/nsNavBookmarks.cpp | 452 ++++++++++-------- toolkit/components/places/nsNavBookmarks.h | 10 +- .../components/places/nsPlacesAutoComplete.js | 14 +- toolkit/components/places/nsPlacesIndexes.h | 9 + toolkit/components/places/nsPlacesTables.h | 2 + toolkit/components/places/nsPlacesTriggers.h | 40 +- .../tests/autocomplete/test_keyword_search.js | 20 +- .../places/tests/bookmarks/test_keywords.js | 372 +++++++++----- .../components/places/tests/head_common.js | 16 +- .../places/tests/migration/places_v26.sqlite | Bin 1179648 -> 1179648 bytes .../places/tests/migration/places_v27.sqlite | Bin 0 -> 1212416 bytes .../tests/migration/test_current_from_v26.js | 75 +++ .../places/tests/migration/xpcshell.ini | 2 + .../places/tests/queries/test_sorting.js | 2 +- .../unifiedcomplete/test_keyword_search.js | 26 +- .../test_keyword_search_actions.js | 29 +- .../places/tests/unit/test_398914.js | 132 +---- .../tests/unit/test_async_transactions.js | 6 +- .../places/tests/unit/test_placesTxn.js | 12 +- .../tests/unit/test_preventive_maintenance.js | 61 +-- .../places/tests/unit/test_telemetry.js | 4 +- 27 files changed, 827 insertions(+), 678 deletions(-) create mode 100644 toolkit/components/places/tests/migration/places_v27.sqlite create mode 100644 toolkit/components/places/tests/migration/test_current_from_v26.js diff --git a/toolkit/components/places/Database.cpp b/toolkit/components/places/Database.cpp index 502e24e294aa..3af8e4c6fed5 100644 --- a/toolkit/components/places/Database.cpp +++ b/toolkit/components/places/Database.cpp @@ -735,6 +735,13 @@ Database::InitSchema(bool* aDatabaseMigrated) // Firefox 37 uses schema version 26. + if (currentSchemaVersion < 27) { + rv = MigrateV27Up(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Firefox 38 uses schema version 27. + // Schema Upgrades must add migration code here. rv = UpdateBookmarkRootTitles(); @@ -801,6 +808,8 @@ Database::InitSchema(bool* aDatabaseMigrated) // moz_keywords. rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_KEYWORDS); NS_ENSURE_SUCCESS(rv, rv); + rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA); + NS_ENSURE_SUCCESS(rv, rv); // moz_favicons. rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_FAVICONS); @@ -951,11 +960,18 @@ Database::InitTempTriggers() rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_TYPED_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); - rv = mMainConn->ExecuteSimpleSQL(CREATE_FOREIGNCOUNT_AFTERDELETE_TRIGGER); + rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERDELETE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); - rv = mMainConn->ExecuteSimpleSQL(CREATE_FOREIGNCOUNT_AFTERINSERT_TRIGGER); + rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERINSERT_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); - rv = mMainConn->ExecuteSimpleSQL(CREATE_FOREIGNCOUNT_AFTERUPDATE_TRIGGER); + rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERDELETE_TRIGGER); + NS_ENSURE_SUCCESS(rv, rv); + rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERINSERT_TRIGGER); + NS_ENSURE_SUCCESS(rv, rv); + rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; @@ -1487,6 +1503,66 @@ Database::MigrateV26Up() { return NS_OK; } +nsresult +Database::MigrateV27Up() { + MOZ_ASSERT(NS_IsMainThread()); + + // Change keywords store, moving their relation from bookmarks to urls. + nsCOMPtr stmt; + nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT place_id FROM moz_keywords" + ), getter_AddRefs(stmt)); + if (NS_FAILED(rv)) { + // Even if these 2 columns have a unique constraint, we allow NULL values + // for backwards compatibility. NULL never breaks a unique constraint. + rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_keywords ADD COLUMN place_id INTEGER")); + NS_ENSURE_SUCCESS(rv, rv); + rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_keywords ADD COLUMN post_data TEXT")); + NS_ENSURE_SUCCESS(rv, rv); + rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Associate keywords with uris. A keyword could be associated to multiple + // bookmarks uris, or multiple keywords could be associated to the same uri. + // The new system only allows multiple uris per keyword, provided they have + // a different post_data value. + rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT OR REPLACE INTO moz_keywords (id, keyword, place_id, post_data) " + "SELECT k.id, k.keyword, h.id, MAX(a.content) " + "FROM moz_places h " + "JOIN moz_bookmarks b ON b.fk = h.id " + "JOIN moz_keywords k ON k.id = b.keyword_id " + "LEFT JOIN moz_items_annos a ON a.item_id = b.id " + "LEFT JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " + "AND n.name = 'bookmarkProperties/POSTData'" + "WHERE k.place_id ISNULL " + "GROUP BY keyword")); + NS_ENSURE_SUCCESS(rv, rv); + + // Remove any keyword that points to a non-existing place id. + rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DELETE FROM moz_keywords " + "WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = moz_keywords.place_id)")); + NS_ENSURE_SUCCESS(rv, rv); + rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE moz_bookmarks SET keyword_id = NULL " + "WHERE NOT EXISTS (SELECT 1 FROM moz_keywords WHERE id = moz_bookmarks.keyword_id)")); + NS_ENSURE_SUCCESS(rv, rv); + + // Adjust foreign_count for all the rows. + rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE moz_places SET foreign_count = " + "(SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id) + " + "(SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id) " + )); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + void Database::Shutdown() { diff --git a/toolkit/components/places/Database.h b/toolkit/components/places/Database.h index c6ac97064f0e..b89f52be9240 100644 --- a/toolkit/components/places/Database.h +++ b/toolkit/components/places/Database.h @@ -16,7 +16,7 @@ // This is the schema version. Update it at any schema change and add a // corresponding migrateVxx method below. -#define DATABASE_SCHEMA_VERSION 26 +#define DATABASE_SCHEMA_VERSION 27 // Fired after Places inited. #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete" @@ -274,6 +274,7 @@ protected: nsresult MigrateV24Up(); nsresult MigrateV25Up(); nsresult MigrateV26Up(); + nsresult MigrateV27Up(); nsresult UpdateBookmarkRootTitles(); diff --git a/toolkit/components/places/PlacesDBUtils.jsm b/toolkit/components/places/PlacesDBUtils.jsm index d1fa8de7fef5..b8f4e1affc17 100644 --- a/toolkit/components/places/PlacesDBUtils.jsm +++ b/toolkit/components/places/PlacesDBUtils.jsm @@ -474,23 +474,6 @@ this.PlacesDBUtils = { fixOrphanItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; cleanupStatements.push(fixOrphanItems); - // D.5 fix wrong keywords - let fixInvalidKeywords = DBConn.createAsyncStatement( - `UPDATE moz_bookmarks SET keyword_id = NULL WHERE guid NOT IN ( - :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ - ) AND id IN ( - SELECT id FROM moz_bookmarks b - WHERE keyword_id NOT NULL - AND NOT EXISTS - (SELECT id FROM moz_keywords WHERE id = b.keyword_id LIMIT 1) - )`); - fixInvalidKeywords.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; - fixInvalidKeywords.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; - fixInvalidKeywords.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; - fixInvalidKeywords.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; - fixInvalidKeywords.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; - cleanupStatements.push(fixInvalidKeywords); - // D.6 fix wrong item types // Folders and separators should not have an fk. // If they have a valid fk convert them to bookmarks. Later in D.9 we @@ -681,7 +664,7 @@ this.PlacesDBUtils = { `DELETE FROM moz_keywords WHERE id IN ( SELECT id FROM moz_keywords k WHERE NOT EXISTS - (SELECT id FROM moz_bookmarks WHERE keyword_id = k.id LIMIT 1) + (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) )`); cleanupStatements.push(deleteUnusedKeywords); diff --git a/toolkit/components/places/PlacesUtils.jsm b/toolkit/components/places/PlacesUtils.jsm index f84799173778..305ba3a4433f 100644 --- a/toolkit/components/places/PlacesUtils.jsm +++ b/toolkit/components/places/PlacesUtils.jsm @@ -812,13 +812,26 @@ this.PlacesUtils = { * @param aBookmarkId * @returns string of POST data */ - setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) { - const annos = this.annotations; - if (aPostData) - annos.setItemAnnotation(aBookmarkId, this.POST_DATA_ANNO, aPostData, - 0, Ci.nsIAnnotationService.EXPIRE_NEVER); - else if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO)) - annos.removeItemAnnotation(aBookmarkId, this.POST_DATA_ANNO); + setPostDataForBookmark(aBookmarkId, aPostData) { + // For now we don't have a unified API to create a keyword with postData, + // thus here we can just try to complete a keyword that should already exist + // without any post data. + let nullPostDataFragment = aPostData ? "AND post_data ISNULL" : ""; + let stmt = PlacesUtils.history.DBConnection.createStatement( + `UPDATE moz_keywords SET post_data = :post_data + WHERE id = (SELECT k.id FROM moz_keywords k + JOIN moz_bookmarks b ON b.fk = k.place_id + WHERE b.id = :item_id + ${nullPostDataFragment} + LIMIT 1)`); + stmt.params.item_id = aBookmarkId; + stmt.params.post_data = aPostData; + try { + stmt.execute(); + } + finally { + stmt.finalize(); + } }, /** @@ -826,12 +839,22 @@ this.PlacesUtils = { * @param aBookmarkId * @returns string of POST data if set for aBookmarkId. null otherwise. */ - getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) { - const annos = this.annotations; - if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO)) - return annos.getItemAnnotation(aBookmarkId, this.POST_DATA_ANNO); - - return null; + getPostDataForBookmark(aBookmarkId) { + let stmt = PlacesUtils.history.DBConnection.createStatement( + `SELECT k.post_data + FROM moz_keywords k + JOIN moz_places h ON h.id = k.place_id + JOIN moz_bookmarks b ON b.fk = h.id + WHERE b.id = :item_id`); + stmt.params.item_id = aBookmarkId; + try { + if (!stmt.executeStep()) + return null; + return stmt.row.post_data; + } + finally { + stmt.finalize(); + } }, /** @@ -839,24 +862,21 @@ this.PlacesUtils = { * @param aKeyword string keyword * @returns an array containing a string URL and a string of POST data */ - getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) { - var url = null, postdata = null; + getURLAndPostDataForKeyword(aKeyword) { + let stmt = PlacesUtils.history.DBConnection.createStatement( + `SELECT h.url, k.post_data + FROM moz_keywords k + JOIN moz_places h ON h.id = k.place_id + WHERE k.keyword = :keyword`); + stmt.params.keyword = aKeyword; try { - var uri = this.bookmarks.getURIForKeyword(aKeyword); - if (uri) { - url = uri.spec; - var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri); - for (let i = 0; i < bookmarks.length; i++) { - var bookmark = bookmarks[i]; - var kw = this.bookmarks.getKeywordForBookmark(bookmark); - if (kw == aKeyword) { - postdata = this.getPostDataForBookmark(bookmark); - break; - } - } - } - } catch(ex) {} - return [url, postdata]; + if (!stmt.executeStep()) + return [ null, null ]; + return [ stmt.row.url, stmt.row.post_data ]; + } + finally { + stmt.finalize(); + } }, /** @@ -3069,7 +3089,7 @@ PlacesSortFolderByNameTransaction.prototype = { let callback = { _self: this, runBatched: function() { - for (item in this._self._oldOrder) + for (let item in this._self._oldOrder) PlacesUtils.bookmarks.setItemIndex(item, this._self._oldOrder[item]); } }; diff --git a/toolkit/components/places/UnifiedComplete.js b/toolkit/components/places/UnifiedComplete.js index b6d0621cc506..727f70a7bdcb 100644 --- a/toolkit/components/places/UnifiedComplete.js +++ b/toolkit/components/places/UnifiedComplete.js @@ -152,24 +152,20 @@ const SQL_ADAPTIVE_QUERY = const SQL_KEYWORD_QUERY = `/* do not warn (bug 487787) */ SELECT :query_type, - (SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) - AS search_url, h.title, + REPLACE(h.url, '%s', :query_string) AS search_url, h.title, IFNULL(f.url, (SELECT f.url FROM moz_places JOIN moz_favicons f ON f.id = favicon_id - WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk) + WHERE rev_host = h.rev_host ORDER BY frecency DESC LIMIT 1) ), - 1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk), - t.open_count, h.frecency + 1, NULL, NULL, h.visit_count, h.typed, h.id, t.open_count, h.frecency FROM moz_keywords k - JOIN moz_bookmarks b ON b.keyword_id = k.id - LEFT JOIN moz_places h ON h.url = search_url + JOIN moz_places h ON k.place_id = h.id LEFT JOIN moz_favicons f ON f.id = h.favicon_id LEFT JOIN moz_openpages_temp t ON t.url = search_url - WHERE LOWER(k.keyword) = LOWER(:keyword) - ORDER BY h.frecency DESC`; + WHERE k.keyword = LOWER(:keyword)`; function hostQuery(conditions = "") { let query = @@ -1241,24 +1237,13 @@ Search.prototype = { let title = bookmarkTitle || historyTitle; if (queryType == QUERYTYPE_KEYWORD) { + match.style = "keyword"; if (this._enableActions) { - match.style = "keyword"; url = makeActionURL("keyword", { url: escapedURL, input: this._originalSearchString, }); action = "keyword"; - } else { - // If we do not have a title, then we must have a keyword, so let the UI - // know it is a keyword. Otherwise, we found an exact page match, so just - // show the page like a regular result. Because the page title is likely - // going to be more specific than the bookmark title (keyword title). - if (!historyTitle) { - match.style = "keyword" - } - else { - title = historyTitle; - } } } diff --git a/toolkit/components/places/nsINavBookmarksService.idl b/toolkit/components/places/nsINavBookmarksService.idl index 2afa1fbe1a5d..13d803d27569 100644 --- a/toolkit/components/places/nsINavBookmarksService.idl +++ b/toolkit/components/places/nsINavBookmarksService.idl @@ -222,7 +222,7 @@ interface nsINavBookmarkObserver : nsISupports * folders. A URI in history can be contained in one or more such folders. */ -[scriptable, uuid(b0f9a80a-d7f0-4421-8513-444125f0d828)] +[scriptable, uuid(24533891-afa6-4663-b72d-3143d03f1b04)] interface nsINavBookmarksService : nsISupports { /** @@ -536,12 +536,6 @@ interface nsINavBookmarksService : nsISupports */ void setKeywordForBookmark(in long long aItemId, in AString aKeyword); - /** - * Retrieves the keyword for the given URI. Will be void string - * (null in JS) if no such keyword is found. - */ - AString getKeywordForURI(in nsIURI aURI); - /** * Retrieves the keyword for the given bookmark. Will be void string * (null in JS) if no such keyword is found. diff --git a/toolkit/components/places/nsNavBookmarks.cpp b/toolkit/components/places/nsNavBookmarks.cpp index cbf8310802a7..7161ca458c82 100644 --- a/toolkit/components/places/nsNavBookmarks.cpp +++ b/toolkit/components/places/nsNavBookmarks.cpp @@ -19,8 +19,6 @@ #include "GeckoProfiler.h" -#define BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH 32 - using namespace mozilla; // These columns sit to the right of the kGetInfoIndex_* columns. @@ -40,25 +38,6 @@ PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService) namespace { -struct keywordSearchData -{ - int64_t itemId; - nsString keyword; -}; - -PLDHashOperator -SearchBookmarkForKeyword(nsTrimInt64HashKey::KeyType aKey, - const nsString aValue, - void* aUserArg) -{ - keywordSearchData* data = reinterpret_cast(aUserArg); - if (data->keyword.Equals(aValue)) { - data->itemId = aKey; - return PL_DHASH_STOP; - } - return PL_DHASH_NEXT; -} - template class AsyncGetBookmarksForURI : public AsyncStatementCallback { @@ -143,8 +122,6 @@ nsNavBookmarks::nsNavBookmarks() , mCanNotify(false) , mCacheObservers("bookmark-observers") , mBatching(false) - , mBookmarkToKeywordHash(BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH) - , mBookmarkToKeywordHashInitialized(false) { NS_ASSERTION(!gBookmarksService, "Attempting to create two instances of the service!"); @@ -646,11 +623,12 @@ nsNavBookmarks::RemoveItem(int64_t aItemId) NS_ENSURE_SUCCESS(rv, rv); } - rv = UpdateKeywordsHashForRemovedBookmark(aItemId); - NS_ENSURE_SUCCESS(rv, rv); - // A broken url should not interrupt the removal process. - (void)NS_NewURI(getter_AddRefs(uri), bookmark.url); + rv = NS_NewURI(getter_AddRefs(uri), bookmark.url); + if (NS_SUCCEEDED(rv)) { + rv = UpdateKeywordsForRemovedBookmark(bookmark); + NS_ENSURE_SUCCESS(rv, rv); + } } NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, @@ -1108,8 +1086,14 @@ nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId) rv = SetItemDateInternal(LAST_MODIFIED, folder.id, RoundedPRNow()); NS_ENSURE_SUCCESS(rv, rv); - for (uint32_t i = 0; i < folderChildrenArray.Length(); i++) { + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + // Call observers in reverse order to serve children before their parent. + for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) { BookmarkData& child = folderChildrenArray[i]; + + nsCOMPtr uri; if (child.type == TYPE_BOOKMARK) { // If not a tag, recalculate frecency for this entry, since it changed. if (child.grandParentId != mTagsRoot) { @@ -1119,21 +1103,12 @@ nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId) NS_ENSURE_SUCCESS(rv, rv); } - rv = UpdateKeywordsHashForRemovedBookmark(child.id); - NS_ENSURE_SUCCESS(rv, rv); - } - } - - rv = transaction.Commit(); - NS_ENSURE_SUCCESS(rv, rv); - - // Call observers in reverse order to serve children before their parent. - for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) { - BookmarkData& child = folderChildrenArray[i]; - nsCOMPtr uri; - if (child.type == TYPE_BOOKMARK) { // A broken url should not interrupt the removal process. - (void)NS_NewURI(getter_AddRefs(uri), child.url); + rv = NS_NewURI(getter_AddRefs(uri), child.url); + if (NS_SUCCEEDED(rv)) { + rv = UpdateKeywordsForRemovedBookmark(child); + NS_ENSURE_SUCCESS(rv, rv); + } } NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, @@ -2264,39 +2239,69 @@ nsNavBookmarks::SetItemIndex(int64_t aItemId, int32_t aNewIndex) nsresult -nsNavBookmarks::UpdateKeywordsHashForRemovedBookmark(int64_t aItemId) +nsNavBookmarks::UpdateKeywordsForRemovedBookmark(const BookmarkData& aBookmark) { - nsAutoString keyword; - if (NS_SUCCEEDED(GetKeywordForBookmark(aItemId, keyword)) && - !keyword.IsEmpty()) { - nsresult rv = EnsureKeywordsHash(); + // If there are no keywords for this URI, there's nothing to do. + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aBookmark.url); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray keywords; + { + nsCOMPtr stmt = mDB->GetStatement( + "SELECT keyword FROM moz_keywords WHERE place_id = :place_id " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), aBookmark.placeId); NS_ENSURE_SUCCESS(rv, rv); - mBookmarkToKeywordHash.Remove(aItemId); - // If the keyword is unused, remove it from the database. - keywordSearchData searchData; - searchData.keyword.Assign(keyword); - searchData.itemId = -1; - mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData); - if (searchData.itemId == -1) { - nsCOMPtr stmt = mDB->GetAsyncStatement( - "DELETE FROM moz_keywords " - "WHERE keyword = :keyword " - "AND NOT EXISTS ( " - "SELECT id " - "FROM moz_bookmarks " - "WHERE keyword_id = moz_keywords.id " - ")" - ); - NS_ENSURE_STATE(stmt); - - rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); - NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr pendingStmt; - rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt)); + bool hasMore; + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { + nsAutoString keyword; + rv = stmt->GetString(0, keyword); NS_ENSURE_SUCCESS(rv, rv); + keywords.AppendElement(keyword); } } + + if (keywords.Length() == 0) { + // This uri has no keywords associated, so there's nothing to do. + return NS_OK; + } + + // If the uri is not bookmarked anymore, we can remove its keywords. + nsTArray bookmarks; + rv = GetBookmarksForURI(uri, bookmarks); + NS_ENSURE_SUCCESS(rv, rv); + if (bookmarks.Length() == 0) { + for (uint32_t i = 0; i < keywords.Length(); ++i) { + nsString keyword = keywords[i]; + + nsCOMPtr stmt = mDB->GetStatement( + "DELETE FROM moz_keywords WHERE keyword = :keyword " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(aBookmark.id, + NS_LITERAL_CSTRING("keyword"), + false, + EmptyCString(), + aBookmark.lastModified, + TYPE_BOOKMARK, + aBookmark.parentId, + aBookmark.guid, + aBookmark.parentGuid)); + } + return NS_OK; } @@ -2311,121 +2316,163 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId, BookmarkData bookmark; nsresult rv = FetchItemInfo(aBookmarkId, bookmark); NS_ENSURE_SUCCESS(rv, rv); - - rv = EnsureKeywordsHash(); + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), bookmark.url); NS_ENSURE_SUCCESS(rv, rv); // Shortcuts are always lowercased internally. nsAutoString keyword(aUserCasedKeyword); ToLowerCase(keyword); - // Check if bookmark was already associated to a keyword. - nsAutoString oldKeyword; - rv = GetKeywordForBookmark(bookmark.id, oldKeyword); - NS_ENSURE_SUCCESS(rv, rv); + // The same URI can be associated to more than one keyword, provided the post + // data differs. Check if there are already keywords associated to this uri. + nsTArray oldKeywords; + { + nsCOMPtr stmt = mDB->GetStatement( + "SELECT keyword FROM moz_keywords WHERE place_id = :place_id" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), bookmark.placeId); + NS_ENSURE_SUCCESS(rv, rv); - // Trying to set the same value or to remove a nonexistent keyword is a no-op. - if (keyword.Equals(oldKeyword) || (keyword.IsEmpty() && oldKeyword.IsEmpty())) + bool hasMore; + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { + nsString oldKeyword; + rv = stmt->GetString(0, oldKeyword); + NS_ENSURE_SUCCESS(rv, rv); + oldKeywords.AppendElement(oldKeyword); + } + } + + // Trying to remove a non-existent keyword is a no-op. + if (keyword.IsEmpty() && oldKeywords.Length() == 0) { return NS_OK; - - mozStorageTransaction transaction(mDB->MainConn(), false); - - nsCOMPtr updateBookmarkStmt = mDB->GetStatement( - "UPDATE moz_bookmarks " - "SET keyword_id = (SELECT id FROM moz_keywords WHERE keyword = :keyword), " - "lastModified = :date " - "WHERE id = :item_id " - ); - NS_ENSURE_STATE(updateBookmarkStmt); - mozStorageStatementScoper updateBookmarkScoper(updateBookmarkStmt); + } if (keyword.IsEmpty()) { - // Remove keyword association from the hash. - mBookmarkToKeywordHash.Remove(bookmark.id); - rv = updateBookmarkStmt->BindNullByName(NS_LITERAL_CSTRING("keyword")); + // We are removing the existing keywords. + for (uint32_t i = 0; i < oldKeywords.Length(); ++i) { + nsCOMPtr stmt = mDB->GetStatement( + "DELETE FROM moz_keywords WHERE keyword = :old_keyword" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("old_keyword"), + oldKeywords[i]); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsTArray bookmarks; + rv = GetBookmarksForURI(uri, bookmarks); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t i = 0; i < bookmarks.Length(); ++i) { + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmarks[i].id, + NS_LITERAL_CSTRING("keyword"), + false, + EmptyCString(), + bookmarks[i].lastModified, + TYPE_BOOKMARK, + bookmarks[i].parentId, + bookmarks[i].guid, + bookmarks[i].parentGuid)); + } + + return NS_OK; } - else { - // We are associating bookmark to a new keyword. Create a new keyword - // record if needed. - nsCOMPtr newKeywordStmt = mDB->GetStatement( - "INSERT OR IGNORE INTO moz_keywords (keyword) VALUES (:keyword)" + + // A keyword can only be associated to a single URI. Check if the requested + // keyword was already associated, in such a case we will need to notify about + // the change. + nsCOMPtr oldUri; + { + nsCOMPtr stmt = mDB->GetStatement( + "SELECT url " + "FROM moz_keywords " + "JOIN moz_places h ON h.id = place_id " + "WHERE keyword = :keyword" ); - NS_ENSURE_STATE(newKeywordStmt); - mozStorageStatementScoper newKeywordScoper(newKeywordStmt); - - rv = newKeywordStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), - keyword); - NS_ENSURE_SUCCESS(rv, rv); - rv = newKeywordStmt->Execute(); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); NS_ENSURE_SUCCESS(rv, rv); - // Add new keyword association to the hash, removing the old one if needed. - if (!oldKeyword.IsEmpty()) - mBookmarkToKeywordHash.Remove(bookmark.id); - mBookmarkToKeywordHash.Put(bookmark.id, keyword); - rv = updateBookmarkStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); + bool hasMore; + if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { + nsAutoCString spec; + rv = stmt->GetUTF8String(0, spec); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewURI(getter_AddRefs(oldUri), spec); + NS_ENSURE_SUCCESS(rv, rv); + } } - NS_ENSURE_SUCCESS(rv, rv); - bookmark.lastModified = RoundedPRNow(); - rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), - bookmark.lastModified); - NS_ENSURE_SUCCESS(rv, rv); - rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), - bookmark.id); - NS_ENSURE_SUCCESS(rv, rv); - rv = updateBookmarkStmt->Execute(); - NS_ENSURE_SUCCESS(rv, rv); - rv = transaction.Commit(); - NS_ENSURE_SUCCESS(rv, rv); + // If another uri is using the new keyword, we must update the keyword entry. + // Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete + // trigger. + nsCOMPtr stmt; + if (oldUri) { + // In both cases, notify about the change. + nsTArray bookmarks; + rv = GetBookmarksForURI(oldUri, bookmarks); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t i = 0; i < bookmarks.Length(); ++i) { + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmarks[i].id, + NS_LITERAL_CSTRING("keyword"), + false, + EmptyCString(), + bookmarks[i].lastModified, + TYPE_BOOKMARK, + bookmarks[i].parentId, + bookmarks[i].guid, + bookmarks[i].parentGuid)); + } - NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, - nsINavBookmarkObserver, - OnItemChanged(bookmark.id, - NS_LITERAL_CSTRING("keyword"), - false, - NS_ConvertUTF16toUTF8(keyword), - bookmark.lastModified, - bookmark.type, - bookmark.parentId, - bookmark.guid, - bookmark.parentGuid)); - - return NS_OK; -} - - -NS_IMETHODIMP -nsNavBookmarks::GetKeywordForURI(nsIURI* aURI, nsAString& aKeyword) -{ - PLACES_WARN_DEPRECATED(); - - NS_ENSURE_ARG(aURI); - aKeyword.Truncate(0); - - nsCOMPtr stmt = mDB->GetStatement( - "SELECT k.keyword " - "FROM moz_places h " - "JOIN moz_bookmarks b ON b.fk = h.id " - "JOIN moz_keywords k ON k.id = b.keyword_id " - "WHERE h.url = :page_url " - ); + stmt = mDB->GetStatement( + "UPDATE moz_keywords SET place_id = :place_id WHERE keyword = :keyword" + ); + NS_ENSURE_STATE(stmt); + } + else { + stmt = mDB->GetStatement( + "INSERT INTO moz_keywords (keyword, place_id) " + "VALUES (:keyword, :place_id)" + ); + } NS_ENSURE_STATE(stmt); mozStorageStatementScoper scoper(stmt); - nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), bookmark.placeId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); - bool hasMore = false; - rv = stmt->ExecuteStep(&hasMore); - if (NS_FAILED(rv) || !hasMore) { - aKeyword.SetIsVoid(true); - return NS_OK; // not found: return void keyword string + // In both cases, notify about the change. + nsTArray bookmarks; + rv = GetBookmarksForURI(uri, bookmarks); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t i = 0; i < bookmarks.Length(); ++i) { + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmarks[i].id, + NS_LITERAL_CSTRING("keyword"), + false, + NS_ConvertUTF16toUTF8(keyword), + bookmarks[i].lastModified, + TYPE_BOOKMARK, + bookmarks[i].parentId, + bookmarks[i].guid, + bookmarks[i].parentGuid)); } - // found, get the keyword - rv = stmt->GetString(0, aKeyword); - NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } @@ -2436,17 +2483,34 @@ nsNavBookmarks::GetKeywordForBookmark(int64_t aBookmarkId, nsAString& aKeyword) NS_ENSURE_ARG_MIN(aBookmarkId, 1); aKeyword.Truncate(0); - nsresult rv = EnsureKeywordsHash(); + // We can have multiple keywords for the same uri, here we'll just return the + // last created one. + nsCOMPtr stmt = mDB->GetStatement(NS_LITERAL_CSTRING( + "SELECT k.keyword " + "FROM moz_bookmarks b " + "JOIN moz_keywords k ON k.place_id = b.fk " + "WHERE b.id = :item_id " + "ORDER BY k.ROWID DESC " + "LIMIT 1" + )); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), + aBookmarkId); NS_ENSURE_SUCCESS(rv, rv); - nsAutoString keyword; - if (!mBookmarkToKeywordHash.Get(aBookmarkId, &keyword)) { - aKeyword.SetIsVoid(true); - } - else { - aKeyword.Assign(keyword); + bool hasMore; + if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { + nsAutoString keyword; + rv = stmt->GetString(0, keyword); + NS_ENSURE_SUCCESS(rv, rv); + aKeyword = keyword; + return NS_OK; } + aKeyword.SetIsVoid(true); + return NS_OK; } @@ -2463,51 +2527,27 @@ nsNavBookmarks::GetURIForKeyword(const nsAString& aUserCasedKeyword, nsAutoString keyword(aUserCasedKeyword); ToLowerCase(keyword); - nsresult rv = EnsureKeywordsHash(); - NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr stmt = mDB->GetStatement(NS_LITERAL_CSTRING( + "SELECT h.url " + "FROM moz_places h " + "JOIN moz_keywords k ON k.place_id = h.id " + "WHERE k.keyword = :keyword" + )); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); - keywordSearchData searchData; - searchData.keyword.Assign(keyword); - searchData.itemId = -1; - mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData); - - if (searchData.itemId == -1) { - // Not found. - return NS_OK; - } - - rv = GetBookmarkURI(searchData.itemId, aURI); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; -} - - -nsresult -nsNavBookmarks::EnsureKeywordsHash() { - if (mBookmarkToKeywordHashInitialized) { - return NS_OK; - } - mBookmarkToKeywordHashInitialized = true; - - nsCOMPtr stmt; - nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING( - "SELECT b.id, k.keyword " - "FROM moz_bookmarks b " - "JOIN moz_keywords k ON k.id = b.keyword_id " - ), getter_AddRefs(stmt)); + nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); NS_ENSURE_SUCCESS(rv, rv); bool hasMore; - while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { - int64_t itemId; - rv = stmt->GetInt64(0, &itemId); + if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { + nsAutoCString spec; + rv = stmt->GetUTF8String(0, spec); NS_ENSURE_SUCCESS(rv, rv); - nsAutoString keyword; - rv = stmt->GetString(1, keyword); + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), spec); NS_ENSURE_SUCCESS(rv, rv); - - mBookmarkToKeywordHash.Put(itemId, keyword); + uri.forget(aURI); } return NS_OK; diff --git a/toolkit/components/places/nsNavBookmarks.h b/toolkit/components/places/nsNavBookmarks.h index 6aeef4e66a5e..e5ac2fc9b803 100644 --- a/toolkit/components/places/nsNavBookmarks.h +++ b/toolkit/components/places/nsNavBookmarks.h @@ -421,21 +421,13 @@ private: // Note: this is only tracking bookmarks batches, not history ones. bool mBatching; - /** - * Always call EnsureKeywordsHash() and check it for errors before actually - * using the hash. Internal keyword methods are already doing that. - */ - nsresult EnsureKeywordsHash(); - nsDataHashtable mBookmarkToKeywordHash; - bool mBookmarkToKeywordHashInitialized; - /** * This function must be called every time a bookmark is removed. * * @param aURI * Uri to test. */ - nsresult UpdateKeywordsHashForRemovedBookmark(int64_t aItemId); + nsresult UpdateKeywordsForRemovedBookmark(const BookmarkData& aBookmark); }; #endif // nsNavBookmarks_h_ diff --git a/toolkit/components/places/nsPlacesAutoComplete.js b/toolkit/components/places/nsPlacesAutoComplete.js index ec1986a157e6..366ce144a04e 100644 --- a/toolkit/components/places/nsPlacesAutoComplete.js +++ b/toolkit/components/places/nsPlacesAutoComplete.js @@ -414,24 +414,20 @@ function nsPlacesAutoComplete() XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() { return this._db.createAsyncStatement( `/* do not warn (bug 487787) */ - SELECT - (SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) - AS search_url, h.title, + SELECT REPLACE(h.url, '%s', :query_string) AS search_url, h.title, IFNULL(f.url, (SELECT f.url FROM moz_places JOIN moz_favicons f ON f.id = favicon_id - WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk) + WHERE rev_host = h.rev_host ORDER BY frecency DESC LIMIT 1) - ), 1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk), + ), 1, NULL, NULL, h.visit_count, h.typed, h.id, :query_type, t.open_count FROM moz_keywords k - JOIN moz_bookmarks b ON b.keyword_id = k.id - LEFT JOIN moz_places h ON h.url = search_url + JOIN moz_places h ON k.place_id = h.id LEFT JOIN moz_favicons f ON f.id = h.favicon_id LEFT JOIN moz_openpages_temp t ON t.url = search_url - WHERE LOWER(k.keyword) = LOWER(:keyword) - ORDER BY h.frecency DESC` + WHERE k.keyword = LOWER(:keyword)` ); }); diff --git a/toolkit/components/places/nsPlacesIndexes.h b/toolkit/components/places/nsPlacesIndexes.h index be52de66f989..0327960395d2 100644 --- a/toolkit/components/places/nsPlacesIndexes.h +++ b/toolkit/components/places/nsPlacesIndexes.h @@ -121,4 +121,13 @@ "guid_uniqueindex", "moz_favicons", "guid", "UNIQUE" \ ) +/** + * moz_keywords + */ + +#define CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA \ + CREATE_PLACES_IDX( \ + "placepostdata_uniqueindex", "moz_keywords", "place_id, post_data", "UNIQUE" \ + ) + #endif // nsPlacesIndexes_h__ diff --git a/toolkit/components/places/nsPlacesTables.h b/toolkit/components/places/nsPlacesTables.h index c9a9637f339e..db1a2e67d618 100644 --- a/toolkit/components/places/nsPlacesTables.h +++ b/toolkit/components/places/nsPlacesTables.h @@ -121,6 +121,8 @@ "CREATE TABLE moz_keywords (" \ " id INTEGER PRIMARY KEY AUTOINCREMENT" \ ", keyword TEXT UNIQUE" \ + ", place_id INTEGER" \ + ", post_data TEXT" \ ")" \ ) diff --git a/toolkit/components/places/nsPlacesTriggers.h b/toolkit/components/places/nsPlacesTriggers.h index 52b96f9c6007..00b6380b730c 100644 --- a/toolkit/components/places/nsPlacesTriggers.h +++ b/toolkit/components/places/nsPlacesTriggers.h @@ -175,7 +175,7 @@ "END" \ ) -#define CREATE_FOREIGNCOUNT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \ +#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \ "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterdelete_trigger " \ "AFTER DELETE ON moz_bookmarks FOR EACH ROW " \ "BEGIN " \ @@ -185,7 +185,7 @@ "END" \ ) -#define CREATE_FOREIGNCOUNT_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \ +#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \ "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterinsert_trigger " \ "AFTER INSERT ON moz_bookmarks FOR EACH ROW " \ "BEGIN " \ @@ -195,7 +195,7 @@ "END" \ ) -#define CREATE_FOREIGNCOUNT_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \ +#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \ "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterupdate_trigger " \ "AFTER UPDATE OF fk ON moz_bookmarks FOR EACH ROW " \ "BEGIN " \ @@ -207,4 +207,38 @@ "WHERE id = OLD.fk;" \ "END" \ ) + +#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \ + "CREATE TEMP TRIGGER moz_keywords_foreign_count_afterdelete_trigger " \ + "AFTER DELETE ON moz_keywords FOR EACH ROW " \ + "BEGIN " \ + "UPDATE moz_places " \ + "SET foreign_count = foreign_count - 1 " \ + "WHERE id = OLD.place_id;" \ + "END" \ +) + +#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \ + "CREATE TEMP TRIGGER moz_keyords_foreign_count_afterinsert_trigger " \ + "AFTER INSERT ON moz_keywords FOR EACH ROW " \ + "BEGIN " \ + "UPDATE moz_places " \ + "SET foreign_count = foreign_count + 1 " \ + "WHERE id = NEW.place_id;" \ + "END" \ +) + +#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \ + "CREATE TEMP TRIGGER moz_keywords_foreign_count_afterupdate_trigger " \ + "AFTER UPDATE OF place_id ON moz_keywords FOR EACH ROW " \ + "BEGIN " \ + "UPDATE moz_places " \ + "SET foreign_count = foreign_count + 1 " \ + "WHERE id = NEW.place_id; " \ + "UPDATE moz_places " \ + "SET foreign_count = foreign_count - 1 " \ + "WHERE id = OLD.place_id; " \ + "END" \ +) + #endif // __nsPlacesTriggers_h__ diff --git a/toolkit/components/places/tests/autocomplete/test_keyword_search.js b/toolkit/components/places/tests/autocomplete/test_keyword_search.js index 8691d8185873..c8ddc84a83ef 100644 --- a/toolkit/components/places/tests/autocomplete/test_keyword_search.js +++ b/toolkit/components/places/tests/autocomplete/test_keyword_search.js @@ -43,13 +43,13 @@ let kTitles = [ // Add the keyword bookmark addPageBook(0, 0, 1, [], keyKey); // Add in the "fake pages" for keyword searches -gPages[1] = [1,1]; -gPages[2] = [2,1]; -gPages[3] = [3,1]; -gPages[4] = [4,1]; +gPages[1] = [1,0]; +gPages[2] = [2,0]; +gPages[3] = [3,0]; +gPages[4] = [4,0]; // Add a page into history addPageBook(5, 0); -gPages[6] = [6,1]; +gPages[6] = [6,0]; // Provide for each test: description; search terms; array of gPages indices of // pages that should match; optional function to be run before the test @@ -68,14 +68,4 @@ let gTests = [ keyKey, [6]], ["6: Keyword without query (with space)", keyKey + " ", [6]], - - // This adds a second keyword so anything after this will match 2 keywords - ["7: Two keywords matched", - keyKey + " twoKey", [8,9], - function() { - // Add the keyword search as well as search results - addPageBook(7, 0, 1, [], keyKey); - gPages[8] = [8,1]; - gPages[9] = [9,1]; - }] ]; diff --git a/toolkit/components/places/tests/bookmarks/test_keywords.js b/toolkit/components/places/tests/bookmarks/test_keywords.js index 193af622cb83..242fa4977e0d 100644 --- a/toolkit/components/places/tests/bookmarks/test_keywords.js +++ b/toolkit/components/places/tests/bookmarks/test_keywords.js @@ -1,169 +1,307 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ +const URI1 = NetUtil.newURI("http://test1.mozilla.org/"); +const URI2 = NetUtil.newURI("http://test2.mozilla.org/"); +const URI3 = NetUtil.newURI("http://test3.mozilla.org/"); -function check_bookmark_keyword(aItemId, aKeyword) -{ - let keyword = aKeyword ? aKeyword.toLowerCase() : null; - do_check_eq(PlacesUtils.bookmarks.getKeywordForBookmark(aItemId), - keyword); -} - -function check_uri_keyword(aURI, aKeyword) -{ - let keyword = aKeyword ? aKeyword.toLowerCase() : null; +function check_keyword(aURI, aKeyword) { + if (aKeyword) + aKeyword = aKeyword.toLowerCase(); for (let bm of PlacesUtils.getBookmarksForURI(aURI)) { - let kid = PlacesUtils.bookmarks.getKeywordForBookmark(bm); - if (kid && !keyword) { - Assert.ok(false, `${aURI.spec} should not have a keyword`); - } else if (keyword && kid == keyword) { - Assert.equal(kid, keyword, "Found the keyword"); - break; + let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bm); + if (keyword && !aKeyword) { + throw(`${aURI.spec} should not have a keyword`); + } else if (aKeyword && keyword == aKeyword) { + Assert.equal(keyword, aKeyword); } } if (aKeyword) { - // This API can't tell which uri the user wants, so it returns a random one. - let re = /http:\/\/test[0-9]\.mozilla\.org/; - let url = PlacesUtils.bookmarks.getURIForKeyword(aKeyword).spec; - do_check_true(re.test(url)); + let uri = PlacesUtils.bookmarks.getURIForKeyword(aKeyword); + Assert.equal(uri.spec, aURI.spec); // Check case insensitivity. - url = PlacesUtils.bookmarks.getURIForKeyword(aKeyword.toUpperCase()).spec - do_check_true(re.test(url)); + uri = PlacesUtils.bookmarks.getURIForKeyword(aKeyword.toUpperCase()); + Assert.equal(uri.spec, aURI.spec); } } -function check_orphans() -{ - let stmt = DBConn().createStatement( - `SELECT id FROM moz_keywords k WHERE NOT EXISTS ( - SELECT id FROM moz_bookmarks WHERE keyword_id = k.id - )` - ); - try { - do_check_false(stmt.executeStep()); - } finally { - stmt.finalize(); - } - - print("Check there are no orphan database entries"); - stmt = DBConn().createStatement( - `SELECT b.id FROM moz_bookmarks b - LEFT JOIN moz_keywords k ON b.keyword_id = k.id - WHERE keyword_id NOTNULL AND k.id ISNULL` - ); - try { - do_check_false(stmt.executeStep()); - } finally { - stmt.finalize(); - } +function check_orphans() { + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.executeCached( + `SELECT id FROM moz_keywords k + WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id) + `); + Assert.equal(rows.length, 0); } -const URIS = [ - uri("http://test1.mozilla.org/"), - uri("http://test2.mozilla.org/"), -]; +function expectNotifications() { + let notifications = []; + let observer = new Proxy(NavBookmarkObserver, { + get(target, name) { + if (name == "check") { + PlacesUtils.bookmarks.removeObserver(observer); + return expectedNotifications => + Assert.deepEqual(notifications, expectedNotifications); + } -add_test(function test_addBookmarkWithKeyword() -{ - check_uri_keyword(URIS[0], null); + if (name.startsWith("onItemChanged")) { + return (id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid) => { + if (prop != "keyword") + return; + let args = Array.from(arguments, arg => { + if (arg && arg instanceof Ci.nsIURI) + return new URL(arg.spec); + if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000) + return new Date(parseInt(arg/1000)); + return arg; + }); + notifications.push({ name: name, arguments: args }); + } + } + + return target[name]; + } + }); + PlacesUtils.bookmarks.addObserver(observer, false); + return observer; +} + +add_task(function test_invalid_input() { + Assert.throws(() => PlacesUtils.bookmarks.getURIForKeyword(null), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.getURIForKeyword(""), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.getKeywordForBookmark(null), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.getKeywordForBookmark(0), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(null, "k"), + /NS_ERROR_ILLEGAL_VALUE/); + Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(0, "k"), + /NS_ERROR_ILLEGAL_VALUE/); +}); + +add_task(function test_addBookmarkAndKeyword() { + check_keyword(URI1, null); + let fc = yield foreign_count(URI1); + let observer = expectNotifications(); let itemId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, - URIS[0], + URI1, PlacesUtils.bookmarks.DEFAULT_INDEX, "test"); + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword"); - check_bookmark_keyword(itemId, "keyword"); - check_uri_keyword(URIS[0], "keyword"); + let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI1 }); + observer.check([ { name: "onItemChanged", + arguments: [ itemId, "keyword", false, "keyword", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid ] } + ]); + yield PlacesTestUtils.promiseAsyncUpdates(); - PlacesTestUtils.promiseAsyncUpdates().then(() => { - check_orphans(); - run_next_test(); - }); + check_keyword(URI1, "keyword"); + Assert.equal((yield foreign_count(URI1)), fc + 2); // + 1 bookmark + 1 keyword + + yield PlacesTestUtils.promiseAsyncUpdates(); + yield check_orphans(); }); -add_test(function test_addBookmarkToURIHavingKeyword() -{ +add_task(function test_addBookmarkToURIHavingKeyword() { + // The uri has already a keyword. + check_keyword(URI1, "keyword"); + let fc = yield foreign_count(URI1); + let itemId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, - URIS[0], + URI1, PlacesUtils.bookmarks.DEFAULT_INDEX, "test"); - // The uri has a keyword, but this specific bookmark has not. - check_bookmark_keyword(itemId, null); - check_uri_keyword(URIS[0], "keyword"); + check_keyword(URI1, "keyword"); + Assert.equal((yield foreign_count(URI1)), fc + 1); // + 1 bookmark - PlacesTestUtils.promiseAsyncUpdates().then(() => { - check_orphans(); - run_next_test(); - }); + PlacesUtils.bookmarks.removeItem(itemId); + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); }); -add_test(function test_addSameKeywordToOtherURI() -{ +add_task(function test_sameKeywordDifferentURI() { + let fc1 = yield foreign_count(URI1); + let fc2 = yield foreign_count(URI2); + let observer = expectNotifications(); + let itemId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, - URIS[1], + URI2, PlacesUtils.bookmarks.DEFAULT_INDEX, "test2"); - check_bookmark_keyword(itemId, null); - check_uri_keyword(URIS[1], null); + check_keyword(URI1, "keyword"); + check_keyword(URI2, null); PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "kEyWoRd"); - check_bookmark_keyword(itemId, "kEyWoRd"); - check_uri_keyword(URIS[1], "kEyWoRd"); - // Check case insensitivity. - check_uri_keyword(URIS[0], "kEyWoRd"); - check_bookmark_keyword(itemId, "keyword"); - check_uri_keyword(URIS[1], "keyword"); - check_uri_keyword(URIS[0], "keyword"); + let bookmark1 = yield PlacesUtils.bookmarks.fetch({ url: URI1 }); + let bookmark2 = yield PlacesUtils.bookmarks.fetch({ url: URI2 }); + observer.check([ { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), + "keyword", false, "", + bookmark1.lastModified, bookmark1.type, + (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), + bookmark1.guid, bookmark1.parentGuid ] }, + { name: "onItemChanged", + arguments: [ itemId, "keyword", false, "keyword", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid ] } + ]); + yield PlacesTestUtils.promiseAsyncUpdates(); - PlacesTestUtils.promiseAsyncUpdates().then(() => { - check_orphans(); - run_next_test(); - }); + // The keyword should have been "moved" to the new URI. + check_keyword(URI1, null); + Assert.equal((yield foreign_count(URI1)), fc1 - 1); // - 1 keyword + check_keyword(URI2, "keyword"); + Assert.equal((yield foreign_count(URI2)), fc2 + 2); // + 1 bookmark + 1 keyword + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); }); -add_test(function test_removeBookmarkWithKeyword() -{ +add_task(function test_sameURIDifferentKeyword() { + let fc = yield foreign_count(URI2); + let observer = expectNotifications(); + let itemId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, - URIS[1], + URI2, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test2"); + check_keyword(URI2, "keyword"); + + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword2"); + + let bookmarks = []; + yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark)); + observer.check([ { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)), + "keyword", false, "keyword2", + bookmarks[0].lastModified, bookmarks[0].type, + (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)), + bookmarks[0].guid, bookmarks[0].parentGuid ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)), + "keyword", false, "keyword2", + bookmarks[1].lastModified, bookmarks[1].type, + (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)), + bookmarks[1].guid, bookmarks[1].parentGuid ] } + ]); + yield PlacesTestUtils.promiseAsyncUpdates(); + + check_keyword(URI2, "keyword2"); + Assert.equal((yield foreign_count(URI2)), fc + 2); // + 1 bookmark + 1 keyword + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +add_task(function test_removeBookmarkWithKeyword() { + let fc = yield foreign_count(URI2); + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI2, PlacesUtils.bookmarks.DEFAULT_INDEX, "test"); - PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword"); - check_bookmark_keyword(itemId, "keyword"); - check_uri_keyword(URIS[1], "keyword"); - // The keyword should not be removed from other bookmarks. + // The keyword should not be removed, since there are other bookmarks yet. + PlacesUtils.bookmarks.removeItem(itemId); + + check_keyword(URI2, "keyword2"); + Assert.equal((yield foreign_count(URI2)), fc); // + 1 bookmark - 1 bookmark + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +add_task(function test_unsetKeyword() { + let fc = yield foreign_count(URI2); + let observer = expectNotifications(); + + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI2, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test"); + + // The keyword should be removed from any bookmark. + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, null); + + let bookmarks = []; + yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark)); + do_print(bookmarks.length); + observer.check([ { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)), + "keyword", false, "", + bookmarks[0].lastModified, bookmarks[0].type, + (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)), + bookmarks[0].guid, bookmarks[0].parentGuid ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)), + "keyword", false, "", + bookmarks[1].lastModified, bookmarks[1].type, + (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)), + bookmarks[1].guid, bookmarks[1].parentGuid ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[2].guid)), + "keyword", false, "", + bookmarks[2].lastModified, bookmarks[2].type, + (yield PlacesUtils.promiseItemId(bookmarks[2].parentGuid)), + bookmarks[2].guid, bookmarks[2].parentGuid ] } + ]); + + check_keyword(URI1, null); + check_keyword(URI2, null); + Assert.equal((yield foreign_count(URI2)), fc - 1); // + 1 bookmark - 2 keyword + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); +}); + +add_task(function test_addRemoveBookmark() { + let fc = yield foreign_count(URI3); + let observer = expectNotifications(); + + let itemId = + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + URI3, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "test3"); + + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword"); + let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI3 }); + let parentId = yield PlacesUtils.promiseItemId(bookmark.parentGuid); PlacesUtils.bookmarks.removeItem(itemId); - check_uri_keyword(URIS[1], "keyword"); - check_uri_keyword(URIS[0], "keyword"); + observer.check([ { name: "onItemChanged", + arguments: [ itemId, + "keyword", false, "keyword", + bookmark.lastModified, bookmark.type, + parentId, + bookmark.guid, bookmark.parentGuid ] }, + { name: "onItemChanged", + arguments: [ itemId, + "keyword", false, "", + bookmark.lastModified, bookmark.type, + parentId, + bookmark.guid, bookmark.parentGuid ] } + ]); - PlacesTestUtils.promiseAsyncUpdates().then(() => { - check_orphans(); - run_next_test(); - }); + check_keyword(URI3, null); + Assert.equal((yield foreign_count(URI3)), fc); + + yield PlacesTestUtils.promiseAsyncUpdates(); + check_orphans(); }); -add_test(function test_removeFolderWithKeywordedBookmarks() -{ - // Keyword should be removed as well. - PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId); - - check_uri_keyword(URIS[1], null); - check_uri_keyword(URIS[0], null); - - PlacesTestUtils.promiseAsyncUpdates().then(() => { - check_orphans(); - run_next_test(); - }); -}); - -function run_test() -{ +function run_test() { run_next_test(); } diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js index dcbd89dfc0e2..411a75d28acf 100644 --- a/toolkit/components/places/tests/head_common.js +++ b/toolkit/components/places/tests/head_common.js @@ -3,7 +3,7 @@ * License, v. 2.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 CURRENT_SCHEMA_VERSION = 26; +const CURRENT_SCHEMA_VERSION = 27; const FIRST_UPGRADABLE_SCHEMA_VERSION = 11; const NS_APP_USER_PROFILE_50_DIR = "ProfD"; @@ -851,3 +851,17 @@ function checkBookmarkObject(info) { Assert.ok(info.lastModified >= info.dateAdded, "lastModified should never be smaller than dateAdded"); Assert.ok(typeof info.type == "number", "type should be a number"); } + +/** + * Reads foreign_count value for a given url. + */ +function* foreign_count(url) { + if (url instanceof Ci.nsIURI) + url = url.spec; + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.executeCached( + `SELECT foreign_count FROM moz_places + WHERE url = :url + `, { url }); + return rows.length == 0 ? 0 : rows[0].getResultByName("foreign_count"); +} diff --git a/toolkit/components/places/tests/migration/places_v26.sqlite b/toolkit/components/places/tests/migration/places_v26.sqlite index b43dc2389e4a17471bc01ae421f9521fbc2a0afa..b4b238179736aa64b22329e9c8ef3b2a69d3effc 100644 GIT binary patch delta 176 zcmZo@@Mvi8m>@04!oa|w0>m&NH&Mr!m4!j~8AoGEYYJm)3e(mU=57C&{69>W`OmD! z<^O?!l{0^pIH-0zWvL8W*}w(Vpbq#17da{<^W<&Am##M mZXo6XV&3gv{`38*7x4eU!o$MA#tE~ct%1L-L10;fU;_XSElEiL delta 176 zcmZo@@Mvi8m>@04%)r2)0>m&NJ5k4&m6<{J8GB<&YYJm)3e(mU=57C&j4w==`OmD! zWqg5wl{0^pIH-0zWvL8W*}w(Vpbq#17da{<^W<&Am##M mZXo6XV&3gv{`38*7cjoS!o$MA#tE~ct%1L-L10;fU;_YJ{6{GO diff --git a/toolkit/components/places/tests/migration/places_v27.sqlite b/toolkit/components/places/tests/migration/places_v27.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..57dfb75627d10e9a246b91573ffcae357b574f0c GIT binary patch literal 1212416 zcmeI*d3+Pq!tn8I%~G1OTM&hSY!$jM6p)K8Ep6#aX%V=3Ynn{c&?Hl`khUyC0TC4! zTo6~#tGMEZ__$xVh%1PS2)LjqDy|@UMO5^iNt)6YP@m_1KJQiUZ=h%9?B{&voXMpB zbjsvnr(dwtie9(XZ%I=vP-!%(vn>{tN~M*5hR8pWV}N|2i$vtJTK?(RE`Fj*tt#Oo zqj}!Ls=@W=spc=7_vqZm=lSOBntNgMrlv#9bDR1!J=XYa@U6y6gX-Ybfpz{$zrnZC zm*U;*ojU7{S!Xpo(vU1ZCH}>8wlsn6{!ljcN;y?fa1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#~Ea|*N!9&Cu4JVM9$(wH@-zP&=6NROx-fD zZG<(VSnsxa>wVRZfYaW2WNo>FPByf*635k#45_VzWw_Es{A`OMZo)uyOYgQZgm6wg zfN<8pll6X*nvt%<*{%Kh1{mV9<$k+&?l<6db)I)?#b4>RQ^n4`%GvQP-TNEjrpV$M zZN0X6gtn5)>hrrryR+6Q*pDwVpmhYtHqo!tj*OwLm}Piv{YZ<_OG#@l@$*hM#En16 zB51IB1&{yuf%N}{fwZf0g6_k)BQ5eObo!m5Cw|sxhPbhE_vYh!35gt^eEKhRt~?<^ zHBQjAGIM`ZgAm`M?I*9fr1tB+>`u-lo}3c~m}ScO2V}9kNb4uI~=n$7)9U?~rX7 z9_k=eN*=|jzdVXsuNa<-^0e&GPQPCnMO)32j6%t;4h*))#uF+OdW^R7ncqi#pvxQd*uyEj4Xc&a8!FefaHZKWO8EY`;|y`*B_$TZ_l*4MGZojVH4hT4f2dK=<0QYVJLP0uwUzDMlUJA> zYAD{_OWxXYH(fe4SzT+L?X-!WPRYGb)>Hf9ojYsIQ(ggzdg|kb3{n5pZ>@0&zF98$ zSy1isTm32RuY0w2Qkk7ooM&mDGu&U}8tJq*j#TQzFFd!0A#TJ7^g{8cruq3-;x}_*@`bbOb3r$;QX<4PEw5quH1aCNzG9xW+ zr(3929%PoO*%cECvMYwCrN&!w^Co6j6-TPex_iOelZlUj@;&Uy;Y~2AhCN(lG&ha>V*T_X4bmn zLsN=e#~wGANX{=!LVhD_zd>5RqJ$Q<)sjDAtkdE*)BHpL23NOLkRdSz)PMvm~#ya-^lTtq?(*W9JPSdg}{+_h_~1 z@@rO+F@~)ov6vQvF}&b2+Gf`Fid*d!MSo~@bV@QEH_uK5l<6v!t2?ajl+5_H8P$p| zyWnm2jET=P{n|S3)Jb?Iof4xL_AnUYh7D6MvK>Dm<&LzKH=cYFC%;?a_uj~JQT`yP zk6gEX3WPs(!f)r1%wuoq$5Q-Gze{MpYufI8I~BQyFjVNHL)jK5__XNwHRMeO)1y}BT`?&77`L@qh zrrmkVSt7HP-!+tm+uyl%c>$kL9eOo9HurY#DeZrI5Nahpe!NZ}Hznbh=GuN_;VHLI ziIRU=rfbR97~+zW)Cp_wn)D zFY>zG{?3GL0`4kKF;PHDmcew zWfW9SEuNB|Q(9xSHlzg|rH-6%3zpUvED4qgg4brX*hG&dKh&S!nJBoFt`n6p7(&A? z*|+&%RZ)Fe{e)_vwydVW8zYa}tQui58CC6d+l|^Lx_|qy-J-31r{O-|Ue?$+tuE7% zUEs>8EAg}#yO>pT3KLEw)&6!0o`k9?ZFkh;bkzA>O-kJ7_Xa#kt#`;$8qQGen9pyi z2zV^z^4)HKb&(NwmZVa7! z3C*`SdeQTv{mr9%sR?ENTxVLn$*huoy^{v7I8(6seQh&N%SuePrW~8_2|Ft~nm4?_ zndhj>NzWB$+N;MJ&8o>Nl~KN*M%55g=nvD6QU3n>xXhtG(d!?Vl0MXJZLAP{0hix5 zE+yGhGP|&Pf_HNDgo)mkZU%WdG!)) z%`BOcrZ=myI}a!{k*tobBoqYeE1L7p$tbU%TpndsRjCq;@*_hdBSan<$~tI&Tf+;? zS|bMhNu6P&Ughjb8I!7XW>xiXZBgEWey_78;1`Z-R;yYT6ef+HFg;I5 zom!oKuGXw_|JG(BtI!tk`9-(0`S{i~s%06alSWM`pOEdIV)ISam{qn=>!x34h_qfO zxEhWdwrbgoinR3RX=46_+}Xl(wOQp1wX6E=?b_t0tk>!~LDRD{OI_Z8YsTpE(p0nj zS*MpSS4m$=Z%NNd>!n-d2nPZPAba(b z%?I_}bgiXgLP>8gYwV$IEv4zABo%jGuq&#&#-K7OHKRjG(Tkps>ZWe3Xi}1jj^+(F zEAw6OpPe+lBY*$`2q1s}0tg_000IagfB*srAbWi_K1#%bF;99rkWoy=s7*~y)TiK5qG)(%iRMA6|A5}kf+tae}|W)t0- zQ?w~+tKBYoe2ML<=*;q*yUu-7CGC)Il+Kru=O3E?=={0!|1w`a@A-LG&HL-T)8>9C zXLBHc00IagfB*srAbxXDH8dX&6^rGS&$&qt;Q&w5Zw8BhZxjmQ>8>2F*)uuB-g^ijD z!6tb8mP)I`7cRc>&hUBb`O^bpMeaG%ldXZq&Wi|$?9n}q)G2!Y%Le=%!64B?kOm+I4{z$zQ4}|NjDGzung8tDH znoFJa!L%-+dg{|d^`Z>g>>4rPw^W74I%(s^cU7zrI&gz23vKcO4)of2`S?XEF@Dx)*l{JO}!PQ`~dhC|UfM4`Ft*+KF zCoBlpuDqhG-s!3gR5TT3xiZEW!uN1QxVA|X*ey7J_nc}y4BfNzl6ohIP9@-zA&@`&CeAeii%%ZHbqLf}u>eex9 zt2{nYsnd2>Lt~zFWa)R|+T*hVo?53%uvaTbqApy!ck7rVYba7)Is4^x6)G$TW3-|2 zt-PAZ%JEq$MbTAb^(uWt>J}BRc)%Y{4pkqe3Dq^mhxfdB#sAbsI^d~wx&(WaQK#*# z_FEl3T^FsK>=#8>jn%6)YUE_M;0b69F*Lv`^Y4UveOT z00IagfB*srAb z7U-hwrw({(oi4#1rCj?((N$yhYK_|7YQNRt)0wn=O${!qP4Fp6Zow1Kn3WkX_(M;d z!6SeG0tg_000IagfB*srAbjauX1N99%r&ZcTjs26vImoW_coY3iE;u5 z0tg_000IagfB*srAbnidq1ba7B^^D?eY?5MX#C* z`u5(svOMoIp?K`#XVT+rsXyC4TJ`hT#2!gmXZC;j(70_)O{4QKG9I|Q@pAXn?k~Sr zm%q0>rs(f}TD43weCt2n-=C=VpT6!B?WzHu-Q`n? zUX9WvZu{bem$HVr-szKnk5RQfZOV^@*_P#xEy&8& zjkzIq;AN-uJn-P9Z{FE5ZTgOnHov{Sr>f=9gCBn1`~2Ix8|q&g_hen#v-f4CjQQub zcL#j=gMW_g!c&{F<{tXzHLHGHIC5giV-weEa;IiZo>Fqt?xA-rpDfKzoW7=4(Zt2? z^gNOhIQ*04vV~vl+EF_o>79`Q%X^zo?Nf4Dt)upi%T@o-+@kisoREC;=^HP}s5xUv zLEk=WCx3d4cGl{I??zXvgFT*V>AL#y%ih}f#_4P4JoRQ^%uW5bZQZu5aBg*d`Umgk z#OK|9*Pt~!_vQ}QjXq^)WkXWN-$wR2bn4ctXXPE4{rQ0hzy37yhE2DAGw{Y9r+0hs z`f1ZME_h+u$|q+_eJ^nBe(Wx9$y2)?F`K7qHmx83le77)fiL`Y|640fug$HToPX>1 zoBp-#NRs-+5ua~(IccVCo7Y!)_soh#J#YUsVV*FSKkl4+;ZUHx+8VBj(XJC zZT!p;(b)^CdKWuRd1Z;;WQo~#!<*a;5D$Fa*y)Wndg-*BCAtW_;9IB|;j|NQb*;t-6(=%ySW>#Ic!?kFd z!Kf-*lH6_|Dto{oiVl~M==A?D_ndlqc&FUHeO%F|v-=nh&Rut<<(k>$=k^-EFg9w~ ztLsOvozT7Lu0`+lTR7FQ`OLbt(dUi)uHu%bTP#z5NS$`i@}k#@yhol`|DsS3cy#ZV zH-CKitmob-RNs1m{hBv=<-EG}imDl1x+M%gI^@W(K~3&&iYE_V7jxGcOFnx@`=#cs zk574aXUV`fdKScu-170Xeow4zxN*pIe0zLyaKi@I`8AtzrE{PA^7cz#9kOZs>TVY=&(mHsK_B;B;Iwxa`TF>C z-#%ybWuoizICt*UXY3c0|9CiU$B@x?Um#rb#uNI=8O#4ZAkG6-My6`tnyRsxM!?eusf~cN37E;)QXMmZ~OSv zAjj;B%v)1<++Fht0ckh;w3Aa{G`)m3W^@+B-rd;g}o_FE$^q#ffKXr7Id-_jb z|Mc_GH$T4o@P+%OVRr?}uYUb66)SfAL$&wcld;lP&bGkvq_*1mjnRbJ84 z=lJbMj_!NU`;Klzld?i#&vd)Bgj4UKcMms}M;siAM+;1<`Cm-c+t?amidMjblM^KfO$&(kh`y7syq z`m-+SeRS?qmu$Rk@A*qVxFYTm{Yy1zDdV5kd|f51Ts!=k8(*2bW%(T+JojDIL0kSS z=RS7xut8%74SH?FGgoO-VkTx>`11MHuivS8AUWB5^IH!+Fl2O8=?xDJoOWIG{X5Q_ zJE!l$Rkg9(>fagaic8lQK5p(Fv*^K?`zvGCRL0()cJ%xgKWq>$PwMsJaO2VUuef$y zZ1vc;Q*H_7?wqyrlzt6Omk!SVN6Rf`8jy{cN`q4ypOT*ZYE+Ns&VPIBEd!fP->&^vzV*8^r^WjE zBwVra#;vd1cJQMCuE5(_LYKV1ynoZ$O9nqVU%P1FLaSwob@?)k`%?3nu1jzW;3be0856E?NCvOZ6is-J++*ep%jBKBc)({+zF# zo>g_BI!fN6=4Cnm#uiOT8J(FZKiz-J_Iy9QJukSbzIf}bKF>*4zZAQDndgqzUijqj z5BF~!`p5@YzSQl(sn@+YZffDfr*2yD*u4qYKKtxS>yr=kO&UHmG2iz>)~V*@(-)V1 z{N`O}Tzl?{J}*Ca$@L%gSar^}OAZ`f=h*hG@zpK{S9QQ~pkzwY+Cx2G2Vz3K1oe>hnyCg#3!I9L1A?Nfg& zTKmnoSI;ge%<#Sy`1IbSy_fVpZP2H8tj>6MzwhG_*LUf?cFwYgtCyX>_2zf_T=HVy zx!(;`|8SstWkt!pHTUQo{h#~zjuBPBl@sD@1@um?z!rW#^A?U+k-bw zsGfV(>h-;2UK*Fz@VCB?FSEV!NY5MRJ+4dmYt5QBPboV0vFM#ME^w^-=bVPY*-Z{p z!th}aufB2bJ*9sy8oOb^Gd=b^EbV(@Sl6dtuh};0@x^zP7deZs&3dTU%V%xAy^s04 z_Y;47t?O&z-tONgoR#zQ;xE2g_24zxvp#!t?)-6&244AFLH%{=y)VA@@{VDJ*Pl{& z+V1GLyqE6mcgCxm<8Ha3YReVZZQlR>C4G9_u<4bRLmsaBw5jTpjlsVT7<1p^kD>=I zzVoJAzH;6C%p;YrT|IEtDgM(RzI~eYvWjO)N^Fg;*p}rlJnR{DaAS{xAJ#u`^BFUq zjsLXA;H1|cAADKwi?b#S+H`BhtNC~ItSuh0<%%s&U-RITYnN_#LK?jFv1j{lywq9q z-rBEDy}UM9w4&hi=@*af`*-8(+XiNx)pzHZ&39~?{OB7aPCIh(_e~GwTNZcwsP2<> zE7SXLdA;%aLCzVktiN>Jl)cX{PrrZpu64b?-BNe+mZNLmE?K&}Pf69i6SvwgeSG?r z!wRqb_^vCT^%s6Hw{YIY``+yHNza_c-+mjrH)rYS+W5!DOqw<4%4d%@7H;j8x7EJ+ zi&=NC8~b(jkBhshbCz}+nA>pp^TYr1Ue_&wN49?87;)jr_n^-5Kt;yra$BjhxMWs( z@!}Mnya%-xj%?k7@;4FX4Jh&zMEsOL4JyohMJ=Ln~Z|ox{8vKPNd4@ePF)>|w zMy=R4N=`c6Svk!=L3FzXk6)RYYJyj=`USfs==9fF@(WAK?TLaxrBbJdcKA!1uY9mX zJGoo#yCYW&Ty2`US~Y6Hwt}Y@T)jXuye0Pe<(7&6v}AoUXpnd20?nNpc8%2q9iMNy z?Sb9%x32nm_P3j)YgHE)I^R0~ys10hX-bJVFWC64yJ~CcWkWx7?%J7q_Ryt=?|5!^ z%d;PRvS{}EE7PAfW{f(cp?2ris0rPot^EqKzRnmqGqvB1#$^Njdv6zYmVxQHnn!1U zAH8UM!K|z+KHgUMKLcO3KX&lW`y5HCyva8YYT5dJT7F>O&zj`KHI7>cUUc`}dyMOy zXAjdn@Xf13*3ZAMbY!3O7i)(W-q8K69~$R;|LpUF-gxcVPxf5=^b471PK{ZcU4QGY z$|ufinbT)Rz3rX#WeYCNc{lC;%ibB4eA~eJhkix8 ztlBW=0by*y+DC@|xbt68z7LmAoA`k@M}a-zTk~m&v#ywwl~7xp?=H2M+3OdlX#Y=Z zCbj*VNl9hR{HfPW&I1_}h0Ljr=E{UA_O!(*n*Y<9Nol`kl36oDs^tf`9^JZj@&Z)0cekE$0Z&pbgBuV`wbt2Ao9IcD14E&%rFUB~ zx7aLt9f>x(CsDq1x?I*okKj*o3wEb9$?0|^)jGXGt=QPUh4HD$DQOAGS^7L(i~0ZD zQbNk;%)|zdBWkoZ*w%>EZWleidq77AToo{3Y@{E#Flan)& z+*Z4g5NJqfoqkeULP~mST1rMrMrKN8YKHEA-9T#l22xVB|LX=)+BcA#su?d&!KB=? z-#Uflb`4~VPBzQyT_ZiClHQR64g?TD009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1pdGRaT={Equgb+3BIH$ZmZXyBZ~F%xqhHpt17H% zjg))E2Eprh%7yHL&*pVD_?@CBTAsH?+N_d3mIDq15I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009L4$N~ekg{B%&tan?z_2pi%LGb#W zf-lJ~_-tNhgWoB7`e-xM~A} zGUN*m1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmdXNa)EeV7nNGA8fa9j_4-&%grkcEPJeeS#n-32uws>1%LVn*_T>^e8Eo$bfv7N>Oyx zSiP;|DtyKm>PtSG<-4wvQdH6wX}NU1e8GVL0tg_000IagfB*srAb?dh2~1BGpd6 z;C6cKLSu$S{+FcH4IBSU#T%?%!Q*eOkY-64t9j{90(wQ00IagfB*srAbc!eqO@wYF;)o;W+l{jQ9@mm5^8lysL|@QCbdSVRjFf?AX*7bN?=q1gA(Yq zT9qkUo?oogMJ4?peJgz>?Upu64@fsiOQc!Sxl+EAD)pB$IS@bq0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**{+k4PXyes>XQJS8h@vAg z7z`$wdum6h-J-31T1+o(g4*E~6Rj?nvsrY93U)bBV)Tg;O(#l>I#HtTM2XrHC2D$V z6HJ|l6sJw=Fif9EwA-zTu{y0vZBzn-66mAlI~*%Lp^|=-4oiomFQom_2htvCxAeNS zO?pwjB+NcBuCD1EDloIHaK&u2At=4FY zmgg5M-J_Dem;NOkls=O_l-`wgOWUQb(sR-V=`racIg9j_*Q3(u6pjQ%q?h-A}FIKulCH*KJmJUf@Nc*J^ zq&?DZX^Zp^=^^QMX{DUWfdB#sAbXf;_eG}Y1<;vi=NkX(dtb~14T#khDXcu?J8ZTlD?O|mOht0l=etFrB|dE zrA^Y4(t7DW=?-bNoXvp%0tg_000IagfB*srAbMP1jJa zCN`9-G3#|EwOSh$I%#xKI+IC`=}bnoMyE3xP0{k~x=O=U($~_5(oX3G=?Q7Av`Sho zEtGtcU8<5MN~7d#4g?TD009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1VRF_I<;PHR7J&3FDl-8erzae2uIf+G>4-4aCA+1Oeh)^icZ?m z*d-Lzg`;a$Mu(!>aCFXUQz)tlM=P%|nsp|dTG5@0ZmGcMB z+duE2dH%WI&D||$av*>J0tg_000IagfB*srAb;AD5Co)NO675PSib-#0EL+1Ov3 z-C8V^pVcv2KS$Tn)S6!_y6l2?Tuxb8QAu`1(Uj_vywa+nt*KR|6AO#;a;w{NBN>%t zWyLw!6+>qQ1aDKNGLn?mk)(F)YN*g?a|P_eWVxVIkjG{ir+Bqk!C3q65rnGfxjPJm|=p2<0Y)N1o6+ z@}$^kd6&mZJyp^HX^*s3dQ!Sax?Z|SYLpz(R4Gr&kOs?{90(wQ00IagfB*srAbKOJSDRWt0#CTS5KJeH7W_U z6Z5k3CkDmz-16dBC80)~lxAzptDHS4V^WloFry+Zy?L6LKOuLvpivU4it5YiCsYfy zWi}c2BYSv`RvXTg-1L7ix<#(`{XpgoJ`%eMNKLIT_{kll9T^9quY=RMG)ykF-^K zQo2XFUb;wXlpNAI(nKjk8Z7mcvpEnz009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1pf2_-Lym0!3j;H8p~&muE{LQ(klr8Z$;2QdO~xl zvpyITO7PS=U4p$@Iii#VzbLwDtlm(9Hk4p>_(Cx`&~?=gF?mJNA4yOOxCKuj64xjR z&Ze17=QK^rv)Y8rE=t1mfLM`x&h%t!pfNdCNtisf!X9+aC=y2(+6_uVc~e$d%Cy2v zU%5TlHI$H6Z*2^wRAl7`^P`o7vU;bhGEmV}l;z4WDhUORWiwp9s@a~>vQ(3jkm+hJ ztjlZ+R6D90%+bpIUGS&B$xILd1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmdVX6o^)G7yM!oG7&%k0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5ctyzM9VYON$;zq@1;Z1r}6~{0tg_0 z00IagfB*srAbFs5o!Vs5{y&EWK&$`& literal 0 HcmV?d00001 diff --git a/toolkit/components/places/tests/migration/test_current_from_v26.js b/toolkit/components/places/tests/migration/test_current_from_v26.js new file mode 100644 index 000000000000..16705f85fea8 --- /dev/null +++ b/toolkit/components/places/tests/migration/test_current_from_v26.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function* setup() { + yield setupPlacesDatabase("places_v26.sqlite"); + // Setup database contents to be migrated. + let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME); + let db = yield Sqlite.openConnection({ path }); + // Add pages. + yield db.execute(`INSERT INTO moz_places (url, guid) + VALUES ("http://test1.com/", "test1_______") + , ("http://test2.com/", "test2_______") + `); + // Add keywords. + yield db.execute(`INSERT INTO moz_keywords (keyword) + VALUES ("kw1") + , ("kw2") + , ("kw3") + `); + // Add bookmarks. + let now = Date.now() * 1000; + let index = 0; + yield db.execute(`INSERT INTO moz_bookmarks (type, fk, parent, position, dateAdded, lastModified, keyword_id, guid) + VALUES (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark1___") + /* same uri, different keyword */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw2'), "bookmark2___") + /* different uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test2_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark3___") + /* same uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark4___") + /* same uri, same keyword as 2 */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test2_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw2'), "bookmark5___") + /* different uri, same keyword as 1 */ + , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now}, + (SELECT id FROM moz_keywords WHERE keyword = 'kw3'), "bookmark6___") + `); + // Add postData. + yield db.execute(`INSERT INTO moz_anno_attributes (name) + VALUES ("bookmarkProperties/POSTData")`); + yield db.execute(`INSERT INTO moz_items_annos(anno_attribute_id, item_id, content) + VALUES ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark3___"), "postData1") + , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"), + (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2")`); + yield db.close(); +}); + +add_task(function* database_is_valid() { + Assert.equal(PlacesUtils.history.databaseStatus, + PlacesUtils.history.DATABASE_STATUS_UPGRADED); + + let db = yield PlacesUtils.promiseDBConnection(); + Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION); +}); + +add_task(function* test_keywords() { + // When 2 urls have the same keyword, if one has postData it will be + // preferred. + let [ url1, postData1 ] = PlacesUtils.getURLAndPostDataForKeyword("kw1"); + Assert.equal(url1, "http://test2.com/"); + Assert.equal(postData1, "postData1"); + let [ url2, postData2 ] = PlacesUtils.getURLAndPostDataForKeyword("kw2"); + Assert.equal(url2, "http://test2.com/"); + Assert.equal(postData2, "postData2"); + let [ url3, postData3 ] = PlacesUtils.getURLAndPostDataForKeyword("kw3"); + Assert.equal(url3, "http://test1.com/"); + + Assert.equal((yield foreign_count("http://test1.com/")), 5); // 4 bookmark2 + 1 keywords + Assert.equal((yield foreign_count("http://test2.com/")), 4); // 2 bookmark2 + 2 keywords +}); diff --git a/toolkit/components/places/tests/migration/xpcshell.ini b/toolkit/components/places/tests/migration/xpcshell.ini index cc3c24773776..a22d17d60bf6 100644 --- a/toolkit/components/places/tests/migration/xpcshell.ini +++ b/toolkit/components/places/tests/migration/xpcshell.ini @@ -15,6 +15,7 @@ support-files = places_v24.sqlite places_v25.sqlite places_v26.sqlite + places_v27.sqlite [test_current_from_downgraded.js] [test_current_from_v6.js] @@ -22,3 +23,4 @@ support-files = [test_current_from_v19.js] [test_current_from_v24.js] [test_current_from_v25.js] +[test_current_from_v26.js] diff --git a/toolkit/components/places/tests/queries/test_sorting.js b/toolkit/components/places/tests/queries/test_sorting.js index e6c28ce3b263..a5ca67586cea 100644 --- a/toolkit/components/places/tests/queries/test_sorting.js +++ b/toolkit/components/places/tests/queries/test_sorting.js @@ -504,7 +504,7 @@ tests.push({ // if keywords are equal, should fall back to title { isBookmark: true, - uri: "http://example.com/b2", + uri: "http://example.com/b1", parentFolder: PlacesUtils.bookmarks.toolbarFolder, index: PlacesUtils.bookmarks.DEFAULT_INDEX, title: "y8", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js index 0d133a4e8b53..62fb1a97f75c 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js @@ -19,30 +19,30 @@ add_task(function* test_keyword_searc() { { uri: uri1, title: "Generic page title" }, { uri: uri2, title: "Generic page title" } ]); - yield addBookmark({ uri: uri1, title: "Keyword title", keyword: "key"}); + yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"}); do_print("Plain keyword query"); yield check_autocomplete({ search: "key term", - matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "Keyword title", style: ["keyword"] } ] + matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "Generic page title", style: ["keyword"] } ] }); do_print("Multi-word keyword query"); yield check_autocomplete({ search: "key multi word", - matches: [ { uri: NetUtil.newURI("http://abc/?search=multi+word"), title: "Keyword title", style: ["keyword"] } ] + matches: [ { uri: NetUtil.newURI("http://abc/?search=multi+word"), title: "Generic page title", style: ["keyword"] } ] }); do_print("Keyword query with +"); yield check_autocomplete({ search: "key blocking+", - matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "Keyword title", style: ["keyword"] } ] + matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "Generic page title", style: ["keyword"] } ] }); do_print("Unescaped term in query"); yield check_autocomplete({ search: "key ユニコード", - matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "Keyword title", style: ["keyword"] } ] + matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "Generic page title", style: ["keyword"] } ] }); do_print("Keyword that happens to match a page"); @@ -54,25 +54,13 @@ add_task(function* test_keyword_searc() { do_print("Keyword without query (without space)"); yield check_autocomplete({ search: "key", - matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Keyword title", style: ["keyword"] } ] + matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Generic page title", style: ["keyword"] } ] }); do_print("Keyword without query (with space)"); yield check_autocomplete({ search: "key ", - matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Keyword title", style: ["keyword"] } ] - }); - - // This adds a second keyword so anything after this will match 2 keywords - let uri3 = NetUtil.newURI("http://xyz/?foo=%s"); - yield PlacesTestUtils.addVisits([ { uri: uri3, title: "Generic page title" } ]); - yield addBookmark({ uri: uri3, title: "Keyword title", keyword: "key", style: ["keyword"] }); - - do_print("Two keywords matched"); - yield check_autocomplete({ - search: "key twoKey", - matches: [ { uri: NetUtil.newURI("http://abc/?search=twoKey"), title: "Keyword title", style: ["keyword"] }, - { uri: NetUtil.newURI("http://xyz/?foo=twoKey"), title: "Keyword title", style: ["keyword"] } ] + matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Generic page title", style: ["keyword"] } ] }); yield cleanup(); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js index 3c6c5d804f8a..410411ddeda9 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js @@ -19,68 +19,55 @@ add_task(function* test_keyword_search() { { uri: uri1, title: "Generic page title" }, { uri: uri2, title: "Generic page title" } ]); - yield addBookmark({ uri: uri1, title: "Keyword title", keyword: "key"}); + yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"}); do_print("Plain keyword query"); yield check_autocomplete({ search: "key term", searchParam: "enable-actions", - matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), title: "Keyword title", style: [ "action", "keyword" ] } ] + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), title: "Generic page title", style: [ "action", "keyword" ] } ] }); do_print("Multi-word keyword query"); yield check_autocomplete({ search: "key multi word", searchParam: "enable-actions", - matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi+word", input: "key multi word"}), title: "Keyword title", style: [ "action", "keyword" ] } ] + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi+word", input: "key multi word"}), title: "Generic page title", style: [ "action", "keyword" ] } ] }); do_print("Keyword query with +"); yield check_autocomplete({ search: "key blocking+", searchParam: "enable-actions", - matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), title: "Keyword title", style: [ "action", "keyword" ] } ] + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), title: "Generic page title", style: [ "action", "keyword" ] } ] }); do_print("Unescaped term in query"); yield check_autocomplete({ search: "key ユニコード", searchParam: "enable-actions", - matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ユニコード", input: "key ユニコード"}), title: "Keyword title", style: [ "action", "keyword" ] } ] + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ユニコード", input: "key ユニコード"}), title: "Generic page title", style: [ "action", "keyword" ] } ] }); do_print("Keyword that happens to match a page"); yield check_autocomplete({ search: "key ThisPageIsInHistory", searchParam: "enable-actions", - matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), title: "Keyword title", style: [ "action", "keyword" ] } ] + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), title: "Generic page title", style: [ "action", "keyword" ] } ] }); do_print("Keyword without query (without space)"); yield check_autocomplete({ search: "key", searchParam: "enable-actions", - matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), title: "Keyword title", style: [ "action", "keyword" ] } ] + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), title: "Generic page title", style: [ "action", "keyword" ] } ] }); do_print("Keyword without query (with space)"); yield check_autocomplete({ search: "key ", searchParam: "enable-actions", - matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}), title: "Keyword title", style: [ "action", "keyword" ] } ] - }); - - // This adds a second keyword so anything after this will match 2 keywords - let uri3 = NetUtil.newURI("http://xyz/?foo=%s"); - yield PlacesTestUtils.addVisits([ { uri: uri3, title: "Generic page title" } ]); - yield addBookmark({ uri: uri3, title: "Keyword title", keyword: "key"}); - - do_print("Two keywords matched"); - yield check_autocomplete({ - search: "key twoKey", - searchParam: "enable-actions", - matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=twoKey", input: "key twoKey"}), title: "Keyword title", style: [ "action", "keyword" ] }, - { uri: makeActionURI("keyword", {url: "http://xyz/?foo=twoKey", input: "key twoKey"}), title: "Keyword title", style: [ "action", "keyword" ] } ] + matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}), title: "Generic page title", style: [ "action", "keyword" ] } ] }); yield cleanup(); diff --git a/toolkit/components/places/tests/unit/test_398914.js b/toolkit/components/places/tests/unit/test_398914.js index 1e4ac8ac0eb4..5c4ee37afcdb 100644 --- a/toolkit/components/places/tests/unit/test_398914.js +++ b/toolkit/components/places/tests/unit/test_398914.js @@ -1,13 +1,3 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim:set ts=2 sw=2 sts=2 et: */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const bmsvc = PlacesUtils.bookmarks; -const testFolderId = PlacesUtils.bookmarksMenuFolderId; - -// main function run_test() { var testURI = uri("http://foo.com"); @@ -16,11 +6,11 @@ function run_test() { 2. Create a bookmark for the same URI, with a different keyword and different post data. 3. Confirm that our method for getting a URI+postdata retains bookmark affinity. */ - var bm1 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah"); - bmsvc.setKeywordForBookmark(bm1, "foo"); + var bm1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, testURI, -1, "blah"); + PlacesUtils.bookmarks.setKeywordForBookmark(bm1, "foo"); PlacesUtils.setPostDataForBookmark(bm1, "pdata1"); - var bm2 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah"); - bmsvc.setKeywordForBookmark(bm2, "bar"); + var bm2 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, testURI, -1, "blah"); + PlacesUtils.bookmarks.setKeywordForBookmark(bm2, "bar"); PlacesUtils.setPostDataForBookmark(bm2, "pdata2"); // check kw, pd for bookmark 1 @@ -35,116 +25,6 @@ function run_test() { do_check_eq(postdata, "pdata2"); // cleanup - bmsvc.removeItem(bm1); - bmsvc.removeItem(bm2); - - /* - 1. Create two bookmarks with the same URI and keyword. - 2. Confirm that the most recently created one is returned for that keyword. - */ - var bm1 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah"); - bmsvc.setKeywordForBookmark(bm1, "foo"); - PlacesUtils.setPostDataForBookmark(bm1, "pdata1"); - var bm2 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah"); - bmsvc.setKeywordForBookmark(bm2, "foo"); - PlacesUtils.setPostDataForBookmark(bm2, "pdata2"); - - var bm1da = bmsvc.getItemDateAdded(bm1); - var bm1lm = bmsvc.getItemLastModified(bm1); - LOG("bm1 dateAdded: " + bm1da + ", lastModified: " + bm1lm); - var bm2da = bmsvc.getItemDateAdded(bm2); - var bm2lm = bmsvc.getItemLastModified(bm2); - LOG("bm2 dateAdded: " + bm2da + ", lastModified: " + bm2lm); - do_check_true(bm1da <= bm2da); - do_check_true(bm1lm <= bm2lm); - - [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo"); - do_check_eq(testURI.spec, url); - do_check_eq(postdata, "pdata2"); - - // cleanup - bmsvc.removeItem(bm1); - bmsvc.removeItem(bm2); - - /* - 1. Create two bookmarks with the same URI and keyword. - 2. Modify the first-created bookmark. - 3. Confirm that the most recently modified one is returned for that keyword. - */ - var bm1 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah"); - bmsvc.setKeywordForBookmark(bm1, "foo"); - PlacesUtils.setPostDataForBookmark(bm1, "pdata1"); - var bm2 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah"); - bmsvc.setKeywordForBookmark(bm2, "foo"); - PlacesUtils.setPostDataForBookmark(bm2, "pdata2"); - - // modify the older bookmark - bmsvc.setItemTitle(bm1, "change"); - - var bm1da = bmsvc.getItemDateAdded(bm1); - var bm1lm = bmsvc.getItemLastModified(bm1); - LOG("bm1 dateAdded: " + bm1da + ", lastModified: " + bm1lm); - var bm2da = bmsvc.getItemDateAdded(bm2); - var bm2lm = bmsvc.getItemLastModified(bm2); - LOG("bm2 dateAdded: " + bm2da + ", lastModified: " + bm2lm); - do_check_true(bm1da <= bm2da); - // the last modified for bm1 should be at least as big as bm2 - // but could be equal if the test runs faster than our PRNow() - // granularity - do_check_true(bm1lm >= bm2lm); - - // we need to ensure that bm1 last modified date is greater - // that the modified date of bm2, otherwise in case of a "tie" - // bm2 will win, as it has a bigger item id - if (bm1lm == bm2lm) - bmsvc.setItemLastModified(bm1, bm2lm + 1000); - - [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo"); - do_check_eq(testURI.spec, url); - do_check_eq(postdata, "pdata1"); - - // cleanup - bmsvc.removeItem(bm1); - bmsvc.removeItem(bm2); - - /* - Test that id breaks ties: - 1. Create two bookmarks with the same URI and keyword, dateAdded and lastModified. - 2. Confirm that the most recently created one is returned for that keyword. - */ - var testDate = Date.now() * 1000; - var bm1 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah"); - bmsvc.setKeywordForBookmark(bm1, "foo"); - PlacesUtils.setPostDataForBookmark(bm1, "pdata1"); - bmsvc.setItemDateAdded(bm1, testDate); - bmsvc.setItemLastModified(bm1, testDate); - - var bm2 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah"); - bmsvc.setKeywordForBookmark(bm2, "foo"); - PlacesUtils.setPostDataForBookmark(bm2, "pdata2"); - bmsvc.setItemDateAdded(bm2, testDate); - bmsvc.setItemLastModified(bm2, testDate); - - var bm1da = bmsvc.getItemDateAdded(bm1, testDate); - var bm1lm = bmsvc.getItemLastModified(bm1); - LOG("bm1 dateAdded: " + bm1da + ", lastModified: " + bm1lm); - var bm2da = bmsvc.getItemDateAdded(bm2); - var bm2lm = bmsvc.getItemLastModified(bm2); - LOG("bm2 dateAdded: " + bm2da + ", lastModified: " + bm2lm); - - do_check_eq(bm1da, bm2da); - do_check_eq(bm1lm, bm2lm); - - - var ids = bmsvc.getBookmarkIdsForURI(testURI); - do_check_eq(ids[0], bm2); - do_check_eq(ids[1], bm1); - - [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo"); - do_check_eq(testURI.spec, url); - do_check_eq(postdata, "pdata2"); - - // cleanup - bmsvc.removeItem(bm1); - bmsvc.removeItem(bm2); + PlacesUtils.bookmarks.removeItem(bm1); + PlacesUtils.bookmarks.removeItem(bm2); } diff --git a/toolkit/components/places/tests/unit/test_async_transactions.js b/toolkit/components/places/tests/unit/test_async_transactions.js index 61bddb9a5c4f..de59152de63e 100644 --- a/toolkit/components/places/tests/unit/test_async_transactions.js +++ b/toolkit/components/places/tests/unit/test_async_transactions.js @@ -739,11 +739,7 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() { , newValue: ANNO.value }, { guid: b2_info.guid , property: "keyword" - , newValue: KEYWORD }, - { guid: b2_info.guid - , isAnnoProperty: true - , property: PlacesUtils.POST_DATA_ANNO - , newValue: POST_DATA } ]; + , newValue: KEYWORD } ]; ensureItemsChanged(...b2_post_creation_changes); ensureTags([TAG_1, TAG_2]); diff --git a/toolkit/components/places/tests/unit/test_placesTxn.js b/toolkit/components/places/tests/unit/test_placesTxn.js index a715f0199885..f8bb1aa83b7c 100644 --- a/toolkit/components/places/tests/unit/test_placesTxn.js +++ b/toolkit/components/places/tests/unit/test_placesTxn.js @@ -721,15 +721,19 @@ add_test(function test_edit_postData() { let postData = "post-test_edit_postData"; let testURI = NetUtil.newURI("http://test_edit_postData.com"); let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit Post Data"); - + PlacesUtils.bookmarks.setKeywordForBookmark(testBkmId, "kw"); let txn = new PlacesEditBookmarkPostDataTransaction(testBkmId, postData); txn.doTransaction(); - do_check_true(annosvc.itemHasAnnotation(testBkmId, POST_DATA_ANNO)); - do_check_eq(annosvc.getItemAnnotation(testBkmId, POST_DATA_ANNO), postData); + let [url, post_data] = PlacesUtils.getURLAndPostDataForKeyword("kw"); + Assert.equal(url, testURI.spec); + Assert.equal(postData, post_data); txn.undoTransaction(); - do_check_false(annosvc.itemHasAnnotation(testBkmId, POST_DATA_ANNO)); + [url, post_data] = PlacesUtils.getURLAndPostDataForKeyword("kw"); + Assert.equal(url, testURI.spec); + Assert.equal(null, post_data); + run_next_test(); }); diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js index 8d85e188b203..490e6cff1abd 100644 --- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js +++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js @@ -544,53 +544,6 @@ tests.push({ //------------------------------------------------------------------------------ -tests.push({ - name: "D.5", - desc: "Fix wrong keywords", - - _validKeywordItemId: null, - _invalidKeywordItemId: null, - _validKeywordId: 1, - _invalidKeywordId: 8888, - _placeId: null, - - setup: function() { - // Insert a keyword - let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword) VALUES(:id, :keyword)"); - stmt.params["id"] = this._validKeywordId; - stmt.params["keyword"] = "used"; - stmt.execute(); - stmt.finalize(); - // Add a place to ensure place_id = 1 is valid - this._placeId = addPlace(); - // Add a bookmark using the keyword - this._validKeywordItemId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, bs.unfiledBookmarksFolder, this._validKeywordId); - // Add a bookmark using a nonexistent keyword - this._invalidKeywordItemId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, bs.unfiledBookmarksFolder, this._invalidKeywordId); - }, - - check: function() { - // Check that item with valid keyword is there - let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND keyword_id = :keyword"); - stmt.params["item_id"] = this._validKeywordItemId; - stmt.params["keyword"] = this._validKeywordId; - do_check_true(stmt.executeStep()); - stmt.reset(); - // Check that item with invalid keyword has been corrected - stmt.params["item_id"] = this._invalidKeywordItemId; - stmt.params["keyword"] = this._invalidKeywordId; - do_check_false(stmt.executeStep()); - stmt.finalize(); - // Check that item with invalid keyword has not been removed - stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id"); - stmt.params["item_id"] = this._invalidKeywordItemId; - do_check_true(stmt.executeStep()); - stmt.finalize(); - } -}); - -//------------------------------------------------------------------------------ - tests.push({ name: "D.6", desc: "Fix wrong item types | bookmarks", @@ -1053,27 +1006,17 @@ tests.push({ setup: function() { // Insert 2 keywords - let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword) VALUES(:id, :keyword)"); + let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword, place_id) VALUES(:id, :keyword, :place_id)"); stmt.params["id"] = 1; - stmt.params["keyword"] = "used"; - stmt.execute(); - stmt.reset(); - stmt.params["id"] = 2; stmt.params["keyword"] = "unused"; + stmt.params["place_id"] = 100; stmt.execute(); stmt.finalize(); - // Add a place to ensure place_id = 1 is valid - this._placeId = addPlace(); - // Insert a bookmark using the "used" keyword - this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, bs.unfiledBookmarksFolder, 1); }, check: function() { // Check that "used" keyword is still there let stmt = mDBConn.createStatement("SELECT id FROM moz_keywords WHERE keyword = :keyword"); - stmt.params["keyword"] = "used"; - do_check_true(stmt.executeStep()); - stmt.reset(); // Check that "unused" keyword has gone stmt.params["keyword"] = "unused"; do_check_false(stmt.executeStep()); diff --git a/toolkit/components/places/tests/unit/test_telemetry.js b/toolkit/components/places/tests/unit/test_telemetry.js index a0f18b7a8719..0cb3230b4e46 100644 --- a/toolkit/components/places/tests/unit/test_telemetry.js +++ b/toolkit/components/places/tests/unit/test_telemetry.js @@ -32,7 +32,7 @@ function run_test() add_task(function test_execute() { // Put some trash in the database. - const URI = NetUtil.newURI("http://moz.org/"); + let uri = NetUtil.newURI("http://moz.org/"); let folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, "moz test", @@ -64,7 +64,7 @@ add_task(function test_execute() // Test expiration probes. for (let i = 0; i < 2; i++) { yield PlacesTestUtils.addVisits({ - uri: uri("http://" + i + ".moz.org/"), + uri: NetUtil.newURI("http://" + i + ".moz.org/"), visitDate: Date.now() // [sic] }); } From 05596be753d0536231fce2c9a2dbcc30674a8347 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Fri, 20 Mar 2015 09:39:25 +0100 Subject: [PATCH 17/80] Bug 1125115 - Write a new keywords pseudo-API in PlacesUtils. r=ttaubert --- .../test/general/browser_getshortcutoruri.js | 67 ++- .../general/browser_keywordBookmarklets.js | 50 +- browser/components/nsBrowserGlue.js | 32 +- .../places/tests/unit/head_bookmarks.js | 16 - .../tests/unit/test_browserGlue_corrupt.js | 2 +- .../unit/test_browserGlue_corrupt_nobackup.js | 2 +- ...st_browserGlue_corrupt_nobackup_default.js | 2 +- .../tests/unit/test_browserGlue_migrate.js | 2 +- .../tests/unit/test_browserGlue_prefs.js | 4 +- .../tests/unit/test_browserGlue_restore.js | 2 +- .../unit/test_browserGlue_smartBookmarks.js | 2 - toolkit/components/places/Bookmarks.jsm | 38 +- .../places/PlacesCategoriesStarter.js | 17 +- toolkit/components/places/PlacesUtils.jsm | 379 +++++++++++++- toolkit/components/places/nsNavBookmarks.cpp | 82 +-- .../places/tests/bookmarks/test_bookmarks.js | 42 +- .../places/tests/bookmarks/test_keywords.js | 9 +- .../unifiedcomplete/test_keyword_search.js | 2 +- .../places/tests/unit/test_421180.js | 94 ---- .../places/tests/unit/test_keywords.js | 488 ++++++++++++++++++ .../places/tests/unit/test_placesTxn.js | 4 +- .../components/places/tests/unit/xpcshell.ini | 2 +- 22 files changed, 963 insertions(+), 375 deletions(-) delete mode 100644 toolkit/components/places/tests/unit/test_421180.js create mode 100644 toolkit/components/places/tests/unit/test_keywords.js diff --git a/browser/base/content/test/general/browser_getshortcutoruri.js b/browser/base/content/test/general/browser_getshortcutoruri.js index dd47b84d4a14..83959e815fae 100644 --- a/browser/base/content/test/general/browser_getshortcutoruri.js +++ b/browser/base/content/test/general/browser_getshortcutoruri.js @@ -91,57 +91,54 @@ var testData = [ new keywordResult(null, null, true)] ]; -function test() { - waitForExplicitFinish(); +add_task(function* test_getshortcutoruri() { + yield setupKeywords(); - setupKeywords(); + for (let item of testData) { + let [data, result] = item; - Task.spawn(function() { - for each (var item in testData) { - let [data, result] = item; + let query = data.keyword; + if (data.searchWord) + query += " " + data.searchWord; + let returnedData = yield new Promise( + resolve => getShortcutOrURIAndPostData(query, resolve)); + // null result.url means we should expect the same query we sent in + let expected = result.url || query; + is(returnedData.url, expected, "got correct URL for " + data.keyword); + is(getPostDataString(returnedData.postData), result.postData, "got correct postData for " + data.keyword); + is(returnedData.mayInheritPrincipal, !result.isUnsafe, "got correct mayInheritPrincipal for " + data.keyword); + } - let query = data.keyword; - if (data.searchWord) - query += " " + data.searchWord; - let returnedData = yield new Promise( - resolve => getShortcutOrURIAndPostData(query, resolve)); - // null result.url means we should expect the same query we sent in - let expected = result.url || query; - is(returnedData.url, expected, "got correct URL for " + data.keyword); - is(getPostDataString(returnedData.postData), result.postData, "got correct postData for " + data.keyword); - is(returnedData.mayInheritPrincipal, !result.isUnsafe, "got correct mayInheritPrincipal for " + data.keyword); - } - cleanupKeywords(); - }).then(finish); -} + yield cleanupKeywords(); +}); -var gBMFolder = null; -var gAddedEngines = []; -function setupKeywords() { - gBMFolder = Application.bookmarks.menu.addFolder("keyword-test"); - for each (var item in testData) { - var data = item[0]; +let folder = null; +let gAddedEngines = []; + +function* setupKeywords() { + folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "keyword-test" }); + for (let item of testData) { + let data = item[0]; if (data instanceof bmKeywordData) { - var bm = gBMFolder.addBookmark(data.keyword, data.uri); - bm.keyword = data.keyword; - if (data.postData) - bm.annotations.set("bookmarkProperties/POSTData", data.postData, Ci.nsIAnnotationService.EXPIRE_SESSION); + yield PlacesUtils.bookmarks.insert({ url: data.uri, parentGuid: folder.guid }); + yield PlacesUtils.keywords.insert({ keyword: data.keyword, url: data.uri.spec, postData: data.postData }); } if (data instanceof searchKeywordData) { Services.search.addEngineWithDetails(data.keyword, "", data.keyword, "", data.method, data.uri.spec); - var addedEngine = Services.search.getEngineByName(data.keyword); + let addedEngine = Services.search.getEngineByName(data.keyword); if (data.postData) { - var [paramName, paramValue] = data.postData.split("="); + let [paramName, paramValue] = data.postData.split("="); addedEngine.addParam(paramName, paramValue, null); } - gAddedEngines.push(addedEngine); } } } -function cleanupKeywords() { - gBMFolder.remove(); +function* cleanupKeywords() { + PlacesUtils.bookmarks.remove(folder); gAddedEngines.map(Services.search.removeEngine); } diff --git a/browser/base/content/test/general/browser_keywordBookmarklets.js b/browser/base/content/test/general/browser_keywordBookmarklets.js index 8b075d74cc2b..434951251bdd 100644 --- a/browser/base/content/test/general/browser_keywordBookmarklets.js +++ b/browser/base/content/test/general/browser_keywordBookmarklets.js @@ -1,38 +1,34 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict" -function test() { - waitForExplicitFinish(); - - let bmFolder = Application.bookmarks.menu.addFolder("keyword-test"); +add_task(function* test_keyword_bookmarklet() { + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmarklet", + url: "javascript:1;" }); let tab = gBrowser.selectedTab = gBrowser.addTab(); - - registerCleanupFunction (function () { - bmFolder.remove(); + registerCleanupFunction (function* () { gBrowser.removeTab(tab); + yield PlacesUtils.bookmarks.remove(bm); }); + yield promisePageShow(); + let originalPrincipal = gBrowser.contentPrincipal; - let bm = bmFolder.addBookmark("bookmarklet", makeURI("javascript:1;")); - bm.keyword = "bm"; + yield PlacesUtils.keywords.insert({ keyword: "bm", url: "javascript:1;" }) - addPageShowListener(function () { - let originalPrincipal = gBrowser.contentPrincipal; + // Enter bookmarklet keyword in the URL bar + gURLBar.value = "bm"; + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}); - // Enter bookmarklet keyword in the URL bar - gURLBar.value = "bm"; - gURLBar.focus(); - EventUtils.synthesizeKey("VK_RETURN", {}); + yield promisePageShow(); - addPageShowListener(function () { - ok(gBrowser.contentPrincipal.equals(originalPrincipal), "javascript bookmarklet should inherit principal"); - finish(); + ok(gBrowser.contentPrincipal.equals(originalPrincipal), "javascript bookmarklet should inherit principal"); +}); + +function* promisePageShow() { + return new Promise(resolve => { + gBrowser.selectedBrowser.addEventListener("pageshow", function listen() { + gBrowser.selectedBrowser.removeEventListener("pageshow", listen); + resolve(); }); }); } - -function addPageShowListener(func) { - gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() { - gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false); - func(); - }); -} diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index cb688ab001f1..e1d13296b819 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -1429,7 +1429,7 @@ BrowserGlue.prototype = { () => BookmarkHTMLUtils.exportToFile(BookmarkHTMLUtils.defaultPath)); } - Task.spawn(function() { + Task.spawn(function* () { // Check if Safe Mode or the user has required to restore bookmarks from // default profile's bookmarks.html let restoreDefaultBookmarks = false; @@ -1505,23 +1505,21 @@ BrowserGlue.prototype = { if (bookmarksUrl) { // Import from bookmarks.html file. try { - BookmarkHTMLUtils.importFromURL(bookmarksUrl, true).then(null, - function onFailure() { - Cu.reportError("Bookmarks.html file could be corrupt."); - } - ).then( - function onComplete() { - // 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(); - }.bind(this) - ); - } catch (err) { - Cu.reportError("Bookmarks.html file could be corrupt. " + err); + yield BookmarkHTMLUtils.importFromURL(bookmarksUrl, true); + } catch (e) { + Cu.reportError("Bookmarks.html file could be corrupt. " + e); } + 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); + } + } else { Cu.reportError("Unable to find bookmarks.html file."); diff --git a/browser/components/places/tests/unit/head_bookmarks.js b/browser/components/places/tests/unit/head_bookmarks.js index a45f4d1428c1..f277d34d7eba 100644 --- a/browser/components/places/tests/unit/head_bookmarks.js +++ b/browser/components/places/tests/unit/head_bookmarks.js @@ -78,22 +78,6 @@ function checkItemHasAnnotation(guid, name) { }); } -function waitForImportAndSmartBookmarks() { - return Promise.all([ - promiseTopicObserved("bookmarks-restore-success"), - PlacesTestUtils.promiseAsyncUpdates() - ]); -} - -function promiseEndUpdateBatch() { - return new Promise(resolve => { - PlacesUtils.bookmarks.addObserver({ - __proto__: NavBookmarkObserver.prototype, - onEndUpdateBatch: resolve - }, false); - }); -} - let createCorruptDB = Task.async(function* () { let dbPath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite"); yield OS.File.remove(dbPath); diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt.js b/browser/components/places/tests/unit/test_browserGlue_corrupt.js index 56e55c55d813..4460453eb794 100644 --- a/browser/components/places/tests/unit/test_browserGlue_corrupt.js +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt.js @@ -41,7 +41,7 @@ add_task(function* test_main() { // The test will continue once restore has finished and smart bookmarks // have been created. - yield promiseEndUpdateBatch(); + yield promiseTopicObserved("places-browser-init-complete"); let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js index 532643e2faa0..50f5522eaf06 100644 --- a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js @@ -35,7 +35,7 @@ add_task(function* () { // The test will continue once import has finished and smart bookmarks // have been created. - yield promiseEndUpdateBatch(); + yield promiseTopicObserved("places-browser-init-complete"); let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js index 035172619d12..65750a914f4a 100644 --- a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js +++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js @@ -33,7 +33,7 @@ add_task(function* () { // The test will continue once import has finished and smart bookmarks // have been created. - yield promiseEndUpdateBatch(); + yield promiseTopicObserved("places-browser-init-complete"); let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, diff --git a/browser/components/places/tests/unit/test_browserGlue_migrate.js b/browser/components/places/tests/unit/test_browserGlue_migrate.js index 5d3ec13e1112..817f10c81a60 100644 --- a/browser/components/places/tests/unit/test_browserGlue_migrate.js +++ b/browser/components/places/tests/unit/test_browserGlue_migrate.js @@ -40,7 +40,7 @@ add_task(function* test_migrate_bookmarks() { title: "migrated" }); - let promise = promiseEndUpdateBatch(); + let promise = promiseTopicObserved("places-browser-init-complete"); bg.observe(null, "initial-migration-did-import-default-bookmarks", null); yield promise; diff --git a/browser/components/places/tests/unit/test_browserGlue_prefs.js b/browser/components/places/tests/unit/test_browserGlue_prefs.js index 77814581445d..49dbf2b42181 100644 --- a/browser/components/places/tests/unit/test_browserGlue_prefs.js +++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js @@ -38,11 +38,9 @@ do_register_cleanup(function () { function simulatePlacesInit() { do_print("Simulate Places init"); - let promise = waitForImportAndSmartBookmarks(); - // Force nsBrowserGlue::_initPlaces(). bg.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_FORCE_PLACES_INIT); - return promise; + return promiseTopicObserved("places-browser-init-complete"); } add_task(function* test_checkPreferences() { diff --git a/browser/components/places/tests/unit/test_browserGlue_restore.js b/browser/components/places/tests/unit/test_browserGlue_restore.js index b2614f4c5291..9482d81d46db 100644 --- a/browser/components/places/tests/unit/test_browserGlue_restore.js +++ b/browser/components/places/tests/unit/test_browserGlue_restore.js @@ -44,7 +44,7 @@ add_task(function* test_main() { // The test will continue once restore has finished and smart bookmarks // have been created. - yield promiseEndUpdateBatch(); + yield promiseTopicObserved("places-browser-init-complete"); let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, diff --git a/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js index e6498ed621c3..ca66889f878f 100644 --- a/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js +++ b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js @@ -70,8 +70,6 @@ add_task(function* setup() { Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML)); Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS)); Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML)); - - yield waitForImportAndSmartBookmarks(); }); add_task(function* test_version_0() { diff --git a/toolkit/components/places/Bookmarks.jsm b/toolkit/components/places/Bookmarks.jsm index 7de4e67052b0..2697e6c576f6 100644 --- a/toolkit/components/places/Bookmarks.jsm +++ b/toolkit/components/places/Bookmarks.jsm @@ -258,7 +258,7 @@ let Bookmarks = Object.freeze({ , validIf: b => b.lastModified >= item.dateAdded } }); - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); let parent; if (updateInfo.hasOwnProperty("parentGuid")) { if (item.type == this.TYPE_FOLDER) { @@ -426,7 +426,7 @@ let Bookmarks = Object.freeze({ * @resolves once the removal is complete. */ eraseEverything: Task.async(function* () { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); yield db.executeTransaction(function* () { const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid]; yield removeFoldersContents(db, folderGuids); @@ -670,29 +670,11 @@ function notify(observers, notification, args) { } } -XPCOMUtils.defineLazyGetter(this, "DBConnPromised", - () => new Promise((resolve, reject) => { - Sqlite.wrapStorageConnection({ connection: PlacesUtils.history.DBConnection } ) - .then(db => { - try { - Sqlite.shutdown.addBlocker("Places Bookmarks.jsm wrapper closing", - db.close.bind(db)); - } - catch (ex) { - // It's too late to block shutdown, just close the connection. - db.close(); - reject(ex); - } - resolve(db); - }); - }) -); - //////////////////////////////////////////////////////////////////////////////// // Update implementation. function* updateBookmark(info, item, newParent) { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); let tuples = new Map(); if (info.hasOwnProperty("lastModified")) @@ -779,7 +761,7 @@ function* updateBookmark(info, item, newParent) { // Insert implementation. function* insertBookmark(item, parent) { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); // If a guid was not provided, generate one, so we won't need to fetch the // bookmark just after having created it. @@ -834,7 +816,7 @@ function* insertBookmark(item, parent) { // Fetch implementation. function* fetchBookmark(info) { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); let rows = yield db.executeCached( `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index', @@ -852,7 +834,7 @@ function* fetchBookmark(info) { } function* fetchBookmarkByPosition(info) { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index; let rows = yield db.executeCached( @@ -874,7 +856,7 @@ function* fetchBookmarkByPosition(info) { } function* fetchBookmarksByURL(info) { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); let rows = yield db.executeCached( `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index', @@ -895,7 +877,7 @@ function* fetchBookmarksByURL(info) { } function* fetchBookmarksByParent(info) { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); let rows = yield db.executeCached( `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index', @@ -917,7 +899,7 @@ function* fetchBookmarksByParent(info) { // Remove implementation. function* removeBookmark(item) { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId; @@ -960,7 +942,7 @@ function* removeBookmark(item) { // Reorder implementation. function* reorderChildren(parent, orderedChildrenGuids) { - let db = yield DBConnPromised; + let db = yield PlacesUtils.promiseWrappedConnection(); return db.executeTransaction(function* () { // Select all of the direct children for the given parent. diff --git a/toolkit/components/places/PlacesCategoriesStarter.js b/toolkit/components/places/PlacesCategoriesStarter.js index b4378396507e..b611f337e56a 100644 --- a/toolkit/components/places/PlacesCategoriesStarter.js +++ b/toolkit/components/places/PlacesCategoriesStarter.js @@ -36,22 +36,25 @@ function PlacesCategoriesStarter() Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, false); // nsINavBookmarkObserver implementation. - let notify = (function () { + let notify = () => { if (!this._notifiedBookmarksSvcReady) { + // TODO (bug 1145424): for whatever reason, even if we remove this + // component from the category (and thus from the category cache we use + // to notify), we keep being notified. + this._notifiedBookmarksSvcReady = true; // For perf reasons unregister from the category, since no further // notifications are needed. Cc["@mozilla.org/categorymanager;1"] .getService(Ci.nsICategoryManager) - .deleteCategoryEntry("bookmarks-observer", this, false); + .deleteCategoryEntry("bookmark-observers", "PlacesCategoriesStarter", false); // Directly notify PlacesUtils, to ensure it catches the notification. PlacesUtils.observe(null, "bookmarks-service-ready", null); } - }).bind(this); + }; + [ "onItemAdded", "onItemRemoved", "onItemChanged", "onBeginUpdateBatch", - "onEndUpdateBatch", "onItemVisited", - "onItemMoved" ].forEach(function(aMethod) { - this[aMethod] = notify; - }, this); + "onEndUpdateBatch", "onItemVisited", "onItemMoved" + ].forEach(aMethod => this[aMethod] = notify); } PlacesCategoriesStarter.prototype = { diff --git a/toolkit/components/places/PlacesUtils.jsm b/toolkit/components/places/PlacesUtils.jsm index 305ba3a4433f..03fc0ec4fb8a 100644 --- a/toolkit/components/places/PlacesUtils.jsm +++ b/toolkit/components/places/PlacesUtils.jsm @@ -27,6 +27,8 @@ this.EXPORTED_SYMBOLS = [ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; +Cu.importGlobalProperties(["URL"]); + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); @@ -73,6 +75,55 @@ function QI_node(aNode, aIID) { function asContainer(aNode) QI_node(aNode, Ci.nsINavHistoryContainerResultNode); function asQuery(aNode) QI_node(aNode, Ci.nsINavHistoryQueryResultNode); +/** + * Sends a bookmarks notification through the given observers. + * + * @param observers + * array of nsINavBookmarkObserver objects. + * @param notification + * the notification name. + * @param args + * array of arguments to pass to the notification. + */ +function notify(observers, notification, args) { + for (let observer of observers) { + try { + observer[notification](...args); + } catch (ex) {} + } +} + +/** + * Sends a keyword change notification. + * + * @param url + * the url to notify about. + * @param keyword + * The keyword to notify, or empty string if a keyword was removed. + */ +function* notifyKeywordChange(url, keyword) { + // Notify bookmarks about the removal. + let bookmarks = []; + yield PlacesUtils.bookmarks.fetch({ url }, b => bookmarks.push(b)); + // We don't want to yield in the gIgnoreKeywordNotifications section. + for (let bookmark of bookmarks) { + bookmark.id = yield PlacesUtils.promiseItemId(bookmark.guid); + bookmark.parentId = yield PlacesUtils.promiseItemId(bookmark.parentGuid); + } + let observers = PlacesUtils.bookmarks.getObservers(); + gIgnoreKeywordNotifications = true; + for (let bookmark of bookmarks) { + notify(observers, "onItemChanged", [ bookmark.id, "keyword", false, + keyword, + bookmark.lastModified * 1000, + bookmark.type, + bookmark.parentId, + bookmark.guid, bookmark.parentGuid + ]); + } + gIgnoreKeywordNotifications = false; +} + this.PlacesUtils = { // Place entries that are containers, e.g. bookmark folders or queries. TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container", @@ -244,6 +295,11 @@ this.PlacesUtils = { let observerInfo = this._bookmarksServiceObserversQueue.shift(); this.bookmarks.addObserver(observerInfo.observer, observerInfo.weak); } + + // Initialize the keywords cache to start observing bookmarks + // notifications. This is needed as far as we support both the old and + // the new bookmarking APIs at the same time. + gKeywordsCachePromise.catch(Cu.reportError); break; } }, @@ -810,19 +866,19 @@ this.PlacesUtils = { * Set the POST data associated with a bookmark, if any. * Used by POST keywords. * @param aBookmarkId - * @returns string of POST data */ setPostDataForBookmark(aBookmarkId, aPostData) { + if (!aPostData) + throw new Error("Must provide valid POST data"); // For now we don't have a unified API to create a keyword with postData, // thus here we can just try to complete a keyword that should already exist // without any post data. - let nullPostDataFragment = aPostData ? "AND post_data ISNULL" : ""; let stmt = PlacesUtils.history.DBConnection.createStatement( `UPDATE moz_keywords SET post_data = :post_data WHERE id = (SELECT k.id FROM moz_keywords k JOIN moz_bookmarks b ON b.fk = k.place_id WHERE b.id = :item_id - ${nullPostDataFragment} + AND post_data ISNULL LIMIT 1)`); stmt.params.item_id = aBookmarkId; stmt.params.post_data = aPostData; @@ -832,6 +888,21 @@ this.PlacesUtils = { finally { stmt.finalize(); } + + // Update the cache. + return Task.spawn(function* () { + let guid = yield PlacesUtils.promiseItemGuid(aBookmarkId); + let bm = yield PlacesUtils.bookmarks.fetch(guid); + + // Fetch keywords for this href. + let cache = yield gKeywordsCachePromise; + for (let [ keyword, entry ] of cache) { + // Set the POST data on keywords not having it. + if (entry.url.href == bm.url.href && !entry.postData) { + entry.postData = aPostData; + } + } + }).catch(Cu.reportError); }, /** @@ -1256,6 +1327,14 @@ this.PlacesUtils = { */ promiseDBConnection: () => gAsyncDBConnPromised, + /** + * Gets a Sqlite.jsm wrapped connection to the Places database. + * This is intended to be used mostly internally, and by other Places modules. + * Keep in mind the Places DB schema is by no means frozen or even stable. + * Your custom queries can - and will - break overtime. + */ + promiseWrappedConnection: () => gAsyncDBWrapperPromised, + /** * Given a uri returns list of itemIds associated to it. * @@ -1842,6 +1921,8 @@ XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "livemarks", "@mozilla.org/browser/livemark-service;2", "mozIAsyncLivemarks"); +XPCOMUtils.defineLazyGetter(PlacesUtils, "keywords", () => Keywords); + XPCOMUtils.defineLazyGetter(PlacesUtils, "transactionManager", function() { let tm = Cc["@mozilla.org/transactionmanager;1"]. createInstance(Ci.nsITransactionManager); @@ -1881,24 +1962,266 @@ XPCOMUtils.defineLazyGetter(this, "bundle", function() { createBundle(PLACES_STRING_BUNDLE_URI); }); -XPCOMUtils.defineLazyGetter(this, "gAsyncDBConnPromised", () => { - let connPromised = Sqlite.cloneStorageConnection({ - connection: PlacesUtils.history.DBConnection, - readOnly: true }); - connPromised.then(conn => { - try { - Sqlite.shutdown.addBlocker("Places DB readonly connection closing", - conn.close.bind(conn)); +XPCOMUtils.defineLazyGetter(this, "gAsyncDBConnPromised", + () => new Promise((resolve) => { + Sqlite.cloneStorageConnection({ + connection: PlacesUtils.history.DBConnection, + readOnly: true + }).then(conn => { + try { + Sqlite.shutdown.addBlocker( + "PlacesUtils read-only connection closing", + conn.close.bind(conn)); + } catch(ex) { + // It's too late to block shutdown, just close the connection. + conn.close(); + throw ex; + } + resolve(conn); + }); + }) +); + +XPCOMUtils.defineLazyGetter(this, "gAsyncDBWrapperPromised", + () => new Promise((resolve) => { + Sqlite.wrapStorageConnection({ + connection: PlacesUtils.history.DBConnection, + }).then(conn => { + try { + Sqlite.shutdown.addBlocker( + "PlacesUtils wrapped connection closing", + conn.close.bind(conn)); + } catch(ex) { + // It's too late to block shutdown, just close the connection. + conn.close(); + throw ex; + } + resolve(conn); + }); + }) +); + +/** + * Keywords management API. + * Sooner or later these keywords will merge with search keywords, this is an + * interim API that should then be replaced by a unified one. + * Keywords are associated with URLs and can have POST data. + * A single URL can have multiple keywords, provided they differ by POST data. + */ +let Keywords = { + /** + * Fetches URL and postData for a given keyword. + * + * @param keyword + * The keyword to fetch. + * @return {Promise} + * @resolves to an object in the form: { keyword, url, postData }, + * or null if a keyword was not found. + */ + fetch(keyword) { + if (!keyword || typeof(keyword) != "string") + throw new Error("Invalid keyword"); + keyword = keyword.trim().toLowerCase(); + return gKeywordsCachePromise.then(cache => cache.get(keyword) || null); + }, + + /** + * Adds a new keyword and postData for the given URL. + * + * @param keywordEntry + * An object describing the keyword to insert, in the form: + * { + * keyword: non-empty string, + * URL: URL or href to associate to the keyword, + * postData: optional POST data to associate to the keyword + * } + * @note Do not define a postData property if there isn't any POST data. + * @resolves when the addition is complete. + */ + insert(keywordEntry) { + if (!keywordEntry || typeof keywordEntry != "object") + throw new Error("Input should be a valid object"); + + if (!("keyword" in keywordEntry) || !keywordEntry.keyword || + typeof(keywordEntry.keyword) != "string") + throw new Error("Invalid keyword"); + if (("postData" in keywordEntry) && keywordEntry.postData && + typeof(keywordEntry.postData) != "string") + throw new Error("Invalid POST data"); + if (!("url" in keywordEntry)) + throw new Error("undefined is not a valid URL"); + let { keyword, url } = keywordEntry; + keyword = keyword.trim().toLowerCase(); + let postData = keywordEntry.postData || null; + // This also checks href for validity + url = new URL(url); + + return Task.spawn(function* () { + let cache = yield gKeywordsCachePromise; + + // Trying to set the same keyword is a no-op. + let oldEntry = cache.get(keyword); + if (oldEntry && oldEntry.url.href == url.href && + oldEntry.postData == keywordEntry.postData) { + return; + } + + // A keyword can only be associated to a single page. + // If another page is using the new keyword, we must update the keyword + // entry. + // Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete + // trigger. + let db = yield PlacesUtils.promiseWrappedConnection(); + if (oldEntry) { + yield db.executeCached( + `UPDATE moz_keywords + SET place_id = (SELECT id FROM moz_places WHERE url = :url), + post_data = :post_data + WHERE keyword = :keyword + `, { url: url.href, keyword: keyword, post_data: postData }); + yield notifyKeywordChange(oldEntry.url.href, ""); + } else { + // An entry for the given page could be missing, in such a case we need to + // create it. + yield db.executeCached( + `INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid) + VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID()) + `, { url: url.href, rev_host: PlacesUtils.getReversedHost(url), + frecency: url.protocol == "place:" ? 0 : -1 }); + yield db.executeCached( + `INSERT INTO moz_keywords (keyword, place_id, post_data) + VALUES (:keyword, (SELECT id FROM moz_places WHERE url = :url), :post_data) + `, { url: url.href, keyword: keyword, post_data: postData }); + } + + cache.set(keyword, { keyword, url, postData }); + + // In any case, notify about the new keyword. + yield notifyKeywordChange(url.href, keyword); + }.bind(this)); + }, + + /** + * Removes a keyword. + * + * @param keyword + * The keyword to remove. + * @return {Promise} + * @resolves when the removal is complete. + */ + remove(keyword) { + if (!keyword || typeof(keyword) != "string") + throw new Error("Invalid keyword"); + keyword = keyword.trim().toLowerCase(); + return Task.spawn(function* () { + let cache = yield gKeywordsCachePromise; + if (!cache.has(keyword)) + return; + let { url } = cache.get(keyword); + cache.delete(keyword); + + let db = yield PlacesUtils.promiseWrappedConnection(); + yield db.execute(`DELETE FROM moz_keywords WHERE keyword = :keyword`, + { keyword }); + + // Notify bookmarks about the removal. + yield notifyKeywordChange(url.href, ""); + }.bind(this)); + } +}; + +// Set by the keywords API to distinguish notifications fired by the old API. +// Once the old API will be gone, we can remove this and stop observing. +let gIgnoreKeywordNotifications = false; + +XPCOMUtils.defineLazyGetter(this, "gKeywordsCachePromise", Task.async(function* () { + let cache = new Map(); + let db = yield PlacesUtils.promiseWrappedConnection(); + let rows = yield db.execute( + `SELECT keyword, url, post_data + FROM moz_keywords k + JOIN moz_places h ON h.id = k.place_id + `); + for (let row of rows) { + let keyword = row.getResultByName("keyword"); + let entry = { keyword, + url: new URL(row.getResultByName("url")), + postData: row.getResultByName("post_data") }; + cache.set(keyword, entry); + } + + // Helper to get a keyword from an href. + function keywordsForHref(href) { + let keywords = []; + for (let [ key, val ] of cache) { + if (val.url.href == href) + keywords.push(key); } - catch(ex) { - // It's too late to block shutdown, just close the connection. - return conn.close(); - throw (ex); + return keywords; + } + + // Start observing changes to bookmarks. For now we are going to keep that + // relation for backwards compatibility reasons, but mostly because we are + // lacking a UI to manage keywords directly. + let observer = { + QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver), + onBeginUpdateBatch() {}, + onEndUpdateBatch() {}, + onItemAdded() {}, + onItemVisited() {}, + onItemMoved() {}, + + onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) { + if (itemType != PlacesUtils.bookmarks.TYPE_BOOKMARK) + return; + + let keywords = keywordsForHref(uri.spec); + // This uri has no keywords associated, so there's nothing to do. + if (keywords.length == 0) + return; + + Task.spawn(function* () { + // If the uri is not bookmarked anymore, we can remove this keyword. + let bookmark = yield PlacesUtils.bookmarks.fetch({ url: uri }); + if (!bookmark) { + for (let keyword of keywords) { + yield PlacesUtils.keywords.remove(keyword); + } + } + }).catch(Cu.reportError); + }, + + onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid) { + if (gIgnoreKeywordNotifications || + prop != "keyword") + return; + + Task.spawn(function* () { + let bookmark = yield PlacesUtils.bookmarks.fetch(guid); + // By this time the bookmark could have gone, there's nothing we can do. + if (!bookmark) + return; + + if (val.length == 0) { + // We are removing a keyword. + let keywords = keywordsForHref(bookmark.url.href) + for (let keyword of keywords) { + cache.delete(keyword); + } + } else { + // We are adding a new keyword. + cache.set(val, { keyword: val, url: bookmark.url }); + } + }).catch(Cu.reportError); } - return Promise.resolve(); - }).then(null, Cu.reportError); - return connPromised; -}); + }; + + PlacesUtils.bookmarks.addObserver(observer, false); + PlacesUtils.registerShutdownFunction(() => { + PlacesUtils.bookmarks.removeObserver(observer); + }); + return cache; +})); // Sometime soon, likely as part of the transition to mozIAsyncBookmarks, // itemIds will be deprecated in favour of GUIDs, which play much better @@ -2919,15 +3242,19 @@ this.PlacesEditBookmarkPostDataTransaction = PlacesEditBookmarkPostDataTransaction.prototype = { __proto__: BaseTransaction.prototype, - doTransaction: function EBPDTXN_doTransaction() - { - this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id); - PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData); + doTransaction() { + // Setting null postData is not supported by the current schema. + if (this.new.postData) { + this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id); + PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData); + } }, - undoTransaction: function EBPDTXN_undoTransaction() - { - PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData); + undoTransaction() { + // Setting null postData is not supported by the current schema. + if (this.item.postData) { + PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData); + } } }; diff --git a/toolkit/components/places/nsNavBookmarks.cpp b/toolkit/components/places/nsNavBookmarks.cpp index 7161ca458c82..c66116dd00ad 100644 --- a/toolkit/components/places/nsNavBookmarks.cpp +++ b/toolkit/components/places/nsNavBookmarks.cpp @@ -622,13 +622,8 @@ nsNavBookmarks::RemoveItem(int64_t aItemId) rv = history->UpdateFrecency(bookmark.placeId); NS_ENSURE_SUCCESS(rv, rv); } - // A broken url should not interrupt the removal process. - rv = NS_NewURI(getter_AddRefs(uri), bookmark.url); - if (NS_SUCCEEDED(rv)) { - rv = UpdateKeywordsForRemovedBookmark(bookmark); - NS_ENSURE_SUCCESS(rv, rv); - } + (void)NS_NewURI(getter_AddRefs(uri), bookmark.url); } NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, @@ -1102,13 +1097,8 @@ nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId) rv = history->UpdateFrecency(child.placeId); NS_ENSURE_SUCCESS(rv, rv); } - // A broken url should not interrupt the removal process. - rv = NS_NewURI(getter_AddRefs(uri), child.url); - if (NS_SUCCEEDED(rv)) { - rv = UpdateKeywordsForRemovedBookmark(child); - NS_ENSURE_SUCCESS(rv, rv); - } + (void)NS_NewURI(getter_AddRefs(uri), child.url); } NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, @@ -2238,74 +2228,6 @@ nsNavBookmarks::SetItemIndex(int64_t aItemId, int32_t aNewIndex) } -nsresult -nsNavBookmarks::UpdateKeywordsForRemovedBookmark(const BookmarkData& aBookmark) -{ - // If there are no keywords for this URI, there's nothing to do. - nsCOMPtr uri; - nsresult rv = NS_NewURI(getter_AddRefs(uri), aBookmark.url); - NS_ENSURE_SUCCESS(rv, rv); - - nsTArray keywords; - { - nsCOMPtr stmt = mDB->GetStatement( - "SELECT keyword FROM moz_keywords WHERE place_id = :place_id " - ); - NS_ENSURE_STATE(stmt); - mozStorageStatementScoper scoper(stmt); - rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), aBookmark.placeId); - NS_ENSURE_SUCCESS(rv, rv); - - bool hasMore; - while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { - nsAutoString keyword; - rv = stmt->GetString(0, keyword); - NS_ENSURE_SUCCESS(rv, rv); - keywords.AppendElement(keyword); - } - } - - if (keywords.Length() == 0) { - // This uri has no keywords associated, so there's nothing to do. - return NS_OK; - } - - // If the uri is not bookmarked anymore, we can remove its keywords. - nsTArray bookmarks; - rv = GetBookmarksForURI(uri, bookmarks); - NS_ENSURE_SUCCESS(rv, rv); - if (bookmarks.Length() == 0) { - for (uint32_t i = 0; i < keywords.Length(); ++i) { - nsString keyword = keywords[i]; - - nsCOMPtr stmt = mDB->GetStatement( - "DELETE FROM moz_keywords WHERE keyword = :keyword " - ); - NS_ENSURE_STATE(stmt); - mozStorageStatementScoper scoper(stmt); - rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); - NS_ENSURE_SUCCESS(rv, rv); - rv = stmt->Execute(); - NS_ENSURE_SUCCESS(rv, rv); - } - - NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, - nsINavBookmarkObserver, - OnItemChanged(aBookmark.id, - NS_LITERAL_CSTRING("keyword"), - false, - EmptyCString(), - aBookmark.lastModified, - TYPE_BOOKMARK, - aBookmark.parentId, - aBookmark.guid, - aBookmark.parentGuid)); - } - - return NS_OK; -} - - NS_IMETHODIMP nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId, const nsAString& aUserCasedKeyword) diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks.js b/toolkit/components/places/tests/bookmarks/test_bookmarks.js index cc4dfdd029b0..8911f68a5805 100644 --- a/toolkit/components/places/tests/bookmarks/test_bookmarks.js +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks.js @@ -321,31 +321,25 @@ add_task(function test_bookmarks() { // test setKeywordForBookmark let kwTestItemId = bs.insertBookmark(testRoot, uri("http://keywordtest.com"), bs.DEFAULT_INDEX, ""); - try { - let dateAdded = bs.getItemDateAdded(kwTestItemId); - // after just inserting, modified should not be set - let lastModified = bs.getItemLastModified(kwTestItemId); - do_check_eq(lastModified, dateAdded); - // Workaround possible VM timers issues moving lastModified and dateAdded - // to the past. - lastModified -= 1000; - bs.setItemLastModified(kwTestItemId, --lastModified); - dateAdded -= 1000; - bs.setItemDateAdded(kwTestItemId, dateAdded); - - bs.setKeywordForBookmark(kwTestItemId, "bar"); - - let lastModified2 = bs.getItemLastModified(kwTestItemId); - LOG("test setKeywordForBookmark"); - LOG("dateAdded = " + dateAdded); - LOG("lastModified = " + lastModified); - LOG("lastModified2 = " + lastModified2); - do_check_true(is_time_ordered(lastModified, lastModified2)); - do_check_true(is_time_ordered(dateAdded, lastModified2)); - } catch(ex) { - do_throw("setKeywordForBookmark: " + ex); - } + dateAdded = bs.getItemDateAdded(kwTestItemId); + // after just inserting, modified should not be set + lastModified = bs.getItemLastModified(kwTestItemId); + do_check_eq(lastModified, dateAdded); + // Workaround possible VM timers issues moving lastModified and dateAdded + // to the past. + lastModified -= 1000; + bs.setItemLastModified(kwTestItemId, lastModified); + dateAdded -= 1000; + bs.setItemDateAdded(kwTestItemId, dateAdded); + bs.setKeywordForBookmark(kwTestItemId, "bar"); + lastModified2 = bs.getItemLastModified(kwTestItemId); + LOG("test setKeywordForBookmark"); + LOG("dateAdded = " + dateAdded); + LOG("lastModified = " + lastModified); + LOG("lastModified2 = " + lastModified2); + do_check_true(is_time_ordered(lastModified, lastModified2)); + do_check_true(is_time_ordered(dateAdded, lastModified2)); let lastModified3 = bs.getItemLastModified(kwTestItemId); // test getKeywordForBookmark diff --git a/toolkit/components/places/tests/bookmarks/test_keywords.js b/toolkit/components/places/tests/bookmarks/test_keywords.js index 242fa4977e0d..b3e728b89a7f 100644 --- a/toolkit/components/places/tests/bookmarks/test_keywords.js +++ b/toolkit/components/places/tests/bookmarks/test_keywords.js @@ -286,17 +286,12 @@ add_task(function test_addRemoveBookmark() { "keyword", false, "keyword", bookmark.lastModified, bookmark.type, parentId, - bookmark.guid, bookmark.parentGuid ] }, - { name: "onItemChanged", - arguments: [ itemId, - "keyword", false, "", - bookmark.lastModified, bookmark.type, - parentId, bookmark.guid, bookmark.parentGuid ] } ]); check_keyword(URI3, null); - Assert.equal((yield foreign_count(URI3)), fc); + // Don't check the foreign count since the process is async. + // The new test_keywords.js in unit is checking this though. yield PlacesTestUtils.promiseAsyncUpdates(); check_orphans(); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js index 62fb1a97f75c..6b1d516631a9 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js @@ -48,7 +48,7 @@ add_task(function* test_keyword_searc() { do_print("Keyword that happens to match a page"); yield check_autocomplete({ search: "key ThisPageIsInHistory", - matches: [ { uri: NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"), title: "Generic page title", style: ["bookmark"] } ] + matches: [ { uri: NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"), title: "Generic page title", style: ["keyword"] } ] }); do_print("Keyword without query (without space)"); diff --git a/toolkit/components/places/tests/unit/test_421180.js b/toolkit/components/places/tests/unit/test_421180.js deleted file mode 100644 index 69cac5662abd..000000000000 --- a/toolkit/components/places/tests/unit/test_421180.js +++ /dev/null @@ -1,94 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim:set ts=2 sw=2 sts=2 et: */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// Get bookmarks service -try { - var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); -} -catch(ex) { - do_throw("Could not get bookmarks service\n"); -} - -// Get database connection -try { - var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - var mDBConn = histsvc.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; -} -catch(ex) { - do_throw("Could not get database connection\n"); -} - -add_test(function test_keywordRemovedOnUniqueItemRemoval() { - var bookmarkedURI = uri("http://foo.bar"); - var keyword = "testkeyword"; - - // TEST 1 - // 1. add a bookmark - // 2. add a keyword to it - // 3. remove bookmark - // 4. check that keyword has gone - var bookmarkId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, - bookmarkedURI, - bmsvc.DEFAULT_INDEX, - "A bookmark"); - bmsvc.setKeywordForBookmark(bookmarkId, keyword); - // remove bookmark - bmsvc.removeItem(bookmarkId); - - PlacesTestUtils.promiseAsyncUpdates().then(() => { - // Check that keyword has been removed from the database. - // The removal is asynchronous. - var sql = "SELECT id FROM moz_keywords WHERE keyword = ?1"; - var stmt = mDBConn.createStatement(sql); - stmt.bindByIndex(0, keyword); - do_check_false(stmt.executeStep()); - stmt.finalize(); - - run_next_test(); - }); -}); - -add_test(function test_keywordNotRemovedOnNonUniqueItemRemoval() { - var bookmarkedURI = uri("http://foo.bar"); - var keyword = "testkeyword"; - - // TEST 2 - // 1. add 2 bookmarks - // 2. add the same keyword to them - // 3. remove first bookmark - // 4. check that keyword is still there - var bookmarkId1 = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, - bookmarkedURI, - bmsvc.DEFAULT_INDEX, - "A bookmark"); - bmsvc.setKeywordForBookmark(bookmarkId1, keyword); - - var bookmarkId2 = bmsvc.insertBookmark(bmsvc.toolbarFolder, - bookmarkedURI, - bmsvc.DEFAULT_INDEX, - keyword); - bmsvc.setKeywordForBookmark(bookmarkId2, keyword); - - // remove first bookmark - bmsvc.removeItem(bookmarkId1); - - PlacesTestUtils.promiseAsyncUpdates().then(() => { - // check that keyword is still there - var sql = "SELECT id FROM moz_keywords WHERE keyword = ?1"; - var stmt = mDBConn.createStatement(sql); - stmt.bindByIndex(0, keyword); - do_check_true(stmt.executeStep()); - stmt.finalize(); - - run_next_test(); - }); -}); - -function run_test() { - run_next_test(); -} diff --git a/toolkit/components/places/tests/unit/test_keywords.js b/toolkit/components/places/tests/unit/test_keywords.js new file mode 100644 index 000000000000..90cf8fba266e --- /dev/null +++ b/toolkit/components/places/tests/unit/test_keywords.js @@ -0,0 +1,488 @@ +"use strict" + +function* check_keyword(aExpectExists, aHref, aKeyword, aPostData = null) { + // Check case-insensitivity. + aKeyword = aKeyword.toUpperCase(); + + let entry = yield PlacesUtils.keywords.fetch(aKeyword); + if (aExpectExists) { + Assert.ok(!!entry, "A keyword should exist"); + Assert.equal(entry.url.href, aHref); + Assert.equal(entry.postData, aPostData); + } else { + Assert.ok(!entry || entry.url.href != aHref, + "The given keyword entry should not exist"); + } +} + +/** + * Polls the keywords cache waiting for the given keyword entry. + */ +function* promiseKeyword(keyword, expectedHref) { + let href = null; + do { + yield new Promise(resolve => do_timeout(100, resolve)); + let entry = yield PlacesUtils.keywords.fetch(keyword); + if (entry) + href = entry.url.href; + } while (href != expectedHref); +} + +function* check_no_orphans() { + let db = yield PlacesUtils.promiseDBConnection(); + let rows = yield db.executeCached( + `SELECT id FROM moz_keywords k + WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id) + `); + Assert.equal(rows.length, 0); +} + +function expectBookmarkNotifications() { + let notifications = []; + let observer = new Proxy(NavBookmarkObserver, { + get(target, name) { + if (name == "check") { + PlacesUtils.bookmarks.removeObserver(observer); + return expectedNotifications => + Assert.deepEqual(notifications, expectedNotifications); + } + + if (name.startsWith("onItemChanged")) { + return (itemId, property) => { + if (property != "keyword") + return; + let args = Array.from(arguments, arg => { + if (arg && arg instanceof Ci.nsIURI) + return new URL(arg.spec); + if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000) + return new Date(parseInt(arg/1000)); + return arg; + }); + notifications.push({ name: name, arguments: args }); + } + } + + if (name in target) + return target[name]; + return undefined; + } + }); + PlacesUtils.bookmarks.addObserver(observer, false); + return observer; +} + +add_task(function* test_invalid_input() { + Assert.throws(() => PlacesUtils.keywords.fetch(null), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch(""), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.fetch(5), + /Invalid keyword/); + + Assert.throws(() => PlacesUtils.keywords.insert(null), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.keywords.insert("test"), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.keywords.insert(undefined), + /Input should be a valid object/); + Assert.throws(() => PlacesUtils.keywords.insert({ }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: null }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: 5 }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "" }), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", postData: 5 }), + /Invalid POST data/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", postData: {} }), + /Invalid POST data/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test" }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: 5 }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: "" }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: null }), + /is not a valid URL/); + Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: "mozilla" }), + /is not a valid URL/); + + Assert.throws(() => PlacesUtils.keywords.remove(null), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.remove(""), + /Invalid keyword/); + Assert.throws(() => PlacesUtils.keywords.remove(5), + /Invalid keyword/); +}); + +add_task(function* test_addKeyword() { + yield check_keyword(false, "http://example.com/", "keyword"); + let fc = yield foreign_count("http://example.com/"); + let observer = expectBookmarkNotifications(); + + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + observer.check([]); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + observer.check([]); + + yield check_keyword(false, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword + + // Check using URL. + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: new URL("http://example.com/") }); + yield check_keyword(true, "http://example.com/", "keyword"); + yield PlacesUtils.keywords.remove("keyword"); + yield check_keyword(false, "http://example.com/", "keyword"); + + yield check_no_orphans(); +}); + +add_task(function* test_addBookmarkAndKeyword() { + yield check_keyword(false, "http://example.com/", "keyword"); + let fc = yield foreign_count("http://example.com/"); + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + + let observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "keyword", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid ] } ]); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid ] } ]); + + yield check_keyword(false, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // -1 keyword + + // Add again the keyword, then remove the bookmark. + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + + observer = expectBookmarkNotifications(); + yield PlacesUtils.bookmarks.remove(bookmark.guid); + // the notification is synchronous but the removal process is async. + // Unfortunately there's nothing explicit we can wait for. + while ((yield foreign_count("http://example.com/"))); + // We don't get any itemChanged notification since the bookmark has been + // removed already. + observer.check([]); + + yield check_keyword(false, "http://example.com/", "keyword"); + + yield check_no_orphans(); +}); + +add_task(function* test_addKeywordToURIHavingKeyword() { + yield check_keyword(false, "http://example.com/", "keyword"); + let fc = yield foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + observer.check([]); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword + + yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" }); + + yield check_keyword(true, "http://example.com/", "keyword"); + yield check_keyword(true, "http://example.com/", "keyword2"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 keyword + + // Now remove the keywords. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + yield PlacesUtils.keywords.remove("keyword2"); + observer.check([]); + + yield check_keyword(false, "http://example.com/", "keyword"); + yield check_keyword(false, "http://example.com/", "keyword2"); + Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword + + yield check_no_orphans(); +}); + +add_task(function* test_addBookmarkToURIHavingKeyword() { + yield check_keyword(false, "http://example.com/", "keyword"); + let fc = yield foreign_count("http://example.com/"); + let observer = expectBookmarkNotifications(); + + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + observer.check([]); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword + + observer = expectBookmarkNotifications(); + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark + observer.check([]); + + observer = expectBookmarkNotifications(); + yield PlacesUtils.bookmarks.remove(bookmark.guid); + // the notification is synchronous but the removal process is async. + // Unfortunately there's nothing explicit we can wait for. + while ((yield foreign_count("http://example.com/"))); + // We don't get any itemChanged notification since the bookmark has been + // removed already. + observer.check([]); + + yield check_keyword(false, "http://example.com/", "keyword"); + + yield check_no_orphans(); +}); + +add_task(function* test_sameKeywordDifferentURL() { + let fc1 = yield foreign_count("http://example1.com/"); + let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let fc2 = yield foreign_count("http://example2.com/"); + let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example2.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example1.com/" }); + + yield check_keyword(true, "http://example1.com/", "keyword"); + Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword + yield check_keyword(false, "http://example2.com/", "keyword"); + Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // +1 bookmark + + // Assign the same keyword to another url. + let observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example2.com/" }); + + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), + "keyword", false, "", + bookmark1.lastModified, bookmark1.type, + (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), + bookmark1.guid, bookmark1.parentGuid ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), + "keyword", false, "keyword", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid ] } ]); + + yield check_keyword(false, "http://example1.com/", "keyword"); + Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); // -1 keyword + yield check_keyword(true, "http://example2.com/", "keyword"); + Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 keyword + + // Now remove the keyword. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), + "keyword", false, "", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid ] } ]); + + yield check_keyword(false, "http://example1.com/", "keyword"); + yield check_keyword(false, "http://example2.com/", "keyword"); + Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); + Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // -1 keyword + + yield PlacesUtils.bookmarks.remove(bookmark1); + yield PlacesUtils.bookmarks.remove(bookmark2); + Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark + while ((yield foreign_count("http://example2.com/"))); // -1 keyword + + yield check_no_orphans(); +}); + +add_task(function* test_sameURIDifferentKeyword() { + let fc = yield foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({keyword: "keyword", url: "http://example.com/" }); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword + + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "keyword", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid ] } ]); + + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" }); + yield check_keyword(true, "http://example.com/", "keyword"); + yield check_keyword(true, "http://example.com/", "keyword2"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +1 keyword + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "keyword2", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid ] } ]); + + // Add a third keyword. + yield PlacesUtils.keywords.insert({ keyword: "keyword3", url: "http://example.com/" }); + yield check_keyword(true, "http://example.com/", "keyword"); + yield check_keyword(true, "http://example.com/", "keyword2"); + yield check_keyword(true, "http://example.com/", "keyword3"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 4); // +1 keyword + + // Remove one of the keywords. + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + yield check_keyword(false, "http://example.com/", "keyword"); + yield check_keyword(true, "http://example.com/", "keyword2"); + yield check_keyword(true, "http://example.com/", "keyword3"); + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), + "keyword", false, "", + bookmark.lastModified, bookmark.type, + (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), + bookmark.guid, bookmark.parentGuid ] } ]); + Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // -1 keyword + + // Now remove the bookmark. + yield PlacesUtils.bookmarks.remove(bookmark); + while ((yield foreign_count("http://example.com/"))); + yield check_keyword(false, "http://example.com/", "keyword"); + yield check_keyword(false, "http://example.com/", "keyword2"); + yield check_keyword(false, "http://example.com/", "keyword3"); + + check_no_orphans(); +}); + +add_task(function* test_deleteKeywordMultipleBookmarks() { + let fc = yield foreign_count("http://example.com/"); + + let observer = expectBookmarkNotifications(); + let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + + yield check_keyword(true, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +2 bookmark +1 keyword + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), + "keyword", false, "keyword", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), + "keyword", false, "keyword", + bookmark1.lastModified, bookmark1.type, + (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), + bookmark1.guid, bookmark1.parentGuid ] } ]); + + observer = expectBookmarkNotifications(); + yield PlacesUtils.keywords.remove("keyword"); + yield check_keyword(false, "http://example.com/", "keyword"); + Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // -1 keyword + observer.check([{ name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), + "keyword", false, "", + bookmark2.lastModified, bookmark2.type, + (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), + bookmark2.guid, bookmark2.parentGuid ] }, + { name: "onItemChanged", + arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), + "keyword", false, "", + bookmark1.lastModified, bookmark1.type, + (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), + bookmark1.guid, bookmark1.parentGuid ] } ]); + + // Now remove the bookmarks. + yield PlacesUtils.bookmarks.remove(bookmark1); + yield PlacesUtils.bookmarks.remove(bookmark2); + Assert.equal((yield foreign_count("http://example.com/")), fc); // -2 bookmarks + + check_no_orphans(); +}); + +add_task(function* test_multipleKeywordsSamePostData() { + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", postData: "postData1" }); + yield check_keyword(true, "http://example.com/", "keyword", "postData1"); + // Add another keyword with same postData, should fail. + yield Assert.rejects(PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", postData: "postData1" }), + /constraint failed/); + yield check_keyword(false, "http://example.com/", "keyword2", "postData1"); + + yield PlacesUtils.keywords.remove("keyword"); + + check_no_orphans(); +}); + +add_task(function* test_oldPostDataAPI() { + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); + let itemId = yield PlacesUtils.promiseItemId(bookmark.guid); + yield PlacesUtils.setPostDataForBookmark(itemId, "postData"); + yield check_keyword(true, "http://example.com/", "keyword", "postData"); + Assert.equal(PlacesUtils.getPostDataForBookmark(itemId), "postData"); + + yield PlacesUtils.keywords.remove("keyword"); + yield PlacesUtils.bookmarks.remove(bookmark); + + check_no_orphans(); +}); + +add_task(function* test_oldKeywordsAPI() { + let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + yield check_keyword(false, "http://example.com/", "keyword"); + let itemId = yield PlacesUtils.promiseItemId(bookmark.guid); + + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword"); + yield promiseKeyword("keyword", "http://example.com/"); + + // Remove the keyword. + PlacesUtils.bookmarks.setKeywordForBookmark(itemId, ""); + yield promiseKeyword("keyword", null); + + yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com" }); + Assert.equal(PlacesUtils.bookmarks.getKeywordForBookmark(itemId), "keyword"); + Assert.equal(PlacesUtils.bookmarks.getURIForKeyword("keyword").spec, "http://example.com/"); + yield PlacesUtils.bookmarks.remove(bookmark); + + check_no_orphans(); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/test_placesTxn.js b/toolkit/components/places/tests/unit/test_placesTxn.js index f8bb1aa83b7c..9c8979fe2990 100644 --- a/toolkit/components/places/tests/unit/test_placesTxn.js +++ b/toolkit/components/places/tests/unit/test_placesTxn.js @@ -732,8 +732,8 @@ add_test(function test_edit_postData() { txn.undoTransaction(); [url, post_data] = PlacesUtils.getURLAndPostDataForKeyword("kw"); Assert.equal(url, testURI.spec); - Assert.equal(null, post_data); - + // We don't allow anymore to set a null post data. + //Assert.equal(null, post_data); run_next_test(); }); diff --git a/toolkit/components/places/tests/unit/xpcshell.ini b/toolkit/components/places/tests/unit/xpcshell.ini index d815f91b5efb..5aa5be74d0c8 100644 --- a/toolkit/components/places/tests/unit/xpcshell.ini +++ b/toolkit/components/places/tests/unit/xpcshell.ini @@ -44,7 +44,6 @@ skip-if = os == "android" [test_419731.js] [test_419792_node_tags_property.js] [test_420331_wyciwyg.js] -[test_421180.js] [test_425563.js] [test_429505_remove_shortcuts.js] [test_433317_query_title_update.js] @@ -104,6 +103,7 @@ skip-if = os == "android" [test_hosts_triggers.js] [test_isURIVisited.js] [test_isvisited.js] +[test_keywords.js] [test_lastModified.js] [test_markpageas.js] [test_mozIAsyncLivemarks.js] From bc9ba496861b4395d5632e73fac402ddbda738fa Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Fri, 20 Mar 2015 12:08:47 +0100 Subject: [PATCH 18/80] Bug 1125115 - Remove (now) pointless test that is failing to reopen a CLOSED TREE --- .../places/tests/bookmarks/test_bookmarks.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks.js b/toolkit/components/places/tests/bookmarks/test_bookmarks.js index 8911f68a5805..96901fec9508 100644 --- a/toolkit/components/places/tests/bookmarks/test_bookmarks.js +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks.js @@ -321,27 +321,8 @@ add_task(function test_bookmarks() { // test setKeywordForBookmark let kwTestItemId = bs.insertBookmark(testRoot, uri("http://keywordtest.com"), bs.DEFAULT_INDEX, ""); - - dateAdded = bs.getItemDateAdded(kwTestItemId); - // after just inserting, modified should not be set - lastModified = bs.getItemLastModified(kwTestItemId); - do_check_eq(lastModified, dateAdded); - // Workaround possible VM timers issues moving lastModified and dateAdded - // to the past. - lastModified -= 1000; - bs.setItemLastModified(kwTestItemId, lastModified); - dateAdded -= 1000; - bs.setItemDateAdded(kwTestItemId, dateAdded); bs.setKeywordForBookmark(kwTestItemId, "bar"); - lastModified2 = bs.getItemLastModified(kwTestItemId); - LOG("test setKeywordForBookmark"); - LOG("dateAdded = " + dateAdded); - LOG("lastModified = " + lastModified); - LOG("lastModified2 = " + lastModified2); - do_check_true(is_time_ordered(lastModified, lastModified2)); - do_check_true(is_time_ordered(dateAdded, lastModified2)); - let lastModified3 = bs.getItemLastModified(kwTestItemId); // test getKeywordForBookmark let k = bs.getKeywordForBookmark(kwTestItemId); do_check_eq("bar", k); From 79075a61e1a37d993c5a1bbc6147dc4a945d263f Mon Sep 17 00:00:00 2001 From: Jorg K Date: Fri, 20 Mar 2015 11:18:07 +0100 Subject: [PATCH 19/80] Bug 1042561 - Autocomplete: Typed text in red despite results/matches found if suggestions change by last input r=mak --- toolkit/components/autocomplete/nsAutoCompleteController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.cpp b/toolkit/components/autocomplete/nsAutoCompleteController.cpp index 8eaa95b74b21..bdaa78625ba1 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp +++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp @@ -1505,7 +1505,7 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes // get results in the future to avoid unnecessarily canceling searches. if (mRowCount || !minResults) { OpenPopup(); - } else if (result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { + } else if (mSearchesOngoing == 0) { ClosePopup(); } } From 6b0e509bcd09a1ad14c88c1806bb5c9f159a0cdb Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Fri, 20 Mar 2015 11:15:40 +0100 Subject: [PATCH 20/80] Bug 1042561 - Correct autocomplete search result update handling in OnUpdateSearchResult() r=mak --- .../autocomplete/nsAutoCompleteController.cpp | 66 ++++++++++++------- .../autocomplete/nsAutoCompleteController.h | 2 + 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.cpp b/toolkit/components/autocomplete/nsAutoCompleteController.cpp index bdaa78625ba1..7df45105b1c8 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp +++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp @@ -732,6 +732,18 @@ nsAutoCompleteController::GetSearchString(nsAString &aSearchString) return NS_OK; } +void +nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch, + nsIAutoCompleteResult *aResult) +{ + // Look up the index of the search which is returning. + for (uint32_t i = 0; i < mSearches.Length(); ++i) { + if (mSearches[i] == aSearch) { + ProcessResult(i, aResult); + } + } +} + //////////////////////////////////////////////////////////////////////// //// nsIAutoCompleteObserver @@ -739,18 +751,41 @@ nsAutoCompleteController::GetSearchString(nsAString &aSearchString) NS_IMETHODIMP nsAutoCompleteController::OnUpdateSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult) { + MOZ_ASSERT(mSearches.Contains(aSearch)); + ClearResults(); - return OnSearchResult(aSearch, aResult); + HandleSearchResult(aSearch, aResult); + return NS_OK; } NS_IMETHODIMP nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult) { - // look up the index of the search which is returning - for (uint32_t i = 0; i < mSearches.Length(); ++i) { - if (mSearches[i] == aSearch) { - ProcessResult(i, aResult); - } + MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch)); + + // If this is the first search result we are processing + // we should clear out the previously cached results. + if (mFirstSearchResult) { + ClearResults(); + mFirstSearchResult = false; + } + + uint16_t result = 0; + if (aResult) { + aResult->GetSearchResult(&result); + } + + // If our results are incremental, the search is still ongoing. + if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING && + result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { + --mSearchesOngoing; + } + + HandleSearchResult(aSearch, aResult); + + if (mSearchesOngoing == 0) { + // If this is the last search to return, cleanup. + PostSearchCleanup(); } return NS_OK; @@ -1107,6 +1142,7 @@ nsAutoCompleteController::StartSearch(uint16_t aSearchType) rv = search->StartSearch(mSearchString, searchParam, result, static_cast(this)); if (NS_FAILED(rv)) { ++mSearchesFailed; + MOZ_ASSERT(mSearchesOngoing > 0); --mSearchesOngoing; } // Because of the joy of nested event loops (which can easily happen when some @@ -1429,23 +1465,10 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes NS_ENSURE_STATE(mInput); nsCOMPtr input(mInput); - // If this is the first search result we are processing - // we should clear out the previously cached results - if (mFirstSearchResult) { - ClearResults(); - mFirstSearchResult = false; - } - uint16_t result = 0; if (aResult) aResult->GetSearchResult(&result); - // if our results are incremental, the search is still ongoing - if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING && - result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { - --mSearchesOngoing; - } - uint32_t oldMatchCount = 0; uint32_t matchCount = 0; if (aResult) @@ -1516,11 +1539,6 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes CompleteDefaultIndex(resultIndex); } - if (mSearchesOngoing == 0) { - // If this is the last search to return, cleanup. - PostSearchCleanup(); - } - return NS_OK; } diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.h b/toolkit/components/autocomplete/nsAutoCompleteController.h index f96e55f3ab6b..6d2676a6018c 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.h +++ b/toolkit/components/autocomplete/nsAutoCompleteController.h @@ -50,6 +50,8 @@ protected: nsresult ClearSearchTimer(); void MaybeCompletePlaceholder(); + void HandleSearchResult(nsIAutoCompleteSearch *aSearch, + nsIAutoCompleteResult *aResult); nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult); nsresult PostSearchCleanup(); From 1d9e7740aa73fdc62334b2763fd2dce4a8e2e78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eddy=20Bru=C3=ABl?= Date: Fri, 20 Mar 2015 12:11:54 +0100 Subject: [PATCH 21/80] Bug 1138975 - Refactor breakpoint sliding for non-source mapped sources;r=jlongster --- .../test/browser_dbg_source-maps-04.js | 18 +- toolkit/devtools/server/actors/script.js | 218 +++++++++++++++--- .../server/actors/utils/ScriptStore.js | 6 + 3 files changed, 205 insertions(+), 37 deletions(-) diff --git a/browser/devtools/debugger/test/browser_dbg_source-maps-04.js b/browser/devtools/debugger/test/browser_dbg_source-maps-04.js index cc285c493125..226a40a2de90 100644 --- a/browser/devtools/debugger/test/browser_dbg_source-maps-04.js +++ b/browser/devtools/debugger/test/browser_dbg_source-maps-04.js @@ -89,11 +89,11 @@ function testSetBreakpoint() { let sourceForm = getSourceForm(gSources, JS_URL); let source = gDebugger.gThreadClient.source(sourceForm); - source.setBreakpoint({ line: 3, column: 61 }, aResponse => { + source.setBreakpoint({ line: 3, column: 60 }, aResponse => { ok(!aResponse.error, "Should be able to set a breakpoint in a js file."); ok(!aResponse.actualLocation, - "Should be able to set a breakpoint on line 3 and column 61."); + "Should be able to set a breakpoint on line 3 and column 60."); deferred.resolve(); }); @@ -115,20 +115,16 @@ function testHitBreakpoint() { is(aResponse.type, "resumed", "Type should be 'resumed'."); waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { - is(gFrames.itemCount, 2, "Should have two frames."); + is(gFrames.itemCount, 1, "Should have one frame."); // This is weird, but we need to let the debugger a chance to // update first executeSoon(() => { gDebugger.gThreadClient.resume(() => { - gDebugger.gThreadClient.addOneTimeListener("paused", () => { - gDebugger.gThreadClient.resume(() => { - // We also need to make sure the next step doesn't add a - // "resumed" handler until this is completely finished - executeSoon(() => { - deferred.resolve(); - }); - }); + // We also need to make sure the next step doesn't add a + // "resumed" handler until this is completely finished + executeSoon(() => { + deferred.resolve(); }); }); }); diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index f4b56a9ee621..842fa02518ae 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -2791,21 +2791,16 @@ SourceActor.prototype = { * Ensure the given BreakpointActor is set as a breakpoint handler on all * scripts that match its location in the original source. * - * It is possible that no scripts match the given location, because they have - * all been garbage collected. In that case, the BreakpointActor is not set as - * a breakpoint handler for any script, but is still inserted in the - * BreakpointActorMap as a pending breakpoint. Whenever a new script is - * introduced, we call this method again to see if there are now any scripts - * that matches the given location. + * If there are no scripts that match the location of the BreakpointActor, + * we slide its location to the next closest line (for line breakpoints) or + * column (for column breakpoint) that does. * - * The first time we find one or more scripts that matches the given location, - * we check if any of these scripts has any entry points for the given - * location. If not, we assume that the given location does not have any code. - * - * If the given location does not contain any code, we slide the breakpoint - * down to the next closest line that does, and update the BreakpointActorMap - * accordingly. Note that we only do so if the BreakpointActor is still - * pending (i.e. is not set as a breakpoint handler for any script). + * If breakpoint sliding fails, then either there are no scripts that contain + * any code for the given location, or they were all garbage collected before + * the debugger started running. We cannot distinguish between these two + * cases, so we insert the BreakpointActor in the BreakpointActorMap as + * a pending breakpoint. Whenever a new script is introduced, this method is + * called again for each pending breakpoint. * * @param BreakpointActor actor * The BreakpointActor to be set as a breakpoint handler. @@ -2813,21 +2808,125 @@ SourceActor.prototype = { * @returns A Promise that resolves to the given BreakpointActor. */ _setBreakpointForActor: function (actor) { + let { originalLocation } = actor; + if (this.isSourceMapped) { - return this.threadActor.sources.getGeneratedLocation( - actor.originalLocation - ).then((generatedLocation) => { + // TODO: Refactor breakpoint sliding for source mapped sources. + return this.threadActor.sources.getGeneratedLocation(originalLocation) + .then((generatedLocation) => { return generatedLocation.generatedSourceActor - ._setBreakpointForActorAtLocation( + ._setBreakpointForActorAtLocationWithSliding( actor, generatedLocation ); }); } else { - return Promise.resolve(this._setBreakpointForActorAtLocation( - actor, - GeneratedLocation.fromOriginalLocation(actor.originalLocation) - )); + // If this is a non-source mapped source, the original location and + // generated location are the same, so we can safely convert between them. + let generatedLocation = GeneratedLocation.fromOriginalLocation(originalLocation); + let { generatedColumn } = generatedLocation; + + // Try to set the breakpoint on the generated location directly. If this + // succeeds, we can avoid the more expensive breakpoint sliding algorithm + // below. + if (this._setBreakpointForActorAtLocation(actor, generatedLocation)) { + return Promise.resolve(actor); + } + + // There were no scripts that matched the given location, so we need to + // perform breakpoint sliding. + if (generatedColumn === undefined) { + // To perform breakpoint sliding for line breakpoints, we need to build + // a map from line numbers to a list of entry points for each line, + // implemented as a sparse array. An entry point is a (script, offsets) + // pair, and represents all offsets in that script that are entry points + // for the corresponding line. + let lineToEntryPointsMap = []; + + // Iterate over all scripts that correspond to this source actor. + let scripts = this.scripts.getScriptsBySourceActor(this); + for (let script of scripts) { + // Get all offsets for each line in the current script. This returns + // a map from line numbers fo a list of offsets for each line, + // implemented as a sparse array. + let lineToOffsetsMap = script.getAllOffsets(); + + // Iterate over each line, and add their list of offsets to the map + // from line numbers to entry points by forming a (script, offsets) + // pair, where script is the current script, and offsets is the list + // of offsets for the current line. + for (let line = 0; line < lineToOffsetsMap.length; ++line) { + let offsets = lineToOffsetsMap[line]; + if (offsets) { + let entryPoints = lineToEntryPointsMap[line]; + if (!entryPoints) { + // We dont have a list of entry points for the current line + // number yet, so create it and add it to the map. + entryPoints = []; + lineToEntryPointsMap[line] = entryPoints; + } + entryPoints.push({ script, offsets }); + } + } + } + + let { + originalSourceActor, + originalLine, + originalColumn + } = originalLocation; + + // Now that we have a map from line numbers to a list of entry points + // for each line, we can use it to perform breakpoint sliding. Start + // at the original line of the breakpoint actor, and keep incrementing + // it by one, until either we find a line that has at least one entry + // point, or we go past the last line in the map. + // + // Note that by computing the entire map up front, and implementing it + // as a sparse array, we can easily tell when we went past the last line + // in the map. + let actualLine = originalLine; + while (actualLine < lineToEntryPointsMap.length) { + let entryPoints = lineToEntryPointsMap[actualLine]; + if (entryPoints) { + setBreakpointForActorAtEntryPoints(actor, entryPoints); + break; + } + ++actualLine; + } + if (actualLine === lineToEntryPointsMap.length) { + // We went past the last line in the map, so breakpoint sliding + // failed. Keep the BreakpointActor in the BreakpointActorMap as a + // pending breakpoint, so we can try again whenever a new script is + // introduced. + return Promise.resolve(actor); + } + + // If the actual line on which the BreakpointActor was set differs from + // the original line that was requested, the BreakpointActor and the + // BreakpointActorMap need to be updated accordingly. + if (actualLine !== originalLine) { + let actualLocation = new OriginalLocation( + originalSourceActor, + actualLine + ); + let existingActor = this.breakpointActorMap.getActor(actualLocation); + if (existingActor) { + actor.onDelete(); + this.breakpointActorMap.deleteActor(originalLocation); + actor = existingActor; + } else { + this.breakpointActorMap.deleteActor(originalLocation); + actor.originalLocation = actualLocation; + this.breakpointActorMap.setActor(actualLocation, actor); + } + } + + return Promise.resolve(actor); + } else { + // TODO: Implement breakpoint sliding for column breakpoints + return Promise.resolve(actor); + } } }, @@ -2841,8 +2940,79 @@ SourceActor.prototype = { * A GeneratedLocation representing the location in the generated * source for which the given BreakpointActor is to be set as a * breakpoint handler. + * + * @returns A Boolean that is true if the BreakpointActor was set as a + * breakpoint handler on at least one script, and false otherwise. */ _setBreakpointForActorAtLocation: function (actor, generatedLocation) { + let { generatedLine, generatedColumn } = generatedLocation; + + // Find all scripts that match the given source actor and line number. + let scripts = this.scripts.getScriptsBySourceActorAndLine( + this, + generatedLine + ).filter((script) => !actor.hasScript(script)); + + // Find all entry points that correspond to the given location. + let entryPoints = []; + if (generatedColumn === undefined) { + // This is a line breakpoint, so we are interested in all offsets + // that correspond to the given line number. + for (let script of scripts) { + let offsets = script.getLineOffsets(generatedLine); + if (offsets.length > 0) { + entryPoints.push({ script, offsets }); + } + } + } else { + // This is a column breakpoint, so we are interested in all column + // offsets that correspond to the given line *and* column number. + for (let script of scripts) { + let columnToOffsetMap = script.getAllColumnOffsets() + .filter(({ lineNumber }) => { + return lineNumber === generatedLine; + }); + for (let { columnNumber: column, offset } of columnToOffsetMap) { + // TODO: What we are actually interested in here is a range of + // columns, rather than a single one. + if (column == generatedColumn) { + entryPoints.push({ script, offsets: [offset] }); + } + } + } + } + + if (entryPoints.length === 0) { + return false; + } + setBreakpointForActorAtEntryPoints(actor, entryPoints); + return true; + }, + + /* + * Ensure the given BreakpointActor is set as breakpoint handler on all + * scripts that match the given location in the generated source. + * + * TODO: This method is bugged, because it performs breakpoint sliding on + * generated locations. Breakpoint sliding should be performed on original + * locations, because there is no guarantee that the next line in the + * generated source corresponds to the next line in an original source. + * + * The only place this method is still used is from setBreakpointForActor + * when called for a source mapped source. Once that code has been refactored, + * this method can be removed. + * + * @param BreakpointActor actor + * The BreakpointActor to be set as a breakpoint handler. + * @param GeneratedLocation generatedLocation + * A GeneratedLocation representing the location in the generated + * source for which the given BreakpointActor is to be set as a + * breakpoint handler. + * + * @returns A Boolean that is true if the BreakpointActor was set as a + * breakpoint handler on at least one script, and false otherwise. + */ + _setBreakpointForActorAtLocationWithSliding: function (actor, generatedLocation) { let originalLocation = actor.originalLocation; let { generatedLine, generatedColumn } = generatedLocation; @@ -2967,10 +3137,6 @@ SourceActor.prototype = { } actor.addScript(script, this.threadActor); } - - return { - actor: actor.actorID - }; }, /** diff --git a/toolkit/devtools/server/actors/utils/ScriptStore.js b/toolkit/devtools/server/actors/utils/ScriptStore.js index 9d9a557057e6..a1e5d8a1e70d 100644 --- a/toolkit/devtools/server/actors/utils/ScriptStore.js +++ b/toolkit/devtools/server/actors/utils/ScriptStore.js @@ -79,6 +79,12 @@ ScriptStore.prototype = { return this._scripts.items; }, + getScriptsBySourceActor(sourceActor) { + return sourceActor.source ? + this.getScriptsBySource(sourceActor.source) : + this.getScriptsByURL(sourceActor._originalUrl); + }, + getScriptsBySourceActorAndLine(sourceActor, line) { return sourceActor.source ? this.getScriptsBySourceAndLine(sourceActor.source, line) : From c150e8dbed50f3efcd05d18d76f4d86d0120a163 Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Fri, 20 Mar 2015 13:32:01 +0100 Subject: [PATCH 22/80] Backed out changeset ef86c7c53d21 (bug 1042561) --- .../autocomplete/nsAutoCompleteController.cpp | 66 +++++++------------ .../autocomplete/nsAutoCompleteController.h | 2 - 2 files changed, 24 insertions(+), 44 deletions(-) diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.cpp b/toolkit/components/autocomplete/nsAutoCompleteController.cpp index 7df45105b1c8..bdaa78625ba1 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp +++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp @@ -732,18 +732,6 @@ nsAutoCompleteController::GetSearchString(nsAString &aSearchString) return NS_OK; } -void -nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch, - nsIAutoCompleteResult *aResult) -{ - // Look up the index of the search which is returning. - for (uint32_t i = 0; i < mSearches.Length(); ++i) { - if (mSearches[i] == aSearch) { - ProcessResult(i, aResult); - } - } -} - //////////////////////////////////////////////////////////////////////// //// nsIAutoCompleteObserver @@ -751,41 +739,18 @@ nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch, NS_IMETHODIMP nsAutoCompleteController::OnUpdateSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult) { - MOZ_ASSERT(mSearches.Contains(aSearch)); - ClearResults(); - HandleSearchResult(aSearch, aResult); - return NS_OK; + return OnSearchResult(aSearch, aResult); } NS_IMETHODIMP nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult) { - MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch)); - - // If this is the first search result we are processing - // we should clear out the previously cached results. - if (mFirstSearchResult) { - ClearResults(); - mFirstSearchResult = false; - } - - uint16_t result = 0; - if (aResult) { - aResult->GetSearchResult(&result); - } - - // If our results are incremental, the search is still ongoing. - if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING && - result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { - --mSearchesOngoing; - } - - HandleSearchResult(aSearch, aResult); - - if (mSearchesOngoing == 0) { - // If this is the last search to return, cleanup. - PostSearchCleanup(); + // look up the index of the search which is returning + for (uint32_t i = 0; i < mSearches.Length(); ++i) { + if (mSearches[i] == aSearch) { + ProcessResult(i, aResult); + } } return NS_OK; @@ -1142,7 +1107,6 @@ nsAutoCompleteController::StartSearch(uint16_t aSearchType) rv = search->StartSearch(mSearchString, searchParam, result, static_cast(this)); if (NS_FAILED(rv)) { ++mSearchesFailed; - MOZ_ASSERT(mSearchesOngoing > 0); --mSearchesOngoing; } // Because of the joy of nested event loops (which can easily happen when some @@ -1465,10 +1429,23 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes NS_ENSURE_STATE(mInput); nsCOMPtr input(mInput); + // If this is the first search result we are processing + // we should clear out the previously cached results + if (mFirstSearchResult) { + ClearResults(); + mFirstSearchResult = false; + } + uint16_t result = 0; if (aResult) aResult->GetSearchResult(&result); + // if our results are incremental, the search is still ongoing + if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING && + result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { + --mSearchesOngoing; + } + uint32_t oldMatchCount = 0; uint32_t matchCount = 0; if (aResult) @@ -1539,6 +1516,11 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes CompleteDefaultIndex(resultIndex); } + if (mSearchesOngoing == 0) { + // If this is the last search to return, cleanup. + PostSearchCleanup(); + } + return NS_OK; } diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.h b/toolkit/components/autocomplete/nsAutoCompleteController.h index 6d2676a6018c..f96e55f3ab6b 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.h +++ b/toolkit/components/autocomplete/nsAutoCompleteController.h @@ -50,8 +50,6 @@ protected: nsresult ClearSearchTimer(); void MaybeCompletePlaceholder(); - void HandleSearchResult(nsIAutoCompleteSearch *aSearch, - nsIAutoCompleteResult *aResult); nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult); nsresult PostSearchCleanup(); From cb5b2490cb6822a428c46a30f43fe4591de35765 Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Fri, 20 Mar 2015 13:32:28 +0100 Subject: [PATCH 23/80] backed out changeset b798fd098e09 (bug 1042561) for assertion failure in mochitests --- toolkit/components/autocomplete/nsAutoCompleteController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.cpp b/toolkit/components/autocomplete/nsAutoCompleteController.cpp index bdaa78625ba1..8eaa95b74b21 100644 --- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp +++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp @@ -1505,7 +1505,7 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes // get results in the future to avoid unnecessarily canceling searches. if (mRowCount || !minResults) { OpenPopup(); - } else if (mSearchesOngoing == 0) { + } else if (result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { ClosePopup(); } } From 2ba8b0c44c91fca9b124a2848add6c1327fcdd09 Mon Sep 17 00:00:00 2001 From: Alex Verstak Date: Fri, 20 Mar 2015 02:07:00 -0700 Subject: [PATCH 24/80] Bug 1144816 - focus the content window before popping up the context menu, rs=gijs --- browser/base/content/test/referrer/head.js | 49 +++++++++++++++------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/browser/base/content/test/referrer/head.js b/browser/base/content/test/referrer/head.js index 30885391b8c2..b1c86e6ed156 100644 --- a/browser/base/content/test/referrer/head.js +++ b/browser/base/content/test/referrer/head.js @@ -99,26 +99,45 @@ function clickTheLink(aWindow, aLinkId, aOptions) { function(data) { let element = content.document.getElementById(data.id); let options = data.options; - element.focus(); // EventUtils.synthesizeMouseAtCenter(element, options, content); // Alas, EventUtils doesn't work in the content task environment. - var domWindowUtils = - content.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindowUtils); - var rect = element.getBoundingClientRect(); - var left = rect.left + rect.width / 2; - var top = rect.top + rect.height / 2; - var button = options.button || 0; - function sendMouseEvent(type) { - domWindowUtils.sendMouseEvent(type, left, top, button, - 1, 0, false, 0, 0, true); + function doClick() { + var domWindowUtils = + content.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + var rect = element.getBoundingClientRect(); + var left = rect.left + rect.width / 2; + var top = rect.top + rect.height / 2; + var button = options.button || 0; + function sendMouseEvent(type) { + domWindowUtils.sendMouseEvent(type, left, top, button, + 1, 0, false, 0, 0, true); + } + if ("type" in options) { + sendMouseEvent(options.type); // e.g., "contextmenu" + } else { + sendMouseEvent("mousedown"); + sendMouseEvent("mouseup"); + } } - if ("type" in options) { - sendMouseEvent(options.type); // e.g., "contextmenu" + + // waitForFocus(doClick, content); + let focusManager = Components.classes["@mozilla.org/focus-manager;1"]. + getService(Components.interfaces.nsIFocusManager); + let desiredWindow = {}; + focusManager.getFocusedElementForWindow(content, true, desiredWindow); + desiredWindow = desiredWindow.value; + if (desiredWindow == focusManager.focusedWindow) { + // The window is already focused - click away. + doClick(); } else { - sendMouseEvent("mousedown"); - sendMouseEvent("mouseup"); + // Focus the window first, then click. + desiredWindow.addEventListener("focus", function onFocus() { + desiredWindow.removeEventListener("focus", onFocus, true); + setTimeout(doClick, 0); + }, true); + desiredWindow.focus(); } }); } From dc4ca20d503f6a74e01220a9de6ed168bbb9641f Mon Sep 17 00:00:00 2001 From: Martyn Haigh Date: Fri, 20 Mar 2015 13:42:09 +0000 Subject: [PATCH 25/80] Bug 1129433 - Show "# tabs opened in background" notification in system tray (r=mcomella) --- .../base/locales/en-US/android_strings.dtd | 14 +++++ mobile/android/base/resources/values/ids.xml | 1 + mobile/android/base/strings.xml.in | 4 ++ .../android/base/tabqueue/TabQueueHelper.java | 58 ++++++++++++++++--- .../base/tabqueue/TabQueueService.java | 7 ++- 5 files changed, 73 insertions(+), 11 deletions(-) diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index b0c870fcbcfb..b804ac187027 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -186,6 +186,20 @@ + + + + + + + + diff --git a/mobile/android/base/resources/values/ids.xml b/mobile/android/base/resources/values/ids.xml index 5dfad221447e..de79130c2d30 100644 --- a/mobile/android/base/resources/values/ids.xml +++ b/mobile/android/base/resources/values/ids.xml @@ -5,6 +5,7 @@ + diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in index 74722e51f360..c31bec472e5c 100644 --- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -241,6 +241,10 @@ &tab_queue_toast_message; &tab_queue_toast_action; + &tab_queue_notification_text_singular; + &tab_queue_notification_text_plural; + &tab_queue_notification_title_singular; + &tab_queue_notification_title_plural; &pref_about_firefox; &pref_vendor_faqs; diff --git a/mobile/android/base/tabqueue/TabQueueHelper.java b/mobile/android/base/tabqueue/TabQueueHelper.java index ffa80cd3dcd7..aa9babbd9284 100644 --- a/mobile/android/base/tabqueue/TabQueueHelper.java +++ b/mobile/android/base/tabqueue/TabQueueHelper.java @@ -5,20 +5,24 @@ package org.mozilla.gecko.tabqueue; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.util.ThreadUtils; - -import android.text.TextUtils; -import android.util.Log; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.support.v4.app.NotificationCompat; import org.json.JSONArray; -import org.json.JSONException; - -import java.io.IOException; +import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.R; +import org.mozilla.gecko.util.ThreadUtils; public class TabQueueHelper { private static final String LOGTAG = "Gecko" + TabQueueHelper.class.getSimpleName(); public static final String FILE_NAME = "tab_queue_url_list.json"; + public static final String LOAD_URLS_ACTION = "TAB_QUEUE_LOAD_URLS_ACTION"; + public static final int TAB_QUEUE_NOTIFICATION_ID = R.id.tabQueueNotification; /** * Reads file and converts any content to JSON, adds passed in URL to the data and writes back to the file, @@ -27,8 +31,9 @@ public class TabQueueHelper { * @param profile * @param url URL to add * @param filename filename to add URL to + * @return the number of tabs currently queued */ - public static void queueURL(final GeckoProfile profile, final String url, final String filename) { + public static int queueURL(final GeckoProfile profile, final String url, final String filename) { ThreadUtils.assertNotOnUiThread(); JSONArray jsonArray = profile.readJSONArrayFromFile(filename); @@ -36,5 +41,40 @@ public class TabQueueHelper { jsonArray.put(url); profile.writeFile(filename, jsonArray.toString()); + + return jsonArray.length(); + } + + /** + * Displays a notification showing the total number of tabs queue. If there is already a notification displayed, it + * will be replaced. + * + * @param context + * @param tabsQueued + */ + static public void showNotification(Context context, int tabsQueued) { + Intent resultIntent = new Intent(context, BrowserApp.class); + resultIntent.setAction(TabQueueHelper.LOAD_URLS_ACTION); + + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT); + + String title, text; + final Resources resources = context.getResources(); + if(tabsQueued == 1) { + title = resources.getString(R.string.tab_queue_notification_title_singular); + text = resources.getString(R.string.tab_queue_notification_text_singular); + } else { + title = resources.getString(R.string.tab_queue_notification_title_plural); + text = resources.getString(R.string.tab_queue_notification_text_plural, tabsQueued); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context) + .setSmallIcon(R.drawable.ic_status_logo) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(pendingIntent); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(TabQueueHelper.TAB_QUEUE_NOTIFICATION_ID, builder.build()); } } \ No newline at end of file diff --git a/mobile/android/base/tabqueue/TabQueueService.java b/mobile/android/base/tabqueue/TabQueueService.java index 4a98817def9c..691918716730 100644 --- a/mobile/android/base/tabqueue/TabQueueService.java +++ b/mobile/android/base/tabqueue/TabQueueService.java @@ -6,6 +6,7 @@ package org.mozilla.gecko.tabqueue; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.PixelFormat; @@ -160,8 +161,10 @@ public class TabQueueService extends Service { executorService.submit(new Runnable() { @Override public void run() { - final GeckoProfile profile = GeckoProfile.get(getApplicationContext()); - TabQueueHelper.queueURL(profile, intentData, filename); + Context applicationContext = getApplicationContext(); + final GeckoProfile profile = GeckoProfile.get(applicationContext); + int tabsQueued = TabQueueHelper.queueURL(profile, intentData, filename); + TabQueueHelper.showNotification(applicationContext, tabsQueued); } }); } From 9bacd9217eaf42dd5a1841255c563a5866a06ebb Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Fri, 20 Mar 2015 15:23:45 +0100 Subject: [PATCH 26/80] Backed out changeset de24b63c6966 (bug 1138975) for m-dt test failures --- .../test/browser_dbg_source-maps-04.js | 18 +- toolkit/devtools/server/actors/script.js | 218 +++--------------- .../server/actors/utils/ScriptStore.js | 6 - 3 files changed, 37 insertions(+), 205 deletions(-) diff --git a/browser/devtools/debugger/test/browser_dbg_source-maps-04.js b/browser/devtools/debugger/test/browser_dbg_source-maps-04.js index 226a40a2de90..cc285c493125 100644 --- a/browser/devtools/debugger/test/browser_dbg_source-maps-04.js +++ b/browser/devtools/debugger/test/browser_dbg_source-maps-04.js @@ -89,11 +89,11 @@ function testSetBreakpoint() { let sourceForm = getSourceForm(gSources, JS_URL); let source = gDebugger.gThreadClient.source(sourceForm); - source.setBreakpoint({ line: 3, column: 60 }, aResponse => { + source.setBreakpoint({ line: 3, column: 61 }, aResponse => { ok(!aResponse.error, "Should be able to set a breakpoint in a js file."); ok(!aResponse.actualLocation, - "Should be able to set a breakpoint on line 3 and column 60."); + "Should be able to set a breakpoint on line 3 and column 61."); deferred.resolve(); }); @@ -115,16 +115,20 @@ function testHitBreakpoint() { is(aResponse.type, "resumed", "Type should be 'resumed'."); waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => { - is(gFrames.itemCount, 1, "Should have one frame."); + is(gFrames.itemCount, 2, "Should have two frames."); // This is weird, but we need to let the debugger a chance to // update first executeSoon(() => { gDebugger.gThreadClient.resume(() => { - // We also need to make sure the next step doesn't add a - // "resumed" handler until this is completely finished - executeSoon(() => { - deferred.resolve(); + gDebugger.gThreadClient.addOneTimeListener("paused", () => { + gDebugger.gThreadClient.resume(() => { + // We also need to make sure the next step doesn't add a + // "resumed" handler until this is completely finished + executeSoon(() => { + deferred.resolve(); + }); + }); }); }); }); diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index 842fa02518ae..f4b56a9ee621 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -2791,16 +2791,21 @@ SourceActor.prototype = { * Ensure the given BreakpointActor is set as a breakpoint handler on all * scripts that match its location in the original source. * - * If there are no scripts that match the location of the BreakpointActor, - * we slide its location to the next closest line (for line breakpoints) or - * column (for column breakpoint) that does. + * It is possible that no scripts match the given location, because they have + * all been garbage collected. In that case, the BreakpointActor is not set as + * a breakpoint handler for any script, but is still inserted in the + * BreakpointActorMap as a pending breakpoint. Whenever a new script is + * introduced, we call this method again to see if there are now any scripts + * that matches the given location. * - * If breakpoint sliding fails, then either there are no scripts that contain - * any code for the given location, or they were all garbage collected before - * the debugger started running. We cannot distinguish between these two - * cases, so we insert the BreakpointActor in the BreakpointActorMap as - * a pending breakpoint. Whenever a new script is introduced, this method is - * called again for each pending breakpoint. + * The first time we find one or more scripts that matches the given location, + * we check if any of these scripts has any entry points for the given + * location. If not, we assume that the given location does not have any code. + * + * If the given location does not contain any code, we slide the breakpoint + * down to the next closest line that does, and update the BreakpointActorMap + * accordingly. Note that we only do so if the BreakpointActor is still + * pending (i.e. is not set as a breakpoint handler for any script). * * @param BreakpointActor actor * The BreakpointActor to be set as a breakpoint handler. @@ -2808,125 +2813,21 @@ SourceActor.prototype = { * @returns A Promise that resolves to the given BreakpointActor. */ _setBreakpointForActor: function (actor) { - let { originalLocation } = actor; - if (this.isSourceMapped) { - // TODO: Refactor breakpoint sliding for source mapped sources. - return this.threadActor.sources.getGeneratedLocation(originalLocation) - .then((generatedLocation) => { + return this.threadActor.sources.getGeneratedLocation( + actor.originalLocation + ).then((generatedLocation) => { return generatedLocation.generatedSourceActor - ._setBreakpointForActorAtLocationWithSliding( + ._setBreakpointForActorAtLocation( actor, generatedLocation ); }); } else { - // If this is a non-source mapped source, the original location and - // generated location are the same, so we can safely convert between them. - let generatedLocation = GeneratedLocation.fromOriginalLocation(originalLocation); - let { generatedColumn } = generatedLocation; - - // Try to set the breakpoint on the generated location directly. If this - // succeeds, we can avoid the more expensive breakpoint sliding algorithm - // below. - if (this._setBreakpointForActorAtLocation(actor, generatedLocation)) { - return Promise.resolve(actor); - } - - // There were no scripts that matched the given location, so we need to - // perform breakpoint sliding. - if (generatedColumn === undefined) { - // To perform breakpoint sliding for line breakpoints, we need to build - // a map from line numbers to a list of entry points for each line, - // implemented as a sparse array. An entry point is a (script, offsets) - // pair, and represents all offsets in that script that are entry points - // for the corresponding line. - let lineToEntryPointsMap = []; - - // Iterate over all scripts that correspond to this source actor. - let scripts = this.scripts.getScriptsBySourceActor(this); - for (let script of scripts) { - // Get all offsets for each line in the current script. This returns - // a map from line numbers fo a list of offsets for each line, - // implemented as a sparse array. - let lineToOffsetsMap = script.getAllOffsets(); - - // Iterate over each line, and add their list of offsets to the map - // from line numbers to entry points by forming a (script, offsets) - // pair, where script is the current script, and offsets is the list - // of offsets for the current line. - for (let line = 0; line < lineToOffsetsMap.length; ++line) { - let offsets = lineToOffsetsMap[line]; - if (offsets) { - let entryPoints = lineToEntryPointsMap[line]; - if (!entryPoints) { - // We dont have a list of entry points for the current line - // number yet, so create it and add it to the map. - entryPoints = []; - lineToEntryPointsMap[line] = entryPoints; - } - entryPoints.push({ script, offsets }); - } - } - } - - let { - originalSourceActor, - originalLine, - originalColumn - } = originalLocation; - - // Now that we have a map from line numbers to a list of entry points - // for each line, we can use it to perform breakpoint sliding. Start - // at the original line of the breakpoint actor, and keep incrementing - // it by one, until either we find a line that has at least one entry - // point, or we go past the last line in the map. - // - // Note that by computing the entire map up front, and implementing it - // as a sparse array, we can easily tell when we went past the last line - // in the map. - let actualLine = originalLine; - while (actualLine < lineToEntryPointsMap.length) { - let entryPoints = lineToEntryPointsMap[actualLine]; - if (entryPoints) { - setBreakpointForActorAtEntryPoints(actor, entryPoints); - break; - } - ++actualLine; - } - if (actualLine === lineToEntryPointsMap.length) { - // We went past the last line in the map, so breakpoint sliding - // failed. Keep the BreakpointActor in the BreakpointActorMap as a - // pending breakpoint, so we can try again whenever a new script is - // introduced. - return Promise.resolve(actor); - } - - // If the actual line on which the BreakpointActor was set differs from - // the original line that was requested, the BreakpointActor and the - // BreakpointActorMap need to be updated accordingly. - if (actualLine !== originalLine) { - let actualLocation = new OriginalLocation( - originalSourceActor, - actualLine - ); - let existingActor = this.breakpointActorMap.getActor(actualLocation); - if (existingActor) { - actor.onDelete(); - this.breakpointActorMap.deleteActor(originalLocation); - actor = existingActor; - } else { - this.breakpointActorMap.deleteActor(originalLocation); - actor.originalLocation = actualLocation; - this.breakpointActorMap.setActor(actualLocation, actor); - } - } - - return Promise.resolve(actor); - } else { - // TODO: Implement breakpoint sliding for column breakpoints - return Promise.resolve(actor); - } + return Promise.resolve(this._setBreakpointForActorAtLocation( + actor, + GeneratedLocation.fromOriginalLocation(actor.originalLocation) + )); } }, @@ -2940,79 +2841,8 @@ SourceActor.prototype = { * A GeneratedLocation representing the location in the generated * source for which the given BreakpointActor is to be set as a * breakpoint handler. - * - * @returns A Boolean that is true if the BreakpointActor was set as a - * breakpoint handler on at least one script, and false otherwise. */ _setBreakpointForActorAtLocation: function (actor, generatedLocation) { - let { generatedLine, generatedColumn } = generatedLocation; - - // Find all scripts that match the given source actor and line number. - let scripts = this.scripts.getScriptsBySourceActorAndLine( - this, - generatedLine - ).filter((script) => !actor.hasScript(script)); - - // Find all entry points that correspond to the given location. - let entryPoints = []; - if (generatedColumn === undefined) { - // This is a line breakpoint, so we are interested in all offsets - // that correspond to the given line number. - for (let script of scripts) { - let offsets = script.getLineOffsets(generatedLine); - if (offsets.length > 0) { - entryPoints.push({ script, offsets }); - } - } - } else { - // This is a column breakpoint, so we are interested in all column - // offsets that correspond to the given line *and* column number. - for (let script of scripts) { - let columnToOffsetMap = script.getAllColumnOffsets() - .filter(({ lineNumber }) => { - return lineNumber === generatedLine; - }); - for (let { columnNumber: column, offset } of columnToOffsetMap) { - // TODO: What we are actually interested in here is a range of - // columns, rather than a single one. - if (column == generatedColumn) { - entryPoints.push({ script, offsets: [offset] }); - } - } - } - } - - if (entryPoints.length === 0) { - return false; - } - setBreakpointForActorAtEntryPoints(actor, entryPoints); - return true; - }, - - /* - * Ensure the given BreakpointActor is set as breakpoint handler on all - * scripts that match the given location in the generated source. - * - * TODO: This method is bugged, because it performs breakpoint sliding on - * generated locations. Breakpoint sliding should be performed on original - * locations, because there is no guarantee that the next line in the - * generated source corresponds to the next line in an original source. - * - * The only place this method is still used is from setBreakpointForActor - * when called for a source mapped source. Once that code has been refactored, - * this method can be removed. - * - * @param BreakpointActor actor - * The BreakpointActor to be set as a breakpoint handler. - * @param GeneratedLocation generatedLocation - * A GeneratedLocation representing the location in the generated - * source for which the given BreakpointActor is to be set as a - * breakpoint handler. - * - * @returns A Boolean that is true if the BreakpointActor was set as a - * breakpoint handler on at least one script, and false otherwise. - */ - _setBreakpointForActorAtLocationWithSliding: function (actor, generatedLocation) { let originalLocation = actor.originalLocation; let { generatedLine, generatedColumn } = generatedLocation; @@ -3137,6 +2967,10 @@ SourceActor.prototype = { } actor.addScript(script, this.threadActor); } + + return { + actor: actor.actorID + }; }, /** diff --git a/toolkit/devtools/server/actors/utils/ScriptStore.js b/toolkit/devtools/server/actors/utils/ScriptStore.js index a1e5d8a1e70d..9d9a557057e6 100644 --- a/toolkit/devtools/server/actors/utils/ScriptStore.js +++ b/toolkit/devtools/server/actors/utils/ScriptStore.js @@ -79,12 +79,6 @@ ScriptStore.prototype = { return this._scripts.items; }, - getScriptsBySourceActor(sourceActor) { - return sourceActor.source ? - this.getScriptsBySource(sourceActor.source) : - this.getScriptsByURL(sourceActor._originalUrl); - }, - getScriptsBySourceActorAndLine(sourceActor, line) { return sourceActor.source ? this.getScriptsBySourceAndLine(sourceActor.source, line) : From 4191e9ac61511b0f992bf5e874fbc37bbd14b147 Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Fri, 20 Mar 2015 14:30:49 +0000 Subject: [PATCH 27/80] Bug 1141133 - Implement encrypt/decrypt of context information ready for Loop's context in conversation work. r=mikedeboer --- .../loop/content/shared/js/crypto.js | 238 +++++++++++ .../loop/content/shared/js/utils.js | 228 ++++++++++- .../loop/test/shared/crypto_test.js | 113 ++++++ .../components/loop/test/shared/index.html | 3 + .../components/loop/test/shared/utils_test.js | 47 +++ .../shared/vendor/chai-as-promised-4.3.0.js | 377 ++++++++++++++++++ 6 files changed, 1005 insertions(+), 1 deletion(-) create mode 100644 browser/components/loop/content/shared/js/crypto.js create mode 100644 browser/components/loop/test/shared/crypto_test.js create mode 100644 browser/components/loop/test/shared/vendor/chai-as-promised-4.3.0.js diff --git a/browser/components/loop/content/shared/js/crypto.js b/browser/components/loop/content/shared/js/crypto.js new file mode 100644 index 000000000000..fd2abb648541 --- /dev/null +++ b/browser/components/loop/content/shared/js/crypto.js @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global loop:true */ + +var loop = loop || {}; + +loop.crypto = (function() { + "use strict"; + + var ALGORITHM = "AES-GCM"; + var KEY_LENGTH = 128; + // We use JSON web key formats for the generated keys. + // https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41 + var KEY_FORMAT = "jwk"; + // This is the JSON web key type from the generateKey algorithm. + var KEY_TYPE = "oct"; + var ENCRYPT_TAG_LENGTH = 128; + var INITIALIZATION_VECTOR_LENGTH = 12; + + var sharedUtils = loop.shared.utils; + + /** + * Root object, by default set to window. + * @type {DOMWindow|Object} + */ + var rootObject = window; + + /** + * Sets a new root object. This is useful for testing crypto not supported as + * it allows us to fake crypto not being present. + * In beforeEach(), loop.crypto.setRootObject is used to + * substitute a fake window, and in afterEach(), the real window object is + * replaced. + * + * @param {Object} + */ + function setRootObject(obj) { + console.log("loop.crpyto.mixins: rootObject set to " + obj); + rootObject = obj; + } + + /** + * Determines if Web Crypto is supported by this browser. + * + * @return {Boolean} True if Web Crypto is supported + */ + function isSupported() { + return "crypto" in rootObject; + } + + /** + * Generates a random key using the Web Crypto libraries. + * + * @return {Promise} A promise which is rejected on failure, or resolved + * with a string that is in the JSON web key format. + */ + function generateKey() { + if (!isSupported()) { + throw new Error("Web Crypto is not supported"); + } + + return new Promise(function(resolve, reject) { + // First get a crypto key. + rootObject.crypto.subtle.generateKey({name: ALGORITHM, length: KEY_LENGTH }, + // `true` means that the key can be extracted from the CryptoKey object. + true, + // Usages for the key. + ["encrypt", "decrypt"] + ).then(function(cryptoKey) { + // Now extract the key in the JSON web key format. + return rootObject.crypto.subtle.exportKey(KEY_FORMAT, cryptoKey); + }).then(function(exportedKey) { + // Lastly resolve the promise with the new key. + resolve(exportedKey.k); + }).catch(function(error) { + reject(error); + }); + }); + } + + /** + * Encrypts an object using the specified key. + * + * @param {String} key The key to use for encryption. This should have + * been generated by generateKey. + * @param {String} data The string to be encrypted. + * + * @return {Promise} A promise which is rejected on failure, or resolved + * with a string that is the encrypted context. + */ + function encryptBytes(key, data) { + if (!isSupported()) { + throw new Error("Web Crypto is not supported"); + } + + var iv = new Uint8Array(INITIALIZATION_VECTOR_LENGTH); + + return new Promise(function(resolve, reject) { + // First import the key to a format we can use. + rootObject.crypto.subtle.importKey(KEY_FORMAT, + {k: key, kty: KEY_TYPE}, + ALGORITHM, + // If the key is extractable. + true, + // What we're using it for. + ["encrypt"] + ).then(function(cryptoKey) { + // Now we've got the cryptoKey, we can do the actual encryption. + + // First get the data into the format we need. + var dataBuffer = sharedUtils.strToUint8Array(data); + + // It is critically important to change the IV any time the + // encrypted information is updated. + rootObject.crypto.getRandomValues(iv); + + return rootObject.crypto.subtle.encrypt({ + name: ALGORITHM, + iv: iv, + tagLength: ENCRYPT_TAG_LENGTH + }, cryptoKey, + dataBuffer); + }).then(function(cipherText) { + // Join the initialization vector and context for returning. + var joinedData = _mergeIVandCipherText(iv, new DataView(cipherText)); + + // Now convert to a string and base-64 encode. + var encryptedData = loop.shared.utils.btoa(joinedData); + + resolve(encryptedData); + }).catch(function(error) { + reject(error); + }); + }); + } + + /** + * Decrypts an object using the specified key. + * + * @param {String} key The key to use for encryption. This should have + * been generated by generateKey. + * @param {String} encryptedData The encrypted context. + * @return {Promise} A promise which is rejected on failure, or resolved + * with a string that is the decrypted context. + */ + function decryptBytes(key, encryptedData) { + if (!isSupported()) { + throw new Error("Web Crypto is not supported"); + } + + return new Promise(function(resolve, reject) { + // First import the key to a format we can use. + rootObject.crypto.subtle.importKey(KEY_FORMAT, + {k: key, kty: KEY_TYPE}, + ALGORITHM, + // If the key is extractable. + true, + // What we're using it for. + ["decrypt"] + ).then(function(cryptoKey) { + // Now we've got the key, start the decryption. + var splitData = _splitIVandCipherText(encryptedData); + + return rootObject.crypto.subtle.decrypt({ + name: ALGORITHM, + iv: splitData.iv, + tagLength: ENCRYPT_TAG_LENGTH + }, cryptoKey, splitData.cipherText); + }).then(function(plainText) { + // Now we just turn it back into a string and then an object. + resolve(sharedUtils.Uint8ArrayToStr(new Uint8Array(plainText))); + }).catch(function(error) { + reject(error); + }); + }); + } + + /** + * Appends the cipher text to the end of the initialization vector and + * returns the result. + * + * @param {Uint8Array} ivArray The array of initialization vector values. + * @param {DataView} cipherTextDataView The cipherText in data view format. + * @return {Uint8Array} An array of the IV and cipherText. + */ + function _mergeIVandCipherText(ivArray, cipherTextDataView) { + // First we translate the data view to an array so we can get + // the length. + var cipherText = new Uint8Array(cipherTextDataView.buffer); + var cipherTextLength = cipherText.length; + + var joinedContext = new Uint8Array(INITIALIZATION_VECTOR_LENGTH + cipherTextLength); + + var i; + for (i = 0; i < INITIALIZATION_VECTOR_LENGTH; i++) { + joinedContext[i] = ivArray[i]; + } + + for (i = 0; i < cipherTextLength; i++) { + joinedContext[i + INITIALIZATION_VECTOR_LENGTH] = cipherText[i]; + } + + return joinedContext; + } + + /** + * Takes the IV from the start of the passed in array and separates + * out the cipher text. + * + * @param {String} encryptedData Encrypted data in base64 format. + * @return {Object} An object consisting of two items: iv and cipherText, + * both are Uint8Arrays. + */ + function _splitIVandCipherText(encryptedData) { + // Convert into byte arrays. + var encryptedDataArray = loop.shared.utils.atob(encryptedData); + + // Now split out the initialization vector and the cipherText. + var iv = encryptedDataArray.slice(0, INITIALIZATION_VECTOR_LENGTH); + var cipherText = encryptedDataArray.slice(INITIALIZATION_VECTOR_LENGTH, + encryptedDataArray.length); + + return { + iv: iv, + cipherText: cipherText + }; + } + + return { + decryptBytes: decryptBytes, + encryptBytes: encryptBytes, + generateKey: generateKey, + isSupported: isSupported, + setRootObject: setRootObject + }; +})(); diff --git a/browser/components/loop/content/shared/js/utils.js b/browser/components/loop/content/shared/js/utils.js index 7a49e2ea7cbe..556dac2095c0 100644 --- a/browser/components/loop/content/shared/js/utils.js +++ b/browser/components/loop/content/shared/js/utils.js @@ -168,6 +168,228 @@ loop.shared.utils = (function(mozL10n) { ); } + /** + * Binary-compatible Base64 decoding. + * + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + * + * @param {String} base64str The string to decode. + * @return {Uint8Array} The decoded result in array format. + */ + function atob(base64str) { + var strippedEncoding = base64str.replace(/[^A-Za-z0-9\+\/]/g, ""); + var inLength = strippedEncoding.length; + var outLength = inLength * 3 + 1 >> 2; + var result = new Uint8Array(outLength); + + var mod3; + var mod4; + var uint24 = 0; + var outIndex = 0; + + for (var inIndex = 0; inIndex < inLength; inIndex++) { + mod4 = inIndex & 3; + uint24 |= _b64ToUint6(strippedEncoding.charCodeAt(inIndex)) << 6 * (3 - mod4); + + if (mod4 === 3 || inLength - inIndex === 1) { + for (mod3 = 0; mod3 < 3 && outIndex < outLength; mod3++, outIndex++) { + result[outIndex] = uint24 >>> (16 >>> mod3 & 24) & 255; + } + uint24 = 0; + } + } + + return result; + } + + /** + * Binary-compatible Base64 encoding. + * + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + * + * @param {Uint8Array} bytes The data to encode. + * @return {String} The base64 encoded string. + */ + function btoa(bytes) { + var mod3 = 2; + var result = ""; + var length = bytes.length; + var uint24 = 0; + + for (var index = 0; index < length; index++) { + mod3 = index % 3; + if (index > 0 && (index * 4 / 3) % 76 === 0) { + result += "\r\n"; + } + uint24 |= bytes[index] << (16 >>> mod3 & 24); + if (mod3 === 2 || length - index === 1) { + result += String.fromCharCode(_uint6ToB64(uint24 >>> 18 & 63), + _uint6ToB64(uint24 >>> 12 & 63), + _uint6ToB64(uint24 >>> 6 & 63), + _uint6ToB64(uint24 & 63)); + uint24 = 0; + } + } + + return result.substr(0, result.length - 2 + mod3) + + (mod3 === 2 ? "" : mod3 === 1 ? "=" : "=="); + } + + /** + * Utility function to decode a base64 character into an integer. + * + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + * + * @param {Number} chr The character code to decode. + * @return {Number} The decoded value. + */ + function _b64ToUint6 (chr) { + return chr > 64 && chr < 91 ? chr - 65 : + chr > 96 && chr < 123 ? chr - 71 : + chr > 47 && chr < 58 ? chr + 4 : + chr === 43 ? 62 : + chr === 47 ? 63 : 0; + } + + /** + * Utility function to encode an integer into a base64 character code. + * + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + * + * @param {Number} uint6 The number to encode. + * @return {Number} The encoded value. + */ + function _uint6ToB64 (uint6) { + return uint6 < 26 ? uint6 + 65 : + uint6 < 52 ? uint6 + 71 : + uint6 < 62 ? uint6 - 4 : + uint6 === 62 ? 43 : + uint6 === 63 ? 47 : 65; + } + + /** + * Utility function to convert a string into a uint8 array. + * + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + * + * @param {String} inString The string to convert. + * @return {Uint8Array} The converted string in array format. + */ + function strToUint8Array(inString) { + var inLength = inString.length; + var arrayLength = 0; + var chr; + + // Mapping. + for (var mapIndex = 0; mapIndex < inLength; mapIndex++) { + chr = inString.charCodeAt(mapIndex); + arrayLength += chr < 0x80 ? 1 : + chr < 0x800 ? 2 : + chr < 0x10000 ? 3 : + chr < 0x200000 ? 4 : + chr < 0x4000000 ? 5 : 6; + } + + var result = new Uint8Array(arrayLength); + var index = 0; + + // Transcription. + for (var chrIndex = 0; index < arrayLength; chrIndex++) { + chr = inString.charCodeAt(chrIndex); + if (chr < 128) { + // One byte. + result[index++] = chr; + } else if (chr < 0x800) { + // Two bytes. + result[index++] = 192 + (chr >>> 6); + result[index++] = 128 + (chr & 63); + } else if (chr < 0x10000) { + // Three bytes. + result[index++] = 224 + (chr >>> 12); + result[index++] = 128 + (chr >>> 6 & 63); + result[index++] = 128 + (chr & 63); + } else if (chr < 0x200000) { + // Four bytes. + result[index++] = 240 + (chr >>> 18); + result[index++] = 128 + (chr >>> 12 & 63); + result[index++] = 128 + (chr >>> 6 & 63); + result[index++] = 128 + (chr & 63); + } else if (chr < 0x4000000) { + // Five bytes. + result[index++] = 248 + (chr >>> 24); + result[index++] = 128 + (chr >>> 18 & 63); + result[index++] = 128 + (chr >>> 12 & 63); + result[index++] = 128 + (chr >>> 6 & 63); + result[index++] = 128 + (chr & 63); + } else { // if (chr <= 0x7fffffff) + // Six bytes. + result[index++] = 252 + (chr >>> 30); + result[index++] = 128 + (chr >>> 24 & 63); + result[index++] = 128 + (chr >>> 18 & 63); + result[index++] = 128 + (chr >>> 12 & 63); + result[index++] = 128 + (chr >>> 6 & 63); + result[index++] = 128 + (chr & 63); + } + } + + return result; + } + + /** + * Utility function to change a uint8 based integer array to a string. + * + * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + * + * @param {Uint8Array} arrayBytes Array to convert. + * @param {String} The array as a string. + */ + function Uint8ArrayToStr(arrayBytes) { + var result = ""; + var length = arrayBytes.length; + var part; + + for (var index = 0; index < length; index++) { + part = arrayBytes[index]; + result += String.fromCharCode( + part > 251 && part < 254 && index + 5 < length ? + // Six bytes. + // (part - 252 << 30) may be not so safe in ECMAScript! So...: + (part - 252) * 1073741824 + + (arrayBytes[++index] - 128 << 24) + + (arrayBytes[++index] - 128 << 18) + + (arrayBytes[++index] - 128 << 12) + + (arrayBytes[++index] - 128 << 6) + + arrayBytes[++index] - 128 : + part > 247 && part < 252 && index + 4 < length ? + // Five bytes. + (part - 248 << 24) + + (arrayBytes[++index] - 128 << 18) + + (arrayBytes[++index] - 128 << 12) + + (arrayBytes[++index] - 128 << 6) + + arrayBytes[++index] - 128 : + part > 239 && part < 248 && index + 3 < length ? + // Four bytes. + (part - 240 << 18) + + (arrayBytes[++index] - 128 << 12) + + (arrayBytes[++index] - 128 << 6) + + arrayBytes[++index] - 128 : + part > 223 && part < 240 && index + 2 < length ? + // Three bytes. + (part - 224 << 12) + + (arrayBytes[++index] - 128 << 6) + + arrayBytes[++index] - 128 : + part > 191 && part < 224 && index + 1 < length ? + // Two bytes. + (part - 192 << 6) + + arrayBytes[++index] - 128 : + // One byte. + part + ); + } + + return result; + } + return { CALL_TYPES: CALL_TYPES, FAILURE_DETAILS: FAILURE_DETAILS, @@ -183,6 +405,10 @@ loop.shared.utils = (function(mozL10n) { isFirefoxOS: isFirefoxOS, isOpera: isOpera, getUnsupportedPlatform: getUnsupportedPlatform, - locationData: locationData + locationData: locationData, + atob: atob, + btoa: btoa, + strToUint8Array: strToUint8Array, + Uint8ArrayToStr: Uint8ArrayToStr }; })(document.mozL10n || navigator.mozL10n); diff --git a/browser/components/loop/test/shared/crypto_test.js b/browser/components/loop/test/shared/crypto_test.js new file mode 100644 index 000000000000..54329e936469 --- /dev/null +++ b/browser/components/loop/test/shared/crypto_test.js @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global loop, sinon */ + +var expect = chai.expect; + +describe("loop.crypto", function() { + "use strict"; + + var sandbox, oldCrypto; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + loop.crypto.setRootObject(window); + }); + + describe("#isSupported", function() { + it("should return true by default", function() { + expect(loop.crypto.isSupported()).eql(true); + }); + + it("should return false if crypto isn't supported", function() { + loop.crypto.setRootObject({}); + + expect(loop.crypto.isSupported()).eql(false); + }); + }); + + describe("#generateKey", function() { + it("should throw if web crypto is not available", function() { + loop.crypto.setRootObject({}); + + expect(function() { + loop.crypto.generateKey(); + }).to.Throw(/not supported/); + }); + + it("should generate a key", function() { + // The key is a random string, so we can't really test much else. + return expect(loop.crypto.generateKey()).to.eventually.be.a("string"); + }); + }); + + describe("#encryptBytes", function() { + it("should throw if web crypto is not available", function() { + loop.crypto.setRootObject({}); + + expect(function() { + loop.crypto.encryptBytes(); + }).to.Throw(/not supported/); + }); + + it("should encrypt an object with a specific key", function() { + return expect(loop.crypto.encryptBytes("Wt2-bZKeHO2wnaq00ZM6Nw", + JSON.stringify({test: true}))).to.eventually.be.a("string"); + }); + }); + + describe("#decryptBytes", function() { + it("should throw if web crypto is not available", function() { + loop.crypto.setRootObject({}); + + expect(function() { + loop.crypto.decryptBytes(); + }).to.Throw(/not supported/); + }); + + it("should decypt an object via a specific key", function() { + var key = "Wt2-bZKeHO2wnaq00ZM6Nw"; + var encryptedContext = "XvN9FDEm/GtE/5Bx5ezpn7JVDeZrtwOJy2CBjTGgJ4L33HhHOqEW+5k="; + + return expect(loop.crypto.decryptBytes(key, encryptedContext)).to.eventually.eql(JSON.stringify({test: true})); + }); + + it("should fail if the key didn't work", function() { + var bad = "Bad-bZKeHO2wnaq00ZM6Nw"; + var encryptedContext = "TGZaAE3mqsBFK0GfheZXXDCaRKXJmIKJ8WzF0KBEl4Aldzf3iYlAsLQdA8XSXXvtJR2UYz+f"; + + return expect(loop.crypto.decryptBytes(bad, encryptedContext)).to.be.rejected; + }); + }); + + describe("Full cycle", function() { + it("should be able to encrypt and decypt in a full cycle", function(done) { + var context = JSON.stringify({ + contextObject: true, + UTF8String: "对话" + }); + + return loop.crypto.generateKey().then(function (key) { + loop.crypto.encryptBytes(key, context).then(function(encryptedContext) { + loop.crypto.decryptBytes(key, encryptedContext).then(function(decryptedContext) { + expect(decryptedContext).eql(context); + done(); + }).catch(function(error) { + done(error); + }); + }).catch(function(error) { + done(error); + }); + }).catch(function(error) { + done(error); + }); + }); + }); + +}); diff --git a/browser/components/loop/test/shared/index.html b/browser/components/loop/test/shared/index.html index 20e4da4d1199..467ff2a82950 100644 --- a/browser/components/loop/test/shared/index.html +++ b/browser/components/loop/test/shared/index.html @@ -31,6 +31,7 @@ + + @@ -62,6 +64,7 @@ + diff --git a/browser/components/loop/test/shared/utils_test.js b/browser/components/loop/test/shared/utils_test.js index 8b98835b77b2..179e2f146f61 100644 --- a/browser/components/loop/test/shared/utils_test.js +++ b/browser/components/loop/test/shared/utils_test.js @@ -171,4 +171,51 @@ describe("loop.shared.utils", function() { "subject", "body", "fake@invalid.tld"); }); }); + + describe("#btoa", function() { + it("should encode a basic base64 string", function() { + var result = sharedUtils.btoa(sharedUtils.strToUint8Array("crypto is great")); + + expect(result).eql("Y3J5cHRvIGlzIGdyZWF0"); + }); + + it("should pad encoded base64 strings", function() { + var result = sharedUtils.btoa(sharedUtils.strToUint8Array("crypto is grea")); + + expect(result).eql("Y3J5cHRvIGlzIGdyZWE="); + + result = sharedUtils.btoa(sharedUtils.strToUint8Array("crypto is gre")); + + expect(result).eql("Y3J5cHRvIGlzIGdyZQ=="); + }); + + it("should encode a non-unicode base64 string", function() { + var result = sharedUtils.btoa(sharedUtils.strToUint8Array("\uFDFD")); + expect(result).eql("77e9"); + }); + }); + + describe("#atob", function() { + it("should decode a basic base64 string", function() { + var result = sharedUtils.Uint8ArrayToStr(sharedUtils.atob("Y3J5cHRvIGlzIGdyZWF0")); + + expect(result).eql("crypto is great"); + }); + + it("should decode a padded base64 string", function() { + var result = sharedUtils.Uint8ArrayToStr(sharedUtils.atob("Y3J5cHRvIGlzIGdyZWE=")); + + expect(result).eql("crypto is grea"); + + result = sharedUtils.Uint8ArrayToStr(sharedUtils.atob("Y3J5cHRvIGlzIGdyZQ==")); + + expect(result).eql("crypto is gre"); + }); + + it("should decode a base64 string that has unicode characters", function() { + var result = sharedUtils.Uint8ArrayToStr(sharedUtils.atob("77e9")); + + expect(result).eql("\uFDFD"); + }); + }); }); diff --git a/browser/components/loop/test/shared/vendor/chai-as-promised-4.3.0.js b/browser/components/loop/test/shared/vendor/chai-as-promised-4.3.0.js new file mode 100644 index 000000000000..f37c0ccb8dc7 --- /dev/null +++ b/browser/components/loop/test/shared/vendor/chai-as-promised-4.3.0.js @@ -0,0 +1,377 @@ +(function () { + "use strict"; + + // Module systems magic dance. + + /* istanbul ignore else */ + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") { + // NodeJS + module.exports = chaiAsPromised; + } else if (typeof define === "function" && define.amd) { + // AMD + define(function () { + return chaiAsPromised; + }); + } else { + /*global self: false */ + + // Other environment (usually