mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			326 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			326 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | 
						|
/* vim: set sts=2 sw=2 et tw=80: */
 | 
						|
/* 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/. */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(this, {
 | 
						|
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
var { normalizeTime } = ExtensionCommon;
 | 
						|
 | 
						|
let nsINavHistoryService = Ci.nsINavHistoryService;
 | 
						|
const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
 | 
						|
  ["link", nsINavHistoryService.TRANSITION_LINK],
 | 
						|
  ["typed", nsINavHistoryService.TRANSITION_TYPED],
 | 
						|
  ["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK],
 | 
						|
  ["auto_subframe", nsINavHistoryService.TRANSITION_EMBED],
 | 
						|
  ["manual_subframe", nsINavHistoryService.TRANSITION_FRAMED_LINK],
 | 
						|
  ["reload", nsINavHistoryService.TRANSITION_RELOAD],
 | 
						|
]);
 | 
						|
 | 
						|
let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map();
 | 
						|
for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) {
 | 
						|
  TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition);
 | 
						|
}
 | 
						|
 | 
						|
const getTransitionType = transition => {
 | 
						|
  // cannot set a default value for the transition argument as the framework sets it to null
 | 
						|
  transition = transition || "link";
 | 
						|
  let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition);
 | 
						|
  if (!transitionType) {
 | 
						|
    throw new Error(
 | 
						|
      `|${transition}| is not a supported transition for history`
 | 
						|
    );
 | 
						|
  }
 | 
						|
  return transitionType;
 | 
						|
};
 | 
						|
 | 
						|
const getTransition = transitionType => {
 | 
						|
  return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link";
 | 
						|
};
 | 
						|
 | 
						|
/*
 | 
						|
 * Converts a mozIStorageRow into a HistoryItem
 | 
						|
 */
 | 
						|
const convertRowToHistoryItem = row => {
 | 
						|
  return {
 | 
						|
    id: row.getResultByName("guid"),
 | 
						|
    url: row.getResultByName("url"),
 | 
						|
    title: row.getResultByName("page_title"),
 | 
						|
    lastVisitTime: PlacesUtils.toDate(
 | 
						|
      row.getResultByName("last_visit_date")
 | 
						|
    ).getTime(),
 | 
						|
    visitCount: row.getResultByName("visit_count"),
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
/*
 | 
						|
 * Converts a mozIStorageRow into a VisitItem
 | 
						|
 */
 | 
						|
const convertRowToVisitItem = row => {
 | 
						|
  return {
 | 
						|
    id: row.getResultByName("guid"),
 | 
						|
    visitId: String(row.getResultByName("id")),
 | 
						|
    visitTime: PlacesUtils.toDate(row.getResultByName("visit_date")).getTime(),
 | 
						|
    referringVisitId: String(row.getResultByName("from_visit")),
 | 
						|
    transition: getTransition(row.getResultByName("visit_type")),
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
/*
 | 
						|
 * Converts a mozIStorageResultSet into an array of objects
 | 
						|
 */
 | 
						|
const accumulateNavHistoryResults = (resultSet, converter, results) => {
 | 
						|
  let row;
 | 
						|
  while ((row = resultSet.getNextRow())) {
 | 
						|
    results.push(converter(row));
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
function executeAsyncQuery(historyQuery, options, resultConverter) {
 | 
						|
  let results = [];
 | 
						|
  return new Promise((resolve, reject) => {
 | 
						|
    PlacesUtils.history.asyncExecuteLegacyQuery(historyQuery, options, {
 | 
						|
      handleResult(resultSet) {
 | 
						|
        accumulateNavHistoryResults(resultSet, resultConverter, results);
 | 
						|
      },
 | 
						|
      handleError(error) {
 | 
						|
        reject(
 | 
						|
          new Error(
 | 
						|
            "Async execution error (" + error.result + "): " + error.message
 | 
						|
          )
 | 
						|
        );
 | 
						|
      },
 | 
						|
      handleCompletion() {
 | 
						|
        resolve(results);
 | 
						|
      },
 | 
						|
    });
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
this.history = class extends ExtensionAPIPersistent {
 | 
						|
  PERSISTENT_EVENTS = {
 | 
						|
    onVisited({ fire }) {
 | 
						|
      const listener = events => {
 | 
						|
        for (const event of events) {
 | 
						|
          const visit = {
 | 
						|
            id: event.pageGuid,
 | 
						|
            url: event.url,
 | 
						|
            title: event.lastKnownTitle || "",
 | 
						|
            lastVisitTime: event.visitTime,
 | 
						|
            visitCount: event.visitCount,
 | 
						|
            typedCount: event.typedCount,
 | 
						|
          };
 | 
						|
          fire.sync(visit);
 | 
						|
        }
 | 
						|
      };
 | 
						|
 | 
						|
      PlacesUtils.observers.addListener(["page-visited"], listener);
 | 
						|
      return {
 | 
						|
        unregister() {
 | 
						|
          PlacesUtils.observers.removeListener(["page-visited"], listener);
 | 
						|
        },
 | 
						|
        convert(_fire) {
 | 
						|
          fire = _fire;
 | 
						|
        },
 | 
						|
      };
 | 
						|
    },
 | 
						|
    onVisitRemoved({ fire }) {
 | 
						|
      const listener = events => {
 | 
						|
        const removedURLs = [];
 | 
						|
 | 
						|
        for (const event of events) {
 | 
						|
          switch (event.type) {
 | 
						|
            case "history-cleared": {
 | 
						|
              fire.sync({ allHistory: true, urls: [] });
 | 
						|
              break;
 | 
						|
            }
 | 
						|
            case "page-removed": {
 | 
						|
              if (!event.isPartialVisistsRemoval) {
 | 
						|
                removedURLs.push(event.url);
 | 
						|
              }
 | 
						|
              break;
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (removedURLs.length) {
 | 
						|
          fire.sync({ allHistory: false, urls: removedURLs });
 | 
						|
        }
 | 
						|
      };
 | 
						|
 | 
						|
      PlacesUtils.observers.addListener(
 | 
						|
        ["history-cleared", "page-removed"],
 | 
						|
        listener
 | 
						|
      );
 | 
						|
      return {
 | 
						|
        unregister() {
 | 
						|
          PlacesUtils.observers.removeListener(
 | 
						|
            ["history-cleared", "page-removed"],
 | 
						|
            listener
 | 
						|
          );
 | 
						|
        },
 | 
						|
        convert(_fire) {
 | 
						|
          fire = _fire;
 | 
						|
        },
 | 
						|
      };
 | 
						|
    },
 | 
						|
    onTitleChanged({ fire }) {
 | 
						|
      const listener = events => {
 | 
						|
        for (const event of events) {
 | 
						|
          const titleChanged = {
 | 
						|
            id: event.pageGuid,
 | 
						|
            url: event.url,
 | 
						|
            title: event.title,
 | 
						|
          };
 | 
						|
          fire.sync(titleChanged);
 | 
						|
        }
 | 
						|
      };
 | 
						|
 | 
						|
      PlacesUtils.observers.addListener(["page-title-changed"], listener);
 | 
						|
      return {
 | 
						|
        unregister() {
 | 
						|
          PlacesUtils.observers.removeListener(
 | 
						|
            ["page-title-changed"],
 | 
						|
            listener
 | 
						|
          );
 | 
						|
        },
 | 
						|
        convert(_fire) {
 | 
						|
          fire = _fire;
 | 
						|
        },
 | 
						|
      };
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  getAPI(context) {
 | 
						|
    return {
 | 
						|
      history: {
 | 
						|
        addUrl: function (details) {
 | 
						|
          let transition, date;
 | 
						|
          try {
 | 
						|
            transition = getTransitionType(details.transition);
 | 
						|
          } catch (error) {
 | 
						|
            return Promise.reject({ message: error.message });
 | 
						|
          }
 | 
						|
          if (details.visitTime) {
 | 
						|
            date = normalizeTime(details.visitTime);
 | 
						|
          }
 | 
						|
          let pageInfo = {
 | 
						|
            title: details.title,
 | 
						|
            url: details.url,
 | 
						|
            visits: [
 | 
						|
              {
 | 
						|
                transition,
 | 
						|
                date,
 | 
						|
              },
 | 
						|
            ],
 | 
						|
          };
 | 
						|
          try {
 | 
						|
            return PlacesUtils.history.insert(pageInfo).then(() => undefined);
 | 
						|
          } catch (error) {
 | 
						|
            return Promise.reject({ message: error.message });
 | 
						|
          }
 | 
						|
        },
 | 
						|
 | 
						|
        deleteAll: function () {
 | 
						|
          return PlacesUtils.history.clear();
 | 
						|
        },
 | 
						|
 | 
						|
        deleteRange: function (filter) {
 | 
						|
          let newFilter = {
 | 
						|
            beginDate: normalizeTime(filter.startTime),
 | 
						|
            endDate: normalizeTime(filter.endTime),
 | 
						|
          };
 | 
						|
          // History.removeVisitsByFilter returns a boolean, but our API should return nothing
 | 
						|
          return PlacesUtils.history
 | 
						|
            .removeVisitsByFilter(newFilter)
 | 
						|
            .then(() => undefined);
 | 
						|
        },
 | 
						|
 | 
						|
        deleteUrl: function (details) {
 | 
						|
          let url = details.url;
 | 
						|
          // History.remove returns a boolean, but our API should return nothing
 | 
						|
          return PlacesUtils.history.remove(url).then(() => undefined);
 | 
						|
        },
 | 
						|
 | 
						|
        search: function (query) {
 | 
						|
          let beginTime =
 | 
						|
            query.startTime == null
 | 
						|
              ? PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000)
 | 
						|
              : PlacesUtils.toPRTime(normalizeTime(query.startTime));
 | 
						|
          let endTime =
 | 
						|
            query.endTime == null
 | 
						|
              ? Number.MAX_VALUE
 | 
						|
              : PlacesUtils.toPRTime(normalizeTime(query.endTime));
 | 
						|
          if (beginTime > endTime) {
 | 
						|
            return Promise.reject({
 | 
						|
              message: "The startTime cannot be after the endTime",
 | 
						|
            });
 | 
						|
          }
 | 
						|
 | 
						|
          let options = PlacesUtils.history.getNewQueryOptions();
 | 
						|
          options.includeHidden = true;
 | 
						|
          options.sortingMode = options.SORT_BY_DATE_DESCENDING;
 | 
						|
          options.maxResults = query.maxResults || 100;
 | 
						|
 | 
						|
          let historyQuery = PlacesUtils.history.getNewQuery();
 | 
						|
          historyQuery.searchTerms = query.text;
 | 
						|
          historyQuery.beginTime = beginTime;
 | 
						|
          historyQuery.endTime = endTime;
 | 
						|
          return executeAsyncQuery(
 | 
						|
            historyQuery,
 | 
						|
            options,
 | 
						|
            convertRowToHistoryItem
 | 
						|
          );
 | 
						|
        },
 | 
						|
 | 
						|
        getVisits: function (details) {
 | 
						|
          let url = details.url;
 | 
						|
          if (!url) {
 | 
						|
            return Promise.reject({
 | 
						|
              message: "A URL must be provided for getVisits",
 | 
						|
            });
 | 
						|
          }
 | 
						|
 | 
						|
          let options = PlacesUtils.history.getNewQueryOptions();
 | 
						|
          options.includeHidden = true;
 | 
						|
          options.sortingMode = options.SORT_BY_DATE_DESCENDING;
 | 
						|
          options.resultType = options.RESULTS_AS_VISIT;
 | 
						|
 | 
						|
          let historyQuery = PlacesUtils.history.getNewQuery();
 | 
						|
          historyQuery.uri = Services.io.newURI(url);
 | 
						|
          return executeAsyncQuery(
 | 
						|
            historyQuery,
 | 
						|
            options,
 | 
						|
            convertRowToVisitItem
 | 
						|
          );
 | 
						|
        },
 | 
						|
 | 
						|
        onVisited: new EventManager({
 | 
						|
          context,
 | 
						|
          module: "history",
 | 
						|
          event: "onVisited",
 | 
						|
          extensionApi: this,
 | 
						|
        }).api(),
 | 
						|
 | 
						|
        onVisitRemoved: new EventManager({
 | 
						|
          context,
 | 
						|
          module: "history",
 | 
						|
          event: "onVisitRemoved",
 | 
						|
          extensionApi: this,
 | 
						|
        }).api(),
 | 
						|
 | 
						|
        onTitleChanged: new EventManager({
 | 
						|
          context,
 | 
						|
          module: "history",
 | 
						|
          event: "onTitleChanged",
 | 
						|
          extensionApi: this,
 | 
						|
        }).api(),
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
};
 |