fix for bug #378558: Organize Bookmarks - paste doesn't work after cut

includes fixes for:

bug #341953:  unable to paste links into the organize bookmarks dialog
bug #383044:  Drag-and-drop bookmark creation should support meta-data
bug #385381:  Paste doesn't work in the bookmarks Manager during copy if item is modified between copy and paste

r=sspitzer
This commit is contained in:
sspitzer@mozilla.org 2007-07-13 17:25:26 -07:00
Родитель df2d51d512
Коммит ef9e63b3df
2 изменённых файлов: 455 добавлений и 210 удалений

Просмотреть файл

@ -114,8 +114,7 @@ PlacesController.prototype = {
case "cmd_copy":
return this._view.hasSelection;
case "cmd_paste":
return this._canInsert() &&
this._hasClipboardData() && this._canPaste();
return this._canInsert() && this._canPaste();
case "cmd_selectAll":
if (this._view.selType != "single") {
var result = this._view.getResult();
@ -375,31 +374,6 @@ PlacesController.prototype = {
return true;
},
/**
* Determines whether or not the clipboard contains data that the active
* view can support in a paste operation.
* @returns true if the clipboard contains data compatible with the active
* view, false otherwise.
*/
_hasClipboardData: function PC__hasClipboardData() {
var types = this._view.peerDropTypes;
var flavors =
Cc["@mozilla.org/supports-array;1"].
createInstance(Ci.nsISupportsArray);
for (var i = 0; i < types.length; ++i) {
var cstring =
Cc["@mozilla.org/supports-cstring;1"].
createInstance(Ci.nsISupportsCString);
cstring.data = types[i];
flavors.AppendElement(cstring);
}
var clipboard =
Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
return clipboard.hasDataMatchingFlavors(flavors,
Ci.nsIClipboard.kGlobalClipboard);
},
/**
* Determines whether or not nodes can be inserted relative to the selection.
*/
@ -426,22 +400,41 @@ PlacesController.prototype = {
* Looks at the data on the clipboard to see if it is paste-able.
* Paste-able data is:
* - in a format that the view can receive
* - not a set of URIs that is entirely already present in the view,
* since we can only have one instance of a URI per container.
* @returns true if the data is paste-able, false if the clipboard data
* cannot be pasted
*/
_canPaste: function PC__canPaste() {
var types = this._view.peerDropTypes;
var flavors =
Cc["@mozilla.org/supports-array;1"].
createInstance(Ci.nsISupportsArray);
for (var i = 0; i < types.length; ++i) {
var cstring =
Cc["@mozilla.org/supports-cstring;1"].
createInstance(Ci.nsISupportsCString);
cstring.data = types[i];
flavors.AppendElement(cstring);
}
var clipboard =
Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
var hasClipboardData = clipboard.hasDataMatchingFlavors(flavors,
Ci.nsIClipboard.kGlobalClipboard);
if (!hasClipboardData)
return false;
// XXX todo
// see bug #387007 for an idea on how to make this more efficient
// right now, we are pulling data off the clipboard and using
// unwrapNodes() to verify it.
var xferable =
Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE);
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
var clipboard = Cc["@mozilla.org/widget/clipboard;1"].
getService(Ci.nsIClipboard);
for (var j = 0; j < types.length; ++j) {
xferable.addDataFlavor(types[j]);
}
clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
try {
@ -452,14 +445,12 @@ PlacesController.prototype = {
if (this._view.peerDropTypes.indexOf(type.value) == -1)
return false;
// unwrapNodes will throw if the data blob is malformed.
var nodes = PlacesUtils.unwrapNodes(data, type.value);
var ip = this._view.insertionPoint;
return ip != null;
// unwrapNodes() will throw if the data blob is malformed.
var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value);
return this._view.insertionPoint != null;
}
catch (e) {
// Unwrap nodes failed, possibly because a field that should have
// unwrapeNodes() failed, possibly because a field that should have
// contained a URI did not actually contain something that is
// parse-able as a URI.
return false;
@ -1039,19 +1030,16 @@ PlacesController.prototype = {
/**
* Creates a set of transactions for the removal of a range of items. A range is
* an array of adjacent nodes in a view.
* @param range
* An array of nodes to remove. Should all be adjacent.
* @param transactions
* An array of transactions.
* Walk the list of folders we're removing in this delete operation, and
* see if the selected node specified is already implicitly being removed
* because it is a child of that folder.
* @param node
* Node to check for containment.
* @param pastFolders
* List of folders the calling function has already traversed
* @returns true if the node should be skipped, false otherwise.
*/
_removeRange: function PC__removeRange(range, transactions) {
NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
var index = PlacesUtils.getIndexOfNode(range[0]);
var removedFolders = [];
_shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
/**
* Determines if a node is contained by another node within a resultset.
* @param node
@ -1069,26 +1057,31 @@ PlacesController.prototype = {
}
return false;
}
/**
* Walk the list of folders we're removing in this delete operation, and
* see if the selected node specified is already implicitly being removed
* because it is a child of that folder.
* @param node
* Node to check for containment.
* @returns true if the node should be skipped, false otherwise.
*/
function shouldSkipNode(node) {
for (var j = 0; j < removedFolders.length; ++j) {
if (isContainedBy(node, removedFolders[j]))
for (var j = 0; j < pastFolders.length; ++j) {
if (isContainedBy(node, pastFolders[j]))
return true;
}
return false;
}
return false;
},
/**
* Creates a set of transactions for the removal of a range of items.
* A range is an array of adjacent nodes in a view.
* @param range
* An array of nodes to remove. Should all be adjacent.
* @param transactions
* An array of transactions.
*/
_removeRange: function PC__removeRange(range, transactions) {
NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
var index = PlacesUtils.getIndexOfNode(range[0]);
var removedFolders = [];
for (var i = 0; i < range.length; ++i) {
var node = range[i];
if (shouldSkipNode(node))
if (this._shouldSkipNode(node, removedFolders))
continue;
if (PlacesUtils.nodeIsFolder(node)) {
@ -1247,35 +1240,36 @@ PlacesController.prototype = {
Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
var foundFolder = false, foundLink = false;
var pcString = psString = placeString = mozURLString = htmlString = unicodeString = "";
var copiedFolders = [];
var placeString = mozURLString = htmlString = unicodeString = "";
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
if (this._shouldSkipNode(node, copiedFolders))
continue;
if (PlacesUtils.nodeIsFolder(node))
copiedFolders.push(node);
function generateChunk(type, overrideURI) {
var suffix = i < (nodes.length - 1) ? NEWLINE : "";
return PlacesUtils.wrapNode(node, type, overrideURI) + suffix;
var suffix = i < (nodes.length - 1) ? "\n" : "";
var uri = overrideURI;
if (PlacesUtils.nodeIsLivemarkContainer(node))
uri = PlacesUtils.livemarks.getFeedURI(node.itemId).spec
mozURLString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_X_MOZ_URL,
uri) + suffix);
unicodeString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_UNICODE,
uri) + suffix);
htmlString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_HTML,
uri) + suffix);
var placeSuffix = i < (nodes.length - 1) ? "," : "";
return PlacesUtils.wrapNode(node, type, overrideURI) + placeSuffix;
}
function generateURIChunks(overrideURI) {
mozURLString += generateChunk(PlacesUtils.TYPE_X_MOZ_URL, overrideURI);
htmlString += generateChunk(PlacesUtils.TYPE_HTML, overrideURI);
unicodeString += generateChunk(PlacesUtils.TYPE_UNICODE, overrideURI);
}
if (PlacesUtils.nodeIsFolder(node) || PlacesUtils.nodeIsQuery(node)) {
pcString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
// Also copy the feed URI for live-bookmark folders
if (PlacesUtils.nodeIsLivemarkContainer(node)) {
var uri = PlacesUtils.livemarks.getFeedURI(node.itemId);
generateURIChunks(uri.spec);
}
}
else if (PlacesUtils.nodeIsSeparator(node))
psString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
else {
// all items wrapped as TYPE_X_MOZ_PLACE
placeString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE);
generateURIChunks();
}
}
function addData(type, data) {
@ -1283,11 +1277,7 @@ PlacesController.prototype = {
xferable.setTransferData(type, PlacesUtils._wrapString(data), data.length * 2);
}
// This order is _important_! It controls how this and other applications
// select data to be inserted based on type.
if (pcString)
addData(PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, pcString);
if (psString)
addData(PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, psString);
// select data to be inserted based on type.
if (placeString)
addData(PlacesUtils.TYPE_X_MOZ_PLACE, placeString);
if (mozURLString)
@ -1297,8 +1287,7 @@ PlacesController.prototype = {
if (htmlString)
addData(PlacesUtils.TYPE_HTML, htmlString);
if (pcString || psString || placeString || unicodeString || htmlString ||
mozURLString) {
if (placeString || unicodeString || htmlString || mozURLString) {
var clipboard =
Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
@ -1363,9 +1352,14 @@ PlacesController.prototype = {
data = data.value.QueryInterface(Ci.nsISupportsString).data;
var items = PlacesUtils.unwrapNodes(data, type.value);
var transactions = [];
var index = ip.index;
for (var i = 0; i < items.length; ++i) {
// adjusted to make sure that items are given the correct index -
// transactions insert differently if index == -1
if (ip.index > -1)
index = ip.index + i;
transactions.push(PlacesUtils.makeTransaction(items[i], type.value,
ip.itemId, ip.index,
ip.itemId, index,
true));
}
return transactions;
@ -1382,12 +1376,9 @@ PlacesController.prototype = {
// Get transactions to paste any folders, separators or links that might
// be on the clipboard, aggregate them and execute them.
var transactions =
[].concat(getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER]),
getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR]),
getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE,
PlacesUtils.TYPE_X_MOZ_URL,
PlacesUtils.TYPE_UNICODE]));
var transactions = getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE,
PlacesUtils.TYPE_X_MOZ_URL,
PlacesUtils.TYPE_UNICODE]);
var txn = new PlacesAggregateTransaction("Paste", transactions);
PlacesUtils.tm.doTransaction(txn);
}
@ -1612,7 +1603,7 @@ PlacesAggregateTransaction.prototype = {
},
commit: function PAT_commit(aUndo) {
for (var i = this._transactions.length - 1; i >= 0; --i) {
for (var i = 0; i < this._transactions.length; ++i) {
var txn = this._transactions[i];
if (this.container > -1)
txn.container = this.container;

Просмотреть файл

@ -453,41 +453,167 @@ var PlacesUtils = {
* @returns A string serialization of the node
*/
wrapNode: function PU_wrapNode(aNode, aType, aOverrideURI) {
var self = this;
// when wrapping a node, we want all the items, even if the original
// query options are excluding them.
// this can happen when copying from the left hand pane of the bookmarks
// organizer
function convertNode(cNode) {
try {
if (self.nodeIsFolder(cNode) && cNode.queryOptions.excludeItems)
return self.getFolderContents(cNode.itemId, false, true).root;
}
catch (e) {
}
return cNode;
}
switch (aType) {
case this.TYPE_X_MOZ_PLACE_CONTAINER:
case this.TYPE_X_MOZ_PLACE:
case this.TYPE_X_MOZ_PLACE_SEPARATOR:
// Data is encoded like this:
// bookmarks folder: <itemId>\n<>\n<parentId>\n<indexInParent>
// uri: 0\n<uri>\n<parentId>\n<indexInParent>
// bookmark: <itemId>\n<uri>\n<parentId>\n<indexInParent>
// separator: <itemId>\n<>\n<parentId>\n<indexInParent>
var wrapped = "";
if (aNode.itemId != -1) //
wrapped += aNode.itemId + NEWLINE;
else
wrapped += "0" + NEWLINE;
if (this.nodeIsURI(aNode) || this.nodeIsQuery(aNode))
wrapped += aNode.uri + NEWLINE;
else
wrapped += NEWLINE;
if (this.nodeIsFolder(aNode.parent))
wrapped += aNode.parent.itemId + NEWLINE;
else
wrapped += "0" + NEWLINE;
wrapped += this.getIndexOfNode(aNode);
return wrapped;
case this.TYPE_X_MOZ_URL:
return (aOverrideURI || aNode.uri) + NEWLINE + aNode.title;
case this.TYPE_HTML:
return "<A HREF=\"" + (aOverrideURI || aNode.uri) + "\">" +
aNode.title + "</A>";
case this.TYPE_X_MOZ_PLACE:
case this.TYPE_X_MOZ_PLACE_SEPARATOR:
case this.TYPE_X_MOZ_PLACE_CONTAINER:
function gatherDataPlace(bNode) {
var nodeId = 0;
if (bNode.itemId != -1)
nodeId = bNode.itemId;
var nodeUri = bNode.uri
var nodeTitle = bNode.title;
var nodeParentId = 0;
if (bNode.parent && self.nodeIsFolder(bNode.parent))
nodeParentId = bNode.parent.itemId;
var nodeIndex = self.getIndexOfNode(bNode);
var nodeKeyword = self.bookmarks.getKeywordForBookmark(bNode.itemId);
var nodeAnnos = self.getAnnotationsForItem(bNode.itemId);
var nodeType = "";
if (self.nodeIsContainer(bNode))
nodeType = self.TYPE_X_MOZ_PLACE_CONTAINER;
else if (self.nodeIsURI(bNode)) // a bookmark or a history visit
nodeType = self.TYPE_X_MOZ_PLACE;
else if (self.nodeIsSeparator(bNode))
nodeType = self.TYPE_X_MOZ_PLACE_SEPARATOR;
var node = { id: nodeId,
uri: nodeUri,
title: nodeTitle,
parent: nodeParentId,
index: nodeIndex,
keyword: nodeKeyword,
annos: nodeAnnos,
type: nodeType };
// Recurse down children if the node is a folder
if (self.nodeIsContainer(bNode)) {
if (self.nodeIsLivemarkContainer(bNode)) {
// just save the livemark info, reinstantiate on other end
var feedURI = self.livemarks.getFeedURI(bNode.itemId).spec;
var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
node.uri = { feed: feedURI,
site: siteURI };
}
else { // bookmark folders + history containers
var wasOpen = bNode.containerOpen;
if (!wasOpen)
bNode.containerOpen = true;
var childNodes = [];
var cc = bNode.childCount;
for (var i = 0; i < cc; ++i) {
var childObj = gatherDataPlace(bNode.getChild(i));
if (childObj != null)
childNodes.push(childObj);
}
var parent = node;
node = { folder: parent,
children: childNodes,
type: self.TYPE_X_MOZ_PLACE_CONTAINER };
bNode.containerOpen = wasOpen;
}
}
return node;
}
return this.toJSONString(gatherDataPlace(convertNode(aNode)));
case this.TYPE_X_MOZ_URL:
function gatherDataUrl(bNode) {
if (self.nodeIsLivemarkContainer(bNode)) {
var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
return siteURI + "\n" + bNode.title;
}
else if (self.nodeIsURI(bNode))
return (aOverrideURI || bNode.uri) + "\n" + bNode.title;
else // ignore containers and separators - items without valid URIs
return "";
}
return gatherDataUrl(convertNode(aNode));
case this.TYPE_HTML:
function gatherDataHtml(bNode) {
function htmlEscape(s) {
s = s.replace(/&/g, "&amp;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/</g, "&lt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&apos;");
return s;
}
// escape out potential HTML in the title
var escapedTitle = htmlEscape(bNode.title);
if (self.nodeIsLivemarkContainer(bNode)) {
var siteURI = self.livemarks.getSiteURI(bNode.itemId).spec;
return "<A HREF=\"" + siteURI + "\">" + escapedTitle + "</A>\n";
}
else if (self.nodeIsContainer(bNode)) {
var wasOpen = bNode.containerOpen;
if (!wasOpen)
bNode.containerOpen = true;
var childString = "<DL><DT>" + escapedTitle + "</DT>\n";
var cc = bNode.childCount;
for (var i = 0; i < cc; ++i)
childString += "<DD>\n"
+ gatherDataHtml(bNode.getChild(i))
+ "</DD>\n";
bNode.containerOpen = wasOpen;
return childString + "</DL>\n";
}
else if (self.nodeIsURI(bNode))
return "<A HREF=\"" + bNode.uri + "\">" + escapedTitle + "</A>\n";
else if (self.nodeIsSeparator(bNode))
return "<HR>\n";
else
return "";
}
return gatherDataHtml(convertNode(aNode));
}
// case this.TYPE_UNICODE:
return (aOverrideURI || aNode.uri);
function gatherDataText(bNode) {
if (self.nodeIsLivemarkContainer(bNode)) {
return self.livemarks.getSiteURI(bNode.itemId).spec;
}
else if (self.nodeIsContainer(bNode)) {
var wasOpen = bNode.containerOpen;
if (!wasOpen)
bNode.containerOpen = true;
var childString = bNode.title + "\n";
var cc = bNode.childCount;
for (var i = 0; i < cc; ++i) {
var child = bNode.getChild(i);
var suffix = i < (cc - 1) ? "\n" : "";
childString += gatherDataText(child) + suffix;
}
bNode.containerOpen = wasOpen;
return childString;
}
else if (self.nodeIsURI(bNode))
return (aOverrideURI || bNode.uri);
else if (self.nodeIsSeparator(bNode))
return "--------------------";
else
return "";
}
return gatherDataText(convertNode(aNode));
},
/**
@ -501,9 +627,10 @@ var PlacesUtils = {
* The index within the container the item is copied to
* @returns A nsITransaction object that performs the copy.
*/
_getURIItemCopyTransaction: function (aURI, aContainer, aIndex) {
var title = this.history.getPageTitle(aURI);
return new PlacesCreateItemTransaction(aURI, aContainer, aIndex, title);
_getURIItemCopyTransaction: function (aData, aContainer, aIndex) {
var itemURL = this._uri(aData.uri);
return new PlacesCreateItemTransaction(itemURL, aContainer,
aIndex, aData.title);
},
/**
@ -521,19 +648,19 @@ var PlacesUtils = {
* @returns A nsITransaction object that performs the copy.
*/
_getBookmarkItemCopyTransaction:
function PU__getBookmarkItemCopyTransaction(aId, aContainer, aIndex,
function PU__getBookmarkItemCopyTransaction(aData, aContainer, aIndex,
aExcludeAnnotations) {
var bookmarks = this.bookmarks;
var itemURL = bookmarks.getBookmarkURI(aId);
var itemTitle = bookmarks.getItemTitle(aId);
var keyword = bookmarks.getKeywordForBookmark(aId);
var annos = this.getAnnotationsForItem(aId);
var itemURL = this._uri(aData.uri);
var itemTitle = aData.title;
var keyword = aData.keyword;
var annos = aData.annos;
if (aExcludeAnnotations) {
annos =
annos.filter(function(aValue, aIndex, aArray) {
return aExcludeAnnotations.indexOf(aValue.name) == -1;
});
}
var createTxn =
new PlacesCreateItemTransaction(itemURL, aContainer, aIndex, itemTitle,
keyword, annos);
@ -542,9 +669,10 @@ var PlacesUtils = {
/**
* Gets a transaction for copying (recursively nesting to include children)
* a folder and its contents from one folder to another.
* a folder (or container) and its contents from one folder to another.
*
* @param aData
* Unwrapped dropped folder data
* Unwrapped dropped folder data - Obj containing folder and children
* @param aContainer
* The container we are copying into
* @param aIndex
@ -554,33 +682,39 @@ var PlacesUtils = {
_getFolderCopyTransaction:
function PU__getFolderCopyTransaction(aData, aContainer, aIndex) {
var self = this;
function getChildItemsTransactions(aFolderId) {
function getChildItemsTransactions(aChildren) {
var childItemsTransactions = [];
var children = self.getFolderContents(aFolderId, false, false);
var cc = children.childCount;
var cc = aChildren.length;
var index = aIndex;
for (var i = 0; i < cc; ++i) {
var txn = null;
var node = children.getChild(i);
if (self.nodeIsFolder(node)) {
var nodeFolderId = node.itemId;
var title = self.bookmarks.getItemTitle(nodeFolderId);
var annos = self.getAnnotationsForItem(nodeFolderId);
var folderItemsTransactions =
getChildItemsTransactions(nodeFolderId);
txn = new PlacesCreateFolderTransaction(title, -1, aIndex, annos,
folderItemsTransactions);
var node = aChildren[i];
// adjusted to make sure that items are given the correct index -
// transactions insert differently if index == -1
if (aIndex > -1)
index = aIndex + i;
if (node.type == self.TYPE_X_MOZ_PLACE_CONTAINER) {
if (node.folder) {
var title = node.folder.title;
var annos = node.folder.annos;
var folderItemsTransactions =
getChildItemsTransactions(node.children);
txn = new PlacesCreateFolderTransaction(title, -1, index, annos,
folderItemsTransactions);
}
else { // node is a livemark
var feedURI = self._uri(node.uri.feed);
var siteURI = self._uri(node.uri.site);
txn = new PlacesCreateLivemarkTransaction(feedURI, siteURI,
node.title, aContainer, index, node.annos);
}
}
else if (self.nodeIsBookmark(node)) {
txn = self._getBookmarkItemCopyTransaction(node.itemId, -1,
aIndex);
}
else if (self.nodeIsURI(node) || self.nodeIsQuery(node)) {
// XXXmano: can this ^ ever happen?
txn = self._getURIItemCopyTransaction(self._uri(node.uri), -1,
aIndex);
}
else if (self.nodeIsSeparator(node))
txn = new PlacesCreateSeparatorTransaction(-1, aIndex);
else if (node.type == self.TYPE_X_MOZ_PLACE_SEPARATOR)
txn = new PlacesCreateSeparatorTransaction(-1, index);
else if (node.type == self.TYPE_X_MOZ_PLACE)
txn = self._getBookmarkItemCopyTransaction(node, -1, index);
NS_ASSERT(txn, "Unexpected item under a bookmarks folder");
if (txn)
@ -588,12 +722,13 @@ var PlacesUtils = {
}
return childItemsTransactions;
}
var title = aData.folder.title;
var annos = aData.folder.annos;
var title = this.bookmarks.getItemTitle(aData.id);
var annos = this.getAnnotationsForItem(aData.id);
var createTxn =
new PlacesCreateFolderTransaction(title, aContainer, aIndex, annos,
getChildItemsTransactions(aData.id));
getChildItemsTransactions(aData.children));
return createTxn;
},
@ -607,41 +742,42 @@ var PlacesUtils = {
* @returns An array of objects representing each item contained by the source.
*/
unwrapNodes: function PU_unwrapNodes(blob, type) {
// We use \n here because the transferable system converts \r\n to \n
var parts = blob.split("\n");
// We split on "\n" because the transferable system converts "\r\n" to "\n"
var nodes = [];
for (var i = 0; i < parts.length; ++i) {
var data = { };
switch (type) {
case this.TYPE_X_MOZ_PLACE_CONTAINER:
switch(type) {
case this.TYPE_X_MOZ_PLACE:
case this.TYPE_X_MOZ_PLACE_SEPARATOR:
// Data in these types has 4 parts, so if there are less than 4 parts
// remaining, the data blob is malformed and we should stop.
if (i > (parts.length - 4))
break;
nodes.push({ id: parseInt(parts[i++]),
uri: parts[i] ? this._uri(parts[i]) : null,
parent: parseInt(parts[++i]),
index: parseInt(parts[++i]) });
case this.TYPE_X_MOZ_PLACE_CONTAINER:
nodes = this.parseJSON("[" + blob + "]");
break;
case this.TYPE_X_MOZ_URL:
// See above.
if (i > (parts.length - 2))
var parts = blob.split("\n");
// data in this type has 2 parts per entry, so if there are fewer
// than 2 parts left, the blob is malformed and we should stop
if (parts.length % 2)
break;
nodes.push({ uri: this._uri(parts[i++]),
title: parts[i] ? parts[i] : parts[i-1] });
for (var i = 0; i < parts.length; i=i+2) {
var uriString = parts[i];
var titleString = parts[i+1];
// note: this._uri() will throw if uriString is not a valid URI
if (this._uri(uriString)) {
nodes.push({ uri: uriString,
title: titleString ? titleString : uriString });
}
}
break;
case this.TYPE_UNICODE:
// See above.
if (i > (parts.length - 1))
break;
nodes.push({ uri: this._uri(parts[i]) });
var parts = blob.split("\n");
for (var i = 0; i < parts.length; i++) {
var uriString = parts[i];
// note: this._uri() will throw if uriString is not a valid URI
if (uriString != "" && this._uri(uriString))
nodes.push({ uri: uriString, title: uriString });
}
break;
default:
LOG("Cannot unwrap data of type " + type);
throw Cr.NS_ERROR_INVALID_ARG;
}
}
return nodes;
},
@ -664,23 +800,30 @@ var PlacesUtils = {
*/
makeTransaction: function PU_makeTransaction(data, type, container,
index, copy) {
switch (type) {
switch (data.type) {
case this.TYPE_X_MOZ_PLACE_CONTAINER:
if (data.id > 0) {
if (data.folder) {
// Place is a folder.
if (copy)
return this._getFolderCopyTransaction(data, container, index);
}
else if (copy) {
// Place is a Livemark Container, should be reinstantiated
var feedURI = this._uri(data.uri.feed);
var siteURI = this._uri(data.uri.site);
return new PlacesCreateLivemarkTransaction(feedURI, siteURI,
data.title, container, index, data.annos);
}
break;
case this.TYPE_X_MOZ_PLACE:
if (data.id <= 0)
return this._getURIItemCopyTransaction(data.uri, container, index);
return this._getURIItemCopyTransaction(data, container, index);
if (copy) {
// Copying a child of a live-bookmark by itself should result
// as a new normal bookmark item (bug 376731)
var copyBookmarkAnno =
this._getBookmarkItemCopyTransaction(data.id, container, index,
this._getBookmarkItemCopyTransaction(data, container, index,
["livemark/bookmarkFeedURI"]);
return copyBookmarkAnno;
}
@ -692,20 +835,24 @@ var PlacesUtils = {
return new PlacesCreateSeparatorTransaction(container, index);
}
break;
case this.TYPE_X_MOZ_URL:
case this.TYPE_UNICODE:
var title = type == this.TYPE_X_MOZ_URL ? data.title : data.uri.spec;
var createTxn =
new PlacesCreateItemTransaction(data.uri, container, index, title);
return createTxn;
default:
return null;
if (type == this.TYPE_X_MOZ_URL || type == this.TYPE_UNICODE) {
var title = (type == this.TYPE_X_MOZ_URL) ? data.title : data.uri;
var url = this._uri(data.uri);
var createTxn =
new PlacesCreateItemTransaction(url, container, index, title);
return createTxn;
}
else {
return null;
}
}
if (data.id <= 0)
return null;
// Move the item otherwise
return new PlacesMoveItemTransaction(data.id, container, index);
var id = data.folder ? data.folder.id : data.id;
return new PlacesMoveItemTransaction(id, container, index);
},
/**
@ -1323,6 +1470,113 @@ var PlacesUtils = {
return annos.getPageAnnotation(aURI, POST_DATA_ANNO);
return null;
},
/**
* Converts a JavaScript object into a JSON string
* (see http://www.json.org/ for the full grammar).
*
* The inverse operation consists of eval("(" + JSON_string + ")");
* and should be provably safe.
*
* @param aJSObject is the object to be converted
* @return the object's JSON representation
*/
toJSONString: function PU_toJSONString(aJSObject) {
// these characters have a special escape notation
const charMap = { "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f",
"\r": "\\r", '"': '\\"', "\\": "\\\\" };
// we use a single string builder for efficiency reasons
var parts = [];
// this recursive function walks through all objects and appends their
// JSON representation to the string builder
function jsonIfy(aObj) {
if (typeof aObj == "boolean") {
parts.push(aObj ? "true" : "false");
}
else if (typeof aObj == "number" && isFinite(aObj)) {
// there is no representation for infinite numbers or for NaN!
parts.push(aObj.toString());
}
else if (typeof aObj == "string") {
aObj = aObj.replace(/[\\"\x00-\x1F\u0080-\uFFFF]/g, function($0) {
// use the special escape notation if one exists, otherwise
// produce a general unicode escape sequence
return charMap[$0] ||
"\\u" + ("0000" + $0.charCodeAt(0).toString(16)).slice(-4);
});
parts.push('"' + aObj + '"');
}
else if (aObj == null) {
parts.push("null");
}
else if (aObj instanceof Array) {
parts.push("[");
for (var i = 0; i < aObj.length; i++) {
jsonIfy(aObj[i]);
parts.push(",");
}
if (parts[parts.length - 1] == ",")
parts.pop(); // drop the trailing colon
parts.push("]");
}
else if (typeof aObj == "object") {
parts.push("{");
for (var key in aObj) {
jsonIfy(key.toString());
parts.push(":");
jsonIfy(aObj[key]);
parts.push(",");
}
if (parts[parts.length - 1] == ",")
parts.pop(); // drop the trailing colon
parts.push("}");
}
else {
throw new Error("No JSON representation for this object!");
}
} // end of jsonIfy definition
jsonIfy(aJSObject);
var newJSONString = parts.join(" ");
// sanity check - so that API consumers can just eval this string
if (/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
newJSONString.replace(/"(\\.|[^"\\])*"/g, "")
))
throw new Error("JSON conversion failed unexpectedly!");
return newJSONString;
},
/**
* Converts a JSON string into a JavaScript object
* (see http://www.json.org/ for the full grammar).
*
* @param jsonText is the object to be converted
* @return a JS Object
*/
parseJSON: function parseJSON(jsonText) {
var m = {
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
};
var EVAL_SANDBOX = new Components.utils.Sandbox("about:blank");
if (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/.test(jsonText.
replace(/\\./g, '@').
replace(/"[^"\\\n\r]*"/g, ''))) {
var j = Components.utils.evalInSandbox(jsonText, EVAL_SANDBOX);
return j;
}
else
throw new SyntaxError('parseJSON');
}
};