Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/components/places/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 114 kB image not shown  

SSL Bookmarks.sys.mjs   Interaktion und
Portierbarkeitunbekannt

 
/* 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 module provides an asynchronous API for managing bookmarks.
 *
 * Bookmarks are organized in a tree structure, and include URLs, folders and
 * separators.  Multiple bookmarks for the same URL are allowed.
 *
 * Note that if you are handling bookmarks operations in the UI, you should
 * not use this API directly, but rather use PlacesTransactions, so that
 * any operation is undo/redo-able.
 *
 * Each bookmark-item is represented by an object having the following
 * properties:
 *
 *  - guid (string)
 *      The globally unique identifier of the item.
 *  - parentGuid (string)
 *      The globally unique identifier of the folder containing the item.
 *      This will be an empty string for the Places root folder.
 *  - index (number)
 *      The 0-based position of the item in the parent folder.
 *  - dateAdded (Date)
 *      The time at which the item was added.
 *  - lastModified (Date)
 *      The time at which the item was last modified.
 *  - type (number)
 *      The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR.
 *
 *  The following properties are only valid for URLs or folders.
 *
 *  - title (string)
 *      The item's title, if any.  Empty titles and null titles are considered
 *      the same. Titles longer than DB_TITLE_LENGTH_MAX will be truncated.
 *
 *  The following properties are only valid for URLs:
 *
 *  - url (URL, href or nsIURI)
 *      The item's URL.  Note that while input objects can contains either
 *      an URL object, an href string, or an nsIURI, output objects will always
 *      contain an URL object.
 *      An URL cannot be longer than DB_URL_LENGTH_MAX, methods will throw if a
 *      longer value is provided.
 *
 * Each successful operation notifies through the PlacesObservers
 * interface.  To listen to such notifications you must register using
 * PlacesUtils.observers.addListener and PlacesUtils.observers.removeListener
 * methods.
 * Note that bookmark addition or order changes won't notify bookmark-moved for
 * items that have their indexes changed.
 * Similarly, lastModified changes not done explicitly (like changing another
 * property) won't fire a bookmark-time-changed notification.
 * @see PlacesObservers
 */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
  PlacesSyncUtils: "resource://gre/modules/PlacesSyncUtils.sys.mjs",
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});

const MATCH_ANYWHERE_UNMODIFIED =
  Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;

export var Bookmarks = Object.freeze({
  /**
   * Item's type constants.
   * These should stay consistent with nsINavBookmarksService.idl
   */
  TYPE_BOOKMARK: 1,
  TYPE_FOLDER: 2,
  TYPE_SEPARATOR: 3,

  /**
   * Sync status constants, stored for each item.
   */
  SYNC_STATUS: {
    UNKNOWN: Ci.nsINavBookmarksService.SYNC_STATUS_UNKNOWN,
    NEW: Ci.nsINavBookmarksService.SYNC_STATUS_NEW,
    NORMAL: Ci.nsINavBookmarksService.SYNC_STATUS_NORMAL,
  },

  /**
   * Default index used to append a bookmark-item at the end of a folder.
   * This should stay consistent with nsINavBookmarksService.idl
   */
  DEFAULT_INDEX: -1,

  /**
   * Maximum length of a tag.
   * Any tag above this length is rejected.
   */
  MAX_TAG_LENGTH: 100,

  /**
   * Bookmark change source constants, passed as optional properties and
   * forwarded to observers. See nsINavBookmarksService.idl for an explanation.
   */
  SOURCES: {
    DEFAULT: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
    SYNC: Ci.nsINavBookmarksService.SOURCE_SYNC,
    IMPORT: Ci.nsINavBookmarksService.SOURCE_IMPORT,
    SYNC_REPARENT_REMOVED_FOLDER_CHILDREN:
      Ci.nsINavBookmarksService.SOURCE_SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
    RESTORE: Ci.nsINavBookmarksService.SOURCE_RESTORE,
    RESTORE_ON_STARTUP: Ci.nsINavBookmarksService.SOURCE_RESTORE_ON_STARTUP,
  },

  /**
   * Special GUIDs associated with bookmark roots.
   * It's guaranteed that the roots will always have these guids.
   */
  rootGuid: "root________",
  menuGuid: "menu________",
  toolbarGuid: "toolbar_____",
  unfiledGuid: "unfiled_____",
  mobileGuid: "mobile______",

  // With bug 424160, tags will stop being bookmarks, thus this root will
  // be removed.  Do not rely on this, rather use the tagging service API.
  tagsGuid: "tags________",

  /**
   * The GUIDs of the user content root folders that we support, for easy access
   * as a set.
   */
  userContentRoots: [
    "toolbar_____",
    "menu________",
    "unfiled_____",
    "mobile______",
  ],

  /**
   * GUID associated with bookmarks that haven't been saved to the database yet.
   */
  unsavedGuid: "new_________",

  /**
   * GUIDs associated with virtual queries that are used for displaying bookmark
   * folders in the left pane.
   */
  virtualMenuGuid: "menu_______v",
  virtualToolbarGuid: "toolbar____v",
  virtualUnfiledGuid: "unfiled____v",
  virtualMobileGuid: "mobile_____v",

  /**
   * Checks if a guid is a virtual root.
   *
   * @param {String} guid The guid of the item to look for.
   * @returns {Boolean} true if guid is a virtual root, false otherwise.
   */
  isVirtualRootItem(guid) {
    return (
      guid == lazy.PlacesUtils.bookmarks.virtualMenuGuid ||
      guid == lazy.PlacesUtils.bookmarks.virtualToolbarGuid ||
      guid == lazy.PlacesUtils.bookmarks.virtualUnfiledGuid ||
      guid == lazy.PlacesUtils.bookmarks.virtualMobileGuid
    );
  },

  /**
   * Returns the title to use on the UI for a bookmark item. Root folders
   * in the database don't store fully localised versions of the title. To
   * get those this function should be called.
   *
   * Hence, this function should only be called if a root folder object is
   * likely to be displayed to the user.
   *
   * @param {Object} info An object representing a bookmark-item.
   * @returns {String} The correct string.
   * @throws {Error} If the guid in PlacesUtils.bookmarks.userContentRoots is
   *                 not supported.
   */
  getLocalizedTitle(info) {
    if (!lazy.PlacesUtils.bookmarks.userContentRoots.includes(info.guid)) {
      return info.title;
    }

    switch (info.guid) {
      case lazy.PlacesUtils.bookmarks.toolbarGuid:
        return lazy.PlacesUtils.getString("BookmarksToolbarFolderTitle");
      case lazy.PlacesUtils.bookmarks.menuGuid:
        return lazy.PlacesUtils.getString("BookmarksMenuFolderTitle");
      case lazy.PlacesUtils.bookmarks.unfiledGuid:
        return lazy.PlacesUtils.getString("OtherBookmarksFolderTitle");
      case lazy.PlacesUtils.bookmarks.mobileGuid:
        return lazy.PlacesUtils.getString("MobileBookmarksFolderTitle");
      default:
        throw new Error(
          `Unsupported guid ${info.guid} passed to getLocalizedTitle!`
        );
    }
  },

  /**
   * Inserts a bookmark-item into the bookmarks tree.
   *
   * For creating a bookmark, the following set of properties is required:
   *  - type
   *  - parentGuid
   *  - url, only for bookmarked URLs
   *
   * If an index is not specified, it defaults to appending.
   * It's also possible to pass a non-existent GUID to force creation of an
   * item with the given GUID, but unless you have a very sound reason, such as
   * an undo manager implementation or synchronization, don't do that.
   *
   * Note that any known properties that don't apply to the specific item type
   * cause an exception.
   *
   * @param info
   *        object representing a bookmark-item.
   *
   * @return {Promise} resolved when the creation is complete.
   * @resolves to an object representing the created bookmark.
   * @rejects if it's not possible to create the requested bookmark.
   * @throws if the arguments are invalid.
   */
  insert(info) {
    let now = new Date();
    let addedTime = (info && info.dateAdded) || now;
    let modTime = addedTime;
    if (addedTime > now) {
      modTime = now;
    }
    let insertInfo = validateBookmarkObject("Bookmarks.sys.mjs: insert", info, {
      type: { defaultValue: this.TYPE_BOOKMARK },
      index: { defaultValue: this.DEFAULT_INDEX },
      url: {
        requiredIf: b => b.type == this.TYPE_BOOKMARK,
        validIf: b => b.type == this.TYPE_BOOKMARK,
      },
      parentGuid: {
        required: true,
        // Inserting into the root folder is not allowed unless it's testing.
        validIf: b =>
          lazy.PlacesUtils.isInAutomation || b.parentGuid != this.rootGuid,
      },
      title: {
        defaultValue: "",
        validIf: b =>
          b.type == this.TYPE_BOOKMARK ||
          b.type == this.TYPE_FOLDER ||
          b.title === "",
      },
      dateAdded: { defaultValue: addedTime },
      lastModified: {
        defaultValue: modTime,
        validIf: b =>
          b.lastModified >= now ||
          (b.dateAdded && b.lastModified >= b.dateAdded),
      },
      source: { defaultValue: this.SOURCES.DEFAULT },
    });

    return (async () => {
      // Ensure the parent exists.
      let parent = await fetchBookmark({ guid: insertInfo.parentGuid });
      if (!parent) {
        throw new Error("parentGuid must be valid");
      }

      // Set index in the appending case.
      if (
        insertInfo.index == this.DEFAULT_INDEX ||
        insertInfo.index > parent._childCount
      ) {
        insertInfo.index = parent._childCount;
      }

      let item = await insertBookmark(insertInfo, parent);
      let itemDetailMap = await getBookmarkDetailMap([item.guid]);
      let itemDetail = itemDetailMap.get(item.guid);

      // Pass tagging information for the observers to skip over these notifications when needed.
      let isTagging = parent._parentId == lazy.PlacesUtils.tagsFolderId;
      let isTagsFolder = parent._id == lazy.PlacesUtils.tagsFolderId;
      let url = "";
      if (item.type == Bookmarks.TYPE_BOOKMARK) {
        url = item.url.href;
      }

      const notifications = [
        new PlacesBookmarkAddition({
          id: itemDetail.id,
          url,
          itemType: item.type,
          parentId: parent._id,
          index: item.index,
          title: item.title,
          dateAdded: item.dateAdded,
          guid: item.guid,
          parentGuid: item.parentGuid,
          source: item.source,
          isTagging: isTagging || isTagsFolder,
          tags: itemDetail.tags,
          frecency: itemDetail.frecency,
          hidden: itemDetail.hidden,
          visitCount: itemDetail.visitCount,
          lastVisitDate: itemDetail.lastVisitDate,
          targetFolderGuid: itemDetail.targetFolderGuid,
          targetFolderItemId: itemDetail.targetFolderItemId,
          targetFolderTitle: itemDetail.targetFolderTitle,
        }),
      ];

      // If it's a tag, notify bookmark-tags-changed event to all bookmarks for this URL.
      if (isTagging) {
        for (let entry of await fetchBookmarksByURL(item, {
          concurrent: true,
        })) {
          notifications.push(
            new PlacesBookmarkTags({
              id: entry._id,
              itemType: entry.type,
              url,
              guid: entry.guid,
              parentGuid: entry.parentGuid,
              tags: entry._tags,
              lastModified: entry.lastModified,
              source: item.source,
              isTagging: false,
            })
          );
        }
      }

      PlacesObservers.notifyListeners(notifications);

      // Remove non-enumerable properties.
      delete item.source;
      return Object.assign({}, item);
    })();
  },

  /**
   * Inserts a bookmark-tree into the existing bookmarks tree.
   *
   * All the specified folders and bookmarks will be inserted as new, even
   * if duplicates. There's no merge support at this time.
   *
   * The input should be of the form:
   * {
   *   guid: "<some-existing-guid-to-use-as-parent>",
   *   source: "<some valid source>", (optional)
   *   children: [
   *     ... valid bookmark objects.
   *   ]
   * }
   *
   * Children will be appended to any existing children of the parent
   * that is specified. The source specified on the root of the tree
   * will be used for all the items inserted. Any indices or custom parentGuids
   * set on children will be ignored and overwritten.
   *
   * @param {Object} tree
   *        object representing a tree of bookmark items to insert.
   * @param {Object} options [optional]
   *        object with properties representing options.  Current options are:
   *         - fixupOrSkipInvalidEntries: makes the insert more lenient to
   *           mistakes in the input tree.  Properties of an entry that are
   *           fixable will be corrected, otherwise the entry will be skipped.
   *           This is particularly convenient for import/restore operations,
   *           but should not be abused for common inserts, since it may hide
   *           bugs in the calling code.
   *
   * @return {Promise} resolved when the creation is complete.
   * @resolves to an array of objects representing the created bookmark(s).
   * @rejects if it's not possible to create the requested bookmark.
   * @throws if the arguments are invalid.
   */
  insertTree(tree, options) {
    if (!tree || typeof tree != "object") {
      throw new Error("Should be provided a valid tree object.");
    }
    if (!Array.isArray(tree.children) || !tree.children.length) {
      throw new Error("Should have a non-zero number of children to insert.");
    }
    if (!lazy.PlacesUtils.isValidGuid(tree.guid)) {
      throw new Error(
        `The parent guid is not valid (${tree.guid} ${tree.title}).`
      );
    }
    if (tree.guid == this.rootGuid) {
      throw new Error("Can't insert into the root.");
    }
    if (tree.guid == this.tagsGuid) {
      throw new Error("Can't use insertTree to insert tags.");
    }
    if (
      tree.hasOwnProperty("source") &&
      !Object.values(this.SOURCES).includes(tree.source)
    ) {
      throw new Error("Can't use source value " + tree.source);
    }
    if (options && typeof options != "object") {
      throw new Error("Options should be a valid object");
    }
    let fixupOrSkipInvalidEntries =
      options && !!options.fixupOrSkipInvalidEntries;

    // Serialize the tree into an array of items to insert into the db.
    let insertInfos = [];
    let urlsThatMightNeedPlaces = [];

    // We want to use the same 'last added' time for all the entries
    // we import (so they won't differ by a few ms based on where
    // they are in the tree, and so we don't needlessly construct
    // multiple dates).
    let fallbackLastAdded = new Date();

    const { TYPE_BOOKMARK, TYPE_FOLDER, SOURCES } = this;

    // Reuse the 'source' property for all the entries.
    let source = tree.source || SOURCES.DEFAULT;

    // This is recursive.
    function appendInsertionInfoForInfoArray(infos, indexToUse, parentGuid) {
      // We want to keep the index of items that will be inserted into the root
      // NULL, and then use a subquery to select the right index, to avoid
      // races where other consumers might add items between when we determine
      // the index and when we insert. However, the validator does not allow
      // NULL values in in the index, so we fake it while validating and then
      // correct later. Keep track of whether we're doing this:
      let shouldUseNullIndices = false;
      if (indexToUse === null) {
        shouldUseNullIndices = true;
        indexToUse = 0;
      }

      // When a folder gets an item added, its last modified date is updated
      // to be equal to the date we added the item (if that date is newer).
      // Because we're inserting a tree, we keep track of this date for the
      // loop, updating it for inserted items as well as from any subfolders
      // we insert.
      let lastAddedForParent = new Date(0);
      for (let info of infos) {
        // Ensure to use the same date for dateAdded and lastModified, even if
        // dateAdded may be imposed by the caller.
        let time = (info && info.dateAdded) || fallbackLastAdded;
        let insertInfo = {
          guid: { defaultValue: lazy.PlacesUtils.history.makeGuid() },
          type: { defaultValue: TYPE_BOOKMARK },
          url: {
            requiredIf: b => b.type == TYPE_BOOKMARK,
            validIf: b => b.type == TYPE_BOOKMARK,
          },
          parentGuid: { replaceWith: parentGuid }, // Set the correct parent guid.
          title: {
            defaultValue: "",
            validIf: b =>
              b.type == TYPE_BOOKMARK ||
              b.type == TYPE_FOLDER ||
              b.title === "",
          },
          dateAdded: {
            defaultValue: time,
            validIf: b => !b.lastModified || b.dateAdded <= b.lastModified,
          },
          lastModified: {
            defaultValue: time,
            validIf: b =>
              (!b.dateAdded && b.lastModified >= time) ||
              (b.dateAdded && b.lastModified >= b.dateAdded),
          },
          index: { replaceWith: indexToUse++ },
          source: { replaceWith: source },
          keyword: { validIf: b => b.type == TYPE_BOOKMARK },
          charset: { validIf: b => b.type == TYPE_BOOKMARK },
          postData: { validIf: b => b.type == TYPE_BOOKMARK },
          tags: { validIf: b => b.type == TYPE_BOOKMARK },
          children: {
            validIf: b => b.type == TYPE_FOLDER && Array.isArray(b.children),
          },
        };
        if (fixupOrSkipInvalidEntries) {
          insertInfo.guid.fixup = b =>
            (b.guid = lazy.PlacesUtils.history.makeGuid());
          insertInfo.dateAdded.fixup = insertInfo.lastModified.fixup = b =>
            (b.lastModified = b.dateAdded = fallbackLastAdded);
        }
        try {
          insertInfo = validateBookmarkObject(
            "Bookmarks.sys.mjs: insertTree",
            info,
            insertInfo
          );
        } catch (ex) {
          if (fixupOrSkipInvalidEntries) {
            indexToUse--;
            continue;
          } else {
            throw ex;
          }
        }

        if (shouldUseNullIndices) {
          insertInfo.index = null;
        }
        // Store the URL if this is a bookmark, so we can ensure we create an
        // entry in moz_places for it.
        if (insertInfo.type == Bookmarks.TYPE_BOOKMARK) {
          urlsThatMightNeedPlaces.push(insertInfo.url);
        }

        insertInfos.push(insertInfo);
        // Process any children. We have to use info.children here rather than
        // insertInfo.children because validateBookmarkObject doesn't copy over
        // the children ref, as the default bookmark validators object doesn't
        // know about children.
        if (info.children) {
          // start children of this item off at index 0.
          let childrenLastAdded = appendInsertionInfoForInfoArray(
            info.children,
            0,
            insertInfo.guid
          );
          if (childrenLastAdded > insertInfo.lastModified) {
            insertInfo.lastModified = childrenLastAdded;
          }
          if (childrenLastAdded > lastAddedForParent) {
            lastAddedForParent = childrenLastAdded;
          }
        }

        // Ensure we track what time to update the parent to.
        if (insertInfo.dateAdded > lastAddedForParent) {
          lastAddedForParent = insertInfo.dateAdded;
        }
      }
      return lastAddedForParent;
    }

    // We want to validate synchronously, but we can't know the index at which
    // we're inserting into the parent. We just use NULL instead,
    // and the SQL query with which we insert will update it as necessary.
    let lastAddedForParent = appendInsertionInfoForInfoArray(
      tree.children,
      null,
      tree.guid
    );

    // appendInsertionInfoForInfoArray will remove invalid items and may leave
    // us with nothing to insert, if so, just return early.
    if (!insertInfos.length) {
      return [];
    }

    return (async function () {
      let treeParent = await fetchBookmark({ guid: tree.guid });
      if (!treeParent) {
        throw new Error("The parent you specified doesn't exist.");
      }

      if (treeParent._parentId == lazy.PlacesUtils.tagsFolderId) {
        throw new Error("Can't use insertTree to insert tags.");
      }

      await insertBookmarkTree(
        insertInfos,
        source,
        treeParent,
        urlsThatMightNeedPlaces,
        lastAddedForParent
      );

      // Now update the indices of root items in the objects we return.
      // These may be wrong if someone else modified the table between
      // when we fetched the parent and inserted our items, but the actual
      // inserts will have been correct, and we don't want to query the DB
      // again if we don't have to. bug 1347230 covers improving this.
      let rootIndex = treeParent._childCount;
      for (let insertInfo of insertInfos) {
        if (insertInfo.parentGuid == tree.guid) {
          insertInfo.index += rootIndex++;
        }
      }

      let itemDetailMap = await getBookmarkDetailMap(
        insertInfos.map(info => info.guid)
      );

      let notifications = [];
      for (let i = 0; i < insertInfos.length; i++) {
        let item = insertInfos[i];
        let itemDetail = itemDetailMap.get(item.guid);

        // For sub-folders, we need to make sure their children have the correct parent ids.
        let parentId;
        if (item.parentGuid === treeParent.guid) {
          // This is a direct child of the tree parent, so we can use the
          // existing parent's id.
          parentId = treeParent._id;
        } else {
          // This is a parent folder that's been updated, so we need to
          // use the new item id.
          parentId = itemDetail.parentId;
        }

        let url = "";
        if (item.type == Bookmarks.TYPE_BOOKMARK) {
          url = URL.isInstance(item.url) ? item.url.href : item.url;
        }

        notifications.push(
          new PlacesBookmarkAddition({
            id: itemDetail.id,
            url,
            itemType: item.type,
            parentId,
            index: item.index,
            title: item.title,
            dateAdded: item.dateAdded,
            guid: item.guid,
            parentGuid: item.parentGuid,
            source: item.source,
            isTagging: false,
            tags: itemDetail.tags,
            frecency: itemDetail.frecency,
            hidden: itemDetail.hidden,
            visitCount: itemDetail.visitCount,
            lastVisitDate: itemDetail.lastVisitDate,
            targetFolderGuid: itemDetail.targetFolderGuid,
            targetFolderItemId: itemDetail.targetFolderItemId,
            targetFolderTitle: itemDetail.targetFolderTitle,
          })
        );

        try {
          await handleBookmarkItemSpecialData(itemDetail.id, item);
        } catch (ex) {
          // This is not critical, regardless the bookmark has been created
          // and we should continue notifying the next ones.
          console.error(
            "An error occured while handling special bookmark data:",
            ex
          );
        }

        // Remove non-enumerable properties.
        delete item.source;

        insertInfos[i] = Object.assign({}, item);
      }

      if (notifications.length) {
        PlacesObservers.notifyListeners(notifications);
      }

      return insertInfos;
    })();
  },

  /**
   * Updates a bookmark-item.
   *
   * Only set the properties which should be changed (undefined properties
   * won't be taken into account).
   * Moreover, the item's type or dateAdded cannot be changed, since they are
   * immutable after creation.  Trying to change them will reject.
   *
   * Note that any known properties that don't apply to the specific item type
   * cause an exception.
   *
   * @param info
   *        object representing a bookmark-item, as defined above.
   *
   * @return {Promise} resolved when the update is complete.
   * @resolves to an object representing the updated bookmark.
   * @rejects if it's not possible to update the given bookmark.
   * @throws if the arguments are invalid.
   */
  update(info) {
    // The info object is first validated here to ensure it's consistent, then
    // it's compared to the existing item to remove any properties that don't
    // need to be updated.
    let updateInfo = validateBookmarkObject("Bookmarks.sys.mjs: update", info, {
      guid: { required: true },
      index: {
        requiredIf: b => b.hasOwnProperty("parentGuid"),
        validIf: b => b.index >= 0 || b.index == this.DEFAULT_INDEX,
      },
      parentGuid: { validIf: b => b.parentGuid != this.rootGuid },
      source: { defaultValue: this.SOURCES.DEFAULT },
    });

    // There should be at last one more property in addition to guid and source.
    if (Object.keys(updateInfo).length < 3) {
      throw new Error("Not enough properties to update");
    }

    return (async () => {
      // Ensure the item exists.
      let item = await fetchBookmark(updateInfo);
      if (!item) {
        throw new Error("No bookmarks found for the provided GUID");
      }
      if (updateInfo.hasOwnProperty("type") && updateInfo.type != item.type) {
        throw new Error("The bookmark type cannot be changed");
      }

      // Remove any property that will stay the same.
      removeSameValueProperties(updateInfo, item);
      // Check if anything should still be updated.
      if (Object.keys(updateInfo).length < 3) {
        // Remove non-enumerable properties.
        return Object.assign({}, item);
      }
      const now = new Date();
      let lastModifiedDefault = now;
      // In the case where `dateAdded` is specified, but `lastModified` is not,
      // we only update `lastModified` if it is older than the new `dateAdded`.
      if (!("lastModified" in updateInfo) && "dateAdded" in updateInfo) {
        lastModifiedDefault = new Date(
          Math.max(item.lastModified, updateInfo.dateAdded)
        );
      }
      updateInfo = validateBookmarkObject(
        "Bookmarks.sys.mjs: update",
        updateInfo,
        {
          url: { validIf: () => item.type == this.TYPE_BOOKMARK },
          title: {
            validIf: () =>
              [this.TYPE_BOOKMARK, this.TYPE_FOLDER].includes(item.type),
          },
          lastModified: {
            defaultValue: lastModifiedDefault,
            validIf: b =>
              b.lastModified >= now ||
              b.lastModified >= (b.dateAdded || item.dateAdded),
          },
          dateAdded: { defaultValue: item.dateAdded },
        }
      );

      return lazy.PlacesUtils.withConnectionWrapper(
        "Bookmarks.sys.mjs: update",
        async db => {
          let parent;
          if (updateInfo.hasOwnProperty("parentGuid")) {
            if (lazy.PlacesUtils.isRootItem(item.guid)) {
              throw new Error("It's not possible to move Places root folders.");
            }
            if (item.type == this.TYPE_FOLDER) {
              // Make sure we are not moving a folder into itself or one of its
              // descendants.
              let rows = await db.executeCached(
                `WITH RECURSIVE
               descendants(did) AS (
                 VALUES(:id)
                 UNION ALL
                 SELECT id FROM moz_bookmarks
                 JOIN descendants ON parent = did
                 WHERE type = :type
               )
               SELECT guid FROM moz_bookmarks
               WHERE id IN descendants
              `,
                { id: item._id, type: this.TYPE_FOLDER }
              );
              if (
                rows
                  .map(r => r.getResultByName("guid"))
                  .includes(updateInfo.parentGuid)
              ) {
                throw new Error(
                  "Cannot insert a folder into itself or one of its descendants"
                );
              }
            }

            parent = await fetchBookmark({ guid: updateInfo.parentGuid });
            if (!parent) {
              throw new Error("No bookmarks found for the provided parentGuid");
            }
          }

          if (updateInfo.hasOwnProperty("index")) {
            if (lazy.PlacesUtils.isRootItem(item.guid)) {
              throw new Error("It's not possible to move Places root folders.");
            }
            // If at this point we don't have a parent yet, we are moving into
            // the same container.  Thus we know it exists.
            if (!parent) {
              parent = await fetchBookmark({ guid: item.parentGuid });
            }

            if (
              updateInfo.index >= parent._childCount ||
              updateInfo.index == this.DEFAULT_INDEX
            ) {
              updateInfo.index = parent._childCount;

              // Fix the index when moving within the same container.
              if (parent.guid == item.parentGuid) {
                updateInfo.index--;
              }
            }
          }

          let syncChangeDelta =
            lazy.PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
              info.source
            );

          let updatedItem = await db.executeTransaction(async function () {
            let updatedItem = await updateBookmark(
              db,
              updateInfo,
              item,
              item.index,
              parent,
              syncChangeDelta
            );
            if (parent) {
              await setAncestorsLastModified(
                db,
                parent.guid,
                updatedItem.lastModified,
                syncChangeDelta
              );
            }
            return updatedItem;
          });

          const notifications = [];

          // For lastModified, we only care about the original input, since we
          // should not notify implicit lastModified changes.
          if (
            (info.hasOwnProperty("lastModified") &&
              updateInfo.hasOwnProperty("lastModified") &&
              item.lastModified != updatedItem.lastModified) ||
            (info.hasOwnProperty("dateAdded") &&
              updateInfo.hasOwnProperty("dateAdded") &&
              item.dateAdded != updatedItem.dateAdded)
          ) {
            let isTagging = updatedItem.parentGuid == Bookmarks.tagsGuid;
            if (!isTagging) {
              if (!parent) {
                parent = await fetchBookmark({ guid: updatedItem.parentGuid });
              }
              isTagging = parent.parentGuid === Bookmarks.tagsGuid;
            }

            notifications.push(
              new PlacesBookmarkTime({
                id: updatedItem._id,
                itemType: updatedItem.type,
                url: updatedItem.url?.href,
                guid: updatedItem.guid,
                parentGuid: updatedItem.parentGuid,
                dateAdded: updatedItem.dateAdded,
                lastModified: updatedItem.lastModified,
                source: updatedItem.source,
                isTagging,
              })
            );
          }

          if (updateInfo.hasOwnProperty("title")) {
            let isTagging = updatedItem.parentGuid == Bookmarks.tagsGuid;
            if (!isTagging) {
              if (!parent) {
                parent = await fetchBookmark({ guid: updatedItem.parentGuid });
              }
              isTagging = parent.parentGuid === Bookmarks.tagsGuid;
            }

            notifications.push(
              new PlacesBookmarkTitle({
                id: updatedItem._id,
                itemType: updatedItem.type,
                url: updatedItem.url?.href,
                guid: updatedItem.guid,
                parentGuid: updatedItem.parentGuid,
                title: updatedItem.title,
                lastModified: updatedItem.lastModified,
                source: updatedItem.source,
                isTagging,
              })
            );

            // If we're updating a tag, we must notify all the tagged bookmarks
            // about the change.
            if (isTagging) {
              for (let entry of await fetchBookmarksByTags(
                { tags: [updatedItem.title] },
                { concurrent: true }
              )) {
                notifications.push(
                  new PlacesBookmarkTags({
                    id: entry._id,
                    itemType: entry.type,
                    url: entry.url,
                    guid: entry.guid,
                    parentGuid: entry.parentGuid,
                    tags: entry._tags,
                    lastModified: entry.lastModified,
                    source: updatedItem.source,
                    isTagging: false,
                  })
                );
              }
            }
          }
          if (updateInfo.hasOwnProperty("url")) {
            await lazy.PlacesUtils.keywords.reassign(
              item.url,
              updatedItem.url,
              updatedItem.source
            );

            let isTagging = updatedItem.parentGuid == Bookmarks.tagsGuid;
            if (!isTagging) {
              if (!parent) {
                parent = await fetchBookmark({ guid: updatedItem.parentGuid });
              }
              isTagging = parent.parentGuid === Bookmarks.tagsGuid;
            }

            notifications.push(
              new PlacesBookmarkUrl({
                id: updatedItem._id,
                itemType: updatedItem.type,
                url: updatedItem.url.href,
                guid: updatedItem.guid,
                parentGuid: updatedItem.parentGuid,
                source: updatedItem.source,
                isTagging,
                lastModified: updatedItem.lastModified,
              })
            );
          }
          // If the item was moved, notify bookmark-moved.
          if (
            item.parentGuid != updatedItem.parentGuid ||
            item.index != updatedItem.index
          ) {
            let details = (await getBookmarkDetailMap([updatedItem.guid])).get(
              updatedItem.guid
            );
            notifications.push(
              new PlacesBookmarkMoved({
                id: updatedItem._id,
                itemType: updatedItem.type,
                url: updatedItem.url && updatedItem.url.href,
                guid: updatedItem.guid,
                parentGuid: updatedItem.parentGuid,
                source: updatedItem.source,
                index: updatedItem.index,
                oldParentGuid: item.parentGuid,
                oldIndex: item.index,
                isTagging:
                  updatedItem.parentGuid === Bookmarks.tagsGuid ||
                  parent.parentGuid === Bookmarks.tagsGuid,
                title: updatedItem.title,
                tags: details.tags,
                frecency: details.frecency,
                hidden: details.hidden,
                visitCount: details.visitCount,
                dateAdded: updatedItem.dateAdded ?? Date.now(),
                lastVisitDate: details.lastVisitDate,
              })
            );
          }

          if (notifications.length) {
            PlacesObservers.notifyListeners(notifications);
          }

          // Remove non-enumerable properties.
          delete updatedItem.source;
          return Object.assign({}, updatedItem);
        }
      );
    })();
  },

  /**
   * Moves multiple bookmark-items to a specific folder.
   *
   * If you are only updating/moving a single bookmark, use update() instead.
   *
   * @param {Array} guids
   *        An array of GUIDs representing the bookmarks to move.
   * @param {String} parentGuid
   *        Optional, the parent GUID to move the bookmarks to.
   * @param {Integer} index
   *        The index to move the bookmarks to. If this is -1, the bookmarks
   *        will be appended to the folder.
   * @param {Integer} source
   *        One of the Bookmarks.SOURCES.* options, representing the source of
   *        this change.
   *
   * @return {Promise} resolved when the move is complete.
   * @resolves to an array of objects representing the moved bookmarks.
   * @rejects if it's not possible to move the given bookmark(s).
   * @throws if the arguments are invalid.
   */
  moveToFolder(guids, parentGuid, index, source) {
    if (!Array.isArray(guids) || guids.length < 1) {
      throw new Error("guids should be an array of at least one item");
    }
    if (!guids.every(guid => lazy.PlacesUtils.isValidGuid(guid))) {
      throw new Error("Expected only valid GUIDs to be passed.");
    }
    if (parentGuid && !lazy.PlacesUtils.isValidGuid(parentGuid)) {
      throw new Error("parentGuid should be a valid GUID");
    }
    if (parentGuid == lazy.PlacesUtils.bookmarks.rootGuid) {
      throw new Error("Cannot move bookmarks into root.");
    }
    if (typeof index != "number" || index < this.DEFAULT_INDEX) {
      throw new Error(
        `index should be a number greater than ${this.DEFAULT_INDEX}`
      );
    }

    if (!source) {
      source = this.SOURCES.DEFAULT;
    }

    return (async () => {
      let updateInfos = [];
      let syncChangeDelta =
        lazy.PlacesSyncUtils.bookmarks.determineSyncChangeDelta(source);

      await lazy.PlacesUtils.withConnectionWrapper(
        "Bookmarks.sys.mjs: moveToFolder",
        async db => {
          const lastModified = new Date();

          let targetParentGuid = parentGuid || undefined;

          for (let guid of guids) {
            // Ensure the item exists.
            let existingItem = await fetchBookmark({ guid }, { db });
            if (!existingItem) {
              throw new Error("No bookmarks found for the provided GUID");
            }

            if (parentGuid) {
              // We're moving to a different folder.
              if (existingItem.type == this.TYPE_FOLDER) {
                // Make sure we are not moving a folder into itself or one of its
                // descendants.
                let rows = await db.executeCached(
                  `WITH RECURSIVE
                 descendants(did) AS (
                   VALUES(:id)
                   UNION ALL
                   SELECT id FROM moz_bookmarks
                   JOIN descendants ON parent = did
                   WHERE type = :type
                 )
                 SELECT guid FROM moz_bookmarks
                 WHERE id IN descendants
                `,
                  { id: existingItem._id, type: this.TYPE_FOLDER }
                );
                if (
                  rows.map(r => r.getResultByName("guid")).includes(parentGuid)
                ) {
                  throw new Error(
                    "Cannot insert a folder into itself or one of its descendants"
                  );
                }
              }
            } else if (!targetParentGuid) {
              targetParentGuid = existingItem.parentGuid;
            } else if (existingItem.parentGuid != targetParentGuid) {
              throw new Error(
                "All bookmarks should be in the same folder if no parent is specified"
              );
            }

            updateInfos.push({ existingItem, currIndex: existingItem.index });
          }

          let newParent = await fetchBookmark(
            { guid: targetParentGuid },
            { db }
          );

          if (newParent._grandParentId == lazy.PlacesUtils.tagsFolderId) {
            throw new Error("Can't move to a tags folder");
          }

          let newParentChildCount = newParent._childCount;

          await db.executeTransaction(async () => {
            // Now that we have all the existing items, we can do the actual updates.
            for (let i = 0; i < updateInfos.length; i++) {
              let info = updateInfos[i];
              if (index != this.DEFAULT_INDEX) {
                // If we're dropping on the same folder, then we may need to adjust
                // the index to insert at the correct place.
                if (info.existingItem.parentGuid == newParent.guid) {
                  if (index > info.existingItem.index) {
                    // If we're dragging down, we need to go one lower to insert at
                    // the real point as moving the element changes the index of
                    // everything below by 1.
                    index--;
                  } else if (index == info.existingItem.index) {
                    // This isn't moving so we skip it, but copy the data so we have
                    // an easy way for the notifications to check.
                    info.updatedItem = { ...info.existingItem };
                    continue;
                  }
                }
              }

              // Never let the index go higher than the max count of the folder.
              if (index == this.DEFAULT_INDEX || index >= newParentChildCount) {
                index = newParentChildCount;

                // If this is moving within the same folder, then we need to drop the
                // index by one to compensate for "removing" it, then re-inserting.
                if (info.existingItem.parentGuid == newParent.guid) {
                  index--;
                }
              }

              info.updatedItem = await updateBookmark(
                db,
                { lastModified, index },
                info.existingItem,
                info.currIndex,
                newParent,
                syncChangeDelta
              );
              info.newParent = newParent;

              // For items moving within the same folder, we have to keep track
              // of their indexes. Otherwise we run the risk of not correctly
              // updating the indexes of other items in the folder.
              // This section simulates the database write in moveBookmark, which
              // allows us to avoid re-reading the database.
              if (info.existingItem.parentGuid == newParent.guid) {
                let sign = index < info.currIndex ? 1 : -1;
                for (let j = 0; j < updateInfos.length; j++) {
                  if (j == i) {
                    continue;
                  }
                  if (
                    updateInfos[j].currIndex >=
                      Math.min(info.currIndex, index) &&
                    updateInfos[j].currIndex <= Math.max(info.currIndex, index)
                  ) {
                    updateInfos[j].currIndex += sign;
                  }
                }
              }
              info.currIndex = index;

              // We only bump the parent count if we're moving from a different folder.
              if (info.existingItem.parentGuid != newParent.guid) {
                newParentChildCount++;
              }
              index++;
            }

            await setAncestorsLastModified(
              db,
              newParent.guid,
              lastModified,
              syncChangeDelta
            );
          });
        }
      );

      const notifications = [];
      let detailsMap = await getBookmarkDetailMap(
        updateInfos.map(({ updatedItem }) => updatedItem.guid)
      );
      // Updates complete, time to notify everyone.
      for (let { updatedItem, existingItem, newParent } of updateInfos) {
        // If the item was moved, notify bookmark-moved.
        // We use the updatedItem.index here, rather than currIndex, as the views
        // need to know where we inserted the item as opposed to where it ended
        // up.
        if (
          existingItem.parentGuid != updatedItem.parentGuid ||
          existingItem.index != updatedItem.index
        ) {
          let details = detailsMap.get(updatedItem.guid);
          notifications.push(
            new PlacesBookmarkMoved({
              id: updatedItem._id,
              itemType: updatedItem.type,
              url: existingItem.url,
              guid: updatedItem.guid,
              parentGuid: updatedItem.parentGuid,
              source,
              index: updatedItem.index,
              oldParentGuid: existingItem.parentGuid,
              oldIndex: existingItem.index,
              isTagging:
                updatedItem.parentGuid === Bookmarks.tagsGuid ||
                newParent.parentGuid === Bookmarks.tagsGuid,
              title: updatedItem.title,
              tags: details.tags,
              frecency: details.frecency,
              hidden: details.hidden,
              visitCount: details.visitCount,
              dateAdded: updatedItem.dateAdded,
              lastVisitDate: details.lastVisitDate,
            })
          );
        }
        // Remove non-enumerable properties.
        delete updatedItem.source;
      }

      if (notifications.length) {
        PlacesObservers.notifyListeners(notifications);
      }

      return updateInfos.map(updateInfo =>
        Object.assign({}, updateInfo.updatedItem)
      );
    })();
  },

  /**
   * Removes one or more bookmark-items.
   *
   * @param guidOrInfo This may be:
   *        - The globally unique identifier of the item to remove
   *        - an object representing the item, as defined above
   *        - an array of objects representing the items to be removed
   * @param {Object} [options={}]
   *        Additional options that can be passed to the function.
   *        Currently supports the following properties:
   *         - preventRemovalOfNonEmptyFolders: Causes an exception to be
   *           thrown when attempting to remove a folder that is not empty.
   *         - source: The change source, forwarded to all bookmark observers.
   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
   *
   * @return {Promise}
   * @resolves when the removal is complete
   * @rejects if the provided guid doesn't match any existing bookmark.
   * @throws if the arguments are invalid.
   */
  remove(guidOrInfo, options = {}) {
    let infos = guidOrInfo;
    if (!infos) {
      throw new Error("Input should be a valid object");
    }
    if (!Array.isArray(guidOrInfo)) {
      if (typeof guidOrInfo != "object") {
        infos = [{ guid: guidOrInfo }];
      } else {
        infos = [guidOrInfo];
      }
    }

    if (!("source" in options)) {
      options.source = Bookmarks.SOURCES.DEFAULT;
    }

    let removeInfos = [];
    for (let info of infos) {
      // Disallow removing the root folders.
      if (
        [
          Bookmarks.rootGuid,
          Bookmarks.menuGuid,
          Bookmarks.toolbarGuid,
          Bookmarks.unfiledGuid,
          Bookmarks.tagsGuid,
          Bookmarks.mobileGuid,
        ].includes(info.guid)
      ) {
        throw new Error("It's not possible to remove Places root folders.");
      }

      // Even if we ignore any other unneeded property, we still validate any
      // known property to reduce likelihood of hidden bugs.
      let removeInfo = validateBookmarkObject(
        "Bookmarks.sys.mjs: remove",
        info
      );
      removeInfos.push(removeInfo);
    }

    return (async function () {
      let removeItems = [];
      for (let info of removeInfos) {
        // We must be able to remove a bookmark even if it has an invalid url.
        // In that case the item won't have a url property.
        let item = await fetchBookmark(info, { ignoreInvalidURLs: true });
        if (!item) {
          throw new Error("No bookmarks found for the provided GUID.");
        }

        removeItems.push(item);
      }

      await removeBookmarks(removeItems, options);

      // Notify bookmark-removed to listeners.
      let notifications = [];

      for (let item of removeItems) {
        let isUntagging = item._grandParentId == lazy.PlacesUtils.tagsFolderId;
        let url = "";
        if (item.type == Bookmarks.TYPE_BOOKMARK) {
          url = item.hasOwnProperty("url") ? item.url.href : null;
        }

        notifications.push(
          new PlacesBookmarkRemoved({
            id: item._id,
            url,
            title: item.title,
            itemType: item.type,
            parentId: item._parentId,
            index: item.index,
            guid: item.guid,
            parentGuid: item.parentGuid,
            source: options.source,
            isTagging: isUntagging,
            isDescendantRemoval: false,
          })
        );

        if (isUntagging) {
          for (let entry of await fetchBookmarksByURL(item, {
            concurrent: true,
          })) {
            notifications.push(
              new PlacesBookmarkTags({
                id: entry._id,
                itemType: entry.type,
                url,
                guid: entry.guid,
                parentGuid: entry.parentGuid,
                tags: entry._tags,
                lastModified: entry.lastModified,
                source: options.source,
                isTagging: false,
              })
            );
          }
        }
      }

      PlacesObservers.notifyListeners(notifications);
    })();
  },

  /**
   * Removes ALL bookmarks, resetting the bookmarks storage to an empty tree.
   *
   * Note that roots are preserved, only their children will be removed.
   *
   * @param {Object} [options={}]
   *        Additional options. Currently supports the following properties:
   *         - source: The change source, forwarded to all bookmark observers.
   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
   *
   * @return {Promise} resolved when the removal is complete.
   * @resolves once the removal is complete.
   */
  eraseEverything(options = {}) {
    if (!options.source) {
      options.source = Bookmarks.SOURCES.DEFAULT;
    }

    return lazy.PlacesUtils.withConnectionWrapper(
      "Bookmarks.sys.mjs: eraseEverything",
      async function (db) {
        let urls;
        await db.executeTransaction(async function () {
          urls = await removeFoldersContents(
            db,
            Bookmarks.userContentRoots,
            options
          );
          const time = lazy.PlacesUtils.toPRTime(new Date());
          const syncChangeDelta =
            lazy.PlacesSyncUtils.bookmarks.determineSyncChangeDelta(
              options.source
            );
          for (let folderGuid of Bookmarks.userContentRoots) {
            await db.executeCached(
              `UPDATE moz_bookmarks SET lastModified = :time,
                                        syncChangeCounter = syncChangeCounter + :syncChangeDelta
               WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
              `,
              { folderGuid, time, syncChangeDelta }
            );
          }

          await lazy.PlacesSyncUtils.bookmarks.resetSyncMetadata(
            db,
            options.source
          );
        });

        if (urls?.length) {
          await lazy.PlacesUtils.keywords.eraseEverything();
        }
      }
    );
  },

  /**
   * Returns a list of recently bookmarked items.
   * Only includes actual bookmarks. Excludes folders, separators and queries.
   *
   * @param {integer} numberOfItems
   *        The maximum number of bookmark items to return.
   *
   * @return {Promise} resolved when the listing is complete.
   * @resolves to an array of recent bookmark-items.
   * @rejects if an error happens while querying.
   */
  getRecent(numberOfItems) {
    if (numberOfItems === undefined) {
      throw new Error("numberOfItems argument is required");
    }
    if (typeof numberOfItems !== "number" || numberOfItems % 1 !== 0) {
      throw new Error("numberOfItems argument must be an integer");
    }
    if (numberOfItems <= 0) {
      throw new Error("numberOfItems argument must be greater than zero");
    }

    return fetchRecentBookmarks(numberOfItems);
  },

  /**
   * Fetches information about a bookmark-item.
   *
   * REMARK: any successful call to this method resolves to a single
   *         bookmark-item (or null), even when multiple bookmarks may exist
   *         (e.g. fetching by url).  If you wish to retrieve all of the
   *         bookmarks for a given match, use the callback instead.
   *
   * Input can be either a guid or an object with one, and only one, of these
   * filtering properties set:
   *  - guid
   *      retrieves the item with the specified guid.
   *  - parentGuid and index
   *      retrieves the item by its position.
   *  - url
   *      retrieves the most recent bookmark having the given URL.
   *      To retrieve ALL of the bookmarks for that URL, you must pass in an
   *      onResult callback, that will be invoked once for each found bookmark.
   *  - guidPrefix
   *      retrieves the most recent item with the specified guid prefix.
   *      To retrieve ALL of the bookmarks for that guid prefix, you must pass
   *      in an onResult callback, that will be invoked once for each bookmark.
   *  - tags
   *      Retrieves the most recent item with all the specified tags.
   *      The tags are matched in a case-insensitive way.
   *      To retrieve ALL of the bookmarks having these tags, pass in an
   *      onResult callback, that will be invoked once for each bookmark.
   *      Note, there can be multiple bookmarks for the same url, if you need
   *      unique tagged urls you can filter duplicates by accumulating in a Set.
   *
   * @param guidOrInfo
   *        The globally unique identifier of the item to fetch, or an
   *        object representing it, as defined above.
   * @param onResult [optional]
   *        Callback invoked for each found bookmark.
   * @param options [optional]
   *        an optional object whose properties describe options for the fetch:
   *         - concurrent: fetches concurrently to any writes, returning results
   *                       faster. On the negative side, it may return stale
   *                       information missing the currently ongoing write.
   *         - includePath: additionally fetches the path for the bookmarks.
   *                        This is a potentially expensive operation.  When
   *                        set to true, the path property is set on results
   *                        containing an array of {title, guid} objects
   *                        ordered from root to leaf.
   *         - includeItemIds:
   *             include .itemId and .parentId in the results.
   *             ALWAYS USE THE GUIDs instead of these, unless it's _really_
   *             necessary to get them, e.g. when sending Places notifications.
   *
   * @return {Promise} resolved when the fetch is complete.
   * @resolves to an object representing the found item, as described above, or
   *           an array of such objects.  if no item is found, the returned
   *           promise is resolved to null.
   * @rejects if an error happens while fetching.
   * @throws if the arguments are invalid.
   *
   * @note Any unknown property in the info object is ignored.  Known properties
   *       may be overwritten.
   */
  async fetch(guidOrInfo, onResult = null, options = {}) {
    if (onResult && typeof onResult != "function") {
      throw new Error("onResult callback must be a valid function");
    }
    let info = guidOrInfo;
    if (!info) {
      throw new Error("Input should be a valid object");
    }
    if (typeof info != "object") {
      info = { guid: guidOrInfo };
    } else if (Object.keys(info).length == 1) {
      // Just a faster code path.
      if (
        !["url", "guid", "parentGuid", "index", "guidPrefix", "tags"].includes(
          Object.keys(info)[0]
        )
      ) {
        throw new Error(`Unexpected number of conditions provided: 0`);
      }
    } else {
      // Only one condition at a time can be provided.
      let conditionsCount = [
        v => v.hasOwnProperty("guid"),
        v => v.hasOwnProperty("parentGuid") && v.hasOwnProperty("index"),
        v => v.hasOwnProperty("url"),
        v => v.hasOwnProperty("guidPrefix"),
        v => v.hasOwnProperty("tags"),
      ].reduce((old, fn) => (old + fn(info)) | 0, 0);
      if (conditionsCount != 1) {
        throw new Error(
          `Unexpected number of conditions provided: ${conditionsCount}`
        );
      }
    }

    // Create a new options object with just the support properties, because
    // we may augment it and hand it down to other methods.
    options = {
      concurrent: !!options.concurrent,
      includePath: !!options.includePath,
      includeItemIds: !!options.includeItemIds,
    };

    let behavior = {};
    if (info.hasOwnProperty("parentGuid") || info.hasOwnProperty("index")) {
      behavior = {
        parentGuid: { requiredIf: b => b.hasOwnProperty("index") },
        index: {
          validIf: b =>
            (typeof b.index == "number" && b.index >= 0) ||
            b.index == this.DEFAULT_INDEX,
        },
      };
    }

    // Even if we ignore any other unneeded property, we still validate any
    // known property to reduce likelihood of hidden bugs.
    let fetchInfo = validateBookmarkObject(
      "Bookmarks.sys.mjs: fetch",
      info,
      behavior
    );

    let results;
    if (fetchInfo.hasOwnProperty("url")) {
      results = await fetchBookmarksByURL(fetchInfo, options);
    } else if (fetchInfo.hasOwnProperty("guid")) {
      results = await fetchBookmark(fetchInfo, options);
    } else if (fetchInfo.hasOwnProperty("parentGuid")) {
      if (fetchInfo.hasOwnProperty("index")) {
        results = await fetchBookmarkByPosition(fetchInfo, options);
      } else {
        results = await fetchBookmarksByParentGUID(fetchInfo, options);
      }
    } else if (fetchInfo.hasOwnProperty("guidPrefix")) {
      results = await fetchBookmarksByGUIDPrefix(fetchInfo, options);
    } else if (fetchInfo.hasOwnProperty("tags")) {
      results = await fetchBookmarksByTags(fetchInfo, options);
    }

    if (!results) {
      return null;
    }

    if (!Array.isArray(results)) {
      results = [results];
    }
    // Remove non-enumerable properties.
    results = results.map(r => {
      if (r.type == this.TYPE_FOLDER) {
        r.childCount = r._childCount;
      }
      if (options.includeItemIds) {
        r.itemId = r._id;
        r.parentId = r._parentId;
      }
      return Object.assign({}, r);
    });

    if (options.includePath) {
      for (let result of results) {
        let folderPath = await retrieveFullBookmarkPath(result.parentGuid);
        if (folderPath) {
          result.path = folderPath;
        }
      }
    }

    // Ideally this should handle an incremental behavior and thus be invoked
    // while we fetch.  Though, the likelihood of 2 or more bookmarks for the
    // same match is very low, so it's not worth the added code complication.
    if (onResult) {
      for (let result of results) {
        try {
          onResult(result);
        } catch (ex) {
          console.error(ex);
        }
      }
    }

    return results[0];
  },

  /**
   * Retrieves an object representation of a bookmark-item, along with all of
   * its descendants, if any.
   *
   * Each node in the tree is an object that extends the item representation
   * described above with some additional properties:
   *
   *  - [deprecated] itemId (number)
   *      the item's id.  Defined only if aOptions.includeItemIds is set.
   *  - [deprecated] parentId (number)
   *      the item's parent id.  Defined only if aOptions.includeItemIds is set.
   *  - annos (array)
   *      the item's annotations.  This is not set if there are no annotations
   *      set for the item.
   *
   * The root object of the tree also has the following properties set:
   *  - itemsCount (number, not enumerable)
   *      the number of items, including the root item itself, which are
   *      represented in the resolved object.
   *
   * Bookmarked URLs may also have the following properties:
   *  - tags (string)
   *      csv string of the bookmark's tags, if any.
   *  - charset (string)
   *      the last known charset of the bookmark, if any.
   *  - iconurl (URL)
   *      the bookmark's favicon URL, if any.
   *
   * Folders may also have the following properties:
   *  - children (array)
   *      the folder's children information, each of them having the same set of
   *      properties as above.
   *
   * @param [optional] guid
   *        the topmost item to be queried.  If it's not passed, the Places
   *        root folder is queried: that is, you get a representation of the
   *        entire bookmarks hierarchy.
   * @param [optional] options
   *        Options for customizing the query behavior, in the form of an
   *        object with any of the following properties:
   *         - excludeItemsCallback: a function for excluding items, along with
   *           their descendants.  Given an item object (that has everything set
   *           apart its potential children data), it should return true if the
   *           item should be excluded.  Once an item is excluded, the function
   *           isn't called for any of its descendants.  This isn't called for
   *           the root item.
   *           WARNING: since the function may be called for each item, using
   *           this option can slow down the process significantly if the
   *           callback does anything that's not relatively trivial.  It is
   *           highly recommended to avoid any synchronous I/O or DB queries.
   *         - includeItemIds: opt-in to include the deprecated id property.
   *           Use it if you must. It'll be removed once the switch to guids is
   *           complete.
   *
   * @return {Promise} resolved when the fetch is complete.
   * @resolves to an object that represents either a single item or a
   *           bookmarks tree.  if guid points to a non-existent item, the
   *           returned promise is resolved to null.
   * @rejects if an error happens while fetching.
   * @throws if the arguments are invalid.
   */
  // TODO must implement these methods yet:
  // PlacesUtils.promiseBookmarksTree()
  fetchTree() {
    throw new Error("Not yet implemented");
  },

  /**
   * Fetch all the existing tags, sorted alphabetically.
   * @return {Promise} resolves to an array of objects representing tags, when
   *         fetching is complete.
   *         Each object looks like {
   *           name: the name of the tag,
   *           count: number of bookmarks with this tag
   *         }
   */
  async fetchTags() {
    // TODO: Once the tagging API is implemented in Bookmarks.sys.mjs, we can cache
    // the list of tags, instead of querying every time.
    let db = await lazy.PlacesUtils.promiseDBConnection();
    let rows = await db.executeCached(
      `
      SELECT b.title AS name, count(*) AS count
      FROM moz_bookmarks b
      JOIN moz_bookmarks p ON b.parent = p.id
      JOIN moz_bookmarks c ON c.parent = b.id
      WHERE p.guid = :tagsGuid
      GROUP BY name
      ORDER BY name COLLATE nocase ASC
    `,
      { tagsGuid: this.tagsGuid }
    );
    return rows.map(r => ({
      name: r.getResultByName("name"),
      count: r.getResultByName("count"),
    }));
  },

  /**
   * Reorders contents of a folder based on a provided array of GUIDs.
   *
   * @param parentGuid
   *        The globally unique identifier of the folder whose contents should
   *        be reordered.
   * @param orderedChildrenGuids
   *        Ordered array of the children's GUIDs.  If this list contains
   *        non-existing entries they will be ignored.  If the list is
   *        incomplete, and the current child list is already in order with
   *        respect to orderedChildrenGuids, no change is made. Otherwise, the
   *        new items are appended but maintain their current order relative to
   *        eachother.
   * @param {Object} [options={}]
   *        Additional options. Currently supports the following properties:
   *         - lastModified: The last modified time to use for the folder and
               reordered children. Defaults to the current time.
   *         - source: The change source, forwarded to all bookmark observers.
   *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
   *
   * @return {Promise} resolved when reordering is complete.
   * @rejects if an error happens while reordering.
   * @throws if the arguments are invalid.
   */
  reorder(parentGuid, orderedChildrenGuids, options = {}) {
    let info = { guid: parentGuid };
    info = validateBookmarkObject("Bookmarks.sys.mjs: reorder", info, {
      guid: { required: true },
    });

    if (!Array.isArray(orderedChildrenGuids) || !orderedChildrenGuids.length) {
      throw new Error("Must provide a sorted array of children GUIDs.");
    }
    try {
      orderedChildrenGuids.forEach(lazy.PlacesUtils.BOOKMARK_VALIDATORS.guid);
    } catch (ex) {
      throw new Error("Invalid GUID found in the sorted children array.");
    }

    options.source =
      "source" in options
        ? lazy.PlacesUtils.BOOKMARK_VALIDATORS.source(options.source)
        : Bookmarks.SOURCES.DEFAULT;
    options.lastModified =
      "lastModified" in options
        ? lazy.PlacesUtils.BOOKMARK_VALIDATORS.lastModified(
            options.lastModified
          )
        : new Date();

    return (async () => {
      let parent = await fetchBookmark(info);
--> --------------------

--> maximum size reached

--> --------------------

[ Verzeichnis aufwärts0.50unsichere Verbindung  Übersetzung europäischer Sprachen durch Browser  ]