tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ext-history.js (9605B)


      1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set sts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 "use strict";
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     11 });
     12 
     13 var { normalizeTime } = ExtensionCommon;
     14 
     15 let nsINavHistoryService = Ci.nsINavHistoryService;
     16 const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
     17  ["link", nsINavHistoryService.TRANSITION_LINK],
     18  ["typed", nsINavHistoryService.TRANSITION_TYPED],
     19  ["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK],
     20  ["auto_subframe", nsINavHistoryService.TRANSITION_EMBED],
     21  ["manual_subframe", nsINavHistoryService.TRANSITION_FRAMED_LINK],
     22  ["reload", nsINavHistoryService.TRANSITION_RELOAD],
     23 ]);
     24 
     25 let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map();
     26 for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) {
     27  TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition);
     28 }
     29 
     30 const getTransitionType = transition => {
     31  // cannot set a default value for the transition argument as the framework sets it to null
     32  transition = transition || "link";
     33  let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition);
     34  if (!transitionType) {
     35    throw new Error(
     36      `|${transition}| is not a supported transition for history`
     37    );
     38  }
     39  return transitionType;
     40 };
     41 
     42 const getTransition = transitionType => {
     43  return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link";
     44 };
     45 
     46 /*
     47 * Converts a mozIStorageRow into a HistoryItem
     48 */
     49 const convertRowToHistoryItem = row => {
     50  return {
     51    id: row.getResultByName("guid"),
     52    url: row.getResultByName("url"),
     53    title: row.getResultByName("page_title"),
     54    lastVisitTime: PlacesUtils.toDate(
     55      row.getResultByName("last_visit_date")
     56    ).getTime(),
     57    visitCount: row.getResultByName("visit_count"),
     58  };
     59 };
     60 
     61 /*
     62 * Converts a mozIStorageRow into a VisitItem
     63 */
     64 const convertRowToVisitItem = row => {
     65  return {
     66    id: row.getResultByName("guid"),
     67    visitId: String(row.getResultByName("id")),
     68    visitTime: PlacesUtils.toDate(row.getResultByName("visit_date")).getTime(),
     69    referringVisitId: String(row.getResultByName("from_visit")),
     70    transition: getTransition(row.getResultByName("visit_type")),
     71  };
     72 };
     73 
     74 /*
     75 * Converts a mozIStorageResultSet into an array of objects
     76 */
     77 const accumulateNavHistoryResults = (resultSet, converter, results) => {
     78  let row;
     79  while ((row = resultSet.getNextRow())) {
     80    results.push(converter(row));
     81  }
     82 };
     83 
     84 function executeAsyncQuery(historyQuery, options, resultConverter) {
     85  let results = [];
     86  return new Promise((resolve, reject) => {
     87    PlacesUtils.history.asyncExecuteLegacyQuery(historyQuery, options, {
     88      handleResult(resultSet) {
     89        accumulateNavHistoryResults(resultSet, resultConverter, results);
     90      },
     91      handleError(error) {
     92        reject(
     93          new Error(
     94            "Async execution error (" + error.result + "): " + error.message
     95          )
     96        );
     97      },
     98      handleCompletion() {
     99        resolve(results);
    100      },
    101    });
    102  });
    103 }
    104 
    105 this.history = class extends ExtensionAPIPersistent {
    106  PERSISTENT_EVENTS = {
    107    onVisited({ fire }) {
    108      const listener = events => {
    109        for (const event of events) {
    110          const visit = {
    111            id: event.pageGuid,
    112            url: event.url,
    113            title: event.lastKnownTitle || "",
    114            lastVisitTime: event.visitTime,
    115            visitCount: event.visitCount,
    116            typedCount: event.typedCount,
    117          };
    118          fire.sync(visit);
    119        }
    120      };
    121 
    122      PlacesUtils.observers.addListener(["page-visited"], listener);
    123      return {
    124        unregister() {
    125          PlacesUtils.observers.removeListener(["page-visited"], listener);
    126        },
    127        convert(_fire) {
    128          fire = _fire;
    129        },
    130      };
    131    },
    132    onVisitRemoved({ fire }) {
    133      const listener = events => {
    134        const removedURLs = [];
    135 
    136        for (const event of events) {
    137          switch (event.type) {
    138            case "history-cleared": {
    139              fire.sync({ allHistory: true, urls: [] });
    140              break;
    141            }
    142            case "page-removed": {
    143              if (!event.isPartialVisistsRemoval) {
    144                removedURLs.push(event.url);
    145              }
    146              break;
    147            }
    148          }
    149        }
    150 
    151        if (removedURLs.length) {
    152          fire.sync({ allHistory: false, urls: removedURLs });
    153        }
    154      };
    155 
    156      PlacesUtils.observers.addListener(
    157        ["history-cleared", "page-removed"],
    158        listener
    159      );
    160      return {
    161        unregister() {
    162          PlacesUtils.observers.removeListener(
    163            ["history-cleared", "page-removed"],
    164            listener
    165          );
    166        },
    167        convert(_fire) {
    168          fire = _fire;
    169        },
    170      };
    171    },
    172    onTitleChanged({ fire }) {
    173      const listener = events => {
    174        for (const event of events) {
    175          const titleChanged = {
    176            id: event.pageGuid,
    177            url: event.url,
    178            title: event.title,
    179          };
    180          fire.sync(titleChanged);
    181        }
    182      };
    183 
    184      PlacesUtils.observers.addListener(["page-title-changed"], listener);
    185      return {
    186        unregister() {
    187          PlacesUtils.observers.removeListener(
    188            ["page-title-changed"],
    189            listener
    190          );
    191        },
    192        convert(_fire) {
    193          fire = _fire;
    194        },
    195      };
    196    },
    197  };
    198 
    199  getAPI(context) {
    200    return {
    201      history: {
    202        addUrl: function (details) {
    203          let transition, date;
    204          try {
    205            transition = getTransitionType(details.transition);
    206          } catch (error) {
    207            return Promise.reject({ message: error.message });
    208          }
    209          if (details.visitTime) {
    210            date = normalizeTime(details.visitTime);
    211          }
    212          let pageInfo = {
    213            title: details.title,
    214            url: details.url,
    215            visits: [
    216              {
    217                transition,
    218                date,
    219              },
    220            ],
    221          };
    222          try {
    223            return PlacesUtils.history.insert(pageInfo).then(() => undefined);
    224          } catch (error) {
    225            return Promise.reject({ message: error.message });
    226          }
    227        },
    228 
    229        deleteAll: function () {
    230          return PlacesUtils.history.clear();
    231        },
    232 
    233        deleteRange: function (filter) {
    234          let newFilter = {
    235            beginDate: normalizeTime(filter.startTime),
    236            endDate: normalizeTime(filter.endTime),
    237          };
    238          // History.removeVisitsByFilter returns a boolean, but our API should return nothing
    239          return PlacesUtils.history
    240            .removeVisitsByFilter(newFilter)
    241            .then(() => undefined);
    242        },
    243 
    244        deleteUrl: function (details) {
    245          let url = details.url;
    246          // History.remove returns a boolean, but our API should return nothing
    247          return PlacesUtils.history.remove(url).then(() => undefined);
    248        },
    249 
    250        search: function (query) {
    251          let beginTime =
    252            query.startTime == null
    253              ? PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000)
    254              : PlacesUtils.toPRTime(normalizeTime(query.startTime));
    255          let endTime =
    256            query.endTime == null
    257              ? Number.MAX_VALUE
    258              : PlacesUtils.toPRTime(normalizeTime(query.endTime));
    259          if (beginTime > endTime) {
    260            return Promise.reject({
    261              message: "The startTime cannot be after the endTime",
    262            });
    263          }
    264 
    265          let options = PlacesUtils.history.getNewQueryOptions();
    266          options.includeHidden = true;
    267          options.sortingMode = options.SORT_BY_DATE_DESCENDING;
    268          options.maxResults = query.maxResults || 100;
    269 
    270          let historyQuery = PlacesUtils.history.getNewQuery();
    271          historyQuery.searchTerms = query.text;
    272          historyQuery.beginTime = beginTime;
    273          historyQuery.endTime = endTime;
    274          return executeAsyncQuery(
    275            historyQuery,
    276            options,
    277            convertRowToHistoryItem
    278          );
    279        },
    280 
    281        getVisits: function (details) {
    282          let url = details.url;
    283          if (!url) {
    284            return Promise.reject({
    285              message: "A URL must be provided for getVisits",
    286            });
    287          }
    288 
    289          let options = PlacesUtils.history.getNewQueryOptions();
    290          options.includeHidden = true;
    291          options.sortingMode = options.SORT_BY_DATE_DESCENDING;
    292          options.resultType = options.RESULTS_AS_VISIT;
    293 
    294          let historyQuery = PlacesUtils.history.getNewQuery();
    295          historyQuery.uri = Services.io.newURI(url);
    296          return executeAsyncQuery(
    297            historyQuery,
    298            options,
    299            convertRowToVisitItem
    300          );
    301        },
    302 
    303        onVisited: new EventManager({
    304          context,
    305          module: "history",
    306          event: "onVisited",
    307          extensionApi: this,
    308        }).api(),
    309 
    310        onVisitRemoved: new EventManager({
    311          context,
    312          module: "history",
    313          event: "onVisitRemoved",
    314          extensionApi: this,
    315        }).api(),
    316 
    317        onTitleChanged: new EventManager({
    318          context,
    319          module: "history",
    320          event: "onTitleChanged",
    321          extensionApi: this,
    322        }).api(),
    323      },
    324    };
    325  }
    326 };