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 8ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); 9ChromeUtils.import("resource://gre/modules/Services.jsm"); 10ChromeUtils.import("resource://gre/modules/Timer.jsm"); 11 12XPCOMUtils.defineLazyModuleGetters(this, { 13 OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm", 14 PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", 15 PluralForm: "resource://gre/modules/PluralForm.jsm", 16 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", 17 RecentWindow: "resource:///modules/RecentWindow.jsm", 18 PromiseUtils: "resource://gre/modules/PromiseUtils.jsm", 19 PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm", 20 Weave: "resource://services-sync/main.js", 21}); 22 23XPCOMUtils.defineLazyGetter(this, "bundle", function() { 24 return Services.strings.createBundle("chrome://browser/locale/places/places.properties"); 25}); 26 27const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; 28const FAVICON_REQUEST_TIMEOUT = 60 * 1000; 29// Map from windows to arrays of data about pending favicon loads. 30let gFaviconLoadDataMap = new Map(); 31 32const ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD = 10; 33 34// copied from utilityOverlay.js 35const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab"; 36const PREF_LOAD_BOOKMARKS_IN_BACKGROUND = "browser.tabs.loadBookmarksInBackground"; 37const PREF_LOAD_BOOKMARKS_IN_TABS = "browser.tabs.loadBookmarksInTabs"; 38 39let InternalFaviconLoader = { 40 /** 41 * This gets called for every inner window that is destroyed. 42 * In the parent process, we process the destruction ourselves. In the child process, 43 * we notify the parent which will then process it based on that message. 44 */ 45 observe(subject, topic, data) { 46 let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; 47 this.removeRequestsForInner(innerWindowID); 48 }, 49 50 /** 51 * Actually cancel the request, and clear the timeout for cancelling it. 52 */ 53 _cancelRequest({uri, innerWindowID, timerID, callback}, reason) { 54 // Break cycle 55 let request = callback.request; 56 delete callback.request; 57 // Ensure we don't time out. 58 clearTimeout(timerID); 59 try { 60 request.cancel(); 61 } catch (ex) { 62 Cu.reportError("When cancelling a request for " + uri.spec + " because " + reason + ", it was already canceled!"); 63 } 64 }, 65 66 /** 67 * Called for every inner that gets destroyed, only in the parent process. 68 */ 69 removeRequestsForInner(innerID) { 70 for (let [window, loadDataForWindow] of gFaviconLoadDataMap) { 71 let newLoadDataForWindow = loadDataForWindow.filter(loadData => { 72 let innerWasDestroyed = loadData.innerWindowID == innerID; 73 if (innerWasDestroyed) { 74 this._cancelRequest(loadData, "the inner window was destroyed or a new favicon was loaded for it"); 75 } 76 // Keep the items whose inner is still alive. 77 return !innerWasDestroyed; 78 }); 79 // Map iteration with for...of is safe against modification, so 80 // now just replace the old value: 81 gFaviconLoadDataMap.set(window, newLoadDataForWindow); 82 } 83 }, 84 85 /** 86 * Called when a toplevel chrome window unloads. We use this to tidy up after ourselves, 87 * avoid leaks, and cancel any remaining requests. The last part should in theory be 88 * handled by the inner-window-destroyed handlers. We clean up just to be on the safe side. 89 */ 90 onUnload(win) { 91 let loadDataForWindow = gFaviconLoadDataMap.get(win); 92 if (loadDataForWindow) { 93 for (let loadData of loadDataForWindow) { 94 this._cancelRequest(loadData, "the chrome window went away"); 95 } 96 } 97 gFaviconLoadDataMap.delete(win); 98 }, 99 100 /** 101 * Remove a particular favicon load's loading data from our map tracking 102 * load data per chrome window. 103 * 104 * @param win 105 * the chrome window in which we should look for this load 106 * @param filterData ({innerWindowID, uri, callback}) 107 * the data we should use to find this particular load to remove. 108 * 109 * @return the loadData object we removed, or null if we didn't find any. 110 */ 111 _removeLoadDataFromWindowMap(win, {innerWindowID, uri, callback}) { 112 let loadDataForWindow = gFaviconLoadDataMap.get(win); 113 if (loadDataForWindow) { 114 let itemIndex = loadDataForWindow.findIndex(loadData => { 115 return loadData.innerWindowID == innerWindowID && 116 loadData.uri.equals(uri) && 117 loadData.callback.request == callback.request; 118 }); 119 if (itemIndex != -1) { 120 let loadData = loadDataForWindow[itemIndex]; 121 loadDataForWindow.splice(itemIndex, 1); 122 return loadData; 123 } 124 } 125 return null; 126 }, 127 128 /** 129 * Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling 130 * information when the request succeeds. Note that right now there are some edge-cases, 131 * such as about: URIs with chrome:// favicons where the success callback is not invoked. 132 * This is OK: we will 'cancel' the request after the timeout (or when the window goes 133 * away) but that will be a no-op in such cases. 134 */ 135 _makeCompletionCallback(win, id) { 136 return { 137 onComplete(uri) { 138 let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, { 139 uri, 140 innerWindowID: id, 141 callback: this, 142 }); 143 if (loadData) { 144 clearTimeout(loadData.timerID); 145 } 146 delete this.request; 147 }, 148 }; 149 }, 150 151 ensureInitialized() { 152 if (this._initialized) { 153 return; 154 } 155 this._initialized = true; 156 157 Services.obs.addObserver(this, "inner-window-destroyed"); 158 Services.ppmm.addMessageListener("Toolkit:inner-window-destroyed", msg => { 159 this.removeRequestsForInner(msg.data); 160 }); 161 }, 162 163 loadFavicon(browser, principal, uri, requestContextID) { 164 this.ensureInitialized(); 165 let win = browser.ownerGlobal; 166 if (!gFaviconLoadDataMap.has(win)) { 167 gFaviconLoadDataMap.set(win, []); 168 let unloadHandler = event => { 169 let doc = event.target; 170 let eventWin = doc.defaultView; 171 if (eventWin == win) { 172 win.removeEventListener("unload", unloadHandler); 173 this.onUnload(win); 174 } 175 }; 176 win.addEventListener("unload", unloadHandler, true); 177 } 178 179 let {innerWindowID, currentURI} = browser; 180 181 // First we do the actual setAndFetch call: 182 let loadType = PrivateBrowsingUtils.isWindowPrivate(win) 183 ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE 184 : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE; 185 let callback = this._makeCompletionCallback(win, innerWindowID); 186 let request = PlacesUtils.favicons.setAndFetchFaviconForPage(currentURI, uri, false, 187 loadType, callback, principal, 188 requestContextID); 189 190 // Now register the result so we can cancel it if/when necessary. 191 if (!request) { 192 // The favicon service can return with success but no-op (and leave request 193 // as null) if the icon is the same as the page (e.g. for images) or if it is 194 // the favicon for an error page. In this case, we do not need to do anything else. 195 return; 196 } 197 callback.request = request; 198 let loadData = {innerWindowID, uri, callback}; 199 loadData.timerID = setTimeout(() => { 200 this._cancelRequest(loadData, "it timed out"); 201 this._removeLoadDataFromWindowMap(win, loadData); 202 }, FAVICON_REQUEST_TIMEOUT); 203 let loadDataForWindow = gFaviconLoadDataMap.get(win); 204 loadDataForWindow.push(loadData); 205 }, 206}; 207 208var PlacesUIUtils = { 209 ORGANIZER_LEFTPANE_VERSION: 8, 210 ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder", 211 ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery", 212 213 LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar", 214 DESCRIPTION_ANNO: "bookmarkProperties/description", 215 216 /** 217 * Makes a URI from a spec, and do fixup 218 * @param aSpec 219 * The string spec of the URI 220 * @return A URI object for the spec. 221 */ 222 createFixedURI: function PUIU_createFixedURI(aSpec) { 223 return Services.uriFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE); 224 }, 225 226 getFormattedString: function PUIU_getFormattedString(key, params) { 227 return bundle.formatStringFromName(key, params, params.length); 228 }, 229 230 /** 231 * Get a localized plural string for the specified key name and numeric value 232 * substituting parameters. 233 * 234 * @param aKey 235 * String, key for looking up the localized string in the bundle 236 * @param aNumber 237 * Number based on which the final localized form is looked up 238 * @param aParams 239 * Array whose items will substitute #1, #2,... #n parameters 240 * in the string. 241 * 242 * @see https://developer.mozilla.org/en/Localization_and_Plurals 243 * @return The localized plural string. 244 */ 245 getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) { 246 let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey)); 247 248 // Replace #1 with aParams[0], #2 with aParams[1], and so on. 249 return str.replace(/\#(\d+)/g, function(matchedId, matchedNumber) { 250 let param = aParams[parseInt(matchedNumber, 10) - 1]; 251 return param !== undefined ? param : matchedId; 252 }); 253 }, 254 255 getString: function PUIU_getString(key) { 256 return bundle.GetStringFromName(key); 257 }, 258 259 /** 260 * Shows the bookmark dialog corresponding to the specified info. 261 * 262 * @param aInfo 263 * Describes the item to be edited/added in the dialog. 264 * See documentation at the top of bookmarkProperties.js 265 * @param aWindow 266 * Owner window for the new dialog. 267 * 268 * @see documentation at the top of bookmarkProperties.js 269 * @return true if any transaction has been performed, false otherwise. 270 */ 271 showBookmarkDialog(aInfo, aParentWindow) { 272 // Preserve size attributes differently based on the fact the dialog has 273 // a folder picker or not, since it needs more horizontal space than the 274 // other controls. 275 let hasFolderPicker = !("hiddenRows" in aInfo) || 276 !aInfo.hiddenRows.includes("folderPicker"); 277 // Use a different chrome url to persist different sizes. 278 let dialogURL = hasFolderPicker ? 279 "chrome://browser/content/places/bookmarkProperties2.xul" : 280 "chrome://browser/content/places/bookmarkProperties.xul"; 281 282 let features = "centerscreen,chrome,modal,resizable=yes"; 283 284 let topUndoEntry; 285 let batchBlockingDeferred; 286 287 // Set the transaction manager into batching mode. 288 topUndoEntry = PlacesTransactions.topUndoEntry; 289 batchBlockingDeferred = PromiseUtils.defer(); 290 PlacesTransactions.batch(async () => { 291 await batchBlockingDeferred.promise; 292 }); 293 294 aParentWindow.openDialog(dialogURL, "", features, aInfo); 295 296 let performed = ("performed" in aInfo && aInfo.performed); 297 298 batchBlockingDeferred.resolve(); 299 300 if (!performed && 301 topUndoEntry != PlacesTransactions.topUndoEntry) { 302 PlacesTransactions.undo().catch(Cu.reportError); 303 } 304 305 return performed; 306 }, 307 308 /** 309 * set and fetch a favicon. Can only be used from the parent process. 310 * @param browser {Browser} The XUL browser element for which we're fetching a favicon. 311 * @param principal {Principal} The loading principal to use for the fetch. 312 * @param uri {URI} The URI to fetch. 313 */ 314 loadFavicon(browser, principal, uri, requestContextID) { 315 if (gInContentProcess) { 316 throw new Error("Can't track loads from within the child process!"); 317 } 318 InternalFaviconLoader.loadFavicon(browser, principal, uri, requestContextID); 319 }, 320 321 /** 322 * Returns the closet ancestor places view for the given DOM node 323 * @param aNode 324 * a DOM node 325 * @return the closet ancestor places view if exists, null otherwsie. 326 */ 327 getViewForNode: function PUIU_getViewForNode(aNode) { 328 let node = aNode; 329 330 if (node.localName == "panelview" && node._placesView) { 331 return node._placesView; 332 } 333 334 // The view for a <menu> of which its associated menupopup is a places 335 // view, is the menupopup. 336 if (node.localName == "menu" && !node._placesNode && 337 node.lastChild._placesView) 338 return node.lastChild._placesView; 339 340 while (node instanceof Ci.nsIDOMElement) { 341 if (node._placesView) 342 return node._placesView; 343 if (node.localName == "tree" && node.getAttribute("type") == "places") 344 return node; 345 346 node = node.parentNode; 347 } 348 349 return null; 350 }, 351 352 /** 353 * Returns the active PlacesController for a given command. 354 * 355 * @param win The window containing the affected view 356 * @param command The command 357 * @return a PlacesController 358 */ 359 getControllerForCommand(win, command) { 360 // A context menu may be built for non-focusable views. Thus, we first try 361 // to look for a view associated with document.popupNode 362 let popupNode; 363 try { 364 popupNode = win.document.popupNode; 365 } catch (e) { 366 // The document went away (bug 797307). 367 return null; 368 } 369 if (popupNode) { 370 let view = this.getViewForNode(popupNode); 371 if (view && view._contextMenuShown) 372 return view.controllers.getControllerForCommand(command); 373 } 374 375 // When we're not building a context menu, only focusable views 376 // are possible. Thus, we can safely use the command dispatcher. 377 let controller = win.top.document.commandDispatcher 378 .getControllerForCommand(command); 379 return controller || null; 380 }, 381 382 /** 383 * Update all the Places commands for the given window. 384 * 385 * @param win The window to update. 386 */ 387 updateCommands(win) { 388 // Get the controller for one of the places commands. 389 let controller = this.getControllerForCommand(win, "placesCmd_open"); 390 for (let command of [ 391 "placesCmd_open", 392 "placesCmd_open:window", 393 "placesCmd_open:privatewindow", 394 "placesCmd_open:tab", 395 "placesCmd_new:folder", 396 "placesCmd_new:bookmark", 397 "placesCmd_new:separator", 398 "placesCmd_show:info", 399 "placesCmd_reload", 400 "placesCmd_sortBy:name", 401 "placesCmd_cut", 402 "placesCmd_copy", 403 "placesCmd_paste", 404 "placesCmd_delete", 405 ]) { 406 win.goSetCommandEnabled(command, 407 controller && controller.isCommandEnabled(command)); 408 } 409 }, 410 411 /** 412 * Executes the given command on the currently active controller. 413 * 414 * @param win The window containing the affected view 415 * @param command The command to execute 416 */ 417 doCommand(win, command) { 418 let controller = this.getControllerForCommand(win, command); 419 if (controller && controller.isCommandEnabled(command)) 420 controller.doCommand(command); 421 }, 422 423 /** 424 * By calling this before visiting an URL, the visit will be associated to a 425 * TRANSITION_TYPED transition (if there is no a referrer). 426 * This is used when visiting pages from the history menu, history sidebar, 427 * url bar, url autocomplete results, and history searches from the places 428 * organizer. If this is not called visits will be marked as 429 * TRANSITION_LINK. 430 */ 431 markPageAsTyped: function PUIU_markPageAsTyped(aURL) { 432 PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL)); 433 }, 434 435 /** 436 * By calling this before visiting an URL, the visit will be associated to a 437 * TRANSITION_BOOKMARK transition. 438 * This is used when visiting pages from the bookmarks menu, 439 * personal toolbar, and bookmarks from within the places organizer. 440 * If this is not called visits will be marked as TRANSITION_LINK. 441 */ 442 markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) { 443 PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL)); 444 }, 445 446 /** 447 * By calling this before visiting an URL, any visit in frames will be 448 * associated to a TRANSITION_FRAMED_LINK transition. 449 * This is actually used to distinguish user-initiated visits in frames 450 * so automatic visits can be correctly ignored. 451 */ 452 markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) { 453 PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL)); 454 }, 455 456 /** 457 * Allows opening of javascript/data URI only if the given node is 458 * bookmarked (see bug 224521). 459 * @param aURINode 460 * a URI node 461 * @param aWindow 462 * a window on which a potential error alert is shown on. 463 * @return true if it's safe to open the node in the browser, false otherwise. 464 * 465 */ 466 checkURLSecurity: function PUIU_checkURLSecurity(aURINode, aWindow) { 467 if (PlacesUtils.nodeIsBookmark(aURINode)) 468 return true; 469 470 var uri = Services.io.newURI(aURINode.uri); 471 if (uri.schemeIs("javascript") || uri.schemeIs("data")) { 472 const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties"; 473 var brandShortName = Services.strings. 474 createBundle(BRANDING_BUNDLE_URI). 475 GetStringFromName("brandShortName"); 476 477 var errorStr = this.getString("load-js-data-url-error"); 478 Services.prompt.alert(aWindow, brandShortName, errorStr); 479 return false; 480 } 481 return true; 482 }, 483 484 /** 485 * Get the description associated with a document, as specified in a <META> 486 * element. 487 * @param doc 488 * A DOM Document to get a description for 489 * @return A description string if a META element was discovered with a 490 * "description" or "httpequiv" attribute, empty string otherwise. 491 */ 492 getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) { 493 var metaElements = doc.getElementsByTagName("META"); 494 for (var i = 0; i < metaElements.length; ++i) { 495 if (metaElements[i].name.toLowerCase() == "description" || 496 metaElements[i].httpEquiv.toLowerCase() == "description") { 497 return metaElements[i].content; 498 } 499 } 500 return ""; 501 }, 502 503 /** 504 * Retrieve the description of an item 505 * @param aItemId 506 * item identifier 507 * @return the description of the given item, or an empty string if it is 508 * not set. 509 */ 510 getItemDescription: function PUIU_getItemDescription(aItemId) { 511 if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO)) 512 return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO); 513 return ""; 514 }, 515 516 /** 517 * Check whether or not the given node represents a removable entry (either in 518 * history or in bookmarks). 519 * 520 * @param aNode 521 * a node, except the root node of a query. 522 * @param aView 523 * The view originating the request. 524 * @return true if the aNode represents a removable entry, false otherwise. 525 */ 526 canUserRemove(aNode, aView) { 527 let parentNode = aNode.parent; 528 if (!parentNode) { 529 // canUserRemove doesn't accept root nodes. 530 return false; 531 } 532 533 // Is it a query pointing to one of the special root folders? 534 if (PlacesUtils.nodeIsQuery(parentNode) && PlacesUtils.nodeIsFolder(aNode)) { 535 let guid = PlacesUtils.getConcreteItemGuid(aNode); 536 // If the parent folder is not a folder, it must be a query, and so this node 537 // cannot be removed. 538 if (PlacesUtils.isRootItem(guid)) { 539 return false; 540 } 541 } 542 543 // If it's not a bookmark, we can remove it unless it's a child of a 544 // livemark. 545 if (aNode.itemId == -1) { 546 // Rather than executing a db query, checking the existence of the feedURI 547 // annotation, detect livemark children by the fact that they are the only 548 // direct non-bookmark children of bookmark folders. 549 return !PlacesUtils.nodeIsFolder(parentNode); 550 } 551 552 // Generally it's always possible to remove children of a query. 553 if (PlacesUtils.nodeIsQuery(parentNode)) 554 return true; 555 556 // Otherwise it has to be a child of an editable folder. 557 return !this.isFolderReadOnly(parentNode, aView); 558 }, 559 560 /** 561 * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH 562 * TO GUIDS IS COMPLETE (BUG 1071511). 563 * 564 * Check whether or not the given Places node points to a folder which 565 * should not be modified by the user (i.e. its children should be unremovable 566 * and unmovable, new children should be disallowed, etc). 567 * These semantics are not inherited, meaning that read-only folder may 568 * contain editable items (for instance, the places root is read-only, but all 569 * of its direct children aren't). 570 * 571 * You should only pass folder nodes. 572 * 573 * @param placesNode 574 * any folder result node. 575 * @param view 576 * The view originating the request. 577 * @throws if placesNode is not a folder result node or views is invalid. 578 * @note livemark "folders" are considered read-only (but see bug 1072833). 579 * @return true if placesNode is a read-only folder, false otherwise. 580 */ 581 isFolderReadOnly(placesNode, view) { 582 if (typeof placesNode != "object" || !PlacesUtils.nodeIsFolder(placesNode)) { 583 throw new Error("invalid value for placesNode"); 584 } 585 if (!view || typeof view != "object") { 586 throw new Error("invalid value for aView"); 587 } 588 let itemId = PlacesUtils.getConcreteItemId(placesNode); 589 if (itemId == PlacesUtils.placesRootId || 590 view.controller.hasCachedLivemarkInfo(placesNode)) 591 return true; 592 593 // leftPaneFolderId is a lazy getter 594 // performing at least a synchronous DB query (and on its very first call 595 // in a fresh profile, it also creates the entire structure). 596 // Therefore we don't want to this function, which is called very often by 597 // isCommandEnabled, to ever be the one that invokes it first, especially 598 // because isCommandEnabled may be called way before the left pane folder is 599 // even created (for example, if the user only uses the bookmarks menu or 600 // toolbar for managing bookmarks). To do so, we avoid comparing to those 601 // special folder if the lazy getter is still in place. This is safe merely 602 // because the only way to access the left pane contents goes through 603 // "resolving" the leftPaneFolderId getter. 604 if (typeof Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").get == "function") { 605 return false; 606 } 607 return itemId == this.leftPaneFolderId; 608 }, 609 610 /** aItemsToOpen needs to be an array of objects of the form: 611 * {uri: string, isBookmark: boolean} 612 */ 613 _openTabset: function PUIU__openTabset(aItemsToOpen, aEvent, aWindow) { 614 if (!aItemsToOpen.length) 615 return; 616 617 // Prefer the caller window if it's a browser window, otherwise use 618 // the top browser window. 619 var browserWindow = null; 620 browserWindow = 621 aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ? 622 aWindow : RecentWindow.getMostRecentBrowserWindow(); 623 624 var urls = []; 625 let skipMarking = browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow); 626 for (let item of aItemsToOpen) { 627 urls.push(item.uri); 628 if (skipMarking) { 629 continue; 630 } 631 632 if (item.isBookmark) 633 this.markPageAsFollowedBookmark(item.uri); 634 else 635 this.markPageAsTyped(item.uri); 636 } 637 638 // whereToOpenLink doesn't return "window" when there's no browser window 639 // open (Bug 630255). 640 var where = browserWindow ? 641 browserWindow.whereToOpenLink(aEvent, false, true) : "window"; 642 if (where == "window") { 643 // There is no browser window open, thus open a new one. 644 var uriList = PlacesUtils.toISupportsString(urls.join("|")); 645 var args = Cc["@mozilla.org/array;1"]. 646 createInstance(Ci.nsIMutableArray); 647 args.appendElement(uriList); 648 browserWindow = Services.ww.openWindow(aWindow, 649 "chrome://browser/content/browser.xul", 650 null, "chrome,dialog=no,all", args); 651 return; 652 } 653 654 var loadInBackground = where == "tabshifted"; 655 // For consistency, we want all the bookmarks to open in new tabs, instead 656 // of having one of them replace the currently focused tab. Hence we call 657 // loadTabs with aReplace set to false. 658 browserWindow.gBrowser.loadTabs(urls, { 659 inBackground: loadInBackground, 660 replace: false, 661 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 662 }); 663 }, 664 665 openLiveMarkNodesInTabs: 666 function PUIU_openLiveMarkNodesInTabs(aNode, aEvent, aView) { 667 let window = aView.ownerWindow; 668 669 PlacesUtils.livemarks.getLivemark({id: aNode.itemId}) 670 .then(aLivemark => { 671 let urlsToOpen = []; 672 673 let nodes = aLivemark.getNodesForContainer(aNode); 674 for (let node of nodes) { 675 urlsToOpen.push({uri: node.uri, isBookmark: false}); 676 } 677 678 if (OpenInTabsUtils.confirmOpenInTabs(urlsToOpen.length, window)) { 679 this._openTabset(urlsToOpen, aEvent, window); 680 } 681 }, Cu.reportError); 682 }, 683 684 openContainerNodeInTabs: 685 function PUIU_openContainerInTabs(aNode, aEvent, aView) { 686 let window = aView.ownerWindow; 687 688 let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode); 689 if (OpenInTabsUtils.confirmOpenInTabs(urlsToOpen.length, window)) { 690 this._openTabset(urlsToOpen, aEvent, window); 691 } 692 }, 693 694 openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent, aView) { 695 let window = aView.ownerWindow; 696 697 let urlsToOpen = []; 698 for (var i = 0; i < aNodes.length; i++) { 699 // Skip over separators and folders. 700 if (PlacesUtils.nodeIsURI(aNodes[i])) 701 urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])}); 702 } 703 this._openTabset(urlsToOpen, aEvent, window); 704 }, 705 706 /** 707 * Loads the node's URL in the appropriate tab or window or as a web 708 * panel given the user's preference specified by modifier keys tracked by a 709 * DOM mouse/key event. 710 * @param aNode 711 * An uri result node. 712 * @param aEvent 713 * The DOM mouse/key event with modifier keys set that track the 714 * user's preferred destination window or tab. 715 */ 716 openNodeWithEvent: 717 function PUIU_openNodeWithEvent(aNode, aEvent) { 718 let window = aEvent.target.ownerGlobal; 719 720 let where = window.whereToOpenLink(aEvent, false, true); 721 if (where == "current" && this.loadBookmarksInTabs && 722 PlacesUtils.nodeIsBookmark(aNode) && !aNode.uri.startsWith("javascript:")) { 723 where = "tab"; 724 } 725 726 this._openNodeIn(aNode, where, window); 727 let view = this.getViewForNode(aEvent.target); 728 if (view && view.controller.hasCachedLivemarkInfo(aNode.parent)) { 729 Services.telemetry.scalarAdd("browser.feeds.livebookmark_item_opened", 1); 730 } 731 }, 732 733 /** 734 * Loads the node's URL in the appropriate tab or window or as a 735 * web panel. 736 * see also openUILinkIn 737 */ 738 openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) { 739 let window = aView.ownerWindow; 740 this._openNodeIn(aNode, aWhere, window, aPrivate); 741 }, 742 743 _openNodeIn: function PUIU__openNodeIn(aNode, aWhere, aWindow, aPrivate = false) { 744 if (aNode && PlacesUtils.nodeIsURI(aNode) && 745 this.checkURLSecurity(aNode, aWindow)) { 746 let isBookmark = PlacesUtils.nodeIsBookmark(aNode); 747 748 if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) { 749 if (isBookmark) 750 this.markPageAsFollowedBookmark(aNode.uri); 751 else 752 this.markPageAsTyped(aNode.uri); 753 } 754 755 // Check whether the node is a bookmark which should be opened as 756 // a web panel 757 if (aWhere == "current" && isBookmark) { 758 if (PlacesUtils.annotations 759 .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) { 760 let browserWin = RecentWindow.getMostRecentBrowserWindow(); 761 if (browserWin) { 762 browserWin.openWebPanel(aNode.title, aNode.uri); 763 return; 764 } 765 } 766 } 767 768 aWindow.openUILinkIn(aNode.uri, aWhere, { 769 allowPopups: aNode.uri.startsWith("javascript:"), 770 inBackground: this.loadBookmarksInBackground, 771 private: aPrivate, 772 }); 773 } 774 }, 775 776 /** 777 * Helper for guessing scheme from an url string. 778 * Used to avoid nsIURI overhead in frequently called UI functions. 779 * 780 * @param aUrlString the url to guess the scheme from. 781 * 782 * @return guessed scheme for this url string. 783 * 784 * @note this is not supposed be perfect, so use it only for UI purposes. 785 */ 786 guessUrlSchemeForUI: function PUIU_guessUrlSchemeForUI(aUrlString) { 787 return aUrlString.substr(0, aUrlString.indexOf(":")); 788 }, 789 790 getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) { 791 var title; 792 if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) { 793 // if node title is empty, try to set the label using host and filename 794 // Services.io.newURI will throw if aNode.uri is not a valid URI 795 try { 796 var uri = Services.io.newURI(aNode.uri); 797 var host = uri.host; 798 var fileName = uri.QueryInterface(Ci.nsIURL).fileName; 799 // if fileName is empty, use path to distinguish labels 800 if (aDoNotCutTitle) { 801 title = host + uri.pathQueryRef; 802 } else { 803 title = host + (fileName ? 804 (host ? "/" + this.ellipsis + "/" : "") + fileName : 805 uri.pathQueryRef); 806 } 807 } catch (e) { 808 // Use (no title) for non-standard URIs (data:, javascript:, ...) 809 title = ""; 810 } 811 } else 812 title = aNode.title; 813 814 return title || this.getString("noTitle"); 815 }, 816 817 get leftPaneQueries() { 818 // build the map 819 this.leftPaneFolderId; 820 return this.leftPaneQueries; 821 }, 822 823 get leftPaneFolderId() { 824 delete this.leftPaneFolderId; 825 return this.leftPaneFolderId = this.maybeRebuildLeftPane(); 826 }, 827 828 // Get the folder id for the organizer left-pane folder. 829 maybeRebuildLeftPane() { 830 let leftPaneRoot = -1; 831 832 // Shortcuts to services. 833 let bs = PlacesUtils.bookmarks; 834 let as = PlacesUtils.annotations; 835 836 // This is the list of the left pane queries. 837 let queries = { 838 "PlacesRoot": { title: "" }, 839 "History": { title: this.getString("OrganizerQueryHistory") }, 840 "Downloads": { title: this.getString("OrganizerQueryDownloads") }, 841 "Tags": { title: this.getString("OrganizerQueryTags") }, 842 "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") }, 843 }; 844 // All queries but PlacesRoot. 845 const EXPECTED_QUERY_COUNT = 4; 846 847 // Removes an item and associated annotations, ignoring eventual errors. 848 function safeRemoveItem(aItemId) { 849 try { 850 if (as.itemHasAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) && 851 !(as.getItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) in queries)) { 852 // Some extension annotated their roots with our query annotation, 853 // so we should not delete them. 854 return; 855 } 856 // removeItemAnnotation does not check if item exists, nor the anno, 857 // so this is safe to do. 858 as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO); 859 as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO); 860 // This will throw if the annotation is an orphan. 861 bs.removeItem(aItemId); 862 } catch (e) { /* orphan anno */ } 863 } 864 865 // Returns true if item really exists, false otherwise. 866 function itemExists(aItemId) { 867 try { 868 bs.getFolderIdForItem(aItemId); 869 return true; 870 } catch (e) { 871 return false; 872 } 873 } 874 875 // Get all items marked as being the left pane folder. 876 let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO); 877 if (items.length > 1) { 878 // Something went wrong, we cannot have more than one left pane folder, 879 // remove all left pane folders and continue. We will create a new one. 880 items.forEach(safeRemoveItem); 881 } else if (items.length == 1 && items[0] != -1) { 882 leftPaneRoot = items[0]; 883 // Check that organizer left pane root is valid. 884 let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO); 885 if (version != this.ORGANIZER_LEFTPANE_VERSION || 886 !itemExists(leftPaneRoot)) { 887 // Invalid root, we must rebuild the left pane. 888 safeRemoveItem(leftPaneRoot); 889 leftPaneRoot = -1; 890 } 891 } 892 893 if (leftPaneRoot != -1) { 894 // A valid left pane folder has been found. 895 // Build the leftPaneQueries Map. This is used to quickly access them, 896 // associating a mnemonic name to the real item ids. 897 delete this.leftPaneQueries; 898 this.leftPaneQueries = {}; 899 900 let queryItems = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO); 901 // While looping through queries we will also check for their validity. 902 let queriesCount = 0; 903 let corrupt = false; 904 for (let i = 0; i < queryItems.length; i++) { 905 let queryName = as.getItemAnnotation(queryItems[i], this.ORGANIZER_QUERY_ANNO); 906 907 // Some extension did use our annotation to decorate their items 908 // with icons, so we should check only our elements, to avoid dataloss. 909 if (!(queryName in queries)) 910 continue; 911 912 let query = queries[queryName]; 913 query.itemId = queryItems[i]; 914 915 if (!itemExists(query.itemId)) { 916 // Orphan annotation, bail out and create a new left pane root. 917 corrupt = true; 918 break; 919 } 920 921 // Check that all queries have valid parents. 922 let parentId = bs.getFolderIdForItem(query.itemId); 923 if (!queryItems.includes(parentId) && parentId != leftPaneRoot) { 924 // The parent is not part of the left pane, bail out and create a new 925 // left pane root. 926 corrupt = true; 927 break; 928 } 929 930 // Titles could have been corrupted or the user could have changed his 931 // locale. Check title and eventually fix it. 932 if (bs.getItemTitle(query.itemId) != query.title) 933 bs.setItemTitle(query.itemId, query.title); 934 if ("concreteId" in query) { 935 if (bs.getItemTitle(query.concreteId) != query.concreteTitle) 936 bs.setItemTitle(query.concreteId, query.concreteTitle); 937 } 938 939 // Add the query to our cache. 940 this.leftPaneQueries[queryName] = query.itemId; 941 queriesCount++; 942 } 943 944 // Note: it's not enough to just check for queriesCount, since we may 945 // find an invalid query just after accounting for a sufficient number of 946 // valid ones. As well as we can't just rely on corrupt since we may find 947 // less valid queries than expected. 948 if (corrupt || queriesCount != EXPECTED_QUERY_COUNT) { 949 // Queries number is wrong, so the left pane must be corrupt. 950 // Note: we can't just remove the leftPaneRoot, because some query could 951 // have a bad parent, so we have to remove all items one by one. 952 queryItems.forEach(safeRemoveItem); 953 safeRemoveItem(leftPaneRoot); 954 } else { 955 // Everything is fine, return the current left pane folder. 956 return leftPaneRoot; 957 } 958 } 959 960 // Create a new left pane folder. 961 var callback = { 962 // Helper to create an organizer special query. 963 create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) { 964 let itemId = bs.insertBookmark(aParentId, 965 Services.io.newURI(aQueryUrl), 966 bs.DEFAULT_INDEX, 967 queries[aQueryName].title); 968 // Mark as special organizer query. 969 as.setItemAnnotation(itemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aQueryName, 970 0, as.EXPIRE_NEVER); 971 // We should never backup this, since it changes between profiles. 972 as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 973 0, as.EXPIRE_NEVER); 974 // Add to the queries map. 975 PlacesUIUtils.leftPaneQueries[aQueryName] = itemId; 976 return itemId; 977 }, 978 979 // Helper to create an organizer special folder. 980 create_folder: function CB_create_folder(aFolderName, aParentId, aIsRoot) { 981 // Left Pane Root Folder. 982 let folderId = bs.createFolder(aParentId, 983 queries[aFolderName].title, 984 bs.DEFAULT_INDEX); 985 // We should never backup this, since it changes between profiles. 986 as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1, 987 0, as.EXPIRE_NEVER); 988 989 if (aIsRoot) { 990 // Mark as special left pane root. 991 as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO, 992 PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION, 993 0, as.EXPIRE_NEVER); 994 } else { 995 // Mark as special organizer folder. 996 as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName, 997 0, as.EXPIRE_NEVER); 998 PlacesUIUtils.leftPaneQueries[aFolderName] = folderId; 999 } 1000 return folderId; 1001 }, 1002 1003 runBatched: function CB_runBatched(aUserData) { 1004 delete PlacesUIUtils.leftPaneQueries; 1005 PlacesUIUtils.leftPaneQueries = { }; 1006 1007 // Left Pane Root Folder. 1008 leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true); 1009 1010 // History Query. 1011 this.create_query("History", leftPaneRoot, 1012 "place:type=" + 1013 Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY + 1014 "&sort=" + 1015 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); 1016 1017 // Downloads. 1018 this.create_query("Downloads", leftPaneRoot, 1019 "place:transition=" + 1020 Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + 1021 "&sort=" + 1022 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING); 1023 1024 // Tags Query. 1025 this.create_query("Tags", leftPaneRoot, 1026 "place:type=" + 1027 Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY + 1028 "&sort=" + 1029 Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING); 1030 1031 // All Bookmarks Folder. 1032 this.create_query("AllBookmarks", leftPaneRoot, 1033 "place:type=" + 1034 Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY); 1035 } 1036 }; 1037 bs.runInBatchMode(callback, null); 1038 1039 return leftPaneRoot; 1040 }, 1041 1042 /** 1043 * If an item is a left-pane query, returns the name of the query 1044 * or an empty string if not. 1045 * 1046 * @param aItemId id of a container 1047 * @return the name of the query, or empty string if not a left-pane query 1048 */ 1049 getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) { 1050 var queryName = ""; 1051 // If the let pane hasn't been built, use the annotation service 1052 // directly, to avoid building the left pane too early. 1053 if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) { 1054 try { 1055 queryName = PlacesUtils.annotations. 1056 getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO); 1057 } catch (ex) { 1058 // doesn't have the annotation 1059 queryName = ""; 1060 } 1061 } else { 1062 // If the left pane has already been built, use the name->id map 1063 // cached in PlacesUIUtils. 1064 for (let [name, id] of Object.entries(this.leftPaneQueries)) { 1065 if (aItemId == id) 1066 queryName = name; 1067 } 1068 } 1069 return queryName; 1070 }, 1071 1072 shouldShowTabsFromOtherComputersMenuitem() { 1073 let weaveOK = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED && 1074 Weave.Svc.Prefs.get("firstSync", "") != "notReady"; 1075 return weaveOK; 1076 }, 1077 1078 /** 1079 * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A 1080 * FUTURE RELEASE. 1081 * 1082 * Checks if a place: href represents a folder shortcut. 1083 * 1084 * @param queryString 1085 * the query string to check (a place: href) 1086 * @return whether or not queryString represents a folder shortcut. 1087 * @throws if queryString is malformed. 1088 */ 1089 isFolderShortcutQueryString(queryString) { 1090 // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp. 1091 1092 let queriesParam = { }, optionsParam = { }; 1093 PlacesUtils.history.queryStringToQueries(queryString, 1094 queriesParam, 1095 { }, 1096 optionsParam); 1097 let queries = queries.value; 1098 if (queries.length == 0) 1099 throw new Error(`Invalid place: uri: ${queryString}`); 1100 return queries.length == 1 && 1101 queries[0].folderCount == 1 && 1102 !queries[0].hasBeginTime && 1103 !queries[0].hasEndTime && 1104 !queries[0].hasDomain && 1105 !queries[0].hasURI && 1106 !queries[0].hasSearchTerms && 1107 !queries[0].tags.length == 0 && 1108 optionsParam.value.maxResults == 0; 1109 }, 1110 1111 /** 1112 * Helpers for consumers of editBookmarkOverlay which don't have a node as their input. 1113 * 1114 * Given a bookmark object for either a url bookmark or a folder, returned by 1115 * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for 1116 * initialising the edit overlay with it. 1117 * 1118 * @param aFetchInfo 1119 * a bookmark object returned by Bookmarks.fetch. 1120 * @return a node-like object suitable for initialising editBookmarkOverlay. 1121 * @throws if aFetchInfo is representing a separator. 1122 */ 1123 async promiseNodeLikeFromFetchInfo(aFetchInfo) { 1124 if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) 1125 throw new Error("promiseNodeLike doesn't support separators"); 1126 1127 let parent = { 1128 itemId: await PlacesUtils.promiseItemId(aFetchInfo.parentGuid), 1129 bookmarkGuid: aFetchInfo.parentGuid, 1130 type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER 1131 }; 1132 1133 return Object.freeze({ 1134 itemId: await PlacesUtils.promiseItemId(aFetchInfo.guid), 1135 bookmarkGuid: aFetchInfo.guid, 1136 title: aFetchInfo.title, 1137 uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "", 1138 1139 get type() { 1140 if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER) 1141 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER; 1142 1143 if (this.uri.length == 0) 1144 throw new Error("Unexpected item type"); 1145 1146 if (/^place:/.test(this.uri)) { 1147 if (this.isFolderShortcutQueryString(this.uri)) 1148 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT; 1149 1150 return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY; 1151 } 1152 1153 return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI; 1154 }, 1155 1156 get parent() { 1157 return parent; 1158 } 1159 }); 1160 }, 1161 1162 /** 1163 * This function wraps potentially large places transaction operations 1164 * with batch notifications to the result node, hence switching the views 1165 * to batch mode. 1166 * 1167 * @param {nsINavHistoryResult} resultNode The result node to turn on batching. 1168 * @note If resultNode is not supplied, the function will pass-through to 1169 * functionToWrap. 1170 * @param {Integer} itemsBeingChanged The count of items being changed. If the 1171 * count is lower than a threshold, then 1172 * batching won't be set. 1173 * @param {Function} functionToWrap The function to 1174 */ 1175 async batchUpdatesForNode(resultNode, itemsBeingChanged, functionToWrap) { 1176 if (!resultNode) { 1177 await functionToWrap(); 1178 return; 1179 } 1180 1181 resultNode = resultNode.QueryInterface(Ci.nsINavBookmarkObserver); 1182 1183 if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) { 1184 resultNode.onBeginUpdateBatch(); 1185 } 1186 1187 try { 1188 await functionToWrap(); 1189 } finally { 1190 if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) { 1191 resultNode.onEndUpdateBatch(); 1192 } 1193 } 1194 }, 1195 1196 /** 1197 * Constructs a Places Transaction for the drop or paste of a blob of data 1198 * into a container. 1199 * 1200 * @param aData 1201 * The unwrapped data blob of dropped or pasted data. 1202 * @param aNewParentGuid 1203 * GUID of the container the data was dropped or pasted into. 1204 * @param aIndex 1205 * The index within the container the item was dropped or pasted at. 1206 * @param aCopy 1207 * The drag action was copy, so don't move folders or links. 1208 * 1209 * @return a Places Transaction that can be transacted for performing the 1210 * move/insert command. 1211 */ 1212 getTransactionForData(aData, aNewParentGuid, aIndex, aCopy) { 1213 if (!this.SUPPORTED_FLAVORS.includes(aData.type)) 1214 throw new Error(`Unsupported '${aData.type}' data type`); 1215 1216 if ("itemGuid" in aData && "instanceId" in aData && 1217 aData.instanceId == PlacesUtils.instanceId) { 1218 if (!this.PLACES_FLAVORS.includes(aData.type)) 1219 throw new Error(`itemGuid unexpectedly set on ${aData.type} data`); 1220 1221 let info = { guid: aData.itemGuid, 1222 newParentGuid: aNewParentGuid, 1223 newIndex: aIndex }; 1224 if (aCopy) { 1225 info.excludingAnnotation = "Places/SmartBookmark"; 1226 return PlacesTransactions.Copy(info); 1227 } 1228 return PlacesTransactions.Move(info); 1229 } 1230 1231 // Since it's cheap and harmless, we allow the paste of separators and 1232 // bookmarks from builds that use legacy transactions (i.e. when itemGuid 1233 // was not set on PLACES_FLAVORS data). Containers are a different story, 1234 // and thus disallowed. 1235 if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) 1236 throw new Error("Can't copy a container from a legacy-transactions build"); 1237 1238 if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) { 1239 return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid, 1240 index: aIndex }); 1241 } 1242 1243 let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title 1244 : aData.uri; 1245 return PlacesTransactions.NewBookmark({ url: Services.io.newURI(aData.uri), 1246 title, 1247 parentGuid: aNewParentGuid, 1248 index: aIndex }); 1249 }, 1250 1251 /** 1252 * Processes a set of transfer items that have been dropped or pasted. 1253 * Batching will be applied where necessary. 1254 * 1255 * @param {Array} items A list of unwrapped nodes to process. 1256 * @param {Object} insertionPoint The requested point for insertion. 1257 * @param {Boolean} doCopy Set to true to copy the items, false will move them 1258 * if possible. 1259 * @paramt {Object} view The view that should be used for batching. 1260 * @return {Array} Returns an empty array when the insertion point is a tag, else 1261 * returns an array of copied or moved guids. 1262 */ 1263 async handleTransferItems(items, insertionPoint, doCopy, view) { 1264 let transactions; 1265 let itemsCount; 1266 if (insertionPoint.isTag) { 1267 let urls = items.filter(item => "uri" in item).map(item => item.uri); 1268 itemsCount = urls.length; 1269 transactions = [PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName })]; 1270 } else { 1271 let insertionIndex = await insertionPoint.getIndex(); 1272 itemsCount = items.length; 1273 transactions = await getTransactionsForTransferItems( 1274 items, insertionIndex, insertionPoint.guid, doCopy); 1275 } 1276 1277 // Check if we actually have something to add, if we don't it probably wasn't 1278 // valid, or it was moving to the same location, so just ignore it. 1279 if (!transactions.length) { 1280 return []; 1281 } 1282 1283 let guidsToSelect = []; 1284 let resultForBatching = getResultForBatching(view); 1285 1286 // If we're inserting into a tag, we don't get the guid, so we'll just 1287 // pass the transactions direct to the batch function. 1288 let batchingItem = transactions; 1289 if (!insertionPoint.isTag) { 1290 // If we're not a tag, then we need to get the ids of the items to select. 1291 batchingItem = async () => { 1292 for (let transaction of transactions) { 1293 let guid = await transaction.transact(); 1294 if (guid) { 1295 guidsToSelect.push(guid); 1296 } 1297 } 1298 }; 1299 } 1300 1301 await this.batchUpdatesForNode(resultForBatching, itemsCount, async () => { 1302 await PlacesTransactions.batch(batchingItem); 1303 }); 1304 1305 return guidsToSelect; 1306 }, 1307}; 1308 1309// These are lazy getters to avoid importing PlacesUtils immediately. 1310XPCOMUtils.defineLazyGetter(PlacesUIUtils, "PLACES_FLAVORS", () => { 1311 return [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, 1312 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, 1313 PlacesUtils.TYPE_X_MOZ_PLACE]; 1314}); 1315XPCOMUtils.defineLazyGetter(PlacesUIUtils, "URI_FLAVORS", () => { 1316 return [PlacesUtils.TYPE_X_MOZ_URL, 1317 TAB_DROP_TYPE, 1318 PlacesUtils.TYPE_UNICODE]; 1319}); 1320XPCOMUtils.defineLazyGetter(PlacesUIUtils, "SUPPORTED_FLAVORS", () => { 1321 return [...PlacesUIUtils.PLACES_FLAVORS, 1322 ...PlacesUIUtils.URI_FLAVORS]; 1323}); 1324 1325XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() { 1326 return Services.prefs.getComplexValue("intl.ellipsis", 1327 Ci.nsIPrefLocalizedString).data; 1328}); 1329 1330XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "loadBookmarksInBackground", 1331 PREF_LOAD_BOOKMARKS_IN_BACKGROUND, false); 1332XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "loadBookmarksInTabs", 1333 PREF_LOAD_BOOKMARKS_IN_TABS, false); 1334XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "openInTabClosesMenu", 1335 "browser.bookmarks.openInTabClosesMenu", false); 1336 1337/** 1338 * Determines if an unwrapped node can be moved. 1339 * 1340 * @param unwrappedNode 1341 * A node unwrapped by PlacesUtils.unwrapNodes(). 1342 * @return True if the node can be moved, false otherwise. 1343 */ 1344function canMoveUnwrappedNode(unwrappedNode) { 1345 if ((unwrappedNode.concreteGuid && PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) || 1346 unwrappedNode.id <= 0 || PlacesUtils.isRootItem(unwrappedNode.id)) { 1347 return false; 1348 } 1349 1350 let parentGuid = unwrappedNode.parentGuid; 1351 // If there's no parent Guid, this was likely a virtual query that returns 1352 // bookmarks, such as a tags query. 1353 if (!parentGuid || 1354 parentGuid == PlacesUtils.bookmarks.rootGuid) { 1355 return false; 1356 } 1357 // leftPaneFolderId and allBookmarksFolderId are lazy getters running 1358 // at least a synchronous DB query. Therefore we don't want to invoke 1359 // them first, especially because isCommandEnabled may be called way 1360 // before the left pane folder is even necessary. 1361 if (typeof Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId").get != "function" && 1362 (unwrappedNode.parent == PlacesUIUtils.leftPaneFolderId)) { 1363 return false; 1364 } 1365 return true; 1366} 1367 1368/** 1369 * This gets the most appropriate item for using for batching. In the case of multiple 1370 * views being related, the method returns the most expensive result to batch. 1371 * For example, if it detects the left-hand library pane, then it will look for 1372 * and return the reference to the right-hand pane. 1373 * 1374 * @param {Object} viewOrElement The item to check. 1375 * @return {Object} Will return the best result node to batch, or null 1376 * if one could not be found. 1377 */ 1378function getResultForBatching(viewOrElement) { 1379 if (viewOrElement && viewOrElement instanceof Ci.nsIDOMElement && 1380 viewOrElement.id === "placesList") { 1381 // Note: fall back to the existing item if we can't find the right-hane pane. 1382 viewOrElement = viewOrElement.ownerDocument.getElementById("placeContent") || viewOrElement; 1383 } 1384 1385 if (viewOrElement && viewOrElement.result) { 1386 return viewOrElement.result; 1387 } 1388 1389 return null; 1390} 1391 1392/** 1393 * Processes a set of transfer items and returns transactions to insert or 1394 * move them. 1395 * 1396 * @param {Array} items A list of unwrapped nodes to get transactions for. 1397 * @param {Integer} insertionIndex The requested index for insertion. 1398 * @param {String} insertionParentGuid The guid of the parent folder to insert 1399 * or move the items to. 1400 * @param {Boolean} doCopy Set to true to copy the items, false will move them 1401 * if possible. 1402 * @return {Array} Returns an array of created PlacesTransactions. 1403 */ 1404async function getTransactionsForTransferItems(items, insertionIndex, 1405 insertionParentGuid, doCopy) { 1406 let transactions = []; 1407 let index = insertionIndex; 1408 1409 for (let item of items) { 1410 if (index != -1 && item.itemGuid) { 1411 // Note: we use the parent from the existing bookmark as the sidebar 1412 // gives us an unwrapped.parent that is actually a query and not the real 1413 // parent. 1414 let existingBookmark = await PlacesUtils.bookmarks.fetch(item.itemGuid); 1415 1416 // If we're dropping on the same folder, then we may need to adjust 1417 // the index to insert at the correct place. 1418 if (existingBookmark && insertionParentGuid == existingBookmark.parentGuid) { 1419 if (index > existingBookmark.index) { 1420 // If we're dragging down, we need to go one lower to insert at 1421 // the real point as moving the element changes the index of 1422 // everything below by 1. 1423 index--; 1424 } else if (index == existingBookmark.index) { 1425 // This isn't moving so we skip it. 1426 continue; 1427 } 1428 } 1429 } 1430 1431 // If this is not a copy, check for safety that we can move the 1432 // source, otherwise report an error and fallback to a copy. 1433 if (!doCopy && !canMoveUnwrappedNode(item)) { 1434 Cu.reportError("Tried to move an unmovable Places " + 1435 "node, reverting to a copy operation."); 1436 doCopy = true; 1437 } 1438 transactions.push( 1439 PlacesUIUtils.getTransactionForData(item, 1440 insertionParentGuid, 1441 index, 1442 doCopy)); 1443 1444 if (index != -1 && item.itemGuid) { 1445 index++; 1446 } 1447 } 1448 return transactions; 1449} 1450