Commit ccdbcc93 authored by beng%bengoodger.com's avatar beng%bengoodger.com
Browse files

324958 - folder undo delete back to previous folder id. Implement folder...

324958 - folder undo delete back to previous folder id. Implement folder removal transaction in bookmarks service itself, using private methods to restore folders back to previous ids. Adjust fe to use new api. r=brettw (C++), annie.sullivan (JS)
parent 714da423
Loading
Loading
Loading
Loading
+103 −60
Original line number Diff line number Diff line
@@ -686,7 +686,7 @@ var PlacesController = {
    // selected folder (if a single folder is selected).     
    var sortingChildren = false;
    var name = result.root.title;
    if (selectedNode) 
    if (selectedNode && selectedNode.parent) 
      name = selectedNode.parent.title;
    if (hasSingleSelection && this.nodeIsFolder(selectedNode)) {
      name = selectedNode.title;
@@ -860,7 +860,7 @@ var PlacesController = {
        isLivemarkItem = true;
        command.setAttribute("label", strings.getString("livemarkReloadAll"));
      }
      else {
      else if (this.nodeIsURI(selectedNode)) {
        var uri = this._uri(selectedNode.uri);
        isLivemarkItem = 
          this.annotations.hasAnnotation(uri, "livemark/bookmarkFeedURI");
@@ -923,15 +923,20 @@ var PlacesController = {
      var node = nodes[i];
      if (!this.nodeIsURI(node))
        foundNonLeaf = true;
      if (!this.nodeIsReadOnly(node) && !this.nodeIsReadOnly(node.parent))
      if (!this.nodeIsReadOnly(node) && 
          (node.parent && !this.nodeIsReadOnly(node.parent)))
        metadata["mutable"] = true;
      
      var uri = this._uri(node.uri);
      var uri = null;
      if (this.nodeIsURI(node))
        uri = this._uri(node.uri);
      if (this.nodeIsFolder(node))
        uri = this.bookmarks.getFolderURI(asFolder(node).folderId);
      if (uri) {
        var names = this.annotations.getPageAnnotationNames(uri, { });
        for (var j = 0; j < names.length; ++j)
          metadata[names[i]] = true;
      }
      
      if (nodes[i].parent != lastParent || nodes[i].type != lastType)
        metadata["mixed"] = true;
@@ -1265,13 +1270,57 @@ var PlacesController = {
    NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
    var index = this.getIndexOfNode(range[0]);
    
    // Walk backwards to preserve insertion order on undo
    for (var i = range.length - 1; i >= 0; --i) {
    var removedFolders = [];
    
    /**
     * Determines if a node is contained by another node within a resultset. 
     * @param   node
     *          The node to check for containment for
     * @param   parent
     *          The parent container to check for containment in
     * @returns true if node is a member of parent's children, false otherwise.
     */
    function isContainedBy(node, parent) {
      var cursor = node.parent;
      while (cursor) {
        if (cursor == parent)
          return true;
        cursor = cursor.parent;
      }
      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]))
          return true;
      }
      return false;          
    }
    
    for (var i = 0; i < range.length; ++i) {
      var node = range[i];
      if (shouldSkipNode(node))
        continue;
      
      if (this.nodeIsFolder(node)) {
        // TODO -- node.parent might be a query and not a folder.  See bug 324948
        transactions.push(new PlacesRemoveFolderTransaction(
          asFolder(node).folderId, asFolder(node.parent).folderId, index));
        var folder = asFolder(node);
        removedFolders.push(folder);
        // Undo for bookmark folder removals is handled by the bookmarks service,
        // for various reasons. See: 
        // http://groups.google.com/group/mozilla.dev.apps.firefox/msg/215e88fcdb91ff25
        var txn = this.bookmarks.getRemoveFolderTransaction(folder.folderId);
        var removeTxn = new PlacesRemoveFolderTransaction(txn, folder.folderId);
        transactions.push(removeTxn);
      }
      else if (this.nodeIsSeparator(node)) {
        // A Bookmark separator.
@@ -2057,79 +2106,73 @@ function PlacesRemoveFolderSaveChildFolder(name) {
 * likely have a different id.
 */

function PlacesRemoveFolderTransaction(id, oldContainer, oldIndex) {
function PlacesRemoveFolderTransaction(removeTxn, id) {
  this._removeTxn = removeTxn;
  this._id = id;
  this._oldContainer = oldContainer;
  this._oldIndex = oldIndex;
  this._oldFolderTitle = null;
  this._contents = null; // The encoded contents of this folder
  this._transactions = []; // A set of transactions to remove content.
  this.redoTransaction = this.doTransaction;
}
PlacesRemoveFolderTransaction.prototype = {
  __proto__: PlacesBaseTransaction.prototype, 
    
  /**
   * Save the contents of a folder (items and containers) for restoration 
   * purposes later.
   * Create a flat, ordered list of transactions for a depth-first recreation
   * of items within this folder. 
   * @param   id
   *          The id of the folder
   * @param   parent
   *          The parent PlacesRemoveFolderSaveChildFolder object
   *          The id of the folder to save the contents of
   */
  _saveFolderContents: function PRFT__saveFolderContents(id, parent) {
    var contents = PlacesController.getFolderContents(id, false, false);
  _saveFolderContents: function PRFT__saveFolderContents() {
    this._transactions = [];
    var contents = PlacesController.getFolderContents(this._id, false, false);
    // Container open status doesn't need to be reset to what it was before
    // because it's being deleted.
    contents.containerOpen = true;
    for (var i = contents.childCount - 1; i >= 0; --i) {
    var ios = 
        Cc["@mozilla.org/network/io-service;1"].
        getService(Ci.nsIIOService);  
    for (var i = 0; i < contents.childCount; ++i) {
      var child = contents.getChild(i);
      var obj = null;
      var txn;
      if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER) {
        obj = new PlacesRemoveFolderSaveChildFolder(child.title);
        parent.children.push(obj);
        this._saveFolderContents(asFolder(child).folderId, obj);
      }
      else {
        obj = new PlacesRemoveFolderSaveChildItem(child.title, child.uri);
        parent.children.push(obj);
        txn = this.bookmarks.getRemoveFolderTransaction(this._id);
        var removeTxn = new PlacesRemoveFolderTransaction(txn, this._id);
        this._transactions.push(txn);
      }
    }
  },
  
  /**
   * Recreate a folder hierarchy from a saved data set.
   * @param   parent
   *          The id of the folder to create the items beneath
   * @param   item
   *          The object that contains the saved data.
   */
  _restore: function PRFT__restore(parent, item) {
    if (item instanceof PlacesRemoveFolderSaveChildFolder) {
      var id = this.bookmarks.createFolder(parent, item.name, 0);
      this._restore(id, item);
      else if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
        txn = new PlacesRemoveSeparatorTransaction(this._id, i);
        this._transactions.push(txn);
      }
      else {
      this.bookmarks.insertItem(parent, item.uri, 0);
      this.bookmarks.setItemTitle(item.uri, item.name);
        txn = new PlacesRemoveItemTransaction(ios.newURI(child.uri, null, null),
                                              this._id, i);
        this._transactions.push(txn);
      }
    }
  },
  
  doTransaction: function PRFT_doTransaction() {
    this._oldFolderTitle = this.bookmarks.getFolderTitle(this._id);
    LOG("Remove Folder: " + this._oldFolderTitle + " from: " + this._oldContainer + "," + this._oldIndex);
    var title = this.bookmarks.getFolderTitle(this._id);
    LOG("Remove Folder: " + title);
    
    this._contents = new PlacesRemoveFolderSaveChildFolder(this._oldFolderTitle);
    this._saveFolderContents(this._id, this._contents);
    this._saveFolderContents();
    
    this.bookmarks.removeFolder(this._id);
    // Remove children backwards to preserve parent-child relationships.
    for (var i = this._transactions.length - 1; i >= 0; --i)
      this._transactions[i].doTransaction();
    
    // Remove this folder itself. 
    this._removeTxn.doTransaction();
  },
  
  undoTransaction: function PRFT_undoTransaction() {
    LOG("UNRemove Folder: " + this._oldFolderTitle + " from: " + this._oldContainer + "," + this._oldIndex);
    this._id = this.bookmarks.createFolder(this._oldContainer, this._oldFolderTitle, this._oldIndex);
    this._removeTxn.undoTransaction();
    
    var title = this.bookmarks.getFolderTitle(this._id);
    LOG("UNRemove Folder: " + title);
    
    for (var i = 0; i < this._contents.children.length; ++i)
      this._restore(this._id, this._contents.children[i]);
    // Create children forwards to preserve parent-child relationships.
    for (var i = 0; i < this._transactions.length; ++i)
      this._transactions[i].undoTransaction();
  }
};

+3 −2
Original line number Diff line number Diff line
@@ -94,7 +94,8 @@
          var result = this.history.executeQueries(queries, queries.length, 
                                                   options);

          var treeviewer = Cc["@mozilla.org/browser/nav-history/result-tree-viewer;1"].
          var treeviewer = 
              Cc["@mozilla.org/browser/nav-history/result-tree-viewer;1"].
              createInstance(Ci.nsINavHistoryResultViewer);
          result.viewer = treeviewer;

+19 −1
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@
%}

interface nsIURI;
interface nsITransaction;

[ptr] native PRInt64Array(nsTArray<PRInt64>);

@@ -199,7 +200,7 @@ interface nsINavBookmarkObserver : nsISupports
 * folders.  A URI in history can be contained in one or more such folders.
 */

[scriptable, uuid(6979da92-25d4-4553-8450-bfec2b3772db)]
[scriptable, uuid(df93900d-7ef1-4c5f-8e44-a930aeaf1462)]
interface nsINavBookmarksService : nsISupports
{
  /**
@@ -276,6 +277,23 @@ interface nsINavBookmarksService : nsISupports
   */
  void removeFolder(in PRInt64 folder);

  /**
   * Gets an undo-able transaction for removing a folder from the bookmarks
   * tree. 
   *  @param folder     The id of the folder to remove.
   *  @returns An object implementing nsITransaction that can be used to undo 
   *           or redo the action. 
   *
   * This method exists because complex delete->undo operations rely on 
   * recreated folders to have the same ID they had before they were deleted, 
   * so that any other items deleted in different transactions can be 
   * re-inserted correctly. This provides a safe encapsulation of this 
   * functionality without exposing the ability to recreate folders with 
   * specific IDs (potentially dangerous if abused by other code!) in the
   * public API.
   */
  nsITransaction getRemoveFolderTransaction(in PRInt64 folder);

  /**
   * Convenience function for container services.  Removes
   * all children of the given folder.
+99 −31
Original line number Diff line number Diff line
@@ -283,6 +283,9 @@ nsNavBookmarks::InitTables(mozIStorageConnection* aDBConn)
  // bookmark folders will reference a random folder. This slows down inserts
  // a little bit, which is why we don't always use it, but bookmark folders
  // are not created very often. 
  // NOTE: The folder undo code depends on the autoincrement behavior because
  // it must be able to undo deleting of a folder back to the old folder ID,
  // which we assume has not been taken by something else.
  rv = aDBConn->TableExists(NS_LITERAL_CSTRING("moz_bookmarks_folders"), &exists);
  NS_ENSURE_SUCCESS(rv, rv);
  if (! exists) {
@@ -962,6 +965,22 @@ nsNavBookmarks::ReplaceItem(PRInt64 aFolder, nsIURI *aItem, nsIURI *aNewItem)
NS_IMETHODIMP
nsNavBookmarks::CreateFolder(PRInt64 aParent, const nsAString &aName,
                             PRInt32 aIndex, PRInt64 *aNewFolder)
{
  return CreateFolderWithID(-1, aParent, aName, aIndex, aNewFolder);
}

NS_IMETHODIMP
nsNavBookmarks::CreateContainer(PRInt64 aParent, const nsAString &aName,
                                PRInt32 aIndex, const nsAString &aType,
                                PRInt64 *aNewFolder)
{
  return CreateContainerWithID(-1, aParent, aName, aIndex, aType, aNewFolder);
}

nsresult
nsNavBookmarks::CreateFolderWithID(PRInt64 aFolder, PRInt64 aParent, 
                                   const nsAString& aName, PRInt32 aIndex,
                                   PRInt64* aNewFolder)
{
  mozIStorageConnection *dbConn = DBConn();
  mozStorageTransaction transaction(dbConn, PR_FALSE);
@@ -973,13 +992,24 @@ nsNavBookmarks::CreateFolder(PRInt64 aParent, const nsAString &aName,

  {
    nsCOMPtr<mozIStorageStatement> statement;
    if (aFolder == -1) {
      rv = dbConn->CreateStatement(NS_LITERAL_CSTRING("INSERT INTO moz_bookmarks_folders (name, type) VALUES (?1, null)"),
                                   getter_AddRefs(statement));
      NS_ENSURE_SUCCESS(rv, rv);

      rv = statement->BindStringParameter(0, aName);
      NS_ENSURE_SUCCESS(rv, rv);
    }
    else {
      rv = dbConn->CreateStatement(NS_LITERAL_CSTRING("INSERT INTO moz_bookmarks_folders (id, name, type) VALUES (?1, ?2, null)"),
                                   getter_AddRefs(statement));
      NS_ENSURE_SUCCESS(rv, rv);

      rv = statement->BindInt64Parameter(0, aFolder);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = statement->BindStringParameter(1, aName);
      NS_ENSURE_SUCCESS(rv, rv);
    }
    rv = statement->Execute();
    NS_ENSURE_SUCCESS(rv, rv);
  }
@@ -1009,13 +1039,13 @@ nsNavBookmarks::CreateFolder(PRInt64 aParent, const nsAString &aName,
  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::CreateContainer(PRInt64 aParent, const nsAString &aName,
                                PRInt32 aIndex, const nsAString &aType,
                                PRInt64 *aNewFolder)
nsresult 
nsNavBookmarks::CreateContainerWithID(PRInt64 aFolder, PRInt64 aParent, 
                                      const nsAString &aName, PRInt32 aIndex,
                                      const nsAString &aType, PRInt64 *aNewFolder)
{
  // Containers are wrappers around read-only folders, with a specific type.
  nsresult rv = CreateFolder(aParent, aName, aIndex, aNewFolder);
  nsresult rv = CreateFolderWithID(aFolder, aParent, aName, aIndex, aNewFolder);
  NS_ENSURE_SUCCESS(rv, rv);

  // Set the type.
@@ -1035,7 +1065,6 @@ nsNavBookmarks::CreateContainer(PRInt64 aParent, const nsAString &aName,
  return NS_OK;
}


NS_IMETHODIMP
nsNavBookmarks::InsertSeparator(PRInt64 aParent, PRInt32 aIndex)
{
@@ -1165,6 +1194,31 @@ nsNavBookmarks::RemoveChildAt(PRInt64 aParent, PRInt32 aIndex)
  return NS_OK;
}

nsresult 
nsNavBookmarks::GetParentAndIndexOfFolder(PRInt64 aFolder, PRInt64* aParent, 
                                          PRInt32* aIndex)
{
  nsCAutoString buffer;
  buffer.AssignLiteral("SELECT parent, position FROM moz_bookmarks WHERE folder_child = ");
  buffer.AppendInt(aFolder);

  nsCOMPtr<mozIStorageStatement> statement;
  nsresult rv = DBConn()->CreateStatement(buffer, getter_AddRefs(statement));
  NS_ENSURE_SUCCESS(rv, rv);

  PRBool results;
  rv = statement->ExecuteStep(&results);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!results) {
    return NS_ERROR_INVALID_ARG; // folder is not in the hierarchy
  }

  *aParent = statement->AsInt64(0);
  *aIndex = statement->AsInt32(1);
  
  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::RemoveFolder(PRInt64 aFolder)
{
@@ -1186,32 +1240,16 @@ nsNavBookmarks::RemoveFolder(PRInt64 aFolder)
  mozIStorageConnection *dbConn = DBConn();
  mozStorageTransaction transaction(dbConn, PR_FALSE);

  nsCAutoString buffer;
  buffer.AssignLiteral("SELECT parent, position FROM moz_bookmarks WHERE folder_child = ");
  buffer.AppendInt(aFolder);

  PRInt64 parent;
  PRInt32 index;
  {
    nsCOMPtr<mozIStorageStatement> statement;
    rv = dbConn->CreateStatement(buffer, getter_AddRefs(statement));
  rv = GetParentAndIndexOfFolder(aFolder, &parent, &index);
  NS_ENSURE_SUCCESS(rv, rv);

    PRBool results;
    rv = statement->ExecuteStep(&results);
    NS_ENSURE_SUCCESS(rv, rv);
    if (!results) {
      return NS_ERROR_INVALID_ARG; // folder is not in the hierarchy
    }

    parent = statement->AsInt64(0);
    index = statement->AsInt32(1);
  }

  // Remove all of the folder's children
  RemoveFolderChildren(aFolder);

  // Remove the folder from its parent
  nsCAutoString buffer;
  buffer.AssignLiteral("DELETE FROM moz_bookmarks WHERE folder_child = ");
  buffer.AppendInt(aFolder);
  rv = dbConn->ExecuteSimpleSQL(buffer);
@@ -1235,6 +1273,36 @@ nsNavBookmarks::RemoveFolder(PRInt64 aFolder)
  return NS_OK;
}

NS_IMPL_ISUPPORTS1(nsNavBookmarks::RemoveFolderTransaction, nsITransaction)

NS_IMETHODIMP
nsNavBookmarks::GetRemoveFolderTransaction(PRInt64 aFolder, nsITransaction** aResult)
{
  // Create and initialize a RemoveFolderTransaction object that can be used to
  // recreate the folder safely later. 

  nsAutoString title;
  nsresult rv = GetFolderTitle(aFolder, title);
  NS_ENSURE_SUCCESS(rv, rv);

  PRInt64 parent;
  PRInt32 index;
  rv = GetParentAndIndexOfFolder(aFolder, &parent, &index);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCAutoString type;
  rv = GetFolderType(aFolder, type);
  NS_ENSURE_SUCCESS(rv, rv);

  RemoveFolderTransaction* rft = 
    new RemoveFolderTransaction(aFolder, parent, title, index, type);
  if (!rft)
    return NS_ERROR_OUT_OF_MEMORY;

  NS_ADDREF(*aResult = rft);
  return NS_OK;
}


NS_IMETHODIMP
nsNavBookmarks::RemoveFolderChildren(PRInt64 aFolder)
+63 −0
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@

#include "nsINavBookmarksService.h"
#include "nsIStringBundle.h"
#include "nsITransaction.h"
#include "nsNavHistory.h"
#include "nsNavHistoryResult.h" // need for Int64 hashtable
#include "nsBrowserCompsCID.h"
@@ -81,6 +82,14 @@ public:
                               nsNavHistoryQueryOptions *aOptions,
                               nsCOMArray<nsNavHistoryResultNode> *children);

  // If aFolder is -1, use the autoincrement id for folder index. 
  nsresult CreateFolderWithID(PRInt64 aFolder, PRInt64 aParent, 
                              const nsAString& title, PRInt32 aIndex,
                              PRInt64* aNewFolder);
  nsresult CreateContainerWithID(PRInt64 aFolder, PRInt64 aParent, 
                                 const nsAString& title, PRInt32 aIndex, 
                                 const nsAString& type, PRInt64* aNewFolder);

  // Returns a statement to get information about a folder id
  mozIStorageStatement* DBGetFolderInfo() { return mDBGetFolderInfo; }
  // constants for the above statement
@@ -132,6 +141,10 @@ private:
  nsresult RecursiveAddBookmarkHash(PRInt64 aBookmarkId, PRInt64 aCurrentSource,
                                    PRTime aMinTime);
  nsresult UpdateBookmarkHashOnRemove(PRInt64 aBookmarkId);

  nsresult GetParentAndIndexOfFolder(PRInt64 aFolder, PRInt64* aParent, 
                                     PRInt32* aIndex);

  nsresult IsBookmarkedInDatabase(PRInt64 aBookmarkID, PRBool* aIsBookmarked);

  nsCOMPtr<mozIStorageStatement> mDBGetFolderInfo;    // kGetFolderInfoIndex_* results
@@ -162,6 +175,56 @@ private:

  nsCOMPtr<nsIStringBundle> mBundle;

  class RemoveFolderTransaction : public nsITransaction {
  public:
    RemoveFolderTransaction(PRInt64 aID, PRInt64 aParent, 
                            const nsAString& aTitle, PRInt32 aIndex,
                            const nsACString& aType) 
                            : mID(aID),
                              mParent(aParent),
                              mIndex(aIndex){
      mTitle = aTitle;
      mType = aType;
    }
    
    NS_DECL_ISUPPORTS

    NS_IMETHOD DoTransaction() {
      nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
      return bookmarks->RemoveFolder(mID);
    }

    NS_IMETHOD UndoTransaction() {
      nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
      PRInt64 newFolder;
      if (mType.IsEmpty())
        return bookmarks->CreateFolderWithID(mID, mParent, mTitle, mIndex, &newFolder);
      nsAutoString type; type.AssignWithConversion(mType);
      return bookmarks->CreateContainerWithID(mID, mParent, mTitle, mIndex, type, &newFolder); 
    }

    NS_IMETHOD RedoTransaction() {
      return DoTransaction();
    }

    NS_IMETHOD GetIsTransient(PRBool* aResult) {
      *aResult = PR_FALSE;
      return NS_OK;
    }
    
    NS_IMETHOD Merge(nsITransaction* aTransaction, PRBool* aResult) {
      *aResult = PR_FALSE;
      return NS_OK;
    }

  private:
    PRInt64 mID;
    PRInt64 mParent;
    nsString mTitle;
    nsCString mType;
    PRInt32 mIndex;
  };

  // in nsBookmarksHTML
  nsresult ImportBookmarksHTMLInternal(nsIURI* aURL,
                                       PRBool aAllowRootChanges,