toolkit/components/places/nsLivemarkService.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     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 const Cc = Components.classes;
     6 const Ci = Components.interfaces;
     7 const Cr = Components.results;
     8 const Cu = Components.utils;
    10 ////////////////////////////////////////////////////////////////////////////////
    11 //// Modules
    13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    14 Cu.import("resource://gre/modules/Services.jsm");
    15 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    16                                   "resource://gre/modules/PlacesUtils.jsm");
    17 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    18                                   "resource://gre/modules/NetUtil.jsm");
    19 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    20                                   "resource://gre/modules/Promise.jsm");
    21 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
    22                                   "resource://gre/modules/Deprecated.jsm");
    24 ////////////////////////////////////////////////////////////////////////////////
    25 //// Services
    27 XPCOMUtils.defineLazyServiceGetter(this, "secMan",
    28                                    "@mozilla.org/scriptsecuritymanager;1",
    29                                    "nsIScriptSecurityManager");
    30 XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () {
    31   // Lazily add an history observer when it's actually needed.
    32   PlacesUtils.history.addObserver(PlacesUtils.livemarks, true);
    33   return Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory);
    34 });
    36 ////////////////////////////////////////////////////////////////////////////////
    37 //// Constants
    39 // Security flags for checkLoadURIWithPrincipal.
    40 const SEC_FLAGS = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL;
    42 // Delay between reloads of consecute livemarks.
    43 const RELOAD_DELAY_MS = 500;
    44 // Expire livemarks after this time.
    45 const EXPIRE_TIME_MS = 3600000; // 1 hour.
    46 // Expire livemarks after this time on error.
    47 const ONERROR_EXPIRE_TIME_MS = 300000; // 5 minutes.
    49 ////////////////////////////////////////////////////////////////////////////////
    50 //// LivemarkService
    52 function LivemarkService()
    53 {
    54   // Cleanup on shutdown.
    55   Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);
    57   // Observe bookmarks and history, but don't init the services just for that.
    58   PlacesUtils.addLazyBookmarkObserver(this, true);
    60   // Asynchronously build the livemarks cache.
    61   this._ensureAsynchronousCache();
    62 }
    64 LivemarkService.prototype = {
    65   // Cache of Livemark objects, hashed by bookmarks folder ids.
    66   _livemarks: {},
    67   // Hash associating guids to bookmarks folder ids.
    68   _guids: {},
    70   get _populateCacheSQL()
    71   {
    72     function getAnnoSQLFragment(aAnnoParam) {
    73       return "SELECT a.content "
    74            + "FROM moz_items_annos a "
    75            + "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
    76            + "WHERE a.item_id = b.id "
    77            +   "AND n.name = " + aAnnoParam;
    78     }
    80     return "SELECT b.id, b.title, b.parent, b.position, b.guid, b.lastModified, "
    81          +        "(" + getAnnoSQLFragment(":feedURI_anno") + ") AS feedURI, "
    82          +        "(" + getAnnoSQLFragment(":siteURI_anno") + ") AS siteURI "
    83          + "FROM moz_bookmarks b "
    84          + "JOIN moz_items_annos a ON a.item_id = b.id "
    85          + "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id "
    86          + "WHERE b.type = :folder_type "
    87          +   "AND n.name = :feedURI_anno ";
    88   },
    90   _ensureAsynchronousCache: function LS__ensureAsynchronousCache()
    91   {
    92     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
    93                                 .DBConnection;
    94     let stmt = db.createAsyncStatement(this._populateCacheSQL);
    95     stmt.params.folder_type = Ci.nsINavBookmarksService.TYPE_FOLDER;
    96     stmt.params.feedURI_anno = PlacesUtils.LMANNO_FEEDURI;
    97     stmt.params.siteURI_anno = PlacesUtils.LMANNO_SITEURI;
    99     let livemarkSvc = this;
   100     this._pendingStmt = stmt.executeAsync({
   101       handleResult: function LS_handleResult(aResults)
   102       {
   103         for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
   104           let id = row.getResultByName("id");
   105           let siteURL = row.getResultByName("siteURI");
   106           let guid = row.getResultByName("guid");
   107           livemarkSvc._livemarks[id] =
   108             new Livemark({ id: id,
   109                            guid: guid,
   110                            title: row.getResultByName("title"),
   111                            parentId: row.getResultByName("parent"),
   112                            index: row.getResultByName("position"),
   113                            lastModified: row.getResultByName("lastModified"),
   114                            feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
   115                            siteURI: siteURL ? NetUtil.newURI(siteURL) : null,
   116             });
   117           livemarkSvc._guids[guid] = id;
   118         }
   119       },
   120       handleError: function LS_handleError(aErr)
   121       {
   122         Cu.reportError("AsyncStmt error (" + aErr.result + "): '" + aErr.message);
   123       },
   124       handleCompletion: function LS_handleCompletion() {
   125         livemarkSvc._pendingStmt = null;
   126       }
   127     });
   128     stmt.finalize();
   129   },
   131   _onCacheReady: function LS__onCacheReady(aCallback)
   132   {
   133     if (this._pendingStmt) {
   134       // The cache is still being populated, so enqueue the job to the Storage
   135       // async thread.  Ideally this should just dispatch a runnable to it,
   136       // that would call back on the main thread, but bug 608142 made that
   137       // impossible.  Thus just enqueue the cheapest query possible.
   138       let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   139                                   .DBConnection;
   140       let stmt = db.createAsyncStatement("PRAGMA encoding");
   141       stmt.executeAsync({
   142         handleError: function () {},
   143         handleResult: function () {},
   144         handleCompletion: function ETAT_handleCompletion()
   145         {
   146           aCallback();
   147         }
   148       });
   149       stmt.finalize();
   150     }
   151     else {
   152       // The callbacks should always be enqueued per the interface.
   153       // Just enque on the main thread.
   154       Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
   155     }
   156   },
   158   _reloading: false,
   159   _startReloadTimer: function LS__startReloadTimer()
   160   {
   161     if (this._reloadTimer) {
   162       this._reloadTimer.cancel();
   163     }
   164     else {
   165       this._reloadTimer = Cc["@mozilla.org/timer;1"]
   166                             .createInstance(Ci.nsITimer);
   167     }
   168     this._reloading = true;
   169     this._reloadTimer.initWithCallback(this._reloadNextLivemark.bind(this),
   170                                        RELOAD_DELAY_MS,
   171                                        Ci.nsITimer.TYPE_ONE_SHOT);
   172   },
   174   //////////////////////////////////////////////////////////////////////////////
   175   //// nsIObserver
   177   observe: function LS_observe(aSubject, aTopic, aData)
   178   {
   179     if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) {
   180       if (this._pendingStmt) {
   181         this._pendingStmt.cancel();
   182         this._pendingStmt = null;
   183         // Initialization never finished, so just bail out.
   184         return;
   185       }
   187       if (this._reloadTimer) {
   188         this._reloading = false;
   189         this._reloadTimer.cancel();
   190         delete this._reloadTimer;
   191       }
   193       // Stop any ongoing update.
   194       for each (let livemark in this._livemarks) {
   195         livemark.terminate();
   196       }
   197       this._livemarks = {};
   198     }
   199   },
   201   //////////////////////////////////////////////////////////////////////////////
   202   //// mozIAsyncLivemarks
   204   addLivemark: function LS_addLivemark(aLivemarkInfo,
   205                                        aLivemarkCallback)
   206   {
   207     // Must provide at least non-null parentId, index and feedURI.
   208     if (!aLivemarkInfo ||
   209         ("parentId" in aLivemarkInfo && aLivemarkInfo.parentId < 1) ||
   210         !("index" in aLivemarkInfo) || aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX ||
   211         !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) ||
   212         (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) ||
   213         (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) {
   214       throw Cr.NS_ERROR_INVALID_ARG;
   215     }
   217     if (aLivemarkCallback) {
   218       Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
   219                          "Please use the returned promise instead.",
   220                          "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
   221     }
   223     // The addition is done synchronously due to the fact importExport service
   224     // and JSON backups require that.  The notification is async though.
   225     // Once bookmarks are async, this may be properly fixed.
   226     let deferred = Promise.defer();
   227     let addLivemarkEx = null;
   228     let livemark = null;
   229     try {
   230       // Disallow adding a livemark inside another livemark.
   231       if (aLivemarkInfo.parentId in this._livemarks) {
   232         throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   233       }
   235       // Don't pass unexpected input data to the livemark constructor.
   236       livemark = new Livemark({ title:        aLivemarkInfo.title
   237                               , parentId:     aLivemarkInfo.parentId
   238                               , index:        aLivemarkInfo.index
   239                               , feedURI:      aLivemarkInfo.feedURI
   240                               , siteURI:      aLivemarkInfo.siteURI
   241                               , guid:         aLivemarkInfo.guid
   242                               , lastModified: aLivemarkInfo.lastModified
   243                               });
   244       if (this._itemAdded && this._itemAdded.id == livemark.id) {
   245         livemark.index = this._itemAdded.index;
   246         livemark.guid = this._itemAdded.guid;
   247         if (!aLivemarkInfo.lastModified) {
   248           livemark.lastModified = this._itemAdded.lastModified;
   249         }
   250       }
   252       // Updating the cache even if it has not yet been populated doesn't
   253       // matter since it will just be overwritten.
   254       this._livemarks[livemark.id] = livemark;
   255       this._guids[livemark.guid] = livemark.id;
   256     }
   257     catch (ex) {
   258       addLivemarkEx = ex;
   259       livemark = null;
   260     }
   261     finally {
   262       this._onCacheReady( () => {
   263         if (addLivemarkEx) {
   264           if (aLivemarkCallback) {
   265             try {
   266               aLivemarkCallback.onCompletion(addLivemarkEx.result, livemark);
   267             }
   268             catch(ex2) { }
   269           } else {
   270             deferred.reject(addLivemarkEx);
   271           }
   272         }
   273         else {
   274           if (aLivemarkCallback) {
   275             try {
   276               aLivemarkCallback.onCompletion(Cr.NS_OK, livemark);
   277             }
   278             catch(ex2) { }
   279           } else {
   280             deferred.resolve(livemark);
   281           }
   282         }
   283       });
   284     }
   286     return aLivemarkCallback ? null : deferred.promise;
   287   },
   289   removeLivemark: function LS_removeLivemark(aLivemarkInfo, aLivemarkCallback)
   290   {
   291     if (!aLivemarkInfo) {
   292       throw Cr.NS_ERROR_INVALID_ARG;
   293     }
   295     // Accept either a guid or an id.
   296     let id = aLivemarkInfo.guid || aLivemarkInfo.id;
   297     if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
   298         ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) ||
   299         !id) {
   300       throw Cr.NS_ERROR_INVALID_ARG;
   301     }
   303     if (aLivemarkCallback) {
   304       Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
   305                          "Please use the returned promise instead.",
   306                          "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
   307     }
   309     // Convert the guid to an id.
   310     if (id in this._guids) {
   311       id = this._guids[id];
   312     }
   314     let deferred = Promise.defer();
   315     let removeLivemarkEx = null;
   316     try {
   317       if (!(id in this._livemarks)) {
   318         throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   319       }
   320       this._livemarks[id].remove();
   321     }
   322     catch (ex) {
   323       removeLivemarkEx = ex;
   324     }
   325     finally {
   326       this._onCacheReady( () => {
   327         if (removeLivemarkEx) {
   328           if (aLivemarkCallback) {
   329             try {
   330               aLivemarkCallback.onCompletion(removeLivemarkEx.result, null);
   331             }
   332             catch(ex2) { }
   333           } else {
   334             deferred.reject(removeLivemarkEx);
   335           }
   336         }
   337         else {
   338           if (aLivemarkCallback) {
   339             try {
   340               aLivemarkCallback.onCompletion(Cr.NS_OK, null);
   341             }
   342             catch(ex2) { }
   343           } else {
   344             deferred.resolve();
   345           }
   346         }
   347       });
   348     }
   350     return aLivemarkCallback ? null : deferred.promise;
   351   },
   353   _reloaded: [],
   354   _reloadNextLivemark: function LS__reloadNextLivemark()
   355   {
   356     this._reloading = false;
   357     // Find first livemark to be reloaded.
   358     for (let id in this._livemarks) {
   359       if (this._reloaded.indexOf(id) == -1) {
   360         this._reloaded.push(id);
   361         this._livemarks[id].reload(this._forceUpdate);
   362         this._startReloadTimer();
   363         break;
   364       }
   365     }
   366   },
   368   reloadLivemarks: function LS_reloadLivemarks(aForceUpdate)
   369   {
   370     // Check if there's a currently running reload, to save some useless work.
   371     let notWorthRestarting =
   372       this._forceUpdate || // We're already forceUpdating.
   373       !aForceUpdate;       // The caller didn't request a forced update.
   374     if (this._reloading && notWorthRestarting) {
   375       // Ignore this call.
   376       return;
   377     }
   379     this._onCacheReady( () => {
   380       this._forceUpdate = !!aForceUpdate;
   381       this._reloaded = [];
   382       // Livemarks reloads happen on a timer, and are delayed for performance
   383       // reasons.
   384       this._startReloadTimer();
   385     });
   386   },
   388   getLivemark: function LS_getLivemark(aLivemarkInfo, aLivemarkCallback)
   389   {
   390     if (!aLivemarkInfo) {
   391       throw Cr.NS_ERROR_INVALID_ARG;
   392     }
   393     // Accept either a guid or an id.
   394     let id = aLivemarkInfo.guid || aLivemarkInfo.id;
   395     if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
   396         ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) ||
   397         !id) {
   398       throw Cr.NS_ERROR_INVALID_ARG;
   399     }
   401     if (aLivemarkCallback) {
   402       Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
   403                          "Please use the returned promise instead.",
   404                          "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
   405     }
   407     let deferred = Promise.defer();
   408     this._onCacheReady( () => {
   409       // Convert the guid to an id.
   410       if (id in this._guids) {
   411         id = this._guids[id];
   412       }
   413       if (id in this._livemarks) {
   414         if (aLivemarkCallback) {
   415           try {
   416             aLivemarkCallback.onCompletion(Cr.NS_OK, this._livemarks[id]);
   417           } catch (ex) {}
   418         } else {
   419           deferred.resolve(this._livemarks[id]);
   420         }
   421       }
   422       else {
   423         if (aLivemarkCallback) {
   424           try {
   425             aLivemarkCallback.onCompletion(Cr.NS_ERROR_INVALID_ARG, null);
   426           } catch (ex) { }
   427         } else {
   428           deferred.reject(Components.Exception("", Cr.NS_ERROR_INVALID_ARG));
   429         }
   430       }
   431     });
   433     return aLivemarkCallback ? null : deferred.promise;
   434   },
   436   //////////////////////////////////////////////////////////////////////////////
   437   //// nsINavBookmarkObserver
   439   onBeginUpdateBatch:  function () {},
   440   onEndUpdateBatch:    function () {},
   441   onItemVisited:       function () {},
   443   _itemAdded: null,
   444   onItemAdded: function LS_onItemAdded(aItemId, aParentId, aIndex, aItemType,
   445                                        aURI, aTitle, aDateAdded, aGUID)
   446   {
   447     if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
   448       this._itemAdded = { id: aItemId
   449                         , guid: aGUID
   450                         , index: aIndex
   451                         , lastModified: aDateAdded
   452                         };
   453     }
   454   },
   456   onItemChanged: function LS_onItemChanged(aItemId, aProperty, aIsAnno, aValue,
   457                                            aLastModified, aItemType)
   458   {
   459     if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
   460       if (this._itemAdded && this._itemAdded.id == aItemId) {
   461         this._itemAdded.lastModified = aLastModified;
   462      }
   463       if (aItemId in this._livemarks) {
   464         if (aProperty == "title") {
   465           this._livemarks[aItemId].title = aValue;
   466         }
   467         this._livemarks[aItemId].lastModified = aLastModified;
   468       }
   469     }
   470   },
   472   onItemMoved: function LS_onItemMoved(aItemId, aOldParentId, aOldIndex,
   473                                       aNewParentId, aNewIndex, aItemType)
   474   {
   475     if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER &&
   476         aItemId in this._livemarks) {
   477       this._livemarks[aItemId].parentId = aNewParentId;
   478       this._livemarks[aItemId].index = aNewIndex;
   479     }
   480   },
   482   onItemRemoved: function LS_onItemRemoved(aItemId, aParentId, aIndex,
   483                                            aItemType, aURI, aGUID)
   484   {
   485     if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER &&
   486         aItemId in this._livemarks) {
   487       this._livemarks[aItemId].terminate();
   488       delete this._livemarks[aItemId];
   489       delete this._guids[aGUID];
   490     }
   491   },
   493   //////////////////////////////////////////////////////////////////////////////
   494   //// nsINavHistoryObserver
   496   onBeginUpdateBatch: function () {},
   497   onEndUpdateBatch:   function () {},
   498   onPageChanged:      function () {},
   499   onTitleChanged:     function () {},
   500   onDeleteVisits:     function () {},
   501   onClearHistory:     function () {},
   503   onDeleteURI: function PS_onDeleteURI(aURI) {
   504     for each (let livemark in this._livemarks) {
   505       livemark.updateURIVisitedStatus(aURI, false);
   506     }
   507   },
   509   onVisit: function PS_onVisit(aURI) {
   510     for each (let livemark in this._livemarks) {
   511       livemark.updateURIVisitedStatus(aURI, true);
   512     }
   513   },
   515   //////////////////////////////////////////////////////////////////////////////
   516   //// nsISupports
   518   classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"),
   520   _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService),
   522   QueryInterface: XPCOMUtils.generateQI([
   523     Ci.mozIAsyncLivemarks
   524   , Ci.nsINavBookmarkObserver
   525   , Ci.nsINavHistoryObserver
   526   , Ci.nsIObserver
   527   , Ci.nsISupportsWeakReference
   528   ])
   529 };
   531 ////////////////////////////////////////////////////////////////////////////////
   532 //// Livemark
   534 /**
   535  * Object used internally to represent a livemark.
   536  *
   537  * @param aLivemarkInfo
   538  *        Object containing information on the livemark.  If the livemark is
   539  *        not included in the object, a new livemark will be created.
   540  *
   541  * @note terminate() must be invoked before getting rid of this object.
   542  */
   543 function Livemark(aLivemarkInfo)
   544 {
   545   this.title = aLivemarkInfo.title;
   546   this.parentId = aLivemarkInfo.parentId;
   547   this.index = aLivemarkInfo.index;
   549   this._status = Ci.mozILivemark.STATUS_READY;
   551   // Hash of resultObservers, hashed by container.
   552   this._resultObservers = new Map();
   553   // This keeps a list of the containers used as keys in the map, since
   554   // it's not iterable.  In future may use an iterable Map.
   555   this._resultObserversList = [];
   557   // Sorted array of objects representing livemark children in the form
   558   // { uri, title, visited }.
   559   this._children = [];
   561   // Keeps a separate array of nodes for each requesting container, hashed by
   562   // the container itself.
   563   this._nodes = new Map();
   565   this._guid = "";
   566   this._lastModified = 0;
   568   this.loadGroup = null;
   569   this.feedURI = null;
   570   this.siteURI = null;
   571   this.expireTime = 0;
   573   if (aLivemarkInfo.id) {
   574     // This request comes from the cache.
   575     this.id = aLivemarkInfo.id;
   576     this.guid = aLivemarkInfo.guid;
   577     this.feedURI = aLivemarkInfo.feedURI;
   578     this.siteURI = aLivemarkInfo.siteURI;
   579     this.lastModified = aLivemarkInfo.lastModified;
   580   }
   581   else {
   582     // Create a new livemark.
   583     this.id = PlacesUtils.bookmarks.createFolder(aLivemarkInfo.parentId,
   584                                                  aLivemarkInfo.title,
   585                                                  aLivemarkInfo.index,
   586                                                  aLivemarkInfo.guid);
   587     PlacesUtils.bookmarks.setFolderReadonly(this.id, true);
   588     this.writeFeedURI(aLivemarkInfo.feedURI);
   589     if (aLivemarkInfo.siteURI) {
   590       this.writeSiteURI(aLivemarkInfo.siteURI);
   591     }
   592     // Last modified time must be the last change.
   593     if (aLivemarkInfo.lastModified) {
   594       this.lastModified = aLivemarkInfo.lastModified;
   595       PlacesUtils.bookmarks.setItemLastModified(this.id, this.lastModified);
   596     }
   597   }
   598 }
   600 Livemark.prototype = {
   601   get status() this._status,
   602   set status(val) {
   603     if (this._status != val) {
   604       this._status = val;
   605       this._invalidateRegisteredContainers();
   606     }
   607     return this._status;
   608   },
   610   /**
   611    * Sets an annotation on the bookmarks folder id representing the livemark.
   612    *
   613    * @param aAnnoName
   614    *        Name of the annotation.
   615    * @param aValue
   616    *        Value of the annotation.
   617    * @return The annotation value.
   618    * @throws If the folder is invalid.
   619    */
   620   _setAnno: function LM__setAnno(aAnnoName, aValue)
   621   {
   622     PlacesUtils.annotations
   623                .setItemAnnotation(this.id, aAnnoName, aValue, 0,
   624                                   PlacesUtils.annotations.EXPIRE_NEVER);
   625   },
   627   writeFeedURI: function LM_writeFeedURI(aFeedURI)
   628   {
   629     this._setAnno(PlacesUtils.LMANNO_FEEDURI, aFeedURI.spec);
   630     this.feedURI = aFeedURI;
   631   },
   633   writeSiteURI: function LM_writeSiteURI(aSiteURI)
   634   {
   635     if (!aSiteURI) {
   636       PlacesUtils.annotations.removeItemAnnotation(this.id,
   637                                                    PlacesUtils.LMANNO_SITEURI)
   638       this.siteURI = null;
   639       return;
   640     }
   642     // Security check the site URI against the feed URI principal.
   643     let feedPrincipal = secMan.getSimpleCodebasePrincipal(this.feedURI);
   644     try {
   645       secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI, SEC_FLAGS);
   646     }
   647     catch (ex) {
   648       return;
   649     }
   651     this._setAnno(PlacesUtils.LMANNO_SITEURI, aSiteURI.spec)
   652     this.siteURI = aSiteURI;
   653   },
   655   set guid(aGUID) {
   656     this._guid = aGUID;
   657     return aGUID;
   658   },
   659   get guid() this._guid,
   661   set lastModified(aLastModified) {
   662     this._lastModified = aLastModified;
   663     return aLastModified;
   664   },
   665   get lastModified() this._lastModified,
   667   /**
   668    * Tries to updates the livemark if needed.
   669    * The update process is asynchronous.
   670    *
   671    * @param [optional] aForceUpdate
   672    *        If true will try to update the livemark even if its contents have
   673    *        not yet expired.
   674    */
   675   updateChildren: function LM_updateChildren(aForceUpdate)
   676   {
   677     // Check if the livemark is already updating.
   678     if (this.status == Ci.mozILivemark.STATUS_LOADING)
   679       return;
   681     // Check the TTL/expiration on this, to check if there is no need to update
   682     // this livemark.
   683     if (!aForceUpdate && this.children.length && this.expireTime > Date.now())
   684       return;
   686     this.status = Ci.mozILivemark.STATUS_LOADING;
   688     // Setting the status notifies observers that may remove the livemark.
   689     if (this._terminated)
   690       return;
   692     try {
   693       // Create a load group for the request.  This will allow us to
   694       // automatically keep track of redirects, so we can always
   695       // cancel the channel.
   696       let loadgroup = Cc["@mozilla.org/network/load-group;1"].
   697                       createInstance(Ci.nsILoadGroup);
   698       let channel = NetUtil.newChannel(this.feedURI.spec).
   699                     QueryInterface(Ci.nsIHttpChannel);
   700       channel.loadGroup = loadgroup;
   701       channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND |
   702                            Ci.nsIRequest.LOAD_BYPASS_CACHE;
   703       channel.requestMethod = "GET";
   704       channel.setRequestHeader("X-Moz", "livebookmarks", false);
   706       // Stream the result to the feed parser with this listener
   707       let listener = new LivemarkLoadListener(this);
   708       channel.notificationCallbacks = listener;
   709       channel.asyncOpen(listener, null);
   711       this.loadGroup = loadgroup;
   712     }
   713     catch (ex) {
   714       this.status = Ci.mozILivemark.STATUS_FAILED;
   715     }
   716   },
   718   reload: function LM_reload(aForceUpdate)
   719   {
   720     this.updateChildren(aForceUpdate);
   721   },
   723   remove: function LM_remove() {
   724     PlacesUtils.bookmarks.removeItem(this.id);
   725   },
   727   get children() this._children,
   728   set children(val) {
   729     this._children = val;
   731     // Discard the previous cached nodes, new ones should be generated.
   732     for (let i = 0; i < this._resultObserversList.length; i++) {
   733       let container = this._resultObserversList[i];
   734       this._nodes.delete(container);
   735     }
   737     // Update visited status for each entry.
   738     for (let i = 0; i < this._children.length; i++) {
   739       let child = this._children[i];
   740       asyncHistory.isURIVisited(child.uri,
   741         (function(aURI, aIsVisited) {
   742           this.updateURIVisitedStatus(aURI, aIsVisited);
   743         }).bind(this));
   744     }
   746     return this._children;
   747   },
   749   _isURIVisited: function LM__isURIVisited(aURI) {
   750     for (let i = 0; i < this.children.length; i++) {
   751       if (this.children[i].uri.equals(aURI)) {
   752         return this.children[i].visited;
   753       }
   754     }
   755   },
   757   getNodesForContainer: function LM_getNodesForContainer(aContainerNode)
   758   {
   759     if (this._nodes.has(aContainerNode)) {
   760       return this._nodes.get(aContainerNode);
   761     }
   763     let livemark = this;
   764     let nodes = [];
   765     let now = Date.now() * 1000;
   766     for (let i = 0; i < this._children.length; i++) {
   767       let child = this._children[i];
   768       let node = {
   769         // The QueryInterface is needed cause aContainerNode is a jsval.
   770         // This is required to avoid issues with scriptable wrappers that would
   771         // not allow the view to correctly set expandos.
   772         get parent()
   773           aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode),
   774         get parentResult() this.parent.parentResult,
   775         get uri() child.uri.spec,
   776         get type() Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
   777         get title() child.title,
   778         get accessCount()
   779           Number(livemark._isURIVisited(NetUtil.newURI(this.uri))),
   780         get time() 0,
   781         get icon() "",
   782         get indentLevel() this.parent.indentLevel + 1,
   783         get bookmarkIndex() -1,
   784         get itemId() -1,
   785         get dateAdded() now + i,
   786         get lastModified() now + i,
   787         get tags()
   788           PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", "),
   789         QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode])
   790       };
   791       nodes.push(node);
   792     }
   793     this._nodes.set(aContainerNode, nodes);
   794     return nodes;
   795   },
   797   registerForUpdates: function LM_registerForUpdates(aContainerNode,
   798                                                      aResultObserver)
   799   {
   800     this._resultObservers.set(aContainerNode, aResultObserver);
   801     this._resultObserversList.push(aContainerNode);
   802   },
   804   unregisterForUpdates: function LM_unregisterForUpdates(aContainerNode)
   805   {
   806     this._resultObservers.delete(aContainerNode);
   807     let index = this._resultObserversList.indexOf(aContainerNode);
   808     this._resultObserversList.splice(index, 1);
   810     this._nodes.delete(aContainerNode);
   811   },
   813   _invalidateRegisteredContainers: function LM__invalidateRegisteredContainers()
   814   {
   815     for (let i = 0; i < this._resultObserversList.length; i++) {
   816       let container = this._resultObserversList[i];
   817       let observer = this._resultObservers.get(container);
   818       observer.invalidateContainer(container);
   819     }
   820   },
   822   updateURIVisitedStatus:
   823   function LM_updateURIVisitedStatus(aURI, aVisitedStatus)
   824   {
   825     for (let i = 0; i < this.children.length; i++) {
   826       if (this.children[i].uri.equals(aURI)) {
   827         this.children[i].visited = aVisitedStatus;
   828       }
   829     }
   831     for (let i = 0; i < this._resultObserversList.length; i++) {
   832       let container = this._resultObserversList[i];
   833       let observer = this._resultObservers.get(container);
   834       if (this._nodes.has(container)) {
   835         let nodes = this._nodes.get(container);
   836         for (let j = 0; j < nodes.length; j++) {
   837           let node = nodes[j];
   838           if (node.uri == aURI.spec) {
   839             Services.tm.mainThread.dispatch((function () {
   840               observer.nodeHistoryDetailsChanged(node, 0, aVisitedStatus);
   841             }).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
   842           }
   843         }
   844       }
   845     }
   846   },
   848   /**
   849    * Terminates the livemark entry, cancelling any ongoing load.
   850    * Must be invoked before destroying the entry.
   851    */
   852   terminate: function LM_terminate()
   853   {
   854     // Avoid handling any updateChildren request from now on.
   855     this._terminated = true;
   856     // Clear the list before aborting, since abort() would try to set the
   857     // status and notify about it, but that's not really useful at this point.
   858     this._resultObserversList = [];
   859     this.abort();
   860   },
   862   /**
   863    * Aborts the livemark loading if needed.
   864    */
   865   abort: function LM_abort()
   866   {
   867     this.status = Ci.mozILivemark.STATUS_FAILED;
   868     if (this.loadGroup) {
   869       this.loadGroup.cancel(Cr.NS_BINDING_ABORTED);
   870       this.loadGroup = null;
   871     }
   872   },
   874   QueryInterface: XPCOMUtils.generateQI([
   875     Ci.mozILivemark
   876   ])
   877 }
   879 ////////////////////////////////////////////////////////////////////////////////
   880 //// LivemarkLoadListener
   882 /**
   883  * Object used internally to handle loading a livemark's contents.
   884  *
   885  * @param aLivemark
   886  *        The Livemark that is loading.
   887  */
   888 function LivemarkLoadListener(aLivemark)
   889 {
   890   this._livemark = aLivemark;
   891   this._processor = null;
   892   this._isAborted = false;
   893   this._ttl = EXPIRE_TIME_MS;
   894 }
   896 LivemarkLoadListener.prototype = {
   897   abort: function LLL_abort(aException)
   898   {
   899     if (!this._isAborted) {
   900       this._isAborted = true;
   901       this._livemark.abort();
   902       this._setResourceTTL(ONERROR_EXPIRE_TIME_MS);
   903     }
   904   },
   906   // nsIFeedResultListener
   907   handleResult: function LLL_handleResult(aResult)
   908   {
   909     if (this._isAborted) {
   910       return;
   911     }
   913     try {
   914       // We need this to make sure the item links are safe
   915       let feedPrincipal =
   916         secMan.getSimpleCodebasePrincipal(this._livemark.feedURI);
   918       // Enforce well-formedness because the existing code does
   919       if (!aResult || !aResult.doc || aResult.bozo) {
   920         throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
   921       }
   923       let feed = aResult.doc.QueryInterface(Ci.nsIFeed);
   924       let siteURI = this._livemark.siteURI;
   925       if (feed.link && (!siteURI || !feed.link.equals(siteURI))) {
   926         siteURI = feed.link;
   927         this._livemark.writeSiteURI(siteURI);
   928       }
   930       // Insert feed items.
   931       let livemarkChildren = [];
   932       for (let i = 0; i < feed.items.length; ++i) {
   933         let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);
   934         let uri = entry.link || siteURI;
   935         if (!uri) {
   936           continue;
   937         }
   939         try {
   940           secMan.checkLoadURIWithPrincipal(feedPrincipal, uri, SEC_FLAGS);
   941         }
   942         catch(ex) {
   943           continue;
   944         }
   946         let title = entry.title ? entry.title.plainText() : "";
   947         livemarkChildren.push({ uri: uri, title: title, visited: false });
   948       }
   950       this._livemark.children = livemarkChildren;
   951     }
   952     catch (ex) {
   953       this.abort(ex);
   954     }
   955     finally {
   956       this._processor.listener = null;
   957       this._processor = null;
   958     }
   959   },
   961   onDataAvailable: function LLL_onDataAvailable(aRequest, aContext,
   962                                                 aInputStream, aSourceOffset,
   963                                                 aCount)
   964   {
   965     if (this._processor) {
   966       this._processor.onDataAvailable(aRequest, aContext, aInputStream,
   967                                       aSourceOffset, aCount);
   968     }
   969   },
   971   onStartRequest: function LLL_onStartRequest(aRequest, aContext)
   972   {
   973     if (this._isAborted) {
   974       throw Cr.NS_ERROR_UNEXPECTED;
   975     }
   977     let channel = aRequest.QueryInterface(Ci.nsIChannel);
   978     try {
   979       // Parse feed data as it comes in
   980       this._processor = Cc["@mozilla.org/feed-processor;1"].
   981                         createInstance(Ci.nsIFeedProcessor);
   982       this._processor.listener = this;
   983       this._processor.parseAsync(null, channel.URI);
   984       this._processor.onStartRequest(aRequest, aContext);
   985     }
   986     catch (ex) {
   987       Components.utils.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec);
   988       this.abort(ex);
   989     }
   990   },
   992   onStopRequest: function LLL_onStopRequest(aRequest, aContext, aStatus)
   993   {
   994     if (!Components.isSuccessCode(aStatus)) {
   995       this.abort();
   996       return;
   997     }
   999     // Set an expiration on the livemark, to reloading the data in future.
  1000     try {
  1001       if (this._processor) {
  1002         this._processor.onStopRequest(aRequest, aContext, aStatus);
  1005       // Calculate a new ttl
  1006       let channel = aRequest.QueryInterface(Ci.nsICachingChannel);
  1007       if (channel) {
  1008         let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry);
  1009         if (entryInfo) {
  1010           // nsICacheEntry returns value as seconds.
  1011           let expireTime = entryInfo.expirationTime * 1000;
  1012           let nowTime = Date.now();
  1013           // Note, expireTime can be 0, see bug 383538.
  1014           if (expireTime > nowTime) {
  1015             this._setResourceTTL(Math.max((expireTime - nowTime),
  1016                                           EXPIRE_TIME_MS));
  1017             return;
  1021       this._setResourceTTL(EXPIRE_TIME_MS);
  1023     catch (ex) {
  1024       this.abort(ex);
  1026     finally {
  1027       if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) {
  1028         this._livemark.status = Ci.mozILivemark.STATUS_READY;
  1030       this._livemark.locked = false;
  1031       this._livemark.loadGroup = null;
  1033   },
  1035   _setResourceTTL: function LLL__setResourceTTL(aMilliseconds)
  1037     this._livemark.expireTime = Date.now() + aMilliseconds;
  1038   },
  1040   // nsIInterfaceRequestor
  1041   getInterface: function LLL_getInterface(aIID)
  1043     return this.QueryInterface(aIID);
  1044   },
  1046   // nsISupports
  1047   QueryInterface: XPCOMUtils.generateQI([
  1048     Ci.nsIFeedResultListener
  1049   , Ci.nsIStreamListener
  1050   , Ci.nsIRequestObserver
  1051   , Ci.nsIInterfaceRequestor
  1052   ])
  1055 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);

mercurial