Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  PlacesUtils.sys.mjs   Sprache: unbekannt

 
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  Bookmarks: "resource://gre/modules/Bookmarks.sys.mjs",
  History: "resource://gre/modules/History.sys.mjs",
  PlacesSyncUtils: "resource://gre/modules/PlacesSyncUtils.sys.mjs",
  Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "MOZ_ACTION_REGEX", () => {
  return /^moz-action:([^,]+),(.*)$/;
});

ChromeUtils.defineLazyGetter(lazy, "CryptoHash", () => {
  return Components.Constructor(
    "@mozilla.org/security/hash;1",
    "nsICryptoHash",
    "initWithString"
  );
});

// On Mac OSX, the transferable system converts "\r\n" to "\n\n", where
// we really just want "\n". On other platforms, the transferable system
// converts "\r\n" to "\n".
const NEWLINE = AppConstants.platform == "macosx" ? "\n" : "\r\n";

// Timers resolution is not always good, it can have a 16ms precision on Win.
const TIMERS_RESOLUTION_SKEW_MS = 16;

function QI_node(aNode, aIID) {
  try {
    return aNode.QueryInterface(aIID);
  } catch (ex) {}
  return null;
}
function asContainer(aNode) {
  return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);
}
function asQuery(aNode) {
  return QI_node(aNode, Ci.nsINavHistoryQueryResultNode);
}

/**
 * Sends a keyword change notification.
 *
 * @param url
 *        the url to notify about.
 * @param keyword
 *        The keyword to notify, or empty string if a keyword was removed.
 */
async function notifyKeywordChange(url, keyword, source) {
  // Notify bookmarks about the removal.
  let bookmarks = [];
  await PlacesUtils.bookmarks.fetch({ url }, b => bookmarks.push(b), {
    includeItemIds: true,
  });

  const notifications = bookmarks.map(
    bookmark =>
      new PlacesBookmarkKeyword({
        id: bookmark.itemId,
        itemType: bookmark.type,
        url,
        guid: bookmark.guid,
        parentGuid: bookmark.parentGuid,
        keyword,
        lastModified: bookmark.lastModified,
        source,
        isTagging: false,
      })
  );
  if (notifications.length) {
    PlacesObservers.notifyListeners(notifications);
  }
}

/**
 * Serializes the given node in JSON format.
 *
 * @param aNode
 *        An nsINavHistoryResultNode
 */
function serializeNode(aNode) {
  let data = {};

  data.title = aNode.title;
  // The id is no longer used for copying within the same instance/session of
  // Firefox as of at least 61. However, we keep the id for now to maintain
  // backwards compat of drag and drop with older Firefox versions.
  data.id = aNode.itemId;
  data.itemGuid = aNode.bookmarkGuid;
  // Add an instanceId so we can tell which instance of an FF session the data
  // is coming from.
  data.instanceId = PlacesUtils.instanceId;

  let guid = aNode.bookmarkGuid;

  // Some nodes, e.g. the unfiled/menu/toolbar ones can have a virtual guid, so
  // we ignore any that are a folder shortcut. These will be handled below.
  if (
    guid &&
    !PlacesUtils.bookmarks.isVirtualRootItem(guid) &&
    !PlacesUtils.isVirtualLeftPaneItem(guid)
  ) {
    if (aNode.parent) {
      data.parent = aNode.parent.itemId;
      data.parentGuid = aNode.parent.bookmarkGuid;
    }

    data.dateAdded = aNode.dateAdded;
    data.lastModified = aNode.lastModified;
  }

  if (PlacesUtils.nodeIsURI(aNode)) {
    // Check for url validity.
    new URL(aNode.uri);
    data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
    data.uri = aNode.uri;
    if (aNode.tags) {
      data.tags = aNode.tags;
    }
  } else if (PlacesUtils.nodeIsFolderOrShortcut(aNode)) {
    if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
      data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
      data.uri = aNode.uri;
      data.concreteGuid = PlacesUtils.getConcreteItemGuid(aNode);
    } else {
      data.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
    }
  } else if (PlacesUtils.nodeIsQuery(aNode)) {
    data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
    data.uri = aNode.uri;
  } else if (PlacesUtils.nodeIsSeparator(aNode)) {
    data.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
  }

  return JSON.stringify(data);
}

// Imposed to limit database size.
const DB_URL_LENGTH_MAX = 65536;
const DB_TITLE_LENGTH_MAX = 4096;
const DB_DESCRIPTION_LENGTH_MAX = 256;
const DB_SITENAME_LENGTH_MAX = 50;

/**
 * Executes a boolean validate function, throwing if it returns false.
 *
 * @param boolValidateFn
 *        A boolean validate function.
 * @return the input value.
 * @throws if input doesn't pass the validate function.
 */
function simpleValidateFunc(boolValidateFn) {
  return (v, input) => {
    if (!boolValidateFn(v, input)) {
      throw new Error("Invalid value");
    }
    return v;
  };
}

/**
 * List of bookmark object validators, one per each known property.
 * Validators must throw if the property value is invalid and return a fixed up
 * version of the value, if needed.
 */
const BOOKMARK_VALIDATORS = Object.freeze({
  guid: simpleValidateFunc(v => PlacesUtils.isValidGuid(v)),
  parentGuid: simpleValidateFunc(v => PlacesUtils.isValidGuid(v)),
  guidPrefix: simpleValidateFunc(v => PlacesUtils.isValidGuidPrefix(v)),
  index: simpleValidateFunc(
    v => Number.isInteger(v) && v >= PlacesUtils.bookmarks.DEFAULT_INDEX
  ),
  dateAdded: simpleValidateFunc(v => v.constructor.name == "Date" && !isNaN(v)),
  lastModified: simpleValidateFunc(
    v => v.constructor.name == "Date" && !isNaN(v)
  ),
  type: simpleValidateFunc(
    v =>
      Number.isInteger(v) &&
      [
        PlacesUtils.bookmarks.TYPE_BOOKMARK,
        PlacesUtils.bookmarks.TYPE_FOLDER,
        PlacesUtils.bookmarks.TYPE_SEPARATOR,
      ].includes(v)
  ),
  title: v => {
    if (v === null) {
      return "";
    }
    if (typeof v == "string") {
      return v.slice(0, DB_TITLE_LENGTH_MAX);
    }
    throw new Error("Invalid title");
  },
  url: v => {
    simpleValidateFunc(
      val =>
        (typeof val == "string" && val.length <= DB_URL_LENGTH_MAX) ||
        (val instanceof Ci.nsIURI && val.spec.length <= DB_URL_LENGTH_MAX) ||
        (URL.isInstance(val) && val.href.length <= DB_URL_LENGTH_MAX)
    )(v);
    if (typeof v === "string") {
      return new URL(v);
    }
    if (v instanceof Ci.nsIURI) {
      return URL.fromURI(v);
    }
    return v;
  },
  source: simpleValidateFunc(
    v =>
      Number.isInteger(v) &&
      Object.values(PlacesUtils.bookmarks.SOURCES).includes(v)
  ),
  keyword: simpleValidateFunc(v => typeof v == "string" && v.length),
  charset: simpleValidateFunc(v => typeof v == "string" && v.length),
  postData: simpleValidateFunc(v => typeof v == "string" && v.length),
  tags: simpleValidateFunc(
    v =>
      Array.isArray(v) &&
      v.length &&
      v.every(item => item && typeof item == "string")
  ),
});

// Sync bookmark records can contain additional properties.
const SYNC_BOOKMARK_VALIDATORS = Object.freeze({
  // Sync uses Places GUIDs for all records except roots.
  recordId: simpleValidateFunc(
    v =>
      typeof v == "string" &&
      (lazy.PlacesSyncUtils.bookmarks.ROOTS.includes(v) ||
        PlacesUtils.isValidGuid(v))
  ),
  parentRecordId: v => SYNC_BOOKMARK_VALIDATORS.recordId(v),
  // Sync uses kinds instead of types.
  kind: simpleValidateFunc(
    v =>
      typeof v == "string" &&
      Object.values(lazy.PlacesSyncUtils.bookmarks.KINDS).includes(v)
  ),
  query: simpleValidateFunc(v => v === null || (typeof v == "string" && v)),
  folder: simpleValidateFunc(
    v =>
      typeof v == "string" &&
      v &&
      v.length <= PlacesUtils.bookmarks.MAX_TAG_LENGTH
  ),
  tags: v => {
    if (v === null) {
      return [];
    }
    if (!Array.isArray(v)) {
      throw new Error("Invalid tag array");
    }
    for (let tag of v) {
      if (
        typeof tag != "string" ||
        !tag ||
        tag.length > PlacesUtils.bookmarks.MAX_TAG_LENGTH
      ) {
        throw new Error(`Invalid tag: ${tag}`);
      }
    }
    return v;
  },
  keyword: simpleValidateFunc(v => v === null || typeof v == "string"),
  dateAdded: simpleValidateFunc(
    v =>
      typeof v === "number" &&
      v > lazy.PlacesSyncUtils.bookmarks.EARLIEST_BOOKMARK_TIMESTAMP
  ),
  feed: v => (v === null ? v : BOOKMARK_VALIDATORS.url(v)),
  site: v => (v === null ? v : BOOKMARK_VALIDATORS.url(v)),
  title: BOOKMARK_VALIDATORS.title,
  url: BOOKMARK_VALIDATORS.url,
});

// Sync change records are passed between `PlacesSyncUtils` and the Sync
// bookmarks engine, and are used to update an item's sync status and change
// counter at the end of a sync.
const SYNC_CHANGE_RECORD_VALIDATORS = Object.freeze({
  modified: simpleValidateFunc(v => typeof v == "number" && v >= 0),
  counter: simpleValidateFunc(v => typeof v == "number" && v >= 0),
  status: simpleValidateFunc(
    v =>
      typeof v == "number" &&
      Object.values(PlacesUtils.bookmarks.SYNC_STATUS).includes(v)
  ),
  tombstone: simpleValidateFunc(v => v === true || v === false),
  synced: simpleValidateFunc(v => v === true || v === false),
});
/**
 * List PageInfo bookmark object validators.
 */
const PAGEINFO_VALIDATORS = Object.freeze({
  guid: BOOKMARK_VALIDATORS.guid,
  url: BOOKMARK_VALIDATORS.url,
  title: v => {
    if (v == null || v == undefined) {
      return undefined;
    } else if (typeof v === "string") {
      return v;
    }
    throw new TypeError(
      `title property of PageInfo object: ${v} must be a string if provided`
    );
  },
  previewImageURL: v => {
    if (!v) {
      return null;
    }
    return BOOKMARK_VALIDATORS.url(v);
  },
  description: v => {
    if (typeof v === "string" || v === null) {
      return v ? v.slice(0, DB_DESCRIPTION_LENGTH_MAX) : null;
    }
    throw new TypeError(
      `description property of pageInfo object: ${v} must be either a string or null if provided`
    );
  },
  siteName: v => {
    if (typeof v === "string" || v === null) {
      return v ? v.slice(0, DB_SITENAME_LENGTH_MAX) : null;
    }
    throw new TypeError(
      `siteName property of pageInfo object: ${v} must be either a string or null if provided`
    );
  },
  annotations: v => {
    if (typeof v != "object" || v.constructor.name != "Map") {
      throw new TypeError("annotations must be a Map");
    }

    if (v.size == 0) {
      throw new TypeError("there must be at least one annotation");
    }

    for (let [key, value] of v.entries()) {
      if (typeof key != "string") {
        throw new TypeError("all annotation keys must be strings");
      }
      if (
        typeof value != "string" &&
        typeof value != "number" &&
        typeof value != "boolean" &&
        value !== null &&
        value !== undefined
      ) {
        throw new TypeError(
          "all annotation values must be Boolean, Numbers or Strings"
        );
      }
    }
    return v;
  },
  visits: v => {
    if (!Array.isArray(v) || !v.length) {
      throw new TypeError("PageInfo object must have an array of visits");
    }
    let visits = [];
    for (let inVisit of v) {
      let visit = {
        date: new Date(),
        transition: inVisit.transition || lazy.History.TRANSITIONS.LINK,
      };

      if (!PlacesUtils.history.isValidTransition(visit.transition)) {
        throw new TypeError(
          `transition: ${visit.transition} is not a valid transition type`
        );
      }

      if (inVisit.date) {
        PlacesUtils.history.ensureDate(inVisit.date);
        if (inVisit.date > Date.now() + TIMERS_RESOLUTION_SKEW_MS) {
          throw new TypeError(`date: ${inVisit.date} cannot be a future date`);
        }
        visit.date = inVisit.date;
      }

      if (inVisit.referrer) {
        visit.referrer = PlacesUtils.normalizeToURLOrGUID(inVisit.referrer);
      }
      visits.push(visit);
    }
    return visits;
  },
});

export var PlacesUtils = {
  // Place entries that are containers, e.g. bookmark folders or queries.
  TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
  // Place entries that are bookmark separators.
  TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
  // Place entries that are not containers or separators
  TYPE_X_MOZ_PLACE: "text/x-moz-place",
  // Place entries in shortcut url format (url\ntitle)
  TYPE_X_MOZ_URL: "text/x-moz-url",
  // Place entries formatted as HTML anchors
  TYPE_HTML: "text/html",
  // Place entries as raw URL text
  TYPE_PLAINTEXT: "text/plain",
  // Used to track the action that populated the clipboard.
  TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",

  // Deprecated: Remaining only for supporting migration of old livemarks.
  LMANNO_FEEDURI: "livemark/feedURI",
  LMANNO_SITEURI: "livemark/siteURI",
  CHARSET_ANNO: "URIProperties/characterSet",
  // Deprecated: This is only used for supporting import from older datasets.
  MOBILE_ROOT_ANNO: "mobile/bookmarksRoot",

  TOPIC_SHUTDOWN: "places-shutdown",
  TOPIC_INIT_COMPLETE: "places-init-complete",
  TOPIC_DATABASE_LOCKED: "places-database-locked",
  TOPIC_EXPIRATION_FINISHED: "places-expiration-finished",
  TOPIC_FAVICONS_EXPIRED: "places-favicons-expired",
  TOPIC_VACUUM_STARTING: "places-vacuum-starting",
  TOPIC_BOOKMARKS_RESTORE_BEGIN: "bookmarks-restore-begin",
  TOPIC_BOOKMARKS_RESTORE_SUCCESS: "bookmarks-restore-success",
  TOPIC_BOOKMARKS_RESTORE_FAILED: "bookmarks-restore-failed",

  observers: PlacesObservers,

  /**
   * GUIDs associated with virtual queries that are used for displaying the
   * top-level folders in the left pane.
   */
  virtualAllBookmarksGuid: "allbms_____v",
  virtualHistoryGuid: "history____v",
  virtualDownloadsGuid: "downloads__v",
  virtualTagsGuid: "tags_______v",

  /**
   * Checks if a guid is a virtual left-pane root.
   *
   * @param {String} guid The guid of the item to look for.
   * @returns {Boolean} true if guid is a virtual root, false otherwise.
   */
  isVirtualLeftPaneItem(guid) {
    return (
      guid == PlacesUtils.virtualAllBookmarksGuid ||
      guid == PlacesUtils.virtualHistoryGuid ||
      guid == PlacesUtils.virtualDownloadsGuid ||
      guid == PlacesUtils.virtualTagsGuid
    );
  },

  asContainer: aNode => asContainer(aNode),
  asQuery: aNode => asQuery(aNode),

  endl: NEWLINE,

  /**
   * Is a string a valid GUID?
   *
   * @param guid: (String)
   * @return (Boolean)
   */
  isValidGuid(guid) {
    return typeof guid == "string" && guid && /^[a-zA-Z0-9\-_]{12}$/.test(guid);
  },

  /**
   * Is a string a valid GUID prefix?
   *
   * @param guidPrefix: (String)
   * @return (Boolean)
   */
  isValidGuidPrefix(guidPrefix) {
    return (
      typeof guidPrefix == "string" &&
      guidPrefix &&
      /^[a-zA-Z0-9\-_]{1,11}$/.test(guidPrefix)
    );
  },

  /**
   * Generates a random GUID and replace its beginning with the given
   * prefix. We do this instead of just prepending the prefix to keep
   * the correct character length.
   *
   * @param prefix: (String)
   * @return (String)
   */
  generateGuidWithPrefix(prefix) {
    return prefix + this.history.makeGuid().substring(prefix.length);
  },

  /**
   * Converts a string or n URL object to an nsIURI.
   *
   * @param url (URL) or (String)
   *        the URL to convert.
   * @return nsIURI for the given URL.
   */
  toURI(url) {
    if (url instanceof Ci.nsIURI) {
      return url;
    }
    if (URL.isInstance(url)) {
      return url.URI;
    }
    return Services.io.newURI(url);
  },

  /**
   * Convert a Date object to a PRTime (microseconds).
   *
   * @param date
   *        the Date object to convert.
   * @return microseconds from the epoch.
   */
  toPRTime(date) {
    if (
      (typeof date != "number" && date.constructor.name != "Date") ||
      isNaN(date)
    ) {
      throw new Error("Invalid value passed to toPRTime");
    }
    return date * 1000;
  },

  /**
   * Convert a PRTime to a Date object.
   *
   * @param time
   *        microseconds from the epoch.
   * @return a Date object.
   */
  toDate(time) {
    if (typeof time != "number" || isNaN(time)) {
      throw new Error("Invalid value passed to toDate");
    }
    return new Date(parseInt(time / 1000));
  },

  /**
   * Wraps a string in a nsISupportsString wrapper.
   * @param   aString
   *          The string to wrap.
   * @returns A nsISupportsString object containing a string.
   */
  toISupportsString: function PU_toISupportsString(aString) {
    let s = Cc["@mozilla.org/supports-string;1"].createInstance(
      Ci.nsISupportsString
    );
    s.data = aString;
    return s;
  },

  getFormattedString: function PU_getFormattedString(key, params) {
    return lazy.bundle.formatStringFromName(key, params);
  },

  getString: function PU_getString(key) {
    return lazy.bundle.GetStringFromName(key);
  },

  /**
   * Parses a moz-action URL and returns its parts.
   *
   * @param url A moz-action URI.
   * @note URL is in the format moz-action:ACTION,JSON_ENCODED_PARAMS
   */
  parseActionUrl(url) {
    if (url instanceof Ci.nsIURI) {
      url = url.spec;
    } else if (URL.isInstance(url)) {
      url = url.href;
    }
    // Faster bailout.
    if (!url.startsWith("moz-action:")) {
      return null;
    }

    try {
      let [, type, params] = url.match(lazy.MOZ_ACTION_REGEX);
      let action = {
        type,
        params: JSON.parse(params),
      };
      for (let key in action.params) {
        action.params[key] = decodeURIComponent(action.params[key]);
      }
      return action;
    } catch (ex) {
      console.error(`Invalid action url "${url}"`);
      return null;
    }
  },

  /**
   * Determines if a bookmark folder or folder shortcut is generated by a query.
   *
   * @param {nsINavHistoryResultNode} node A result node.
   * @returns {boolean} whether `node` is a bookmark folder or folder shortcut
   * generated as result of a query.
   */
  nodeIsQueryGeneratedFolder(node) {
    return (
      node.parent &&
      this.nodeIsFolderOrShortcut(node) &&
      this.nodeIsQuery(node.parent)
    );
  },

  /**
   * Determines whether or not a ResultNode is a bookmark folder or folder
   * shortcut.
   *
   * @param {nsINavHistoryResultNode} node A result node.
   * @returns {boolean} whether `node` is a bookmark folder or folder shortcut.
   */
  nodeIsFolderOrShortcut(node) {
    return [
      Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
      Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
    ].includes(node.type);
  },

  /**
   * Determines whether or not a ResultNode is a bookmark folder.
   * For most UI purposes it's better to use nodeIsFolderOrShortcut,
   * as folder shortcuts behave like the target folder.
   *
   * @param {nsINavHistoryResultNode} node A result node.
   * @returns {boolean} whether the node is a bookmark folder.
   */
  nodeIsConcreteFolder(node) {
    return node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
  },

  /**
   * Determines whether or not a ResultNode represents a bookmarked URI.
   *
   * @param {nsINavHistoryResultNode} node A result node.
   * @returns {boolean} whether the node is a bookmarked URI.
   */
  nodeIsBookmark(node) {
    return (
      node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
      (node.itemId != -1 || node.bookmarkGuid)
    );
  },

  /**
   * Determines whether or not a ResultNode is a bookmark separator.
   *
   * @param {nsINavHistoryResultNode} node A result node.
   * @returns {boolean} whether the node is a bookmark separator.
   */
  nodeIsSeparator(node) {
    return node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR;
  },

  /**
   * Determines whether or not a ResultNode represents a URL.
   *
   * @param {nsINavHistoryResultNode} node A result node.
   * @returns {boolean} whether the node represents a URL.
   */
  nodeIsURI(node) {
    return node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
  },

  /**
   * Determines whether or not a ResultNode is a Places query.
   *
   * @param {nsINavHistoryResultNode} node A result node.
   * @returns {boolean} whether the node is a Places Query.
   */
  nodeIsQuery(node) {
    return node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
  },

  /**
   * Generator for a node's ancestors.
   * @param aNode
   *        A result node
   */
  nodeAncestors: function* PU_nodeAncestors(aNode) {
    let node = aNode.parent;
    while (node) {
      yield node;
      node = node.parent;
    }
  },

  /**
   * Checks validity of an object, filling up default values for optional
   * properties.
   *
   * @param {string} name
   *        The operation name. This is included in the error message if
   *        validation fails.
   * @param validators (object)
   *        An object containing input validators. Keys should be field names;
   *        values should be validation functions.
   * @param props (object)
   *        The object to validate.
   * @param behavior (object) [optional]
   *        Object defining special behavior for some of the properties.
   *        The following behaviors may be optionally set:
   *         - required: this property is required.
   *         - replaceWith: this property will be overwritten with the value
   *                        provided
   *         - requiredIf: if the provided condition is satisfied, then this
   *                       property is required.
   *         - validIf: if the provided condition is not satisfied, then this
   *                    property is invalid.
   *         - defaultValue: an undefined property should default to this value.
   *         - fixup: a function invoked when validation fails, takes the input
   *                  object as argument and must fix the property.
   *
   * @return a validated and normalized item.
   * @throws if the object contains invalid data.
   * @note any unknown properties are pass-through.
   */
  validateItemProperties(name, validators, props, behavior = {}) {
    if (typeof props != "object" || !props) {
      throw new Error(`${name}: Input should be a valid object`);
    }
    // Make a shallow copy of `props` to avoid mutating the original object
    // when filling in defaults.
    let input = Object.assign({}, props);
    let normalizedInput = {};
    let required = new Set();
    for (let prop in behavior) {
      if (
        behavior[prop].hasOwnProperty("required") &&
        behavior[prop].required
      ) {
        required.add(prop);
      }
      if (
        behavior[prop].hasOwnProperty("requiredIf") &&
        behavior[prop].requiredIf(input)
      ) {
        required.add(prop);
      }
      if (
        behavior[prop].hasOwnProperty("validIf") &&
        input[prop] !== undefined &&
        !behavior[prop].validIf(input)
      ) {
        if (behavior[prop].hasOwnProperty("fixup")) {
          behavior[prop].fixup(input);
        } else {
          throw new Error(
            `${name}: Invalid value for property '${prop}': ${JSON.stringify(
              input[prop]
            )}`
          );
        }
      }
      if (
        behavior[prop].hasOwnProperty("defaultValue") &&
        input[prop] === undefined
      ) {
        input[prop] = behavior[prop].defaultValue;
      }
      if (behavior[prop].hasOwnProperty("replaceWith")) {
        input[prop] = behavior[prop].replaceWith;
      }
    }

    for (let prop in input) {
      if (required.has(prop)) {
        required.delete(prop);
      } else if (input[prop] === undefined) {
        // Skip undefined properties that are not required.
        continue;
      }
      if (validators.hasOwnProperty(prop)) {
        try {
          normalizedInput[prop] = validators[prop](input[prop], input);
        } catch (ex) {
          if (
            behavior.hasOwnProperty(prop) &&
            behavior[prop].hasOwnProperty("fixup")
          ) {
            behavior[prop].fixup(input);
            normalizedInput[prop] = input[prop];
          } else {
            throw new Error(
              `${name}: Invalid value for property '${prop}': ${JSON.stringify(
                input[prop]
              )}`
            );
          }
        }
      }
    }
    if (required.size > 0) {
      throw new Error(
        `${name}: The following properties were expected: ${[...required].join(
          ", "
        )}`
      );
    }
    return normalizedInput;
  },

  BOOKMARK_VALIDATORS,
  PAGEINFO_VALIDATORS,
  SYNC_BOOKMARK_VALIDATORS,
  SYNC_CHANGE_RECORD_VALIDATORS,

  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),

  _shutdownFunctions: [],
  registerShutdownFunction: function PU_registerShutdownFunction(aFunc) {
    // If this is the first registered function, add the shutdown observer.
    if (!this._shutdownFunctions.length) {
      Services.obs.addObserver(this, this.TOPIC_SHUTDOWN);
    }
    this._shutdownFunctions.push(aFunc);
  },

  // nsIObserver
  observe: function PU_observe(aSubject, aTopic) {
    switch (aTopic) {
      case this.TOPIC_SHUTDOWN:
        Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
        while (this._shutdownFunctions.length) {
          this._shutdownFunctions.shift().apply(this);
        }
        break;
    }
  },

  /**
   * Determines whether or not a ResultNode is a host container.
   * @param   aNode
   *          A result node
   * @returns true if the node is a host container, false otherwise
   */
  nodeIsHost: function PU_nodeIsHost(aNode) {
    return (
      aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
      aNode.parent &&
      asQuery(aNode.parent).queryOptions.resultType ==
        Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY
    );
  },

  /**
   * Determines whether or not a ResultNode is a day container.
   * @param   node
   *          A NavHistoryResultNode
   * @returns true if the node is a day container, false otherwise
   */
  nodeIsDay: function PU_nodeIsDay(aNode) {
    var resultType;
    return (
      aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
      aNode.parent &&
      ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
        Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
        resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY)
    );
  },

  /**
   * Determines whether or not a result-node is a tag container.
   * @param   aNode
   *          A result-node
   * @returns true if the node is a tag container, false otherwise
   */
  nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
    if (aNode.type != Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
      return false;
    }
    // Direct child of RESULTS_AS_TAGS_ROOT.
    let parent = aNode.parent;
    if (
      parent &&
      PlacesUtils.asQuery(parent).queryOptions.resultType ==
        Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT
    ) {
      return true;
    }
    // We must also support the right pane of the Library, when the tag query
    // is the root node. Unfortunately this is also valid for any tag query
    // selected in the left pane that is not a direct child of RESULTS_AS_TAGS_ROOT.
    if (
      !parent &&
      aNode == aNode.parentResult.root &&
      PlacesUtils.asQuery(aNode).query.tags.length == 1
    ) {
      return true;
    }
    return false;
  },

  /**
   * Determines whether or not a ResultNode is a container.
   * @param   aNode
   *          A result node
   * @returns true if the node is a container item, false otherwise
   */
  containerTypes: [
    Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
    Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
    Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
  ],
  nodeIsContainer: function PU_nodeIsContainer(aNode) {
    return this.containerTypes.includes(aNode.type);
  },

  /**
   * Determines whether or not a ResultNode is an history related container.
   * @param   node
   *          A result node
   * @returns true if the node is an history related container, false otherwise
   */
  nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
    var resultType;
    return (
      this.nodeIsQuery(aNode) &&
      ((resultType = asQuery(aNode).queryOptions.resultType) ==
        Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
        resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
        resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
        this.nodeIsDay(aNode) ||
        this.nodeIsHost(aNode))
    );
  },

  /**
   * Gets the concrete item-guid for the given node. For everything but folder
   * shortcuts, this is just node.bookmarkGuid.  For folder shortcuts, this is
   * node.targetFolderGuid (see nsINavHistoryService.idl for the semantics).
   *
   * @param aNode
   *        a result node.
   * @return the concrete item-guid for aNode.
   */
  getConcreteItemGuid(aNode) {
    if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
      return asQuery(aNode).targetFolderGuid;
    }
    return aNode.bookmarkGuid;
  },

  /**
   * Reverse a host based on the moz_places algorithm, that is reverse the host
   * string and add a trailing period.  For example "google.com" becomes
   * "moc.elgoog.".
   *
   * @param url
   *        the URL to generate a rev host for.
   * @return the reversed host string.
   */
  getReversedHost(url) {
    return url.host.split("").reverse().join("") + ".";
  },

  /**
   * String-wraps a result node according to the rules of the specified
   * content type for copy or move operations.
   *
   * @param   aNode
   *          The Result node to wrap (serialize)
   * @param   aType
   *          The content type to serialize as
   * @return  A string serialization of the node
   */
  wrapNode(aNode, aType) {
    // 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 pane of the Library.
    // Since this method is only used for text/plain and text/html, we can
    // use the concrete GUID without the risk of dragging root folders.
    function gatherTextDataFromNode(node, gatherDataFunc) {
      if (
        PlacesUtils.nodeIsFolderOrShortcut(node) &&
        asQuery(node).queryOptions.excludeItems
      ) {
        let folderRoot = PlacesUtils.getFolderContents(
          PlacesUtils.getConcreteItemGuid(node),
          false,
          true
        ).root;
        try {
          return gatherDataFunc(folderRoot);
        } finally {
          folderRoot.containerOpen = false;
        }
      }
      return gatherDataFunc(node);
    }

    function gatherDataHtml(node, recursiveOpen = true) {
      let htmlEscape = s =>
        s
          .replace(/&/g, "&")
          .replace(/>/g, ">")
          .replace(/</g, "<")
          .replace(/"/g, """)
          .replace(/'/g, "'");

      // escape out potential HTML in the title
      let escapedTitle = node.title ? htmlEscape(node.title) : "";

      if (PlacesUtils.nodeIsContainer(node)) {
        asContainer(node);

        let childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;

        let wasOpen = node.containerOpen;
        if (!wasOpen && recursiveOpen) {
          node.containerOpen = true;
        }
        if (node.containerOpen) {
          for (let i = 0, count = node.childCount; i < count; ++i) {
            let child = node.getChild(i);
            // We only allow recursively opening concrete folders, not queries
            // nor shortcuts, to avoid the possibility of walking infinite loops.
            childString +=
              "<DD>" +
              NEWLINE +
              gatherDataHtml(child, PlacesUtils.nodeIsConcreteFolder(child)) +
              "</DD>" +
              NEWLINE;
          }
          node.containerOpen = wasOpen;
        }
        return childString + "</DL>" + NEWLINE;
      }
      if (PlacesUtils.nodeIsURI(node)) {
        return `<A HREF="${node.uri}">${escapedTitle}</A>${NEWLINE}`;
      }
      if (PlacesUtils.nodeIsSeparator(node)) {
        return "<HR>" + NEWLINE;
      }
      return "";
    }

    function gatherDataText(node, recursiveOpen = true) {
      if (PlacesUtils.nodeIsContainer(node)) {
        asContainer(node);

        let childString = node.title + NEWLINE;

        let wasOpen = node.containerOpen;
        if (!wasOpen && recursiveOpen) {
          node.containerOpen = true;
        }
        if (node.containerOpen) {
          for (let i = 0, count = node.childCount; i < count; ++i) {
            let child = node.getChild(i);
            let suffix = i < count - 1 ? NEWLINE : "";
            childString +=
              gatherDataText(child, PlacesUtils.nodeIsConcreteFolder(child)) +
              suffix;
          }
          node.containerOpen = wasOpen;
        }
        return childString;
      }
      if (PlacesUtils.nodeIsURI(node)) {
        return node.uri;
      }
      if (PlacesUtils.nodeIsSeparator(node)) {
        return "--------------------";
      }
      return "";
    }

    switch (aType) {
      case this.TYPE_X_MOZ_PLACE:
      case this.TYPE_X_MOZ_PLACE_SEPARATOR:
      case this.TYPE_X_MOZ_PLACE_CONTAINER: {
        // Serialize the node to JSON.
        return serializeNode(aNode);
      }
      case this.TYPE_X_MOZ_URL: {
        if (PlacesUtils.nodeIsURI(aNode)) {
          return aNode.uri + NEWLINE + aNode.title;
        }
        if (PlacesUtils.nodeIsContainer(aNode)) {
          return PlacesUtils.getURLsForContainerNode(aNode)
            .map(item => item.uri + "\n" + item.title)
            .join("\n");
        }
        return "";
      }
      case this.TYPE_HTML: {
        return gatherTextDataFromNode(aNode, gatherDataHtml);
      }
    }

    // Otherwise, we wrap as TYPE_PLAINTEXT.
    return gatherTextDataFromNode(aNode, gatherDataText);
  },

  /**
   * Unwraps data from the Clipboard or the current Drag Session.
   * @param   blob
   *          A blob (string) of data, in some format we potentially know how
   *          to parse.
   * @param   type
   *          The content type of the blob.
   * @returns An array of objects representing each item contained by the source.
   * @throws if the blob contains invalid data.
   */
  unwrapNodes: function PU_unwrapNodes(blob, type) {
    // We split on "\n"  because the transferable system converts "\r\n" to "\n"
    let validNodes = [];
    let invalidNodes = [];
    switch (type) {
      case this.TYPE_X_MOZ_PLACE:
      case this.TYPE_X_MOZ_PLACE_SEPARATOR:
      case this.TYPE_X_MOZ_PLACE_CONTAINER:
        validNodes = JSON.parse("[" + blob + "]");
        break;
      case this.TYPE_X_MOZ_URL: {
        let 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
        // but drag and drop of files from the shell has parts.length = 1
        if (parts.length != 1 && parts.length % 2) {
          break;
        }
        for (let i = 0; i < parts.length; i = i + 2) {
          let uriString = parts[i].trimStart();
          if (!uriString) {
            continue;
          }
          let titleString = "";
          if (parts.length > i + 1) {
            titleString = parts[i + 1];
          } else {
            // for drag and drop of files, try to use the leafName as title
            try {
              titleString = Services.io
                .newURI(uriString)
                .QueryInterface(Ci.nsIURL).fileName;
            } catch (ex) {}
          }

          try {
            let uri = Services.io.newURI(uriString);
            if (uri.scheme != "place") {
              validNodes.push({
                uri: uriString,
                title: titleString ? titleString : uriString,
                type: this.TYPE_X_MOZ_URL,
              });
            }
          } catch (e) {
            console.error(e);
            invalidNodes.push({
              uri: uriString,
            });
          }
        }
        break;
      }
      case this.TYPE_PLAINTEXT: {
        let parts = blob.split("\n");
        for (let i = 0; i < parts.length; i++) {
          let uriString = parts[i].trimStart();
          // text/uri-list is converted to TYPE_PLAINTEXT but it could contain
          // comments line prepended by #, we should skip them, as well as
          // empty uris.
          if (uriString.substr(0, 1) == "\x23" || uriString == "") {
            continue;
          }
          try {
            let uri = Services.io.newURI(uriString);
            if (uri.scheme != "place") {
              validNodes.push({
                uri: uriString,
                title: uriString,
                type: this.TYPE_X_MOZ_URL,
              });
            }
          } catch (e) {
            console.error(e);
            invalidNodes.push({
              uri: uriString,
            });
          }
        }
        break;
      }
      default:
        throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
    }

    return { validNodes, invalidNodes };
  },

  /**
   * Validate an input PageInfo object, returning a valid PageInfo object.
   *
   * @param pageInfo: (PageInfo)
   * @return (PageInfo)
   */
  validatePageInfo(pageInfo, validateVisits = true) {
    return this.validateItemProperties(
      "PageInfo",
      PAGEINFO_VALIDATORS,
      pageInfo,
      {
        url: { requiredIf: b => !b.guid },
        guid: { requiredIf: b => !b.url },
        visits: { requiredIf: () => validateVisits },
      }
    );
  },
  /**
   * Normalize a key to either a string (if it is a valid GUID) or an
   * instance of `URL` (if it is a `URL`, `nsIURI`, or a string
   * representing a valid url).
   *
   * @throws (TypeError)
   *         If the key is neither a valid guid nor a valid url.
   */
  normalizeToURLOrGUID(key) {
    if (typeof key === "string") {
      // A string may be a URL or a guid
      if (this.isValidGuid(key)) {
        return key;
      }
      return new URL(key);
    }
    if (URL.isInstance(key)) {
      return key;
    }
    if (key instanceof Ci.nsIURI) {
      return URL.fromURI(key);
    }
    throw new TypeError("Invalid url or guid: " + key);
  },

  /**
   * Generates a nsINavHistoryResult for the contents of a folder.
   * @param   aFolderGuid
   *          The folder to open
   * @param   [optional] excludeItems
   *          True to hide all items (individual bookmarks). This is used on
   *          the left places pane so you just get a folder hierarchy.
   * @param   [optional] expandQueries
   *          True to make query items expand as new containers. For managing,
   *          you want this to be false, for menus and such, you want this to
   *          be true.
   * @returns A nsINavHistoryResult containing the contents of the
   *          folder. The result.root is guaranteed to be open.
   */
  getFolderContents(aFolderGuid, aExcludeItems, aExpandQueries) {
    if (!this.isValidGuid(aFolderGuid)) {
      throw new Error("aFolderGuid should be a valid GUID.");
    }
    var query = this.history.getNewQuery();
    query.setParents([aFolderGuid]);
    var options = this.history.getNewQueryOptions();
    options.excludeItems = aExcludeItems;
    options.expandQueries = aExpandQueries;

    var result = this.history.executeQuery(query, options);
    result.root.containerOpen = true;
    return result;
  },

  // Identifier getters for special folders.
  // You should use these everywhere PlacesUtils is available to avoid XPCOM
  // traversal just to get roots' ids.
  get tagsFolderId() {
    delete this.tagsFolderId;
    return (this.tagsFolderId = this.bookmarks.tagsFolder);
  },

  /**
   * Checks if item is a root.
   *
   * @param {String} guid The guid of the item to look for.
   * @returns {Boolean} true if guid is a root, false otherwise.
   */
  isRootItem(guid) {
    return (
      guid == PlacesUtils.bookmarks.menuGuid ||
      guid == PlacesUtils.bookmarks.toolbarGuid ||
      guid == PlacesUtils.bookmarks.unfiledGuid ||
      guid == PlacesUtils.bookmarks.tagsGuid ||
      guid == PlacesUtils.bookmarks.rootGuid ||
      guid == PlacesUtils.bookmarks.mobileGuid
    );
  },

  /**
   * Returns a nsNavHistoryContainerResultNode with forced excludeItems and
   * expandQueries.
   * @param   aNode
   *          The node to convert
   * @param   [optional] excludeItems
   *          True to hide all items (individual bookmarks). This is used on
   *          the left places pane so you just get a folder hierarchy.
   * @param   [optional] expandQueries
   *          True to make query items expand as new containers. For managing,
   *          you want this to be false, for menus and such, you want this to
   *          be true.
   * @returns A nsINavHistoryContainerResultNode containing the unfiltered
   *          contents of the container.
   * @note    The returned container node could be open or closed, we don't
   *          guarantee its status.
   */
  getContainerNodeWithOptions: function PU_getContainerNodeWithOptions(
    aNode,
    aExcludeItems,
    aExpandQueries
  ) {
    if (!this.nodeIsContainer(aNode)) {
      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
    }

    // excludeItems is inherited by child containers in an excludeItems view.
    var excludeItems =
      asQuery(aNode).queryOptions.excludeItems ||
      asQuery(aNode.parentResult.root).queryOptions.excludeItems;
    // expandQueries is inherited by child containers in an expandQueries view.
    var expandQueries =
      asQuery(aNode).queryOptions.expandQueries &&
      asQuery(aNode.parentResult.root).queryOptions.expandQueries;

    // If our options are exactly what we expect, directly return the node.
    if (excludeItems == aExcludeItems && expandQueries == aExpandQueries) {
      return aNode;
    }

    // Otherwise, get contents manually.
    var query = {},
      options = {};
    this.history.queryStringToQuery(aNode.uri, query, options);
    options.value.excludeItems = aExcludeItems;
    options.value.expandQueries = aExpandQueries;
    return this.history.executeQuery(query.value, options.value).root;
  },

  /**
   * Returns true if a container has uri nodes in its first level.
   * Has better performance than (getURLsForContainerNode(node).length > 0).
   * @param aNode
   *        The container node to search through.
   * @returns true if the node contains uri nodes, false otherwise.
   */
  hasChildURIs: function PU_hasChildURIs(aNode) {
    if (!this.nodeIsContainer(aNode)) {
      return false;
    }

    let root = this.getContainerNodeWithOptions(aNode, false, true);
    let result = root.parentResult;
    let didSuppressNotifications = false;
    let wasOpen = root.containerOpen;
    if (!wasOpen) {
      didSuppressNotifications = result.suppressNotifications;
      if (!didSuppressNotifications) {
        result.suppressNotifications = true;
      }

      root.containerOpen = true;
    }

    let found = false;
    for (let i = 0, count = root.childCount; i < count && !found; i++) {
      let child = root.getChild(i);
      if (this.nodeIsURI(child)) {
        found = true;
      }
    }

    if (!wasOpen) {
      root.containerOpen = false;
      if (!didSuppressNotifications) {
        result.suppressNotifications = false;
      }
    }
    return found;
  },

  /**
   * Returns an array containing all the uris in the first level of the
   * passed in container.
   * If you only need to know if the node contains uris, use hasChildURIs.
   * @param aNode
   *        The container node to search through
   * @returns array of uris in the first level of the container.
   */
  getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
    let urls = [];
    if (!this.nodeIsContainer(aNode)) {
      return urls;
    }

    let root = this.getContainerNodeWithOptions(aNode, false, true);
    let result = root.parentResult;
    let wasOpen = root.containerOpen;
    let didSuppressNotifications = false;
    if (!wasOpen) {
      didSuppressNotifications = result.suppressNotifications;
      if (!didSuppressNotifications) {
        result.suppressNotifications = true;
      }

      root.containerOpen = true;
    }

    for (let i = 0, count = root.childCount; i < count; ++i) {
      let child = root.getChild(i);
      if (this.nodeIsURI(child)) {
        urls.push({
          uri: child.uri,
          isBookmark: this.nodeIsBookmark(child),
          title: child.title,
        });
      }
    }

    if (!wasOpen) {
      root.containerOpen = false;
      if (!didSuppressNotifications) {
        result.suppressNotifications = false;
      }
    }
    return urls;
  },

  /**
   * Gets a shared Sqlite.sys.mjs readonly connection to the Places database,
   * usable only for SELECT queries.
   *
   * This is intended to be used mostly internally, components outside of
   * Places should, when possible, use API calls and file bugs to get proper
   * APIs, where they are missing.
   * Keep in mind the Places DB schema is by no means frozen or even stable.
   * Your custom queries can - and will - break overtime.
   *
   * Example:
   * let db = await PlacesUtils.promiseDBConnection();
   * let rows = await db.executeCached(sql, params);
   */
  promiseDBConnection: () => lazy.gAsyncDBConnPromised,

  /**
   * This is pretty much the same as promiseDBConnection, but with a larger
   * page cache, useful for consumers doing large table scans, like the urlbar.
   * @see promiseDBConnection
   */
  promiseLargeCacheDBConnection: () => lazy.gAsyncDBLargeCacheConnPromised,
  get largeCacheDBConnDeferred() {
    return gAsyncDBLargeCacheConnDeferred;
  },

  /**
   * Returns a Sqlite.sys.mjs wrapper for the main Places connection. Most callers
   * should prefer `withConnectionWrapper`, which ensures that all database
   * operations finish before the connection is closed.
   */
  promiseUnsafeWritableDBConnection: () => lazy.gAsyncDBWrapperPromised,

  /**
   * Performs a read/write operation on the Places database through a Sqlite.sys.mjs
   * wrapped connection to the Places database.
   *
   * This is intended to be used only by Places itself, always use APIs if you
   * need to modify the Places database. Use promiseDBConnection if you need to
   * SELECT from the database and there's no covering API.
   * Keep in mind the Places DB schema is by no means frozen or even stable.
   * Your custom queries can - and will - break overtime.
   *
   * As all operations on the Places database are asynchronous, if shutdown
   * is initiated while an operation is pending, this could cause dataloss.
   * Using `withConnectionWrapper` ensures that shutdown waits until all
   * operations are complete before proceeding.
   *
   * Example:
   * await withConnectionWrapper("Bookmarks: Remove a bookmark", Task.async(function*(db) {
   *    // Proceed with the db, asynchronously.
   *    // Shutdown will not interrupt operations that take place here.
   * }));
   *
   * @param {string} name The name of the operation. Used for debugging, logging
   *   and crash reporting.
   * @param {function(db)} task A function that takes as argument a Sqlite.sys.mjs
   *   connection and returns a Promise. Shutdown is guaranteed to not interrupt
   *   execution of `task`.
   */
  async withConnectionWrapper(name, task) {
    if (!name) {
      throw new TypeError("Expecting a user-readable name");
    }
    let db = await lazy.gAsyncDBWrapperPromised;
    return db.executeBeforeShutdown(name, task);
  },

  /**
   * Gets favicon data for a given page url.
   *
   * @param {string | URL | nsIURI} aPageUrl
   *   url of the page to look favicon for.
   * @param {number} preferredWidth
   *   The preferred width of the favicon in pixels. The default value of 0
   *   returns the largest icon available.
   * @resolves to an object representing a favicon entry, having the following
   *           properties: { uri, dataLen, data, mimeType }
   * @rejects JavaScript exception if the given url has no associated favicon.
   */
  promiseFaviconData(aPageUrl, preferredWidth = 0) {
    return new Promise((resolve, reject) => {
      if (!(aPageUrl instanceof Ci.nsIURI)) {
        aPageUrl = PlacesUtils.toURI(aPageUrl);
      }
      PlacesUtils.favicons.getFaviconDataForPage(
        aPageUrl,
        function (uri, dataLen, data, mimeType, size) {
          if (uri) {
            resolve({ uri, dataLen, data, mimeType, size });
          } else {
            reject();
          }
        },
        preferredWidth
      );
    });
  },

  /**
   * Returns the passed URL with a #size ref for the specified size and
   * devicePixelRatio.
   *
   * @param window
   *        The window where the icon will appear.
   * @param href
   *        The string href we should add the ref to.
   * @param size
   *        The target image size
   * @return The URL with the fragment at the end, in the same formar as input.
   */
  urlWithSizeRef(window, href, size) {
    return (
      href +
      (href.includes("#") ? "&" : "#") +
      "size=" +
      Math.round(size) * window.devicePixelRatio
    );
  },

  /**
   * Asynchronously retrieve a JS-object representation of a places bookmarks
   * item (a bookmark, a folder, or a separator) along with all of its
   * descendants.
   *
   * @param [optional] aItemGuid
   *        the (topmost) item to be queried.  If it's not passed, the places
   *        root is queried: that is, you get a representation of the entire
   *        bookmarks hierarchy.
   * @param [optional] aOptions
   *        Options for customizing the query behavior, in the form of a JS
   *        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}
   * @resolves to a JS object that represents either a single item or a
   * bookmarks tree.  Each node in the tree has the following properties set:
   *  - guid (string): the item's GUID (same as aItemGuid for the top item).
   *  - [deprecated] id (number): the item's id. This is only if
   *    aOptions.includeItemIds is set.
   *  - type (string):  the item's type.  @see PlacesUtils.TYPE_X_*
   *  - typeCode (number):  the item's type in numeric format.
   *    @see PlacesUtils.bookmarks.TYPE_*
   *  - title (string): the item's title. If it has no title, this property
   *    isn't set.
   *  - dateAdded (number, microseconds from the epoch): the date-added value of
   *    the item.
   *  - lastModified (number, microseconds from the epoch): the last-modified
   *    value of the item.
   *  - index: the item's index under it's parent.
   *
   * The root object (i.e. the one for aItemGuid) also has the following
   * properties set:
   *  - parentGuid (string): the GUID of the root's parent.  This isn't set if
   *    the root item is the places root.
   *  - itemsCount (number, not enumerable): the number of items, including the
   *    root item itself, which are represented in the resolved object.
   *
   * Bookmark items also have the following properties:
   *  - uri (string): the item's url.
   *  - tags (string): csv string of the bookmark's tags.
   *  - charset (string): the last known charset of the bookmark.
   *  - keyword (string): the bookmark's keyword (unset if none).
   *  - postData (string): the bookmark's keyword postData (unset if none).
   *  - iconUri (string): the bookmark's favicon url.
   * The last four properties are not set at all if they're irrelevant (e.g.
   * |charset| is not set if no charset was previously set for the bookmark
   * url).
   *
   * 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.
   *
   * @rejects if the query failed for any reason.
   * @note if aItemGuid points to a non-existent item, the returned promise is
   * resolved to null.
   */
  async promiseBookmarksTree(aItemGuid = "", aOptions = {}) {
    let createItemInfoObject = async function (aRow, aIncludeParentGuid) {
      let item = {};
      let copyProps = (...props) => {
        for (let prop of props) {
          let val = aRow.getResultByName(prop);
          if (val !== null) {
            item[prop] = val;
          }
        }
      };
      copyProps("guid", "title", "index", "dateAdded", "lastModified");
      if (aIncludeParentGuid) {
        copyProps("parentGuid");
      }

      let itemId = aRow.getResultByName("id");
      if (aOptions.includeItemIds) {
        item.id = itemId;
      }

      let type = aRow.getResultByName("type");
      item.typeCode = type;
      if (type == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
        copyProps("charset", "tags", "iconUri");
      }

      switch (type) {
        case PlacesUtils.bookmarks.TYPE_BOOKMARK: {
          item.type = PlacesUtils.TYPE_X_MOZ_PLACE;
          // If this throws due to an invalid url, the item will be skipped.
          try {
            item.uri = new URL(aRow.getResultByName("url")).href;
          } catch (ex) {
            let error = new Error("Invalid bookmark URL");
            error.becauseInvalidURL = true;
            throw error;
          }
          // Keywords are cached, so this should be decently fast.
          let entry = await PlacesUtils.keywords.fetch({ url: item.uri });
          if (entry) {
            item.keyword = entry.keyword;
            item.postData = entry.postData;
          }
          break;
        }
        case PlacesUtils.bookmarks.TYPE_FOLDER:
          item.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
          // Mark root folders.
          if (item.guid == PlacesUtils.bookmarks.rootGuid) {
            item.root = "placesRoot";
          } else if (item.guid == PlacesUtils.bookmarks.menuGuid) {
            item.root = "bookmarksMenuFolder";
          } else if (item.guid == PlacesUtils.bookmarks.unfiledGuid) {
            item.root = "unfiledBookmarksFolder";
          } else if (item.guid == PlacesUtils.bookmarks.toolbarGuid) {
            item.root = "toolbarFolder";
          } else if (item.guid == PlacesUtils.bookmarks.mobileGuid) {
            item.root = "mobileFolder";
          }
          break;
        case PlacesUtils.bookmarks.TYPE_SEPARATOR:
          item.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
          break;
        default:
          console.error(`Unexpected bookmark type ${type}`);
          break;
      }
      return item;
    };

    const QUERY_STR = `/* do not warn (bug no): cannot use an index */
       WITH RECURSIVE
       descendants(fk, level, type, id, guid, parent, parentGuid, position,
                   title, dateAdded, lastModified) AS (
         SELECT b1.fk, 0, b1.type, b1.id, b1.guid, b1.parent,
                (SELECT guid FROM moz_bookmarks WHERE id = b1.parent),
                b1.position, b1.title, b1.dateAdded, b1.lastModified
         FROM moz_bookmarks b1 WHERE b1.guid=:item_guid
         UNION ALL
         SELECT b2.fk, level + 1, b2.type, b2.id, b2.guid, b2.parent,
                descendants.guid, b2.position, b2.title, b2.dateAdded,
                b2.lastModified
         FROM moz_bookmarks b2
         JOIN descendants ON b2.parent = descendants.id AND b2.id <> :tags_folder),
       tagged(place_id, tags) AS (
         SELECT b.fk, group_concat(p.title ORDER BY p.title)
         FROM moz_bookmarks b
         JOIN moz_bookmarks p ON p.id = b.parent
         JOIN moz_bookmarks g ON g.id = p.parent
         WHERE g.guid = '${PlacesUtils.bookmarks.tagsGuid}'
         GROUP BY b.fk
       )
       SELECT d.level, d.id, d.guid, d.parent, d.parentGuid, d.type,
              d.position AS [index], IFNULL(d.title, '') AS title, d.dateAdded,
              d.lastModified, h.url, (SELECT icon_url FROM moz_icons i
                      JOIN moz_icons_to_pages ON icon_id = i.id
                      JOIN moz_pages_w_icons pi ON page_id = pi.id
                      WHERE pi.page_url_hash = hash(h.url) AND pi.page_url = h.url
                      ORDER BY (flags & ${Ci.nsIFaviconService.ICONDATA_FLAGS_RICH}) ASC, width DESC LIMIT 1) AS iconUri,
              (SELECT tags FROM tagged WHERE place_id = h.id) AS tags,
              (SELECT a.content FROM moz_annos a
               JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
               WHERE place_id = h.id AND n.name = :charset_anno
              ) AS charset
       FROM descendants d
       LEFT JOIN moz_bookmarks b3 ON b3.id = d.parent
       LEFT JOIN moz_places h ON h.id = d.fk
       ORDER BY d.level, d.parent, d.position`;

    if (!aItemGuid) {
      aItemGuid = this.bookmarks.rootGuid;
    }

    let hasExcludeItemsCallback = aOptions.hasOwnProperty(
      "excludeItemsCallback"
    );
    let excludedParents = new Set();
    let shouldExcludeItem = (aItem, aParentGuid) => {
      let exclude =
        excludedParents.has(aParentGuid) ||
        aOptions.excludeItemsCallback(aItem);
      if (exclude) {
        if (aItem.type == this.TYPE_X_MOZ_PLACE_CONTAINER) {
          excludedParents.add(aItem.guid);
        }
      }
      return exclude;
    };

    let rootItem = null;
    let parentsMap = new Map();
    let conn = await this.promiseDBConnection();
    let rows = await conn.executeCached(QUERY_STR, {
      tags_folder: PlacesUtils.tagsFolderId,
      charset_anno: PlacesUtils.CHARSET_ANNO,
      item_guid: aItemGuid,
    });
    let yieldCounter = 0;
    for (let row of rows) {
      let item;
      if (!rootItem) {
        try {
          // This is the first row.
          rootItem = item = await createItemInfoObject(row, true);
          Object.defineProperty(rootItem, "itemsCount", {
            value: 1,
            writable: true,
            enumerable: false,
            configurable: false,
          });
        } catch (ex) {
          console.error("Failed to fetch the data for the root item");
          throw ex;
        }
      } else {
        try {
          // Our query guarantees that we always visit parents ahead of their
          // children.
          item = await createItemInfoObject(row, false);
          let parentGuid = row.getResultByName("parentGuid");
          if (hasExcludeItemsCallback && shouldExcludeItem(item, parentGuid)) {
            continue;
          }

          let parentItem = parentsMap.get(parentGuid);
          if ("children" in parentItem) {
            parentItem.children.push(item);
          } else {
            parentItem.children = [item];
          }

          rootItem.itemsCount++;
        } catch (ex) {
          // This is a bogus child, report and skip it.
          console.error("Failed to fetch the data for an item ", ex);
          continue;
        }
      }

      if (item.type == this.TYPE_X_MOZ_PLACE_CONTAINER) {
        parentsMap.set(item.guid, item);
      }

      // With many bookmarks we end up stealing the CPU - even with yielding!
      // So we let everyone else have a go every few items (bug 1186714).
      if (++yieldCounter % 50 == 0) {
        await new Promise(resolve => {
          Services.tm.dispatchToMainThread(resolve);
        });
      }
    }

    return rootItem;
  },

  /**
   * Returns a generator that iterates over `array` and yields slices of no
   * more than `chunkLength` elements at a time.
   *
   * @param  {Array} array An array containing zero or more elements.
   * @param  {number} chunkLength The maximum number of elements in each chunk.
   * @yields {Array} A chunk of the array.
   * @throws if `chunkLength` is negative or not an integer.
   */
  *chunkArray(array, chunkLength) {
    if (chunkLength <= 0 || !Number.isInteger(chunkLength)) {
      throw new TypeError("Chunk length must be a positive integer");
    }
    if (!array.length) {
      return;
    }
    if (array.length <= chunkLength) {
      yield array;
      return;
    }
    let startIndex = 0;
    while (startIndex < array.length) {
      yield array.slice(startIndex, (startIndex += chunkLength));
    }
  },

  /**
   * Returns SQL placeholders to bind multiple values into an IN clause.
   * @param {Array|number} info
   *   Array or number of entries to create.
   * @param {string} [prefix]
   *   String prefix to add before the SQL param.
   * @param {string} [suffix]
   *   String suffix to add after the SQL param.
   */
  sqlBindPlaceholders(info, prefix = "", suffix = "") {
    let length = Array.isArray(info) ? info.length : info;
    return new Array(length).fill(prefix + "?" + suffix).join(",");
  },

  /**
   * Run some text through md5 and return the hash.
   * @param {string} data The string to hash.
   * @param {string} [format] Which format of the hash to return:
   *   - "base64" for ascii format.
   *   - "hex" for hex format.
   * @returns {string} hash of the input data in the required format.
   * @deprecated use sha256 instead.
   */
  md5(data, { format = "base64" } = {}) {
    let hasher = new lazy.CryptoHash("md5");
    // Convert the data to a byte array for hashing.
    data = new TextEncoder().encode(data);
    hasher.update(data, data.length);
    switch (format) {
      case "hex": {
        let hash = hasher.finish(false);
        return Array.from(hash, (c, i) =>
          hash.charCodeAt(i).toString(16).padStart(2, "0")
        ).join("");
      }
      case "base64":
      default:
        return hasher.finish(true);
    }
  },

  /**
   * Run some text through SHA256 and return the hash.
   * @param {string|nsIStringInputStream} data The data to hash.
   * @param {string} [format] Which format of the hash to return:
   *   - "base64" (default) for ascii format, not safe for URIs or file names.
   *   - "hex" for hex format.
   *   - "base64url" for ascii format safe to be used in file names (RFC 4648).
   *       You should normally use the "hex" format for file names, but if the
   *       user may manipulate the file, it would be annoying to have very long
   *       and unreadable file names, thus this provides a shorter alternative.
   *       Note padding "=" are untouched and may have to be encoded in URIs.
   * @returns {string} hash of the input data in the required format.
   */
  sha256(data, { format = "base64" } = {}) {
    let hasher = new lazy.CryptoHash("sha256");
    if (data instanceof Ci.nsIStringInputStream) {
      hasher.updateFromStream(data, -1);
    } else {
      // Convert the data string to a byte array for hashing.
      data = new TextEncoder().encode(data);
      hasher.update(data, data.length);
    }
    switch (format) {
      case "hex": {
        let hash = hasher.finish(false);
        return Array.from(hash, (c, i) =>
          hash.charCodeAt(i).toString(16).padStart(2, "0")
        ).join("");
      }
      case "base64url":
        return hasher.finish(true).replaceAll("+", "-").replaceAll("/", "_");
      case "base64":
      default:
        return hasher.finish(true);
    }
  },

  /**
   * Inserts a new place if one doesn't currently exist.
   *
   * This should only be used from an API that is connecting this new entry to
   * some additional foreign table. Otherwise this will just create an orphan
   * entry that could be expired at any time.
   *
   * @param db
   *        The database connection to use.
   * @param url
   *        A valid URL object.
   * @return {Promise} resolved when the operation is complete.
   */
  async maybeInsertPlace(db, url) {
    // The IGNORE conflict can trigger on `guid`.
    await db.executeCached(
      `INSERT OR IGNORE INTO moz_places (url, url_hash, rev_host, hidden, frecency, guid)
      VALUES (:url, hash(:url), :rev_host,
              (CASE WHEN :url BETWEEN 'place:' AND 'place:' || X'FFFF' THEN 1 ELSE 0 END),
              :frecency,
              IFNULL((SELECT guid FROM moz_places WHERE url_hash = hash(:url) AND url = :url),
                      GENERATE_GUID()))
      `,
      {
        url: url.href,
        rev_host: this.getReversedHost(url),
        frecency: url.protocol == "place:" ? 0 : -1,
      }
    );
  },

  /**
   * Tries to insert a set of new places if they don't exist yet.
   *
   * This should only be used from an API that is connecting this new entry to
   * some additional foreign table. Otherwise this will just create an orphan
   * entry that could be expired at any time.
   *
   * @param db
   *        The database to use
   * @param urls
--> --------------------

--> maximum size reached

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

[ zur Elbe Produktseite wechseln0.48Quellennavigators  Analyse erneut starten  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge