dom/activities/src/ActivitiesService.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict"
     7 const Cu = Components.utils;
     8 const Cc = Components.classes;
     9 const Ci = Components.interfaces;
    11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    12 Cu.import("resource://gre/modules/Services.jsm");
    13 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
    15 XPCOMUtils.defineLazyModuleGetter(this, "ActivitiesServiceFilter",
    16   "resource://gre/modules/ActivitiesServiceFilter.jsm");
    18 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
    19                                    "@mozilla.org/parentprocessmessagemanager;1",
    20                                    "nsIMessageBroadcaster");
    22 XPCOMUtils.defineLazyServiceGetter(this, "NetUtil",
    23                                    "@mozilla.org/network/util;1",
    24                                    "nsINetUtil");
    26 this.EXPORTED_SYMBOLS = [];
    28 function debug(aMsg) {
    29   //dump("-- ActivitiesService.jsm " + Date.now() + " " + aMsg + "\n");
    30 }
    32 const DB_NAME    = "activities";
    33 const DB_VERSION = 1;
    34 const STORE_NAME = "activities";
    36 function ActivitiesDb() {
    38 }
    40 ActivitiesDb.prototype = {
    41   __proto__: IndexedDBHelper.prototype,
    43   init: function actdb_init() {
    44     this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME]);
    45   },
    47   /**
    48    * Create the initial database schema.
    49    *
    50    * The schema of records stored is as follows:
    51    *
    52    * {
    53    *  id:                  String
    54    *  manifest:            String
    55    *  name:                String
    56    *  icon:                String
    57    *  description:         jsval
    58    * }
    59    */
    60   upgradeSchema: function actdb_upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
    61     debug("Upgrade schema " + aOldVersion + " -> " + aNewVersion);
    62     let objectStore = aDb.createObjectStore(STORE_NAME, { keyPath: "id" });
    64     // indexes
    65     objectStore.createIndex("name", "name", { unique: false });
    66     objectStore.createIndex("manifest", "manifest", { unique: false });
    68     debug("Created object stores and indexes");
    69   },
    71   // unique ids made of (uri, action)
    72   createId: function actdb_createId(aObject) {
    73     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
    74                       .createInstance(Ci.nsIScriptableUnicodeConverter);
    75     converter.charset = "UTF-8";
    77     let hasher = Cc["@mozilla.org/security/hash;1"]
    78                    .createInstance(Ci.nsICryptoHash);
    79     hasher.init(hasher.SHA1);
    81     // add uri and action to the hash
    82     ["manifest", "name"].forEach(function(aProp) {
    83       let data = converter.convertToByteArray(aObject[aProp], {});
    84       hasher.update(data, data.length);
    85     });
    87     return hasher.finish(true);
    88   },
    90   // Add all the activities carried in the |aObjects| array.
    91   add: function actdb_add(aObjects, aSuccess, aError) {
    92     this.newTxn("readwrite", STORE_NAME, function (txn, store) {
    93       aObjects.forEach(function (aObject) {
    94         let object = {
    95           manifest: aObject.manifest,
    96           name: aObject.name,
    97           icon: aObject.icon || "",
    98           description: aObject.description
    99         };
   100         object.id = this.createId(object);
   101         debug("Going to add " + JSON.stringify(object));
   102         store.put(object);
   103       }, this);
   104     }.bind(this), aSuccess, aError);
   105   },
   107   // Remove all the activities carried in the |aObjects| array.
   108   remove: function actdb_remove(aObjects) {
   109     this.newTxn("readwrite", STORE_NAME, function (txn, store) {
   110       aObjects.forEach(function (aObject) {
   111         let object = {
   112           manifest: aObject.manifest,
   113           name: aObject.name
   114         };
   115         debug("Going to remove " + JSON.stringify(object));
   116         store.delete(this.createId(object));
   117       }, this);
   118     }.bind(this), function() {}, function() {});
   119   },
   121   find: function actdb_find(aObject, aSuccess, aError, aMatch) {
   122     debug("Looking for " + aObject.options.name);
   124     this.newTxn("readonly", STORE_NAME, function (txn, store) {
   125       let index = store.index("name");
   126       let request = index.mozGetAll(aObject.options.name);
   127       request.onsuccess = function findSuccess(aEvent) {
   128         debug("Request successful. Record count: " + aEvent.target.result.length);
   129         if (!txn.result) {
   130           txn.result = {
   131             name: aObject.options.name,
   132             options: []
   133           };
   134         }
   136         aEvent.target.result.forEach(function(result) {
   137           if (!aMatch(result))
   138             return;
   140           txn.result.options.push({
   141             manifest: result.manifest,
   142             icon: result.icon,
   143             description: result.description
   144           });
   145         });
   146       }
   147     }.bind(this), aSuccess, aError);
   148   }
   149 }
   151 let Activities = {
   152   messages: [
   153     // ActivityProxy.js
   154     "Activity:Start",
   156     // ActivityWrapper.js
   157     "Activity:Ready",
   159     // ActivityRequestHandler.js
   160     "Activity:PostResult",
   161     "Activity:PostError",
   163     "Activities:Register",
   164     "Activities:Unregister",
   165     "Activities:GetContentTypes",
   167     "child-process-shutdown"
   168   ],
   170   init: function activities_init() {
   171     this.messages.forEach(function(msgName) {
   172       ppmm.addMessageListener(msgName, this);
   173     }, this);
   175     Services.obs.addObserver(this, "xpcom-shutdown", false);
   177     this.db = new ActivitiesDb();
   178     this.db.init();
   179     this.callers = {};
   180   },
   182   observe: function activities_observe(aSubject, aTopic, aData) {
   183     this.messages.forEach(function(msgName) {
   184       ppmm.removeMessageListener(msgName, this);
   185     }, this);
   186     ppmm = null;
   188     if (this.db) {
   189       this.db.close();
   190       this.db = null;
   191     }
   193     Services.obs.removeObserver(this, "xpcom-shutdown");
   194   },
   196   /**
   197     * Starts an activity by doing:
   198     * - finds a list of matching activities.
   199     * - calls the UI glue to get the user choice.
   200     * - fire an system message of type "activity" to this app, sending the
   201     *   activity data as a payload.
   202     */
   203   startActivity: function activities_startActivity(aMsg) {
   204     debug("StartActivity: " + JSON.stringify(aMsg));
   206     let successCb = function successCb(aResults) {
   207       debug(JSON.stringify(aResults));
   209       // We have no matching activity registered, let's fire an error.
   210       if (aResults.options.length === 0) {
   211         Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", {
   212           "id": aMsg.id,
   213           "error": "NO_PROVIDER"
   214         });
   215         delete Activities.callers[aMsg.id];
   216         return;
   217       }
   219       function getActivityChoice(aChoice) {
   220         debug("Activity choice: " + aChoice);
   222         // The user has cancelled the choice, fire an error.
   223         if (aChoice === -1) {
   224           Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", {
   225             "id": aMsg.id,
   226             "error": "ActivityCanceled"
   227           });
   228           delete Activities.callers[aMsg.id];
   229           return;
   230         }
   232         let sysmm = Cc["@mozilla.org/system-message-internal;1"]
   233                       .getService(Ci.nsISystemMessagesInternal);
   234         if (!sysmm) {
   235           // System message is not present, what should we do?
   236           delete Activities.callers[aMsg.id];
   237           return;
   238         }
   240         debug("Sending system message...");
   241         let result = aResults.options[aChoice];
   242         sysmm.sendMessage("activity", {
   243             "id": aMsg.id,
   244             "payload": aMsg.options,
   245             "target": result.description
   246           },
   247           Services.io.newURI(result.description.href, null, null),
   248           Services.io.newURI(result.manifest, null, null),
   249           {
   250             "manifestURL": Activities.callers[aMsg.id].manifestURL,
   251             "pageURL": Activities.callers[aMsg.id].pageURL
   252           });
   254         if (!result.description.returnValue) {
   255           Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireSuccess", {
   256             "id": aMsg.id,
   257             "result": null
   258           });
   259           // No need to notify observers, since we don't want the caller
   260           // to be raised on the foreground that quick.
   261           delete Activities.callers[aMsg.id];
   262         }
   263       };
   265       let glue = Cc["@mozilla.org/dom/activities/ui-glue;1"]
   266                    .createInstance(Ci.nsIActivityUIGlue);
   267       glue.chooseActivity(aResults.name, aResults.options, getActivityChoice);
   268     };
   270     let errorCb = function errorCb(aError) {
   271       // Something unexpected happened. Should we send an error back?
   272       debug("Error in startActivity: " + aError + "\n");
   273     };
   275     let matchFunc = function matchFunc(aResult) {
   276       return ActivitiesServiceFilter.match(aMsg.options.data,
   277                                            aResult.description.filters);
   278     };
   280     this.db.find(aMsg, successCb, errorCb, matchFunc);
   281   },
   283   receiveMessage: function activities_receiveMessage(aMessage) {
   284     let mm = aMessage.target;
   285     let msg = aMessage.json;
   287     let caller;
   288     let obsData;
   290     if (aMessage.name == "Activity:PostResult" ||
   291         aMessage.name == "Activity:PostError" ||
   292         aMessage.name == "Activity:Ready") {
   293       caller = this.callers[msg.id];
   294       if (!caller) {
   295         debug("!! caller is null for msg.id=" + msg.id);
   296         return;
   297       }
   298       obsData = JSON.stringify({ manifestURL: caller.manifestURL,
   299                                  pageURL: caller.pageURL,
   300                                  success: aMessage.name == "Activity:PostResult" });
   301     }
   303     switch(aMessage.name) {
   304       case "Activity:Start":
   305         this.callers[msg.id] = { mm: mm,
   306                                  manifestURL: msg.manifestURL,
   307                                  pageURL: msg.pageURL };
   308         this.startActivity(msg);
   309         break;
   311       case "Activity:Ready":
   312         caller.childMM = mm;
   313         break;
   315       case "Activity:PostResult":
   316         caller.mm.sendAsyncMessage("Activity:FireSuccess", msg);
   317         delete this.callers[msg.id];
   318         break;
   319       case "Activity:PostError":
   320         caller.mm.sendAsyncMessage("Activity:FireError", msg);
   321         delete this.callers[msg.id];
   322         break;
   324       case "Activities:Register":
   325         let self = this;
   326         this.db.add(msg,
   327           function onSuccess(aEvent) {
   328             mm.sendAsyncMessage("Activities:Register:OK", null);
   329             let res = [];
   330             msg.forEach(function(aActivity) {
   331               self.updateContentTypeList(aActivity, res);
   332             });
   333             if (res.length) {
   334               ppmm.broadcastAsyncMessage("Activities:RegisterContentTypes",
   335                                          { contentTypes: res });
   336             }
   337           },
   338           function onError(aEvent) {
   339             msg.error = "REGISTER_ERROR";
   340             mm.sendAsyncMessage("Activities:Register:KO", msg);
   341           });
   342         break;
   343       case "Activities:Unregister":
   344         this.db.remove(msg);
   345         let res = [];
   346         msg.forEach(function(aActivity) {
   347           this.updateContentTypeList(aActivity, res);
   348         }, this);
   349         if (res.length) {
   350           ppmm.broadcastAsyncMessage("Activities:UnregisterContentTypes",
   351                                      { contentTypes: res });
   352         }
   353         break;
   354       case "Activities:GetContentTypes":
   355         this.sendContentTypes(mm);
   356         break;
   357       case "child-process-shutdown":
   358         for (let id in this.callers) {
   359           if (this.callers[id].childMM == mm) {
   360             this.callers[id].mm.sendAsyncMessage("Activity:FireError", {
   361               "id": id,
   362               "error": "ActivityCanceled"
   363             });
   364             delete this.callers[id];
   365             break;
   366           }
   367         }
   368         break;
   369     }
   370   },
   372   updateContentTypeList: function updateContentTypeList(aActivity, aResult) {
   373     // Bail out if this is not a "view" activity.
   374     if (aActivity.name != "view") {
   375       return;
   376     }
   378     let types = aActivity.description.filters.type;
   379     if (typeof types == "string") {
   380       types = [types];
   381     }
   383     // Check that this is a real content type and sanitize it.
   384     types.forEach(function(aContentType) {
   385       let hadCharset = { };
   386       let charset = { };
   387       let contentType =
   388         NetUtil.parseContentType(aContentType, charset, hadCharset);
   389       if (contentType) {
   390         aResult.push(contentType);
   391       }
   392     });
   393   },
   395   sendContentTypes: function sendContentTypes(aMm) {
   396     let res = [];
   397     let self = this;
   398     this.db.find({ options: { name: "view" } },
   399       function() { // Success callback.
   400         if (res.length) {
   401           aMm.sendAsyncMessage("Activities:RegisterContentTypes",
   402                                { contentTypes: res });
   403         }
   404       },
   405       null, // Error callback.
   406       function(aActivity) { // Matching callback.
   407         self.updateContentTypeList(aActivity, res)
   408         return false;
   409       }
   410     );
   411   }
   412 }
   414 Activities.init();

mercurial