browser/components/places/src/PlacesUIUtils.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:4f63bb8fdeaa
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 this.EXPORTED_SYMBOLS = ["PlacesUIUtils"];
7
8 var Ci = Components.interfaces;
9 var Cc = Components.classes;
10 var Cr = Components.results;
11 var Cu = Components.utils;
12
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
14 Cu.import("resource://gre/modules/Services.jsm");
15
16 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
17 "resource://gre/modules/PluralForm.jsm");
18
19 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
20 "resource://gre/modules/PrivateBrowsingUtils.jsm");
21
22 #ifdef MOZ_SERVICES_SYNC
23 XPCOMUtils.defineLazyModuleGetter(this, "Weave",
24 "resource://services-sync/main.js");
25 #endif
26
27 XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() {
28 Cu.import("resource://gre/modules/PlacesUtils.jsm");
29 return PlacesUtils;
30 });
31
32 this.PlacesUIUtils = {
33 ORGANIZER_LEFTPANE_VERSION: 7,
34 ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder",
35 ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery",
36
37 LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
38 DESCRIPTION_ANNO: "bookmarkProperties/description",
39
40 TYPE_TAB_DROP: "application/x-moz-tabbrowser-tab",
41
42 /**
43 * Makes a URI from a spec, and do fixup
44 * @param aSpec
45 * The string spec of the URI
46 * @returns A URI object for the spec.
47 */
48 createFixedURI: function PUIU_createFixedURI(aSpec) {
49 return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
50 },
51
52 getFormattedString: function PUIU_getFormattedString(key, params) {
53 return bundle.formatStringFromName(key, params, params.length);
54 },
55
56 /**
57 * Get a localized plural string for the specified key name and numeric value
58 * substituting parameters.
59 *
60 * @param aKey
61 * String, key for looking up the localized string in the bundle
62 * @param aNumber
63 * Number based on which the final localized form is looked up
64 * @param aParams
65 * Array whose items will substitute #1, #2,... #n parameters
66 * in the string.
67 *
68 * @see https://developer.mozilla.org/en/Localization_and_Plurals
69 * @return The localized plural string.
70 */
71 getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) {
72 let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey));
73
74 // Replace #1 with aParams[0], #2 with aParams[1], and so on.
75 return str.replace(/\#(\d+)/g, function (matchedId, matchedNumber) {
76 let param = aParams[parseInt(matchedNumber, 10) - 1];
77 return param !== undefined ? param : matchedId;
78 });
79 },
80
81 getString: function PUIU_getString(key) {
82 return bundle.GetStringFromName(key);
83 },
84
85 get _copyableAnnotations() [
86 this.DESCRIPTION_ANNO,
87 this.LOAD_IN_SIDEBAR_ANNO,
88 PlacesUtils.POST_DATA_ANNO,
89 PlacesUtils.READ_ONLY_ANNO,
90 ],
91
92 /**
93 * Get a transaction for copying a uri item (either a bookmark or a history
94 * entry) from one container to another.
95 *
96 * @param aData
97 * JSON object of dropped or pasted item properties
98 * @param aContainer
99 * The container being copied into
100 * @param aIndex
101 * The index within the container the item is copied to
102 * @return A nsITransaction object that performs the copy.
103 *
104 * @note Since a copy creates a completely new item, only some internal
105 * annotations are synced from the old one.
106 * @see this._copyableAnnotations for the list of copyable annotations.
107 */
108 _getURIItemCopyTransaction:
109 function PUIU__getURIItemCopyTransaction(aData, aContainer, aIndex)
110 {
111 let transactions = [];
112 if (aData.dateAdded) {
113 transactions.push(
114 new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
115 );
116 }
117 if (aData.lastModified) {
118 transactions.push(
119 new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
120 );
121 }
122
123 let keyword = aData.keyword || null;
124 let annos = [];
125 if (aData.annos) {
126 annos = aData.annos.filter(function (aAnno) {
127 return this._copyableAnnotations.indexOf(aAnno.name) != -1;
128 }, this);
129 }
130
131 return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri),
132 aContainer, aIndex, aData.title,
133 keyword, annos, transactions);
134 },
135
136 /**
137 * Gets a transaction for copying (recursively nesting to include children)
138 * a folder (or container) and its contents from one folder to another.
139 *
140 * @param aData
141 * Unwrapped dropped folder data - Obj containing folder and children
142 * @param aContainer
143 * The container we are copying into
144 * @param aIndex
145 * The index in the destination container to insert the new items
146 * @return A nsITransaction object that will perform the copy.
147 *
148 * @note Since a copy creates a completely new item, only some internal
149 * annotations are synced from the old one.
150 * @see this._copyableAnnotations for the list of copyable annotations.
151 */
152 _getFolderCopyTransaction:
153 function PUIU__getFolderCopyTransaction(aData, aContainer, aIndex)
154 {
155 function getChildItemsTransactions(aChildren)
156 {
157 let transactions = [];
158 let index = aIndex;
159 aChildren.forEach(function (node, i) {
160 // Make sure that items are given the correct index, this will be
161 // passed by the transaction manager to the backend for the insertion.
162 // Insertion behaves differently for DEFAULT_INDEX (append).
163 if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) {
164 index = i;
165 }
166
167 if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
168 if (node.livemark && node.annos) {
169 transactions.push(
170 PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index)
171 );
172 }
173 else {
174 transactions.push(
175 PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index)
176 );
177 }
178 }
179 else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
180 transactions.push(new PlacesCreateSeparatorTransaction(-1, index));
181 }
182 else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
183 transactions.push(
184 PlacesUIUtils._getURIItemCopyTransaction(node, -1, index)
185 );
186 }
187 else {
188 throw new Error("Unexpected item under a bookmarks folder");
189 }
190 });
191 return transactions;
192 }
193
194 if (aContainer == PlacesUtils.tagsFolderId) { // Copying a tag folder.
195 let transactions = [];
196 if (aData.children) {
197 aData.children.forEach(function(aChild) {
198 transactions.push(
199 new PlacesTagURITransaction(PlacesUtils._uri(aChild.uri),
200 [aData.title])
201 );
202 });
203 }
204 return new PlacesAggregatedTransaction("addTags", transactions);
205 }
206
207 if (aData.livemark && aData.annos) { // Copying a livemark.
208 return this._getLivemarkCopyTransaction(aData, aContainer, aIndex);
209 }
210
211 let transactions = getChildItemsTransactions(aData.children);
212 if (aData.dateAdded) {
213 transactions.push(
214 new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
215 );
216 }
217 if (aData.lastModified) {
218 transactions.push(
219 new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
220 );
221 }
222
223 let annos = [];
224 if (aData.annos) {
225 annos = aData.annos.filter(function (aAnno) {
226 return this._copyableAnnotations.indexOf(aAnno.name) != -1;
227 }, this);
228 }
229
230 return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex,
231 annos, transactions);
232 },
233
234 /**
235 * Gets a transaction for copying a live bookmark item from one container to
236 * another.
237 *
238 * @param aData
239 * Unwrapped live bookmarkmark data
240 * @param aContainer
241 * The container we are copying into
242 * @param aIndex
243 * The index in the destination container to insert the new items
244 * @return A nsITransaction object that will perform the copy.
245 *
246 * @note Since a copy creates a completely new item, only some internal
247 * annotations are synced from the old one.
248 * @see this._copyableAnnotations for the list of copyable annotations.
249 */
250 _getLivemarkCopyTransaction:
251 function PUIU__getLivemarkCopyTransaction(aData, aContainer, aIndex)
252 {
253 if (!aData.livemark || !aData.annos) {
254 throw new Error("node is not a livemark");
255 }
256
257 let feedURI, siteURI;
258 let annos = [];
259 if (aData.annos) {
260 annos = aData.annos.filter(function (aAnno) {
261 if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) {
262 feedURI = PlacesUtils._uri(aAnno.value);
263 }
264 else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) {
265 siteURI = PlacesUtils._uri(aAnno.value);
266 }
267 return this._copyableAnnotations.indexOf(aAnno.name) != -1
268 }, this);
269 }
270
271 return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title,
272 aContainer, aIndex, annos);
273 },
274
275 /**
276 * Constructs a Transaction for the drop or paste of a blob of data into
277 * a container.
278 * @param data
279 * The unwrapped data blob of dropped or pasted data.
280 * @param type
281 * The content type of the data
282 * @param container
283 * The container the data was dropped or pasted into
284 * @param index
285 * The index within the container the item was dropped or pasted at
286 * @param copy
287 * The drag action was copy, so don't move folders or links.
288 * @returns An object implementing nsITransaction that can perform
289 * the move/insert.
290 */
291 makeTransaction:
292 function PUIU_makeTransaction(data, type, container, index, copy)
293 {
294 switch (data.type) {
295 case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
296 if (copy) {
297 return this._getFolderCopyTransaction(data, container, index);
298 }
299
300 // Otherwise move the item.
301 return new PlacesMoveItemTransaction(data.id, container, index);
302 break;
303 case PlacesUtils.TYPE_X_MOZ_PLACE:
304 if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked.
305 return this._getURIItemCopyTransaction(data, container, index);
306 }
307
308 // Otherwise move the item.
309 return new PlacesMoveItemTransaction(data.id, container, index);
310 break;
311 case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
312 if (copy) {
313 // There is no data in a separator, so copying it just amounts to
314 // inserting a new separator.
315 return new PlacesCreateSeparatorTransaction(container, index);
316 }
317
318 // Otherwise move the item.
319 return new PlacesMoveItemTransaction(data.id, container, index);
320 break;
321 default:
322 if (type == PlacesUtils.TYPE_X_MOZ_URL ||
323 type == PlacesUtils.TYPE_UNICODE ||
324 type == this.TYPE_TAB_DROP) {
325 let title = type != PlacesUtils.TYPE_UNICODE ? data.title
326 : data.uri;
327 return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri),
328 container, index, title);
329 }
330 }
331 return null;
332 },
333
334 /**
335 * Shows the bookmark dialog corresponding to the specified info.
336 *
337 * @param aInfo
338 * Describes the item to be edited/added in the dialog.
339 * See documentation at the top of bookmarkProperties.js
340 * @param aWindow
341 * Owner window for the new dialog.
342 *
343 * @see documentation at the top of bookmarkProperties.js
344 * @return true if any transaction has been performed, false otherwise.
345 */
346 showBookmarkDialog:
347 function PUIU_showBookmarkDialog(aInfo, aParentWindow) {
348 // Preserve size attributes differently based on the fact the dialog has
349 // a folder picker or not. If the picker is visible, the dialog should
350 // be resizable since it may not show enough content for the folders
351 // hierarchy.
352 let hasFolderPicker = !("hiddenRows" in aInfo) ||
353 aInfo.hiddenRows.indexOf("folderPicker") == -1;
354 // Use a different chrome url, since this allows to persist different sizes,
355 // based on resizability of the dialog.
356 let dialogURL = hasFolderPicker ?
357 "chrome://browser/content/places/bookmarkProperties2.xul" :
358 "chrome://browser/content/places/bookmarkProperties.xul";
359
360 let features =
361 "centerscreen,chrome,modal,resizable=" + (hasFolderPicker ? "yes" : "no");
362
363 aParentWindow.openDialog(dialogURL, "", features, aInfo);
364 return ("performed" in aInfo && aInfo.performed);
365 },
366
367 _getTopBrowserWin: function PUIU__getTopBrowserWin() {
368 return Services.wm.getMostRecentWindow("navigator:browser");
369 },
370
371 /**
372 * Returns the closet ancestor places view for the given DOM node
373 * @param aNode
374 * a DOM node
375 * @return the closet ancestor places view if exists, null otherwsie.
376 */
377 getViewForNode: function PUIU_getViewForNode(aNode) {
378 let node = aNode;
379
380 // The view for a <menu> of which its associated menupopup is a places
381 // view, is the menupopup.
382 if (node.localName == "menu" && !node._placesNode &&
383 node.lastChild._placesView)
384 return node.lastChild._placesView;
385
386 while (node instanceof Ci.nsIDOMElement) {
387 if (node._placesView)
388 return node._placesView;
389 if (node.localName == "tree" && node.getAttribute("type") == "places")
390 return node;
391
392 node = node.parentNode;
393 }
394
395 return null;
396 },
397
398 /**
399 * By calling this before visiting an URL, the visit will be associated to a
400 * TRANSITION_TYPED transition (if there is no a referrer).
401 * This is used when visiting pages from the history menu, history sidebar,
402 * url bar, url autocomplete results, and history searches from the places
403 * organizer. If this is not called visits will be marked as
404 * TRANSITION_LINK.
405 */
406 markPageAsTyped: function PUIU_markPageAsTyped(aURL) {
407 PlacesUtils.history.markPageAsTyped(this.createFixedURI(aURL));
408 },
409
410 /**
411 * By calling this before visiting an URL, the visit will be associated to a
412 * TRANSITION_BOOKMARK transition.
413 * This is used when visiting pages from the bookmarks menu,
414 * personal toolbar, and bookmarks from within the places organizer.
415 * If this is not called visits will be marked as TRANSITION_LINK.
416 */
417 markPageAsFollowedBookmark: function PUIU_markPageAsFollowedBookmark(aURL) {
418 PlacesUtils.history.markPageAsFollowedBookmark(this.createFixedURI(aURL));
419 },
420
421 /**
422 * By calling this before visiting an URL, any visit in frames will be
423 * associated to a TRANSITION_FRAMED_LINK transition.
424 * This is actually used to distinguish user-initiated visits in frames
425 * so automatic visits can be correctly ignored.
426 */
427 markPageAsFollowedLink: function PUIU_markPageAsFollowedLink(aURL) {
428 PlacesUtils.history.markPageAsFollowedLink(this.createFixedURI(aURL));
429 },
430
431 /**
432 * Allows opening of javascript/data URI only if the given node is
433 * bookmarked (see bug 224521).
434 * @param aURINode
435 * a URI node
436 * @param aWindow
437 * a window on which a potential error alert is shown on.
438 * @return true if it's safe to open the node in the browser, false otherwise.
439 *
440 */
441 checkURLSecurity: function PUIU_checkURLSecurity(aURINode, aWindow) {
442 if (PlacesUtils.nodeIsBookmark(aURINode))
443 return true;
444
445 var uri = PlacesUtils._uri(aURINode.uri);
446 if (uri.schemeIs("javascript") || uri.schemeIs("data")) {
447 const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
448 var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"].
449 getService(Ci.nsIStringBundleService).
450 createBundle(BRANDING_BUNDLE_URI).
451 GetStringFromName("brandShortName");
452
453 var errorStr = this.getString("load-js-data-url-error");
454 Services.prompt.alert(aWindow, brandShortName, errorStr);
455 return false;
456 }
457 return true;
458 },
459
460 /**
461 * Get the description associated with a document, as specified in a <META>
462 * element.
463 * @param doc
464 * A DOM Document to get a description for
465 * @returns A description string if a META element was discovered with a
466 * "description" or "httpequiv" attribute, empty string otherwise.
467 */
468 getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) {
469 var metaElements = doc.getElementsByTagName("META");
470 for (var i = 0; i < metaElements.length; ++i) {
471 if (metaElements[i].name.toLowerCase() == "description" ||
472 metaElements[i].httpEquiv.toLowerCase() == "description") {
473 return metaElements[i].content;
474 }
475 }
476 return "";
477 },
478
479 /**
480 * Retrieve the description of an item
481 * @param aItemId
482 * item identifier
483 * @returns the description of the given item, or an empty string if it is
484 * not set.
485 */
486 getItemDescription: function PUIU_getItemDescription(aItemId) {
487 if (PlacesUtils.annotations.itemHasAnnotation(aItemId, this.DESCRIPTION_ANNO))
488 return PlacesUtils.annotations.getItemAnnotation(aItemId, this.DESCRIPTION_ANNO);
489 return "";
490 },
491
492 /**
493 * Gives the user a chance to cancel loading lots of tabs at once
494 */
495 _confirmOpenInTabs:
496 function PUIU__confirmOpenInTabs(numTabsToOpen, aWindow) {
497 const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen";
498 var reallyOpen = true;
499
500 if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) {
501 if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
502 // default to true: if it were false, we wouldn't get this far
503 var warnOnOpen = { value: true };
504
505 var messageKey = "tabs.openWarningMultipleBranded";
506 var openKey = "tabs.openButtonMultiple";
507 const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
508 var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"].
509 getService(Ci.nsIStringBundleService).
510 createBundle(BRANDING_BUNDLE_URI).
511 GetStringFromName("brandShortName");
512
513 var buttonPressed = Services.prompt.confirmEx(
514 aWindow,
515 this.getString("tabs.openWarningTitle"),
516 this.getFormattedString(messageKey, [numTabsToOpen, brandShortName]),
517 (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
518 (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1),
519 this.getString(openKey), null, null,
520 this.getFormattedString("tabs.openWarningPromptMeBranded",
521 [brandShortName]),
522 warnOnOpen
523 );
524
525 reallyOpen = (buttonPressed == 0);
526 // don't set the pref unless they press OK and it's false
527 if (reallyOpen && !warnOnOpen.value)
528 Services.prefs.setBoolPref(WARN_ON_OPEN_PREF, false);
529 }
530 }
531
532 return reallyOpen;
533 },
534
535 /** aItemsToOpen needs to be an array of objects of the form:
536 * {uri: string, isBookmark: boolean}
537 */
538 _openTabset: function PUIU__openTabset(aItemsToOpen, aEvent, aWindow) {
539 if (!aItemsToOpen.length)
540 return;
541
542 // Prefer the caller window if it's a browser window, otherwise use
543 // the top browser window.
544 var browserWindow = null;
545 browserWindow =
546 aWindow && aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser" ?
547 aWindow : this._getTopBrowserWin();
548
549 var urls = [];
550 let skipMarking = browserWindow && PrivateBrowsingUtils.isWindowPrivate(browserWindow);
551 for (let item of aItemsToOpen) {
552 urls.push(item.uri);
553 if (skipMarking) {
554 continue;
555 }
556
557 if (item.isBookmark)
558 this.markPageAsFollowedBookmark(item.uri);
559 else
560 this.markPageAsTyped(item.uri);
561 }
562
563 // whereToOpenLink doesn't return "window" when there's no browser window
564 // open (Bug 630255).
565 var where = browserWindow ?
566 browserWindow.whereToOpenLink(aEvent, false, true) : "window";
567 if (where == "window") {
568 // There is no browser window open, thus open a new one.
569 var uriList = PlacesUtils.toISupportsString(urls.join("|"));
570 var args = Cc["@mozilla.org/supports-array;1"].
571 createInstance(Ci.nsISupportsArray);
572 args.AppendElement(uriList);
573 browserWindow = Services.ww.openWindow(aWindow,
574 "chrome://browser/content/browser.xul",
575 null, "chrome,dialog=no,all", args);
576 return;
577 }
578
579 var loadInBackground = where == "tabshifted" ? true : false;
580 // For consistency, we want all the bookmarks to open in new tabs, instead
581 // of having one of them replace the currently focused tab. Hence we call
582 // loadTabs with aReplace set to false.
583 browserWindow.gBrowser.loadTabs(urls, loadInBackground, false);
584 },
585
586 openContainerNodeInTabs:
587 function PUIU_openContainerInTabs(aNode, aEvent, aView) {
588 let window = aView.ownerWindow;
589
590 let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode);
591 if (!this._confirmOpenInTabs(urlsToOpen.length, window))
592 return;
593
594 this._openTabset(urlsToOpen, aEvent, window);
595 },
596
597 openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent, aView) {
598 let window = aView.ownerWindow;
599
600 let urlsToOpen = [];
601 for (var i=0; i < aNodes.length; i++) {
602 // Skip over separators and folders.
603 if (PlacesUtils.nodeIsURI(aNodes[i]))
604 urlsToOpen.push({uri: aNodes[i].uri, isBookmark: PlacesUtils.nodeIsBookmark(aNodes[i])});
605 }
606 this._openTabset(urlsToOpen, aEvent, window);
607 },
608
609 /**
610 * Loads the node's URL in the appropriate tab or window or as a web
611 * panel given the user's preference specified by modifier keys tracked by a
612 * DOM mouse/key event.
613 * @param aNode
614 * An uri result node.
615 * @param aEvent
616 * The DOM mouse/key event with modifier keys set that track the
617 * user's preferred destination window or tab.
618 * @param aView
619 * The controller associated with aNode.
620 */
621 openNodeWithEvent:
622 function PUIU_openNodeWithEvent(aNode, aEvent, aView) {
623 let window = aView.ownerWindow;
624 this._openNodeIn(aNode, window.whereToOpenLink(aEvent, false, true), window);
625 },
626
627 /**
628 * Loads the node's URL in the appropriate tab or window or as a
629 * web panel.
630 * see also openUILinkIn
631 */
632 openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView) {
633 let window = aView.ownerWindow;
634 this._openNodeIn(aNode, aWhere, window);
635 },
636
637 _openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aWindow) {
638 if (aNode && PlacesUtils.nodeIsURI(aNode) &&
639 this.checkURLSecurity(aNode, aWindow)) {
640 let isBookmark = PlacesUtils.nodeIsBookmark(aNode);
641
642 if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
643 if (isBookmark)
644 this.markPageAsFollowedBookmark(aNode.uri);
645 else
646 this.markPageAsTyped(aNode.uri);
647 }
648
649 // Check whether the node is a bookmark which should be opened as
650 // a web panel
651 if (aWhere == "current" && isBookmark) {
652 if (PlacesUtils.annotations
653 .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) {
654 let browserWin = this._getTopBrowserWin();
655 if (browserWin) {
656 browserWin.openWebPanel(aNode.title, aNode.uri);
657 return;
658 }
659 }
660 }
661 aWindow.openUILinkIn(aNode.uri, aWhere, {
662 inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground")
663 });
664 }
665 },
666
667 /**
668 * Helper for guessing scheme from an url string.
669 * Used to avoid nsIURI overhead in frequently called UI functions.
670 *
671 * @param aUrlString the url to guess the scheme from.
672 *
673 * @return guessed scheme for this url string.
674 *
675 * @note this is not supposed be perfect, so use it only for UI purposes.
676 */
677 guessUrlSchemeForUI: function PUIU_guessUrlSchemeForUI(aUrlString) {
678 return aUrlString.substr(0, aUrlString.indexOf(":"));
679 },
680
681 getBestTitle: function PUIU_getBestTitle(aNode, aDoNotCutTitle) {
682 var title;
683 if (!aNode.title && PlacesUtils.nodeIsURI(aNode)) {
684 // if node title is empty, try to set the label using host and filename
685 // PlacesUtils._uri() will throw if aNode.uri is not a valid URI
686 try {
687 var uri = PlacesUtils._uri(aNode.uri);
688 var host = uri.host;
689 var fileName = uri.QueryInterface(Ci.nsIURL).fileName;
690 // if fileName is empty, use path to distinguish labels
691 if (aDoNotCutTitle) {
692 title = host + uri.path;
693 } else {
694 title = host + (fileName ?
695 (host ? "/" + this.ellipsis + "/" : "") + fileName :
696 uri.path);
697 }
698 }
699 catch (e) {
700 // Use (no title) for non-standard URIs (data:, javascript:, ...)
701 title = "";
702 }
703 }
704 else
705 title = aNode.title;
706
707 return title || this.getString("noTitle");
708 },
709
710 get leftPaneQueries() {
711 // build the map
712 this.leftPaneFolderId;
713 return this.leftPaneQueries;
714 },
715
716 // Get the folder id for the organizer left-pane folder.
717 get leftPaneFolderId() {
718 let leftPaneRoot = -1;
719 let allBookmarksId;
720
721 // Shortcuts to services.
722 let bs = PlacesUtils.bookmarks;
723 let as = PlacesUtils.annotations;
724
725 // This is the list of the left pane queries.
726 let queries = {
727 "PlacesRoot": { title: "" },
728 "History": { title: this.getString("OrganizerQueryHistory") },
729 "Downloads": { title: this.getString("OrganizerQueryDownloads") },
730 "Tags": { title: this.getString("OrganizerQueryTags") },
731 "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") },
732 "BookmarksToolbar":
733 { title: null,
734 concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"),
735 concreteId: PlacesUtils.toolbarFolderId },
736 "BookmarksMenu":
737 { title: null,
738 concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"),
739 concreteId: PlacesUtils.bookmarksMenuFolderId },
740 "UnfiledBookmarks":
741 { title: null,
742 concreteTitle: PlacesUtils.getString("UnsortedBookmarksFolderTitle"),
743 concreteId: PlacesUtils.unfiledBookmarksFolderId },
744 };
745 // All queries but PlacesRoot.
746 const EXPECTED_QUERY_COUNT = 7;
747
748 // Removes an item and associated annotations, ignoring eventual errors.
749 function safeRemoveItem(aItemId) {
750 try {
751 if (as.itemHasAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) &&
752 !(as.getItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO) in queries)) {
753 // Some extension annotated their roots with our query annotation,
754 // so we should not delete them.
755 return;
756 }
757 // removeItemAnnotation does not check if item exists, nor the anno,
758 // so this is safe to do.
759 as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO);
760 as.removeItemAnnotation(aItemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO);
761 // This will throw if the annotation is an orphan.
762 bs.removeItem(aItemId);
763 }
764 catch(e) { /* orphan anno */ }
765 }
766
767 // Returns true if item really exists, false otherwise.
768 function itemExists(aItemId) {
769 try {
770 bs.getItemIndex(aItemId);
771 return true;
772 }
773 catch(e) {
774 return false;
775 }
776 }
777
778 // Get all items marked as being the left pane folder.
779 let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO);
780 if (items.length > 1) {
781 // Something went wrong, we cannot have more than one left pane folder,
782 // remove all left pane folders and continue. We will create a new one.
783 items.forEach(safeRemoveItem);
784 }
785 else if (items.length == 1 && items[0] != -1) {
786 leftPaneRoot = items[0];
787 // Check that organizer left pane root is valid.
788 let version = as.getItemAnnotation(leftPaneRoot, this.ORGANIZER_FOLDER_ANNO);
789 if (version != this.ORGANIZER_LEFTPANE_VERSION ||
790 !itemExists(leftPaneRoot)) {
791 // Invalid root, we must rebuild the left pane.
792 safeRemoveItem(leftPaneRoot);
793 leftPaneRoot = -1;
794 }
795 }
796
797 if (leftPaneRoot != -1) {
798 // A valid left pane folder has been found.
799 // Build the leftPaneQueries Map. This is used to quickly access them,
800 // associating a mnemonic name to the real item ids.
801 delete this.leftPaneQueries;
802 this.leftPaneQueries = {};
803
804 let items = as.getItemsWithAnnotation(this.ORGANIZER_QUERY_ANNO);
805 // While looping through queries we will also check for their validity.
806 let queriesCount = 0;
807 let corrupt = false;
808 for (let i = 0; i < items.length; i++) {
809 let queryName = as.getItemAnnotation(items[i], this.ORGANIZER_QUERY_ANNO);
810
811 // Some extension did use our annotation to decorate their items
812 // with icons, so we should check only our elements, to avoid dataloss.
813 if (!(queryName in queries))
814 continue;
815
816 let query = queries[queryName];
817 query.itemId = items[i];
818
819 if (!itemExists(query.itemId)) {
820 // Orphan annotation, bail out and create a new left pane root.
821 corrupt = true;
822 break;
823 }
824
825 // Check that all queries have valid parents.
826 let parentId = bs.getFolderIdForItem(query.itemId);
827 if (items.indexOf(parentId) == -1 && parentId != leftPaneRoot) {
828 // The parent is not part of the left pane, bail out and create a new
829 // left pane root.
830 corrupt = true;
831 break;
832 }
833
834 // Titles could have been corrupted or the user could have changed his
835 // locale. Check title and eventually fix it.
836 if (bs.getItemTitle(query.itemId) != query.title)
837 bs.setItemTitle(query.itemId, query.title);
838 if ("concreteId" in query) {
839 if (bs.getItemTitle(query.concreteId) != query.concreteTitle)
840 bs.setItemTitle(query.concreteId, query.concreteTitle);
841 }
842
843 // Add the query to our cache.
844 this.leftPaneQueries[queryName] = query.itemId;
845 queriesCount++;
846 }
847
848 // Note: it's not enough to just check for queriesCount, since we may
849 // find an invalid query just after accounting for a sufficient number of
850 // valid ones. As well as we can't just rely on corrupt since we may find
851 // less valid queries than expected.
852 if (corrupt || queriesCount != EXPECTED_QUERY_COUNT) {
853 // Queries number is wrong, so the left pane must be corrupt.
854 // Note: we can't just remove the leftPaneRoot, because some query could
855 // have a bad parent, so we have to remove all items one by one.
856 items.forEach(safeRemoveItem);
857 safeRemoveItem(leftPaneRoot);
858 }
859 else {
860 // Everything is fine, return the current left pane folder.
861 delete this.leftPaneFolderId;
862 return this.leftPaneFolderId = leftPaneRoot;
863 }
864 }
865
866 // Create a new left pane folder.
867 var callback = {
868 // Helper to create an organizer special query.
869 create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) {
870 let itemId = bs.insertBookmark(aParentId,
871 PlacesUtils._uri(aQueryUrl),
872 bs.DEFAULT_INDEX,
873 queries[aQueryName].title);
874 // Mark as special organizer query.
875 as.setItemAnnotation(itemId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aQueryName,
876 0, as.EXPIRE_NEVER);
877 // We should never backup this, since it changes between profiles.
878 as.setItemAnnotation(itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
879 0, as.EXPIRE_NEVER);
880 // Add to the queries map.
881 PlacesUIUtils.leftPaneQueries[aQueryName] = itemId;
882 return itemId;
883 },
884
885 // Helper to create an organizer special folder.
886 create_folder: function CB_create_folder(aFolderName, aParentId, aIsRoot) {
887 // Left Pane Root Folder.
888 let folderId = bs.createFolder(aParentId,
889 queries[aFolderName].title,
890 bs.DEFAULT_INDEX);
891 // We should never backup this, since it changes between profiles.
892 as.setItemAnnotation(folderId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, 1,
893 0, as.EXPIRE_NEVER);
894 // Disallow manipulating this folder within the organizer UI.
895 bs.setFolderReadonly(folderId, true);
896
897 if (aIsRoot) {
898 // Mark as special left pane root.
899 as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
900 PlacesUIUtils.ORGANIZER_LEFTPANE_VERSION,
901 0, as.EXPIRE_NEVER);
902 }
903 else {
904 // Mark as special organizer folder.
905 as.setItemAnnotation(folderId, PlacesUIUtils.ORGANIZER_QUERY_ANNO, aFolderName,
906 0, as.EXPIRE_NEVER);
907 PlacesUIUtils.leftPaneQueries[aFolderName] = folderId;
908 }
909 return folderId;
910 },
911
912 runBatched: function CB_runBatched(aUserData) {
913 delete PlacesUIUtils.leftPaneQueries;
914 PlacesUIUtils.leftPaneQueries = { };
915
916 // Left Pane Root Folder.
917 leftPaneRoot = this.create_folder("PlacesRoot", bs.placesRoot, true);
918
919 // History Query.
920 this.create_query("History", leftPaneRoot,
921 "place:type=" +
922 Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY +
923 "&sort=" +
924 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
925
926 // Downloads.
927 this.create_query("Downloads", leftPaneRoot,
928 "place:transition=" +
929 Ci.nsINavHistoryService.TRANSITION_DOWNLOAD +
930 "&sort=" +
931 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
932
933 // Tags Query.
934 this.create_query("Tags", leftPaneRoot,
935 "place:type=" +
936 Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY +
937 "&sort=" +
938 Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING);
939
940 // All Bookmarks Folder.
941 allBookmarksId = this.create_folder("AllBookmarks", leftPaneRoot, false);
942
943 // All Bookmarks->Bookmarks Toolbar Query.
944 this.create_query("BookmarksToolbar", allBookmarksId,
945 "place:folder=TOOLBAR");
946
947 // All Bookmarks->Bookmarks Menu Query.
948 this.create_query("BookmarksMenu", allBookmarksId,
949 "place:folder=BOOKMARKS_MENU");
950
951 // All Bookmarks->Unfiled Bookmarks Query.
952 this.create_query("UnfiledBookmarks", allBookmarksId,
953 "place:folder=UNFILED_BOOKMARKS");
954 }
955 };
956 bs.runInBatchMode(callback, null);
957
958 delete this.leftPaneFolderId;
959 return this.leftPaneFolderId = leftPaneRoot;
960 },
961
962 /**
963 * Get the folder id for the organizer left-pane folder.
964 */
965 get allBookmarksFolderId() {
966 // ensure the left-pane root is initialized;
967 this.leftPaneFolderId;
968 delete this.allBookmarksFolderId;
969 return this.allBookmarksFolderId = this.leftPaneQueries["AllBookmarks"];
970 },
971
972 /**
973 * If an item is a left-pane query, returns the name of the query
974 * or an empty string if not.
975 *
976 * @param aItemId id of a container
977 * @returns the name of the query, or empty string if not a left-pane query
978 */
979 getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) {
980 var queryName = "";
981 // If the let pane hasn't been built, use the annotation service
982 // directly, to avoid building the left pane too early.
983 if (Object.getOwnPropertyDescriptor(this, "leftPaneFolderId").value === undefined) {
984 try {
985 queryName = PlacesUtils.annotations.
986 getItemAnnotation(aItemId, this.ORGANIZER_QUERY_ANNO);
987 }
988 catch (ex) {
989 // doesn't have the annotation
990 queryName = "";
991 }
992 }
993 else {
994 // If the left pane has already been built, use the name->id map
995 // cached in PlacesUIUtils.
996 for (let [name, id] in Iterator(this.leftPaneQueries)) {
997 if (aItemId == id)
998 queryName = name;
999 }
1000 }
1001 return queryName;
1002 },
1003
1004 shouldShowTabsFromOtherComputersMenuitem: function() {
1005 // If Sync isn't configured yet, then don't show the menuitem.
1006 return Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED &&
1007 Weave.Svc.Prefs.get("firstSync", "") != "notReady";
1008 },
1009
1010 shouldEnableTabsFromOtherComputersMenuitem: function() {
1011 // The tabs engine might never be inited (if services.sync.registerEngines
1012 // is modified), so make sure we avoid undefined errors.
1013 return Weave.Service.isLoggedIn &&
1014 Weave.Service.engineManager.get("tabs") &&
1015 Weave.Service.engineManager.get("tabs").enabled;
1016 },
1017 };
1018
1019 XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF",
1020 "@mozilla.org/rdf/rdf-service;1",
1021 "nsIRDFService");
1022
1023 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "localStore", function() {
1024 return PlacesUIUtils.RDF.GetDataSource("rdf:local-store");
1025 });
1026
1027 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
1028 return Services.prefs.getComplexValue("intl.ellipsis",
1029 Ci.nsIPrefLocalizedString).data;
1030 });
1031
1032 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "useAsyncTransactions", function() {
1033 try {
1034 return Services.prefs.getBoolPref("browser.places.useAsyncTransactions");
1035 }
1036 catch(ex) { }
1037 return false;
1038 });
1039
1040 XPCOMUtils.defineLazyServiceGetter(this, "URIFixup",
1041 "@mozilla.org/docshell/urifixup;1",
1042 "nsIURIFixup");
1043
1044 XPCOMUtils.defineLazyGetter(this, "bundle", function() {
1045 const PLACES_STRING_BUNDLE_URI =
1046 "chrome://browser/locale/places/places.properties";
1047 return Cc["@mozilla.org/intl/stringbundle;1"].
1048 getService(Ci.nsIStringBundleService).
1049 createBundle(PLACES_STRING_BUNDLE_URI);
1050 });
1051
1052 XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
1053 "@mozilla.org/focus-manager;1",
1054 "nsIFocusManager");
1055
1056 /**
1057 * This is a compatibility shim for old PUIU.ptm users.
1058 *
1059 * If you're looking for transactions and writing new code using them, directly
1060 * use the transactions objects exported by the PlacesUtils.jsm module.
1061 *
1062 * This object will be removed once enough users are converted to the new API.
1063 */
1064 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() {
1065 // Ensure PlacesUtils is imported in scope.
1066 PlacesUtils;
1067
1068 return {
1069 aggregateTransactions: function(aName, aTransactions)
1070 new PlacesAggregatedTransaction(aName, aTransactions),
1071
1072 createFolder: function(aName, aContainer, aIndex, aAnnotations,
1073 aChildItemsTransactions)
1074 new PlacesCreateFolderTransaction(aName, aContainer, aIndex, aAnnotations,
1075 aChildItemsTransactions),
1076
1077 createItem: function(aURI, aContainer, aIndex, aTitle, aKeyword,
1078 aAnnotations, aChildTransactions)
1079 new PlacesCreateBookmarkTransaction(aURI, aContainer, aIndex, aTitle,
1080 aKeyword, aAnnotations,
1081 aChildTransactions),
1082
1083 createSeparator: function(aContainer, aIndex)
1084 new PlacesCreateSeparatorTransaction(aContainer, aIndex),
1085
1086 createLivemark: function(aFeedURI, aSiteURI, aName, aContainer, aIndex,
1087 aAnnotations)
1088 new PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName, aContainer,
1089 aIndex, aAnnotations),
1090
1091 moveItem: function(aItemId, aNewContainer, aNewIndex)
1092 new PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex),
1093
1094 removeItem: function(aItemId)
1095 new PlacesRemoveItemTransaction(aItemId),
1096
1097 editItemTitle: function(aItemId, aNewTitle)
1098 new PlacesEditItemTitleTransaction(aItemId, aNewTitle),
1099
1100 editBookmarkURI: function(aItemId, aNewURI)
1101 new PlacesEditBookmarkURITransaction(aItemId, aNewURI),
1102
1103 setItemAnnotation: function(aItemId, aAnnotationObject)
1104 new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject),
1105
1106 setPageAnnotation: function(aURI, aAnnotationObject)
1107 new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject),
1108
1109 editBookmarkKeyword: function(aItemId, aNewKeyword)
1110 new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword),
1111
1112 editBookmarkPostData: function(aItemId, aPostData)
1113 new PlacesEditBookmarkPostDataTransaction(aItemId, aPostData),
1114
1115 editLivemarkSiteURI: function(aLivemarkId, aSiteURI)
1116 new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI),
1117
1118 editLivemarkFeedURI: function(aLivemarkId, aFeedURI)
1119 new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI),
1120
1121 editItemDateAdded: function(aItemId, aNewDateAdded)
1122 new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded),
1123
1124 editItemLastModified: function(aItemId, aNewLastModified)
1125 new PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified),
1126
1127 sortFolderByName: function(aFolderId)
1128 new PlacesSortFolderByNameTransaction(aFolderId),
1129
1130 tagURI: function(aURI, aTags)
1131 new PlacesTagURITransaction(aURI, aTags),
1132
1133 untagURI: function(aURI, aTags)
1134 new PlacesUntagURITransaction(aURI, aTags),
1135
1136 /**
1137 * Transaction for setting/unsetting Load-in-sidebar annotation.
1138 *
1139 * @param aBookmarkId
1140 * id of the bookmark where to set Load-in-sidebar annotation.
1141 * @param aLoadInSidebar
1142 * boolean value.
1143 * @returns nsITransaction object.
1144 */
1145 setLoadInSidebar: function(aItemId, aLoadInSidebar)
1146 {
1147 let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
1148 type: Ci.nsIAnnotationService.TYPE_INT32,
1149 flags: 0,
1150 value: aLoadInSidebar,
1151 expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
1152 return new PlacesSetItemAnnotationTransaction(aItemId, annoObj);
1153 },
1154
1155 /**
1156 * Transaction for editing the description of a bookmark or a folder.
1157 *
1158 * @param aItemId
1159 * id of the item to edit.
1160 * @param aDescription
1161 * new description.
1162 * @returns nsITransaction object.
1163 */
1164 editItemDescription: function(aItemId, aDescription)
1165 {
1166 let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO,
1167 type: Ci.nsIAnnotationService.TYPE_STRING,
1168 flags: 0,
1169 value: aDescription,
1170 expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
1171 return new PlacesSetItemAnnotationTransaction(aItemId, annoObj);
1172 },
1173
1174 ////////////////////////////////////////////////////////////////////////////
1175 //// nsITransactionManager forwarders.
1176
1177 beginBatch: function()
1178 PlacesUtils.transactionManager.beginBatch(null),
1179
1180 endBatch: function()
1181 PlacesUtils.transactionManager.endBatch(false),
1182
1183 doTransaction: function(txn)
1184 PlacesUtils.transactionManager.doTransaction(txn),
1185
1186 undoTransaction: function()
1187 PlacesUtils.transactionManager.undoTransaction(),
1188
1189 redoTransaction: function()
1190 PlacesUtils.transactionManager.redoTransaction(),
1191
1192 get numberOfUndoItems()
1193 PlacesUtils.transactionManager.numberOfUndoItems,
1194 get numberOfRedoItems()
1195 PlacesUtils.transactionManager.numberOfRedoItems,
1196 get maxTransactionCount()
1197 PlacesUtils.transactionManager.maxTransactionCount,
1198 set maxTransactionCount(val)
1199 PlacesUtils.transactionManager.maxTransactionCount = val,
1200
1201 clear: function()
1202 PlacesUtils.transactionManager.clear(),
1203
1204 peekUndoStack: function()
1205 PlacesUtils.transactionManager.peekUndoStack(),
1206
1207 peekRedoStack: function()
1208 PlacesUtils.transactionManager.peekRedoStack(),
1209
1210 getUndoStack: function()
1211 PlacesUtils.transactionManager.getUndoStack(),
1212
1213 getRedoStack: function()
1214 PlacesUtils.transactionManager.getRedoStack(),
1215
1216 AddListener: function(aListener)
1217 PlacesUtils.transactionManager.AddListener(aListener),
1218
1219 RemoveListener: function(aListener)
1220 PlacesUtils.transactionManager.RemoveListener(aListener)
1221 }
1222 });

mercurial