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 6var EXPORTED_SYMBOLS = ["PlacesUIUtils"]; 7 8const { XPCOMUtils } = ChromeUtils.import( 9 "resource://gre/modules/XPCOMUtils.jsm" 10); 11const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 12const { clearTimeout, setTimeout } = ChromeUtils.import( 13 "resource://gre/modules/Timer.jsm" 14); 15 16XPCOMUtils.defineLazyGlobalGetters(this, ["Element"]); 17 18XPCOMUtils.defineLazyModuleGetters(this, { 19 AppConstants: "resource://gre/modules/AppConstants.jsm", 20 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", 21 OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm", 22 PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm", 23 PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", 24 PluralForm: "resource://gre/modules/PluralForm.jsm", 25 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", 26 PromiseUtils: "resource://gre/modules/PromiseUtils.jsm", 27 Weave: "resource://services-sync/main.js", 28}); 29 30XPCOMUtils.defineLazyGetter(this, "bundle", function() { 31 return Services.strings.createBundle( 32 "chrome://browser/locale/places/places.properties" 33 ); 34}); 35 36const gInContentProcess = 37 Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; 38const FAVICON_REQUEST_TIMEOUT = 60 * 1000; 39// Map from windows to arrays of data about pending favicon loads. 40let gFaviconLoadDataMap = new Map(); 41 42const ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD = 10; 43 44// copied from utilityOverlay.js 45const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab"; 46const PREF_LOAD_BOOKMARKS_IN_BACKGROUND = 47 "browser.tabs.loadBookmarksInBackground"; 48const PREF_LOAD_BOOKMARKS_IN_TABS = "browser.tabs.loadBookmarksInTabs"; 49 50let InternalFaviconLoader = { 51 /** 52 * This gets called for every inner window that is destroyed. 53 * In the parent process, we process the destruction ourselves. In the child process, 54 * we notify the parent which will then process it based on that message. 55 */ 56 observe(subject, topic, data) { 57 let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; 58 this.removeRequestsForInner(innerWindowID); 59 }, 60 61 /** 62 * Actually cancel the request, and clear the timeout for cancelling it. 63 */ 64 _cancelRequest({ uri, innerWindowID, timerID, callback }, reason) { 65 // Break cycle 66 let request = callback.request; 67 delete callback.request; 68 // Ensure we don't time out. 69 clearTimeout(timerID); 70 try { 71 request.cancel(); 72 } catch (ex) { 73 Cu.reportError( 74 "When cancelling a request for " + 75 uri.spec + 76 " because " + 77 reason + 78 ", it was already canceled!" 79 ); 80 } 81 }, 82 83 /** 84 * Called for every inner that gets destroyed, only in the parent process. 85 */ 86 removeRequestsForInner(innerID) { 87 for (let [window, loadDataForWindow] of gFaviconLoadDataMap) { 88 let newLoadDataForWindow = loadDataForWindow.filter(loadData => { 89 let innerWasDestroyed = loadData.innerWindowID == innerID; 90 if (innerWasDestroyed) { 91 this._cancelRequest( 92 loadData, 93 "the inner window was destroyed or a new favicon was loaded for it" 94 ); 95 } 96 // Keep the items whose inner is still alive. 97 return !innerWasDestroyed; 98 }); 99 // Map iteration with for...of is safe against modification, so 100 // now just replace the old value: 101 gFaviconLoadDataMap.set(window, newLoadDataForWindow); 102 } 103 }, 104 105 /** 106 * Called when a toplevel chrome window unloads. We use this to tidy up after ourselves, 107 * avoid leaks, and cancel any remaining requests. The last part should in theory be 108 * handled by the inner-window-destroyed handlers. We clean up just to be on the safe side. 109 */ 110 onUnload(win) { 111 let loadDataForWindow = gFaviconLoadDataMap.get(win); 112 if (loadDataForWindow) { 113 for (let loadData of loadDataForWindow) { 114 this._cancelRequest(loadData, "the chrome window went away"); 115 } 116 } 117 gFaviconLoadDataMap.delete(win); 118 }, 119 120 /** 121 * Remove a particular favicon load's loading data from our map tracking 122 * load data per chrome window. 123 * 124 * @param win 125 * the chrome window in which we should look for this load 126 * @param filterData ({innerWindowID, uri, callback}) 127 * the data we should use to find this particular load to remove. 128 * 129 * @return the loadData object we removed, or null if we didn't find any. 130 */ 131 _removeLoadDataFromWindowMap(win, { innerWindowID, uri, callback }) { 132 let loadDataForWindow = gFaviconLoadDataMap.get(win); 133 if (loadDataForWindow) { 134 let itemIndex = loadDataForWindow.findIndex(loadData => { 135 return ( 136 loadData.innerWindowID == innerWindowID && 137 loadData.uri.equals(uri) && 138 loadData.callback.request == callback.request 139 ); 140 }); 141 if (itemIndex != -1) { 142 let loadData = loadDataForWindow[itemIndex]; 143 loadDataForWindow.splice(itemIndex, 1); 144 return loadData; 145 } 146 } 147 return null; 148 }, 149 150 /** 151 * Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling 152 * information when the request succeeds. Note that right now there are some edge-cases, 153 * such as about: URIs with chrome:// favicons where the success callback is not invoked. 154 * This is OK: we will 'cancel' the request after the timeout (or when the window goes 155 * away) but that will be a no-op in such cases. 156 */ 157 _makeCompletionCallback(win, id) { 158 return { 159 onComplete(uri) { 160 let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, { 161 uri, 162 innerWindowID: id, 163 callback: this, 164 }); 165 if (loadData) { 166 clearTimeout(loadData.timerID); 167 } 168 delete this.request; 169 }, 170 }; 171 }, 172 173 ensureInitialized() { 174 if (this._initialized) { 175 return; 176 } 177 this._initialized = true; 178 179 Services.obs.addObserver(this, "inner-window-destroyed"); 180 Services.ppmm.addMessageListener("Toolkit:inner-window-destroyed", msg => { 181 this.removeRequestsForInner(msg.data); 182 }); 183 }, 184 185 loadFavicon(browser, principal, pageURI, uri, expiration, iconURI) { 186 this.ensureInitialized(); 187 let { ownerGlobal: win, innerWindowID } = browser; 188 if (!gFaviconLoadDataMap.has(win)) { 189 gFaviconLoadDataMap.set(win, []); 190 let unloadHandler = event => { 191 let doc = event.target; 192 let eventWin = doc.defaultView; 193 if (eventWin == win) { 194 win.removeEventListener("unload", unloadHandler); 195 this.onUnload(win); 196 } 197 }; 198 win.addEventListener("unload", unloadHandler, true); 199 } 200 201 // First we do the actual setAndFetch call: 202 let loadType = PrivateBrowsingUtils.isWindowPrivate(win) 203 ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE 204 : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE; 205 let callback = this._makeCompletionCallback(win, innerWindowID); 206 207 if (iconURI && iconURI.schemeIs("data")) { 208 expiration = PlacesUtils.toPRTime(expiration); 209 PlacesUtils.favicons.replaceFaviconDataFromDataURL( 210 uri, 211 iconURI.spec, 212 expiration, 213 principal 214 ); 215 } 216 217 let request = PlacesUtils.favicons.setAndFetchFaviconForPage( 218 pageURI, 219 uri, 220 false, 221 loadType, 222 callback, 223 principal 224 ); 225 226 // Now register the result so we can cancel it if/when necessary. 227 if (!request) { 228 // The favicon service can return with success but no-op (and leave request 229 // as null) if the icon is the same as the page (e.g. for images) or if it is 230 // the favicon for an error page. In this case, we do not need to do anything else. 231 return; 232 } 233 callback.request = request; 234 let loadData = { innerWindowID, uri, callback }; 235 loadData.timerID = setTimeout(() => { 236 this._cancelRequest(loadData, "it timed out"); 237 this._removeLoadDataFromWindowMap(win, loadData); 238 }, FAVICON_REQUEST_TIMEOUT); 239 let loadDataForWindow = gFaviconLoadDataMap.get(win); 240 loadDataForWindow.push(loadData); 241 }, 242}; 243 244var PlacesUIUtils = { 245 LAST_USED_FOLDERS_META_KEY: "bookmarks/lastusedfolders", 246 247 /** 248 * Makes a URI from a spec, and do fixup 249 * @param aSpec 250 * The string spec of the URI 251 * @return A URI object for the spec. 252 */ 253 createFixedURI: function PUIU_createFixedURI(aSpec) { 254 return Services.uriFixup.createFixupURI( 255 aSpec, 256 Ci.nsIURIFixup.FIXUP_FLAG_NONE 257 ); 258 }, 259 260 getFormattedString: function PUIU_getFormattedString(key, params) { 261 return bundle.formatStringFromName(key, params); 262 }, 263 264 /** 265 * Get a localized plural string for the specified key name and numeric value 266 * substituting parameters. 267 * 268 * @param aKey 269 * String, key for looking up the localized string in the bundle 270 * @param aNumber 271 * Number based on which the final localized form is looked up 272 * @param aParams 273 * Array whose items will substitute #1, #2,... #n parameters 274 * in the string. 275 * 276 * @see https://developer.mozilla.org/en/Localization_and_Plurals 277 * @return The localized plural string. 278 */ 279 getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) { 280 let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey)); 281 282 // Replace #1 with aParams[0], #2 with aParams[1], and so on. 283 return str.replace(/\#(\d+)/g, function(matchedId, matchedNumber) { 284 let param = aParams[parseInt(matchedNumber, 10) - 1]; 285 return param !== undefined ? param : matchedId; 286 }); 287 }, 288 289 getString: function PUIU_getString(key) { 290 return bundle.GetStringFromName(key); 291 }, 292 293 /** 294 * Shows the bookmark dialog corresponding to the specified info. 295 * 296 * @param {object} aInfo 297 * Describes the item to be edited/added in the dialog. 298 * See documentation at the top of bookmarkProperties.js 299 * @param {DOMWindow} [aParentWindow] 300 * Owner window for the new dialog. 301 * 302 * @see documentation at the top of bookmarkProperties.js 303 * @return The guid of the item that was created or edited, undefined otherwise. 304 */ 305 showBookmarkDialog(aInfo, aParentWindow = null) { 306 // Preserve size attributes differently based on the fact the dialog has 307 // a folder picker or not, since it needs more horizontal space than the 308 // other controls. 309 let hasFolderPicker = 310 !("hiddenRows" in aInfo) || !aInfo.hiddenRows.includes("folderPicker"); 311 // Use a different chrome url to persist different sizes. 312 let dialogURL = hasFolderPicker 313 ? "chrome://browser/content/places/bookmarkProperties2.xhtml" 314 : "chrome://browser/content/places/bookmarkProperties.xhtml"; 315 316 let features = "centerscreen,chrome,modal,resizable=yes"; 317 318 let topUndoEntry; 319 let batchBlockingDeferred; 320 321 // Set the transaction manager into batching mode. 322 topUndoEntry = PlacesTransactions.topUndoEntry; 323 batchBlockingDeferred = PromiseUtils.defer(); 324 PlacesTransactions.batch(async () => { 325 await batchBlockingDeferred.promise; 326 }); 327 328 if (!aParentWindow) { 329 aParentWindow = Services.wm.getMostRecentWindow(null); 330 } 331 332 aParentWindow.openDialog(dialogURL, "", features, aInfo); 333 334 let bookmarkGuid = 335 ("bookmarkGuid" in aInfo && aInfo.bookmarkGuid) || undefined; 336 337 batchBlockingDeferred.resolve(); 338 339 if (!bookmarkGuid && topUndoEntry != PlacesTransactions.topUndoEntry) { 340 PlacesTransactions.undo().catch(Cu.reportError); 341 } 342 343 return bookmarkGuid; 344 }, 345 346 /** 347 * Bookmarks one or more pages. If there is more than one, this will create 348 * the bookmarks in a new folder. 349 * 350 * @param {array.<nsIURI>} URIList 351 * The list of URIs to bookmark. 352 * @param {array.<string>} [hiddenRows] 353 * An array of rows to be hidden. 354 * @param {DOMWindow} [window] 355 * The window to use as the parent to display the bookmark dialog. 356 */ 357 showBookmarkPagesDialog(URIList, hiddenRows = [], win = null) { 358 if (!URIList.length) { 359 return; 360 } 361 362 const bookmarkDialogInfo = { action: "add", hiddenRows }; 363 if (URIList.length > 1) { 364 bookmarkDialogInfo.type = "folder"; 365 bookmarkDialogInfo.URIList = URIList; 366 } else { 367 bookmarkDialogInfo.type = "bookmark"; 368 bookmarkDialogInfo.title = URIList[0].title; 369 bookmarkDialogInfo.uri = URIList[0].uri; 370 } 371 372 PlacesUIUtils.showBookmarkDialog(bookmarkDialogInfo, win); 373 }, 374 375 /** 376 * set and fetch a favicon. Can only be used from the parent process. 377 * @param browser {Browser} The XUL browser element for which we're fetching a favicon. 378 * @param principal {Principal} The loading principal to use for the fetch. 379 * @pram pageURI {URI} The page URI associated to this favicon load. 380 * @param uri {URI} The URI to fetch. 381 * @param expiration {Number} An optional expiration time. 382 * @param iconURI {URI} An optional data: URI holding the icon's data. 383 */ 384 loadFavicon( 385 browser, 386 principal, 387 pageURI, 388 uri, 389 expiration = 0, 390 iconURI = null 391 ) { 392 if (gInContentProcess) { 393 throw new Error("Can't track loads from within the child process!"); 394 } 395 InternalFaviconLoader.loadFavicon( 396 browser, 397 principal, 398 pageURI, 399 uri, 400 expiration, 401 iconURI 402 ); 403 }, 404 405 /** 406 * Returns the closet ancestor places view for the given DOM node 407 * @param aNode 408 * a DOM node 409 * @return the closet ancestor places view if exists, null otherwsie. 410 */ 411 getViewForNode: function PUIU_getViewForNode(aNode) { 412 let node = aNode; 413 414 if (Cu.isDeadWrapper(node)) { 415 return null; 416 } 417 418 if (node.localName == "panelview" && node._placesView) { 419 return node._placesView; 420 } 421 422 // The view for a <menu> of which its associated menupopup is a places 423 // view, is the menupopup. 424 if ( 425 node.localName == "menu" && 426 !node._placesNode && 427 node.menupopup._placesView 428 ) { 429 return node.menupopup._placesView; 430 } 431 432 while (Element.isInstance(node)) { 433 if (node._placesView) { 434 return node._placesView; 435 } 436 if ( 437 node.localName == "tree" && 438 node.getAttribute("is") == "places-tree" 439 ) { 440 return node; 441 } 442 443 node = node.parentNode; 444 } 445 446 return null; 447 }, 448 449 /** 450 * Returns the active PlacesController for a given command. 451 * 452 * @param win The window containing the affected view 453 * @param command The command 454 * @return a PlacesController 455 */ 456 getControllerForCommand(win, command) { 457 // A context menu may be built for non-focusable views. Thus, we first try 458 // to look for a view associated with document.popupNode 459 let popupNode; 460 try { 461 popupNode = win.document.popupNode; 462 } catch (e) { 463 // The document went away (bug 797307). 464 return null; 465 } 466 if (popupNode) { 467 let isManaged = !!popupNode.closest("#managed-bookmarks"); 468 if (isManaged) { 469 return this.managedBookmarksController; 470 } 471 let view = this.getViewForNode(popupNode); 472 if (view && view._contextMenuShown) { 473 return view.controllers.getControllerForCommand(command); 474 } 475 } 476 477 // When we're not building a context menu, only focusable views 478 // are possible. Thus, we can safely use the command dispatcher. 479 let controller = win.top.document.commandDispatcher.getControllerForCommand( 480 command 481 ); 482 return controller || null; 483 }, 484 485 /** 486 * Update all the Places commands for the given window. 487 * 488 * @param win The window to update. 489 */ 490 updateCommands(win) { 491 // Get the controller for one of the places commands. 492 let controller = this.getControllerForCommand(win, "placesCmd_open"); 493 for (let command of [ 494 "placesCmd_open", 495 "placesCmd_open:window", 496 "placesCmd_open:privatewindow", 497 "placesCmd_open:tab", 498 "placesCmd_new:folder", 499 "placesCmd_new:bookmark", 500 "placesCmd_new:separator", 501 "placesCmd_show:info", 502 "placesCmd_reload", 503 "placesCmd_sortBy:name", 504 "placesCmd_cut", 505 "placesCmd_copy", 506 "placesCmd_paste", 507 "placesCmd_delete", 508 ]) { 509 win.goSetCommandEnabled( 510 command, 511 controller && controller.isCommandEnabled(command) 512 ); 513 } 514 }, 515 516 /** 517 * Executes the given command on the currently active controller. 518 * 519 * @param win The window containing the affected view 520 * @param command The command to execute 521 */ 522 doCommand(win, command) { 523 let controller = this.getControllerForCommand(win, command); 524 if (controller && controller.isCommandEnabled(command)) { 525 controller.doCommand(command); 526 } 527 }, 528 529 /** 530 * By calling this before visiting an URL, the visit will be associated to a 531 * TRANSITION_TYPED transition (if there is no a referrer). 532 * This is used when visiting pages from the history menu, history sidebar, 533 * url bar, url autocomplete results, and history searches from the places 534 * organizer. If this is not called visits will be marked as 535 * TRANSITION_LINK. 536 */ 537 markPageAsTyped: function PUIU_markPageAsTyped(aURL) { 538 PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL)); 539 }, 540 541 /** 542 * By calling this before visiting an URL, the visit will be associated to a 543 * TRANSITION_BOOKMARK transition. 544 * This is used when visiting pages from the bookmarks menu, 545 * personal toolbar, and bookmarks from within the places organizer. 546 * If this is not called visits will be marked as TRANSITION_LINK. 547 */ 548 markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) { 549 PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL)); 550 }, 551 552 /** 553 * By calling this before visiting an URL, any visit in frames will be 554 * associated to a TRANSITION_FRAMED_LINK transition. 555 * This is actually used to distinguish user-initiated visits in frames 556 * so automatic visits can be correctly ignored. 557 */ 558 markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) { 559 PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL)); 560 }, 561 562 /** 563 * Sets the character-set for a page. The character set will not be saved 564 * if the window is determined to be a private browsing window. 565 * 566 * @param {string|URL|nsIURI} url The URL of the page to set the charset on. 567 * @param {String} charset character-set value. 568 * @param {window} window The window that the charset is being set from. 569 * @return {Promise} 570 */ 571 async setCharsetForPage(url, charset, window) { 572 if (PrivateBrowsingUtils.isWindowPrivate(window)) { 573 return; 574 } 575 576 // UTF-8 is the default. If we are passed the value then set it to null, 577 // to ensure any charset is removed from the database. 578 if (charset.toLowerCase() == "utf-8") { 579 charset = null; 580 } 581 582 await PlacesUtils.history.update({ 583 url, 584 annotations: new Map([[PlacesUtils.CHARSET_ANNO, charset]]), 585 }); 586 }, 587 588 /** 589 * Allows opening of javascript/data URI only if the given node is 590 * bookmarked (see bug 224521). 591 * @param aURINode 592 * a URI node 593 * @param aWindow 594 * a window on which a potential error alert is shown on. 595 * @return true if it's safe to open the node in the browser, false otherwise. 596 * 597 */ 598 checkURLSecurity: function PUIU_checkURLSecurity(aURINode, aWindow) { 599 if (PlacesUtils.nodeIsBookmark(aURINode)) { 600 return true; 601 } 602 603 var uri = Services.io.newURI(aURINode.uri); 604 if (uri.schemeIs("javascript") || uri.schemeIs("data")) { 605 const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; 606 var brandShortName = Services.strings 607 .createBundle(BRANDING_BUNDLE_URI) 608 .GetStringFromName("brandShortName"); 609 610 var errorStr = this.getString("load-js-data-url-error"); 611 Services.prompt.alert(aWindow, brandShortName, errorStr); 612 return false; 613 } 614 return true; 615 }, 616 617 /** 618 * Check whether or not the given node represents a removable entry (either in 619 * history or in bookmarks). 620 * 621 * @param aNode 622 * a node, except the root node of a query. 623 * @return true if the aNode represents a removable entry, false otherwise. 624 */ 625 canUserRemove(aNode) { 626 let parentNode = aNode.parent; 627 if (!parentNode) { 628 // canUserRemove doesn't accept root nodes. 629 return false; 630 } 631 632 // Is it a query pointing to one of the special root folders? 633 if (PlacesUtils.nodeIsQuery(parentNode)) { 634 if (PlacesUtils.nodeIsFolder(aNode)) { 635 let guid = PlacesUtils.getConcreteItemGuid(aNode); 636 // If the parent folder is not a folder, it must be a query, and so this node 637 // cannot be removed. 638 if (PlacesUtils.isRootItem(guid)) { 639 return false; 640 } 641 } else if (PlacesUtils.isVirtualLeftPaneItem(aNode.bookmarkGuid)) { 642 // If the item is a left-pane top-level item, it can't be removed. 643 return false; 644 } 645 } 646 647 // If it's not a bookmark, or it's child of a query, we can remove it. 648 if (aNode.itemId == -1 || PlacesUtils.nodeIsQuery(parentNode)) { 649 return true; 650 } 651 652 // Otherwise it has to be a child of an editable folder. 653 return !this.isFolderReadOnly(parentNode); 654 }, 655 656 /** 657 * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH 658 * TO GUIDS IS COMPLETE (BUG 1071511). 659 * 660 * Check whether or not the given Places node points to a folder which 661 * should not be modified by the user (i.e. its children should be unremovable 662 * and unmovable, new children should be disallowed, etc). 663 * These semantics are not inherited, meaning that read-only folder may 664 * contain editable items (for instance, the places root is read-only, but all 665 * of its direct children aren't). 666 * 667 * You should only pass folder nodes. 668 * 669 * @param placesNode 670 * any folder result node. 671 * @throws if placesNode is not a folder result node or views is invalid. 672 * @return true if placesNode is a read-only folder, false otherwise. 673 */ 674 isFolderReadOnly(placesNode) { 675 if ( 676 typeof placesNode != "object" || 677 !PlacesUtils.nodeIsFolder(placesNode) 678 ) { 679 throw new Error("invalid value for placesNode"); 680 } 681 682 return ( 683 PlacesUtils.getConcreteItemId(placesNode) == PlacesUtils.placesRootId 684 ); 685 }, 686 687 /** aItemsToOpen needs to be an array of objects of the form: 688 * {uri: string, isBookmark: boolean} 689 */ 690 openTabset(aItemsToOpen, aEvent, aWindow) { 691 if (!aItemsToOpen.length) { 692 return; 693 } 694 695 let browserWindow = getBrowserWindow(aWindow); 696 var urls = []; 697 let skipMarking = 698 browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow); 699 for (let item of aItemsToOpen) { 700 urls.push(item.uri); 701 if (skipMarking) { 702 continue; 703 } 704 705 if (item.isBookmark) { 706 this.markPageAsFollowedBookmark(item.uri); 707 } else { 708 this.markPageAsTyped(item.uri); 709 } 710 } 711 712 // whereToOpenLink doesn't return "window" when there's no browser window 713 // open (Bug 630255). 714 var where = browserWindow 715 ? browserWindow.whereToOpenLink(aEvent, false, true) 716 : "window"; 717 if (where == "window") { 718 // There is no browser window open, thus open a new one. 719 var uriList = PlacesUtils.toISupportsString(urls.join("|")); 720 var args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 721 args.appendElement(uriList); 722 browserWindow = Services.ww.openWindow( 723 aWindow, 724 AppConstants.BROWSER_CHROME_URL, 725 null, 726 "chrome,dialog=no,all", 727 args 728 ); 729 return; 730 } 731 732 var loadInBackground = where == "tabshifted"; 733 // For consistency, we want all the bookmarks to open in new tabs, instead 734 // of having one of them replace the currently focused tab. Hence we call 735 // loadTabs with aReplace set to false. 736 browserWindow.gBrowser.loadTabs(urls, { 737 inBackground: loadInBackground, 738 replace: false, 739 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 740 }); 741 }, 742 743 /** 744 * Loads a selected node's or nodes' URLs in tabs, 745 * warning the user when lots of URLs are being opened 746 * 747 * @param {object|array} nodeOrNodes 748 * Contains the node or nodes that we're opening in tabs 749 * @param {event} event 750 * The DOM mouse/key event with modifier keys set that track the 751 * user's preferred destination window or tab. 752 * @param {object} view 753 * The current view that contains the node or nodes selected for 754 * opening 755 */ 756 openMultipleLinksInTabs(nodeOrNodes, event, view) { 757 let window = view.ownerWindow; 758 let urlsToOpen = []; 759 760 if (PlacesUtils.nodeIsContainer(nodeOrNodes)) { 761 urlsToOpen = PlacesUtils.getURLsForContainerNode(nodeOrNodes); 762 } else { 763 for (var i = 0; i < nodeOrNodes.length; i++) { 764 // Skip over separators and folders. 765 if (PlacesUtils.nodeIsURI(nodeOrNodes[i])) { 766 urlsToOpen.push({ 767 uri: nodeOrNodes[i].uri, 768 isBookmark: PlacesUtils.nodeIsBookmark(nodeOrNodes[i]), 769 }); 770 } 771 } 772 } 773 if (OpenInTabsUtils.confirmOpenInTabs(urlsToOpen.length, window)) { 774 this.openTabset(urlsToOpen, event, window); 775 } 776 }, 777 778 /** 779 * Loads the node's URL in the appropriate tab or window given the 780 * user's preference specified by modifier keys tracked by a 781 * DOM mouse/key event. 782 * @param aNode 783 * An uri result node. 784 * @param aEvent 785 * The DOM mouse/key event with modifier keys set that track the 786 * user's preferred destination window or tab. 787 */ 788 openNodeWithEvent: function PUIU_openNodeWithEvent(aNode, aEvent) { 789 let window = aEvent.target.ownerGlobal; 790 791 let browserWindow = getBrowserWindow(window); 792 793 let where = window.whereToOpenLink(aEvent, false, true); 794 if (this.loadBookmarksInTabs && PlacesUtils.nodeIsBookmark(aNode)) { 795 if (where == "current" && !aNode.uri.startsWith("javascript:")) { 796 where = "tab"; 797 } 798 if (where == "tab" && browserWindow.gBrowser.selectedTab.isEmpty) { 799 where = "current"; 800 } 801 } 802 803 this._openNodeIn(aNode, where, window); 804 }, 805 806 /** 807 * Loads the node's URL in the appropriate tab or window. 808 * see also openUILinkIn 809 */ 810 openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) { 811 let window = aView.ownerWindow; 812 this._openNodeIn(aNode, aWhere, window, aPrivate); 813 }, 814 815 _openNodeIn: function PUIU__openNodeIn( 816 aNode, 817 aWhere, 818 aWindow, 819 aPrivate = false 820 ) { 821 if ( 822 aNode && 823 PlacesUtils.nodeIsURI(aNode) && 824 this.checkURLSecurity(aNode, aWindow) 825 ) { 826 let isBookmark = PlacesUtils.nodeIsBookmark(aNode); 827 828 if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) { 829 if (isBookmark) { 830 this.markPageAsFollowedBookmark(aNode.uri); 831 } else { 832 this.markPageAsTyped(aNode.uri); 833 } 834 } 835 836 const isJavaScriptURL = aNode.uri.startsWith("javascript:"); 837 aWindow.openTrustedLinkIn(aNode.uri, aWhere, { 838 allowPopups: isJavaScriptURL, 839 inBackground: this.loadBookmarksInBackground, 840 allowInheritPrincipal: isJavaScriptURL, 841 private: aPrivate, 842 }); 843 } 844 }, 845 846 /** 847 * Helper for guessing scheme from an url string. 848 * Used to avoid nsIURI overhead in frequently called UI functions. 849 * 850 * @param {string} href The url to guess the scheme from. 851 * @return guessed scheme for this url string. 852 * @note this is not supposed be perfect, so use it only for UI purposes. 853 */ 854 guessUrlSchemeForUI(href) { 855 return href.substr(0, href.indexOf(":")); 856 }, 857 858 getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) { 859 var title; 860 if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) { 861 // if node title is empty, try to set the label using host and filename 862 // Services.io.newURI will throw if aNode.uri is not a valid URI 863 try { 864 var uri = Services.io.newURI(aNode.uri); 865 var host = uri.host; 866 var fileName = uri.QueryInterface(Ci.nsIURL).fileName; 867 // if fileName is empty, use path to distinguish labels 868 if (aDoNotCutTitle) { 869 title = host + uri.pathQueryRef; 870 } else { 871 title = 872 host + 873 (fileName 874 ? (host ? "/" + this.ellipsis + "/" : "") + fileName 875 : uri.pathQueryRef); 876 } 877 } catch (e) { 878 // Use (no title) for non-standard URIs (data:, javascript:, ...) 879 title = ""; 880 } 881 } else { 882 title = aNode.title; 883 } 884 885 return title || this.getString("noTitle"); 886 }, 887 888 shouldShowTabsFromOtherComputersMenuitem() { 889 let weaveOK = 890 Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED && 891 Weave.Svc.Prefs.get("firstSync", "") != "notReady"; 892 return weaveOK; 893 }, 894 895 /** 896 * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A 897 * FUTURE RELEASE. 898 * 899 * Checks if a place: href represents a folder shortcut. 900 * 901 * @param queryString 902 * the query string to check (a place: href) 903 * @return whether or not queryString represents a folder shortcut. 904 * @throws if queryString is malformed. 905 */ 906 isFolderShortcutQueryString(queryString) { 907 // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp. 908 909 let query = {}, 910 options = {}; 911 PlacesUtils.history.queryStringToQuery(queryString, query, options); 912 query = query.value; 913 options = options.value; 914 return ( 915 query.folderCount == 1 && 916 !query.hasBeginTime && 917 !query.hasEndTime && 918 !query.hasDomain && 919 !query.hasURI && 920 !query.hasSearchTerms && 921 !query.tags.length == 0 && 922 options.maxResults == 0 923 ); 924 }, 925 926 /** 927 * Helpers for consumers of editBookmarkOverlay which don't have a node as their input. 928 * 929 * Given a bookmark object for either a url bookmark or a folder, returned by 930 * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for 931 * initialising the edit overlay with it. 932 * 933 * @param aFetchInfo 934 * a bookmark object returned by Bookmarks.fetch. 935 * @return a node-like object suitable for initialising editBookmarkOverlay. 936 * @throws if aFetchInfo is representing a separator. 937 */ 938 async promiseNodeLikeFromFetchInfo(aFetchInfo) { 939 if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) { 940 throw new Error("promiseNodeLike doesn't support separators"); 941 } 942 943 let parent = { 944 itemId: await PlacesUtils.promiseItemId(aFetchInfo.parentGuid), 945 bookmarkGuid: aFetchInfo.parentGuid, 946 type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER, 947 }; 948 949 return Object.freeze({ 950 itemId: await PlacesUtils.promiseItemId(aFetchInfo.guid), 951 bookmarkGuid: aFetchInfo.guid, 952 title: aFetchInfo.title, 953 uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "", 954 955 get type() { 956 if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER) { 957 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER; 958 } 959 960 if (!this.uri.length) { 961 throw new Error("Unexpected item type"); 962 } 963 964 if (/^place:/.test(this.uri)) { 965 if (this.isFolderShortcutQueryString(this.uri)) { 966 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT; 967 } 968 969 return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY; 970 } 971 972 return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI; 973 }, 974 975 get parent() { 976 return parent; 977 }, 978 }); 979 }, 980 981 /** 982 * This function wraps potentially large places transaction operations 983 * with batch notifications to the result node, hence switching the views 984 * to batch mode. 985 * 986 * @param {nsINavHistoryResult} resultNode The result node to turn on batching. 987 * @note If resultNode is not supplied, the function will pass-through to 988 * functionToWrap. 989 * @param {Integer} itemsBeingChanged The count of items being changed. If the 990 * count is lower than a threshold, then 991 * batching won't be set. 992 * @param {Function} functionToWrap The function to 993 */ 994 async batchUpdatesForNode(resultNode, itemsBeingChanged, functionToWrap) { 995 if (!resultNode) { 996 await functionToWrap(); 997 return; 998 } 999 1000 resultNode = resultNode.QueryInterface(Ci.nsINavBookmarkObserver); 1001 1002 if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) { 1003 resultNode.onBeginUpdateBatch(); 1004 } 1005 1006 try { 1007 await functionToWrap(); 1008 } finally { 1009 if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) { 1010 resultNode.onEndUpdateBatch(); 1011 } 1012 } 1013 }, 1014 1015 /** 1016 * Processes a set of transfer items that have been dropped or pasted. 1017 * Batching will be applied where necessary. 1018 * 1019 * @param {Array} items A list of unwrapped nodes to process. 1020 * @param {Object} insertionPoint The requested point for insertion. 1021 * @param {Boolean} doCopy Set to true to copy the items, false will move them 1022 * if possible. 1023 * @paramt {Object} view The view that should be used for batching. 1024 * @return {Array} Returns an empty array when the insertion point is a tag, else 1025 * returns an array of copied or moved guids. 1026 */ 1027 async handleTransferItems(items, insertionPoint, doCopy, view) { 1028 let transactions; 1029 let itemsCount; 1030 if (insertionPoint.isTag) { 1031 let urls = items.filter(item => "uri" in item).map(item => item.uri); 1032 itemsCount = urls.length; 1033 transactions = [ 1034 PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName }), 1035 ]; 1036 } else { 1037 let insertionIndex = await insertionPoint.getIndex(); 1038 itemsCount = items.length; 1039 transactions = getTransactionsForTransferItems( 1040 items, 1041 insertionIndex, 1042 insertionPoint.guid, 1043 !doCopy 1044 ); 1045 } 1046 1047 // Check if we actually have something to add, if we don't it probably wasn't 1048 // valid, or it was moving to the same location, so just ignore it. 1049 if (!transactions.length) { 1050 return []; 1051 } 1052 1053 let guidsToSelect = []; 1054 let resultForBatching = getResultForBatching(view); 1055 1056 // If we're inserting into a tag, we don't get the guid, so we'll just 1057 // pass the transactions direct to the batch function. 1058 let batchingItem = transactions; 1059 if (!insertionPoint.isTag) { 1060 // If we're not a tag, then we need to get the ids of the items to select. 1061 batchingItem = async () => { 1062 for (let transaction of transactions) { 1063 let result = await transaction.transact(); 1064 guidsToSelect = guidsToSelect.concat(result); 1065 } 1066 }; 1067 } 1068 1069 await this.batchUpdatesForNode(resultForBatching, itemsCount, async () => { 1070 await PlacesTransactions.batch(batchingItem); 1071 }); 1072 1073 return guidsToSelect; 1074 }, 1075 1076 onSidebarTreeClick(event) { 1077 // right-clicks are not handled here 1078 if (event.button == 2) { 1079 return; 1080 } 1081 1082 let tree = event.target.parentNode; 1083 let cell = tree.getCellAt(event.clientX, event.clientY); 1084 if (cell.row == -1 || cell.childElt == "twisty") { 1085 return; 1086 } 1087 1088 // getCoordsForCellItem returns the x coordinate in logical coordinates 1089 // (i.e., starting from the left and right sides in LTR and RTL modes, 1090 // respectively.) Therefore, we make sure to exclude the blank area 1091 // before the tree item icon (that is, to the left or right of it in 1092 // LTR and RTL modes, respectively) from the click target area. 1093 let win = tree.ownerGlobal; 1094 let rect = tree.getCoordsForCellItem(cell.row, cell.col, "image"); 1095 let isRTL = win.getComputedStyle(tree).direction == "rtl"; 1096 let mouseInGutter = isRTL ? event.clientX > rect.x : event.clientX < rect.x; 1097 1098 let metaKey = 1099 AppConstants.platform === "macosx" ? event.metaKey : event.ctrlKey; 1100 let modifKey = metaKey || event.shiftKey; 1101 let isContainer = tree.view.isContainer(cell.row); 1102 let openInTabs = 1103 isContainer && 1104 (event.button == 1 || (event.button == 0 && modifKey)) && 1105 PlacesUtils.hasChildURIs(tree.view.nodeForTreeIndex(cell.row)); 1106 1107 if (event.button == 0 && isContainer && !openInTabs) { 1108 tree.view.toggleOpenState(cell.row); 1109 } else if ( 1110 !mouseInGutter && 1111 openInTabs && 1112 event.originalTarget.localName == "treechildren" 1113 ) { 1114 tree.view.selection.select(cell.row); 1115 this.openMultipleLinksInTabs(tree.selectedNode, event, tree); 1116 } else if ( 1117 !mouseInGutter && 1118 !isContainer && 1119 event.originalTarget.localName == "treechildren" 1120 ) { 1121 // Clear all other selection since we're loading a link now. We must 1122 // do this *before* attempting to load the link since openURL uses 1123 // selection as an indication of which link to load. 1124 tree.view.selection.select(cell.row); 1125 this.openNodeWithEvent(tree.selectedNode, event); 1126 } 1127 }, 1128 1129 onSidebarTreeKeyPress(event) { 1130 let node = event.target.selectedNode; 1131 if (node) { 1132 if (event.keyCode == event.DOM_VK_RETURN) { 1133 this.openNodeWithEvent(node, event); 1134 } 1135 } 1136 }, 1137 1138 /** 1139 * The following function displays the URL of a node that is being 1140 * hovered over. 1141 */ 1142 onSidebarTreeMouseMove(event) { 1143 let treechildren = event.target; 1144 if (treechildren.localName != "treechildren") { 1145 return; 1146 } 1147 1148 let tree = treechildren.parentNode; 1149 let cell = tree.getCellAt(event.clientX, event.clientY); 1150 1151 // cell.row is -1 when the mouse is hovering an empty area within the tree. 1152 // To avoid showing a URL from a previously hovered node for a currently 1153 // hovered non-url node, we must clear the moused-over URL in these cases. 1154 if (cell.row != -1) { 1155 let node = tree.view.nodeForTreeIndex(cell.row); 1156 if (PlacesUtils.nodeIsURI(node)) { 1157 this.setMouseoverURL(node.uri, tree.ownerGlobal); 1158 return; 1159 } 1160 } 1161 this.setMouseoverURL("", tree.ownerGlobal); 1162 }, 1163 1164 setMouseoverURL(url, win) { 1165 // When the browser window is closed with an open sidebar, the sidebar 1166 // unload event happens after the browser's one. In this case 1167 // top.XULBrowserWindow has been nullified already. 1168 if (win.top.XULBrowserWindow) { 1169 win.top.XULBrowserWindow.setOverLink(url); 1170 } 1171 }, 1172 1173 async managedPlacesContextShowing(event) { 1174 let menupopup = event.target; 1175 let document = menupopup.ownerDocument; 1176 let window = menupopup.ownerGlobal; 1177 // We need to populate the submenus in order to have information 1178 // to show the context menu. 1179 if ( 1180 menupopup.triggerNode.id == "managed-bookmarks" && 1181 !menupopup.triggerNode.menupopup.hasAttribute("hasbeenopened") 1182 ) { 1183 await window.PlacesToolbarHelper.populateManagedBookmarks( 1184 menupopup.triggerNode.menupopup 1185 ); 1186 } 1187 let linkItems = [ 1188 "placesContext_open:newtab", 1189 "placesContext_open:newwindow", 1190 "placesContext_open:newprivatewindow", 1191 "placesContext_openSeparator", 1192 "placesContext_copy", 1193 ]; 1194 Array.from(menupopup.children).forEach(function(child) { 1195 if (!(child.id in linkItems)) { 1196 child.hidden = true; 1197 } 1198 }); 1199 // Store triggerNode in controller for checking if commands are enabled 1200 this.managedBookmarksController.triggerNode = menupopup.triggerNode; 1201 // Container in this context means a folder. 1202 let isFolder = menupopup.triggerNode.hasAttribute("container"); 1203 let openContainerInTabs_menuitem = document.getElementById( 1204 "placesContext_openContainer:tabs" 1205 ); 1206 if (isFolder) { 1207 // Disable the openContainerInTabs menuitem if there 1208 // are no children of the menu that have links. 1209 let menuitems = menupopup.triggerNode.menupopup.children; 1210 let openContainerInTabs = Array.from(menuitems).some( 1211 menuitem => menuitem.link 1212 ); 1213 openContainerInTabs_menuitem.disabled = !openContainerInTabs; 1214 } else { 1215 document.getElementById( 1216 "placesContext_open:newprivatewindow" 1217 ).hidden = PrivateBrowsingUtils.isWindowPrivate(window); 1218 } 1219 openContainerInTabs_menuitem.hidden = !isFolder; 1220 linkItems.forEach(id => (document.getElementById(id).hidden = isFolder)); 1221 1222 event.target.ownerGlobal.updateCommands("places"); 1223 }, 1224 1225 placesContextShowing(event) { 1226 let menupopup = event.target; 1227 let isManaged = !!menupopup.triggerNode.closest("#managed-bookmarks"); 1228 if (isManaged) { 1229 this.managedPlacesContextShowing(event); 1230 return true; 1231 } 1232 let document = menupopup.ownerDocument; 1233 menupopup._view = this.getViewForNode(document.popupNode); 1234 if (!this.openInTabClosesMenu) { 1235 document 1236 .getElementById("placesContext_open:newtab") 1237 .setAttribute("closemenu", "single"); 1238 } 1239 return menupopup._view.buildContextMenu(menupopup); 1240 }, 1241 1242 placesContextHiding(event) { 1243 let menupopup = event.target; 1244 if (menupopup._view) { 1245 menupopup._view.destroyContextMenu(); 1246 } 1247 }, 1248 1249 openSelectionInTabs(event) { 1250 let isManaged = !!event.target.parentNode.triggerNode.closest( 1251 "#managed-bookmarks" 1252 ); 1253 let controller; 1254 if (isManaged) { 1255 controller = this.managedBookmarksController; 1256 } else { 1257 let document = event.target.ownerDocument; 1258 controller = PlacesUIUtils.getViewForNode(document.popupNode).controller; 1259 } 1260 controller.openSelectionInTabs(event); 1261 }, 1262 1263 managedBookmarksController: { 1264 triggerNode: null, 1265 1266 openSelectionInTabs(event) { 1267 let window = event.target.ownerGlobal; 1268 let menuitems = event.target.parentNode.triggerNode.menupopup.children; 1269 let items = []; 1270 for (let i = 0; i < menuitems.length; i++) { 1271 if (menuitems[i].link) { 1272 let item = {}; 1273 item.uri = menuitems[i].link; 1274 item.isBookmark = true; 1275 items.push(item); 1276 } 1277 } 1278 PlacesUIUtils.openTabset(items, event, window); 1279 }, 1280 1281 isCommandEnabled(command) { 1282 switch (command) { 1283 case "placesCmd_copy": 1284 case "placesCmd_open:window": 1285 case "placesCmd_open:privatewindow": 1286 case "placesCmd_open:tab": { 1287 return true; 1288 } 1289 } 1290 return false; 1291 }, 1292 1293 doCommand(command) { 1294 let window = this.triggerNode.ownerGlobal; 1295 switch (command) { 1296 case "placesCmd_copy": 1297 // This is a little hacky, but there is a lot of code in Places that handles 1298 // clipboard stuff, so it's easier to reuse. 1299 let node = {}; 1300 node.type = 0; 1301 node.title = this.triggerNode.label; 1302 node.uri = this.triggerNode.link; 1303 1304 // Copied from _populateClipboard in controller.js 1305 1306 // This order is _important_! It controls how this and other applications 1307 // select data to be inserted based on type. 1308 let contents = [ 1309 { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] }, 1310 { type: PlacesUtils.TYPE_HTML, entries: [] }, 1311 { type: PlacesUtils.TYPE_UNICODE, entries: [] }, 1312 ]; 1313 1314 contents.forEach(function(content) { 1315 content.entries.push(PlacesUtils.wrapNode(node, content.type)); 1316 }); 1317 1318 let xferable = Cc[ 1319 "@mozilla.org/widget/transferable;1" 1320 ].createInstance(Ci.nsITransferable); 1321 xferable.init(null); 1322 1323 function addData(type, data) { 1324 xferable.addDataFlavor(type); 1325 xferable.setTransferData( 1326 type, 1327 PlacesUtils.toISupportsString(data), 1328 data.length * 2 1329 ); 1330 } 1331 1332 contents.forEach(function(content) { 1333 addData(content.type, content.entries.join(PlacesUtils.endl)); 1334 }); 1335 1336 Services.clipboard.setData( 1337 xferable, 1338 null, 1339 Ci.nsIClipboard.kGlobalClipboard 1340 ); 1341 break; 1342 case "placesCmd_open:privatewindow": 1343 window.openUILinkIn(this.triggerNode.link, "window", { 1344 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 1345 private: true, 1346 }); 1347 break; 1348 case "placesCmd_open:window": 1349 window.openUILinkIn(this.triggerNode.link, "window", { 1350 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 1351 private: false, 1352 }); 1353 break; 1354 case "placesCmd_open:tab": { 1355 window.openUILinkIn(this.triggerNode.link, "tab", { 1356 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 1357 }); 1358 } 1359 } 1360 }, 1361 }, 1362}; 1363 1364// These are lazy getters to avoid importing PlacesUtils immediately. 1365XPCOMUtils.defineLazyGetter(PlacesUIUtils, "PLACES_FLAVORS", () => { 1366 return [ 1367 PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, 1368 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, 1369 PlacesUtils.TYPE_X_MOZ_PLACE, 1370 ]; 1371}); 1372XPCOMUtils.defineLazyGetter(PlacesUIUtils, "URI_FLAVORS", () => { 1373 return [PlacesUtils.TYPE_X_MOZ_URL, TAB_DROP_TYPE, PlacesUtils.TYPE_UNICODE]; 1374}); 1375XPCOMUtils.defineLazyGetter(PlacesUIUtils, "SUPPORTED_FLAVORS", () => { 1376 return [...PlacesUIUtils.PLACES_FLAVORS, ...PlacesUIUtils.URI_FLAVORS]; 1377}); 1378 1379XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() { 1380 return Services.prefs.getComplexValue( 1381 "intl.ellipsis", 1382 Ci.nsIPrefLocalizedString 1383 ).data; 1384}); 1385 1386XPCOMUtils.defineLazyPreferenceGetter( 1387 PlacesUIUtils, 1388 "loadBookmarksInBackground", 1389 PREF_LOAD_BOOKMARKS_IN_BACKGROUND, 1390 false 1391); 1392XPCOMUtils.defineLazyPreferenceGetter( 1393 PlacesUIUtils, 1394 "loadBookmarksInTabs", 1395 PREF_LOAD_BOOKMARKS_IN_TABS, 1396 false 1397); 1398XPCOMUtils.defineLazyPreferenceGetter( 1399 PlacesUIUtils, 1400 "openInTabClosesMenu", 1401 "browser.bookmarks.openInTabClosesMenu", 1402 false 1403); 1404XPCOMUtils.defineLazyPreferenceGetter( 1405 PlacesUIUtils, 1406 "maxRecentFolders", 1407 "browser.bookmarks.editDialog.maxRecentFolders", 1408 7 1409); 1410 1411/** 1412 * Determines if an unwrapped node can be moved. 1413 * 1414 * @param unwrappedNode 1415 * A node unwrapped by PlacesUtils.unwrapNodes(). 1416 * @return True if the node can be moved, false otherwise. 1417 */ 1418function canMoveUnwrappedNode(unwrappedNode) { 1419 if ( 1420 (unwrappedNode.concreteGuid && 1421 PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) || 1422 (unwrappedNode.guid && PlacesUtils.isRootItem(unwrappedNode.guid)) 1423 ) { 1424 return false; 1425 } 1426 1427 let parentGuid = unwrappedNode.parentGuid; 1428 if (parentGuid == PlacesUtils.bookmarks.rootGuid) { 1429 return false; 1430 } 1431 1432 return true; 1433} 1434 1435/** 1436 * This gets the most appropriate item for using for batching. In the case of multiple 1437 * views being related, the method returns the most expensive result to batch. 1438 * For example, if it detects the left-hand library pane, then it will look for 1439 * and return the reference to the right-hand pane. 1440 * 1441 * @param {Object} viewOrElement The item to check. 1442 * @return {Object} Will return the best result node to batch, or null 1443 * if one could not be found. 1444 */ 1445function getResultForBatching(viewOrElement) { 1446 if ( 1447 viewOrElement && 1448 Element.isInstance(viewOrElement) && 1449 viewOrElement.id === "placesList" 1450 ) { 1451 // Note: fall back to the existing item if we can't find the right-hane pane. 1452 viewOrElement = 1453 viewOrElement.ownerDocument.getElementById("placeContent") || 1454 viewOrElement; 1455 } 1456 1457 if (viewOrElement && viewOrElement.result) { 1458 return viewOrElement.result; 1459 } 1460 1461 return null; 1462} 1463 1464/** 1465 * Processes a set of transfer items and returns transactions to insert or 1466 * move them. 1467 * 1468 * @param {Array} items A list of unwrapped nodes to get transactions for. 1469 * @param {Integer} insertionIndex The requested index for insertion. 1470 * @param {String} insertionParentGuid The guid of the parent folder to insert 1471 * or move the items to. 1472 * @param {Boolean} doMove Set to true to MOVE the items if possible, false will 1473 * copy them. 1474 * @return {Array} Returns an array of created PlacesTransactions. 1475 */ 1476function getTransactionsForTransferItems( 1477 items, 1478 insertionIndex, 1479 insertionParentGuid, 1480 doMove 1481) { 1482 let canMove = true; 1483 for (let item of items) { 1484 if (!PlacesUIUtils.SUPPORTED_FLAVORS.includes(item.type)) { 1485 throw new Error(`Unsupported '${item.type}' data type`); 1486 } 1487 1488 // Work out if this is data from the same app session we're running in. 1489 if (!("instanceId" in item) || item.instanceId != PlacesUtils.instanceId) { 1490 if (item.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { 1491 throw new Error( 1492 "Can't copy a container from a legacy-transactions build" 1493 ); 1494 } 1495 // Only log if this is one of "our" types as external items, e.g. drag from 1496 // url bar to toolbar, shouldn't complain. 1497 if (PlacesUIUtils.PLACES_FLAVORS.includes(item.type)) { 1498 Cu.reportError( 1499 "Tried to move an unmovable Places " + 1500 "node, reverting to a copy operation." 1501 ); 1502 } 1503 1504 // We can never move from an external copy. 1505 canMove = false; 1506 } 1507 1508 if (doMove && canMove) { 1509 canMove = canMoveUnwrappedNode(item); 1510 } 1511 } 1512 1513 if (doMove && !canMove) { 1514 doMove = false; 1515 } 1516 1517 if (doMove) { 1518 // Move is simple, we pass the transaction a list of GUIDs and where to move 1519 // them to. 1520 return [ 1521 PlacesTransactions.Move({ 1522 guids: items.map(item => item.itemGuid), 1523 newParentGuid: insertionParentGuid, 1524 newIndex: insertionIndex, 1525 }), 1526 ]; 1527 } 1528 1529 return getTransactionsForCopy(items, insertionIndex, insertionParentGuid); 1530} 1531 1532/** 1533 * Processes a set of transfer items and returns an array of transactions. 1534 * 1535 * @param {Array} items A list of unwrapped nodes to get transactions for. 1536 * @param {Integer} insertionIndex The requested index for insertion. 1537 * @param {String} insertionParentGuid The guid of the parent folder to insert 1538 * or move the items to. 1539 * @return {Array} Returns an array of created PlacesTransactions. 1540 */ 1541function getTransactionsForCopy(items, insertionIndex, insertionParentGuid) { 1542 let transactions = []; 1543 let index = insertionIndex; 1544 1545 for (let item of items) { 1546 let transaction; 1547 let guid = item.itemGuid; 1548 1549 if ( 1550 PlacesUIUtils.PLACES_FLAVORS.includes(item.type) && 1551 // For anything that is comming from within this session, we do a 1552 // direct copy, otherwise we fallback and form a new item below. 1553 "instanceId" in item && 1554 item.instanceId == PlacesUtils.instanceId && 1555 // If the Item doesn't have a guid, this could be a virtual tag query or 1556 // other item, so fallback to inserting a new bookmark with the URI. 1557 guid && 1558 // For virtual root items, we fallback to creating a new bookmark, as 1559 // we want a shortcut to be created, not a full tree copy. 1560 !PlacesUtils.bookmarks.isVirtualRootItem(guid) && 1561 !PlacesUtils.isVirtualLeftPaneItem(guid) 1562 ) { 1563 transaction = PlacesTransactions.Copy({ 1564 guid, 1565 newIndex: index, 1566 newParentGuid: insertionParentGuid, 1567 }); 1568 } else if (item.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) { 1569 transaction = PlacesTransactions.NewSeparator({ 1570 index, 1571 parentGuid: insertionParentGuid, 1572 }); 1573 } else { 1574 let title = item.type != PlacesUtils.TYPE_UNICODE ? item.title : item.uri; 1575 transaction = PlacesTransactions.NewBookmark({ 1576 index, 1577 parentGuid: insertionParentGuid, 1578 title, 1579 url: item.uri, 1580 }); 1581 } 1582 1583 transactions.push(transaction); 1584 1585 if (index != -1) { 1586 index++; 1587 } 1588 } 1589 return transactions; 1590} 1591 1592function getBrowserWindow(aWindow) { 1593 // Prefer the caller window if it's a browser window, otherwise use 1594 // the top browser window. 1595 return aWindow && 1596 aWindow.document.documentElement.getAttribute("windowtype") == 1597 "navigator:browser" 1598 ? aWindow 1599 : BrowserWindowTracker.getTopWindow(); 1600} 1601