1/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ 5 6this.EXPORTED_SYMBOLS = ["PlacesUIUtils"]; 7 8var Ci = Components.interfaces; 9var Cc = Components.classes; 10var Cr = Components.results; 11var Cu = Components.utils; 12 13Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 14Cu.import("resource://gre/modules/Services.jsm"); 15Cu.import("resource://gre/modules/Timer.jsm"); 16 17Cu.import("resource://gre/modules/PlacesUtils.jsm"); 18XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", 19 "resource://gre/modules/PluralForm.jsm"); 20XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", 21 "resource://gre/modules/PrivateBrowsingUtils.jsm"); 22XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 23 "resource://gre/modules/NetUtil.jsm"); 24XPCOMUtils.defineLazyModuleGetter(this, "Task", 25 "resource://gre/modules/Task.jsm"); 26XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", 27 "resource:///modules/RecentWindow.jsm"); 28 29// PlacesUtils exposes multiple symbols, so we can't use defineLazyModuleGetter. 30Cu.import("resource://gre/modules/PlacesUtils.jsm"); 31 32XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions", 33 "resource://gre/modules/PlacesTransactions.jsm"); 34 35XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", 36 "resource://gre/modules/CloudSync.jsm"); 37 38XPCOMUtils.defineLazyModuleGetter(this, "Weave", 39 "resource://services-sync/main.js"); 40 41const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; 42const FAVICON_REQUEST_TIMEOUT = 60 * 1000; 43// Map from windows to arrays of data about pending favicon loads. 44let gFaviconLoadDataMap = new Map(); 45 46// copied from utilityOverlay.js 47const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab"; 48 49// This function isn't public both because it's synchronous and because it is 50// going to be removed in bug 1072833. 51function IsLivemark(aItemId) { 52 // Since this check may be done on each dragover event, it's worth maintaining 53 // a cache. 54 let self = IsLivemark; 55 if (!("ids" in self)) { 56 const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI; 57 58 let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO); 59 self.ids = new Set(idsVec); 60 61 let obs = Object.freeze({ 62 QueryInterface: XPCOMUtils.generateQI(Ci.nsIAnnotationObserver), 63 64 onItemAnnotationSet(itemId, annoName) { 65 if (annoName == LIVEMARK_ANNO) 66 self.ids.add(itemId); 67 }, 68 69 onItemAnnotationRemoved(itemId, annoName) { 70 // If annoName is set to an empty string, the item is gone. 71 if (annoName == LIVEMARK_ANNO || annoName == "") 72 self.ids.delete(itemId); 73 }, 74 75 onPageAnnotationSet() { }, 76 onPageAnnotationRemoved() { }, 77 }); 78 PlacesUtils.annotations.addObserver(obs); 79 PlacesUtils.registerShutdownFunction(() => { 80 PlacesUtils.annotations.removeObserver(obs); 81 }); 82 } 83 return self.ids.has(aItemId); 84} 85 86let InternalFaviconLoader = { 87 /** 88 * This gets called for every inner window that is destroyed. 89 * In the parent process, we process the destruction ourselves. In the child process, 90 * we notify the parent which will then process it based on that message. 91 */ 92 observe(subject, topic, data) { 93 let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; 94 this.removeRequestsForInner(innerWindowID); 95 }, 96 97 /** 98 * Actually cancel the request, and clear the timeout for cancelling it. 99 */ 100 _cancelRequest({uri, innerWindowID, timerID, callback}, reason) { 101 // Break cycle 102 let request = callback.request; 103 delete callback.request; 104 // Ensure we don't time out. 105 clearTimeout(timerID); 106 try { 107 request.cancel(); 108 } catch (ex) { 109 Cu.reportError("When cancelling a request for " + uri.spec + " because " + reason + ", it was already canceled!"); 110 } 111 }, 112 113 /** 114 * Called for every inner that gets destroyed, only in the parent process. 115 */ 116 removeRequestsForInner(innerID) { 117 for (let [window, loadDataForWindow] of gFaviconLoadDataMap) { 118 let newLoadDataForWindow = loadDataForWindow.filter(loadData => { 119 let innerWasDestroyed = loadData.innerWindowID == innerID; 120 if (innerWasDestroyed) { 121 this._cancelRequest(loadData, "the inner window was destroyed or a new favicon was loaded for it"); 122 } 123 // Keep the items whose inner is still alive. 124 return !innerWasDestroyed; 125 }); 126 // Map iteration with for...of is safe against modification, so 127 // now just replace the old value: 128 gFaviconLoadDataMap.set(window, newLoadDataForWindow); 129 } 130 }, 131 132 /** 133 * Called when a toplevel chrome window unloads. We use this to tidy up after ourselves, 134 * avoid leaks, and cancel any remaining requests. The last part should in theory be 135 * handled by the inner-window-destroyed handlers. We clean up just to be on the safe side. 136 */ 137 onUnload(win) { 138 let loadDataForWindow = gFaviconLoadDataMap.get(win); 139 if (loadDataForWindow) { 140 for (let loadData of loadDataForWindow) { 141 this._cancelRequest(loadData, "the chrome window went away"); 142 } 143 } 144 gFaviconLoadDataMap.delete(win); 145 }, 146 147 /** 148 * Remove a particular favicon load's loading data from our map tracking 149 * load data per chrome window. 150 * 151 * @param win 152 * the chrome window in which we should look for this load 153 * @param filterData ({innerWindowID, uri, callback}) 154 * the data we should use to find this particular load to remove. 155 * 156 * @return the loadData object we removed, or null if we didn't find any. 157 */ 158 _removeLoadDataFromWindowMap(win, {innerWindowID, uri, callback}) { 159 let loadDataForWindow = gFaviconLoadDataMap.get(win); 160 if (loadDataForWindow) { 161 let itemIndex = loadDataForWindow.findIndex(loadData => { 162 return loadData.innerWindowID == innerWindowID && 163 loadData.uri.equals(uri) && 164 loadData.callback.request == callback.request; 165 }); 166 if (itemIndex != -1) { 167 let loadData = loadDataForWindow[itemIndex]; 168 loadDataForWindow.splice(itemIndex, 1); 169 return loadData; 170 } 171 } 172 return null; 173 }, 174 175 /** 176 * Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling 177 * information when the request succeeds. Note that right now there are some edge-cases, 178 * such as about: URIs with chrome:// favicons where the success callback is not invoked. 179 * This is OK: we will 'cancel' the request after the timeout (or when the window goes 180 * away) but that will be a no-op in such cases. 181 */ 182 _makeCompletionCallback(win, id) { 183 return { 184 onComplete(uri) { 185 let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, { 186 uri, 187 innerWindowID: id, 188 callback: this, 189 }); 190 if (loadData) { 191 clearTimeout(loadData.timerID); 192 } 193 delete this.request; 194 }, 195 }; 196 }, 197 198 ensureInitialized() { 199 if (this._initialized) { 200 return; 201 } 202 this._initialized = true; 203 204 Services.obs.addObserver(this, "inner-window-destroyed", false); 205 Services.ppmm.addMessageListener("Toolkit:inner-window-destroyed", msg => { 206 this.removeRequestsForInner(msg.data); 207 }); 208 }, 209 210 loadFavicon(browser, principal, uri) { 211 this.ensureInitialized(); 212 let win = browser.ownerGlobal; 213 if (!gFaviconLoadDataMap.has(win)) { 214 gFaviconLoadDataMap.set(win, []); 215 let unloadHandler = event => { 216 let doc = event.target; 217 let eventWin = doc.defaultView; 218 if (eventWin == win) { 219 win.removeEventListener("unload", unloadHandler); 220 this.onUnload(win); 221 } 222 }; 223 win.addEventListener("unload", unloadHandler, true); 224 } 225 226 let {innerWindowID, currentURI} = browser; 227 228 // Immediately cancel any earlier requests 229 this.removeRequestsForInner(innerWindowID); 230 231 // First we do the actual setAndFetch call: 232 let loadType = PrivateBrowsingUtils.isWindowPrivate(win) 233 ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE 234 : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE; 235 let callback = this._makeCompletionCallback(win, innerWindowID); 236 let request = PlacesUtils.favicons.setAndFetchFaviconForPage(currentURI, uri, false, 237 loadType, callback, principal); 238 239 // Now register the result so we can cancel it if/when necessary. 240 if (!request) { 241 // The favicon service can return with success but no-op (and leave request 242 // as null) if the icon is the same as the page (e.g. for images) or if it is 243 // the favicon for an error page. In this case, we do not need to do anything else. 244 return; 245 } 246 callback.request = request; 247 let loadData = {innerWindowID, uri, callback}; 248 loadData.timerID = setTimeout(() => { 249 this._cancelRequest(loadData, "it timed out"); 250 this._removeLoadDataFromWindowMap(win, loadData); 251 }, FAVICON_REQUEST_TIMEOUT); 252 let loadDataForWindow = gFaviconLoadDataMap.get(win); 253 loadDataForWindow.push(loadData); 254 }, 255}; 256 257this.PlacesUIUtils = { 258 ORGANIZER_LEFTPANE_VERSION: 7, 259 ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder", 260 ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery", 261 262 LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar", 263 DESCRIPTION_ANNO: "bookmarkProperties/description", 264 265 /** 266 * Makes a URI from a spec, and do fixup 267 * @param aSpec 268 * The string spec of the URI 269 * @return A URI object for the spec. 270 */ 271 createFixedURI: function PUIU_createFixedURI(aSpec) { 272 return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE); 273 }, 274 275 getFormattedString: function PUIU_getFormattedString(key, params) { 276 return bundle.formatStringFromName(key, params, params.length); 277 }, 278 279 /** 280 * Get a localized plural string for the specified key name and numeric value 281 * substituting parameters. 282 * 283 * @param aKey 284 * String, key for looking up the localized string in the bundle 285 * @param aNumber 286 * Number based on which the final localized form is looked up 287 * @param aParams 288 * Array whose items will substitute #1, #2,... #n parameters 289 * in the string. 290 * 291 * @see https://developer.mozilla.org/en/Localization_and_Plurals 292 * @return The localized plural string. 293 */ 294 getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) { 295 let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey)); 296 297 // Replace #1 with aParams[0], #2 with aParams[1], and so on. 298 return str.replace(/\#(\d+)/g, function (matchedId, matchedNumber) { 299 let param = aParams[parseInt(matchedNumber, 10) - 1]; 300 return param !== undefined ? param : matchedId; 301 }); 302 }, 303 304 getString: function PUIU_getString(key) { 305 return bundle.GetStringFromName(key); 306 }, 307 308 get _copyableAnnotations() { 309 return [ 310 this.DESCRIPTION_ANNO, 311 this.LOAD_IN_SIDEBAR_ANNO, 312 PlacesUtils.READ_ONLY_ANNO, 313 ]; 314 }, 315 316 /** 317 * Get a transaction for copying a uri item (either a bookmark or a history 318 * entry) from one container to another. 319 * 320 * @param aData 321 * JSON object of dropped or pasted item properties 322 * @param aContainer 323 * The container being copied into 324 * @param aIndex 325 * The index within the container the item is copied to 326 * @return A nsITransaction object that performs the copy. 327 * 328 * @note Since a copy creates a completely new item, only some internal 329 * annotations are synced from the old one. 330 * @see this._copyableAnnotations for the list of copyable annotations. 331 */ 332 _getURIItemCopyTransaction: 333 function PUIU__getURIItemCopyTransaction(aData, aContainer, aIndex) 334 { 335 let transactions = []; 336 if (aData.dateAdded) { 337 transactions.push( 338 new PlacesEditItemDateAddedTransaction(null, aData.dateAdded) 339 ); 340 } 341 if (aData.lastModified) { 342 transactions.push( 343 new PlacesEditItemLastModifiedTransaction(null, aData.lastModified) 344 ); 345 } 346 347 let annos = []; 348 if (aData.annos) { 349 annos = aData.annos.filter(function (aAnno) { 350 return this._copyableAnnotations.includes(aAnno.name); 351 }, this); 352 } 353 354 // There's no need to copy the keyword since it's bound to the bookmark url. 355 return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri), 356 aContainer, aIndex, aData.title, 357 null, annos, transactions); 358 }, 359 360 /** 361 * Gets a transaction for copying (recursively nesting to include children) 362 * a folder (or container) and its contents from one folder to another. 363 * 364 * @param aData 365 * Unwrapped dropped folder data - Obj containing folder and children 366 * @param aContainer 367 * The container we are copying into 368 * @param aIndex 369 * The index in the destination container to insert the new items 370 * @return A nsITransaction object that will perform the copy. 371 * 372 * @note Since a copy creates a completely new item, only some internal 373 * annotations are synced from the old one. 374 * @see this._copyableAnnotations for the list of copyable annotations. 375 */ 376 _getFolderCopyTransaction(aData, aContainer, aIndex) { 377 function getChildItemsTransactions(aRoot) { 378 let transactions = []; 379 let index = aIndex; 380 for (let i = 0; i < aRoot.childCount; ++i) { 381 let child = aRoot.getChild(i); 382 // Temporary hacks until we switch to PlacesTransactions.jsm. 383 let isLivemark = 384 PlacesUtils.annotations.itemHasAnnotation(child.itemId, 385 PlacesUtils.LMANNO_FEEDURI); 386 let [node] = PlacesUtils.unwrapNodes( 387 PlacesUtils.wrapNode(child, PlacesUtils.TYPE_X_MOZ_PLACE, isLivemark), 388 PlacesUtils.TYPE_X_MOZ_PLACE 389 ); 390 391 // Make sure that items are given the correct index, this will be 392 // passed by the transaction manager to the backend for the insertion. 393 // Insertion behaves differently for DEFAULT_INDEX (append). 394 if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) { 395 index = i; 396 } 397 398 if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { 399 if (node.livemark && node.annos) { 400 transactions.push( 401 PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index) 402 ); 403 } 404 else { 405 transactions.push( 406 PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index) 407 ); 408 } 409 } 410 else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) { 411 transactions.push(new PlacesCreateSeparatorTransaction(-1, index)); 412 } 413 else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) { 414 transactions.push( 415 PlacesUIUtils._getURIItemCopyTransaction(node, -1, index) 416 ); 417 } 418 else { 419 throw new Error("Unexpected item under a bookmarks folder"); 420 } 421 } 422 return transactions; 423 } 424 425 if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder. 426 let transactions = []; 427 if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { 428 let {root} = PlacesUtils.getFolderContents(aData.id, false, false); 429 let urls = PlacesUtils.getURLsForContainerNode(root); 430 root.containerOpen = false; 431 for (let { uri } of urls) { 432 transactions.push( 433 new PlacesTagURITransaction(NetUtil.newURI(uri), [aData.title]) 434 ); 435 } 436 } 437 return new PlacesAggregatedTransaction("addTags", transactions); 438 } 439 440 if (aData.livemark && aData.annos) { // Copying a livemark. 441 return this._getLivemarkCopyTransaction(aData, aContainer, aIndex); 442 } 443 444 let {root} = PlacesUtils.getFolderContents(aData.id, false, false); 445 let transactions = getChildItemsTransactions(root); 446 root.containerOpen = false; 447 448 if (aData.dateAdded) { 449 transactions.push( 450 new PlacesEditItemDateAddedTransaction(null, aData.dateAdded) 451 ); 452 } 453 if (aData.lastModified) { 454 transactions.push( 455 new PlacesEditItemLastModifiedTransaction(null, aData.lastModified) 456 ); 457 } 458 459 let annos = []; 460 if (aData.annos) { 461 annos = aData.annos.filter(function (aAnno) { 462 return this._copyableAnnotations.includes(aAnno.name); 463 }, this); 464 } 465 466 return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex, 467 annos, transactions); 468 }, 469 470 /** 471 * Gets a transaction for copying a live bookmark item from one container to 472 * another. 473 * 474 * @param aData 475 * Unwrapped live bookmarkmark data 476 * @param aContainer 477 * The container we are copying into 478 * @param aIndex 479 * The index in the destination container to insert the new items 480 * @return A nsITransaction object that will perform the copy. 481 * 482 * @note Since a copy creates a completely new item, only some internal 483 * annotations are synced from the old one. 484 * @see this._copyableAnnotations for the list of copyable annotations. 485 */ 486 _getLivemarkCopyTransaction: 487 function PUIU__getLivemarkCopyTransaction(aData, aContainer, aIndex) 488 { 489 if (!aData.livemark || !aData.annos) { 490 throw new Error("node is not a livemark"); 491 } 492 493 let feedURI, siteURI; 494 let annos = []; 495 if (aData.annos) { 496 annos = aData.annos.filter(function (aAnno) { 497 if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) { 498 feedURI = PlacesUtils._uri(aAnno.value); 499 } 500 else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) { 501 siteURI = PlacesUtils._uri(aAnno.value); 502 } 503 return this._copyableAnnotations.includes(aAnno.name) 504 }, this); 505 } 506 507 return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title, 508 aContainer, aIndex, annos); 509 }, 510 511 /** 512 * Constructs a Transaction for the drop or paste of a blob of data into 513 * a container. 514 * @param data 515 * The unwrapped data blob of dropped or pasted data. 516 * @param type 517 * The content type of the data 518 * @param container 519 * The container the data was dropped or pasted into 520 * @param index 521 * The index within the container the item was dropped or pasted at 522 * @param copy 523 * The drag action was copy, so don't move folders or links. 524 * @return An object implementing nsITransaction that can perform 525 * the move/insert. 526 */ 527 makeTransaction: 528 function PUIU_makeTransaction(data, type, container, index, copy) 529 { 530 switch (data.type) { 531 case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: 532 if (copy) { 533 return this._getFolderCopyTransaction(data, container, index); 534 } 535 536 // Otherwise move the item. 537 return new PlacesMoveItemTransaction(data.id, container, index); 538 case PlacesUtils.TYPE_X_MOZ_PLACE: 539 if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked. 540 return this._getURIItemCopyTransaction(data, container, index); 541 } 542 543 // Otherwise move the item. 544 return new PlacesMoveItemTransaction(data.id, container, index); 545 case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: 546 if (copy) { 547 // There is no data in a separator, so copying it just amounts to 548 // inserting a new separator. 549 return new PlacesCreateSeparatorTransaction(container, index); 550 } 551 552 // Otherwise move the item. 553 return new PlacesMoveItemTransaction(data.id, container, index); 554 default: 555 if (type == PlacesUtils.TYPE_X_MOZ_URL || 556 type == PlacesUtils.TYPE_UNICODE || 557 type == TAB_DROP_TYPE) { 558 let title = type != PlacesUtils.TYPE_UNICODE ? data.title 559 : data.uri; 560 return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri), 561 container, index, title); 562 } 563 } 564 return null; 565 }, 566 567 /** 568 * ********* PlacesTransactions version of the function defined above ******** 569 * 570 * Constructs a Places Transaction for the drop or paste of a blob of data 571 * into a container. 572 * 573 * @param aData 574 * The unwrapped data blob of dropped or pasted data. 575 * @param aType 576 * The content type of the data. 577 * @param aNewParentGuid 578 * GUID of the container the data was dropped or pasted into. 579 * @param aIndex 580 * The index within the container the item was dropped or pasted at. 581 * @param aCopy 582 * The drag action was copy, so don't move folders or links. 583 * 584 * @return a Places Transaction that can be transacted for performing the 585 * move/insert command. 586 */ 587 getTransactionForData: function(aData, aType, aNewParentGuid, aIndex, aCopy) { 588 if (!this.SUPPORTED_FLAVORS.includes(aData.type)) 589 throw new Error(`Unsupported '${aData.type}' data type`); 590 591 if ("itemGuid" in aData) { 592 if (!this.PLACES_FLAVORS.includes(aData.type)) 593 throw new Error (`itemGuid unexpectedly set on ${aData.type} data`); 594 595 let info = { guid: aData.itemGuid 596 , newParentGuid: aNewParentGuid 597 , newIndex: aIndex }; 598 if (aCopy) { 599 info.excludingAnnotation = "Places/SmartBookmark"; 600 return PlacesTransactions.Copy(info); 601 } 602 return PlacesTransactions.Move(info); 603 } 604 605 // Since it's cheap and harmless, we allow the paste of separators and 606 // bookmarks from builds that use legacy transactions (i.e. when itemGuid 607 // was not set on PLACES_FLAVORS data). Containers are a different story, 608 // and thus disallowed. 609 if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) 610 throw new Error("Can't copy a container from a legacy-transactions build"); 611 612 if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) { 613 return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid 614 , index: aIndex }); 615 } 616 617 let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title 618 : aData.uri; 619 return PlacesTransactions.NewBookmark({ uri: NetUtil.newURI(aData.uri) 620 , title: title 621 , parentGuid: aNewParentGuid 622 , index: aIndex }); 623 }, 624 625 /** 626 * Shows the bookmark dialog corresponding to the specified info. 627 * 628 * @param aInfo 629 * Describes the item to be edited/added in the dialog. 630 * See documentation at the top of bookmarkProperties.js 631 * @param aWindow 632 * Owner window for the new dialog. 633 * 634 * @see documentation at the top of bookmarkProperties.js 635 * @return true if any transaction has been performed, false otherwise. 636 */ 637 showBookmarkDialog: 638 function PUIU_showBookmarkDialog(aInfo, aParentWindow) { 639 // Preserve size attributes differently based on the fact the dialog has 640 // a folder picker or not, since it needs more horizontal space than the 641 // other controls. 642 let hasFolderPicker = !("hiddenRows" in aInfo) || 643 !aInfo.hiddenRows.includes("folderPicker"); 644 // Use a different chrome url to persist different sizes. 645 let dialogURL = hasFolderPicker ? 646 "chrome://browser/content/places/bookmarkProperties2.xul" : 647 "chrome://browser/content/places/bookmarkProperties.xul"; 648 649 let features = "centerscreen,chrome,modal,resizable=yes"; 650 aParentWindow.openDialog(dialogURL, "", features, aInfo); 651 return ("performed" in aInfo && aInfo.performed); 652 }, 653 654 _getTopBrowserWin: function PUIU__getTopBrowserWin() { 655 return RecentWindow.getMostRecentBrowserWindow(); 656 }, 657 658 /** 659 * set and fetch a favicon. Can only be used from the parent process. 660 * @param browser {Browser} The XUL browser element for which we're fetching a favicon. 661 * @param principal {Principal} The loading principal to use for the fetch. 662 * @param uri {URI} The URI to fetch. 663 */ 664 loadFavicon(browser, principal, uri) { 665 if (gInContentProcess) { 666 throw new Error("Can't track loads from within the child process!"); 667 } 668 InternalFaviconLoader.loadFavicon(browser, principal, uri); 669 }, 670 671 /** 672 * Returns the closet ancestor places view for the given DOM node 673 * @param aNode 674 * a DOM node 675 * @return the closet ancestor places view if exists, null otherwsie. 676 */ 677 getViewForNode: function PUIU_getViewForNode(aNode) { 678 let node = aNode; 679 680 // The view for a <menu> of which its associated menupopup is a places 681 // view, is the menupopup. 682 if (node.localName == "menu" && !node._placesNode && 683 node.lastChild._placesView) 684 return node.lastChild._placesView; 685 686 while (node instanceof Ci.nsIDOMElement) { 687 if (node._placesView) 688 return node._placesView; 689 if (node.localName == "tree" && node.getAttribute("type") == "places") 690 return node; 691 692 node = node.parentNode; 693 } 694 695 return null; 696 }, 697 698 /** 699 * By calling this before visiting an URL, the visit will be associated to a 700 * TRANSITION_TYPED transition (if there is no a referrer). 701 * This is used when visiting pages from the history menu, history sidebar, 702 * url bar, url autocomplete results, and history searches from the places 703 * organizer. If this is not called visits will be marked as 704 * TRANSITION_LINK. 705 */ 706 markPageAsTyped: function PUIU_markPageAsTyped(aURL) { 707 PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL)); 708 }, 709 710 /** 711 * By calling this before visiting an URL, the visit will be associated to a 712 * TRANSITION_BOOKMARK transition. 713 * This is used when visiting pages from the bookmarks menu, 714 * personal toolbar, and bookmarks from within the places organizer. 715 * If this is not called visits will be marked as TRANSITION_LINK. 716 */ 717 markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) { 718 PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL)); 719 }, 720 721 /** 722 * By calling this before visiting an URL, any visit in frames will be 723 * associated to a TRANSITION_FRAMED_LINK transition. 724 * This is actually used to distinguish user-initiated visits in frames 725 * so automatic visits can be correctly ignored. 726 */ 727 markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) { 728 PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL)); 729 }, 730 731 /** 732 * Allows opening of javascript/data URI only if the given node is 733 * bookmarked (see bug 224521). 734 * @param aURINode 735 * a URI node 736 * @param aWindow 737 * a window on which a potential error alert is shown on. 738 * @return true if it's safe to open the node in the browser, false otherwise. 739 * 740 */ 741 checkURLSecurity: function PUIU_checkURLSecurity(aURINode, aWindow) { 742 if (PlacesUtils.nodeIsBookmark(aURINode)) 743 return true; 744 745 var uri = PlacesUtils._uri(aURINode.uri); 746 if (uri.schemeIs("javascript") || uri.schemeIs("data")) { 747 const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; 748 var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]. 749 getService(Ci.nsIStringBundleService). 750 createBundle(BRANDING_BUNDLE_URI). 751 GetStringFromName("brandShortName"); 752 753 var errorStr = this.getString("load-js-data-url-error"); 754 Services.prompt.alert(aWindow, brandShortName, errorStr); 755 return false; 756 } 757 return true; 758 }, 759 760 /** 761 * Get the description associated with a document, as specified in a <META> 762 * element. 763 * @param doc 764 * A DOM Document to get a description for 765 * @return A description string if a META element was discovered with a 766 * "description" or "httpequiv" attribute, empty string otherwise. 767 */ 768 getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) { 769 var metaElements = doc.getElementsByTagName("META"); 770 for (var i = 0; i < metaElements.length; ++i) { 771 if (metaElements[i].name.toLowerCase() == "description" || 772 metaElements[i].httpEquiv.toLowerCase() == "description") { 773 return metaElements[i].content; 774 } 775 } 776 return ""; 777 }, 778 779 /** 780 * Retrieve the description of an item 781 * @param aItemId 782 * item identifier 783 * @return the description of the given item, or an empty string if it is 784 * not set. 785 */ 786 getItemDescription: function PUIU_getItemDescription(aItemId) { 787 if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO)) 788 return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO); 789 return ""; 790 }, 791 792 /** 793 * Check whether or not the given node represents a removable entry (either in 794 * history or in bookmarks). 795 * 796 * @param aNode 797 * a node, except the root node of a query. 798 * @return true if the aNode represents a removable entry, false otherwise. 799 */ 800 canUserRemove: function (aNode) { 801 let parentNode = aNode.parent; 802 if (!parentNode) { 803 // canUserRemove doesn't accept root nodes. 804 return false; 805 } 806 807 // If it's not a bookmark, we can remove it unless it's a child of a 808 // livemark. 809 if (aNode.itemId == -1) { 810 // Rather than executing a db query, checking the existence of the feedURI 811 // annotation, detect livemark children by the fact that they are the only 812 // direct non-bookmark children of bookmark folders. 813 return !PlacesUtils.nodeIsFolder(parentNode); 814 } 815 816 // Generally it's always possible to remove children of a query. 817 if (PlacesUtils.nodeIsQuery(parentNode)) 818 return true; 819 820 // Otherwise it has to be a child of an editable folder. 821 return !this.isContentsReadOnly(parentNode); 822 }, 823 824 /** 825 * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH 826 * TO GUIDS IS COMPLETE (BUG 1071511). 827 * 828 * Check whether or not the given node or item-id points to a folder which 829 * should not be modified by the user (i.e. its children should be unremovable 830 * and unmovable, new children should be disallowed, etc). 831 * These semantics are not inherited, meaning that read-only folder may 832 * contain editable items (for instance, the places root is read-only, but all 833 * of its direct children aren't). 834 * 835 * You should only pass folder item ids or folder nodes for aNodeOrItemId. 836 * While this is only enforced for the node case (if an item id of a separator 837 * or a bookmark is passed, false is returned), it's considered the caller's 838 * job to ensure that it checks a folder. 839 * Also note that folder-shortcuts should only be passed as result nodes. 840 * Otherwise they are just treated as bookmarks (i.e. false is returned). 841 * 842 * @param aNodeOrItemId 843 * any item id or result node. 844 * @throws if aNodeOrItemId is neither an item id nor a folder result node. 845 * @note livemark "folders" are considered read-only (but see bug 1072833). 846 * @return true if aItemId points to a read-only folder, false otherwise. 847 */ 848 isContentsReadOnly: function (aNodeOrItemId) { 849 let itemId; 850 if (typeof(aNodeOrItemId) == "number") { 851 itemId = aNodeOrItemId; 852 } 853 else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) { 854 itemId = PlacesUtils.getConcreteItemId(aNodeOrItemId); 855 } 856 else { 857 throw new Error("invalid value for aNodeOrItemId"); 858 } 859 860 if (itemId == PlacesUtils.placesRootId || IsLivemark(itemId)) 861 return true; 862 863 // leftPaneFolderId, and as a result, allBookmarksFolderId, is a lazy getter 864 // performing at least a synchronous DB query (and on its very first call 865 // in a fresh profile, it also creates the entire structure). 866 // Therefore we don't want to this function, which is called very often by 867 // isCommandEnabled, to ever be the one that invokes it first, especially 868 // because isCommandEnabled may be called way before the left pane folder is 869 // even created (for example, if the user only uses the bookmarks menu or 870 // toolbar for managing bookmarks). To do so, we avoid comparing to those 871 // special folder if the lazy getter is still in place. This is safe merely 872 // because the only way to access the left pane contents goes through 873 // "resolving" the leftPaneFolderId getter. 874 if ("get" in Object.getOwnPropertyDescriptor(this, "leftPaneFolderId")) 875 return false; 876 877 return itemId == this.leftPaneFolderId || 878 itemId == this.allBookmarksFolderId; 879 }, 880 881 /** 882 * Gives the user a chance to cancel loading lots of tabs at once 883 */ 884 confirmOpenInTabs(numTabsToOpen, aWindow) { 885 const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen"; 886 var reallyOpen = true; 887 888 if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) { 889 if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) { 890 // default to true: if it were false, we wouldn't get this far 891 var warnOnOpen = { value: true }; 892 893 var messageKey = "tabs.openWarningMultipleBranded"; 894 var openKey = "tabs.openButtonMultiple"; 895 const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; 896 var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]. 897 getService(Ci.nsIStringBundleService). 898 createBundle(BRANDING_BUNDLE_URI). 899 GetStringFromName("brandShortName"); 900 901 var buttonPressed = Services.prompt.confirmEx( 902 aWindow, 903 this.getString("tabs.openWarningTitle"), 904 this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]), 905 (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + 906 (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1), 907 this.getString(openKey), null, null, 908 this.getFormattedString("tabs.openWarningPromptMeBranded", 909 [brandShortName]), 910 warnOnOpen 911 ); 912 913 reallyOpen = (buttonPressed == 0); 914 // don't set the pref unless they press OK and it's false 915 if (reallyOpen && !warnOnOpen.value) 916 Services.prefs.setBoolPref(WARN_ON_OPEN_PREF, false); 917 } 918 } 919 920 return reallyOpen; 921 }, 922 923 /** aItemsToOpen needs to be an array of objects of the form: 924 * {uri: string, isBookmark: boolean} 925 */ 926 _openTabset: function PUIU__openTabset(aItemsToOpen, aEvent, aWindow) { 927 if (!aItemsToOpen.length) 928 return; 929 930 // Prefer the caller window if it's a browser window, otherwise use 931 // the top browser window. 932 var browserWindow = null; 933 browserWindow = 934 aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ? 935 aWindow : this._getTopBrowserWin(); 936 937 var urls = []; 938 let skipMarking = browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow); 939 for (let item of aItemsToOpen) { 940 urls.push(item.uri); 941 if (skipMarking) { 942 continue; 943 } 944 945 if (item.isBookmark) 946 this.markPageAsFollowedBookmark(item.uri); 947 else 948 this.markPageAsTyped(item.uri); 949 } 950 951 // whereToOpenLink doesn't return "window" when there's no browser window 952 // open (Bug 630255). 953 var where = browserWindow ? 954 browserWindow.whereToOpenLink(aEvent, false, true) : "window"; 955 if (where == "window") { 956 // There is no browser window open, thus open a new one. 957 var uriList = PlacesUtils.toISupportsString(urls.join("|")); 958 var args = Cc["@mozilla.org/array;1"]. 959 createInstance(Ci.nsIMutableArray); 960 args.appendElement(uriList, /* weak =*/ false); 961 browserWindow = Services.ww.openWindow(aWindow, 962 "chrome://browser/content/browser.xul", 963 null, "chrome,dialog=no,all", args); 964 return; 965 } 966 967 var loadInBackground = where == "tabshifted" ? true : false; 968 // For consistency, we want all the bookmarks to open in new tabs, instead 969 // of having one of them replace the currently focused tab. Hence we call 970 // loadTabs with aReplace set to false. 971 browserWindow.gBrowser.loadTabs(urls, loadInBackground, false); 972 }, 973 974 openLiveMarkNodesInTabs: 975 function PUIU_openLiveMarkNodesInTabs(aNode, aEvent, aView) { 976 let window = aView.ownerWindow; 977 978 PlacesUtils.livemarks.getLivemark({id: aNode.itemId}) 979 .then(aLivemark => { 980 let urlsToOpen = []; 981 982 let nodes = aLivemark.getNodesForContainer(aNode); 983 for (let node of nodes) { 984 urlsToOpen.push({uri: node.uri, isBookmark: false}); 985 } 986 987 if (this.confirmOpenInTabs(urlsToOpen.length, window)) { 988 this._openTabset(urlsToOpen, aEvent, window); 989 } 990 }, Cu.reportError); 991 }, 992 993 openContainerNodeInTabs: 994 function PUIU_openContainerInTabs(aNode, aEvent, aView) { 995 let window = aView.ownerWindow; 996 997 let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode); 998 if (this.confirmOpenInTabs(urlsToOpen.length, window)) { 999 this._openTabset(urlsToOpen, aEvent, window); 1000 } 1001 }, 1002 1003 openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent, aView) { 1004 let window = aView.ownerWindow; 1005 1006 let urlsToOpen = []; 1007 for (var i=0; i < aNodes.length; i++) { 1008 // Skip over separators and folders. 1009 if (PlacesUtils.nodeIsURI(aNodes[i])) 1010 urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])}); 1011 } 1012 this._openTabset(urlsToOpen, aEvent, window); 1013 }, 1014 1015 /** 1016 * Loads the node's URL in the appropriate tab or window or as a web 1017 * panel given the user's preference specified by modifier keys tracked by a 1018 * DOM mouse/key event. 1019 * @param aNode 1020 * An uri result node. 1021 * @param aEvent 1022 * The DOM mouse/key event with modifier keys set that track the 1023 * user's preferred destination window or tab. 1024 * @param aView 1025 * The controller associated with aNode. 1026 */ 1027 openNodeWithEvent: 1028 function PUIU_openNodeWithEvent(aNode, aEvent, aView) { 1029 let window = aView.ownerWindow; 1030 this._openNodeIn(aNode, window.whereToOpenLink(aEvent, false, true), window); 1031 }, 1032 1033 /** 1034 * Loads the node's URL in the appropriate tab or window or as a 1035 * web panel. 1036 * see also openUILinkIn 1037 */ 1038 openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) { 1039 let window = aView.ownerWindow; 1040 this._openNodeIn(aNode, aWhere, window, aPrivate); 1041 }, 1042 1043 _openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aWindow, aPrivate=false) { 1044 if (aNode && PlacesUtils.nodeIsURI(aNode) && 1045 this.checkURLSecurity(aNode, aWindow)) { 1046 let isBookmark = PlacesUtils.nodeIsBookmark(aNode); 1047 1048 if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) { 1049 if (isBookmark) 1050 this.markPageAsFollowedBookmark(aNode.uri); 1051 else 1052 this.markPageAsTyped(aNode.uri); 1053 } 1054 1055 // Check whether the node is a bookmark which should be opened as 1056 // a web panel 1057 if (aWhere == "current" && isBookmark) { 1058 if (PlacesUtils.annotations 1059 .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) { 1060 let browserWin = this._getTopBrowserWin(); 1061 if (browserWin) { 1062 browserWin.openWebPanel(aNode.title, aNode.uri); 1063 return; 1064 } 1065 } 1066 } 1067 1068 aWindow.openUILinkIn(aNode.uri, aWhere, { 1069 allowPopups: aNode.uri.startsWith("javascript:"), 1070 inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground"), 1071 private: aPrivate, 1072 }); 1073 } 1074 }, 1075 1076 /** 1077 * Helper for guessing scheme from an url string. 1078 * Used to avoid nsIURI overhead in frequently called UI functions. 1079 * 1080 * @param aUrlString the url to guess the scheme from. 1081 * 1082 * @return guessed scheme for this url string. 1083 * 1084 * @note this is not supposed be perfect, so use it only for UI purposes. 1085 */ 1086 guessUrlSchemeForUI: function PUIU_guessUrlSchemeForUI(aUrlString) { 1087 return aUrlString.substr(0, aUrlString.indexOf(":")); 1088 }, 1089 1090 getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) { 1091 var title; 1092 if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) { 1093 // if node title is empty, try to set the label using host and filename 1094 // PlacesUtils._uri() will throw if aNode.uri is not a valid URI 1095 try { 1096 var uri = PlacesUtils._uri(aNode.uri); 1097 var host = uri.host; 1098 var fileName = uri.QueryInterface(Ci.nsIURL).fileName; 1099 // if fileName is empty, use path to distinguish labels 1100 if (aDoNotCutTitle) { 1101 title = host + uri.path; 1102 } else { 1103 title = host + (fileName ? 1104 (host ? "/" + this.ellipsis + "/" : "") + fileName : 1105 uri.path); 1106 } 1107 } 1108 catch (e) { 1109 // Use (no title) for non-standard URIs (data:, javascript:, ...) 1110 title = ""; 1111 } 1112 } 1113 else 1114 title = aNode.title; 1115 1116 return title || this.getString("noTitle"); 1117 }, 1118 1119 get leftPaneQueries() { 1120 // build the map 1121 this.leftPaneFolderId; 1122 return this.leftPaneQueries; 1123 }, 1124 1125 // Get the folder id for the organizer left-pane folder. 1126 get leftPaneFolderId() { 1127 let leftPaneRoot = -1; 1128 let allBookmarksId; 1129 1130 // Shortcuts to services. 1131 let bs = PlacesUtils.bookmarks; 1132 let as = PlacesUtils.annotations; 1133 1134 // This is the list of the left pane queries. 1135 let queries = { 1136 "PlacesRoot": { title: "" }, 1137 "History": { title: this.getString("OrganizerQueryHistory") }, 1138 "Downloads": { title: this.getString("OrganizerQueryDownloads") }, 1139 "Tags": { title: this.getString("OrganizerQueryTags") }, 1140 "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") }, 1141 "BookmarksToolbar": 1142 { title: null, 1143 concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"), 1144 concreteId: PlacesUtils.toolbarFolderId }, 1145 "BookmarksMenu": 1146 { title: null, 1147 concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"), 1148 concreteId: PlacesUtils.bookmarksMenuFolderId }, 1149 "UnfiledBookmarks": 1150 { title: null, 1151 concreteTitle: PlacesUtils.getString("OtherBookmarksFolderTitle"), 1152 concreteId: PlacesUtils.unfiledBookmarksFolderId }, 1153 }; 1154 // All queries but PlacesRoot. 1155 const EXPECTED_QUERY_COUNT = 7; 1156 1157 // Removes an item and associated annotations, ignoring eventual errors. 1158 function safeRemoveItem(aItemId) { 1159 try { 1160 if (as.itemHasAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) && 1161 !(as.getItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) in queries)) { 1162 // Some extension annotated their roots with our query annotation, 1163 // so we should not delete them. 1164 return; 1165 } 1166 // removeItemAnnotation does not check if item exists, nor the anno, 1167 // so this is safe to do. 1168 as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO); 1169 as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO); 1170 // This will throw if the annotation is an orphan. 1171 bs.removeItem(aItemId); 1172 } 1173 catch (e) { /* orphan anno */ } 1174 } 1175 1176 // Returns true if item really exists, false otherwise. 1177 function itemExists(aItemId) { 1178 try { 1179 bs.getItemIndex(aItemId); 1180 return true; 1181 } 1182 catch (e) { 1183 return false; 1184 } 1185 } 1186 1187 // Get all items marked as being the left pane folder. 1188 let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO); 1189 if (items.length > 1) { 1190 // Something went wrong, we cannot have more than one left pane folder, 1191 // remove all left pane folders and continue. We will create a new one. 1192 items.forEach(safeRemoveItem); 1193 } 1194 else if (items.length == 1 && items[0] != -1) { 1195 leftPaneRoot = items[0]; 1196 // Check that organizer left pane root is valid. 1197 let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO); 1198 if (version != this.ORGANIZER_LEFTPANE_VERSION || 1199 !itemExists(leftPaneRoot)) { 1200 // Invalid root, we must rebuild the left pane. 1201 safeRemoveItem(leftPaneRoot); 1202 leftPaneRoot = -1; 1203 } 1204 } 1205 1206 if (leftPaneRoot != -1) { 1207 // A valid left pane folder has been found. 1208 // Build the leftPaneQueries Map. This is used to quickly access them, 1209 // associating a mnemonic name to the real item ids. 1210 delete this.leftPaneQueries; 1211 this.leftPaneQueries = {}; 1212 1213 let items = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO); 1214 // While looping through queries we will also check for their validity. 1215 let queriesCount = 0; 1216 let corrupt = false; 1217 for (let i = 0; i < items.length; i++) { 1218 let queryName = as.getItemAnnotation(items[i], this.ORGANIZER_QUERY_ANNO); 1219 1220 // Some extension did use our annotation to decorate their items 1221 // with icons, so we should check only our elements, to avoid dataloss. 1222 if (!(queryName in queries)) 1223 continue; 1224 1225 let query = queries[queryName]; 1226 query.itemId = items[i]; 1227 1228 if (!itemExists(query.itemId)) { 1229 // Orphan annotation, bail out and create a new left pane root. 1230 corrupt = true; 1231 break; 1232 } 1233 1234 // Check that all queries have valid parents. 1235 let parentId = bs.getFolderIdForItem(query.itemId); 1236 if (!items.includes(parentId) && parentId != leftPaneRoot) { 1237 // The parent is not part of the left pane, bail out and create a new 1238 // left pane root. 1239 corrupt = true; 1240 break; 1241 } 1242 1243 // Titles could have been corrupted or the user could have changed his 1244 // locale. Check title and eventually fix it. 1245 if (bs.getItemTitle(query.itemId) != query.title) 1246 bs.setItemTitle(query.itemId, query.title); 1247 if ("concreteId" in query) { 1248 if (bs.getItemTitle(query.concreteId) != query.concreteTitle) 1249 bs.setItemTitle(query.concreteId, query.concreteTitle); 1250 } 1251 1252 // Add the query to our cache. 1253 this.leftPaneQueries[queryName] = query.itemId; 1254 queriesCount++; 1255 } 1256 1257 // Note: it's not enough to just check for queriesCount, since we may 1258 // find an invalid query just after accounting for a sufficient number of 1259 // valid ones. As well as we can't just rely on corrupt since we may find 1260 // less valid queries than expected. 1261 if (corrupt || queriesCount != EXPECTED_QUERY_COUNT) { 1262 // Queries number is wrong, so the left pane must be corrupt. 1263 // Note: we can't just remove the leftPaneRoot, because some query could 1264 // have a bad parent, so we have to remove all items one by one. 1265 items.forEach(safeRemoveItem); 1266 safeRemoveItem(leftPaneRoot); 1267 } 1268 else { 1269 // Everything is fine, return the current left pane folder. 1270 delete this.leftPaneFolderId; 1271 return this.leftPaneFolderId = leftPaneRoot; 1272 } 1273 } 1274 1275 // Create a new left pane folder. 1276 var callback = { 1277 // Helper to create an organizer special query. 1278 create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) { 1279 let itemId = bs.insertBookmark(aParentId, 1280 PlacesUtils._uri(aQueryUrl), 1281 bs.DEFAULT_INDEX, 1282 queries[aQueryName].title); 1283 // Mark as special organizer query. 1284 as.setItemAnnotation(itemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aQueryName, 1285 0, as.EXPIRE_NEVER); 1286 // We should never backup this, since it changes between profiles. 1287 as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 1288 0, as.EXPIRE_NEVER); 1289 // Add to the queries map. 1290 PlacesUIUtils.leftPaneQueries[aQueryName] = itemId; 1291 return itemId; 1292 }, 1293 1294 // Helper to create an organizer special folder. 1295 create_folder: function CB_create_folder(aFolderName, aParentId, aIsRoot) { 1296 // Left Pane Root Folder. 1297 let folderId = bs.createFolder(aParentId, 1298 queries[aFolderName].title, 1299 bs.DEFAULT_INDEX); 1300 // We should never backup this, since it changes between profiles. 1301 as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 1302 0, as.EXPIRE_NEVER); 1303 1304 if (aIsRoot) { 1305 // Mark as special left pane root. 1306 as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO, 1307 PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, 1308 0, as.EXPIRE_NEVER); 1309 } 1310 else { 1311 // Mark as special organizer folder. 1312 as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName, 1313 0, as.EXPIRE_NEVER); 1314 PlacesUIUtils.leftPaneQueries[aFolderName] = folderId; 1315 } 1316 return folderId; 1317 }, 1318 1319 runBatched: function CB_runBatched(aUserData) { 1320 delete PlacesUIUtils.leftPaneQueries; 1321 PlacesUIUtils.leftPaneQueries = { }; 1322 1323 // Left Pane Root Folder. 1324 leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true); 1325 1326 // History Query. 1327 this.create_query("History", leftPaneRoot, 1328 "place:type=" + 1329 Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY + 1330 "&sort=" + 1331 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); 1332 1333 // Downloads. 1334 this.create_query("Downloads", leftPaneRoot, 1335 "place:transition=" + 1336 Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + 1337 "&sort=" + 1338 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); 1339 1340 // Tags Query. 1341 this.create_query("Tags", leftPaneRoot, 1342 "place:type=" + 1343 Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY + 1344 "&sort=" + 1345 Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING); 1346 1347 // All Bookmarks Folder. 1348 allBookmarksId = this.create_folder("AllBookmarks", leftPaneRoot, false); 1349 1350 // All Bookmarks->Bookmarks Toolbar Query. 1351 this.create_query("BookmarksToolbar", allBookmarksId, 1352 "place:folder=TOOLBAR"); 1353 1354 // All Bookmarks->Bookmarks Menu Query. 1355 this.create_query("BookmarksMenu", allBookmarksId, 1356 "place:folder=BOOKMARKS_MENU"); 1357 1358 // All Bookmarks->Unfiled Bookmarks Query. 1359 this.create_query("UnfiledBookmarks", allBookmarksId, 1360 "place:folder=UNFILED_BOOKMARKS"); 1361 } 1362 }; 1363 bs.runInBatchMode(callback, null); 1364 1365 delete this.leftPaneFolderId; 1366 return this.leftPaneFolderId = leftPaneRoot; 1367 }, 1368 1369 /** 1370 * Get the folder id for the organizer left-pane folder. 1371 */ 1372 get allBookmarksFolderId() { 1373 // ensure the left-pane root is initialized; 1374 this.leftPaneFolderId; 1375 delete this.allBookmarksFolderId; 1376 return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"]; 1377 }, 1378 1379 /** 1380 * If an item is a left-pane query, returns the name of the query 1381 * or an empty string if not. 1382 * 1383 * @param aItemId id of a container 1384 * @return the name of the query, or empty string if not a left-pane query 1385 */ 1386 getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) { 1387 var queryName = ""; 1388 // If the let pane hasn't been built, use the annotation service 1389 // directly, to avoid building the left pane too early. 1390 if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) { 1391 try { 1392 queryName = PlacesUtils.annotations. 1393 getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO); 1394 } 1395 catch (ex) { 1396 // doesn't have the annotation 1397 queryName = ""; 1398 } 1399 } 1400 else { 1401 // If the left pane has already been built, use the name->id map 1402 // cached in PlacesUIUtils. 1403 for (let [name, id] of Object.entries(this.leftPaneQueries)) { 1404 if (aItemId == id) 1405 queryName = name; 1406 } 1407 } 1408 return queryName; 1409 }, 1410 1411 shouldShowTabsFromOtherComputersMenuitem: function() { 1412 let weaveOK = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED && 1413 Weave.Svc.Prefs.get("firstSync", "") != "notReady"; 1414 return weaveOK; 1415 }, 1416 1417 /** 1418 * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A 1419 * FUTURE RELEASE. 1420 * 1421 * Checks if a place: href represents a folder shortcut. 1422 * 1423 * @param queryString 1424 * the query string to check (a place: href) 1425 * @return whether or not queryString represents a folder shortcut. 1426 * @throws if queryString is malformed. 1427 */ 1428 isFolderShortcutQueryString(queryString) { 1429 // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp. 1430 1431 let queriesParam = { }, optionsParam = { }; 1432 PlacesUtils.history.queryStringToQueries(queryString, 1433 queriesParam, 1434 { }, 1435 optionsParam); 1436 let queries = queries.value; 1437 if (queries.length == 0) 1438 throw new Error(`Invalid place: uri: ${queryString}`); 1439 return queries.length == 1 && 1440 queries[0].folderCount == 1 && 1441 !queries[0].hasBeginTime && 1442 !queries[0].hasEndTime && 1443 !queries[0].hasDomain && 1444 !queries[0].hasURI && 1445 !queries[0].hasSearchTerms && 1446 !queries[0].tags.length == 0 && 1447 optionsParam.value.maxResults == 0; 1448 }, 1449 1450 /** 1451 * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT"S LIKELY TO BE REMOVED IN A 1452 * FUTURE RELEASE. 1453 * 1454 * Helpers for consumers of editBookmarkOverlay which don't have a node as their input. 1455 * Given a partial node-like object, having at least the itemId property set, this 1456 * method completes the rest of the properties necessary for initialising the edit 1457 * overlay with it. 1458 * 1459 * @param aNodeLike 1460 * an object having at least the itemId nsINavHistoryResultNode property set, 1461 * along with any other properties available. 1462 */ 1463 completeNodeLikeObjectForItemId(aNodeLike) { 1464 if (this.useAsyncTransactions) { 1465 // When async-transactions are enabled, node-likes must have 1466 // bookmarkGuid set, and we cannot set it synchronously. 1467 throw new Error("completeNodeLikeObjectForItemId cannot be used when " + 1468 "async transactions are enabled"); 1469 } 1470 if (!("itemId" in aNodeLike)) 1471 throw new Error("itemId missing in aNodeLike"); 1472 1473 let itemId = aNodeLike.itemId; 1474 let defGetter = XPCOMUtils.defineLazyGetter.bind(XPCOMUtils, aNodeLike); 1475 1476 if (!("title" in aNodeLike)) 1477 defGetter("title", () => PlacesUtils.bookmarks.getItemTitle(itemId)); 1478 1479 if (!("uri" in aNodeLike)) { 1480 defGetter("uri", () => { 1481 let uri = null; 1482 try { 1483 uri = PlacesUtils.bookmarks.getBookmarkURI(itemId); 1484 } 1485 catch (ex) { } 1486 return uri ? uri.spec : ""; 1487 }); 1488 } 1489 1490 if (!("type" in aNodeLike)) { 1491 defGetter("type", () => { 1492 if (aNodeLike.uri.length > 0) { 1493 if (/^place:/.test(aNodeLike.uri)) { 1494 if (this.isFolderShortcutQueryString(aNodeLike.uri)) 1495 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT; 1496 1497 return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY; 1498 } 1499 1500 return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI; 1501 } 1502 1503 let itemType = PlacesUtils.bookmarks.getItemType(itemId); 1504 if (itemType == PlacesUtils.bookmarks.TYPE_FOLDER) 1505 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER; 1506 1507 throw new Error("Unexpected item type"); 1508 }); 1509 } 1510 }, 1511 1512 /** 1513 * Helpers for consumers of editBookmarkOverlay which don't have a node as their input. 1514 * 1515 * Given a bookmark object for either a url bookmark or a folder, returned by 1516 * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for 1517 * initialising the edit overlay with it. 1518 * 1519 * @param aFetchInfo 1520 * a bookmark object returned by Bookmarks.fetch. 1521 * @return a node-like object suitable for initialising editBookmarkOverlay. 1522 * @throws if aFetchInfo is representing a separator. 1523 */ 1524 promiseNodeLikeFromFetchInfo: Task.async(function* (aFetchInfo) { 1525 if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) 1526 throw new Error("promiseNodeLike doesn't support separators"); 1527 1528 return Object.freeze({ 1529 itemId: yield PlacesUtils.promiseItemId(aFetchInfo.guid), 1530 bookmarkGuid: aFetchInfo.guid, 1531 title: aFetchInfo.title, 1532 uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "", 1533 1534 get type() { 1535 if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER) 1536 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER; 1537 1538 if (this.uri.length == 0) 1539 throw new Error("Unexpected item type"); 1540 1541 if (/^place:/.test(this.uri)) { 1542 if (this.isFolderShortcutQueryString(this.uri)) 1543 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT; 1544 1545 return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY; 1546 } 1547 1548 return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI; 1549 } 1550 }); 1551 }), 1552 1553 /** 1554 * Shortcut for calling promiseNodeLikeFromFetchInfo on the result of 1555 * Bookmarks.fetch for the given guid/info object. 1556 * 1557 * @see promiseNodeLikeFromFetchInfo above and Bookmarks.fetch in Bookmarks.jsm. 1558 */ 1559 fetchNodeLike: Task.async(function* (aGuidOrInfo) { 1560 let info = yield PlacesUtils.bookmarks.fetch(aGuidOrInfo); 1561 if (!info) 1562 return null; 1563 return (yield this.promiseNodeLikeFromFetchInfo(info)); 1564 }) 1565}; 1566 1567 1568PlacesUIUtils.PLACES_FLAVORS = [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, 1569 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, 1570 PlacesUtils.TYPE_X_MOZ_PLACE]; 1571 1572PlacesUIUtils.URI_FLAVORS = [PlacesUtils.TYPE_X_MOZ_URL, 1573 TAB_DROP_TYPE, 1574 PlacesUtils.TYPE_UNICODE], 1575 1576PlacesUIUtils.SUPPORTED_FLAVORS = [...PlacesUIUtils.PLACES_FLAVORS, 1577 ...PlacesUIUtils.URI_FLAVORS]; 1578 1579XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF", 1580 "@mozilla.org/rdf/rdf-service;1", 1581 "nsIRDFService"); 1582 1583XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() { 1584 return Services.prefs.getComplexValue("intl.ellipsis", 1585 Ci.nsIPrefLocalizedString).data; 1586}); 1587 1588XPCOMUtils.defineLazyGetter(PlacesUIUtils, "useAsyncTransactions", function() { 1589 try { 1590 return Services.prefs.getBoolPref("browser.places.useAsyncTransactions"); 1591 } 1592 catch (ex) { } 1593 return false; 1594}); 1595 1596XPCOMUtils.defineLazyServiceGetter(this, "URIFixup", 1597 "@mozilla.org/docshell/urifixup;1", 1598 "nsIURIFixup"); 1599 1600XPCOMUtils.defineLazyGetter(this, "bundle", function() { 1601 const PLACES_STRING_BUNDLE_URI = 1602 "chrome://browser/locale/places/places.properties"; 1603 return Cc["@mozilla.org/intl/stringbundle;1"]. 1604 getService(Ci.nsIStringBundleService). 1605 createBundle(PLACES_STRING_BUNDLE_URI); 1606}); 1607 1608/** 1609 * This is a compatibility shim for old PUIU.ptm users. 1610 * 1611 * If you're looking for transactions and writing new code using them, directly 1612 * use the transactions objects exported by the PlacesUtils.jsm module. 1613 * 1614 * This object will be removed once enough users are converted to the new API. 1615 */ 1616XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() { 1617 // Ensure PlacesUtils is imported in scope. 1618 PlacesUtils; 1619 1620 return { 1621 aggregateTransactions: (aName, aTransactions) => 1622 new PlacesAggregatedTransaction(aName, aTransactions), 1623 1624 createFolder: (aName, aContainer, aIndex, aAnnotations, 1625 aChildItemsTransactions) => 1626 new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations, 1627 aChildItemsTransactions), 1628 1629 createItem: (aURI, aContainer, aIndex, aTitle, aKeyword, 1630 aAnnotations, aChildTransactions) => 1631 new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle, 1632 aKeyword, aAnnotations, 1633 aChildTransactions), 1634 1635 createSeparator: (aContainer, aIndex) => 1636 new PlacesCreateSeparatorTransaction(aContainer, aIndex), 1637 1638 createLivemark: (aFeedURI, aSiteURI, aName, aContainer, aIndex, 1639 aAnnotations) => 1640 new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer, 1641 aIndex, aAnnotations), 1642 1643 moveItem: (aItemId, aNewContainer, aNewIndex) => 1644 new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex), 1645 1646 removeItem: (aItemId) => 1647 new PlacesRemoveItemTransaction(aItemId), 1648 1649 editItemTitle: (aItemId, aNewTitle) => 1650 new PlacesEditItemTitleTransaction(aItemId, aNewTitle), 1651 1652 editBookmarkURI: (aItemId, aNewURI) => 1653 new PlacesEditBookmarkURITransaction(aItemId, aNewURI), 1654 1655 setItemAnnotation: (aItemId, aAnnotationObject) => 1656 new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject), 1657 1658 setPageAnnotation: (aURI, aAnnotationObject) => 1659 new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject), 1660 1661 editBookmarkKeyword: (aItemId, aNewKeyword) => 1662 new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword), 1663 1664 editLivemarkSiteURI: (aLivemarkId, aSiteURI) => 1665 new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI), 1666 1667 editLivemarkFeedURI: (aLivemarkId, aFeedURI) => 1668 new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI), 1669 1670 editItemDateAdded: (aItemId, aNewDateAdded) => 1671 new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded), 1672 1673 editItemLastModified: (aItemId, aNewLastModified) => 1674 new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified), 1675 1676 sortFolderByName: (aFolderId) => 1677 new PlacesSortFolderByNameTransaction(aFolderId), 1678 1679 tagURI: (aURI, aTags) => 1680 new PlacesTagURITransaction(aURI, aTags), 1681 1682 untagURI: (aURI, aTags) => 1683 new PlacesUntagURITransaction(aURI, aTags), 1684 1685 /** 1686 * Transaction for setting/unsetting Load-in-sidebar annotation. 1687 * 1688 * @param aBookmarkId 1689 * id of the bookmark where to set Load-in-sidebar annotation. 1690 * @param aLoadInSidebar 1691 * boolean value. 1692 * @return nsITransaction object. 1693 */ 1694 setLoadInSidebar: function(aItemId, aLoadInSidebar) 1695 { 1696 let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO, 1697 type: Ci.nsIAnnotationService.TYPE_INT32, 1698 flags: 0, 1699 value: aLoadInSidebar, 1700 expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; 1701 return new PlacesSetItemAnnotationTransaction(aItemId, annoObj); 1702 }, 1703 1704 /** 1705 * Transaction for editing the description of a bookmark or a folder. 1706 * 1707 * @param aItemId 1708 * id of the item to edit. 1709 * @param aDescription 1710 * new description. 1711 * @return nsITransaction object. 1712 */ 1713 editItemDescription: function(aItemId, aDescription) 1714 { 1715 let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO, 1716 type: Ci.nsIAnnotationService.TYPE_STRING, 1717 flags: 0, 1718 value: aDescription, 1719 expires: Ci.nsIAnnotationService.EXPIRE_NEVER }; 1720 return new PlacesSetItemAnnotationTransaction(aItemId, annoObj); 1721 }, 1722 1723 // nsITransactionManager forwarders. 1724 1725 beginBatch: () => 1726 PlacesUtils.transactionManager.beginBatch(null), 1727 1728 endBatch: () => 1729 PlacesUtils.transactionManager.endBatch(false), 1730 1731 doTransaction: (txn) => 1732 PlacesUtils.transactionManager.doTransaction(txn), 1733 1734 undoTransaction: () => 1735 PlacesUtils.transactionManager.undoTransaction(), 1736 1737 redoTransaction: () => 1738 PlacesUtils.transactionManager.redoTransaction(), 1739 1740 get numberOfUndoItems() { 1741 return PlacesUtils.transactionManager.numberOfUndoItems; 1742 }, 1743 get numberOfRedoItems() { 1744 return PlacesUtils.transactionManager.numberOfRedoItems; 1745 }, 1746 get maxTransactionCount() { 1747 return PlacesUtils.transactionManager.maxTransactionCount; 1748 }, 1749 set maxTransactionCount(val) { 1750 PlacesUtils.transactionManager.maxTransactionCount = val; 1751 }, 1752 1753 clear: () => 1754 PlacesUtils.transactionManager.clear(), 1755 1756 peekUndoStack: () => 1757 PlacesUtils.transactionManager.peekUndoStack(), 1758 1759 peekRedoStack: () => 1760 PlacesUtils.transactionManager.peekRedoStack(), 1761 1762 getUndoStack: () => 1763 PlacesUtils.transactionManager.getUndoStack(), 1764 1765 getRedoStack: () => 1766 PlacesUtils.transactionManager.getRedoStack(), 1767 1768 AddListener: (aListener) => 1769 PlacesUtils.transactionManager.AddListener(aListener), 1770 1771 RemoveListener: (aListener) => 1772 PlacesUtils.transactionManager.RemoveListener(aListener) 1773 } 1774}); 1775