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 };