PlacesUIUtils.sys.mjs (64032B)
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 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 9 10 const lazy = {}; 11 12 ChromeUtils.defineESModuleGetters(lazy, { 13 CLIENT_NOT_CONFIGURED: "resource://services-sync/constants.sys.mjs", 14 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 15 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 16 CustomizableUI: 17 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 18 MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", 19 OpenInTabsUtils: 20 "moz-src:///browser/components/tabbrowser/OpenInTabsUtils.sys.mjs", 21 PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs", 22 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 23 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 24 Weave: "resource://services-sync/main.sys.mjs", 25 }); 26 27 const ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD = 10; 28 29 // copied from utilityOverlay.js 30 const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab"; 31 32 /** 33 * Collects all information for a bookmark and performs editmethods 34 */ 35 class BookmarkState { 36 /** 37 * Construct a new BookmarkState. 38 * 39 * @param {object} options 40 * The constructor options. 41 * @param {object} options.info 42 * Either a result node or a node-like object representing the item to be edited. 43 * @param {string} [options.tags] 44 * Tags (if any) for the bookmark in a comma separated string. Empty tags are 45 * skipped 46 * @param {string} [options.keyword] 47 * Existing (if there are any) keyword for bookmark 48 * @param {boolean} [options.isFolder] 49 * If the item is a folder. 50 * @param {Array<{ title: string; url: nsIURI; }>} [options.children] 51 * The list of child URIs to bookmark within the folder. 52 * @param {boolean} [options.autosave] 53 * If changes to bookmark fields should be saved immediately after calling 54 * its respective "changed" method, rather than waiting for save() to be 55 * called. 56 * @param {number} [options.index] 57 * The insertion point index of the bookmark. 58 */ 59 constructor({ 60 info, 61 tags = "", 62 keyword = "", 63 isFolder = false, 64 children = [], 65 autosave = false, 66 index, 67 }) { 68 this._guid = info.itemGuid; 69 this._postData = info.postData; 70 this._isTagContainer = info.isTag; 71 this._bulkTaggingUrls = info.uris?.map(uri => uri.spec); 72 this._isFolder = isFolder; 73 this._children = children; 74 this._autosave = autosave; 75 76 // Original Bookmark 77 this._originalState = { 78 title: this._isTagContainer ? info.tag : info.title, 79 uri: info.uri?.spec, 80 tags: tags 81 .trim() 82 .split(/\s*,\s*/) 83 .filter(tag => !!tag.length), 84 keyword, 85 parentGuid: info.parentGuid, 86 index, 87 }; 88 89 // Edited bookmark 90 this._newState = {}; 91 } 92 93 /** 94 * Save edited title for the bookmark 95 * 96 * @param {string} title 97 * The title of the bookmark 98 */ 99 async _titleChanged(title) { 100 this._newState.title = title; 101 await this._maybeSave(); 102 } 103 104 /** 105 * Save edited location for the bookmark 106 * 107 * @param {string} location 108 * The location of the bookmark 109 */ 110 async _locationChanged(location) { 111 this._newState.uri = location; 112 await this._maybeSave(); 113 } 114 115 /** 116 * Save edited tags for the bookmark 117 * 118 * @param {string} tags 119 * Comma separated list of tags 120 */ 121 async _tagsChanged(tags) { 122 this._newState.tags = tags; 123 await this._maybeSave(); 124 } 125 126 /** 127 * Save edited keyword for the bookmark 128 * 129 * @param {string} keyword 130 * The keyword of the bookmark 131 */ 132 async _keywordChanged(keyword) { 133 this._newState.keyword = keyword; 134 await this._maybeSave(); 135 } 136 137 /** 138 * Save edited parentGuid for the bookmark 139 * 140 * @param {string} parentGuid 141 * The parentGuid of the bookmark 142 */ 143 async _parentGuidChanged(parentGuid) { 144 this._newState.parentGuid = parentGuid; 145 await this._maybeSave(); 146 } 147 148 /** 149 * Save changes if autosave is enabled. 150 */ 151 async _maybeSave() { 152 if (this._autosave) { 153 await this.save(); 154 } 155 } 156 157 /** 158 * Create a new bookmark. 159 * 160 * @returns {Promise<string>} The bookmark's GUID. 161 */ 162 async _createBookmark() { 163 let transactions = [ 164 lazy.PlacesTransactions.NewBookmark({ 165 parentGuid: this.parentGuid, 166 tags: this._newState.tags, 167 title: this._newState.title ?? this._originalState.title, 168 url: this._newState.uri ?? this._originalState.uri, 169 index: this._originalState.index, 170 }), 171 ]; 172 if (this._newState.keyword) { 173 transactions.push(previousResults => 174 lazy.PlacesTransactions.EditKeyword({ 175 guid: previousResults[0], 176 keyword: this._newState.keyword, 177 postData: this._postData, 178 }) 179 ); 180 } 181 let results = await lazy.PlacesTransactions.batch( 182 transactions, 183 "BookmarkState::createBookmark" 184 ); 185 this._guid = results?.[0]; 186 return this._guid; 187 } 188 189 /** 190 * Create a new folder. 191 * 192 * @returns {Promise<string>} The folder's GUID. 193 */ 194 async _createFolder() { 195 let transactions = [ 196 lazy.PlacesTransactions.NewFolder({ 197 parentGuid: this.parentGuid, 198 title: this._newState.title ?? this._originalState.title, 199 children: this._children, 200 index: this._originalState.index, 201 tags: this._newState.tags, 202 }), 203 ]; 204 205 if (this._bulkTaggingUrls) { 206 this._appendTagsTransactions({ 207 transactions, 208 newTags: this._newState.tags, 209 originalTags: this._originalState.tags, 210 urls: this._bulkTaggingUrls, 211 }); 212 } 213 214 let results = await lazy.PlacesTransactions.batch( 215 transactions, 216 "BookmarkState::save::createFolder" 217 ); 218 this._guid = results[0]; 219 return this._guid; 220 } 221 222 get parentGuid() { 223 return this._newState.parentGuid ?? this._originalState.parentGuid; 224 } 225 226 /** 227 * Save() API function for bookmark. 228 * 229 * @returns {Promise<string>} bookmark.guid 230 */ 231 async save() { 232 if (this._guid === lazy.PlacesUtils.bookmarks.unsavedGuid) { 233 return this._isFolder ? this._createFolder() : this._createBookmark(); 234 } 235 236 if (!Object.keys(this._newState).length) { 237 return this._guid; 238 } 239 240 if (this._isTagContainer && this._newState.title) { 241 await lazy.PlacesTransactions.RenameTag({ 242 oldTag: this._originalState.title, 243 tag: this._newState.title, 244 }) 245 .transact() 246 .catch(console.error); 247 return this._guid; 248 } 249 250 let url = this._newState.uri || this._originalState.uri; 251 let transactions = []; 252 253 if (this._newState.uri) { 254 transactions.push( 255 lazy.PlacesTransactions.EditUrl({ 256 guid: this._guid, 257 url, 258 }) 259 ); 260 } 261 262 for (const [key, value] of Object.entries(this._newState)) { 263 switch (key) { 264 case "title": 265 transactions.push( 266 lazy.PlacesTransactions.EditTitle({ 267 guid: this._guid, 268 title: value, 269 }) 270 ); 271 break; 272 case "tags": { 273 this._appendTagsTransactions({ 274 transactions, 275 newTags: value, 276 originalTags: this._originalState.tags, 277 urls: this._bulkTaggingUrls || [url], 278 }); 279 break; 280 } 281 case "keyword": 282 transactions.push( 283 lazy.PlacesTransactions.EditKeyword({ 284 guid: this._guid, 285 keyword: value, 286 postData: this._postData, 287 oldKeyword: this._originalState.keyword, 288 }) 289 ); 290 break; 291 case "parentGuid": 292 transactions.push( 293 lazy.PlacesTransactions.Move({ 294 guid: this._guid, 295 newParentGuid: this._newState.parentGuid, 296 }) 297 ); 298 break; 299 } 300 } 301 if (transactions.length) { 302 await lazy.PlacesTransactions.batch(transactions, "BookmarkState::save"); 303 } 304 305 this._originalState = { ...this._originalState, ...this._newState }; 306 this._newState = {}; 307 return this._guid; 308 } 309 310 /** 311 * Append transactions to update tags by given information. 312 * 313 * @param {object} parameters 314 * The parameters object containing: 315 * @param {object[]} parameters.transactions 316 * Array that transactions will be appended to. 317 * @param {string[]} parameters.newTags 318 * Tags that will be appended to the given urls. 319 * @param {string[]} parameters.originalTags 320 * Tags that had been appended to the given urls. 321 * @param {string[]} parameters.urls 322 * URLs that will be updated. 323 */ 324 _appendTagsTransactions({ 325 transactions, 326 newTags = [], 327 originalTags = [], 328 urls, 329 }) { 330 const addedTags = newTags.filter(tag => !originalTags.includes(tag)); 331 const removedTags = originalTags.filter(tag => !newTags.includes(tag)); 332 if (addedTags.length) { 333 transactions.push( 334 lazy.PlacesTransactions.Tag({ 335 urls, 336 tags: addedTags, 337 }) 338 ); 339 } 340 if (removedTags.length) { 341 transactions.push( 342 lazy.PlacesTransactions.Untag({ 343 urls, 344 tags: removedTags, 345 }) 346 ); 347 } 348 } 349 } 350 351 export var PlacesUIUtils = { 352 BookmarkState, 353 _bookmarkToolbarTelemetryListening: false, 354 LAST_USED_FOLDERS_META_KEY: "bookmarks/lastusedfolders", 355 356 lastContextMenuTriggerNode: null, 357 358 lastContextMenuCommand: null, 359 360 // This allows to await for all the relevant bookmark changes to be applied 361 // when a bookmark dialog is closed. It is resolved to the bookmark guid, 362 // if a bookmark was created or modified. 363 lastBookmarkDialogDeferred: null, 364 365 /** 366 * Obfuscates a place: URL to use it in xulstore without the risk of 367 leaking browsing information. Uses md5 to hash the query string. 368 * 369 * @param {string} url 370 * the URL for xulstore with place: key pairs. 371 * @returns {string} "place:[md5_hash]" hashed url 372 */ 373 374 obfuscateUrlForXulStore(url) { 375 if (!url.startsWith("place:")) { 376 throw new Error("Method must be used to only obfuscate place: uris!"); 377 } 378 let urlNoProtocol = url.substring(url.indexOf(":") + 1); 379 let hashedURL = lazy.PlacesUtils.md5(urlNoProtocol); 380 381 return `place:${hashedURL}`; 382 }, 383 384 /** 385 * Shows the bookmark dialog corresponding to the specified info. 386 * 387 * @param {object} aInfo 388 * Describes the item to be edited/added in the dialog. 389 * See documentation at the top of bookmarkProperties.js 390 * @param {Window} [aParentWindow] 391 * Owner window for the new dialog. 392 * 393 * @see documentation at the top of bookmarkProperties.js 394 * @returns {Promise<string>} The guid of the item that was created or edited, 395 * undefined otherwise. 396 */ 397 async showBookmarkDialog(aInfo, aParentWindow = null) { 398 this.lastBookmarkDialogDeferred = Promise.withResolvers(); 399 400 let dialogURL = "chrome://browser/content/places/bookmarkProperties.xhtml"; 401 let features = "centerscreen,chrome,modal,resizable=no"; 402 let bookmarkGuid; 403 404 if (!aParentWindow) { 405 aParentWindow = Services.wm.getMostRecentWindow(null); 406 } 407 408 if (aParentWindow.gDialogBox) { 409 await aParentWindow.gDialogBox.open(dialogURL, aInfo); 410 } else { 411 aParentWindow.openDialog(dialogURL, "", features, aInfo); 412 } 413 414 if (aInfo.bookmarkState) { 415 bookmarkGuid = await aInfo.bookmarkState.save(); 416 this.lastBookmarkDialogDeferred.resolve(bookmarkGuid); 417 return bookmarkGuid; 418 } 419 bookmarkGuid = undefined; 420 this.lastBookmarkDialogDeferred.resolve(bookmarkGuid); 421 return bookmarkGuid; 422 }, 423 424 /** 425 * Bookmarks one or more pages. If there is more than one, this will create 426 * the bookmarks in a new folder. 427 * 428 * @param {{uri: nsIURI, title: string}[]} URIList 429 * The list of URIs to bookmark. 430 * @param {string[]} [hiddenRows] 431 * An array of rows to be hidden. 432 * @param {Window} [win] 433 * The window to use as the parent to display the bookmark dialog. 434 */ 435 async showBookmarkPagesDialog(URIList, hiddenRows = [], win = null) { 436 if (!URIList.length) { 437 return; 438 } 439 440 const bookmarkDialogInfo = { action: "add", hiddenRows }; 441 if (URIList.length > 1) { 442 bookmarkDialogInfo.type = "folder"; 443 bookmarkDialogInfo.URIList = URIList; 444 } else { 445 bookmarkDialogInfo.type = "bookmark"; 446 bookmarkDialogInfo.title = URIList[0].title; 447 bookmarkDialogInfo.uri = URIList[0].uri; 448 } 449 450 await PlacesUIUtils.showBookmarkDialog(bookmarkDialogInfo, win); 451 }, 452 453 /** 454 * Returns the closet ancestor places view for the given DOM node 455 * 456 * @param {DOMNode} aNode 457 * a DOM node 458 * @returns {DOMNode} the closest ancestor places view if exists, null otherwsie. 459 */ 460 getViewForNode: function PUIU_getViewForNode(aNode) { 461 let node = aNode; 462 463 if (Cu.isDeadWrapper(node)) { 464 return null; 465 } 466 467 if (node.localName == "panelview" && node._placesView) { 468 return node._placesView; 469 } 470 471 // The view for a <menu> of which its associated menupopup is a places 472 // view, is the menupopup. 473 if ( 474 node.localName == "menu" && 475 !node._placesNode && 476 node.menupopup._placesView 477 ) { 478 return node.menupopup._placesView; 479 } 480 481 while (Element.isInstance(node)) { 482 if (node._placesView) { 483 return node._placesView; 484 } 485 if ( 486 node.localName == "tree" && 487 node.getAttribute("is") == "places-tree" 488 ) { 489 return node; 490 } 491 492 node = node.parentNode; 493 } 494 495 return null; 496 }, 497 498 /** 499 * Returns the active PlacesController for a given command. 500 * 501 * @param {Window} win The window containing the affected view 502 * @param {string} command The command 503 * @returns {PlacesController} a places controller 504 */ 505 getControllerForCommand(win, command) { 506 // If we're building a context menu for a non-focusable view, for example 507 // a menupopup, we must return the view that triggered the context menu. 508 let popupNode = PlacesUIUtils.lastContextMenuTriggerNode; 509 if (popupNode) { 510 let isManaged = !!popupNode.closest("#managed-bookmarks"); 511 if (isManaged) { 512 return this.managedBookmarksController; 513 } 514 let view = this.getViewForNode(popupNode); 515 if (view && view._contextMenuShown) { 516 return view.controllers.getControllerForCommand(command); 517 } 518 } 519 520 // When we're not building a context menu, only focusable views 521 // are possible. Thus, we can safely use the command dispatcher. 522 let controller = 523 win.top.document.commandDispatcher.getControllerForCommand(command); 524 return controller || null; 525 }, 526 527 /** 528 * Update all the Places commands for the given window. 529 * 530 * @param {Window} win The window to update. 531 */ 532 updateCommands(win) { 533 // Get the controller for one of the places commands. 534 let controller = this.getControllerForCommand(win, "placesCmd_open"); 535 for (let command of [ 536 "placesCmd_open", 537 "placesCmd_open:window", 538 "placesCmd_open:privatewindow", 539 "placesCmd_open:tab", 540 "placesCmd_new:folder", 541 "placesCmd_new:bookmark", 542 "placesCmd_new:separator", 543 "placesCmd_show:info", 544 "placesCmd_reload", 545 "placesCmd_sortBy:name", 546 "placesCmd_cut", 547 "placesCmd_copy", 548 "placesCmd_paste", 549 "placesCmd_delete", 550 "placesCmd_showInFolder", 551 ]) { 552 win.goSetCommandEnabled( 553 command, 554 controller && controller.isCommandEnabled(command) 555 ); 556 } 557 }, 558 559 /** 560 * Executes the given command on the currently active controller. 561 * 562 * @param {Window} win The window containing the affected view 563 * @param {string} command The command to execute 564 */ 565 doCommand(win, command) { 566 let controller = this.getControllerForCommand(win, command); 567 if (controller && controller.isCommandEnabled(command)) { 568 PlacesUIUtils.lastContextMenuCommand = command; 569 controller.doCommand(command); 570 } 571 }, 572 573 /** 574 * By calling this before visiting an URL, the visit will be associated to a 575 * TRANSITION_TYPED transition (if there is no a referrer). 576 * This is used when visiting pages from the history menu, history sidebar, 577 * url bar, url autocomplete results, and history searches from the places 578 * organizer. If this is not called visits will be marked as 579 * TRANSITION_LINK. 580 * 581 * @param {string} aURL 582 * The URL to mark as typed. 583 */ 584 markPageAsTyped: function PUIU_markPageAsTyped(aURL) { 585 lazy.PlacesUtils.history.markPageAsTyped( 586 Services.uriFixup.getFixupURIInfo(aURL).preferredURI 587 ); 588 }, 589 590 /** 591 * By calling this before visiting an URL, the visit will be associated to a 592 * TRANSITION_BOOKMARK transition. 593 * This is used when visiting pages from the bookmarks menu, 594 * personal toolbar, and bookmarks from within the places organizer. 595 * If this is not called visits will be marked as TRANSITION_LINK. 596 * 597 * @param {string} aURL 598 * The URL to mark as TRANSITION_BOOKMARK. 599 */ 600 markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) { 601 lazy.PlacesUtils.history.markPageAsFollowedBookmark( 602 Services.uriFixup.getFixupURIInfo(aURL).preferredURI 603 ); 604 }, 605 606 /** 607 * By calling this before visiting an URL, any visit in frames will be 608 * associated to a TRANSITION_FRAMED_LINK transition. 609 * This is actually used to distinguish user-initiated visits in frames 610 * so automatic visits can be correctly ignored. 611 * 612 * @param {string} aURL 613 * The URL to mark as TRANSITION_FRAMED_LINK. 614 */ 615 markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) { 616 lazy.PlacesUtils.history.markPageAsFollowedLink( 617 Services.uriFixup.getFixupURIInfo(aURL).preferredURI 618 ); 619 }, 620 621 /** 622 * Sets the character-set for a page. The character set will not be saved 623 * if the window is determined to be a private browsing window. 624 * 625 * @param {string|URL|nsIURI} url The URL of the page to set the charset on. 626 * @param {string} charset character-set value. 627 * @param {Window} window The window that the charset is being set from. 628 * @returns {Promise} 629 */ 630 async setCharsetForPage(url, charset, window) { 631 if (lazy.PrivateBrowsingUtils.isWindowPrivate(window)) { 632 return; 633 } 634 635 // UTF-8 is the default. If we are passed the value then set it to null, 636 // to ensure any charset is removed from the database. 637 if (charset.toLowerCase() == "utf-8") { 638 charset = null; 639 } 640 641 await lazy.PlacesUtils.history.update({ 642 url, 643 annotations: new Map([[lazy.PlacesUtils.CHARSET_ANNO, charset]]), 644 }); 645 }, 646 647 /** 648 * Allows opening of javascript/data URI only if the given node is 649 * bookmarked (see bug 224521). 650 * 651 * @param {object} aURINode 652 * a URI node 653 * @param {Window} aWindow 654 * a window on which a potential error alert is shown on. 655 * @returns {boolean} true if it's safe to open the node in the browser, false otherwise. 656 */ 657 checkURLSecurity(aURINode, aWindow) { 658 if (lazy.PlacesUtils.nodeIsBookmark(aURINode)) { 659 return true; 660 } 661 662 var uri = Services.io.newURI(aURINode.uri); 663 if (uri.schemeIs("javascript") || uri.schemeIs("data")) { 664 const [title, errorStr] = 665 PlacesUIUtils.promptLocalization.formatValuesSync([ 666 "places-error-title", 667 "places-load-js-data-url-error", 668 ]); 669 Services.prompt.alert(aWindow, title, errorStr); 670 return false; 671 } 672 return true; 673 }, 674 675 /** 676 * Check whether or not the given node represents a removable entry (either in 677 * history or in bookmarks). 678 * 679 * @param {object} aNode 680 * a node, except the root node of a query. 681 * @returns {boolean} true if the aNode represents a removable entry, false otherwise. 682 */ 683 canUserRemove(aNode) { 684 let parentNode = aNode.parent; 685 if (!parentNode) { 686 // canUserRemove doesn't accept root nodes. 687 return false; 688 } 689 690 // Is it a query pointing to one of the special root folders? 691 if (lazy.PlacesUtils.nodeIsQuery(parentNode)) { 692 if (lazy.PlacesUtils.nodeIsFolderOrShortcut(aNode)) { 693 let guid = lazy.PlacesUtils.getConcreteItemGuid(aNode); 694 // If the parent folder is not a folder, it must be a query, and so this node 695 // cannot be removed. 696 if (lazy.PlacesUtils.isRootItem(guid)) { 697 return false; 698 } 699 } else if (lazy.PlacesUtils.isVirtualLeftPaneItem(aNode.bookmarkGuid)) { 700 // If the item is a left-pane top-level item, it can't be removed. 701 return false; 702 } 703 } 704 705 // If it's not a bookmark, or it's child of a query, we can remove it. 706 if (aNode.itemId == -1 || lazy.PlacesUtils.nodeIsQuery(parentNode)) { 707 return true; 708 } 709 710 // Otherwise it has to be a child of an editable folder. 711 return !this.isFolderReadOnly(parentNode); 712 }, 713 714 /** 715 * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH 716 * TO GUIDS IS COMPLETE (BUG 1071511). 717 * 718 * Check whether or not the given Places node points to a folder which 719 * should not be modified by the user (i.e. its children should be unremovable 720 * and unmovable, new children should be disallowed, etc). 721 * These semantics are not inherited, meaning that read-only folder may 722 * contain editable items (for instance, the places root is read-only, but all 723 * of its direct children aren't). 724 * 725 * You should only pass folder nodes. 726 * 727 * @param {object} placesNode 728 * any folder result node. 729 * @throws if placesNode is not a folder result node or views is invalid. 730 * @returns {boolean} true if placesNode is a read-only folder, false otherwise. 731 */ 732 isFolderReadOnly(placesNode) { 733 if ( 734 typeof placesNode != "object" || 735 !lazy.PlacesUtils.nodeIsFolderOrShortcut(placesNode) 736 ) { 737 throw new Error("invalid value for placesNode"); 738 } 739 740 return ( 741 lazy.PlacesUtils.getConcreteItemGuid(placesNode) == 742 lazy.PlacesUtils.bookmarks.rootGuid 743 ); 744 }, 745 746 /** 747 * @param {Array<object>} aItemsToOpen 748 * needs to be an array of objects of the form: 749 * {uri: string, isBookmark: boolean} 750 * @param {object} aEvent 751 * The associated event triggering the open. 752 * @param {Window} aWindow 753 * The window associated with the event. 754 */ 755 openTabset(aItemsToOpen, aEvent, aWindow) { 756 if (!aItemsToOpen.length) { 757 return; 758 } 759 760 let browserWindow = getBrowserWindow(aWindow); 761 var urls = []; 762 let isPrivate = 763 browserWindow && lazy.PrivateBrowsingUtils.isWindowPrivate(browserWindow); 764 for (let item of aItemsToOpen) { 765 urls.push(item.uri); 766 if (isPrivate) { 767 continue; 768 } 769 770 if (item.isBookmark) { 771 this.markPageAsFollowedBookmark(item.uri); 772 } else { 773 this.markPageAsTyped(item.uri); 774 } 775 } 776 777 // whereToOpenLink doesn't return "window" when there's no browser window 778 // open (Bug 630255). 779 var where = browserWindow 780 ? lazy.BrowserUtils.whereToOpenLink(aEvent, false, true) 781 : "window"; 782 if (where == "window") { 783 // There is no browser window open, thus open a new one. 784 let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 785 let stringsToLoad = Cc["@mozilla.org/array;1"].createInstance( 786 Ci.nsIMutableArray 787 ); 788 urls.forEach(url => 789 stringsToLoad.appendElement(lazy.PlacesUtils.toISupportsString(url)) 790 ); 791 args.appendElement(stringsToLoad); 792 793 let features = "chrome,dialog=no,all"; 794 if (isPrivate) { 795 features += ",private"; 796 } 797 798 browserWindow = Services.ww.openWindow( 799 aWindow, 800 AppConstants.BROWSER_CHROME_URL, 801 null, 802 features, 803 args 804 ); 805 return; 806 } 807 808 var loadInBackground = where == "tabshifted"; 809 // For consistency, we want all the bookmarks to open in new tabs, instead 810 // of having one of them replace the currently focused tab. Hence we call 811 // loadTabs with aReplace set to false. 812 browserWindow.gBrowser.loadTabs(urls, { 813 inBackground: loadInBackground, 814 replace: false, 815 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 816 }); 817 }, 818 819 /** 820 * Loads a selected node's or nodes' URLs in tabs, 821 * warning the user when lots of URLs are being opened 822 * 823 * @param {object | Array} nodeOrNodes 824 * Contains the node or nodes that we're opening in tabs 825 * @param {event} event 826 * The DOM mouse/key event with modifier keys set that track the 827 * user's preferred destination window or tab. 828 * @param {object} view 829 * The current view that contains the node or nodes selected for 830 * opening 831 */ 832 openMultipleLinksInTabs(nodeOrNodes, event, view) { 833 let window = view.ownerWindow; 834 let urlsToOpen = []; 835 836 if (lazy.PlacesUtils.nodeIsContainer(nodeOrNodes)) { 837 urlsToOpen = lazy.PlacesUtils.getURLsForContainerNode(nodeOrNodes); 838 } else { 839 for (var i = 0; i < nodeOrNodes.length; i++) { 840 // Skip over separators and folders. 841 if (lazy.PlacesUtils.nodeIsURI(nodeOrNodes[i])) { 842 urlsToOpen.push({ 843 uri: nodeOrNodes[i].uri, 844 isBookmark: lazy.PlacesUtils.nodeIsBookmark(nodeOrNodes[i]), 845 }); 846 } 847 } 848 } 849 if (lazy.OpenInTabsUtils.confirmOpenInTabs(urlsToOpen.length, window)) { 850 if (window.updateTelemetry) { 851 window.updateTelemetry(urlsToOpen); 852 } 853 this.openTabset(urlsToOpen, event, window); 854 } 855 }, 856 857 /** 858 * Loads the node's URL in the appropriate tab or window given the 859 * user's preference specified by modifier keys tracked by a 860 * DOM mouse/key event. 861 * 862 * @param {object} aNode 863 * An uri result node. 864 * @param {object} aEvent 865 * The DOM mouse/key event with modifier keys set that track the 866 * user's preferred destination window or tab. 867 */ 868 openNodeWithEvent: function PUIU_openNodeWithEvent(aNode, aEvent) { 869 let window = aEvent.target.ownerGlobal; 870 871 let where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true); 872 if (this.loadBookmarksInTabs && lazy.PlacesUtils.nodeIsBookmark(aNode)) { 873 if (where == "current" && !aNode.uri.startsWith("javascript:")) { 874 where = "tab"; 875 } 876 let browserWindow = getBrowserWindow(window); 877 if (where == "tab" && browserWindow?.gBrowser.selectedTab.isEmpty) { 878 where = "current"; 879 } 880 } 881 882 this._openNodeIn(aNode, where, window); 883 }, 884 885 /** 886 * Loads the node's URL in the appropriate tab or window. 887 * see also URILoadingHelper's openWebLinkIn 888 * 889 * @param {object} aNode 890 * An uri result node. 891 * @param {string} aWhere 892 * Where to open the URL. 893 * @param {object} aView 894 * The associated view of the node being opened. 895 * @param {boolean} aPrivate 896 * True if the window being opened is private. 897 */ 898 openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) { 899 let window = aView.ownerWindow; 900 this._openNodeIn(aNode, aWhere, window, { aPrivate }); 901 }, 902 903 _openNodeIn: function PUIU__openNodeIn( 904 aNode, 905 aWhere, 906 aWindow, 907 { aPrivate = false, userContextId = 0 } = {} 908 ) { 909 if ( 910 aNode && 911 this.checkURLSecurity(aNode, aWindow) && 912 this.isURILike(aNode) 913 ) { 914 let isBookmark = lazy.PlacesUtils.nodeIsBookmark(aNode); 915 916 if (!lazy.PrivateBrowsingUtils.isWindowPrivate(aWindow)) { 917 if (isBookmark) { 918 this.markPageAsFollowedBookmark(aNode.uri); 919 } else { 920 this.markPageAsTyped(aNode.uri); 921 } 922 } else { 923 // This is a targeted fix for bug 1792163, where it was discovered 924 // that if you open the Library from a Private Browsing window, and then 925 // use the "Open in New Window" context menu item to open a new window, 926 // that the window will open under the wrong icon on the Windows taskbar. 927 aPrivate = true; 928 } 929 930 const isJavaScriptURL = aNode.uri.startsWith("javascript:"); 931 aWindow.openTrustedLinkIn(aNode.uri, aWhere, { 932 allowPopups: isJavaScriptURL, 933 inBackground: this.loadBookmarksInBackground, 934 allowInheritPrincipal: isJavaScriptURL, 935 private: aPrivate, 936 userContextId, 937 }); 938 if (aWindow.updateTelemetry) { 939 aWindow.updateTelemetry([aNode]); 940 } 941 } 942 }, 943 944 /** 945 * Determines whether a node represents a URI. 946 * 947 * @param {nsINavHistoryResultNode | HTMLElement} aNode 948 * A result node. 949 * @returns {boolean} 950 * Whether the node represents a URI. 951 */ 952 isURILike(aNode) { 953 if (aNode instanceof Ci.nsINavHistoryResultNode) { 954 return lazy.PlacesUtils.nodeIsURI(aNode); 955 } 956 return !!aNode.uri; 957 }, 958 959 /** 960 * Helper for guessing scheme from an url string. 961 * Used to avoid nsIURI overhead in frequently called UI functions. This is not 962 * supposed be perfect, so use it only for UI purposes. 963 * 964 * @param {string} href The url to guess the scheme from. 965 * @returns {string} guessed scheme for this url string. 966 */ 967 guessUrlSchemeForUI(href) { 968 return href.substr(0, href.indexOf(":")); 969 }, 970 971 getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) { 972 var title; 973 if (!aNode.title && lazy.PlacesUtils.nodeIsURI(aNode)) { 974 // if node title is empty, try to set the label using host and filename 975 // Services.io.newURI will throw if aNode.uri is not a valid URI 976 try { 977 var uri = Services.io.newURI(aNode.uri); 978 var host = uri.host; 979 var fileName = uri.QueryInterface(Ci.nsIURL).fileName; 980 // if fileName is empty, use path to distinguish labels 981 if (aDoNotCutTitle) { 982 title = host + uri.pathQueryRef; 983 } else { 984 title = 985 host + 986 (fileName 987 ? (host ? "/" + Services.locale.ellipsis + "/" : "") + fileName 988 : uri.pathQueryRef); 989 } 990 } catch (e) { 991 // Use (no title) for non-standard URIs (data:, javascript:, ...) 992 title = ""; 993 } 994 } else { 995 title = aNode.title; 996 } 997 998 return title || this.promptLocalization.formatValueSync("places-no-title"); 999 }, 1000 1001 shouldShowTabsFromOtherComputersMenuitem() { 1002 let weaveOK = 1003 lazy.Weave.Status.checkSetup() != lazy.CLIENT_NOT_CONFIGURED && 1004 lazy.Weave.Svc.PrefBranch.getCharPref("firstSync", "") != "notReady"; 1005 return weaveOK; 1006 }, 1007 1008 /** 1009 * Helpers for consumers of editBookmarkOverlay which don't have a node as their input. 1010 * 1011 * Given a bookmark object for either a url bookmark or a folder, returned by 1012 * Bookmarks.fetch (see Bookmark.sys.mjs), this creates a node-like object 1013 * suitable for initialising the edit overlay with it. 1014 * 1015 * @param {object} aFetchInfo 1016 * a bookmark object returned by Bookmarks.fetch. 1017 * @returns {Promise<{bookmarkGuid: string, title: string, uri: string, type: nsINavHistoryResultNode.ResultType}>} 1018 * A node-like object suitable for initialising editBookmarkOverlay. 1019 * @throws if aFetchInfo is representing a separator. 1020 */ 1021 async promiseNodeLikeFromFetchInfo(aFetchInfo) { 1022 if (aFetchInfo.itemType == lazy.PlacesUtils.bookmarks.TYPE_SEPARATOR) { 1023 throw new Error("promiseNodeLike doesn't support separators"); 1024 } 1025 1026 let parent = { 1027 bookmarkGuid: aFetchInfo.parentGuid, 1028 type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER, 1029 }; 1030 1031 return Object.freeze({ 1032 bookmarkGuid: aFetchInfo.guid, 1033 title: aFetchInfo.title, 1034 uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "", 1035 1036 get type() { 1037 if (aFetchInfo.itemType == lazy.PlacesUtils.bookmarks.TYPE_FOLDER) { 1038 return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER; 1039 } 1040 1041 if (!this.uri.length) { 1042 throw new Error("Unexpected item type"); 1043 } 1044 1045 if (/^place:/.test(this.uri)) { 1046 throw new Error("Place URIs are not supported."); 1047 } 1048 1049 return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI; 1050 }, 1051 1052 get parent() { 1053 return parent; 1054 }, 1055 }); 1056 }, 1057 1058 /** 1059 * This function wraps potentially large places transaction operations 1060 * with batch notifications to the result node, hence switching the views 1061 * to batch mode. If resultNode is not supplied, the function will 1062 * pass-through to functionToWrap. 1063 * 1064 * @template T 1065 * @param {nsINavHistoryResult} resultNode The result node to turn on batching. 1066 * @param {number} itemsBeingChanged The count of items being changed. If the 1067 * count is lower than a threshold, then 1068 * batching won't be set. 1069 * @param {() => T} functionToWrap The function to 1070 * @returns {Promise<T>} forwards the functionToWrap return value. 1071 */ 1072 async batchUpdatesForNode(resultNode, itemsBeingChanged, functionToWrap) { 1073 if (!resultNode) { 1074 return functionToWrap(); 1075 } 1076 1077 if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) { 1078 resultNode.onBeginUpdateBatch(); 1079 } 1080 1081 try { 1082 return await functionToWrap(); 1083 } finally { 1084 if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) { 1085 resultNode.onEndUpdateBatch(); 1086 } 1087 } 1088 }, 1089 1090 /** 1091 * Processes a set of transfer items that have been dropped or pasted. 1092 * Batching will be applied where necessary. 1093 * 1094 * @param {object[]} items 1095 * A list of unwrapped nodes to process. 1096 * @param {object} insertionPoint 1097 * The requested point for insertion. 1098 * @param {boolean} doCopy 1099 * Set to true to copy the items, false will move them if possible. 1100 * @param {object} view 1101 * The view that should be used for batching. 1102 * @returns {Promise<string[]>} 1103 * Returns an empty array when the insertion point is a tag, else returns 1104 * an array of copied or moved guids. 1105 */ 1106 async handleTransferItems(items, insertionPoint, doCopy, view) { 1107 let transactions; 1108 let itemsCount; 1109 if (insertionPoint.isTag) { 1110 let urls = items.filter(item => "uri" in item).map(item => item.uri); 1111 itemsCount = urls.length; 1112 transactions = [ 1113 lazy.PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName }), 1114 ]; 1115 } else { 1116 let insertionIndex = await insertionPoint.getIndex(); 1117 itemsCount = items.length; 1118 transactions = getTransactionsForTransferItems( 1119 items, 1120 insertionIndex, 1121 insertionPoint.guid, 1122 !doCopy 1123 ); 1124 } 1125 1126 // Check if we actually have something to add, if we don't it probably wasn't 1127 // valid, or it was moving to the same location, so just ignore it. 1128 if (!transactions.length) { 1129 return []; 1130 } 1131 1132 let guidsToSelect = await this.batchUpdatesForNode( 1133 getResultForBatching(view), 1134 itemsCount, 1135 async () => 1136 lazy.PlacesTransactions.batch(transactions, "handleTransferItems") 1137 ); 1138 1139 // If we're inserting into a tag, we don't get the resulting guids. 1140 return insertionPoint.isTag ? [] : guidsToSelect.flat(); 1141 }, 1142 1143 onSidebarTreeClick(event) { 1144 // right-clicks are not handled here 1145 if (event.button == 2) { 1146 return; 1147 } 1148 1149 let tree = event.target.parentNode; 1150 let cell = tree.getCellAt(event.clientX, event.clientY); 1151 if (cell.row == -1 || cell.childElt == "twisty") { 1152 return; 1153 } 1154 1155 // getCoordsForCellItem returns the x coordinate in logical coordinates 1156 // (i.e., starting from the left and right sides in LTR and RTL modes, 1157 // respectively.) Therefore, we make sure to exclude the blank area 1158 // before the tree item icon (that is, to the left or right of it in 1159 // LTR and RTL modes, respectively) from the click target area. 1160 let win = tree.ownerGlobal; 1161 let rect = tree.getCoordsForCellItem(cell.row, cell.col, "image"); 1162 let isRTL = win.getComputedStyle(tree).direction == "rtl"; 1163 let mouseInGutter = isRTL ? event.clientX > rect.x : event.clientX < rect.x; 1164 1165 let metaKey = 1166 AppConstants.platform === "macosx" ? event.metaKey : event.ctrlKey; 1167 let modifKey = metaKey || event.shiftKey; 1168 let isContainer = tree.view.isContainer(cell.row); 1169 let openInTabs = 1170 isContainer && 1171 (event.button == 1 || (event.button == 0 && modifKey)) && 1172 lazy.PlacesUtils.hasChildURIs(tree.view.nodeForTreeIndex(cell.row)); 1173 1174 if (event.button == 0 && isContainer && !openInTabs) { 1175 tree.view.toggleOpenState(cell.row); 1176 } else if ( 1177 !mouseInGutter && 1178 openInTabs && 1179 event.originalTarget.localName == "treechildren" 1180 ) { 1181 tree.view.selection.select(cell.row); 1182 this.openMultipleLinksInTabs(tree.selectedNode, event, tree); 1183 } else if ( 1184 !mouseInGutter && 1185 !isContainer && 1186 event.originalTarget.localName == "treechildren" 1187 ) { 1188 // Clear all other selection since we're loading a link now. We must 1189 // do this *before* attempting to load the link since openURL uses 1190 // selection as an indication of which link to load. 1191 tree.view.selection.select(cell.row); 1192 this.openNodeWithEvent(tree.selectedNode, event); 1193 } 1194 }, 1195 1196 onSidebarTreeKeyPress(event) { 1197 let node = event.target.selectedNode; 1198 if (node) { 1199 if (event.keyCode == event.DOM_VK_RETURN) { 1200 PlacesUIUtils.openNodeWithEvent(node, event); 1201 } 1202 } 1203 }, 1204 1205 /** 1206 * The following function displays the URL of a node that is being 1207 * hovered over. 1208 * 1209 * @param {object} event 1210 * The event that triggered the hover. 1211 */ 1212 onSidebarTreeMouseMove(event) { 1213 let treechildren = event.target; 1214 if (treechildren.localName != "treechildren") { 1215 return; 1216 } 1217 1218 let tree = treechildren.parentNode; 1219 let cell = tree.getCellAt(event.clientX, event.clientY); 1220 1221 // cell.row is -1 when the mouse is hovering an empty area within the tree. 1222 // To avoid showing a URL from a previously hovered node for a currently 1223 // hovered non-url node, we must clear the moused-over URL in these cases. 1224 if (cell.row != -1) { 1225 let node = tree.view.nodeForTreeIndex(cell.row); 1226 if (lazy.PlacesUtils.nodeIsURI(node)) { 1227 this.setMouseoverURL(node.uri, tree.ownerGlobal); 1228 return; 1229 } 1230 } 1231 this.setMouseoverURL("", tree.ownerGlobal); 1232 }, 1233 1234 setMouseoverURL(url, win) { 1235 // When the browser window is closed with an open sidebar, the sidebar 1236 // unload event happens after the browser's one. In this case 1237 // top.XULBrowserWindow has been nullified already. 1238 if (win.top.XULBrowserWindow) { 1239 win.top.XULBrowserWindow.setOverLink(url); 1240 } 1241 }, 1242 1243 /** 1244 * Uncollapses PersonalToolbar if its collapsed status is not 1245 * persisted, and user customized it or changed default bookmarks. 1246 * 1247 * If the user does not have a persisted value for the toolbar's 1248 * "collapsed" attribute, try to determine whether it's customized. 1249 * 1250 * @param {boolean} aForceVisible Set to true to ignore if the user had 1251 * previously collapsed the toolbar manually. 1252 */ 1253 NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE: 3, 1254 async maybeToggleBookmarkToolbarVisibility(aForceVisible = false) { 1255 const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL; 1256 let xulStore = Services.xulStore; 1257 1258 if ( 1259 aForceVisible || 1260 !xulStore.hasValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed") 1261 ) { 1262 function uncollapseToolbar() { 1263 Services.obs.notifyObservers( 1264 null, 1265 "browser-set-toolbar-visibility", 1266 JSON.stringify([lazy.CustomizableUI.AREA_BOOKMARKS, "true"]) 1267 ); 1268 } 1269 // We consider the toolbar customized if it has more than 1270 // NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE children, or if it has a persisted 1271 // currentset value. 1272 let toolbarIsCustomized = xulStore.hasValue( 1273 BROWSER_DOCURL, 1274 "PersonalToolbar", 1275 "currentset" 1276 ); 1277 if (aForceVisible || toolbarIsCustomized) { 1278 uncollapseToolbar(); 1279 return; 1280 } 1281 1282 let numBookmarksOnToolbar = ( 1283 await lazy.PlacesUtils.bookmarks.fetch( 1284 lazy.PlacesUtils.bookmarks.toolbarGuid 1285 ) 1286 ).childCount; 1287 if (numBookmarksOnToolbar > this.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE) { 1288 uncollapseToolbar(); 1289 } 1290 } 1291 }, 1292 1293 /** 1294 * Determines whether the given "placesContext" menu item would open a link 1295 * under some special conditions, but those special conditions cannot be met. 1296 * 1297 * @param {Element} item The menu or menu item to decide for. 1298 * 1299 * @returns {boolean} Whether the item is an "open" item that should be 1300 * hidden. 1301 */ 1302 shouldHideOpenMenuItem(item) { 1303 if ( 1304 item.hasAttribute("hide-if-disabled-private-browsing") && 1305 !lazy.PrivateBrowsingUtils.enabled 1306 ) { 1307 return true; 1308 } 1309 1310 if ( 1311 item.hasAttribute("hide-if-private-browsing") && 1312 lazy.PrivateBrowsingUtils.isWindowPrivate(item.ownerGlobal) 1313 ) { 1314 return true; 1315 } 1316 1317 if ( 1318 item.hasAttribute("hide-if-usercontext-disabled") && 1319 !Services.prefs.getBoolPref("privacy.userContext.enabled", false) 1320 ) { 1321 return true; 1322 } 1323 1324 return false; 1325 }, 1326 1327 async managedPlacesContextShowing(event) { 1328 let menupopup = event.target; 1329 let document = menupopup.ownerDocument; 1330 let window = menupopup.ownerGlobal; 1331 // We need to populate the submenus in order to have information 1332 // to show the context menu. 1333 if ( 1334 menupopup.triggerNode.id == "managed-bookmarks" && 1335 !menupopup.triggerNode.menupopup.hasAttribute("hasbeenopened") 1336 ) { 1337 await window.PlacesToolbarHelper.populateManagedBookmarks( 1338 menupopup.triggerNode.menupopup 1339 ); 1340 } 1341 // Hide everything. We'll unhide the things we need. 1342 Array.from(menupopup.children).forEach(function (child) { 1343 child.hidden = true; 1344 }); 1345 // Store triggerNode in controller for checking if commands are enabled 1346 this.managedBookmarksController.triggerNode = menupopup.triggerNode; 1347 // Container in this context means a folder. 1348 let isFolder = menupopup.triggerNode.hasAttribute("container"); 1349 if (isFolder) { 1350 // Disable the openContainerInTabs menuitem if there 1351 // are no children of the menu that have links. 1352 let openContainerInTabs_menuitem = document.getElementById( 1353 "placesContext_openContainer:tabs" 1354 ); 1355 let menuitems = menupopup.triggerNode.menupopup.children; 1356 let openContainerInTabs = Array.from(menuitems).some( 1357 menuitem => menuitem.link 1358 ); 1359 openContainerInTabs_menuitem.disabled = !openContainerInTabs; 1360 openContainerInTabs_menuitem.hidden = false; 1361 } else { 1362 for (let id of [ 1363 "placesContext_open:newtab", 1364 "placesContext_open:newcontainertab", 1365 "placesContext_open:newwindow", 1366 "placesContext_open:newprivatewindow", 1367 ]) { 1368 let item = document.getElementById(id); 1369 item.hidden = this.shouldHideOpenMenuItem(item); 1370 } 1371 for (let id of ["placesContext_openSeparator", "placesContext_copy"]) { 1372 document.getElementById(id).hidden = false; 1373 } 1374 } 1375 1376 event.target.ownerGlobal.updateCommands("places"); 1377 }, 1378 1379 placesContextShowing(event) { 1380 let menupopup = /** @type {XULPopupElement} */ (event.target); 1381 if ( 1382 !["placesContext", "sidebar-history-context-menu"].includes(menupopup.id) 1383 ) { 1384 // Ignore any popupshowing events from submenus 1385 return; 1386 } 1387 1388 if (menupopup.id == "sidebar-history-context-menu") { 1389 PlacesUIUtils.lastContextMenuTriggerNode = 1390 menupopup.triggerNode.triggerNode; 1391 return; 1392 } 1393 1394 PlacesUIUtils.lastContextMenuTriggerNode = menupopup.triggerNode; 1395 1396 if (Services.prefs.getBoolPref("browser.tabs.loadBookmarksInTabs", false)) { 1397 menupopup.ownerDocument 1398 .getElementById("placesContext_open") 1399 .removeAttribute("default"); 1400 menupopup.ownerDocument 1401 .getElementById("placesContext_open:newtab") 1402 .setAttribute("default", "true"); 1403 // else clause ensures correct behavior if pref is repeatedly toggled 1404 } else { 1405 menupopup.ownerDocument 1406 .getElementById("placesContext_open:newtab") 1407 .removeAttribute("default"); 1408 menupopup.ownerDocument 1409 .getElementById("placesContext_open") 1410 .setAttribute("default", "true"); 1411 } 1412 1413 let isManaged = !!menupopup.triggerNode.closest("#managed-bookmarks"); 1414 if (isManaged) { 1415 this.managedPlacesContextShowing(event); 1416 return; 1417 } 1418 menupopup._view = this.getViewForNode(menupopup.triggerNode); 1419 if (!menupopup._view) { 1420 // This can happen if we try to invoke the context menu on 1421 // an uninitialized places toolbar. Just bail out: 1422 event.preventDefault(); 1423 return; 1424 } 1425 if (!this.openInTabClosesMenu) { 1426 menupopup.ownerDocument 1427 .getElementById("placesContext_open:newtab") 1428 .setAttribute("closemenu", "single"); 1429 } 1430 if (!menupopup._view.buildContextMenu(menupopup)) { 1431 event.preventDefault(); 1432 } 1433 }, 1434 1435 placesContextHiding(event) { 1436 let menupopup = event.target; 1437 if (menupopup._view) { 1438 menupopup._view.destroyContextMenu(); 1439 } 1440 1441 if ( 1442 [ 1443 "sidebar-history-context-menu", 1444 "placesContext", 1445 "sidebar-synced-tabs-context-menu", 1446 ].includes(menupopup.id) 1447 ) { 1448 PlacesUIUtils.lastContextMenuTriggerNode = null; 1449 PlacesUIUtils.lastContextMenuCommand = null; 1450 } 1451 }, 1452 1453 createContainerTabMenu(event) { 1454 let window = event.target.ownerGlobal; 1455 return window.createUserContextMenu(event, { isContextMenu: true }); 1456 }, 1457 1458 openInContainerTab(event) { 1459 PlacesUIUtils.lastContextMenuCommand = "placesCmd_open:newcontainertab"; 1460 let userContextId = parseInt( 1461 event.target.getAttribute("data-usercontextid") 1462 ); 1463 let triggerNode = this.lastContextMenuTriggerNode; 1464 let isManaged = !!triggerNode?.closest("#managed-bookmarks"); 1465 if (isManaged) { 1466 let window = triggerNode.ownerGlobal; 1467 window.openTrustedLinkIn(triggerNode.link, "tab", { userContextId }); 1468 return; 1469 } 1470 let view = this.getViewForNode(triggerNode); 1471 this._openNodeIn( 1472 view?.selectedNode || triggerNode, 1473 "tab", 1474 view?.ownerWindow || triggerNode.ownerGlobal.top, 1475 { 1476 userContextId, 1477 } 1478 ); 1479 }, 1480 1481 openSelectionInTabs(event) { 1482 let isManaged = 1483 !!event.target.parentNode.triggerNode.closest("#managed-bookmarks"); 1484 let controller; 1485 if (isManaged) { 1486 controller = this.managedBookmarksController; 1487 } else { 1488 controller = PlacesUIUtils.getViewForNode( 1489 PlacesUIUtils.lastContextMenuTriggerNode 1490 ).controller; 1491 } 1492 controller.openSelectionInTabs(event); 1493 }, 1494 1495 managedBookmarksController: { 1496 triggerNode: null, 1497 1498 openSelectionInTabs(event) { 1499 let window = event.target.ownerGlobal; 1500 let menuitems = event.target.parentNode.triggerNode.menupopup.children; 1501 let items = []; 1502 for (let i = 0; i < menuitems.length; i++) { 1503 if (menuitems[i].link) { 1504 let item = {}; 1505 item.uri = menuitems[i].link; 1506 item.isBookmark = true; 1507 items.push(item); 1508 } 1509 } 1510 PlacesUIUtils.openTabset(items, event, window); 1511 }, 1512 1513 isCommandEnabled(command) { 1514 switch (command) { 1515 case "placesCmd_copy": 1516 case "placesCmd_open:window": 1517 case "placesCmd_open:privatewindow": 1518 case "placesCmd_open:tab": { 1519 return true; 1520 } 1521 } 1522 return false; 1523 }, 1524 1525 doCommand(command) { 1526 let window = this.triggerNode.ownerGlobal; 1527 switch (command) { 1528 case "placesCmd_copy": { 1529 lazy.BrowserUtils.copyLink( 1530 this.triggerNode.link, 1531 this.triggerNode.label 1532 ); 1533 break; 1534 } 1535 case "placesCmd_open:privatewindow": 1536 window.openTrustedLinkIn(this.triggerNode.link, "window", { 1537 private: true, 1538 }); 1539 break; 1540 case "placesCmd_open:window": 1541 window.openTrustedLinkIn(this.triggerNode.link, "window", { 1542 private: false, 1543 }); 1544 break; 1545 case "placesCmd_open:tab": { 1546 window.openTrustedLinkIn(this.triggerNode.link, "tab"); 1547 } 1548 } 1549 }, 1550 }, 1551 1552 async maybeAddImportButton() { 1553 if (!Services.policies.isAllowed("profileImport")) { 1554 return; 1555 } 1556 1557 let numberOfBookmarks = await lazy.PlacesUtils.withConnectionWrapper( 1558 "PlacesUIUtils: maybeAddImportButton", 1559 async db => { 1560 let rows = await db.execute( 1561 `SELECT COUNT(*) as n FROM moz_bookmarks b 1562 JOIN moz_bookmarks p ON p.id = b.parent 1563 WHERE p.guid = :guid`, 1564 { guid: lazy.PlacesUtils.bookmarks.toolbarGuid } 1565 ); 1566 return rows[0].getResultByName("n"); 1567 } 1568 ).catch(e => { 1569 // We want to report errors, but we still want to add the button then: 1570 console.error(e); 1571 return 0; 1572 }); 1573 1574 if (numberOfBookmarks < 3) { 1575 lazy.CustomizableUI.addWidgetToArea( 1576 "import-button", 1577 lazy.CustomizableUI.AREA_BOOKMARKS, 1578 0 1579 ); 1580 Services.prefs.setBoolPref("browser.bookmarks.addedImportButton", true); 1581 this.removeImportButtonWhenImportSucceeds(); 1582 } 1583 }, 1584 1585 removeImportButtonWhenImportSucceeds() { 1586 // If the user (re)moved the button, clear the pref and stop worrying about 1587 // moving the item. 1588 let placement = lazy.CustomizableUI.getPlacementOfWidget("import-button"); 1589 if (placement?.area != lazy.CustomizableUI.AREA_BOOKMARKS) { 1590 Services.prefs.clearUserPref("browser.bookmarks.addedImportButton"); 1591 return; 1592 } 1593 // Otherwise, wait for a successful migration: 1594 let obs = (subject, topic, data) => { 1595 if ( 1596 data == lazy.MigrationUtils.resourceTypes.BOOKMARKS && 1597 lazy.MigrationUtils.getImportedCount("bookmarks") > 0 1598 ) { 1599 lazy.CustomizableUI.removeWidgetFromArea("import-button"); 1600 Services.prefs.clearUserPref("browser.bookmarks.addedImportButton"); 1601 Services.obs.removeObserver(obs, "Migration:ItemAfterMigrate"); 1602 Services.obs.removeObserver(obs, "Migration:ItemError"); 1603 } 1604 }; 1605 Services.obs.addObserver(obs, "Migration:ItemAfterMigrate"); 1606 Services.obs.addObserver(obs, "Migration:ItemError"); 1607 }, 1608 1609 /** 1610 * Tries to initiate a speculative connection to a given url. This is not 1611 * infallible, if a speculative connection cannot be initialized, it will be a 1612 * no-op. 1613 * 1614 * @param {string} url entity to initiate 1615 * a speculative connection for. 1616 * @param {Window} window the window from where the connection is initialized. 1617 */ 1618 setupSpeculativeConnection(url, window) { 1619 if ( 1620 !Services.prefs.getBoolPref( 1621 "browser.places.speculativeConnect.enabled", 1622 true 1623 ) 1624 ) { 1625 return; 1626 } 1627 if (!url.startsWith("http")) { 1628 return; 1629 } 1630 try { 1631 let uri = Services.io.newURI(url); 1632 Services.io.speculativeConnect( 1633 uri, 1634 window.gBrowser.contentPrincipal, 1635 null, 1636 false 1637 ); 1638 } catch (ex) { 1639 // Can't setup speculative connection for this url, just ignore it. 1640 } 1641 }, 1642 1643 /** 1644 * Sets up a speculative connection to the target of a 1645 * clicked places DOM node on left and middle click. 1646 * 1647 * @param {MouseEvent} event the mousedown event. 1648 */ 1649 maybeSpeculativeConnectOnMouseDown(event) { 1650 if ( 1651 event.type == "mousedown" && 1652 event.target._placesNode?.uri && 1653 event.button != 2 1654 ) { 1655 PlacesUIUtils.setupSpeculativeConnection( 1656 event.target._placesNode.uri, 1657 event.target.ownerGlobal 1658 ); 1659 } 1660 }, 1661 1662 /** 1663 * Generates a cached-favicon: link for an icon URL, that will allow to fetch 1664 * the icon from the local favicons cache, rather than from the network. 1665 * If the icon URL is invalid, fallbacks to the default favicon URL. 1666 * 1667 * @param {string} icon The url of the icon to load from local cache. 1668 * @returns {string} a "cached-favicon:" prefixed URL, unless the original 1669 * URL protocol refers to a local resource, then it will just pass-through 1670 * unchanged. 1671 */ 1672 getImageURL(icon) { 1673 // don't initiate a connection just to fetch a favicon (see bug 467828) 1674 try { 1675 return lazy.PlacesUtils.favicons.getFaviconLinkForIcon( 1676 Services.io.newURI(icon) 1677 ).spec; 1678 } catch (ex) {} 1679 return lazy.PlacesUtils.favicons.defaultFavicon.spec; 1680 }, 1681 1682 /** 1683 * Determines the string indexes where titles differ from similar titles (where 1684 * the first n characters are the same) in the provided list of items, and 1685 * adds that into the item. 1686 * 1687 * This assumes the titles will be displayed along the lines of 1688 * `Start of title ... place where differs` the index would be reference 1689 * the `p` here. 1690 * 1691 * @param {object[]} candidates 1692 * An array of candidates to modify. The candidates should have a `title` 1693 * property which should be a string or null. 1694 * The order of the array does not matter. The objects are modified 1695 * in-place. 1696 * If a difference to other similar titles is found then a 1697 * `titleDifferentIndex` property will be inserted into all similar 1698 * candidates with the index of the start of the difference. 1699 */ 1700 insertTitleStartDiffs(candidates) { 1701 function findStartDifference(a, b) { 1702 let i; 1703 // We already know the start is the same, so skip that part. 1704 for (i = PlacesUIUtils.similarTitlesMinChars; i < a.length; i++) { 1705 if (a[i] != b[i]) { 1706 return i; 1707 } 1708 } 1709 if (b.length > i) { 1710 return i; 1711 } 1712 // They are the same. 1713 return -1; 1714 } 1715 1716 let longTitles = new Map(); 1717 1718 for (let candidate of candidates) { 1719 // Title is too short for us to care about, simply continue. 1720 if ( 1721 !candidate.title || 1722 candidate.title.length < this.similarTitlesMinChars 1723 ) { 1724 continue; 1725 } 1726 let titleBeginning = candidate.title.slice(0, this.similarTitlesMinChars); 1727 let matches = longTitles.get(titleBeginning); 1728 if (matches) { 1729 for (let match of matches) { 1730 let startDiff = findStartDifference(candidate.title, match.title); 1731 if (startDiff > 0) { 1732 candidate.titleDifferentIndex = startDiff; 1733 // If we have an existing difference index for the match, move 1734 // it forward if this one is earlier in the string. 1735 if ( 1736 !("titleDifferentIndex" in match) || 1737 match.titleDifferentIndex > startDiff 1738 ) { 1739 match.titleDifferentIndex = startDiff; 1740 } 1741 } 1742 } 1743 1744 matches.push(candidate); 1745 } else { 1746 longTitles.set(titleBeginning, [candidate]); 1747 } 1748 } 1749 }, 1750 }; 1751 1752 /** 1753 * Promise used by the toolbar view browser-places to determine whether we 1754 * can start loading its content (which involves IO, and so is postponed 1755 * during startup). 1756 */ 1757 PlacesUIUtils.canLoadToolbarContentPromise = new Promise(resolve => { 1758 PlacesUIUtils.unblockToolbars = resolve; 1759 }); 1760 1761 // These are lazy getters to avoid importing PlacesUtils immediately. 1762 ChromeUtils.defineLazyGetter(PlacesUIUtils, "PLACES_FLAVORS", () => { 1763 return [ 1764 lazy.PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, 1765 lazy.PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, 1766 lazy.PlacesUtils.TYPE_X_MOZ_PLACE, 1767 ]; 1768 }); 1769 ChromeUtils.defineLazyGetter(PlacesUIUtils, "URI_FLAVORS", () => { 1770 return [ 1771 lazy.PlacesUtils.TYPE_X_MOZ_URL, 1772 TAB_DROP_TYPE, 1773 lazy.PlacesUtils.TYPE_PLAINTEXT, 1774 ]; 1775 }); 1776 ChromeUtils.defineLazyGetter(PlacesUIUtils, "SUPPORTED_FLAVORS", () => { 1777 return [ 1778 ...PlacesUIUtils.PLACES_FLAVORS, 1779 ...PlacesUIUtils.URI_FLAVORS, 1780 "application/x-torbrowser-opaque", 1781 ]; 1782 }); 1783 1784 ChromeUtils.defineLazyGetter(PlacesUIUtils, "promptLocalization", () => { 1785 return new Localization( 1786 ["browser/placesPrompts.ftl", "branding/brand.ftl"], 1787 true 1788 ); 1789 }); 1790 1791 XPCOMUtils.defineLazyPreferenceGetter( 1792 PlacesUIUtils, 1793 "similarTitlesMinChars", 1794 "browser.places.similarTitlesMinChars", 1795 20 1796 ); 1797 XPCOMUtils.defineLazyPreferenceGetter( 1798 PlacesUIUtils, 1799 "loadBookmarksInBackground", 1800 "browser.tabs.loadBookmarksInBackground", 1801 false 1802 ); 1803 XPCOMUtils.defineLazyPreferenceGetter( 1804 PlacesUIUtils, 1805 "loadBookmarksInTabs", 1806 "browser.tabs.loadBookmarksInTabs", 1807 false 1808 ); 1809 XPCOMUtils.defineLazyPreferenceGetter( 1810 PlacesUIUtils, 1811 "openInTabClosesMenu", 1812 "browser.bookmarks.openInTabClosesMenu", 1813 false 1814 ); 1815 XPCOMUtils.defineLazyPreferenceGetter( 1816 PlacesUIUtils, 1817 "maxRecentFolders", 1818 "browser.bookmarks.editDialog.maxRecentFolders", 1819 7 1820 ); 1821 1822 XPCOMUtils.defineLazyPreferenceGetter( 1823 PlacesUIUtils, 1824 "defaultParentGuid", 1825 "browser.bookmarks.defaultLocation", 1826 "", // Avoid eagerly loading PlacesUtils. 1827 null, 1828 async prefValue => { 1829 if (!prefValue) { 1830 return lazy.PlacesUtils.bookmarks.toolbarGuid; 1831 } 1832 if (["toolbar", "menu", "unfiled"].includes(prefValue)) { 1833 return lazy.PlacesUtils.bookmarks[prefValue + "Guid"]; 1834 } 1835 1836 try { 1837 return await lazy.PlacesUtils.bookmarks 1838 .fetch({ guid: prefValue }) 1839 .then(bm => bm.guid); 1840 } catch (ex) { 1841 // The guid may have an invalid format. 1842 return lazy.PlacesUtils.bookmarks.toolbarGuid; 1843 } 1844 } 1845 ); 1846 1847 /** 1848 * Determines if an unwrapped node can be moved. 1849 * 1850 * @param {object} unwrappedNode 1851 * A node unwrapped by PlacesUtils.unwrapNodes(). 1852 * @returns {boolean} True if the node can be moved, false otherwise. 1853 */ 1854 function canMoveUnwrappedNode(unwrappedNode) { 1855 if ( 1856 (unwrappedNode.concreteGuid && 1857 lazy.PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) || 1858 (unwrappedNode.guid && lazy.PlacesUtils.isRootItem(unwrappedNode.guid)) 1859 ) { 1860 return false; 1861 } 1862 1863 let parentGuid = unwrappedNode.parentGuid; 1864 if (parentGuid == lazy.PlacesUtils.bookmarks.rootGuid) { 1865 return false; 1866 } 1867 1868 return true; 1869 } 1870 1871 /** 1872 * This gets the most appropriate item for using for batching. In the case of multiple 1873 * views being related, the method returns the most expensive result to batch. 1874 * For example, if it detects the left-hand library pane, then it will look for 1875 * and return the reference to the right-hand pane. 1876 * 1877 * @param {object} viewOrElement The item to check. 1878 * @returns {object} Will return the best result node to batch, or null 1879 * if one could not be found. 1880 */ 1881 function getResultForBatching(viewOrElement) { 1882 if ( 1883 viewOrElement && 1884 Element.isInstance(viewOrElement) && 1885 viewOrElement.id === "placesList" 1886 ) { 1887 // Note: fall back to the existing item if we can't find the right-hane pane. 1888 viewOrElement = 1889 viewOrElement.ownerDocument.getElementById("placeContent") || 1890 viewOrElement; 1891 } 1892 1893 if (viewOrElement && viewOrElement.result) { 1894 return viewOrElement.result; 1895 } 1896 1897 return null; 1898 } 1899 1900 /** 1901 * Processes a set of transfer items and returns transactions to insert or 1902 * move them. 1903 * 1904 * @param {Array} items A list of unwrapped nodes to get transactions for. 1905 * @param {number} insertionIndex The requested index for insertion. 1906 * @param {string} insertionParentGuid The guid of the parent folder to insert 1907 * or move the items to. 1908 * @param {boolean} doMove Set to true to MOVE the items if possible, false will 1909 * copy them. 1910 * @returns {Array} Returns an array of created PlacesTransactions. 1911 */ 1912 function getTransactionsForTransferItems( 1913 items, 1914 insertionIndex, 1915 insertionParentGuid, 1916 doMove 1917 ) { 1918 let canMove = true; 1919 for (let item of items) { 1920 if (!PlacesUIUtils.SUPPORTED_FLAVORS.includes(item.type)) { 1921 throw new Error(`Unsupported '${item.type}' data type`); 1922 } 1923 1924 // Work out if this is data from the same app session we're running in. 1925 if ( 1926 !("instanceId" in item) || 1927 item.instanceId != lazy.PlacesUtils.instanceId 1928 ) { 1929 if (item.type == lazy.PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) { 1930 throw new Error( 1931 "Can't copy a container from a legacy-transactions build" 1932 ); 1933 } 1934 // Only log if this is one of "our" types as external items, e.g. drag from 1935 // url bar to toolbar, shouldn't complain. 1936 if (PlacesUIUtils.PLACES_FLAVORS.includes(item.type)) { 1937 console.error( 1938 "Tried to move an unmovable Places " + 1939 "node, reverting to a copy operation." 1940 ); 1941 } 1942 1943 // We can never move from an external copy. 1944 canMove = false; 1945 } 1946 1947 if (doMove && canMove) { 1948 canMove = canMoveUnwrappedNode(item); 1949 } 1950 } 1951 1952 if (doMove && !canMove) { 1953 doMove = false; 1954 } 1955 1956 if (doMove) { 1957 // Move is simple, we pass the transaction a list of GUIDs and where to move 1958 // them to. 1959 return [ 1960 lazy.PlacesTransactions.Move({ 1961 guids: items.map(item => item.itemGuid), 1962 newParentGuid: insertionParentGuid, 1963 newIndex: insertionIndex, 1964 }), 1965 ]; 1966 } 1967 1968 return getTransactionsForCopy(items, insertionIndex, insertionParentGuid); 1969 } 1970 1971 /** 1972 * Processes a set of transfer items and returns an array of transactions. 1973 * 1974 * @param {Array} items A list of unwrapped nodes to get transactions for. 1975 * @param {number} insertionIndex The requested index for insertion. 1976 * @param {string} insertionParentGuid The guid of the parent folder to insert 1977 * or move the items to. 1978 * @returns {Array} Returns an array of created PlacesTransactions. 1979 */ 1980 function getTransactionsForCopy(items, insertionIndex, insertionParentGuid) { 1981 let transactions = []; 1982 let index = insertionIndex; 1983 1984 for (let item of items) { 1985 let transaction; 1986 let guid = item.itemGuid; 1987 1988 if ( 1989 PlacesUIUtils.PLACES_FLAVORS.includes(item.type) && 1990 // For anything that is comming from within this session, we do a 1991 // direct copy, otherwise we fallback and form a new item below. 1992 "instanceId" in item && 1993 item.instanceId == lazy.PlacesUtils.instanceId && 1994 // If the Item doesn't have a guid, this could be a virtual tag query or 1995 // other item, so fallback to inserting a new bookmark with the URI. 1996 guid && 1997 // For virtual root items, we fallback to creating a new bookmark, as 1998 // we want a shortcut to be created, not a full tree copy. 1999 !lazy.PlacesUtils.bookmarks.isVirtualRootItem(guid) && 2000 !lazy.PlacesUtils.isVirtualLeftPaneItem(guid) 2001 ) { 2002 transaction = lazy.PlacesTransactions.Copy({ 2003 guid, 2004 newIndex: index, 2005 newParentGuid: insertionParentGuid, 2006 }); 2007 } else if (item.type == lazy.PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) { 2008 transaction = lazy.PlacesTransactions.NewSeparator({ 2009 index, 2010 parentGuid: insertionParentGuid, 2011 }); 2012 } else { 2013 let title = 2014 item.type != lazy.PlacesUtils.TYPE_PLAINTEXT ? item.title : item.uri; 2015 transaction = lazy.PlacesTransactions.NewBookmark({ 2016 index, 2017 parentGuid: insertionParentGuid, 2018 title, 2019 url: item.uri, 2020 }); 2021 } 2022 2023 transactions.push(transaction); 2024 2025 if (index != -1) { 2026 index++; 2027 } 2028 } 2029 return transactions; 2030 } 2031 2032 function getBrowserWindow(aWindow) { 2033 // Prefer the caller window if it's a browser window, otherwise use 2034 // the top browser window. 2035 return aWindow && 2036 aWindow.document.documentElement.getAttribute("windowtype") == 2037 "navigator:browser" 2038 ? aWindow 2039 : lazy.BrowserWindowTracker.getTopWindow(); 2040 }