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