/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
this.EXPORTED_SYMBOLS = ["PlacesUIUtils"];
var Ci = Components.interfaces;
var Cc = Components.classes;
var Cr = Components.results;
var Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
// PlacesUtils exposes multiple symbols, so we can't use defineLazyModuleGetter.
Cu.import("resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
"resource://gre/modules/PlacesTransactions.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
"resource://gre/modules/CloudSync.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
"resource://services-sync/main.js");
const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
const FAVICON_REQUEST_TIMEOUT = 60 * 1000;
// Map from windows to arrays of data about pending favicon loads.
let gFaviconLoadDataMap = new Map();
// copied from utilityOverlay.js
const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
// This function isn't public both because it's synchronous and because it is
// going to be removed in bug 1072833.
function IsLivemark(aItemId) {
// Since this check may be done on each dragover event, it's worth maintaining
// a cache.
let self = IsLivemark;
if (!("ids" in self)) {
const LIVEMARK_ANNO = PlacesUtils.LMANNO_FEEDURI;
let idsVec = PlacesUtils.annotations.getItemsWithAnnotation(LIVEMARK_ANNO);
self.ids = new Set(idsVec);
let obs = Object.freeze({
QueryInterface: XPCOMUtils.generateQI(Ci.nsIAnnotationObserver),
onItemAnnotationSet(itemId, annoName) {
if (annoName == LIVEMARK_ANNO)
self.ids.add(itemId);
},
onItemAnnotationRemoved(itemId, annoName) {
// If annoName is set to an empty string, the item is gone.
if (annoName == LIVEMARK_ANNO || annoName == "")
self.ids.delete(itemId);
},
onPageAnnotationSet() { },
onPageAnnotationRemoved() { },
});
PlacesUtils.annotations.addObserver(obs);
PlacesUtils.registerShutdownFunction(() => {
PlacesUtils.annotations.removeObserver(obs);
});
}
return self.ids.has(aItemId);
}
let InternalFaviconLoader = {
/**
* This gets called for every inner window that is destroyed.
* In the parent process, we process the destruction ourselves. In the child process,
* we notify the parent which will then process it based on that message.
*/
observe(subject, topic, data) {
let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
this.removeRequestsForInner(innerWindowID);
},
/**
* Actually cancel the request, and clear the timeout for cancelling it.
*/
_cancelRequest({uri, innerWindowID, timerID, callback}, reason) {
// Break cycle
let request = callback.request;
delete callback.request;
// Ensure we don't time out.
clearTimeout(timerID);
try {
request.cancel();
} catch (ex) {
Cu.reportError("When cancelling a request for " + uri.spec + " because " + reason + ", it was already canceled!");
}
},
/**
* Called for every inner that gets destroyed, only in the parent process.
*/
removeRequestsForInner(innerID) {
for (let [window, loadDataForWindow] of gFaviconLoadDataMap) {
let newLoadDataForWindow = loadDataForWindow.filter(loadData => {
let innerWasDestroyed = loadData.innerWindowID == innerID;
if (innerWasDestroyed) {
this._cancelRequest(loadData, "the inner window was destroyed or a new favicon was loaded for it");
}
// Keep the items whose inner is still alive.
return !innerWasDestroyed;
});
// Map iteration with for...of is safe against modification, so
// now just replace the old value:
gFaviconLoadDataMap.set(window, newLoadDataForWindow);
}
},
/**
* Called when a toplevel chrome window unloads. We use this to tidy up after ourselves,
* avoid leaks, and cancel any remaining requests. The last part should in theory be
* handled by the inner-window-destroyed handlers. We clean up just to be on the safe side.
*/
onUnload(win) {
let loadDataForWindow = gFaviconLoadDataMap.get(win);
if (loadDataForWindow) {
for (let loadData of loadDataForWindow) {
this._cancelRequest(loadData, "the chrome window went away");
}
}
gFaviconLoadDataMap.delete(win);
},
/**
* Remove a particular favicon load's loading data from our map tracking
* load data per chrome window.
*
* @param win
* the chrome window in which we should look for this load
* @param filterData ({innerWindowID, uri, callback})
* the data we should use to find this particular load to remove.
*
* @return the loadData object we removed, or null if we didn't find any.
*/
_removeLoadDataFromWindowMap(win, {innerWindowID, uri, callback}) {
let loadDataForWindow = gFaviconLoadDataMap.get(win);
if (loadDataForWindow) {
let itemIndex = loadDataForWindow.findIndex(loadData => {
return loadData.innerWindowID == innerWindowID &&
loadData.uri.equals(uri) &&
loadData.callback.request == callback.request;
});
if (itemIndex != -1) {
let loadData = loadDataForWindow[itemIndex];
loadDataForWindow.splice(itemIndex, 1);
return loadData;
}
}
return null;
},
/**
* Create a function to use as a nsIFaviconDataCallback, so we can remove cancelling
* information when the request succeeds. Note that right now there are some edge-cases,
* such as about: URIs with chrome:// favicons where the success callback is not invoked.
* This is OK: we will 'cancel' the request after the timeout (or when the window goes
* away) but that will be a no-op in such cases.
*/
_makeCompletionCallback(win, id) {
return {
onComplete(uri) {
let loadData = InternalFaviconLoader._removeLoadDataFromWindowMap(win, {
uri,
innerWindowID: id,
callback: this,
});
if (loadData) {
clearTimeout(loadData.timerID);
}
delete this.request;
},
};
},
ensureInitialized() {
if (this._initialized) {
return;
}
this._initialized = true;
Services.obs.addObserver(this, "inner-window-destroyed", false);
Services.ppmm.addMessageListener("Toolkit:inner-window-destroyed", msg => {
this.removeRequestsForInner(msg.data);
});
},
loadFavicon(browser, principal, uri) {
this.ensureInitialized();
let win = browser.ownerGlobal;
if (!gFaviconLoadDataMap.has(win)) {
gFaviconLoadDataMap.set(win, []);
let unloadHandler = event => {
let doc = event.target;
let eventWin = doc.defaultView;
if (eventWin == win) {
win.removeEventListener("unload", unloadHandler);
this.onUnload(win);
}
};
win.addEventListener("unload", unloadHandler, true);
}
let {innerWindowID, currentURI} = browser;
// Immediately cancel any earlier requests
this.removeRequestsForInner(innerWindowID);
// First we do the actual setAndFetch call:
let loadType = PrivateBrowsingUtils.isWindowPrivate(win)
? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
: PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
let callback = this._makeCompletionCallback(win, innerWindowID);
let request = PlacesUtils.favicons.setAndFetchFaviconForPage(currentURI, uri, false,
loadType, callback, principal);
// Now register the result so we can cancel it if/when necessary.
if (!request) {
// The favicon service can return with success but no-op (and leave request
// as null) if the icon is the same as the page (e.g. for images) or if it is
// the favicon for an error page. In this case, we do not need to do anything else.
return;
}
callback.request = request;
let loadData = {innerWindowID, uri, callback};
loadData.timerID = setTimeout(() => {
this._cancelRequest(loadData, "it timed out");
this._removeLoadDataFromWindowMap(win, loadData);
}, FAVICON_REQUEST_TIMEOUT);
let loadDataForWindow = gFaviconLoadDataMap.get(win);
loadDataForWindow.push(loadData);
},
};
this.PlacesUIUtils = {
ORGANIZER_LEFTPANE_VERSION: 7,
ORGANIZER_FOLDER_ANNO: "PlacesOrganizer/OrganizerFolder",
ORGANIZER_QUERY_ANNO: "PlacesOrganizer/OrganizerQuery",
LOAD_IN_SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
DESCRIPTION_ANNO: "bookmarkProperties/description",
/**
* Makes a URI from a spec, and do fixup
* @param aSpec
* The string spec of the URI
* @return A URI object for the spec.
*/
createFixedURI: function PUIU_createFixedURI(aSpec) {
return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
},
getFormattedString: function PUIU_getFormattedString(key, params) {
return bundle.formatStringFromName(key, params, params.length);
},
/**
* Get a localized plural string for the specified key name and numeric value
* substituting parameters.
*
* @param aKey
* String, key for looking up the localized string in the bundle
* @param aNumber
* Number based on which the final localized form is looked up
* @param aParams
* Array whose items will substitute #1, #2,... #n parameters
* in the string.
*
* @see https://developer.mozilla.org/en/Localization_and_Plurals
* @return The localized plural string.
*/
getPluralString: function PUIU_getPluralString(aKey, aNumber, aParams) {
let str = PluralForm.get(aNumber, bundle.GetStringFromName(aKey));
// Replace #1 with aParams[0], #2 with aParams[1], and so on.
return str.replace(/\#(\d+)/g, function (matchedId, matchedNumber) {
let param = aParams[parseInt(matchedNumber, 10) - 1];
return param !== undefined ? param : matchedId;
});
},
getString: function PUIU_getString(key) {
return bundle.GetStringFromName(key);
},
get _copyableAnnotations() {
return [
this.DESCRIPTION_ANNO,
this.LOAD_IN_SIDEBAR_ANNO,
PlacesUtils.READ_ONLY_ANNO,
];
},
/**
* Get a transaction for copying a uri item (either a bookmark or a history
* entry) from one container to another.
*
* @param aData
* JSON object of dropped or pasted item properties
* @param aContainer
* The container being copied into
* @param aIndex
* The index within the container the item is copied to
* @return A nsITransaction object that performs the copy.
*
* @note Since a copy creates a completely new item, only some internal
* annotations are synced from the old one.
* @see this._copyableAnnotations for the list of copyable annotations.
*/
_getURIItemCopyTransaction:
function PUIU__getURIItemCopyTransaction(aData, aContainer, aIndex)
{
let transactions = [];
if (aData.dateAdded) {
transactions.push(
new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
);
}
if (aData.lastModified) {
transactions.push(
new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
);
}
let annos = [];
if (aData.annos) {
annos = aData.annos.filter(function (aAnno) {
return this._copyableAnnotations.includes(aAnno.name);
}, this);
}
// There's no need to copy the keyword since it's bound to the bookmark url.
return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri),
aContainer, aIndex, aData.title,
null, annos, transactions);
},
/**
* Gets a transaction for copying (recursively nesting to include children)
* a folder (or container) and its contents from one folder to another.
*
* @param aData
* Unwrapped dropped folder data - Obj containing folder and children
* @param aContainer
* The container we are copying into
* @param aIndex
* The index in the destination container to insert the new items
* @return A nsITransaction object that will perform the copy.
*
* @note Since a copy creates a completely new item, only some internal
* annotations are synced from the old one.
* @see this._copyableAnnotations for the list of copyable annotations.
*/
_getFolderCopyTransaction(aData, aContainer, aIndex) {
function getChildItemsTransactions(aRoot) {
let transactions = [];
let index = aIndex;
for (let i = 0; i < aRoot.childCount; ++i) {
let child = aRoot.getChild(i);
// Temporary hacks until we switch to PlacesTransactions.jsm.
let isLivemark =
PlacesUtils.annotations.itemHasAnnotation(child.itemId,
PlacesUtils.LMANNO_FEEDURI);
let [node] = PlacesUtils.unwrapNodes(
PlacesUtils.wrapNode(child, PlacesUtils.TYPE_X_MOZ_PLACE, isLivemark),
PlacesUtils.TYPE_X_MOZ_PLACE
);
// Make sure that items are given the correct index, this will be
// passed by the transaction manager to the backend for the insertion.
// Insertion behaves differently for DEFAULT_INDEX (append).
if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) {
index = i;
}
if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
if (node.livemark && node.annos) {
transactions.push(
PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index)
);
}
else {
transactions.push(
PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index)
);
}
}
else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
transactions.push(new PlacesCreateSeparatorTransaction(-1, index));
}
else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
transactions.push(
PlacesUIUtils._getURIItemCopyTransaction(node, -1, index)
);
}
else {
throw new Error("Unexpected item under a bookmarks folder");
}
}
return transactions;
}
if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder.
let transactions = [];
if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
let {root} = PlacesUtils.getFolderContents(aData.id, false, false);
let urls = PlacesUtils.getURLsForContainerNode(root);
root.containerOpen = false;
for (let { uri } of urls) {
transactions.push(
new PlacesTagURITransaction(NetUtil.newURI(uri), [aData.title])
);
}
}
return new PlacesAggregatedTransaction("addTags", transactions);
}
if (aData.livemark && aData.annos) { // Copying a livemark.
return this._getLivemarkCopyTransaction(aData, aContainer, aIndex);
}
let {root} = PlacesUtils.getFolderContents(aData.id, false, false);
let transactions = getChildItemsTransactions(root);
root.containerOpen = false;
if (aData.dateAdded) {
transactions.push(
new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
);
}
if (aData.lastModified) {
transactions.push(
new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
);
}
let annos = [];
if (aData.annos) {
annos = aData.annos.filter(function (aAnno) {
return this._copyableAnnotations.includes(aAnno.name);
}, this);
}
return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex,
annos, transactions);
},
/**
* Gets a transaction for copying a live bookmark item from one container to
* another.
*
* @param aData
* Unwrapped live bookmarkmark data
* @param aContainer
* The container we are copying into
* @param aIndex
* The index in the destination container to insert the new items
* @return A nsITransaction object that will perform the copy.
*
* @note Since a copy creates a completely new item, only some internal
* annotations are synced from the old one.
* @see this._copyableAnnotations for the list of copyable annotations.
*/
_getLivemarkCopyTransaction:
function PUIU__getLivemarkCopyTransaction(aData, aContainer, aIndex)
{
if (!aData.livemark || !aData.annos) {
throw new Error("node is not a livemark");
}
let feedURI, siteURI;
let annos = [];
if (aData.annos) {
annos = aData.annos.filter(function (aAnno) {
if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) {
feedURI = PlacesUtils._uri(aAnno.value);
}
else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) {
siteURI = PlacesUtils._uri(aAnno.value);
}
return this._copyableAnnotations.includes(aAnno.name)
}, this);
}
return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title,
aContainer, aIndex, annos);
},
/**
* Constructs a Transaction for the drop or paste of a blob of data into
* a container.
* @param data
* The unwrapped data blob of dropped or pasted data.
* @param type
* The content type of the data
* @param container
* The container the data was dropped or pasted into
* @param index
* The index within the container the item was dropped or pasted at
* @param copy
* The drag action was copy, so don't move folders or links.
* @return An object implementing nsITransaction that can perform
* the move/insert.
*/
makeTransaction:
function PUIU_makeTransaction(data, type, container, index, copy)
{
switch (data.type) {
case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
if (copy) {
return this._getFolderCopyTransaction(data, container, index);
}
// Otherwise move the item.
return new PlacesMoveItemTransaction(data.id, container, index);
case PlacesUtils.TYPE_X_MOZ_PLACE:
if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked.
return this._getURIItemCopyTransaction(data, container, index);
}
// Otherwise move the item.
return new PlacesMoveItemTransaction(data.id, container, index);
case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
if (copy) {
// There is no data in a separator, so copying it just amounts to
// inserting a new separator.
return new PlacesCreateSeparatorTransaction(container, index);
}
// Otherwise move the item.
return new PlacesMoveItemTransaction(data.id, container, index);
default:
if (type == PlacesUtils.TYPE_X_MOZ_URL ||
type == PlacesUtils.TYPE_UNICODE ||
type == TAB_DROP_TYPE) {
let title = type != PlacesUtils.TYPE_UNICODE ? data.title
: data.uri;
return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri),
container, index, title);
}
}
return null;
},
/**
* ********* PlacesTransactions version of the function defined above ********
*
* Constructs a Places Transaction for the drop or paste of a blob of data
* into a container.
*
* @param aData
* The unwrapped data blob of dropped or pasted data.
* @param aType
* The content type of the data.
* @param aNewParentGuid
* GUID of the container the data was dropped or pasted into.
* @param aIndex
* The index within the container the item was dropped or pasted at.
* @param aCopy
* The drag action was copy, so don't move folders or links.
*
* @return a Places Transaction that can be transacted for performing the
* move/insert command.
*/
getTransactionForData: function(aData, aType, aNewParentGuid, aIndex, aCopy) {
if (!this.SUPPORTED_FLAVORS.includes(aData.type))
throw new Error(`Unsupported '${aData.type}' data type`);
if ("itemGuid" in aData) {
if (!this.PLACES_FLAVORS.includes(aData.type))
throw new Error (`itemGuid unexpectedly set on ${aData.type} data`);
let info = { guid: aData.itemGuid
, newParentGuid: aNewParentGuid
, newIndex: aIndex };
if (aCopy) {
info.excludingAnnotation = "Places/SmartBookmark";
return PlacesTransactions.Copy(info);
}
return PlacesTransactions.Move(info);
}
// Since it's cheap and harmless, we allow the paste of separators and
// bookmarks from builds that use legacy transactions (i.e. when itemGuid
// was not set on PLACES_FLAVORS data). Containers are a different story,
// and thus disallowed.
if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
throw new Error("Can't copy a container from a legacy-transactions build");
if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid
, index: aIndex });
}
let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title
: aData.uri;
return PlacesTransactions.NewBookmark({ uri: NetUtil.newURI(aData.uri)
, title: title
, parentGuid: aNewParentGuid
, index: aIndex });
},
/**
* Shows the bookmark dialog corresponding to the specified info.
*
* @param aInfo
* Describes the item to be edited/added in the dialog.
* See documentation at the top of bookmarkProperties.js
* @param aWindow
* Owner window for the new dialog.
*
* @see documentation at the top of bookmarkProperties.js
* @return true if any transaction has been performed, false otherwise.
*/
showBookmarkDialog:
function PUIU_showBookmarkDialog(aInfo, aParentWindow) {
// Preserve size attributes differently based on the fact the dialog has
// a folder picker or not, since it needs more horizontal space than the
// other controls.
let hasFolderPicker = !("hiddenRows" in aInfo) ||
!aInfo.hiddenRows.includes("folderPicker");
// Use a different chrome url to persist different sizes.
let dialogURL = hasFolderPicker ?
"chrome://browser/content/places/bookmarkProperties2.xul" :
"chrome://browser/content/places/bookmarkProperties.xul";
let features = "centerscreen,chrome,modal,resizable=yes";
aParentWindow.openDialog(dialogURL, "", features, aInfo);
return ("performed" in aInfo && aInfo.performed);
},
_getTopBrowserWin: function PUIU__getTopBrowserWin() {
return RecentWindow.getMostRecentBrowserWindow();
},
/**
* set and fetch a favicon. Can only be used from the parent process.
* @param browser {Browser} The XUL browser element for which we're fetching a favicon.
* @param principal {Principal} The loading principal to use for the fetch.
* @param uri {URI} The URI to fetch.
*/
loadFavicon(browser, principal, uri) {
if (gInContentProcess) {
throw new Error("Can't track loads from within the child process!");
}
InternalFaviconLoader.loadFavicon(browser, principal, uri);
},
/**
* Returns the closet ancestor places view for the given DOM node
* @param aNode
* a DOM node
* @return the closet ancestor places view if exists, null otherwsie.
*/
getViewForNode: function PUIU_getViewForNode(aNode) {
let node = aNode;
// The view for a