[Pkg-mozext-commits] [tabmixplus] 14/48: Use embedded WebExtension to save our preferences and sessions data to local storage

David Prévot taffit at moszumanska.debian.org
Sun Aug 20 03:14:35 UTC 2017


This is an automated email from the git hooks/post-receive script.

taffit pushed a commit to branch master
in repository tabmixplus.

commit e28a99eefadfad3e335ee429bd95b02767469220
Author: onemen <tabmix.onemen at gmail.com>
Date:   Fri Jul 21 10:16:27 2017 +0300

    Use embedded WebExtension to save our preferences and sessions data to local storage
---
 chrome/content/session/session.js           |   9 +
 modules/TabmixSvc.jsm                       |   9 +-
 modules/extensions/EmbeddedWebExtension.jsm | 422 ++++++++++++++++++++++++++++
 webextension/.eslintrc.js                   |  11 +
 webextension/_locales/en/messages.json      |   3 +
 webextension/background.js                  |  77 +++++
 webextension/manifest.json                  |  15 +
 7 files changed, 545 insertions(+), 1 deletion(-)

diff --git a/chrome/content/session/session.js b/chrome/content/session/session.js
index 4cfc633..ea23926 100644
--- a/chrome/content/session/session.js
+++ b/chrome/content/session/session.js
@@ -310,6 +310,9 @@ TabmixSessionManager = {
     XPCOMUtils.defineLazyModuleGetter(this, "TabmixGroupsMigrator",
       "resource://tabmixplus/TabGroupsMigrator.jsm");
 
+    XPCOMUtils.defineLazyModuleGetter(this, "EmbeddedWebExtension",
+      "resource://tabmixplus/extensions/EmbeddedWebExtension.jsm");
+
     // just in case Tabmix.tablib isn't init yet
     // when Webmail Notifier extension installed and user have master password
     // we can get here before the browser window is loaded
@@ -1248,6 +1251,12 @@ TabmixSessionManager = {
       return;
 
     try {
+      this.EmbeddedWebExtension.saveSessionsData(this.sessionShutDown);
+    } catch (ex) {
+      Tabmix.reportError(ex);
+    }
+
+    try {
       this.DATASource.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush();
       this._lastSaveTime = Date.now();
     } catch (ex) {
diff --git a/modules/TabmixSvc.jsm b/modules/TabmixSvc.jsm
index 8968f7e..4002cb6 100644
--- a/modules/TabmixSvc.jsm
+++ b/modules/TabmixSvc.jsm
@@ -186,7 +186,6 @@ this.TabmixSvc = {
 
       Services.obs.addObserver(this, "quit-application", true);
 
-      // eslint-disable-next-line tabmix/import-globals
       Cu.import("resource://tabmixplus/DownloadLastDir.jsm", {});
 
       TabmixPlacesUtils.init(aWindow);
@@ -200,6 +199,14 @@ this.TabmixSvc = {
       tmp.DynamicRules.init(aWindow);
 
       Cu.import("resource://tabmixplus/TabRestoreQueue.jsm", {});
+
+      if (TabmixSvc.version(510)) {
+        try {
+          Cu.import("resource://tabmixplus/extensions/EmbeddedWebExtension.jsm", {});
+        } catch (ex) {
+          TabmixSvc.console.reportError(ex);
+        }
+      }
     },
 
     addMissingPrefs() {
diff --git a/modules/extensions/EmbeddedWebExtension.jsm b/modules/extensions/EmbeddedWebExtension.jsm
new file mode 100644
index 0000000..bb2936a
--- /dev/null
+++ b/modules/extensions/EmbeddedWebExtension.jsm
@@ -0,0 +1,422 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = ['EmbeddedWebExtension'];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm', this);
+
+XPCOMUtils.defineLazyModuleGetter(this, 'Services',
+  'resource://gre/modules/Services.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, 'TabmixSvc',
+  'resource://tabmixplus/TabmixSvc.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+  "resource://gre/modules/PromiseUtils.jsm");
+
+const PrefFn = {0: '', 32: 'CharPref', 64: 'IntPref', 128: 'BoolPref'};
+// other settings not in extensions.tabmix. branch that we save
+const otherPrefs = [
+  'browser.allTabs.previews', 'browser.ctrlTab.previews',
+  'browser.link.open_newwindow', 'browser.link.open_newwindow.override.external',
+  'browser.link.open_newwindow.restriction', TabmixSvc.newtabUrl,
+  'browser.search.context.loadInBackground', 'browser.search.openintab',
+  'browser.sessionstore.interval', 'browser.sessionstore.max_tabs_undo',
+  'browser.sessionstore.postdata', 'browser.sessionstore.privacy_level',
+  'browser.sessionstore.restore_on_demand',
+  'browser.sessionstore.resume_from_crash', 'browser.startup.page',
+  'browser.tabs.closeWindowWithLastTab',
+  'browser.tabs.insertRelatedAfterCurrent', 'browser.tabs.loadBookmarksInBackground',
+  'browser.tabs.loadDivertedInBackground', 'browser.tabs.loadInBackground',
+  'browser.tabs.tabClipWidth', 'browser.tabs.tabMaxWidth', 'browser.tabs.tabMinWidth',
+  'browser.tabs.warnOnClose', 'browser.warnOnQuit',
+  'toolkit.scrollbox.clickToScroll.scrollDelay', 'toolkit.scrollbox.smoothScroll'
+];
+
+XPCOMUtils.defineLazyGetter(this, 'gPreferenceList', () => {
+  let prefs = Services.prefs.getDefaultBranch('');
+  let tabmixPrefs = Services.prefs.getChildList('extensions.tabmix.').sort();
+  // filter out preference without default value
+  tabmixPrefs = otherPrefs.concat(tabmixPrefs).filter(pref => {
+    try {
+      return prefs['get' + PrefFn[prefs.getPrefType(pref)]](pref) !== undefined;
+    } catch (ex) { }
+    return false;
+  });
+  return tabmixPrefs;
+});
+
+const TABMIX_ID = '{dc572301-7619-498c-a57d-39143191b318}';
+
+const MIGRATE = 'tabmix.session.migrate.';
+
+this.EmbeddedWebExtension = {
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIObserver,
+    Ci.nsISupportsWeakReference
+  ]),
+
+  webextPort: null,
+
+  connected: false,
+
+  init() {
+    if (this._initialized || !TabmixSvc.version(510)) {
+      return;
+    }
+    this._initialized = true;
+
+    this.initPrefsObserver();
+    this.startWebExtension();
+  },
+
+  startWebExtension() {
+    const {AddonManager} = Cu.import('resource://gre/modules/AddonManager.jsm', {});
+
+    AddonManager.getAddonByID(TABMIX_ID, addon => {
+      const baseURI = addon.getResourceURI('/');
+
+      const {LegacyExtensionsUtils} = Cu.import('resource://gre/modules/LegacyExtensionsUtils.jsm', {});
+
+      const embeddedWebExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
+        id: TABMIX_ID,
+        resourceURI: baseURI,
+      });
+
+      embeddedWebExtension.startup().then(({browser}) => {
+        browser.runtime.onConnect.addListener(function onConnect(port) {
+          browser.runtime.onConnect.removeListener(onConnect);
+          this.onConnect(port);
+        }.bind(this));
+      }).catch(err => {
+        TabmixSvc.console.reportError(
+          `${TABMIX_ID} - embedded webext startup failed: ${err.message} ${err.stack}\n`
+        );
+      });
+    });
+  },
+
+  onConnect(port) {
+    if (port.name !== 'tabmix-storage-port') {
+      throw new Error('Invalid port name: ' + port.name);
+    }
+
+    this.webextPort = port;
+    this.connected = true;
+
+    port.onDisconnect.addListener(() => {
+      this.webextPort = null;
+    });
+
+    port.onMessage.addListener(message => this.handleResponse(message));
+
+    this.savePreferencesData();
+    this.saveSessionsData();
+  },
+
+  messageID: 0,
+
+  asyncResponses: new Map(),
+
+  handleStorageRequest(message, asyncResponse) {
+    const errorMsg = 'Attempted to connect to the embedded WebExtension helper, but it died!';
+
+    message.messageID += ':' + this.messageID++;
+    if (this.webextPort) {
+      this.webextPort.postMessage(message);
+    } else if (asyncResponse) {
+      asyncResponse.reject(errorMsg);
+    } else {
+      TabmixSvc.console.reportError(errorMsg);
+    }
+
+    if (this.webextPort && asyncResponse) {
+      this.asyncResponses.set(message.messageID, asyncResponse);
+    }
+  },
+
+  // we show success message only on first save in the session
+  // preferences, remove sessions, add sessions
+  showSuccessMsg: 3,
+
+  handleResponse(message) {
+    const ID = message.messageID;
+    if (!ID.startsWith('migration.')) {
+      TabmixSvc.console.reportError('Unexpected messageID: ' + ID);
+      throw new Error('Unexpected messageID: ' + ID);
+    }
+
+    if (message.error) {
+      // Unexpectedly, an error occurred.
+      TabmixSvc.console.reportError(message.error);
+    } else if (this.showSuccessMsg && message.successMsg) {
+      TabmixSvc.console.log(message.successMsg);
+      this.showSuccessMsg--;
+    }
+
+    if (this.asyncResponses.has(ID)) {
+      const asyncResponse = this.asyncResponses.get(ID);
+      asyncResponse.resolve(message.error ? {} : message.result);
+      this.asyncResponses.delete(ID);
+    }
+  },
+
+  /* Migrate Tab mix preferences */
+
+  initPrefsObserver() {
+    // add prefs observer
+    const OBSERVING = ['extensions.tabmix.', ...otherPrefs];
+    OBSERVING.forEach(prefName => {
+      try {
+        Services.prefs.addObserver(prefName, this, true);
+      } catch (ex) {
+        TabmixSvc.console.log(`EmbeddedWebExtension failed to attach pref observer for ${prefName}:\n${ex}`);
+      }
+    });
+  },
+
+  observe(subject, topic, prefName) {
+    switch (topic) {
+      case "nsPref:changed": // catch pref changes
+        if (gPreferenceList.includes(prefName)) {
+          const key = 'tabmix.preference.migrate.' + prefName;
+          this.handleStorageRequest({
+            messageID: 'migration.preference.changed: ' + prefName,
+            errorMsg: 'fails to save preference to browser.storage',
+            type: 'set',
+            keys: {[key]: TabmixSvc.prefs.get(prefName)},
+          });
+        }
+        break;
+    }
+  },
+
+  savePreferencesData() {
+    const keys = this.getAllPreferences();
+    this.handleStorageRequest({
+      messageID: 'migration.preferences',
+      successMsg: this.tag`Successfully saved ${keys} preferences to browser.storage`,
+      errorMsg: 'fails to save preferences to browser.storage',
+      type: 'set',
+      keys,
+    });
+  },
+
+  getAllPreferences() {
+    return gPreferenceList.reduce((prefs, prefName) => {
+      let val;
+      try {
+        val = TabmixSvc.prefs.get(prefName);
+      } catch (ex) {}
+      if (typeof val != 'undefined') {
+        prefs['tabmix.preference.migrate.' + prefName] = val;
+      }
+      return prefs;
+    }, {});
+  },
+
+  /* Migrate Tab mix sessions data */
+
+  hashList: null,
+
+  saveSessionsData(shutDown) {
+    if (!this.connected) {
+      return;
+    }
+
+    if (shutDown) {
+      // force to save the current session - see getSessionList
+      this._lastSaveTime = 0;
+    }
+
+    if (this.hashList) {
+      const data = this.getCurrentSessionsData(this.hashList);
+      this.sendSessionToStorage(data);
+    } else {
+      this.getCurrentHashList()
+          .then(result => this.getCurrentSessionsData(result || []))
+          .then(data => this.sendSessionToStorage(data))
+          .catch(err => TabmixSvc.console.reportError(err));
+    }
+  },
+
+  getCurrentHashList() {
+    const asyncHashList = PromiseUtils.defer();
+    this.handleStorageRequest({
+      messageID: 'migration.sessions.getHash',
+      errorMsg: 'fails to read current hash list from browser.storage',
+      type: 'getHashList',
+    }, asyncHashList);
+
+    return asyncHashList.promise;
+  },
+
+  getCurrentSessionsData(currentHashList) {
+    const newHashList = [];
+    const sessionsData = {};
+    const sessions = this.getSessionList();
+    const {TabmixConvertSession} = TabmixSvc.topWin();
+
+    const changedSessions = sessions.filter(session => {
+      const hash = session.info.hash;
+      newHashList.push(hash);
+      return !currentHashList.includes(hash);
+    });
+
+    changedSessions.forEach(session => {
+      const state = TabmixConvertSession.getSessionState(session.path);
+      if (state.windows.length == 0) {
+        return;
+      }
+      // we don't need to set state: "stopped" for this migration
+      // state.session = {state: "stopped"};
+      delete state.tabsCount;
+      let {name, info: {hash, timestamp, windows, tabs}} = session;
+
+      sessionsData[hash] = {
+        name,
+        timestamp,
+        windows,
+        tabs,
+        state,
+      };
+    });
+
+    this.hashList = newHashList;
+
+    return {currentHashList, newHashList, sessionsData};
+  },
+
+  tag(strings, data, noun) {
+    let count = typeof data == 'number' ? data : Object.keys(data).length;
+    if (noun) {
+      noun = count == 1 ? noun.slice(0, -1) : noun;
+      return strings[0] + count + strings[1] + noun + strings[2];
+    }
+    return strings[0] + count + strings[1];
+  },
+
+  sendSessionToStorage({currentHashList, newHashList, sessionsData}) {
+    let remove, add;
+    const hashToRemove = currentHashList.filter(hash => !newHashList.includes(hash));
+
+    // remove obsolete data
+    if (hashToRemove.length > 0) {
+      remove = {
+        successMsg: this.tag`Successfully removed ${hashToRemove} obsolete ${'sessions'} from browser.storage`,
+        errorMsg: 'fails to remove obsolete sessions from browser.storage',
+        keys: hashToRemove,
+      };
+    }
+
+    // save sessions data
+    if (Object.keys(sessionsData).length > 0) {
+      add = {
+        successMsg: this.tag`Successfully saved ${sessionsData} ${'sessions'} to browser.storage`,
+        errorMsg: 'fails to save sessions to browser.storage',
+        keys: sessionsData,
+      };
+    }
+
+    if (remove || add) {
+      this.handleStorageRequest({
+        messageID: 'migration.sessions',
+        type: 'update',
+        remove,
+        add,
+      });
+    }
+  },
+
+  _lastSaveTime: 0,
+
+  _currentHash: null,
+
+  getSessionList() {
+    const {TabmixSessionManager: SM} = TabmixSvc.topWin();
+    const sessions = SM.getSessionList() || {list: [], path: []};
+
+    const crashedSession = SM.gSessionPath[3];
+    if (!SM.containerEmpty(crashedSession)) {
+      sessions.path.push(crashedSession);
+      sessions.list.push("Crashed Session");
+    }
+
+    const list = sessions.list.map((name, index) => {
+      const path = sessions.path[index];
+      const nameExt = SM.getLiteralValue(path, "nameExt").replace(/^, /, "");
+      return this.getInfo(name, path, nameExt);
+    });
+
+    // save current session no more than once in 10 sec, unless our session
+    // manager is shutting down
+    const currentSession = SM.gSessionPath[0];
+    this.saveCurrentSession = SM.enableManager && SM.enableBackup &&
+      !SM.containerEmpty(currentSession);
+    if (this.saveCurrentSession) {
+      const currentContainer = SM.initContainer(currentSession);
+      const count = SM.countWinsAndTabs(currentContainer);
+      // 'Current Session' don't have nameExt
+      const nameExt = SM.getNameData(count.win, count.tab);
+      const data = this.getInfo("Current Session", currentSession, nameExt);
+      const prefix = `${MIGRATE}Current_Session.`;
+      data.info.hash = data.info.hash.replace(`${MIGRATE}history.`, prefix);
+      if (this._currentHash && Date.now() - this._lastSaveTime < 10000) {
+        data.info.hash = this._currentHash;
+      } else {
+        this._lastSaveTime = Date.now();
+        this._currentHash = data.info.hash;
+      }
+      list.push(data);
+    }
+
+    return list;
+  },
+
+  getInfo(name, path, nameExt) {
+    if (nameExt.startsWith("(empty)")) {
+      nameExt = nameExt.replace("empty", "0 W, 0 T");
+    }
+
+    let re = /\((\d+\sW)*[,\s]*(\d+\sT)\)\s\((\d{4}\/\d{2}\/\d{2})\s(\d{2}:\d{2}:\d{2})/;
+    let matches = nameExt.match(re);
+    let info;
+    if (matches) {
+      const date = new Date(matches[3] + " " + matches[4]);
+      const timestamp = Date.parse(date);
+      const type = path.startsWith('rdf://tabmix/closed') ? 'history.' : 'saved.';
+      // The hash value is unique identifier for the session generated from
+      // the path timestamp and name of the session
+      info = {
+        windows: matches[1] ? parseInt(matches[1]) : 1,
+        tabs: parseInt(matches[2]),
+        timestamp,
+        hash: MIGRATE + type + generateHash(`${path}-${timestamp}-${name}`),
+      };
+    } else {
+      info = {invalidInfo: nameExt};
+    }
+
+    return {name, path, info};
+  },
+};
+
+/**
+ * Generates an hash for the given string.
+ *
+ * @note The generated hash is returned in base64 form.  Mind the fact base64
+ * is case-sensitive if you are going to reuse this code.
+ */
+function generateHash(aString) {
+  let cryptoHash = Cc["@mozilla.org/security/hash;1"]
+      .createInstance(Ci.nsICryptoHash);
+  cryptoHash.init(Ci.nsICryptoHash.MD5);
+  let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+      .createInstance(Ci.nsIStringInputStream);
+  stringStream.data = aString;
+  cryptoHash.updateFromStream(stringStream, -1);
+  return cryptoHash.finish(true);
+}
+
+this.EmbeddedWebExtension.init();
diff --git a/webextension/.eslintrc.js b/webextension/.eslintrc.js
new file mode 100644
index 0000000..0f38883
--- /dev/null
+++ b/webextension/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+    "env": {
+        "browser": true,
+        "es6": true,
+        "webextensions": true
+    },
+    "rules": {
+      "prefer-const": 2,
+      "quotes": [2, "single"]
+    }
+};
diff --git a/webextension/_locales/en/messages.json b/webextension/_locales/en/messages.json
new file mode 100644
index 0000000..0db3279
--- /dev/null
+++ b/webextension/_locales/en/messages.json
@@ -0,0 +1,3 @@
+{
+
+}
diff --git a/webextension/background.js b/webextension/background.js
new file mode 100644
index 0000000..ec33bc6
--- /dev/null
+++ b/webextension/background.js
@@ -0,0 +1,77 @@
+'use strict';
+
+const MIGRATE = 'tabmix.session.migrate.';
+
+const port = browser.runtime.connect({name: 'tabmix-storage-port'});
+
+function updateSessionsData(message) {
+  const {add, remove} = message;
+  const asyncResult = [Promise.resolve()];
+
+  if (remove) {
+    remove.messageID = message.messageID + '.remove';
+    asyncResult.push(storageMethods('remove', remove));
+  }
+
+  if (add) {
+    add.messageID = message.messageID + '.add';
+    asyncResult.push(storageMethods('set', add));
+  }
+}
+
+function getHashList({messageID}) {
+  browser.storage.local.get().then(result => {
+    return Object.keys(result).filter(key => key.startsWith(MIGRATE));
+  }).then(result => port.postMessage({messageID, result}))
+      .catch(error => port.postMessage({messageID, error}));
+}
+
+function storageMethods(methods, message) {
+  const handler = browser.storage.local[methods];
+
+  if (typeof handler != 'function') {
+    throw new Error('Unexpected message.type: ' + methods);
+  }
+
+  return handler(message.keys).then((result = {}) => {
+    const {messageID, successMsg} = message;
+    port.postMessage({
+      messageID,
+      type: methods,
+      successMsg,
+      result,
+    });
+  }).catch(error => {
+    const {messageID, keys, errorMsg} = message;
+    console.error('Tabmix:\n', error);
+    error = getErrorMsg(methods, keys, errorMsg, error);
+    port.postMessage({
+      messageID,
+      type: methods,
+      error,
+    });
+  });
+}
+
+port.onMessage.addListener(message => {
+  if (message.type == 'update') {
+    updateSessionsData(message);
+  } else if (message.type == 'getHashList') {
+    getHashList(message);
+  } else {
+    storageMethods(message.type, message);
+  }
+});
+
+function getErrorMsg(name, key, errorMsg, error) {
+  let keyVal;
+  if (typeof key == 'string') {
+    keyVal = key;
+  } else if (Array.isArray(key)) {
+    keyVal = key.join('\n');
+  } else if (Object.keys(key).length == 1) {
+    keyVal = Object.keys[0];
+  }
+  const forKey = keyVal ? `for ${keyVal}, ` : '';
+  return `browser.storage.local.${name} ${forKey}${errorMsg}.\n${error}`;
+}
diff --git a/webextension/manifest.json b/webextension/manifest.json
new file mode 100644
index 0000000..db9ed3a
--- /dev/null
+++ b/webextension/manifest.json
@@ -0,0 +1,15 @@
+{
+  "manifest_version": 2,
+  "name": "Tab Mix Plus - Extension storage migration helper",
+  "version": "1.0.0",
+  "description": "Temporary storage for legacy preferences system, Tab mix plus WebExtensions will use this data when it will be ready. Currently the schedule is unknown",
+
+  "background": {
+    "scripts": ["background.js"]
+  },
+  "permissions": [
+    "storage"
+  ],
+
+  "default_locale": "en"
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/tabmixplus.git



More information about the Pkg-mozext-commits mailing list