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
6this.EXPORTED_SYMBOLS = ["PlacesUIUtils"];
7
8var Ci = Components.interfaces;
9var Cc = Components.classes;
10var Cr = Components.results;
11var Cu = Components.utils;
12
13Cu.import("resource://gre/modules/XPCOMUtils.jsm");
14Cu.import("resource://gre/modules/Services.jsm");
15Cu.import("resource://gre/modules/Timer.jsm");
16
17Cu.import("resource://gre/modules/PlacesUtils.jsm");
18XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
19                                  "resource://gre/modules/PluralForm.jsm");
20XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
21                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
22XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
23                                  "resource://gre/modules/NetUtil.jsm");
24XPCOMUtils.defineLazyModuleGetter(this, "Task",
25                                  "resource://gre/modules/Task.jsm");
26XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
27                                  "resource:///modules/RecentWindow.jsm");
28
29// PlacesUtils exposes multiple symbols, so we can't use defineLazyModuleGetter.
30Cu.import("resource://gre/modules/PlacesUtils.jsm");
31
32XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
33                                  "resource://gre/modules/PlacesTransactions.jsm");
34
35XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
36                                  "resource://gre/modules/CloudSync.jsm");
37
38XPCOMUtils.defineLazyModuleGetter(this, "Weave",
39                                  "resource://services-sync/main.js");
40
41const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
42const FAVICON_REQUEST_TIMEOUT = 60 * 1000;
43// Map from windows to arrays of data about pending favicon loads.
44let gFaviconLoadDataMap = new Map();
45
46// copied from utilityOverlay.js
47const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
48
49// This function isn't public both because it's synchronous and because it is
50// going to be removed in bug 1072833.
51function IsLivemark(aItemId) {
52  // Since this check may be done on each dragover event, it's worth maintaining
53  // a cache.
54  let self = IsLivemark;
55  if (!("ids" in self)) {
56    const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI;
57
58    let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO);
59    self.ids = new Set(idsVec);
60
61    let obs = Object.freeze({
62      QueryInterface: XPCOMUtils.generateQI(Ci.nsIAnnotationObserver),
63
64      onItemAnnotationSet(itemId, annoName) {
65        if (annoName == LIVEMARK_ANNO)
66          self.ids.add(itemId);
67      },
68
69      onItemAnnotationRemoved(itemId, annoName) {
70        // If annoName is set to an empty string, the item is gone.
71        if (annoName == LIVEMARK_ANNO || annoName == "")
72          self.ids.delete(itemId);
73      },
74
75      onPageAnnotationSet() { },
76      onPageAnnotationRemoved() { },
77    });
78    PlacesUtils.annotations.addObserver(obs);
79    PlacesUtils.registerShutdownFunction(() => {
80      PlacesUtils.annotations.removeObserver(obs);
81    });
82  }
83  return self.ids.has(aItemId);
84}
85
86let InternalFaviconLoader = {
87  /**
88   * This gets called for every inner window that is destroyed.
89   * In the parent process, we process the destruction ourselves. In the child process,
90   * we notify the parent which will then process it based on that message.
91   */
92  observe(subject, topic, data) {
93    let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
94    this.removeRequestsForInner(innerWindowID);
95  },
96
97  /**
98   * Actually cancel the request, and clear the timeout for cancelling it.
99   */
100  _cancelRequest({uri, innerWindowID, timerID, callback}, reason) {
101    // Break cycle
102    let request = callback.request;
103    delete callback.request;
104    // Ensure we don't time out.
105    clearTimeout(timerID);
106    try {
107      request.cancel();
108    } catch (ex) {
109      Cu.reportError("When cancelling a request for " + uri.spec + " because " + reason + ", it was already canceled!");
110    }
111  },
112
113  /**
114   * Called for every inner that gets destroyed, only in the parent process.
115   */
116  removeRequestsForInner(innerID) {
117    for (let [window, loadDataForWindow] of gFaviconLoadDataMap) {
118      let newLoadDataForWindow = loadDataForWindow.filter(loadData => {
119        let innerWasDestroyed = loadData.innerWindowID == innerID;
120        if (innerWasDestroyed) {
121          this._cancelRequest(loadData, "the inner window was destroyed or a new favicon was loaded for it");
122        }
123        // Keep the items whose inner is still alive.
124        return !innerWasDestroyed;
125      });
126      // Map iteration with for...of is safe against modification, so
127      // now just replace the old value:
128      gFaviconLoadDataMap.set(window, newLoadDataForWindow);
129    }
130  },
131
132  /**
133   * Called when a toplevel chrome window unloads. We use this to tidy up after ourselves,
134   * avoid leaks, and cancel any remaining requests. The last part should in theory be
135   * handled by the inner-window-destroyed handlers. We clean up just to be on the safe side.
136   */
137  onUnload(win) {
138    let loadDataForWindow = gFaviconLoadDataMap.get(win);
139    if (loadDataForWindow) {
140      for (let loadData of loadDataForWindow) {
141        this._cancelRequest(loadData, "the chrome window went away");
142      }
143    }
144    gFaviconLoadDataMap.delete(win);
145  },
146
147  /**
148   * Remove a particular favicon load's loading data from our map tracking
149   * load data per chrome window.
150   *
151   * @param win
152   *        the chrome window in which we should look for this load
153   * @param filterData ({innerWindowID, uri, callback})
154   *        the data we should use to find this particular load to remove.
155   *
156   * @return the loadData object we removed, or null if we didn't find any.
157   */
158  _removeLoadDataFromWindowMap(win, {innerWindowID, uri, callback}) {
159    let loadDataForWindow = gFaviconLoadDataMap.get(win);
160    if (loadDataForWindow) {
161      let itemIndex = loadDataForWindow.findIndex(loadData => {
162        return loadData.innerWindowID == innerWindowID &&
163               loadData.uri.equals(uri) &&
164               loadData.callback.request == callback.request;
165      });
166      if (itemIndex != -1) {
167        let loadData = loadDataForWindow[itemIndex];
168        loadDataForWindow.splice(itemIndex, 1);
169        return loadData;
170      }
171    }
172    return null;
173  },
174
175  /**
176   * Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling
177   * information when the request succeeds. Note that right now there are some edge-cases,
178   * such as about: URIs with chrome:// favicons where the success callback is not invoked.
179   * This is OK: we will 'cancel' the request after the timeout (or when the window goes
180   * away) but that will be a no-op in such cases.
181   */
182  _makeCompletionCallback(win, id) {
183    return {
184      onComplete(uri) {
185        let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, {
186          uri,
187          innerWindowID: id,
188          callback: this,
189        });
190        if (loadData) {
191          clearTimeout(loadData.timerID);
192        }
193        delete this.request;
194      },
195    };
196  },
197
198  ensureInitialized() {
199    if (this._initialized) {
200      return;
201    }
202    this._initialized = true;
203
204    Services.obs.addObserver(this, "inner-window-destroyed", false);
205    Services.ppmm.addMessageListener("Toolkit:inner-window-destroyed", msg => {
206      this.removeRequestsForInner(msg.data);
207    });
208  },
209
210  loadFavicon(browser, principal, uri) {
211    this.ensureInitialized();
212    let win = browser.ownerGlobal;
213    if (!gFaviconLoadDataMap.has(win)) {
214      gFaviconLoadDataMap.set(win, []);
215      let unloadHandler = event => {
216        let doc = event.target;
217        let eventWin = doc.defaultView;
218        if (eventWin == win) {
219          win.removeEventListener("unload", unloadHandler);
220          this.onUnload(win);
221        }
222      };
223      win.addEventListener("unload", unloadHandler, true);
224    }
225
226    let {innerWindowID, currentURI} = browser;
227
228    // Immediately cancel any earlier requests
229    this.removeRequestsForInner(innerWindowID);
230
231    // First we do the actual setAndFetch call:
232    let loadType = PrivateBrowsingUtils.isWindowPrivate(win)
233      ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
234      : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
235    let callback = this._makeCompletionCallback(win, innerWindowID);
236    let request = PlacesUtils.favicons.setAndFetchFaviconForPage(currentURI, uri, false,
237                                                                 loadType, callback, principal);
238
239    // Now register the result so we can cancel it if/when necessary.
240    if (!request) {
241      // The favicon service can return with success but no-op (and leave request
242      // as null) if the icon is the same as the page (e.g. for images) or if it is
243      // the favicon for an error page. In this case, we do not need to do anything else.
244      return;
245    }
246    callback.request = request;
247    let loadData = {innerWindowID, uri, callback};
248    loadData.timerID = setTimeout(() => {
249      this._cancelRequest(loadData, "it timed out");
250      this._removeLoadDataFromWindowMap(win, loadData);
251    }, FAVICON_REQUEST_TIMEOUT);
252    let loadDataForWindow = gFaviconLoadDataMap.get(win);
253    loadDataForWindow.push(loadData);
254  },
255};
256
257this.PlacesUIUtils = {
258  ORGANIZER_LEFTPANE_VERSION: 7,
259  ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder",
260  ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery",
261
262  LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
263  DESCRIPTION_ANNO: "bookmarkProperties/description",
264
265  /**
266   * Makes a URI from a spec, and do fixup
267   * @param   aSpec
268   *          The string spec of the URI
269   * @return A URI object for the spec.
270   */
271  createFixedURI: function PUIU_createFixedURI(aSpec) {
272    return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
273  },
274
275  getFormattedString: function PUIU_getFormattedString(key, params) {
276    return bundle.formatStringFromName(key, params, params.length);
277  },
278
279  /**
280   * Get a localized plural string for the specified key name and numeric value
281   * substituting parameters.
282   *
283   * @param   aKey
284   *          String, key for looking up the localized string in the bundle
285   * @param   aNumber
286   *          Number based on which the final localized form is looked up
287   * @param   aParams
288   *          Array whose items will substitute #1, #2,... #n parameters
289   *          in the string.
290   *
291   * @see https://developer.mozilla.org/en/Localization_and_Plurals
292   * @return The localized plural string.
293   */
294  getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) {
295    let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey));
296
297    // Replace #1 with aParams[0], #2 with aParams[1], and so on.
298    return str.replace(/\#(\d+)/g, function (matchedId, matchedNumber) {
299      let param = aParams[parseInt(matchedNumber, 10) - 1];
300      return param !== undefined ? param : matchedId;
301    });
302  },
303
304  getString: function PUIU_getString(key) {
305    return bundle.GetStringFromName(key);
306  },
307
308  get _copyableAnnotations() {
309    return [
310      this.DESCRIPTION_ANNO,
311      this.LOAD_IN_SIDEBAR_ANNO,
312      PlacesUtils.READ_ONLY_ANNO,
313    ];
314  },
315
316  /**
317   * Get a transaction for copying a uri item (either a bookmark or a history
318   * entry) from one container to another.
319   *
320   * @param   aData
321   *          JSON object of dropped or pasted item properties
322   * @param   aContainer
323   *          The container being copied into
324   * @param   aIndex
325   *          The index within the container the item is copied to
326   * @return A nsITransaction object that performs the copy.
327   *
328   * @note Since a copy creates a completely new item, only some internal
329   *       annotations are synced from the old one.
330   * @see this._copyableAnnotations for the list of copyable annotations.
331   */
332  _getURIItemCopyTransaction:
333  function PUIU__getURIItemCopyTransaction(aData, aContainer, aIndex)
334  {
335    let transactions = [];
336    if (aData.dateAdded) {
337      transactions.push(
338        new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
339      );
340    }
341    if (aData.lastModified) {
342      transactions.push(
343        new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
344      );
345    }
346
347    let annos = [];
348    if (aData.annos) {
349      annos = aData.annos.filter(function (aAnno) {
350        return this._copyableAnnotations.includes(aAnno.name);
351      }, this);
352    }
353
354    // There's no need to copy the keyword since it's bound to the bookmark url.
355    return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri),
356                                               aContainer, aIndex, aData.title,
357                                               null, annos, transactions);
358  },
359
360  /**
361   * Gets a transaction for copying (recursively nesting to include children)
362   * a folder (or container) and its contents from one folder to another.
363   *
364   * @param   aData
365   *          Unwrapped dropped folder data - Obj containing folder and children
366   * @param   aContainer
367   *          The container we are copying into
368   * @param   aIndex
369   *          The index in the destination container to insert the new items
370   * @return A nsITransaction object that will perform the copy.
371   *
372   * @note Since a copy creates a completely new item, only some internal
373   *       annotations are synced from the old one.
374   * @see this._copyableAnnotations for the list of copyable annotations.
375   */
376  _getFolderCopyTransaction(aData, aContainer, aIndex) {
377    function getChildItemsTransactions(aRoot) {
378      let transactions = [];
379      let index = aIndex;
380      for (let i = 0; i < aRoot.childCount; ++i) {
381        let child = aRoot.getChild(i);
382        // Temporary hacks until we switch to PlacesTransactions.jsm.
383        let isLivemark =
384          PlacesUtils.annotations.itemHasAnnotation(child.itemId,
385                                                    PlacesUtils.LMANNO_FEEDURI);
386        let [node] = PlacesUtils.unwrapNodes(
387          PlacesUtils.wrapNode(child, PlacesUtils.TYPE_X_MOZ_PLACE, isLivemark),
388          PlacesUtils.TYPE_X_MOZ_PLACE
389        );
390
391        // Make sure that items are given the correct index, this will be
392        // passed by the transaction manager to the backend for the insertion.
393        // Insertion behaves differently for DEFAULT_INDEX (append).
394        if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) {
395          index = i;
396        }
397
398        if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
399          if (node.livemark && node.annos) {
400            transactions.push(
401              PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index)
402            );
403          }
404          else {
405            transactions.push(
406              PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index)
407            );
408          }
409        }
410        else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
411          transactions.push(new PlacesCreateSeparatorTransaction(-1, index));
412        }
413        else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
414          transactions.push(
415            PlacesUIUtils._getURIItemCopyTransaction(node, -1, index)
416          );
417        }
418        else {
419          throw new Error("Unexpected item under a bookmarks folder");
420        }
421      }
422      return transactions;
423    }
424
425    if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder.
426      let transactions = [];
427      if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
428        let {root} = PlacesUtils.getFolderContents(aData.id, false, false);
429        let urls = PlacesUtils.getURLsForContainerNode(root);
430        root.containerOpen = false;
431        for (let { uri } of urls) {
432          transactions.push(
433            new PlacesTagURITransaction(NetUtil.newURI(uri), [aData.title])
434          );
435        }
436      }
437      return new PlacesAggregatedTransaction("addTags", transactions);
438    }
439
440    if (aData.livemark && aData.annos) { // Copying a livemark.
441      return this._getLivemarkCopyTransaction(aData, aContainer, aIndex);
442    }
443
444    let {root} = PlacesUtils.getFolderContents(aData.id, false, false);
445    let transactions = getChildItemsTransactions(root);
446    root.containerOpen = false;
447
448    if (aData.dateAdded) {
449      transactions.push(
450        new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
451      );
452    }
453    if (aData.lastModified) {
454      transactions.push(
455        new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
456      );
457    }
458
459    let annos = [];
460    if (aData.annos) {
461      annos = aData.annos.filter(function (aAnno) {
462        return this._copyableAnnotations.includes(aAnno.name);
463      }, this);
464    }
465
466    return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex,
467                                             annos, transactions);
468  },
469
470  /**
471   * Gets a transaction for copying a live bookmark item from one container to
472   * another.
473   *
474   * @param   aData
475   *          Unwrapped live bookmarkmark data
476   * @param   aContainer
477   *          The container we are copying into
478   * @param   aIndex
479   *          The index in the destination container to insert the new items
480   * @return A nsITransaction object that will perform the copy.
481   *
482   * @note Since a copy creates a completely new item, only some internal
483   *       annotations are synced from the old one.
484   * @see this._copyableAnnotations for the list of copyable annotations.
485   */
486  _getLivemarkCopyTransaction:
487  function PUIU__getLivemarkCopyTransaction(aData, aContainer, aIndex)
488  {
489    if (!aData.livemark || !aData.annos) {
490      throw new Error("node is not a livemark");
491    }
492
493    let feedURI, siteURI;
494    let annos = [];
495    if (aData.annos) {
496      annos = aData.annos.filter(function (aAnno) {
497        if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) {
498          feedURI = PlacesUtils._uri(aAnno.value);
499        }
500        else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) {
501          siteURI = PlacesUtils._uri(aAnno.value);
502        }
503        return this._copyableAnnotations.includes(aAnno.name)
504      }, this);
505    }
506
507    return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title,
508                                               aContainer, aIndex, annos);
509  },
510
511  /**
512   * Constructs a Transaction for the drop or paste of a blob of data into
513   * a container.
514   * @param   data
515   *          The unwrapped data blob of dropped or pasted data.
516   * @param   type
517   *          The content type of the data
518   * @param   container
519   *          The container the data was dropped or pasted into
520   * @param   index
521   *          The index within the container the item was dropped or pasted at
522   * @param   copy
523   *          The drag action was copy, so don't move folders or links.
524   * @return An object implementing nsITransaction that can perform
525   *         the move/insert.
526   */
527  makeTransaction:
528  function PUIU_makeTransaction(data, type, container, index, copy)
529  {
530    switch (data.type) {
531      case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
532        if (copy) {
533          return this._getFolderCopyTransaction(data, container, index);
534        }
535
536        // Otherwise move the item.
537        return new PlacesMoveItemTransaction(data.id, container, index);
538      case PlacesUtils.TYPE_X_MOZ_PLACE:
539        if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked.
540          return this._getURIItemCopyTransaction(data, container, index);
541        }
542
543        // Otherwise move the item.
544        return new PlacesMoveItemTransaction(data.id, container, index);
545      case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
546        if (copy) {
547          // There is no data in a separator, so copying it just amounts to
548          // inserting a new separator.
549          return new PlacesCreateSeparatorTransaction(container, index);
550        }
551
552        // Otherwise move the item.
553        return new PlacesMoveItemTransaction(data.id, container, index);
554      default:
555        if (type == PlacesUtils.TYPE_X_MOZ_URL ||
556            type == PlacesUtils.TYPE_UNICODE ||
557            type == TAB_DROP_TYPE) {
558          let title = type != PlacesUtils.TYPE_UNICODE ? data.title
559                                                       : data.uri;
560          return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri),
561                                                     container, index, title);
562        }
563    }
564    return null;
565  },
566
567  /**
568   * ********* PlacesTransactions version of the function defined above ********
569   *
570   * Constructs a Places Transaction for the drop or paste of a blob of data
571   * into a container.
572   *
573   * @param   aData
574   *          The unwrapped data blob of dropped or pasted data.
575   * @param   aType
576   *          The content type of the data.
577   * @param   aNewParentGuid
578   *          GUID of the container the data was dropped or pasted into.
579   * @param   aIndex
580   *          The index within the container the item was dropped or pasted at.
581   * @param   aCopy
582   *          The drag action was copy, so don't move folders or links.
583   *
584   * @return  a Places Transaction that can be transacted for performing the
585   *          move/insert command.
586   */
587  getTransactionForData: function(aData, aType, aNewParentGuid, aIndex, aCopy) {
588    if (!this.SUPPORTED_FLAVORS.includes(aData.type))
589      throw new Error(`Unsupported '${aData.type}' data type`);
590
591    if ("itemGuid" in aData) {
592      if (!this.PLACES_FLAVORS.includes(aData.type))
593        throw new Error (`itemGuid unexpectedly set on ${aData.type} data`);
594
595      let info = { guid: aData.itemGuid
596                 , newParentGuid: aNewParentGuid
597                 , newIndex: aIndex };
598      if (aCopy) {
599        info.excludingAnnotation = "Places/SmartBookmark";
600        return PlacesTransactions.Copy(info);
601      }
602      return PlacesTransactions.Move(info);
603    }
604
605    // Since it's cheap and harmless, we allow the paste of separators and
606    // bookmarks from builds that use legacy transactions (i.e. when itemGuid
607    // was not set on PLACES_FLAVORS data). Containers are a different story,
608    // and thus disallowed.
609    if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
610      throw new Error("Can't copy a container from a legacy-transactions build");
611
612    if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
613      return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid
614                                             , index: aIndex });
615    }
616
617    let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title
618                                                       : aData.uri;
619    return PlacesTransactions.NewBookmark({ uri: NetUtil.newURI(aData.uri)
620                                          , title: title
621                                          , parentGuid: aNewParentGuid
622                                          , index: aIndex });
623  },
624
625  /**
626   * Shows the bookmark dialog corresponding to the specified info.
627   *
628   * @param aInfo
629   *        Describes the item to be edited/added in the dialog.
630   *        See documentation at the top of bookmarkProperties.js
631   * @param aWindow
632   *        Owner window for the new dialog.
633   *
634   * @see documentation at the top of bookmarkProperties.js
635   * @return true if any transaction has been performed, false otherwise.
636   */
637  showBookmarkDialog:
638  function PUIU_showBookmarkDialog(aInfo, aParentWindow) {
639    // Preserve size attributes differently based on the fact the dialog has
640    // a folder picker or not, since it needs more horizontal space than the
641    // other controls.
642    let hasFolderPicker = !("hiddenRows" in aInfo) ||
643                          !aInfo.hiddenRows.includes("folderPicker");
644    // Use a different chrome url to persist different sizes.
645    let dialogURL = hasFolderPicker ?
646                    "chrome://browser/content/places/bookmarkProperties2.xul" :
647                    "chrome://browser/content/places/bookmarkProperties.xul";
648
649    let features = "centerscreen,chrome,modal,resizable=yes";
650    aParentWindow.openDialog(dialogURL, "", features, aInfo);
651    return ("performed" in aInfo && aInfo.performed);
652  },
653
654  _getTopBrowserWin: function PUIU__getTopBrowserWin() {
655    return RecentWindow.getMostRecentBrowserWindow();
656  },
657
658  /**
659   * set and fetch a favicon. Can only be used from the parent process.
660   * @param browser   {Browser}   The XUL browser element for which we're fetching a favicon.
661   * @param principal {Principal} The loading principal to use for the fetch.
662   * @param uri       {URI}       The URI to fetch.
663   */
664  loadFavicon(browser, principal, uri) {
665    if (gInContentProcess) {
666      throw new Error("Can't track loads from within the child process!");
667    }
668    InternalFaviconLoader.loadFavicon(browser, principal, uri);
669  },
670
671  /**
672   * Returns the closet ancestor places view for the given DOM node
673   * @param aNode
674   *        a DOM node
675   * @return the closet ancestor places view if exists, null otherwsie.
676   */
677  getViewForNode: function PUIU_getViewForNode(aNode) {
678    let node = aNode;
679
680    // The view for a <menu> of which its associated menupopup is a places
681    // view, is the menupopup.
682    if (node.localName == "menu" && !node._placesNode &&
683        node.lastChild._placesView)
684      return node.lastChild._placesView;
685
686    while (node instanceof Ci.nsIDOMElement) {
687      if (node._placesView)
688        return node._placesView;
689      if (node.localName == "tree" && node.getAttribute("type") == "places")
690        return node;
691
692      node = node.parentNode;
693    }
694
695    return null;
696  },
697
698  /**
699   * By calling this before visiting an URL, the visit will be associated to a
700   * TRANSITION_TYPED transition (if there is no a referrer).
701   * This is used when visiting pages from the history menu, history sidebar,
702   * url bar, url autocomplete results, and history searches from the places
703   * organizer.  If this is not called visits will be marked as
704   * TRANSITION_LINK.
705   */
706  markPageAsTyped: function PUIU_markPageAsTyped(aURL) {
707    PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL));
708  },
709
710  /**
711   * By calling this before visiting an URL, the visit will be associated to a
712   * TRANSITION_BOOKMARK transition.
713   * This is used when visiting pages from the bookmarks menu,
714   * personal toolbar, and bookmarks from within the places organizer.
715   * If this is not called visits will be marked as TRANSITION_LINK.
716   */
717  markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) {
718    PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL));
719  },
720
721  /**
722   * By calling this before visiting an URL, any visit in frames will be
723   * associated to a TRANSITION_FRAMED_LINK transition.
724   * This is actually used to distinguish user-initiated visits in frames
725   * so automatic visits can be correctly ignored.
726   */
727  markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) {
728    PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL));
729  },
730
731  /**
732   * Allows opening of javascript/data URI only if the given node is
733   * bookmarked (see bug 224521).
734   * @param aURINode
735   *        a URI node
736   * @param aWindow
737   *        a window on which a potential error alert is shown on.
738   * @return true if it's safe to open the node in the browser, false otherwise.
739   *
740   */
741  checkURLSecurity: function PUIU_checkURLSecurity(aURINode, aWindow) {
742    if (PlacesUtils.nodeIsBookmark(aURINode))
743      return true;
744
745    var uri = PlacesUtils._uri(aURINode.uri);
746    if (uri.schemeIs("javascript") || uri.schemeIs("data")) {
747      const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
748      var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"].
749                           getService(Ci.nsIStringBundleService).
750                           createBundle(BRANDING_BUNDLE_URI).
751                           GetStringFromName("brandShortName");
752
753      var errorStr = this.getString("load-js-data-url-error");
754      Services.prompt.alert(aWindow, brandShortName, errorStr);
755      return false;
756    }
757    return true;
758  },
759
760  /**
761   * Get the description associated with a document, as specified in a <META>
762   * element.
763   * @param   doc
764   *          A DOM Document to get a description for
765   * @return A description string if a META element was discovered with a
766   *         "description" or "httpequiv" attribute, empty string otherwise.
767   */
768  getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) {
769    var metaElements = doc.getElementsByTagName("META");
770    for (var i = 0; i < metaElements.length; ++i) {
771      if (metaElements[i].name.toLowerCase() == "description" ||
772          metaElements[i].httpEquiv.toLowerCase() == "description") {
773        return metaElements[i].content;
774      }
775    }
776    return "";
777  },
778
779  /**
780   * Retrieve the description of an item
781   * @param aItemId
782   *        item identifier
783   * @return the description of the given item, or an empty string if it is
784   * not set.
785   */
786  getItemDescription: function PUIU_getItemDescription(aItemId) {
787    if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO))
788      return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO);
789    return "";
790  },
791
792  /**
793   * Check whether or not the given node represents a removable entry (either in
794   * history or in bookmarks).
795   *
796   * @param aNode
797   *        a node, except the root node of a query.
798   * @return true if the aNode represents a removable entry, false otherwise.
799   */
800  canUserRemove: function (aNode) {
801    let parentNode = aNode.parent;
802    if (!parentNode) {
803      // canUserRemove doesn't accept root nodes.
804      return false;
805    }
806
807    // If it's not a bookmark, we can remove it unless it's a child of a
808    // livemark.
809    if (aNode.itemId == -1) {
810      // Rather than executing a db query, checking the existence of the feedURI
811      // annotation, detect livemark children by the fact that they are the only
812      // direct non-bookmark children of bookmark folders.
813      return !PlacesUtils.nodeIsFolder(parentNode);
814    }
815
816    // Generally it's always possible to remove children of a query.
817    if (PlacesUtils.nodeIsQuery(parentNode))
818      return true;
819
820    // Otherwise it has to be a child of an editable folder.
821    return !this.isContentsReadOnly(parentNode);
822  },
823
824  /**
825   * DO NOT USE THIS API IN ADDONS. IT IS VERY LIKELY TO CHANGE WHEN THE SWITCH
826   * TO GUIDS IS COMPLETE (BUG 1071511).
827   *
828   * Check whether or not the given node or item-id points to a folder which
829   * should not be modified by the user (i.e. its children should be unremovable
830   * and unmovable, new children should be disallowed, etc).
831   * These semantics are not inherited, meaning that read-only folder may
832   * contain editable items (for instance, the places root is read-only, but all
833   * of its direct children aren't).
834   *
835   * You should only pass folder item ids or folder nodes for aNodeOrItemId.
836   * While this is only enforced for the node case (if an item id of a separator
837   * or a bookmark is passed, false is returned), it's considered the caller's
838   * job to ensure that it checks a folder.
839   * Also note that folder-shortcuts should only be passed as result nodes.
840   * Otherwise they are just treated as bookmarks (i.e. false is returned).
841   *
842   * @param aNodeOrItemId
843   *        any item id or result node.
844   * @throws if aNodeOrItemId is neither an item id nor a folder result node.
845   * @note livemark "folders" are considered read-only (but see bug 1072833).
846   * @return true if aItemId points to a read-only folder, false otherwise.
847   */
848  isContentsReadOnly: function (aNodeOrItemId) {
849    let itemId;
850    if (typeof(aNodeOrItemId) == "number") {
851      itemId = aNodeOrItemId;
852    }
853    else if (PlacesUtils.nodeIsFolder(aNodeOrItemId)) {
854      itemId = PlacesUtils.getConcreteItemId(aNodeOrItemId);
855    }
856    else {
857      throw new Error("invalid value for aNodeOrItemId");
858    }
859
860    if (itemId == PlacesUtils.placesRootId || IsLivemark(itemId))
861      return true;
862
863    // leftPaneFolderId, and as a result, allBookmarksFolderId, is a lazy getter
864    // performing at least a synchronous DB query (and on its very first call
865    // in a fresh profile, it also creates the entire structure).
866    // Therefore we don't want to this function, which is called very often by
867    // isCommandEnabled, to ever be the one that invokes it first, especially
868    // because isCommandEnabled may be called way before the left pane folder is
869    // even created (for example, if the user only uses the bookmarks menu or
870    // toolbar for managing bookmarks).  To do so, we avoid comparing to those
871    // special folder if the lazy getter is still in place.  This is safe merely
872    // because the only way to access the left pane contents goes through
873    // "resolving" the leftPaneFolderId getter.
874    if ("get" in Object.getOwnPropertyDescriptor(this, "leftPaneFolderId"))
875      return false;
876
877    return itemId == this.leftPaneFolderId ||
878           itemId == this.allBookmarksFolderId;
879  },
880
881  /**
882   * Gives the user a chance to cancel loading lots of tabs at once
883   */
884  confirmOpenInTabs(numTabsToOpen, aWindow) {
885    const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen";
886    var reallyOpen = true;
887
888    if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) {
889      if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
890        // default to true: if it were false, we wouldn't get this far
891        var warnOnOpen = { value: true };
892
893        var messageKey = "tabs.openWarningMultipleBranded";
894        var openKey = "tabs.openButtonMultiple";
895        const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
896        var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"].
897                             getService(Ci.nsIStringBundleService).
898                             createBundle(BRANDING_BUNDLE_URI).
899                             GetStringFromName("brandShortName");
900
901        var buttonPressed = Services.prompt.confirmEx(
902          aWindow,
903          this.getString("tabs.openWarningTitle"),
904          this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]),
905          (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
906            (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1),
907          this.getString(openKey), null, null,
908          this.getFormattedString("tabs.openWarningPromptMeBranded",
909                                  [brandShortName]),
910          warnOnOpen
911        );
912
913        reallyOpen = (buttonPressed == 0);
914        // don't set the pref unless they press OK and it's false
915        if (reallyOpen && !warnOnOpen.value)
916          Services.prefs.setBoolPref(WARN_ON_OPEN_PREF, false);
917      }
918    }
919
920    return reallyOpen;
921  },
922
923  /** aItemsToOpen needs to be an array of objects of the form:
924    * {uri: string, isBookmark: boolean}
925    */
926  _openTabset: function PUIU__openTabset(aItemsToOpen, aEvent, aWindow) {
927    if (!aItemsToOpen.length)
928      return;
929
930    // Prefer the caller window if it's a browser window, otherwise use
931    // the top browser window.
932    var browserWindow = null;
933    browserWindow =
934      aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ?
935      aWindow : this._getTopBrowserWin();
936
937    var urls = [];
938    let skipMarking = browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow);
939    for (let item of aItemsToOpen) {
940      urls.push(item.uri);
941      if (skipMarking) {
942        continue;
943      }
944
945      if (item.isBookmark)
946        this.markPageAsFollowedBookmark(item.uri);
947      else
948        this.markPageAsTyped(item.uri);
949    }
950
951    // whereToOpenLink doesn't return "window" when there's no browser window
952    // open (Bug 630255).
953    var where = browserWindow ?
954                browserWindow.whereToOpenLink(aEvent, false, true) : "window";
955    if (where == "window") {
956      // There is no browser window open, thus open a new one.
957      var uriList = PlacesUtils.toISupportsString(urls.join("|"));
958      var args = Cc["@mozilla.org/array;1"].
959                  createInstance(Ci.nsIMutableArray);
960      args.appendElement(uriList, /* weak =*/ false);
961      browserWindow = Services.ww.openWindow(aWindow,
962                                             "chrome://browser/content/browser.xul",
963                                             null, "chrome,dialog=no,all", args);
964      return;
965    }
966
967    var loadInBackground = where == "tabshifted" ? true : false;
968    // For consistency, we want all the bookmarks to open in new tabs, instead
969    // of having one of them replace the currently focused tab.  Hence we call
970    // loadTabs with aReplace set to false.
971    browserWindow.gBrowser.loadTabs(urls, loadInBackground, false);
972  },
973
974  openLiveMarkNodesInTabs:
975  function PUIU_openLiveMarkNodesInTabs(aNode, aEvent, aView) {
976    let window = aView.ownerWindow;
977
978    PlacesUtils.livemarks.getLivemark({id: aNode.itemId})
979      .then(aLivemark => {
980        let urlsToOpen = [];
981
982        let nodes = aLivemark.getNodesForContainer(aNode);
983        for (let node of nodes) {
984          urlsToOpen.push({uri: node.uri, isBookmark: false});
985        }
986
987        if (this.confirmOpenInTabs(urlsToOpen.length, window)) {
988          this._openTabset(urlsToOpen, aEvent, window);
989        }
990      }, Cu.reportError);
991  },
992
993  openContainerNodeInTabs:
994  function PUIU_openContainerInTabs(aNode, aEvent, aView) {
995    let window = aView.ownerWindow;
996
997    let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode);
998    if (this.confirmOpenInTabs(urlsToOpen.length, window)) {
999      this._openTabset(urlsToOpen, aEvent, window);
1000    }
1001  },
1002
1003  openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent, aView) {
1004    let window = aView.ownerWindow;
1005
1006    let urlsToOpen = [];
1007    for (var i=0; i < aNodes.length; i++) {
1008      // Skip over separators and folders.
1009      if (PlacesUtils.nodeIsURI(aNodes[i]))
1010        urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])});
1011    }
1012    this._openTabset(urlsToOpen, aEvent, window);
1013  },
1014
1015  /**
1016   * Loads the node's URL in the appropriate tab or window or as a web
1017   * panel given the user's preference specified by modifier keys tracked by a
1018   * DOM mouse/key event.
1019   * @param   aNode
1020   *          An uri result node.
1021   * @param   aEvent
1022   *          The DOM mouse/key event with modifier keys set that track the
1023   *          user's preferred destination window or tab.
1024   * @param   aView
1025   *          The controller associated with aNode.
1026   */
1027  openNodeWithEvent:
1028  function PUIU_openNodeWithEvent(aNode, aEvent, aView) {
1029    let window = aView.ownerWindow;
1030    this._openNodeIn(aNode, window.whereToOpenLink(aEvent, false, true), window);
1031  },
1032
1033  /**
1034   * Loads the node's URL in the appropriate tab or window or as a
1035   * web panel.
1036   * see also openUILinkIn
1037   */
1038  openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) {
1039    let window = aView.ownerWindow;
1040    this._openNodeIn(aNode, aWhere, window, aPrivate);
1041  },
1042
1043  _openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aWindow, aPrivate=false) {
1044    if (aNode && PlacesUtils.nodeIsURI(aNode) &&
1045        this.checkURLSecurity(aNode, aWindow)) {
1046      let isBookmark = PlacesUtils.nodeIsBookmark(aNode);
1047
1048      if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
1049        if (isBookmark)
1050          this.markPageAsFollowedBookmark(aNode.uri);
1051        else
1052          this.markPageAsTyped(aNode.uri);
1053      }
1054
1055      // Check whether the node is a bookmark which should be opened as
1056      // a web panel
1057      if (aWhere == "current" && isBookmark) {
1058        if (PlacesUtils.annotations
1059                       .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) {
1060          let browserWin = this._getTopBrowserWin();
1061          if (browserWin) {
1062            browserWin.openWebPanel(aNode.title, aNode.uri);
1063            return;
1064          }
1065        }
1066      }
1067
1068      aWindow.openUILinkIn(aNode.uri, aWhere, {
1069        allowPopups: aNode.uri.startsWith("javascript:"),
1070        inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground"),
1071        private: aPrivate,
1072      });
1073    }
1074  },
1075
1076  /**
1077   * Helper for guessing scheme from an url string.
1078   * Used to avoid nsIURI overhead in frequently called UI functions.
1079   *
1080   * @param aUrlString the url to guess the scheme from.
1081   *
1082   * @return guessed scheme for this url string.
1083   *
1084   * @note this is not supposed be perfect, so use it only for UI purposes.
1085   */
1086  guessUrlSchemeForUI: function PUIU_guessUrlSchemeForUI(aUrlString) {
1087    return aUrlString.substr(0, aUrlString.indexOf(":"));
1088  },
1089
1090  getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) {
1091    var title;
1092    if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) {
1093      // if node title is empty, try to set the label using host and filename
1094      // PlacesUtils._uri() will throw if aNode.uri is not a valid URI
1095      try {
1096        var uri = PlacesUtils._uri(aNode.uri);
1097        var host = uri.host;
1098        var fileName = uri.QueryInterface(Ci.nsIURL).fileName;
1099        // if fileName is empty, use path to distinguish labels
1100        if (aDoNotCutTitle) {
1101          title = host + uri.path;
1102        } else {
1103          title = host + (fileName ?
1104                           (host ? "/" + this.ellipsis + "/" : "") + fileName :
1105                           uri.path);
1106        }
1107      }
1108      catch (e) {
1109        // Use (no title) for non-standard URIs (data:, javascript:, ...)
1110        title = "";
1111      }
1112    }
1113    else
1114      title = aNode.title;
1115
1116    return title || this.getString("noTitle");
1117  },
1118
1119  get leftPaneQueries() {
1120    // build the map
1121    this.leftPaneFolderId;
1122    return this.leftPaneQueries;
1123  },
1124
1125  // Get the folder id for the organizer left-pane folder.
1126  get leftPaneFolderId() {
1127    let leftPaneRoot = -1;
1128    let allBookmarksId;
1129
1130    // Shortcuts to services.
1131    let bs = PlacesUtils.bookmarks;
1132    let as = PlacesUtils.annotations;
1133
1134    // This is the list of the left pane queries.
1135    let queries = {
1136      "PlacesRoot": { title: "" },
1137      "History": { title: this.getString("OrganizerQueryHistory") },
1138      "Downloads": { title: this.getString("OrganizerQueryDownloads") },
1139      "Tags": { title: this.getString("OrganizerQueryTags") },
1140      "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") },
1141      "BookmarksToolbar":
1142        { title: null,
1143          concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"),
1144          concreteId: PlacesUtils.toolbarFolderId },
1145      "BookmarksMenu":
1146        { title: null,
1147          concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"),
1148          concreteId: PlacesUtils.bookmarksMenuFolderId },
1149      "UnfiledBookmarks":
1150        { title: null,
1151          concreteTitle: PlacesUtils.getString("OtherBookmarksFolderTitle"),
1152          concreteId: PlacesUtils.unfiledBookmarksFolderId },
1153    };
1154    // All queries but PlacesRoot.
1155    const EXPECTED_QUERY_COUNT = 7;
1156
1157    // Removes an item and associated annotations, ignoring eventual errors.
1158    function safeRemoveItem(aItemId) {
1159      try {
1160        if (as.itemHasAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) &&
1161            !(as.getItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) in queries)) {
1162          // Some extension annotated their roots with our query annotation,
1163          // so we should not delete them.
1164          return;
1165        }
1166        // removeItemAnnotation does not check if item exists, nor the anno,
1167        // so this is safe to do.
1168        as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
1169        as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO);
1170        // This will throw if the annotation is an orphan.
1171        bs.removeItem(aItemId);
1172      }
1173      catch (e) { /* orphan anno */ }
1174    }
1175
1176    // Returns true if item really exists, false otherwise.
1177    function itemExists(aItemId) {
1178      try {
1179        bs.getItemIndex(aItemId);
1180        return true;
1181      }
1182      catch (e) {
1183        return false;
1184      }
1185    }
1186
1187    // Get all items marked as being the left pane folder.
1188    let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO);
1189    if (items.length > 1) {
1190      // Something went wrong, we cannot have more than one left pane folder,
1191      // remove all left pane folders and continue.  We will create a new one.
1192      items.forEach(safeRemoveItem);
1193    }
1194    else if (items.length == 1 && items[0] != -1) {
1195      leftPaneRoot = items[0];
1196      // Check that organizer left pane root is valid.
1197      let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO);
1198      if (version != this.ORGANIZER_LEFTPANE_VERSION ||
1199          !itemExists(leftPaneRoot)) {
1200        // Invalid root, we must rebuild the left pane.
1201        safeRemoveItem(leftPaneRoot);
1202        leftPaneRoot = -1;
1203      }
1204    }
1205
1206    if (leftPaneRoot != -1) {
1207      // A valid left pane folder has been found.
1208      // Build the leftPaneQueries Map.  This is used to quickly access them,
1209      // associating a mnemonic name to the real item ids.
1210      delete this.leftPaneQueries;
1211      this.leftPaneQueries = {};
1212
1213      let items = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO);
1214      // While looping through queries we will also check for their validity.
1215      let queriesCount = 0;
1216      let corrupt = false;
1217      for (let i = 0; i < items.length; i++) {
1218        let queryName = as.getItemAnnotation(items[i], this.ORGANIZER_QUERY_ANNO);
1219
1220        // Some extension did use our annotation to decorate their items
1221        // with icons, so we should check only our elements, to avoid dataloss.
1222        if (!(queryName in queries))
1223          continue;
1224
1225        let query = queries[queryName];
1226        query.itemId = items[i];
1227
1228        if (!itemExists(query.itemId)) {
1229          // Orphan annotation, bail out and create a new left pane root.
1230          corrupt = true;
1231          break;
1232        }
1233
1234        // Check that all queries have valid parents.
1235        let parentId = bs.getFolderIdForItem(query.itemId);
1236        if (!items.includes(parentId) && parentId != leftPaneRoot) {
1237          // The parent is not part of the left pane, bail out and create a new
1238          // left pane root.
1239          corrupt = true;
1240          break;
1241        }
1242
1243        // Titles could have been corrupted or the user could have changed his
1244        // locale.  Check title and eventually fix it.
1245        if (bs.getItemTitle(query.itemId) != query.title)
1246          bs.setItemTitle(query.itemId, query.title);
1247        if ("concreteId" in query) {
1248          if (bs.getItemTitle(query.concreteId) != query.concreteTitle)
1249            bs.setItemTitle(query.concreteId, query.concreteTitle);
1250        }
1251
1252        // Add the query to our cache.
1253        this.leftPaneQueries[queryName] = query.itemId;
1254        queriesCount++;
1255      }
1256
1257      // Note: it's not enough to just check for queriesCount, since we may
1258      // find an invalid query just after accounting for a sufficient number of
1259      // valid ones.  As well as we can't just rely on corrupt since we may find
1260      // less valid queries than expected.
1261      if (corrupt || queriesCount != EXPECTED_QUERY_COUNT) {
1262        // Queries number is wrong, so the left pane must be corrupt.
1263        // Note: we can't just remove the leftPaneRoot, because some query could
1264        // have a bad parent, so we have to remove all items one by one.
1265        items.forEach(safeRemoveItem);
1266        safeRemoveItem(leftPaneRoot);
1267      }
1268      else {
1269        // Everything is fine, return the current left pane folder.
1270        delete this.leftPaneFolderId;
1271        return this.leftPaneFolderId = leftPaneRoot;
1272      }
1273    }
1274
1275    // Create a new left pane folder.
1276    var callback = {
1277      // Helper to create an organizer special query.
1278      create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) {
1279        let itemId = bs.insertBookmark(aParentId,
1280                                       PlacesUtils._uri(aQueryUrl),
1281                                       bs.DEFAULT_INDEX,
1282                                       queries[aQueryName].title);
1283        // Mark as special organizer query.
1284        as.setItemAnnotation(itemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aQueryName,
1285                             0, as.EXPIRE_NEVER);
1286        // We should never backup this, since it changes between profiles.
1287        as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
1288                             0, as.EXPIRE_NEVER);
1289        // Add to the queries map.
1290        PlacesUIUtils.leftPaneQueries[aQueryName] = itemId;
1291        return itemId;
1292      },
1293
1294      // Helper to create an organizer special folder.
1295      create_folder: function CB_create_folder(aFolderName, aParentId, aIsRoot) {
1296              // Left Pane Root Folder.
1297        let folderId = bs.createFolder(aParentId,
1298                                       queries[aFolderName].title,
1299                                       bs.DEFAULT_INDEX);
1300        // We should never backup this, since it changes between profiles.
1301        as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
1302                             0, as.EXPIRE_NEVER);
1303
1304        if (aIsRoot) {
1305          // Mark as special left pane root.
1306          as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
1307                               PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
1308                               0, as.EXPIRE_NEVER);
1309        }
1310        else {
1311          // Mark as special organizer folder.
1312          as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName,
1313                           0, as.EXPIRE_NEVER);
1314          PlacesUIUtils.leftPaneQueries[aFolderName] = folderId;
1315        }
1316        return folderId;
1317      },
1318
1319      runBatched: function CB_runBatched(aUserData) {
1320        delete PlacesUIUtils.leftPaneQueries;
1321        PlacesUIUtils.leftPaneQueries = { };
1322
1323        // Left Pane Root Folder.
1324        leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true);
1325
1326        // History Query.
1327        this.create_query("History", leftPaneRoot,
1328                          "place:type=" +
1329                          Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY +
1330                          "&sort=" +
1331                          Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
1332
1333        // Downloads.
1334        this.create_query("Downloads", leftPaneRoot,
1335                          "place:transition=" +
1336                          Ci.nsINavHistoryService.TRANSITION_DOWNLOAD +
1337                          "&sort=" +
1338                          Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
1339
1340        // Tags Query.
1341        this.create_query("Tags", leftPaneRoot,
1342                          "place:type=" +
1343                          Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY +
1344                          "&sort=" +
1345                          Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING);
1346
1347        // All Bookmarks Folder.
1348        allBookmarksId = this.create_folder("AllBookmarks", leftPaneRoot, false);
1349
1350        // All Bookmarks->Bookmarks Toolbar Query.
1351        this.create_query("BookmarksToolbar", allBookmarksId,
1352                          "place:folder=TOOLBAR");
1353
1354        // All Bookmarks->Bookmarks Menu Query.
1355        this.create_query("BookmarksMenu", allBookmarksId,
1356                          "place:folder=BOOKMARKS_MENU");
1357
1358        // All Bookmarks->Unfiled Bookmarks Query.
1359        this.create_query("UnfiledBookmarks", allBookmarksId,
1360                          "place:folder=UNFILED_BOOKMARKS");
1361      }
1362    };
1363    bs.runInBatchMode(callback, null);
1364
1365    delete this.leftPaneFolderId;
1366    return this.leftPaneFolderId = leftPaneRoot;
1367  },
1368
1369  /**
1370   * Get the folder id for the organizer left-pane folder.
1371   */
1372  get allBookmarksFolderId() {
1373    // ensure the left-pane root is initialized;
1374    this.leftPaneFolderId;
1375    delete this.allBookmarksFolderId;
1376    return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"];
1377  },
1378
1379  /**
1380   * If an item is a left-pane query, returns the name of the query
1381   * or an empty string if not.
1382   *
1383   * @param aItemId id of a container
1384   * @return the name of the query, or empty string if not a left-pane query
1385   */
1386  getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) {
1387    var queryName = "";
1388    // If the let pane hasn't been built, use the annotation service
1389    // directly, to avoid building the left pane too early.
1390    if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) {
1391      try {
1392        queryName = PlacesUtils.annotations.
1393                                getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO);
1394      }
1395      catch (ex) {
1396        // doesn't have the annotation
1397        queryName = "";
1398      }
1399    }
1400    else {
1401      // If the left pane has already been built, use the name->id map
1402      // cached in PlacesUIUtils.
1403      for (let [name, id] of Object.entries(this.leftPaneQueries)) {
1404        if (aItemId == id)
1405          queryName = name;
1406      }
1407    }
1408    return queryName;
1409  },
1410
1411  shouldShowTabsFromOtherComputersMenuitem: function() {
1412    let weaveOK = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED &&
1413                  Weave.Svc.Prefs.get("firstSync", "") != "notReady";
1414    return weaveOK;
1415  },
1416
1417  /**
1418   * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A
1419   * FUTURE RELEASE.
1420   *
1421   * Checks if a place: href represents a folder shortcut.
1422   *
1423   * @param queryString
1424   *        the query string to check (a place: href)
1425   * @return whether or not queryString represents a folder shortcut.
1426   * @throws if queryString is malformed.
1427   */
1428  isFolderShortcutQueryString(queryString) {
1429    // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp.
1430
1431    let queriesParam = { }, optionsParam = { };
1432    PlacesUtils.history.queryStringToQueries(queryString,
1433                                             queriesParam,
1434                                             { },
1435                                             optionsParam);
1436    let queries = queries.value;
1437    if (queries.length == 0)
1438      throw new Error(`Invalid place: uri: ${queryString}`);
1439    return queries.length == 1 &&
1440           queries[0].folderCount == 1 &&
1441           !queries[0].hasBeginTime &&
1442           !queries[0].hasEndTime &&
1443           !queries[0].hasDomain &&
1444           !queries[0].hasURI &&
1445           !queries[0].hasSearchTerms &&
1446           !queries[0].tags.length == 0 &&
1447           optionsParam.value.maxResults == 0;
1448  },
1449
1450  /**
1451   * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT"S LIKELY TO BE REMOVED IN A
1452   * FUTURE RELEASE.
1453   *
1454   * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
1455   * Given a partial node-like object, having at least the itemId property set, this
1456   * method completes the rest of the properties necessary for initialising the edit
1457   * overlay with it.
1458   *
1459   * @param aNodeLike
1460   *        an object having at least the itemId nsINavHistoryResultNode property set,
1461   *        along with any other properties available.
1462   */
1463  completeNodeLikeObjectForItemId(aNodeLike) {
1464    if (this.useAsyncTransactions) {
1465      // When async-transactions are enabled, node-likes must have
1466      // bookmarkGuid set, and we cannot set it synchronously.
1467      throw new Error("completeNodeLikeObjectForItemId cannot be used when " +
1468                      "async transactions are enabled");
1469    }
1470    if (!("itemId" in aNodeLike))
1471      throw new Error("itemId missing in aNodeLike");
1472
1473    let itemId = aNodeLike.itemId;
1474    let defGetter = XPCOMUtils.defineLazyGetter.bind(XPCOMUtils, aNodeLike);
1475
1476    if (!("title" in aNodeLike))
1477      defGetter("title", () => PlacesUtils.bookmarks.getItemTitle(itemId));
1478
1479    if (!("uri" in aNodeLike)) {
1480      defGetter("uri", () => {
1481        let uri = null;
1482        try {
1483          uri = PlacesUtils.bookmarks.getBookmarkURI(itemId);
1484        }
1485        catch (ex) { }
1486        return uri ? uri.spec : "";
1487      });
1488    }
1489
1490    if (!("type" in aNodeLike)) {
1491      defGetter("type", () => {
1492        if (aNodeLike.uri.length > 0) {
1493          if (/^place:/.test(aNodeLike.uri)) {
1494            if (this.isFolderShortcutQueryString(aNodeLike.uri))
1495              return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
1496
1497            return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
1498          }
1499
1500          return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
1501        }
1502
1503        let itemType = PlacesUtils.bookmarks.getItemType(itemId);
1504        if (itemType == PlacesUtils.bookmarks.TYPE_FOLDER)
1505          return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
1506
1507        throw new Error("Unexpected item type");
1508      });
1509    }
1510  },
1511
1512  /**
1513   * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
1514   *
1515   * Given a bookmark object for either a url bookmark or a folder, returned by
1516   * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for
1517   * initialising the edit overlay with it.
1518   *
1519   * @param aFetchInfo
1520   *        a bookmark object returned by Bookmarks.fetch.
1521   * @return a node-like object suitable for initialising editBookmarkOverlay.
1522   * @throws if aFetchInfo is representing a separator.
1523   */
1524  promiseNodeLikeFromFetchInfo: Task.async(function* (aFetchInfo) {
1525    if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR)
1526      throw new Error("promiseNodeLike doesn't support separators");
1527
1528    return Object.freeze({
1529      itemId: yield PlacesUtils.promiseItemId(aFetchInfo.guid),
1530      bookmarkGuid: aFetchInfo.guid,
1531      title: aFetchInfo.title,
1532      uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "",
1533
1534      get type() {
1535        if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER)
1536          return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
1537
1538        if (this.uri.length == 0)
1539          throw new Error("Unexpected item type");
1540
1541        if (/^place:/.test(this.uri)) {
1542          if (this.isFolderShortcutQueryString(this.uri))
1543            return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
1544
1545          return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
1546        }
1547
1548        return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
1549      }
1550    });
1551  }),
1552
1553  /**
1554   * Shortcut for calling promiseNodeLikeFromFetchInfo on the result of
1555   * Bookmarks.fetch for the given guid/info object.
1556   *
1557   * @see promiseNodeLikeFromFetchInfo above and Bookmarks.fetch in Bookmarks.jsm.
1558   */
1559  fetchNodeLike: Task.async(function* (aGuidOrInfo) {
1560    let info = yield PlacesUtils.bookmarks.fetch(aGuidOrInfo);
1561    if (!info)
1562      return null;
1563    return (yield this.promiseNodeLikeFromFetchInfo(info));
1564  })
1565};
1566
1567
1568PlacesUIUtils.PLACES_FLAVORS = [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
1569                                PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
1570                                PlacesUtils.TYPE_X_MOZ_PLACE];
1571
1572PlacesUIUtils.URI_FLAVORS = [PlacesUtils.TYPE_X_MOZ_URL,
1573                             TAB_DROP_TYPE,
1574                             PlacesUtils.TYPE_UNICODE],
1575
1576PlacesUIUtils.SUPPORTED_FLAVORS = [...PlacesUIUtils.PLACES_FLAVORS,
1577                                   ...PlacesUIUtils.URI_FLAVORS];
1578
1579XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF",
1580                                   "@mozilla.org/rdf/rdf-service;1",
1581                                   "nsIRDFService");
1582
1583XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
1584  return Services.prefs.getComplexValue("intl.ellipsis",
1585                                        Ci.nsIPrefLocalizedString).data;
1586});
1587
1588XPCOMUtils.defineLazyGetter(PlacesUIUtils, "useAsyncTransactions", function() {
1589  try {
1590    return Services.prefs.getBoolPref("browser.places.useAsyncTransactions");
1591  }
1592  catch (ex) { }
1593  return false;
1594});
1595
1596XPCOMUtils.defineLazyServiceGetter(this, "URIFixup",
1597                                   "@mozilla.org/docshell/urifixup;1",
1598                                   "nsIURIFixup");
1599
1600XPCOMUtils.defineLazyGetter(this, "bundle", function() {
1601  const PLACES_STRING_BUNDLE_URI =
1602    "chrome://browser/locale/places/places.properties";
1603  return Cc["@mozilla.org/intl/stringbundle;1"].
1604         getService(Ci.nsIStringBundleService).
1605         createBundle(PLACES_STRING_BUNDLE_URI);
1606});
1607
1608/**
1609 * This is a compatibility shim for old PUIU.ptm users.
1610 *
1611 * If you're looking for transactions and writing new code using them, directly
1612 * use the transactions objects exported by the PlacesUtils.jsm module.
1613 *
1614 * This object will be removed once enough users are converted to the new API.
1615 */
1616XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() {
1617  // Ensure PlacesUtils is imported in scope.
1618  PlacesUtils;
1619
1620  return {
1621    aggregateTransactions: (aName, aTransactions) =>
1622      new PlacesAggregatedTransaction(aName, aTransactions),
1623
1624    createFolder: (aName, aContainer, aIndex, aAnnotations,
1625                   aChildItemsTransactions) =>
1626      new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations,
1627                                        aChildItemsTransactions),
1628
1629    createItem: (aURI, aContainer, aIndex, aTitle, aKeyword,
1630                 aAnnotations, aChildTransactions) =>
1631      new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle,
1632                                          aKeyword, aAnnotations,
1633                                          aChildTransactions),
1634
1635    createSeparator: (aContainer, aIndex) =>
1636      new PlacesCreateSeparatorTransaction(aContainer, aIndex),
1637
1638    createLivemark: (aFeedURI, aSiteURI, aName, aContainer, aIndex,
1639                     aAnnotations) =>
1640      new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer,
1641                                          aIndex, aAnnotations),
1642
1643    moveItem: (aItemId, aNewContainer, aNewIndex) =>
1644      new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex),
1645
1646    removeItem: (aItemId) =>
1647      new PlacesRemoveItemTransaction(aItemId),
1648
1649    editItemTitle: (aItemId, aNewTitle) =>
1650      new PlacesEditItemTitleTransaction(aItemId, aNewTitle),
1651
1652    editBookmarkURI: (aItemId, aNewURI) =>
1653      new PlacesEditBookmarkURITransaction(aItemId, aNewURI),
1654
1655    setItemAnnotation: (aItemId, aAnnotationObject) =>
1656      new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject),
1657
1658    setPageAnnotation: (aURI, aAnnotationObject) =>
1659      new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject),
1660
1661    editBookmarkKeyword: (aItemId, aNewKeyword) =>
1662      new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword),
1663
1664    editLivemarkSiteURI: (aLivemarkId, aSiteURI) =>
1665      new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI),
1666
1667    editLivemarkFeedURI: (aLivemarkId, aFeedURI) =>
1668      new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI),
1669
1670    editItemDateAdded: (aItemId, aNewDateAdded) =>
1671      new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded),
1672
1673    editItemLastModified: (aItemId, aNewLastModified) =>
1674      new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified),
1675
1676    sortFolderByName: (aFolderId) =>
1677      new PlacesSortFolderByNameTransaction(aFolderId),
1678
1679    tagURI: (aURI, aTags) =>
1680      new PlacesTagURITransaction(aURI, aTags),
1681
1682    untagURI: (aURI, aTags) =>
1683      new PlacesUntagURITransaction(aURI, aTags),
1684
1685    /**
1686     * Transaction for setting/unsetting Load-in-sidebar annotation.
1687     *
1688     * @param aBookmarkId
1689     *        id of the bookmark where to set Load-in-sidebar annotation.
1690     * @param aLoadInSidebar
1691     *        boolean value.
1692     * @return nsITransaction object.
1693     */
1694    setLoadInSidebar: function(aItemId, aLoadInSidebar)
1695    {
1696      let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
1697                      type: Ci.nsIAnnotationService.TYPE_INT32,
1698                      flags: 0,
1699                      value: aLoadInSidebar,
1700                      expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
1701      return new PlacesSetItemAnnotationTransaction(aItemId, annoObj);
1702    },
1703
1704   /**
1705    * Transaction for editing the description of a bookmark or a folder.
1706    *
1707    * @param aItemId
1708    *        id of the item to edit.
1709    * @param aDescription
1710    *        new description.
1711    * @return nsITransaction object.
1712    */
1713    editItemDescription: function(aItemId, aDescription)
1714    {
1715      let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO,
1716                      type: Ci.nsIAnnotationService.TYPE_STRING,
1717                      flags: 0,
1718                      value: aDescription,
1719                      expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
1720      return new PlacesSetItemAnnotationTransaction(aItemId, annoObj);
1721    },
1722
1723    // nsITransactionManager forwarders.
1724
1725    beginBatch: () =>
1726      PlacesUtils.transactionManager.beginBatch(null),
1727
1728    endBatch: () =>
1729      PlacesUtils.transactionManager.endBatch(false),
1730
1731    doTransaction: (txn) =>
1732      PlacesUtils.transactionManager.doTransaction(txn),
1733
1734    undoTransaction: () =>
1735      PlacesUtils.transactionManager.undoTransaction(),
1736
1737    redoTransaction: () =>
1738      PlacesUtils.transactionManager.redoTransaction(),
1739
1740    get numberOfUndoItems() {
1741      return PlacesUtils.transactionManager.numberOfUndoItems;
1742    },
1743    get numberOfRedoItems() {
1744      return PlacesUtils.transactionManager.numberOfRedoItems;
1745    },
1746    get maxTransactionCount() {
1747      return PlacesUtils.transactionManager.maxTransactionCount;
1748    },
1749    set maxTransactionCount(val) {
1750      PlacesUtils.transactionManager.maxTransactionCount = val;
1751    },
1752
1753    clear: () =>
1754      PlacesUtils.transactionManager.clear(),
1755
1756    peekUndoStack: () =>
1757      PlacesUtils.transactionManager.peekUndoStack(),
1758
1759    peekRedoStack: () =>
1760      PlacesUtils.transactionManager.peekRedoStack(),
1761
1762    getUndoStack: () =>
1763      PlacesUtils.transactionManager.getUndoStack(),
1764
1765    getRedoStack: () =>
1766      PlacesUtils.transactionManager.getRedoStack(),
1767
1768    AddListener: (aListener) =>
1769      PlacesUtils.transactionManager.AddListener(aListener),
1770
1771    RemoveListener: (aListener) =>
1772      PlacesUtils.transactionManager.RemoveListener(aListener)
1773  }
1774});
1775