toolkit/components/places/nsTaggingService.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 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 const Cc = Components.classes;
     7 const Ci = Components.interfaces;
     8 const Cr = Components.results;
    10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    11 Components.utils.import("resource://gre/modules/Services.jsm");
    12 Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
    14 const TOPIC_SHUTDOWN = "places-shutdown";
    16 /**
    17  * The Places Tagging Service
    18  */
    19 function TaggingService() {
    20   // Observe bookmarks changes.
    21   PlacesUtils.bookmarks.addObserver(this, false);
    23   // Cleanup on shutdown.
    24   Services.obs.addObserver(this, TOPIC_SHUTDOWN, false);
    25 }
    27 TaggingService.prototype = {
    28   /**
    29    * Creates a tag container under the tags-root with the given name.
    30    *
    31    * @param aTagName
    32    *        the name for the new tag.
    33    * @returns the id of the new tag container.
    34    */
    35   _createTag: function TS__createTag(aTagName) {
    36     var newFolderId = PlacesUtils.bookmarks.createFolder(
    37       PlacesUtils.tagsFolderId, aTagName, PlacesUtils.bookmarks.DEFAULT_INDEX
    38     );
    39     // Add the folder to our local cache, so we can avoid doing this in the
    40     // observer that would have to check itemType.
    41     this._tagFolders[newFolderId] = aTagName;
    43     return newFolderId;
    44   },
    46   /**
    47    * Checks whether the given uri is tagged with the given tag.
    48    *
    49    * @param [in] aURI
    50    *        url to check for
    51    * @param [in] aTagName
    52    *        the tag to check for
    53    * @returns the item id if the URI is tagged with the given tag, -1
    54    *          otherwise.
    55    */
    56   _getItemIdForTaggedURI: function TS__getItemIdForTaggedURI(aURI, aTagName) {
    57     var tagId = this._getItemIdForTag(aTagName);
    58     if (tagId == -1)
    59       return -1;
    60     // Using bookmarks service API for this would be a pain.
    61     // Until tags implementation becomes sane, go the query way.
    62     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
    63                                 .DBConnection;
    64     let stmt = db.createStatement(
    65       "SELECT id FROM moz_bookmarks "
    66     + "WHERE parent = :tag_id "
    67     + "AND fk = (SELECT id FROM moz_places WHERE url = :page_url)"
    68     );
    69     stmt.params.tag_id = tagId;
    70     stmt.params.page_url = aURI.spec;
    71     try {
    72       if (stmt.executeStep()) {
    73         return stmt.row.id;
    74       }
    75     }
    76     finally {
    77       stmt.finalize();
    78     }
    79     return -1;
    80   },
    82   /**
    83    * Returns the folder id for a tag, or -1 if not found.
    84    * @param [in] aTag
    85    *        string tag to search for
    86    * @returns integer id for the bookmark folder for the tag
    87    */
    88   _getItemIdForTag: function TS_getItemIdForTag(aTagName) {
    89     for (var i in this._tagFolders) {
    90       if (aTagName.toLowerCase() == this._tagFolders[i].toLowerCase())
    91         return parseInt(i);
    92     }
    93     return -1;
    94   },
    96   /**
    97    * Makes a proper array of tag objects like  { id: number, name: string }.
    98    *
    99    * @param aTags
   100    *        Array of tags.  Entries can be tag names or concrete item id.
   101    * @return Array of tag objects like { id: number, name: string }.
   102    *
   103    * @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not
   104    *         a valid tag.
   105    */
   106   _convertInputMixedTagsArray: function TS__convertInputMixedTagsArray(aTags)
   107   {
   108     return aTags.map(function (val)
   109     {
   110       let tag = { _self: this };
   111       if (typeof(val) == "number" && this._tagFolders[val]) {
   112         // This is a tag folder id.
   113         tag.id = val;
   114         // We can't know the name at this point, since a previous tag could
   115         // want to change it.
   116         tag.__defineGetter__("name", function () this._self._tagFolders[this.id]);
   117       }
   118       else if (typeof(val) == "string" && val.length > 0) {
   119         // This is a tag name.
   120         tag.name = val;
   121         // We can't know the id at this point, since a previous tag could
   122         // have created it.
   123         tag.__defineGetter__("id", function () this._self._getItemIdForTag(this.name));
   124       }
   125       else {
   126         throw Cr.NS_ERROR_INVALID_ARG;
   127       }
   128       return tag;
   129     }, this);
   130   },
   132   // nsITaggingService
   133   tagURI: function TS_tagURI(aURI, aTags)
   134   {
   135     if (!aURI || !aTags || !Array.isArray(aTags)) {
   136       throw Cr.NS_ERROR_INVALID_ARG;
   137     }
   139     // This also does some input validation.
   140     let tags = this._convertInputMixedTagsArray(aTags);
   142     let taggingService = this;
   143     PlacesUtils.bookmarks.runInBatchMode({
   144       runBatched: function (aUserData)
   145       {
   146         tags.forEach(function (tag)
   147         {
   148           if (tag.id == -1) {
   149             // Tag does not exist yet, create it.
   150             this._createTag(tag.name);
   151           }
   153           if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) {
   154             // The provided URI is not yet tagged, add a tag for it.
   155             // Note that bookmarks under tag containers must have null titles.
   156             PlacesUtils.bookmarks.insertBookmark(
   157               tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, null
   158             );
   159           }
   161           // Try to preserve user's tag name casing.
   162           // Rename the tag container so the Places view matches the most-recent
   163           // user-typed value.
   164           if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) {
   165             // this._tagFolders is updated by the bookmarks observer.
   166             PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name);
   167           }
   168         }, taggingService);
   169       }
   170     }, null);
   171   },
   173   /**
   174    * Removes the tag container from the tags root if the given tag is empty.
   175    *
   176    * @param aTagId
   177    *        the itemId of the tag element under the tags root
   178    */
   179   _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId) {
   180     let count = 0;
   181     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   182                                 .DBConnection;
   183     let stmt = db.createStatement(
   184       "SELECT count(*) AS count FROM moz_bookmarks "
   185     + "WHERE parent = :tag_id"
   186     );
   187     stmt.params.tag_id = aTagId;
   188     try {
   189       if (stmt.executeStep()) {
   190         count = stmt.row.count;
   191       }
   192     }
   193     finally {
   194       stmt.finalize();
   195     }
   197     if (count == 0) {
   198       PlacesUtils.bookmarks.removeItem(aTagId);
   199     }
   200   },
   202   // nsITaggingService
   203   untagURI: function TS_untagURI(aURI, aTags)
   204   {
   205     if (!aURI || (aTags && !Array.isArray(aTags))) {
   206       throw Cr.NS_ERROR_INVALID_ARG;
   207     }
   209     if (!aTags) {
   210       // Passing null should clear all tags for aURI, see the IDL.
   211       // XXXmano: write a perf-sensitive version of this code path...
   212       aTags = this.getTagsForURI(aURI);
   213     }
   215     // This also does some input validation.
   216     let tags = this._convertInputMixedTagsArray(aTags);
   218     let taggingService = this;
   219     PlacesUtils.bookmarks.runInBatchMode({
   220       runBatched: function (aUserData)
   221       {
   222         tags.forEach(function (tag)
   223         {
   224           if (tag.id != -1) {
   225             // A tag could exist.
   226             let itemId = this._getItemIdForTaggedURI(aURI, tag.name);
   227             if (itemId != -1) {
   228               // There is a tagged item.
   229               PlacesUtils.bookmarks.removeItem(itemId);
   230             }
   231           }
   232         }, taggingService);
   233       }
   234     }, null);
   235   },
   237   // nsITaggingService
   238   getURIsForTag: function TS_getURIsForTag(aTagName) {
   239     if (!aTagName || aTagName.length == 0)
   240       throw Cr.NS_ERROR_INVALID_ARG;
   242     let uris = [];
   243     let tagId = this._getItemIdForTag(aTagName);
   244     if (tagId == -1)
   245       return uris;
   247     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   248                                 .DBConnection;
   249     let stmt = db.createStatement(
   250       "SELECT h.url FROM moz_places h "
   251     + "JOIN moz_bookmarks b ON b.fk = h.id "
   252     + "WHERE b.parent = :tag_id "
   253     );
   254     stmt.params.tag_id = tagId;
   255     try {
   256       while (stmt.executeStep()) {
   257         try {
   258           uris.push(Services.io.newURI(stmt.row.url, null, null));
   259         } catch (ex) {}
   260       }
   261     }
   262     finally {
   263       stmt.finalize();
   264     }
   266     return uris;
   267   },
   269   // nsITaggingService
   270   getTagsForURI: function TS_getTagsForURI(aURI, aCount) {
   271     if (!aURI)
   272       throw Cr.NS_ERROR_INVALID_ARG;
   274     var tags = [];
   275     var bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(aURI);
   276     for (var i=0; i < bookmarkIds.length; i++) {
   277       var folderId = PlacesUtils.bookmarks.getFolderIdForItem(bookmarkIds[i]);
   278       if (this._tagFolders[folderId])
   279         tags.push(this._tagFolders[folderId]);
   280     }
   282     // sort the tag list
   283     tags.sort(function(a, b) {
   284         return a.toLowerCase().localeCompare(b.toLowerCase());
   285       });
   286     if (aCount)
   287       aCount.value = tags.length;
   288     return tags;
   289   },
   291   __tagFolders: null, 
   292   get _tagFolders() {
   293     if (!this.__tagFolders) {
   294       this.__tagFolders = [];
   296       let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   297                                   .DBConnection;
   298       let stmt = db.createStatement(
   299         "SELECT id, title FROM moz_bookmarks WHERE parent = :tags_root "
   300       );
   301       stmt.params.tags_root = PlacesUtils.tagsFolderId;
   302       try {
   303         while (stmt.executeStep()) {
   304           this.__tagFolders[stmt.row.id] = stmt.row.title;
   305         }
   306       }
   307       finally {
   308         stmt.finalize();
   309       }
   310     }
   312     return this.__tagFolders;
   313   },
   315   // nsITaggingService
   316   get allTags() {
   317     var allTags = [];
   318     for (var i in this._tagFolders)
   319       allTags.push(this._tagFolders[i]);
   320     // sort the tag list
   321     allTags.sort(function(a, b) {
   322         return a.toLowerCase().localeCompare(b.toLowerCase());
   323       });
   324     return allTags;
   325   },
   327   // nsITaggingService
   328   get hasTags() {
   329     return this._tagFolders.length > 0;
   330   },
   332   // nsIObserver
   333   observe: function TS_observe(aSubject, aTopic, aData) {
   334     if (aTopic == TOPIC_SHUTDOWN) {
   335       PlacesUtils.bookmarks.removeObserver(this);
   336       Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
   337     }
   338   },
   340   /**
   341    * If the only bookmark items associated with aURI are contained in tag
   342    * folders, returns the IDs of those items.  This can be the case if
   343    * the URI was bookmarked and tagged at some point, but the bookmark was
   344    * removed, leaving only the bookmark items in tag folders.  If the URI is
   345    * either properly bookmarked or not tagged just returns and empty array.
   346    *
   347    * @param   aURI
   348    *          A URI (string) that may or may not be bookmarked
   349    * @returns an array of item ids
   350    */
   351   _getTaggedItemIdsIfUnbookmarkedURI:
   352   function TS__getTaggedItemIdsIfUnbookmarkedURI(aURI) {
   353     var itemIds = [];
   354     var isBookmarked = false;
   356     // Using bookmarks service API for this would be a pain.
   357     // Until tags implementation becomes sane, go the query way.
   358     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
   359                                 .DBConnection;
   360     let stmt = db.createStatement(
   361       "SELECT id, parent "
   362     + "FROM moz_bookmarks "
   363     + "WHERE fk = (SELECT id FROM moz_places WHERE url = :page_url)"
   364     );
   365     stmt.params.page_url = aURI.spec;
   366     try {
   367       while (stmt.executeStep() && !isBookmarked) {
   368         if (this._tagFolders[stmt.row.parent]) {
   369           // This is a tag entry.
   370           itemIds.push(stmt.row.id);
   371         }
   372         else {
   373           // This is a real bookmark, so the bookmarked URI is not an orphan.
   374           isBookmarked = true;
   375         }
   376       }
   377     }
   378     finally {
   379       stmt.finalize();
   380     }
   382     return isBookmarked ? [] : itemIds;
   383   },
   385   // nsINavBookmarkObserver
   386   onItemAdded: function TS_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
   387                                        aURI, aTitle) {
   388     // Nothing to do if this is not a tag.
   389     if (aFolderId != PlacesUtils.tagsFolderId ||
   390         aItemType != PlacesUtils.bookmarks.TYPE_FOLDER)
   391       return;
   393     this._tagFolders[aItemId] = aTitle;
   394   },
   396   onItemRemoved: function TS_onItemRemoved(aItemId, aFolderId, aIndex,
   397                                            aItemType, aURI) {
   398     // Item is a tag folder.
   399     if (aFolderId == PlacesUtils.tagsFolderId && this._tagFolders[aItemId]) {
   400       delete this._tagFolders[aItemId];
   401     }
   402     // Item is a bookmark that was removed from a non-tag folder.
   403     else if (aURI && !this._tagFolders[aFolderId]) {
   404       // If the only bookmark items now associated with the bookmark's URI are
   405       // contained in tag folders, the URI is no longer properly bookmarked, so
   406       // untag it.
   407       let itemIds = this._getTaggedItemIdsIfUnbookmarkedURI(aURI);
   408       for (let i = 0; i < itemIds.length; i++) {
   409         try {
   410           PlacesUtils.bookmarks.removeItem(itemIds[i]);
   411         } catch (ex) {}
   412       }
   413     }
   414     // Item is a tag entry.  If this was the last entry for this tag, remove it.
   415     else if (aURI && this._tagFolders[aFolderId]) {
   416       this._removeTagIfEmpty(aFolderId);
   417     }
   418   },
   420   onItemChanged: function TS_onItemChanged(aItemId, aProperty,
   421                                            aIsAnnotationProperty, aNewValue,
   422                                            aLastModified, aItemType) {
   423     if (aProperty == "title" && this._tagFolders[aItemId])
   424       this._tagFolders[aItemId] = aNewValue;
   425   },
   427   onItemMoved: function TS_onItemMoved(aItemId, aOldParent, aOldIndex,
   428                                       aNewParent, aNewIndex, aItemType) {
   429     if (this._tagFolders[aItemId] && PlacesUtils.tagsFolderId == aOldParent &&
   430         PlacesUtils.tagsFolderId != aNewParent)
   431       delete this._tagFolders[aItemId];
   432   },
   434   onItemVisited: function () {},
   435   onBeginUpdateBatch: function () {},
   436   onEndUpdateBatch: function () {},
   438   //////////////////////////////////////////////////////////////////////////////
   439   //// nsISupports
   441   classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"),
   443   _xpcom_factory: XPCOMUtils.generateSingletonFactory(TaggingService),
   445   QueryInterface: XPCOMUtils.generateQI([
   446     Ci.nsITaggingService
   447   , Ci.nsINavBookmarkObserver
   448   , Ci.nsIObserver
   449   ])
   450 };
   453 function TagAutoCompleteResult(searchString, searchResult,
   454                                defaultIndex, errorDescription,
   455                                results, comments) {
   456   this._searchString = searchString;
   457   this._searchResult = searchResult;
   458   this._defaultIndex = defaultIndex;
   459   this._errorDescription = errorDescription;
   460   this._results = results;
   461   this._comments = comments;
   462 }
   464 TagAutoCompleteResult.prototype = {
   466   /**
   467    * The original search string
   468    */
   469   get searchString() {
   470     return this._searchString;
   471   },
   473   /**
   474    * The result code of this result object, either:
   475    *         RESULT_IGNORED   (invalid searchString)
   476    *         RESULT_FAILURE   (failure)
   477    *         RESULT_NOMATCH   (no matches found)
   478    *         RESULT_SUCCESS   (matches found)
   479    */
   480   get searchResult() {
   481     return this._searchResult;
   482   },
   484   /**
   485    * Index of the default item that should be entered if none is selected
   486    */
   487   get defaultIndex() {
   488     return this._defaultIndex;
   489   },
   491   /**
   492    * A string describing the cause of a search failure
   493    */
   494   get errorDescription() {
   495     return this._errorDescription;
   496   },
   498   /**
   499    * The number of matches
   500    */
   501   get matchCount() {
   502     return this._results.length;
   503   },
   505   get typeAheadResult() false,
   507   /**
   508    * Get the value of the result at the given index
   509    */
   510   getValueAt: function PTACR_getValueAt(index) {
   511     return this._results[index];
   512   },
   514   getLabelAt: function PTACR_getLabelAt(index) {
   515     return this.getValueAt(index);
   516   },
   518   /**
   519    * Get the comment of the result at the given index
   520    */
   521   getCommentAt: function PTACR_getCommentAt(index) {
   522     return this._comments[index];
   523   },
   525   /**
   526    * Get the style hint for the result at the given index
   527    */
   528   getStyleAt: function PTACR_getStyleAt(index) {
   529     if (!this._comments[index])
   530       return null;  // not a category label, so no special styling
   532     if (index == 0)
   533       return "suggestfirst";  // category label on first line of results
   535     return "suggesthint";   // category label on any other line of results
   536   },
   538   /**
   539    * Get the image for the result at the given index
   540    */
   541   getImageAt: function PTACR_getImageAt(index) {
   542     return null;
   543   },
   545   /**
   546    * Get the image for the result at the given index
   547    */
   548   getFinalCompleteValueAt: function PTACR_getFinalCompleteValueAt(index) {
   549     return this.getValueAt(index);
   550   },
   552   /**
   553    * Remove the value at the given index from the autocomplete results.
   554    * If removeFromDb is set to true, the value should be removed from
   555    * persistent storage as well.
   556    */
   557   removeValueAt: function PTACR_removeValueAt(index, removeFromDb) {
   558     this._results.splice(index, 1);
   559     this._comments.splice(index, 1);
   560   },
   562   // nsISupports
   563   QueryInterface: XPCOMUtils.generateQI([
   564     Ci.nsIAutoCompleteResult
   565   ])
   566 };
   568 // Implements nsIAutoCompleteSearch
   569 function TagAutoCompleteSearch() {
   570   XPCOMUtils.defineLazyServiceGetter(this, "tagging",
   571                                      "@mozilla.org/browser/tagging-service;1",
   572                                      "nsITaggingService");
   573 }
   575 TagAutoCompleteSearch.prototype = {
   576   _stopped : false, 
   578   /*
   579    * Search for a given string and notify a listener (either synchronously
   580    * or asynchronously) of the result
   581    *
   582    * @param searchString - The string to search for
   583    * @param searchParam - An extra parameter
   584    * @param previousResult - A previous result to use for faster searching
   585    * @param listener - A listener to notify when the search is complete
   586    */
   587   startSearch: function PTACS_startSearch(searchString, searchParam, result, listener) {
   588     var searchResults = this.tagging.allTags;
   589     var results = [];
   590     var comments = [];
   591     this._stopped = false;
   593     // only search on characters for the last tag
   594     var index = Math.max(searchString.lastIndexOf(","), 
   595       searchString.lastIndexOf(";"));
   596     var before = ''; 
   597     if (index != -1) {  
   598       before = searchString.slice(0, index+1);
   599       searchString = searchString.slice(index+1);
   600       // skip past whitespace
   601       var m = searchString.match(/\s+/);
   602       if (m) {
   603          before += m[0];
   604          searchString = searchString.slice(m[0].length);
   605       }
   606     }
   608     if (!searchString.length) {
   609       var newResult = new TagAutoCompleteResult(searchString,
   610         Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 0, "", results, comments);
   611       listener.onSearchResult(self, newResult);
   612       return;
   613     }
   615     var self = this;
   616     // generator: if yields true, not done
   617     function doSearch() {
   618       var i = 0;
   619       while (i < searchResults.length) {
   620         if (self._stopped)
   621           yield false;
   622         // for each match, prepend what the user has typed so far
   623         if (searchResults[i].toLowerCase()
   624                             .indexOf(searchString.toLowerCase()) == 0 &&
   625             comments.indexOf(searchResults[i]) == -1) {
   626           results.push(before + searchResults[i]);
   627           comments.push(searchResults[i]);
   628         }
   630         ++i;
   632         /* TODO: bug 481451
   633          * For each yield we pass a new result to the autocomplete
   634          * listener. The listener appends instead of replacing previous results,
   635          * causing invalid matchCount values.
   636          *
   637          * As a workaround, all tags are searched through in a single batch,
   638          * making this synchronous until the above issue is fixed.
   639          */
   641         /*
   642         // 100 loops per yield
   643         if ((i % 100) == 0) {
   644           var newResult = new TagAutoCompleteResult(searchString,
   645             Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING, 0, "", results, comments);
   646           listener.onSearchResult(self, newResult);
   647           yield true;
   648         }
   649         */
   650       }
   652       let searchResult = results.length > 0 ?
   653                            Ci.nsIAutoCompleteResult.RESULT_SUCCESS :
   654                            Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
   655       var newResult = new TagAutoCompleteResult(searchString, searchResult, 0,
   656                                                 "", results, comments);
   657       listener.onSearchResult(self, newResult);
   658       yield false;
   659     }
   661     // chunk the search results via the generator
   662     var gen = doSearch();
   663     while (gen.next());
   664     gen.close();
   665   },
   667   /**
   668    * Stop an asynchronous search that is in progress
   669    */
   670   stopSearch: function PTACS_stopSearch() {
   671     this._stopped = true;
   672   },
   674   //////////////////////////////////////////////////////////////////////////////
   675   //// nsISupports
   677   classID: Components.ID("{1dcc23b0-d4cb-11dc-9ad6-479d56d89593}"),
   679   _xpcom_factory: XPCOMUtils.generateSingletonFactory(TagAutoCompleteSearch),
   681   QueryInterface: XPCOMUtils.generateQI([
   682     Ci.nsIAutoCompleteSearch
   683   ])
   684 };
   686 let component = [TaggingService, TagAutoCompleteSearch];
   687 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial