From 10b22038219b4fe4dd6e2d6824d5bfd88d62eb0d Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Wed, 25 Jan 2012 23:44:27 +0100 Subject: [PATCH] Bug 455553 - Part 4 - Shared Module; r=blair,mak,dietrich --- browser/modules/Makefile.in | 1 + browser/modules/NewTabUtils.jsm | 551 ++++++++++++++++++++++++++++++++ toolkit/content/Services.jsm | 2 + 3 files changed, 554 insertions(+) create mode 100644 browser/modules/NewTabUtils.jsm diff --git a/browser/modules/Makefile.in b/browser/modules/Makefile.in index ace3c4936ad4..721d812c6e00 100644 --- a/browser/modules/Makefile.in +++ b/browser/modules/Makefile.in @@ -51,6 +51,7 @@ endif EXTRA_JS_MODULES = \ openLocationLastURL.jsm \ NetworkPrioritizer.jsm \ + NewTabUtils.jsm \ offlineAppCache.jsm \ TelemetryTimestamps.jsm \ $(NULL) diff --git a/browser/modules/NewTabUtils.jsm b/browser/modules/NewTabUtils.jsm new file mode 100644 index 000000000000..9b3ea2c46b62 --- /dev/null +++ b/browser/modules/NewTabUtils.jsm @@ -0,0 +1,551 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let EXPORTED_SYMBOLS = ["NewTabUtils"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gPrivateBrowsing", + "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService"); + +XPCOMUtils.defineLazyModuleGetter(this, "Dict", "resource://gre/modules/Dict.jsm"); + +// The preference that tells whether this feature is enabled. +const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled"; + +// The maximum number of results we want to retrieve from history. +const HISTORY_RESULTS_LIMIT = 100; + +/** + * Singleton that provides storage functionality. + */ +let Storage = { + /** + * The dom storage instance used to persist data belonging to the New Tab Page. + */ + get domStorage() { + let uri = Services.io.newURI("about:newtab", null, null); + let principal = Services.scriptSecurityManager.getCodebasePrincipal(uri); + + let sm = Services.domStorageManager; + let storage = sm.getLocalStorageForPrincipal(principal, ""); + + // Cache this value, overwrite the getter. + let descriptor = {value: storage, enumerable: true}; + Object.defineProperty(this, "domStorage", descriptor); + + return storage; + }, + + /** + * The current storage used to persist New Tab Page data. If we're currently + * in private browsing mode this will return a PrivateBrowsingStorage + * instance. + */ + get currentStorage() { + let storage = this.domStorage; + + // Check if we're starting in private browsing mode. + if (gPrivateBrowsing.privateBrowsingEnabled) + storage = new PrivateBrowsingStorage(storage); + + // Register an observer to listen for private browsing mode changes. + Services.obs.addObserver(this, "private-browsing", true); + + // Cache this value, overwrite the getter. + let descriptor = {value: storage, enumerable: true, writable: true}; + Object.defineProperty(this, "currentStorage", descriptor); + + return storage; + }, + + /** + * Gets the value for a given key from the storage. + * @param aKey The storage key (a string). + * @param aDefault A default value if the key doesn't exist. + * @return The value for the given key. + */ + get: function Storage_get(aKey, aDefault) { + let value; + + try { + value = JSON.parse(this.currentStorage.getItem(aKey)); + } catch (e) {} + + return value || aDefault; + }, + + /** + * Sets the storage value for a given key. + * @param aKey The storage key (a string). + * @param aValue The value to set. + */ + set: function Storage_set(aKey, aValue) { + this.currentStorage.setItem(aKey, JSON.stringify(aValue)); + }, + + /** + * Clears the storage and removes all values. + */ + clear: function Storage_clear() { + this.currentStorage.clear(); + }, + + /** + * Implements the nsIObserver interface to get notified about private + * browsing mode changes. + */ + observe: function Storage_observe(aSubject, aTopic, aData) { + if (aData == "enter") { + // When switching to private browsing mode we keep the current state + // of the grid and provide a volatile storage for it that is + // discarded upon leaving private browsing. + this.currentStorage = new PrivateBrowsingStorage(this.domStorage); + } else { + // Reset to normal DOM storage. + this.currentStorage = this.domStorage; + + // When switching back from private browsing we need to reset the + // grid and re-read its values from the underlying storage. We don't + // want any data from private browsing to show up. + PinnedLinks.resetCache(); + BlockedLinks.resetCache(); + + Pages.update(); + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]) +}; + +/** + * This class implements a temporary storage used while the user is in private + * browsing mode. It is discarded when leaving pb mode. + */ +function PrivateBrowsingStorage(aStorage) { + this._data = new Dict(); + + for (let i = 0; i < aStorage.length; i++) { + let key = aStorage.key(i); + this._data.set(key, aStorage.getItem(key)); + } +} + +PrivateBrowsingStorage.prototype = { + /** + * The data store. + */ + _data: null, + + /** + * Gets the value for a given key from the storage. + * @param aKey The storage key. + * @param aDefault A default value if the key doesn't exist. + * @return The value for the given key. + */ + getItem: function PrivateBrowsingStorage_getItem(aKey) { + return this._data.get(aKey); + }, + + /** + * Sets the storage value for a given key. + * @param aKey The storage key. + * @param aValue The value to set. + */ + setItem: function PrivateBrowsingStorage_setItem(aKey, aValue) { + this._data.set(aKey, aValue); + }, + + /** + * Clears the storage and removes all values. + */ + clear: function PrivateBrowsingStorage_clear() { + this._data.listkeys().forEach(function (akey) { + this._data.del(aKey); + }, this); + } +}; + +/** + * Singleton that serves as a registry for all open 'New Tab Page's. + */ +let AllPages = { + /** + * The array containing all active pages. + */ + _pages: [], + + /** + * Tells whether we already added a preference observer. + */ + _observing: false, + + /** + * Cached value that tells whether the New Tab Page feature is enabled. + */ + _enabled: null, + + /** + * Adds a page to the internal list of pages. + * @param aPage The page to register. + */ + register: function AllPages_register(aPage) { + this._pages.push(aPage); + + // Add the preference observer if we haven't already. + if (!this._observing) { + this._observing = true; + Services.prefs.addObserver(PREF_NEWTAB_ENABLED, this, true); + } + }, + + /** + * Removes a page from the internal list of pages. + * @param aPage The page to unregister. + */ + unregister: function AllPages_unregister(aPage) { + let index = this._pages.indexOf(aPage); + this._pages.splice(index, 1); + }, + + /** + * Returns whether the 'New Tab Page' is enabled. + */ + get enabled() { + if (this._enabled === null) + this._enabled = Services.prefs.getBoolPref(PREF_NEWTAB_ENABLED); + + return this._enabled; + }, + + /** + * Enables or disables the 'New Tab Page' feature. + */ + set enabled(aEnabled) { + if (this.enabled != aEnabled) + Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, !!aEnabled); + }, + + /** + * Updates all currently active pages but the given one. + * @param aExceptPage The page to exclude from updating. + */ + update: function AllPages_update(aExceptPage) { + this._pages.forEach(function (aPage) { + if (aExceptPage != aPage) + aPage.update(); + }); + }, + + /** + * Implements the nsIObserver interface to get notified when the preference + * value changes. + */ + observe: function AllPages_observe() { + // Clear the cached value. + this._enabled = null; + + let args = Array.slice(arguments); + + this._pages.forEach(function (aPage) { + aPage.observe.apply(aPage, args); + }, this); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]) +}; + +/** + * Singleton that keeps track of all pinned links and their positions in the + * grid. + */ +let PinnedLinks = { + /** + * The cached list of pinned links. + */ + _links: null, + + /** + * The array of pinned links. + */ + get links() { + if (!this._links) + this._links = Storage.get("pinnedLinks", []); + + return this._links; + }, + + /** + * Pins a link at the given position. + * @param aLink The link to pin. + * @param aIndex The grid index to pin the cell at. + */ + pin: function PinnedLinks_pin(aLink, aIndex) { + // Clear the link's old position, if any. + this.unpin(aLink); + + this.links[aIndex] = aLink; + Storage.set("pinnedLinks", this.links); + }, + + /** + * Unpins a given link. + * @param aLink The link to unpin. + */ + unpin: function PinnedLinks_unpin(aLink) { + let index = this._indexOfLink(aLink); + if (index != -1) { + this.links[index] = null; + Storage.set("pinnedLinks", this.links); + } + }, + + /** + * Checks whether a given link is pinned. + * @params aLink The link to check. + * @return whether The link is pinned. + */ + isPinned: function PinnedLinks_isPinned(aLink) { + return this._indexOfLink(aLink) != -1; + }, + + /** + * Resets the links cache. + */ + resetCache: function PinnedLinks_resetCache() { + this._links = null; + }, + + /** + * Finds the index of a given link in the list of pinned links. + * @param aLink The link to find an index for. + * @return The link's index. + */ + _indexOfLink: function PinnedLinks_indexOfLink(aLink) { + for (let i = 0; i < this.links.length; i++) { + let link = this.links[i]; + if (link && link.url == aLink.url) + return i; + } + + // The given link is unpinned. + return -1; + } +}; + +/** + * Singleton that keeps track of all blocked links in the grid. + */ +let BlockedLinks = { + /** + * The cached list of blocked links. + */ + _links: null, + + /** + * The list of blocked links. + */ + get links() { + if (!this._links) + this._links = Storage.get("blockedLinks", {}); + + return this._links; + }, + + /** + * Blocks a given link. + * @param aLink The link to block. + */ + block: function BlockedLinks_block(aLink) { + this.links[aLink.url] = 1; + + // Make sure we unpin blocked links. + PinnedLinks.unpin(aLink); + + Storage.set("blockedLinks", this.links); + }, + + /** + * Returns whether a given link is blocked. + * @param aLink The link to check. + */ + isBlocked: function BlockedLinks_isBlocked(aLink) { + return (aLink.url in this.links); + }, + + /** + * Checks whether the list of blocked links is empty. + * @return Whether the list is empty. + */ + isEmpty: function BlockedLinks_isEmpty() { + return Object.keys(this.links).length == 0; + }, + + /** + * Resets the links cache. + */ + resetCache: function BlockedLinks_resetCache() { + this._links = null; + } +}; + +/** + * Singleton that serves as the default link provider for the grid. It queries + * the history to retrieve the most frequently visited sites. + */ +let PlacesProvider = { + /** + * Gets the current set of links delivered by this provider. + * @param aCallback The function that the array of links is passed to. + */ + getLinks: function PlacesProvider_getLinks(aCallback) { + let options = PlacesUtils.history.getNewQueryOptions(); + options.maxResults = HISTORY_RESULTS_LIMIT; + + // Sort by frecency, descending. + options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING + + // We don't want source redirects for this query. + options.redirectsMode = Ci.nsINavHistoryQueryOptions.REDIRECTS_MODE_TARGET; + + let links = []; + + let callback = { + handleResult: function (aResultSet) { + let row; + + while (row = aResultSet.getNextRow()) { + let url = row.getResultByIndex(1); + let title = row.getResultByIndex(2); + links.push({url: url, title: title}); + } + }, + + handleError: function (aError) { + // Should we somehow handle this error? + aCallback([]); + }, + + handleCompletion: function (aReason) { + aCallback(links); + } + }; + + // Execute the query. + let query = PlacesUtils.history.getNewQuery(); + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase); + db.asyncExecuteLegacyQueries([query], 1, options, callback); + } +}; + +/** + * Singleton that provides access to all links contained in the grid (including + * the ones that don't fit on the grid). A link is a plain object with title + * and url properties. + * + * Example: + * + * {url: "http://www.mozilla.org/", title: "Mozilla"} + */ +let Links = { + /** + * The links cache. + */ + _links: [], + + /** + * The default provider for links. + */ + _provider: PlacesProvider, + + /** + * Populates the cache with fresh links from the current provider. + * @param aCallback The callback to call when finished (optional). + */ + populateCache: function Links_populateCache(aCallback) { + let self = this; + + this._provider.getLinks(function (aLinks) { + self._links = aLinks; + aCallback && aCallback(); + }); + }, + + /** + * Gets the current set of links contained in the grid. + * @return The links in the grid. + */ + getLinks: function Links_getLinks() { + let pinnedLinks = Array.slice(PinnedLinks.links); + + // Filter blocked and pinned links. + let links = this._links.filter(function (link) { + return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link); + }); + + // Try to fill the gaps between pinned links. + for (let i = 0; i < pinnedLinks.length && links.length; i++) + if (!pinnedLinks[i]) + pinnedLinks[i] = links.shift(); + + // Append the remaining links if any. + if (links.length) + pinnedLinks = pinnedLinks.concat(links); + + return pinnedLinks; + }, + + /** + * Resets the links cache. + */ + resetCache: function Links_resetCache() { + this._links = []; + } +}; + +/** + * Singleton that provides the public API of this JSM. + */ +let NewTabUtils = { + _initialized: false, + + /** + * Initializes and prepares the NewTabUtils module. + */ + init: function NewTabUtils_init() { + if (!this._initialized) { + // Prefetch the links. + Links.populateCache(); + + this._initialized = true; + } + }, + + /** + * Resets the NewTabUtils module, its links and its storage. + */ + reset: function NewTabUtils_reset() { + Storage.clear(); + Links.resetCache(); + PinnedLinks.resetCache(); + BlockedLinks.resetCache(); + }, + + allPages: AllPages, + links: Links, + pinnedLinks: PinnedLinks, + blockedLinks: BlockedLinks +}; diff --git a/toolkit/content/Services.jsm b/toolkit/content/Services.jsm index 6bef52e71ebf..387b6a7ca30f 100644 --- a/toolkit/content/Services.jsm +++ b/toolkit/content/Services.jsm @@ -77,10 +77,12 @@ let initTable = [ ["perms", "@mozilla.org/permissionmanager;1", "nsIPermissionManager"], ["prompt", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"], ["scriptloader", "@mozilla.org/moz/jssubscript-loader;1", "mozIJSSubScriptLoader"], + ["scriptSecurityManager", "@mozilla.org/scriptsecuritymanager;1", "nsIScriptSecurityManager"], #ifdef MOZ_TOOLKIT_SEARCH ["search", "@mozilla.org/browser/search-service;1", "nsIBrowserSearchService"], #endif ["storage", "@mozilla.org/storage/service;1", "mozIStorageService"], + ["domStorageManager", "@mozilla.org/dom/storagemanager;1", "nsIDOMStorageManager"], ["strings", "@mozilla.org/intl/stringbundle;1", "nsIStringBundleService"], ["telemetry", "@mozilla.org/base/telemetry;1", "nsITelemetry"], ["tm", "@mozilla.org/thread-manager;1", "nsIThreadManager"],