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


Quelle  history.sys.mjs   Sprache: unbekannt

 
Spracherkennung für: .mjs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

/* 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/. */

const HISTORY_TTL = 5184000; // 60 days in milliseconds
const THIRTY_DAYS_IN_MS = 2592000000; // 30 days in milliseconds
// Sync may bring new fields from other clients, not yet understood by our engine.
// Unknown fields outside these fields are aggregated into 'unknownFields' and
// safely synced to prevent data loss.
const VALID_HISTORY_FIELDS = ["id", "title", "histUri", "visits"];
const VALID_VISIT_FIELDS = ["date", "type", "transition"];

import { Async } from "resource://services-common/async.sys.mjs";
import { CommonUtils } from "resource://services-common/utils.sys.mjs";

import {
  MAX_HISTORY_DOWNLOAD,
  MAX_HISTORY_UPLOAD,
  SCORE_INCREMENT_SMALL,
  SCORE_INCREMENT_XLARGE,
} from "resource://services-sync/constants.sys.mjs";

import {
  Store,
  SyncEngine,
  LegacyTracker,
} from "resource://services-sync/engines.sys.mjs";
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
import { Utils } from "resource://services-sync/util.sys.mjs";

const lazy = {};

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

export function HistoryRec(collection, id) {
  CryptoWrapper.call(this, collection, id);
}

HistoryRec.prototype = {
  _logName: "Sync.Record.History",
  ttl: HISTORY_TTL,
};
Object.setPrototypeOf(HistoryRec.prototype, CryptoWrapper.prototype);

Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]);

export function HistoryEngine(service) {
  SyncEngine.call(this, "History", service);
}

HistoryEngine.prototype = {
  _recordObj: HistoryRec,
  _storeObj: HistoryStore,
  _trackerObj: HistoryTracker,
  downloadLimit: MAX_HISTORY_DOWNLOAD,

  syncPriority: 7,

  async getSyncID() {
    return lazy.PlacesSyncUtils.history.getSyncId();
  },

  async ensureCurrentSyncID(newSyncID) {
    this._log.debug(
      "Checking if server sync ID ${newSyncID} matches existing",
      { newSyncID }
    );
    await lazy.PlacesSyncUtils.history.ensureCurrentSyncId(newSyncID);
    return newSyncID;
  },

  async resetSyncID() {
    // First, delete the collection on the server. It's fine if we're
    // interrupted here: on the next sync, we'll detect that our old sync ID is
    // now stale, and start over as a first sync.
    await this._deleteServerCollection();
    // Then, reset our local sync ID.
    return this.resetLocalSyncID();
  },

  async resetLocalSyncID() {
    let newSyncID = await lazy.PlacesSyncUtils.history.resetSyncId();
    this._log.debug("Assigned new sync ID ${newSyncID}", { newSyncID });
    return newSyncID;
  },

  async getLastSync() {
    let lastSync = await lazy.PlacesSyncUtils.history.getLastSync();
    return lastSync;
  },

  async setLastSync(lastSync) {
    await lazy.PlacesSyncUtils.history.setLastSync(lastSync);
  },

  shouldSyncURL(url) {
    return !url.startsWith("file:");
  },

  async pullNewChanges() {
    const changedIDs = await this._tracker.getChangedIDs();
    let modifiedGUIDs = Object.keys(changedIDs);
    if (!modifiedGUIDs.length) {
      return {};
    }

    let guidsToRemove =
      await lazy.PlacesSyncUtils.history.determineNonSyncableGuids(
        modifiedGUIDs
      );
    await this._tracker.removeChangedID(...guidsToRemove);
    return changedIDs;
  },

  async _resetClient() {
    await super._resetClient();
    await lazy.PlacesSyncUtils.history.reset();
  },
};
Object.setPrototypeOf(HistoryEngine.prototype, SyncEngine.prototype);

function HistoryStore(name, engine) {
  Store.call(this, name, engine);
}

HistoryStore.prototype = {
  // We try and only update this many visits at one time.
  MAX_VISITS_PER_INSERT: 500,

  // Some helper functions to handle GUIDs
  async setGUID(uri, guid) {
    if (!guid) {
      guid = Utils.makeGUID();
    }

    try {
      await lazy.PlacesSyncUtils.history.changeGuid(uri, guid);
    } catch (e) {
      this._log.error("Error setting GUID ${guid} for URI ${uri}", guid, uri);
    }

    return guid;
  },

  async GUIDForUri(uri, create) {
    // Use the existing GUID if it exists
    let guid;
    try {
      guid = await lazy.PlacesSyncUtils.history.fetchGuidForURL(uri);
    } catch (e) {
      this._log.error("Error fetching GUID for URL ${uri}", uri);
    }

    // If the URI has an existing GUID, return it.
    if (guid) {
      return guid;
    }

    // If the URI doesn't have a GUID and we were indicated to create one.
    if (create) {
      return this.setGUID(uri);
    }

    // If the URI doesn't have a GUID and we didn't create one for it.
    return null;
  },

  async changeItemID(oldID, newID) {
    let info = await lazy.PlacesSyncUtils.history.fetchURLInfoForGuid(oldID);
    if (!info) {
      throw new Error(`Can't change ID for nonexistent history entry ${oldID}`);
    }
    this.setGUID(info.url, newID);
  },

  async getAllIDs() {
    let urls = await lazy.PlacesSyncUtils.history.getAllURLs({
      since: new Date(Date.now() - THIRTY_DAYS_IN_MS),
      limit: MAX_HISTORY_UPLOAD,
    });

    let urlsByGUID = {};
    for (let url of urls) {
      if (!this.engine.shouldSyncURL(url)) {
        continue;
      }
      let guid = await this.GUIDForUri(url, true);
      urlsByGUID[guid] = url;
    }
    return urlsByGUID;
  },

  async applyIncomingBatch(records, countTelemetry) {
    // Convert incoming records to mozIPlaceInfo objects which are applied as
    // either history additions or removals.
    let failed = [];
    let toAdd = [];
    let toRemove = [];
    let pageGuidsWithUnknownFields = new Map();
    let visitTimesWithUnknownFields = new Map();
    await Async.yieldingForEach(records, async record => {
      if (record.deleted) {
        toRemove.push(record);
      } else {
        try {
          let pageInfo = await this._recordToPlaceInfo(record);
          if (pageInfo) {
            toAdd.push(pageInfo);

            // Pull any unknown fields that may have come from other clients
            let unknownFields = lazy.PlacesSyncUtils.extractUnknownFields(
              record.cleartext,
              VALID_HISTORY_FIELDS
            );
            if (unknownFields) {
              pageGuidsWithUnknownFields.set(pageInfo.guid, { unknownFields });
            }

            // Visits themselves could also contain unknown fields
            for (const visit of pageInfo.visits) {
              let unknownVisitFields =
                lazy.PlacesSyncUtils.extractUnknownFields(
                  visit,
                  VALID_VISIT_FIELDS
                );
              if (unknownVisitFields) {
                // Visits don't have an id at the time of sync so we'll need
                // to use the time instead until it's inserted in the DB
                visitTimesWithUnknownFields.set(visit.date.getTime(), {
                  unknownVisitFields,
                });
              }
            }
          }
        } catch (ex) {
          if (Async.isShutdownException(ex)) {
            throw ex;
          }
          this._log.error("Failed to create a place info", ex);
          this._log.trace("The record that failed", record);
          failed.push(record.id);
          countTelemetry.addIncomingFailedReason(ex.message);
        }
      }
    });
    if (toAdd.length || toRemove.length) {
      if (toRemove.length) {
        // PlacesUtils.history.remove takes an array of visits to remove,
        // but the error semantics are tricky - a single "bad" entry will cause
        // an exception before anything is removed. So we do remove them one at
        // a time.
        await Async.yieldingForEach(toRemove, async record => {
          try {
            await this.remove(record);
          } catch (ex) {
            if (Async.isShutdownException(ex)) {
              throw ex;
            }
            this._log.error("Failed to delete a place info", ex);
            this._log.trace("The record that failed", record);
            failed.push(record.id);
            countTelemetry.addIncomingFailedReason(ex.message);
          }
        });
      }
      for (let chunk of this._generateChunks(toAdd)) {
        // Per bug 1415560, we ignore any exceptions returned by insertMany
        // as they are likely to be spurious. We do supply an onError handler
        // and log the exceptions seen there as they are likely to be
        // informative, but we still never abort the sync based on them.
        let unknownFieldsToInsert = [];
        try {
          await lazy.PlacesUtils.history.insertMany(
            chunk,
            result => {
              const placeToUpdate = pageGuidsWithUnknownFields.get(result.guid);
              // Extract the placeId from this result so we can add the unknownFields
              // to the proper table
              if (placeToUpdate) {
                unknownFieldsToInsert.push({
                  placeId: result.placeId,
                  unknownFields: placeToUpdate.unknownFields,
                });
              }
              // same for visits
              result.visits.forEach(visit => {
                let visitToUpdate = visitTimesWithUnknownFields.get(
                  visit.date.getTime()
                );
                if (visitToUpdate) {
                  unknownFieldsToInsert.push({
                    visitId: visit.visitId,
                    unknownFields: visitToUpdate.unknownVisitFields,
                  });
                }
              });
            },
            failedVisit => {
              this._log.info(
                "Failed to insert a history record",
                failedVisit.guid
              );
              this._log.trace("The record that failed", failedVisit);
              failed.push(failedVisit.guid);
            }
          );
        } catch (ex) {
          this._log.info("Failed to insert history records", ex);
          countTelemetry.addIncomingFailedReason(ex.message);
        }

        // All the top level places or visits that had unknown fields are sent
        // to be added to the appropiate tables
        await lazy.PlacesSyncUtils.history.updateUnknownFieldsBatch(
          unknownFieldsToInsert
        );
      }
    }

    return failed;
  },

  /**
   * Returns a generator that splits records into sanely sized chunks suitable
   * for passing to places to prevent places doing bad things at shutdown.
   */
  *_generateChunks(records) {
    // We chunk based on the number of *visits* inside each record. However,
    // we do not split a single record into multiple records, because at some
    // time in the future, we intend to ensure these records are ordered by
    // lastModified, and advance the engine's timestamp as we process them,
    // meaning we can resume exactly where we left off next sync - although
    // currently that's not done, so we will retry the entire batch next sync
    // if interrupted.
    // ie, this means that if a single record has more than MAX_VISITS_PER_INSERT
    // visits, we will call insertMany() with exactly 1 record, but with
    // more than MAX_VISITS_PER_INSERT visits.
    let curIndex = 0;
    this._log.debug(`adding ${records.length} records to history`);
    while (curIndex < records.length) {
      Async.checkAppReady(); // may throw if we are shutting down.
      let toAdd = []; // what we are going to insert.
      let count = 0; // a counter which tells us when toAdd is full.
      do {
        let record = records[curIndex];
        curIndex += 1;
        toAdd.push(record);
        count += record.visits.length;
      } while (
        curIndex < records.length &&
        count + records[curIndex].visits.length <= this.MAX_VISITS_PER_INSERT
      );
      this._log.trace(`adding ${toAdd.length} items in this chunk`);
      yield toAdd;
    }
  },

  /* An internal helper to determine if we can add an entry to places.
     Exists primarily so tests can override it.
   */
  _canAddURI(uri) {
    return lazy.PlacesUtils.history.canAddURI(uri);
  },

  /**
   * Converts a Sync history record to a mozIPlaceInfo.
   *
   * Throws if an invalid record is encountered (invalid URI, etc.),
   * returns a new PageInfo object if the record is to be applied, null
   * otherwise (no visits to add, etc.),
   */
  async _recordToPlaceInfo(record) {
    // Sort out invalid URIs and ones Places just simply doesn't want.
    record.url = lazy.PlacesUtils.normalizeToURLOrGUID(record.histUri);
    record.uri = CommonUtils.makeURI(record.histUri);

    if (!Utils.checkGUID(record.id)) {
      this._log.warn("Encountered record with invalid GUID: " + record.id);
      return null;
    }
    record.guid = record.id;

    if (
      !this._canAddURI(record.uri) ||
      !this.engine.shouldSyncURL(record.uri.spec)
    ) {
      this._log.trace(
        "Ignoring record " +
          record.id +
          " with URI " +
          record.uri.spec +
          ": can't add this URI."
      );
      return null;
    }

    // We dupe visits by date and type. So an incoming visit that has
    // the same timestamp and type as a local one won't get applied.
    // To avoid creating new objects, we rewrite the query result so we
    // can simply check for containment below.
    let curVisitsAsArray = [];
    let curVisits = new Set();
    try {
      curVisitsAsArray = await lazy.PlacesSyncUtils.history.fetchVisitsForURL(
        record.histUri
      );
    } catch (e) {
      this._log.error(
        "Error while fetching visits for URL ${record.histUri}",
        record.histUri
      );
    }
    let oldestAllowed =
      lazy.PlacesSyncUtils.bookmarks.EARLIEST_BOOKMARK_TIMESTAMP;
    if (curVisitsAsArray.length == 20) {
      let oldestVisit = curVisitsAsArray[curVisitsAsArray.length - 1];
      oldestAllowed = lazy.PlacesSyncUtils.history.clampVisitDate(
        lazy.PlacesUtils.toDate(oldestVisit.date).getTime()
      );
    }

    let i, k;
    for (i = 0; i < curVisitsAsArray.length; i++) {
      // Same logic as used in the loop below to generate visitKey.
      let { date, type } = curVisitsAsArray[i];
      let dateObj = lazy.PlacesUtils.toDate(date);
      let millis = lazy.PlacesSyncUtils.history
        .clampVisitDate(dateObj)
        .getTime();
      curVisits.add(`${millis},${type}`);
    }

    // Walk through the visits, make sure we have sound data, and eliminate
    // dupes. The latter is done by rewriting the array in-place.
    for (i = 0, k = 0; i < record.visits.length; i++) {
      let visit = (record.visits[k] = record.visits[i]);

      if (
        !visit.date ||
        typeof visit.date != "number" ||
        !Number.isInteger(visit.date)
      ) {
        this._log.warn(
          "Encountered record with invalid visit date: " + visit.date
        );
        continue;
      }

      if (
        !visit.type ||
        !Object.values(lazy.PlacesUtils.history.TRANSITIONS).includes(
          visit.type
        )
      ) {
        this._log.warn(
          "Encountered record with invalid visit type: " +
            visit.type +
            "; ignoring."
        );
        continue;
      }

      // Dates need to be integers. Future and far past dates are clamped to the
      // current date and earliest sensible date, respectively.
      let originalVisitDate = lazy.PlacesUtils.toDate(Math.round(visit.date));
      visit.date =
        lazy.PlacesSyncUtils.history.clampVisitDate(originalVisitDate);

      if (visit.date.getTime() < oldestAllowed) {
        // Visit is older than the oldest visit we have, and we have so many
        // visits for this uri that we hit our limit when inserting.
        continue;
      }
      let visitKey = `${visit.date.getTime()},${visit.type}`;
      if (curVisits.has(visitKey)) {
        // Visit is a dupe, don't increment 'k' so the element will be
        // overwritten.
        continue;
      }

      // Note the visit key, so that we don't add duplicate visits with
      // clamped timestamps.
      curVisits.add(visitKey);

      visit.transition = visit.type;
      k += 1;
    }
    record.visits.length = k; // truncate array

    // No update if there aren't any visits to apply.
    // History wants at least one visit.
    // In any case, the only thing we could change would be the title
    // and that shouldn't change without a visit.
    if (!record.visits.length) {
      this._log.trace(
        "Ignoring record " +
          record.id +
          " with URI " +
          record.uri.spec +
          ": no visits to add."
      );
      return null;
    }

    // PageInfo is validated using validateItemProperties which does a shallow
    // copy of the properties. Since record uses getters some of the properties
    // are not copied over. Thus we create and return a new object.
    let pageInfo = {
      title: record.title,
      url: record.url,
      guid: record.guid,
      visits: record.visits,
    };

    return pageInfo;
  },

  async remove(record) {
    this._log.trace("Removing page: " + record.id);
    let removed = await lazy.PlacesUtils.history.remove(record.id);
    if (removed) {
      this._log.trace("Removed page: " + record.id);
    } else {
      this._log.debug("Page already removed: " + record.id);
    }
  },

  async itemExists(id) {
    return !!(await lazy.PlacesSyncUtils.history.fetchURLInfoForGuid(id));
  },

  async createRecord(id, collection) {
    let foo = await lazy.PlacesSyncUtils.history.fetchURLInfoForGuid(id);
    let record = new HistoryRec(collection, id);
    if (foo) {
      record.histUri = foo.url;
      record.title = foo.title;
      record.sortindex = foo.frecency;

      // If we had any unknown fields, ensure we put it back on the
      // top-level record
      if (foo.unknownFields) {
        let unknownFields = JSON.parse(foo.unknownFields);
        Object.assign(record.cleartext, unknownFields);
      }

      try {
        record.visits = await lazy.PlacesSyncUtils.history.fetchVisitsForURL(
          record.histUri
        );
      } catch (e) {
        this._log.error(
          "Error while fetching visits for URL ${record.histUri}",
          record.histUri
        );
        record.visits = [];
      }
    } else {
      record.deleted = true;
    }

    return record;
  },

  async wipe() {
    return lazy.PlacesSyncUtils.history.wipe();
  },
};
Object.setPrototypeOf(HistoryStore.prototype, Store.prototype);

function HistoryTracker(name, engine) {
  LegacyTracker.call(this, name, engine);
}
HistoryTracker.prototype = {
  onStart() {
    this._log.info("Adding Places observer.");
    this._placesObserver = new PlacesWeakCallbackWrapper(
      this.handlePlacesEvents.bind(this)
    );
    PlacesObservers.addListener(
      ["page-visited", "history-cleared", "page-removed"],
      this._placesObserver
    );
  },

  onStop() {
    this._log.info("Removing Places observer.");
    if (this._placesObserver) {
      PlacesObservers.removeListener(
        ["page-visited", "history-cleared", "page-removed"],
        this._placesObserver
      );
    }
  },

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

  handlePlacesEvents(aEvents) {
    this.asyncObserver.enqueueCall(() => this._handlePlacesEvents(aEvents));
  },

  async _handlePlacesEvents(aEvents) {
    if (this.ignoreAll) {
      this._log.trace(
        "ignoreAll: ignoring visits [" +
          aEvents.map(v => v.guid).join(",") +
          "]"
      );
      return;
    }
    for (let event of aEvents) {
      switch (event.type) {
        case "page-visited": {
          this._log.trace("'page-visited': " + event.url);
          if (
            this.engine.shouldSyncURL(event.url) &&
            (await this.addChangedID(event.pageGuid))
          ) {
            this.score += SCORE_INCREMENT_SMALL;
          }
          break;
        }
        case "history-cleared": {
          this._log.trace("history-cleared");
          // Note that we're going to trigger a sync, but none of the cleared
          // pages are tracked, so the deletions will not be propagated.
          // See Bug 578694.
          this.score += SCORE_INCREMENT_XLARGE;
          break;
        }
        case "page-removed": {
          if (event.reason === PlacesVisitRemoved.REASON_EXPIRED) {
            return;
          }

          this._log.trace(
            "page-removed: " + event.url + ", reason " + event.reason
          );
          const added = await this.addChangedID(event.pageGuid);
          if (added) {
            this.score += event.isRemovedFromStore
              ? SCORE_INCREMENT_XLARGE
              : SCORE_INCREMENT_SMALL;
          }
          break;
        }
      }
    }
  },
};
Object.setPrototypeOf(HistoryTracker.prototype, LegacyTracker.prototype);

[ Dauer der Verarbeitung: 0.40 Sekunden  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


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