toolkit/components/places/BookmarkJSONUtils.jsm

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 this.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ];
     7 const Ci = Components.interfaces;
     8 const Cc = Components.classes;
     9 const Cu = Components.utils;
    10 const Cr = Components.results;
    12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    13 Cu.import("resource://gre/modules/Services.jsm");
    14 Cu.import("resource://gre/modules/NetUtil.jsm");
    15 Cu.import("resource://gre/modules/osfile.jsm");
    16 Cu.import("resource://gre/modules/PlacesUtils.jsm");
    17 Cu.import("resource://gre/modules/Promise.jsm");
    18 Cu.import("resource://gre/modules/Task.jsm");
    20 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
    21   "resource://gre/modules/PlacesBackups.jsm");
    22 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
    23   "resource://gre/modules/Deprecated.jsm");
    25 XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
    26 XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder());
    28 /**
    29  * Generates an hash for the given string.
    30  *
    31  * @note The generated hash is returned in base64 form.  Mind the fact base64
    32  * is case-sensitive if you are going to reuse this code.
    33  */
    34 function generateHash(aString) {
    35   let cryptoHash = Cc["@mozilla.org/security/hash;1"]
    36                      .createInstance(Ci.nsICryptoHash);
    37   cryptoHash.init(Ci.nsICryptoHash.MD5);
    38   let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
    39                        .createInstance(Ci.nsIStringInputStream);
    40   stringStream.data = aString;
    41   cryptoHash.updateFromStream(stringStream, -1);
    42   // base64 allows the '/' char, but we can't use it for filenames.
    43   return cryptoHash.finish(true).replace("/", "-", "g");
    44 }
    46 this.BookmarkJSONUtils = Object.freeze({
    47   /**
    48    * Import bookmarks from a url.
    49    *
    50    * @param aSpec
    51    *        url of the bookmark data.
    52    * @param aReplace
    53    *        Boolean if true, replace existing bookmarks, else merge.
    54    *
    55    * @return {Promise}
    56    * @resolves When the new bookmarks have been created.
    57    * @rejects JavaScript exception.
    58    */
    59   importFromURL: function BJU_importFromURL(aSpec, aReplace) {
    60     return Task.spawn(function* () {
    61       notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
    62       try {
    63         let importer = new BookmarkImporter(aReplace);
    64         yield importer.importFromURL(aSpec);
    66         notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
    67       } catch(ex) {
    68         Cu.reportError("Failed to restore bookmarks from " + aSpec + ": " + ex);
    69         notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
    70       }
    71     });
    72   },
    74   /**
    75    * Restores bookmarks and tags from a JSON file.
    76    * @note any item annotated with "places/excludeFromBackup" won't be removed
    77    *       before executing the restore.
    78    *
    79    * @param aFilePath
    80    *        OS.File path string of bookmarks in JSON format to be restored.
    81    * @param aReplace
    82    *        Boolean if true, replace existing bookmarks, else merge.
    83    *
    84    * @return {Promise}
    85    * @resolves When the new bookmarks have been created.
    86    * @rejects JavaScript exception.
    87    * @deprecated passing an nsIFile is deprecated
    88    */
    89   importFromFile: function BJU_importFromFile(aFilePath, aReplace) {
    90     if (aFilePath instanceof Ci.nsIFile) {
    91       Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.importFromFile " +
    92                          "is deprecated. Please use an OS.File path string instead.",
    93                          "https://developer.mozilla.org/docs/JavaScript_OS.File");
    94       aFilePath = aFilePath.path;
    95     }
    97     return Task.spawn(function* () {
    98       notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
    99       try {
   100         if (!(yield OS.File.exists(aFilePath)))
   101           throw new Error("Cannot restore from nonexisting json file");
   103         let importer = new BookmarkImporter(aReplace);
   104         yield importer.importFromURL(OS.Path.toFileURI(aFilePath));
   106         notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
   107       } catch(ex) {
   108         Cu.reportError("Failed to restore bookmarks from " + aFilePath + ": " + ex);
   109         notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
   110         throw ex;
   111       }
   112     });
   113   },
   115   /**
   116    * Serializes bookmarks using JSON, and writes to the supplied file path.
   117    *
   118    * @param aFilePath
   119    *        OS.File path string for the "bookmarks.json" file to be created.
   120    * @param [optional] aOptions
   121    *        Object containing options for the export:
   122    *         - failIfHashIs: if the generated file would have the same hash
   123    *                         defined here, will reject with ex.becauseSameHash
   124    * @return {Promise}
   125    * @resolves once the file has been created, to an object with the
   126    *           following properties:
   127    *            - count: number of exported bookmarks
   128    *            - hash: file hash for contents comparison
   129    * @rejects JavaScript exception.
   130    * @deprecated passing an nsIFile is deprecated
   131    */
   132   exportToFile: function BJU_exportToFile(aFilePath, aOptions={}) {
   133     if (aFilePath instanceof Ci.nsIFile) {
   134       Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.exportToFile " +
   135                          "is deprecated. Please use an OS.File path string instead.",
   136                          "https://developer.mozilla.org/docs/JavaScript_OS.File");
   137       aFilePath = aFilePath.path;
   138     }
   139     return Task.spawn(function* () {
   140       let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
   141       let startTime = Date.now();
   142       let jsonString = JSON.stringify(bookmarks);
   143       // Report the time taken to convert the tree to JSON.
   144       try {
   145         Services.telemetry
   146                 .getHistogramById("PLACES_BACKUPS_TOJSON_MS")
   147                 .add(Date.now() - startTime);
   148       } catch (ex) {
   149         Components.utils.reportError("Unable to report telemetry.");
   150       }
   152       startTime = Date.now();
   153       let hash = generateHash(jsonString);
   154       // Report the time taken to generate the hash.
   155       try {
   156         Services.telemetry
   157                 .getHistogramById("PLACES_BACKUPS_HASHING_MS")
   158                 .add(Date.now() - startTime);
   159       } catch (ex) {
   160         Components.utils.reportError("Unable to report telemetry.");
   161       }
   163       if (hash === aOptions.failIfHashIs) {
   164         let e = new Error("Hash conflict");
   165         e.becauseSameHash = true;
   166         throw e;
   167       }
   169       // Do not write to the tmp folder, otherwise if it has a different
   170       // filesystem writeAtomic will fail.  Eventual dangling .tmp files should
   171       // be cleaned up by the caller.
   172       yield OS.File.writeAtomic(aFilePath, jsonString,
   173                                 { tmpPath: OS.Path.join(aFilePath + ".tmp") });
   174       return { count: count, hash: hash };
   175     });
   176   }
   177 });
   179 function BookmarkImporter(aReplace) {
   180   this._replace = aReplace;
   181 }
   182 BookmarkImporter.prototype = {
   183   /**
   184    * Import bookmarks from a url.
   185    *
   186    * @param aSpec
   187    *        url of the bookmark data.
   188    *
   189    * @return {Promise}
   190    * @resolves When the new bookmarks have been created.
   191    * @rejects JavaScript exception.
   192    */
   193   importFromURL: function BI_importFromURL(aSpec) {
   194     let deferred = Promise.defer();
   196     let streamObserver = {
   197       onStreamComplete: function (aLoader, aContext, aStatus, aLength,
   198                                   aResult) {
   199         let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
   200                         createInstance(Ci.nsIScriptableUnicodeConverter);
   201         converter.charset = "UTF-8";
   203         try {
   204           let jsonString = converter.convertFromByteArray(aResult,
   205                                                           aResult.length);
   206           deferred.resolve(this.importFromJSON(jsonString));
   207         } catch (ex) {
   208           Cu.reportError("Failed to import from URL: " + ex);
   209           deferred.reject(ex);
   210           throw ex;
   211         }
   212       }.bind(this)
   213     };
   215     try {
   216       let channel = Services.io.newChannelFromURI(NetUtil.newURI(aSpec));
   217       let streamLoader = Cc["@mozilla.org/network/stream-loader;1"].
   218                          createInstance(Ci.nsIStreamLoader);
   220       streamLoader.init(streamObserver);
   221       channel.asyncOpen(streamLoader, channel);
   222     } catch (ex) {
   223       deferred.reject(ex);
   224     }
   226     return deferred.promise;
   227   },
   229   /**
   230    * Import bookmarks from a JSON string.
   231    *
   232    * @param aString
   233    *        JSON string of serialized bookmark data.
   234    */
   235   importFromJSON: function BI_importFromJSON(aString) {
   236     let deferred = Promise.defer();
   237     let nodes =
   238       PlacesUtils.unwrapNodes(aString, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
   240     if (nodes.length == 0 || !nodes[0].children ||
   241         nodes[0].children.length == 0) {
   242       deferred.resolve(); // Nothing to restore
   243     } else {
   244       // Ensure tag folder gets processed last
   245       nodes[0].children.sort(function sortRoots(aNode, bNode) {
   246         return (aNode.root && aNode.root == "tagsFolder") ? 1 :
   247                (bNode.root && bNode.root == "tagsFolder") ? -1 : 0;
   248       });
   250       let batch = {
   251         nodes: nodes[0].children,
   252         runBatched: function runBatched() {
   253           if (this._replace) {
   254             // Get roots excluded from the backup, we will not remove them
   255             // before restoring.
   256             let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
   257                                  PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
   258             // Delete existing children of the root node, excepting:
   259             // 1. special folders: delete the child nodes
   260             // 2. tags folder: untag via the tagging api
   261             let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
   262                                                    false, false).root;
   263             let childIds = [];
   264             for (let i = 0; i < root.childCount; i++) {
   265               let childId = root.getChild(i).itemId;
   266               if (excludeItems.indexOf(childId) == -1 &&
   267                   childId != PlacesUtils.tagsFolderId) {
   268                 childIds.push(childId);
   269               }
   270             }
   271             root.containerOpen = false;
   273             for (let i = 0; i < childIds.length; i++) {
   274               let rootItemId = childIds[i];
   275               if (PlacesUtils.isRootItem(rootItemId)) {
   276                 PlacesUtils.bookmarks.removeFolderChildren(rootItemId);
   277               } else {
   278                 PlacesUtils.bookmarks.removeItem(rootItemId);
   279               }
   280             }
   281           }
   283           let searchIds = [];
   284           let folderIdMap = [];
   286           for (let node of batch.nodes) {
   287             if (!node.children || node.children.length == 0)
   288               continue; // Nothing to restore for this root
   290             if (node.root) {
   291               let container = PlacesUtils.placesRootId; // Default to places root
   292               switch (node.root) {
   293                 case "bookmarksMenuFolder":
   294                   container = PlacesUtils.bookmarksMenuFolderId;
   295                   break;
   296                 case "tagsFolder":
   297                   container = PlacesUtils.tagsFolderId;
   298                   break;
   299                 case "unfiledBookmarksFolder":
   300                   container = PlacesUtils.unfiledBookmarksFolderId;
   301                   break;
   302                 case "toolbarFolder":
   303                   container = PlacesUtils.toolbarFolderId;
   304                   break;
   305               }
   307               // Insert the data into the db
   308               for (let child of node.children) {
   309                 let index = child.index;
   310                 let [folders, searches] =
   311                   this.importJSONNode(child, container, index, 0);
   312                 for (let i = 0; i < folders.length; i++) {
   313                   if (folders[i])
   314                     folderIdMap[i] = folders[i];
   315                 }
   316                 searchIds = searchIds.concat(searches);
   317               }
   318             } else {
   319               this.importJSONNode(
   320                 node, PlacesUtils.placesRootId, node.index, 0);
   321             }
   322           }
   324           // Fixup imported place: uris that contain folders
   325           searchIds.forEach(function(aId) {
   326             let oldURI = PlacesUtils.bookmarks.getBookmarkURI(aId);
   327             let uri = fixupQuery(oldURI, folderIdMap);
   328             if (!uri.equals(oldURI)) {
   329               PlacesUtils.bookmarks.changeBookmarkURI(aId, uri);
   330             }
   331           });
   333           deferred.resolve();
   334         }.bind(this)
   335       };
   337       PlacesUtils.bookmarks.runInBatchMode(batch, null);
   338     }
   339     return deferred.promise;
   340   },
   342   /**
   343    * Takes a JSON-serialized node and inserts it into the db.
   344    *
   345    * @param aData
   346    *        The unwrapped data blob of dropped or pasted data.
   347    * @param aContainer
   348    *        The container the data was dropped or pasted into
   349    * @param aIndex
   350    *        The index within the container the item was dropped or pasted at
   351    * @return an array containing of maps of old folder ids to new folder ids,
   352    *         and an array of saved search ids that need to be fixed up.
   353    *         eg: [[[oldFolder1, newFolder1]], [search1]]
   354    */
   355   importJSONNode: function BI_importJSONNode(aData, aContainer, aIndex,
   356                                              aGrandParentId) {
   357     let folderIdMap = [];
   358     let searchIds = [];
   359     let id = -1;
   360     switch (aData.type) {
   361       case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
   362         if (aContainer == PlacesUtils.tagsFolderId) {
   363           // Node is a tag
   364           if (aData.children) {
   365             aData.children.forEach(function(aChild) {
   366               try {
   367                 PlacesUtils.tagging.tagURI(
   368                   NetUtil.newURI(aChild.uri), [aData.title]);
   369               } catch (ex) {
   370                 // Invalid tag child, skip it
   371               }
   372             });
   373             return [folderIdMap, searchIds];
   374           }
   375         } else if (aData.annos &&
   376                    aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
   377           // Node is a livemark
   378           let feedURI = null;
   379           let siteURI = null;
   380           aData.annos = aData.annos.filter(function(aAnno) {
   381             switch (aAnno.name) {
   382               case PlacesUtils.LMANNO_FEEDURI:
   383                 feedURI = NetUtil.newURI(aAnno.value);
   384                 return false;
   385               case PlacesUtils.LMANNO_SITEURI:
   386                 siteURI = NetUtil.newURI(aAnno.value);
   387                 return false;
   388               default:
   389                 return true;
   390             }
   391           });
   393           if (feedURI) {
   394             PlacesUtils.livemarks.addLivemark({
   395               title: aData.title,
   396               feedURI: feedURI,
   397               parentId: aContainer,
   398               index: aIndex,
   399               lastModified: aData.lastModified,
   400               siteURI: siteURI
   401             }).then(function (aLivemark) {
   402               let id = aLivemark.id;
   403               if (aData.dateAdded)
   404                 PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
   405               if (aData.annos && aData.annos.length)
   406                 PlacesUtils.setAnnotationsForItem(id, aData.annos);
   407             }, Cu.reportError);
   408           }
   409         } else {
   410           id = PlacesUtils.bookmarks.createFolder(
   411                  aContainer, aData.title, aIndex);
   412           folderIdMap[aData.id] = id;
   413           // Process children
   414           if (aData.children) {
   415             for (let i = 0; i < aData.children.length; i++) {
   416               let child = aData.children[i];
   417               let [folders, searches] =
   418                 this.importJSONNode(child, id, i, aContainer);
   419               for (let j = 0; j < folders.length; j++) {
   420                 if (folders[j])
   421                   folderIdMap[j] = folders[j];
   422               }
   423               searchIds = searchIds.concat(searches);
   424             }
   425           }
   426         }
   427         break;
   428       case PlacesUtils.TYPE_X_MOZ_PLACE:
   429         id = PlacesUtils.bookmarks.insertBookmark(
   430                aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title);
   431         if (aData.keyword)
   432           PlacesUtils.bookmarks.setKeywordForBookmark(id, aData.keyword);
   433         if (aData.tags) {
   434           // TODO (bug 967196) the tagging service should trim by itself.
   435           let tags = aData.tags.split(",").map(tag => tag.trim());
   436           if (tags.length)
   437             PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags);
   438         }
   439         if (aData.charset) {
   440           PlacesUtils.annotations.setPageAnnotation(
   441             NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset,
   442             0, Ci.nsIAnnotationService.EXPIRE_NEVER);
   443         }
   444         if (aData.uri.substr(0, 6) == "place:")
   445           searchIds.push(id);
   446         if (aData.icon) {
   447           try {
   448             // Create a fake faviconURI to use (FIXME: bug 523932)
   449             let faviconURI = NetUtil.newURI("fake-favicon-uri:" + aData.uri);
   450             PlacesUtils.favicons.replaceFaviconDataFromDataURL(
   451               faviconURI, aData.icon, 0);
   452             PlacesUtils.favicons.setAndFetchFaviconForPage(
   453               NetUtil.newURI(aData.uri), faviconURI, false,
   454               PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
   455           } catch (ex) {
   456             Components.utils.reportError("Failed to import favicon data:" + ex);
   457           }
   458         }
   459         if (aData.iconUri) {
   460           try {
   461             PlacesUtils.favicons.setAndFetchFaviconForPage(
   462               NetUtil.newURI(aData.uri), NetUtil.newURI(aData.iconUri), false,
   463               PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
   464           } catch (ex) {
   465             Components.utils.reportError("Failed to import favicon URI:" + ex);
   466           }
   467         }
   468         break;
   469       case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
   470         id = PlacesUtils.bookmarks.insertSeparator(aContainer, aIndex);
   471         break;
   472       default:
   473         // Unknown node type
   474     }
   476     // Set generic properties, valid for all nodes
   477     if (id != -1 && aContainer != PlacesUtils.tagsFolderId &&
   478         aGrandParentId != PlacesUtils.tagsFolderId) {
   479       if (aData.dateAdded)
   480         PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
   481       if (aData.lastModified)
   482         PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified);
   483       if (aData.annos && aData.annos.length)
   484         PlacesUtils.setAnnotationsForItem(id, aData.annos);
   485     }
   487     return [folderIdMap, searchIds];
   488   }
   489 }
   491 function notifyObservers(topic) {
   492   Services.obs.notifyObservers(null, topic, "json");
   493 }
   495 /**
   496  * Replaces imported folder ids with their local counterparts in a place: URI.
   497  *
   498  * @param   aURI
   499  *          A place: URI with folder ids.
   500  * @param   aFolderIdMap
   501  *          An array mapping old folder id to new folder ids.
   502  * @returns the fixed up URI if all matched. If some matched, it returns
   503  *          the URI with only the matching folders included. If none matched
   504  *          it returns the input URI unchanged.
   505  */
   506 function fixupQuery(aQueryURI, aFolderIdMap) {
   507   let convert = function(str, p1, offset, s) {
   508     return "folder=" + aFolderIdMap[p1];
   509   }
   510   let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);
   512   return NetUtil.newURI(stringURI);
   513 }

mercurial