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