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