1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/modules/engines/history.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,414 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +this.EXPORTED_SYMBOLS = ['HistoryEngine', 'HistoryRec']; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 +const Cr = Components.results; 1.14 + 1.15 +const HISTORY_TTL = 5184000; // 60 days 1.16 + 1.17 +Cu.import("resource://gre/modules/PlacesUtils.jsm", this); 1.18 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.19 +Cu.import("resource://services-common/async.js"); 1.20 +Cu.import("resource://gre/modules/Log.jsm"); 1.21 +Cu.import("resource://services-sync/constants.js"); 1.22 +Cu.import("resource://services-sync/engines.js"); 1.23 +Cu.import("resource://services-sync/record.js"); 1.24 +Cu.import("resource://services-sync/util.js"); 1.25 + 1.26 +this.HistoryRec = function HistoryRec(collection, id) { 1.27 + CryptoWrapper.call(this, collection, id); 1.28 +} 1.29 +HistoryRec.prototype = { 1.30 + __proto__: CryptoWrapper.prototype, 1.31 + _logName: "Sync.Record.History", 1.32 + ttl: HISTORY_TTL 1.33 +}; 1.34 + 1.35 +Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]); 1.36 + 1.37 + 1.38 +this.HistoryEngine = function HistoryEngine(service) { 1.39 + SyncEngine.call(this, "History", service); 1.40 +} 1.41 +HistoryEngine.prototype = { 1.42 + __proto__: SyncEngine.prototype, 1.43 + _recordObj: HistoryRec, 1.44 + _storeObj: HistoryStore, 1.45 + _trackerObj: HistoryTracker, 1.46 + downloadLimit: MAX_HISTORY_DOWNLOAD, 1.47 + applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE 1.48 +}; 1.49 + 1.50 +function HistoryStore(name, engine) { 1.51 + Store.call(this, name, engine); 1.52 + 1.53 + // Explicitly nullify our references to our cached services so we don't leak 1.54 + Svc.Obs.add("places-shutdown", function() { 1.55 + for each ([query, stmt] in Iterator(this._stmts)) { 1.56 + stmt.finalize(); 1.57 + } 1.58 + this._stmts = {}; 1.59 + }, this); 1.60 +} 1.61 +HistoryStore.prototype = { 1.62 + __proto__: Store.prototype, 1.63 + 1.64 + __asyncHistory: null, 1.65 + get _asyncHistory() { 1.66 + if (!this.__asyncHistory) { 1.67 + this.__asyncHistory = Cc["@mozilla.org/browser/history;1"] 1.68 + .getService(Ci.mozIAsyncHistory); 1.69 + } 1.70 + return this.__asyncHistory; 1.71 + }, 1.72 + 1.73 + _stmts: {}, 1.74 + _getStmt: function(query) { 1.75 + if (query in this._stmts) { 1.76 + return this._stmts[query]; 1.77 + } 1.78 + 1.79 + this._log.trace("Creating SQL statement: " + query); 1.80 + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) 1.81 + .DBConnection; 1.82 + return this._stmts[query] = db.createAsyncStatement(query); 1.83 + }, 1.84 + 1.85 + get _setGUIDStm() { 1.86 + return this._getStmt( 1.87 + "UPDATE moz_places " + 1.88 + "SET guid = :guid " + 1.89 + "WHERE url = :page_url"); 1.90 + }, 1.91 + 1.92 + // Some helper functions to handle GUIDs 1.93 + setGUID: function setGUID(uri, guid) { 1.94 + uri = uri.spec ? uri.spec : uri; 1.95 + 1.96 + if (!guid) { 1.97 + guid = Utils.makeGUID(); 1.98 + } 1.99 + 1.100 + let stmt = this._setGUIDStm; 1.101 + stmt.params.guid = guid; 1.102 + stmt.params.page_url = uri; 1.103 + Async.querySpinningly(stmt); 1.104 + return guid; 1.105 + }, 1.106 + 1.107 + get _guidStm() { 1.108 + return this._getStmt( 1.109 + "SELECT guid " + 1.110 + "FROM moz_places " + 1.111 + "WHERE url = :page_url"); 1.112 + }, 1.113 + _guidCols: ["guid"], 1.114 + 1.115 + GUIDForUri: function GUIDForUri(uri, create) { 1.116 + let stm = this._guidStm; 1.117 + stm.params.page_url = uri.spec ? uri.spec : uri; 1.118 + 1.119 + // Use the existing GUID if it exists 1.120 + let result = Async.querySpinningly(stm, this._guidCols)[0]; 1.121 + if (result && result.guid) 1.122 + return result.guid; 1.123 + 1.124 + // Give the uri a GUID if it doesn't have one 1.125 + if (create) 1.126 + return this.setGUID(uri); 1.127 + }, 1.128 + 1.129 + get _visitStm() { 1.130 + return this._getStmt( 1.131 + "/* do not warn (bug 599936) */ " + 1.132 + "SELECT visit_type type, visit_date date " + 1.133 + "FROM moz_historyvisits " + 1.134 + "WHERE place_id = (SELECT id FROM moz_places WHERE url = :url) " + 1.135 + "ORDER BY date DESC LIMIT 10"); 1.136 + }, 1.137 + _visitCols: ["date", "type"], 1.138 + 1.139 + get _urlStm() { 1.140 + return this._getStmt( 1.141 + "SELECT url, title, frecency " + 1.142 + "FROM moz_places " + 1.143 + "WHERE guid = :guid"); 1.144 + }, 1.145 + _urlCols: ["url", "title", "frecency"], 1.146 + 1.147 + get _allUrlStm() { 1.148 + return this._getStmt( 1.149 + "SELECT url " + 1.150 + "FROM moz_places " + 1.151 + "WHERE last_visit_date > :cutoff_date " + 1.152 + "ORDER BY frecency DESC " + 1.153 + "LIMIT :max_results"); 1.154 + }, 1.155 + _allUrlCols: ["url"], 1.156 + 1.157 + // See bug 320831 for why we use SQL here 1.158 + _getVisits: function HistStore__getVisits(uri) { 1.159 + this._visitStm.params.url = uri; 1.160 + return Async.querySpinningly(this._visitStm, this._visitCols); 1.161 + }, 1.162 + 1.163 + // See bug 468732 for why we use SQL here 1.164 + _findURLByGUID: function HistStore__findURLByGUID(guid) { 1.165 + this._urlStm.params.guid = guid; 1.166 + return Async.querySpinningly(this._urlStm, this._urlCols)[0]; 1.167 + }, 1.168 + 1.169 + changeItemID: function HStore_changeItemID(oldID, newID) { 1.170 + this.setGUID(this._findURLByGUID(oldID).url, newID); 1.171 + }, 1.172 + 1.173 + 1.174 + getAllIDs: function HistStore_getAllIDs() { 1.175 + // Only get places visited within the last 30 days (30*24*60*60*1000ms) 1.176 + this._allUrlStm.params.cutoff_date = (Date.now() - 2592000000) * 1000; 1.177 + this._allUrlStm.params.max_results = MAX_HISTORY_UPLOAD; 1.178 + 1.179 + let urls = Async.querySpinningly(this._allUrlStm, this._allUrlCols); 1.180 + let self = this; 1.181 + return urls.reduce(function(ids, item) { 1.182 + ids[self.GUIDForUri(item.url, true)] = item.url; 1.183 + return ids; 1.184 + }, {}); 1.185 + }, 1.186 + 1.187 + applyIncomingBatch: function applyIncomingBatch(records) { 1.188 + let failed = []; 1.189 + 1.190 + // Convert incoming records to mozIPlaceInfo objects. Some records can be 1.191 + // ignored or handled directly, so we're rewriting the array in-place. 1.192 + let i, k; 1.193 + for (i = 0, k = 0; i < records.length; i++) { 1.194 + let record = records[k] = records[i]; 1.195 + let shouldApply; 1.196 + 1.197 + // This is still synchronous I/O for now. 1.198 + try { 1.199 + if (record.deleted) { 1.200 + // Consider using nsIBrowserHistory::removePages() here. 1.201 + this.remove(record); 1.202 + // No further processing needed. Remove it from the list. 1.203 + shouldApply = false; 1.204 + } else { 1.205 + shouldApply = this._recordToPlaceInfo(record); 1.206 + } 1.207 + } catch(ex) { 1.208 + failed.push(record.id); 1.209 + shouldApply = false; 1.210 + } 1.211 + 1.212 + if (shouldApply) { 1.213 + k += 1; 1.214 + } 1.215 + } 1.216 + records.length = k; // truncate array 1.217 + 1.218 + // Nothing to do. 1.219 + if (!records.length) { 1.220 + return failed; 1.221 + } 1.222 + 1.223 + let updatePlacesCallback = { 1.224 + handleResult: function handleResult() {}, 1.225 + handleError: function handleError(resultCode, placeInfo) { 1.226 + failed.push(placeInfo.guid); 1.227 + }, 1.228 + handleCompletion: Async.makeSyncCallback() 1.229 + }; 1.230 + this._asyncHistory.updatePlaces(records, updatePlacesCallback); 1.231 + Async.waitForSyncCallback(updatePlacesCallback.handleCompletion); 1.232 + return failed; 1.233 + }, 1.234 + 1.235 + /** 1.236 + * Converts a Sync history record to a mozIPlaceInfo. 1.237 + * 1.238 + * Throws if an invalid record is encountered (invalid URI, etc.), 1.239 + * returns true if the record is to be applied, false otherwise 1.240 + * (no visits to add, etc.), 1.241 + */ 1.242 + _recordToPlaceInfo: function _recordToPlaceInfo(record) { 1.243 + // Sort out invalid URIs and ones Places just simply doesn't want. 1.244 + record.uri = Utils.makeURI(record.histUri); 1.245 + if (!record.uri) { 1.246 + this._log.warn("Attempted to process invalid URI, skipping."); 1.247 + throw "Invalid URI in record"; 1.248 + } 1.249 + 1.250 + if (!Utils.checkGUID(record.id)) { 1.251 + this._log.warn("Encountered record with invalid GUID: " + record.id); 1.252 + return false; 1.253 + } 1.254 + record.guid = record.id; 1.255 + 1.256 + if (!PlacesUtils.history.canAddURI(record.uri)) { 1.257 + this._log.trace("Ignoring record " + record.id + " with URI " 1.258 + + record.uri.spec + ": can't add this URI."); 1.259 + return false; 1.260 + } 1.261 + 1.262 + // We dupe visits by date and type. So an incoming visit that has 1.263 + // the same timestamp and type as a local one won't get applied. 1.264 + // To avoid creating new objects, we rewrite the query result so we 1.265 + // can simply check for containment below. 1.266 + let curVisits = this._getVisits(record.histUri); 1.267 + let i, k; 1.268 + for (i = 0; i < curVisits.length; i++) { 1.269 + curVisits[i] = curVisits[i].date + "," + curVisits[i].type; 1.270 + } 1.271 + 1.272 + // Walk through the visits, make sure we have sound data, and eliminate 1.273 + // dupes. The latter is done by rewriting the array in-place. 1.274 + for (i = 0, k = 0; i < record.visits.length; i++) { 1.275 + let visit = record.visits[k] = record.visits[i]; 1.276 + 1.277 + if (!visit.date || typeof visit.date != "number") { 1.278 + this._log.warn("Encountered record with invalid visit date: " 1.279 + + visit.date); 1.280 + throw "Visit has no date!"; 1.281 + } 1.282 + 1.283 + if (!visit.type || !(visit.type >= PlacesUtils.history.TRANSITION_LINK && 1.284 + visit.type <= PlacesUtils.history.TRANSITION_FRAMED_LINK)) { 1.285 + this._log.warn("Encountered record with invalid visit type: " 1.286 + + visit.type); 1.287 + throw "Invalid visit type!"; 1.288 + } 1.289 + 1.290 + // Dates need to be integers. 1.291 + visit.date = Math.round(visit.date); 1.292 + 1.293 + if (curVisits.indexOf(visit.date + "," + visit.type) != -1) { 1.294 + // Visit is a dupe, don't increment 'k' so the element will be 1.295 + // overwritten. 1.296 + continue; 1.297 + } 1.298 + visit.visitDate = visit.date; 1.299 + visit.transitionType = visit.type; 1.300 + k += 1; 1.301 + } 1.302 + record.visits.length = k; // truncate array 1.303 + 1.304 + // No update if there aren't any visits to apply. 1.305 + // mozIAsyncHistory::updatePlaces() wants at least one visit. 1.306 + // In any case, the only thing we could change would be the title 1.307 + // and that shouldn't change without a visit. 1.308 + if (!record.visits.length) { 1.309 + this._log.trace("Ignoring record " + record.id + " with URI " 1.310 + + record.uri.spec + ": no visits to add."); 1.311 + return false; 1.312 + } 1.313 + 1.314 + return true; 1.315 + }, 1.316 + 1.317 + remove: function HistStore_remove(record) { 1.318 + let page = this._findURLByGUID(record.id); 1.319 + if (page == null) { 1.320 + this._log.debug("Page already removed: " + record.id); 1.321 + return; 1.322 + } 1.323 + 1.324 + let uri = Utils.makeURI(page.url); 1.325 + PlacesUtils.history.removePage(uri); 1.326 + this._log.trace("Removed page: " + [record.id, page.url, page.title]); 1.327 + }, 1.328 + 1.329 + itemExists: function HistStore_itemExists(id) { 1.330 + return !!this._findURLByGUID(id); 1.331 + }, 1.332 + 1.333 + createRecord: function createRecord(id, collection) { 1.334 + let foo = this._findURLByGUID(id); 1.335 + let record = new HistoryRec(collection, id); 1.336 + if (foo) { 1.337 + record.histUri = foo.url; 1.338 + record.title = foo.title; 1.339 + record.sortindex = foo.frecency; 1.340 + record.visits = this._getVisits(record.histUri); 1.341 + } else { 1.342 + record.deleted = true; 1.343 + } 1.344 + 1.345 + return record; 1.346 + }, 1.347 + 1.348 + wipe: function HistStore_wipe() { 1.349 + PlacesUtils.history.removeAllPages(); 1.350 + } 1.351 +}; 1.352 + 1.353 +function HistoryTracker(name, engine) { 1.354 + Tracker.call(this, name, engine); 1.355 +} 1.356 +HistoryTracker.prototype = { 1.357 + __proto__: Tracker.prototype, 1.358 + 1.359 + startTracking: function() { 1.360 + this._log.info("Adding Places observer."); 1.361 + PlacesUtils.history.addObserver(this, true); 1.362 + }, 1.363 + 1.364 + stopTracking: function() { 1.365 + this._log.info("Removing Places observer."); 1.366 + PlacesUtils.history.removeObserver(this); 1.367 + }, 1.368 + 1.369 + QueryInterface: XPCOMUtils.generateQI([ 1.370 + Ci.nsINavHistoryObserver, 1.371 + Ci.nsISupportsWeakReference 1.372 + ]), 1.373 + 1.374 + onDeleteAffectsGUID: function (uri, guid, reason, source, increment) { 1.375 + if (this.ignoreAll || reason == Ci.nsINavHistoryObserver.REASON_EXPIRED) { 1.376 + return; 1.377 + } 1.378 + this._log.trace(source + ": " + uri.spec + ", reason " + reason); 1.379 + if (this.addChangedID(guid)) { 1.380 + this.score += increment; 1.381 + } 1.382 + }, 1.383 + 1.384 + onDeleteVisits: function (uri, visitTime, guid, reason) { 1.385 + this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteVisits", SCORE_INCREMENT_SMALL); 1.386 + }, 1.387 + 1.388 + onDeleteURI: function (uri, guid, reason) { 1.389 + this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteURI", SCORE_INCREMENT_XLARGE); 1.390 + }, 1.391 + 1.392 + onVisit: function (uri, vid, time, session, referrer, trans, guid) { 1.393 + if (this.ignoreAll) { 1.394 + this._log.trace("ignoreAll: ignoring visit for " + guid); 1.395 + return; 1.396 + } 1.397 + 1.398 + this._log.trace("onVisit: " + uri.spec); 1.399 + if (this.addChangedID(guid)) { 1.400 + this.score += SCORE_INCREMENT_SMALL; 1.401 + } 1.402 + }, 1.403 + 1.404 + onClearHistory: function () { 1.405 + this._log.trace("onClearHistory"); 1.406 + // Note that we're going to trigger a sync, but none of the cleared 1.407 + // pages are tracked, so the deletions will not be propagated. 1.408 + // See Bug 578694. 1.409 + this.score += SCORE_INCREMENT_XLARGE; 1.410 + }, 1.411 + 1.412 + onBeginUpdateBatch: function () {}, 1.413 + onEndUpdateBatch: function () {}, 1.414 + onPageChanged: function () {}, 1.415 + onTitleChanged: function () {}, 1.416 + onBeforeDeleteURI: function () {}, 1.417 +};