Bug 455553 - Part 4 - Shared Module; r=blair,mak,dietrich
☠☠ backed out by e3a59e6affd3 ☠ ☠
authorTim Taubert <tim.taubert@gmx.de>
Fri, 20 Jan 2012 02:43:20 +0100
changeset 86168 34e078df2ed6616b203661efcaff990990e2e5a9
parent 86167 6fd9a7eb3b011e07b7b6d093095fc5dd85a90c52
child 86169 c389d719d4ec42911c0ae0ddaf4f770053dcb6ab
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersblair, mak, dietrich
bugs455553
milestone12.0a1
Bug 455553 - Part 4 - Shared Module; r=blair,mak,dietrich
browser/modules/Makefile.in
browser/modules/NewTabUtils.jsm
toolkit/content/Services.jsm
--- a/browser/modules/Makefile.in
+++ b/browser/modules/Makefile.in
@@ -46,16 +46,17 @@ include $(topsrcdir)/config/config.mk
 
 ifdef ENABLE_TESTS
 DIRS += test
 endif
 
 EXTRA_JS_MODULES = \
 	openLocationLastURL.jsm \
 	NetworkPrioritizer.jsm \
+	NewTabUtils.jsm \
 	offlineAppCache.jsm \
 	PageThumbs.jsm \
 	$(NULL)
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),windows) 
 EXTRA_JS_MODULES += \
 	WindowsPreviewPerTab.jsm \
 	WindowsJumpLists.jsm \
new file mode 100644
--- /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
+};
--- a/toolkit/content/Services.jsm
+++ b/toolkit/content/Services.jsm
@@ -72,20 +72,22 @@ let initTable = [
   ["eTLD", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"],
   ["io", "@mozilla.org/network/io-service;1", "nsIIOService2"],
   ["locale", "@mozilla.org/intl/nslocaleservice;1", "nsILocaleService"],
   ["logins", "@mozilla.org/login-manager;1", "nsILoginManager"],
   ["obs", "@mozilla.org/observer-service;1", "nsIObserverService"],
   ["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"],
   ["urlFormatter", "@mozilla.org/toolkit/URLFormatterService;1", "nsIURLFormatter"],
   ["vc", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator"],
   ["wm", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"],
   ["ww", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"],
   ["startup", "@mozilla.org/toolkit/app-startup;1", "nsIAppStartup"],