1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/nsLivemarkService.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1055 @@ 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 file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const Cc = Components.classes; 1.9 +const Ci = Components.interfaces; 1.10 +const Cr = Components.results; 1.11 +const Cu = Components.utils; 1.12 + 1.13 +//////////////////////////////////////////////////////////////////////////////// 1.14 +//// Modules 1.15 + 1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", 1.19 + "resource://gre/modules/PlacesUtils.jsm"); 1.20 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.21 + "resource://gre/modules/NetUtil.jsm"); 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.23 + "resource://gre/modules/Promise.jsm"); 1.24 +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", 1.25 + "resource://gre/modules/Deprecated.jsm"); 1.26 + 1.27 +//////////////////////////////////////////////////////////////////////////////// 1.28 +//// Services 1.29 + 1.30 +XPCOMUtils.defineLazyServiceGetter(this, "secMan", 1.31 + "@mozilla.org/scriptsecuritymanager;1", 1.32 + "nsIScriptSecurityManager"); 1.33 +XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () { 1.34 + // Lazily add an history observer when it's actually needed. 1.35 + PlacesUtils.history.addObserver(PlacesUtils.livemarks, true); 1.36 + return Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory); 1.37 +}); 1.38 + 1.39 +//////////////////////////////////////////////////////////////////////////////// 1.40 +//// Constants 1.41 + 1.42 +// Security flags for checkLoadURIWithPrincipal. 1.43 +const SEC_FLAGS = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; 1.44 + 1.45 +// Delay between reloads of consecute livemarks. 1.46 +const RELOAD_DELAY_MS = 500; 1.47 +// Expire livemarks after this time. 1.48 +const EXPIRE_TIME_MS = 3600000; // 1 hour. 1.49 +// Expire livemarks after this time on error. 1.50 +const ONERROR_EXPIRE_TIME_MS = 300000; // 5 minutes. 1.51 + 1.52 +//////////////////////////////////////////////////////////////////////////////// 1.53 +//// LivemarkService 1.54 + 1.55 +function LivemarkService() 1.56 +{ 1.57 + // Cleanup on shutdown. 1.58 + Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true); 1.59 + 1.60 + // Observe bookmarks and history, but don't init the services just for that. 1.61 + PlacesUtils.addLazyBookmarkObserver(this, true); 1.62 + 1.63 + // Asynchronously build the livemarks cache. 1.64 + this._ensureAsynchronousCache(); 1.65 +} 1.66 + 1.67 +LivemarkService.prototype = { 1.68 + // Cache of Livemark objects, hashed by bookmarks folder ids. 1.69 + _livemarks: {}, 1.70 + // Hash associating guids to bookmarks folder ids. 1.71 + _guids: {}, 1.72 + 1.73 + get _populateCacheSQL() 1.74 + { 1.75 + function getAnnoSQLFragment(aAnnoParam) { 1.76 + return "SELECT a.content " 1.77 + + "FROM moz_items_annos a " 1.78 + + "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " 1.79 + + "WHERE a.item_id = b.id " 1.80 + + "AND n.name = " + aAnnoParam; 1.81 + } 1.82 + 1.83 + return "SELECT b.id, b.title, b.parent, b.position, b.guid, b.lastModified, " 1.84 + + "(" + getAnnoSQLFragment(":feedURI_anno") + ") AS feedURI, " 1.85 + + "(" + getAnnoSQLFragment(":siteURI_anno") + ") AS siteURI " 1.86 + + "FROM moz_bookmarks b " 1.87 + + "JOIN moz_items_annos a ON a.item_id = b.id " 1.88 + + "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " 1.89 + + "WHERE b.type = :folder_type " 1.90 + + "AND n.name = :feedURI_anno "; 1.91 + }, 1.92 + 1.93 + _ensureAsynchronousCache: function LS__ensureAsynchronousCache() 1.94 + { 1.95 + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) 1.96 + .DBConnection; 1.97 + let stmt = db.createAsyncStatement(this._populateCacheSQL); 1.98 + stmt.params.folder_type = Ci.nsINavBookmarksService.TYPE_FOLDER; 1.99 + stmt.params.feedURI_anno = PlacesUtils.LMANNO_FEEDURI; 1.100 + stmt.params.siteURI_anno = PlacesUtils.LMANNO_SITEURI; 1.101 + 1.102 + let livemarkSvc = this; 1.103 + this._pendingStmt = stmt.executeAsync({ 1.104 + handleResult: function LS_handleResult(aResults) 1.105 + { 1.106 + for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) { 1.107 + let id = row.getResultByName("id"); 1.108 + let siteURL = row.getResultByName("siteURI"); 1.109 + let guid = row.getResultByName("guid"); 1.110 + livemarkSvc._livemarks[id] = 1.111 + new Livemark({ id: id, 1.112 + guid: guid, 1.113 + title: row.getResultByName("title"), 1.114 + parentId: row.getResultByName("parent"), 1.115 + index: row.getResultByName("position"), 1.116 + lastModified: row.getResultByName("lastModified"), 1.117 + feedURI: NetUtil.newURI(row.getResultByName("feedURI")), 1.118 + siteURI: siteURL ? NetUtil.newURI(siteURL) : null, 1.119 + }); 1.120 + livemarkSvc._guids[guid] = id; 1.121 + } 1.122 + }, 1.123 + handleError: function LS_handleError(aErr) 1.124 + { 1.125 + Cu.reportError("AsyncStmt error (" + aErr.result + "): '" + aErr.message); 1.126 + }, 1.127 + handleCompletion: function LS_handleCompletion() { 1.128 + livemarkSvc._pendingStmt = null; 1.129 + } 1.130 + }); 1.131 + stmt.finalize(); 1.132 + }, 1.133 + 1.134 + _onCacheReady: function LS__onCacheReady(aCallback) 1.135 + { 1.136 + if (this._pendingStmt) { 1.137 + // The cache is still being populated, so enqueue the job to the Storage 1.138 + // async thread. Ideally this should just dispatch a runnable to it, 1.139 + // that would call back on the main thread, but bug 608142 made that 1.140 + // impossible. Thus just enqueue the cheapest query possible. 1.141 + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) 1.142 + .DBConnection; 1.143 + let stmt = db.createAsyncStatement("PRAGMA encoding"); 1.144 + stmt.executeAsync({ 1.145 + handleError: function () {}, 1.146 + handleResult: function () {}, 1.147 + handleCompletion: function ETAT_handleCompletion() 1.148 + { 1.149 + aCallback(); 1.150 + } 1.151 + }); 1.152 + stmt.finalize(); 1.153 + } 1.154 + else { 1.155 + // The callbacks should always be enqueued per the interface. 1.156 + // Just enque on the main thread. 1.157 + Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); 1.158 + } 1.159 + }, 1.160 + 1.161 + _reloading: false, 1.162 + _startReloadTimer: function LS__startReloadTimer() 1.163 + { 1.164 + if (this._reloadTimer) { 1.165 + this._reloadTimer.cancel(); 1.166 + } 1.167 + else { 1.168 + this._reloadTimer = Cc["@mozilla.org/timer;1"] 1.169 + .createInstance(Ci.nsITimer); 1.170 + } 1.171 + this._reloading = true; 1.172 + this._reloadTimer.initWithCallback(this._reloadNextLivemark.bind(this), 1.173 + RELOAD_DELAY_MS, 1.174 + Ci.nsITimer.TYPE_ONE_SHOT); 1.175 + }, 1.176 + 1.177 + ////////////////////////////////////////////////////////////////////////////// 1.178 + //// nsIObserver 1.179 + 1.180 + observe: function LS_observe(aSubject, aTopic, aData) 1.181 + { 1.182 + if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) { 1.183 + if (this._pendingStmt) { 1.184 + this._pendingStmt.cancel(); 1.185 + this._pendingStmt = null; 1.186 + // Initialization never finished, so just bail out. 1.187 + return; 1.188 + } 1.189 + 1.190 + if (this._reloadTimer) { 1.191 + this._reloading = false; 1.192 + this._reloadTimer.cancel(); 1.193 + delete this._reloadTimer; 1.194 + } 1.195 + 1.196 + // Stop any ongoing update. 1.197 + for each (let livemark in this._livemarks) { 1.198 + livemark.terminate(); 1.199 + } 1.200 + this._livemarks = {}; 1.201 + } 1.202 + }, 1.203 + 1.204 + ////////////////////////////////////////////////////////////////////////////// 1.205 + //// mozIAsyncLivemarks 1.206 + 1.207 + addLivemark: function LS_addLivemark(aLivemarkInfo, 1.208 + aLivemarkCallback) 1.209 + { 1.210 + // Must provide at least non-null parentId, index and feedURI. 1.211 + if (!aLivemarkInfo || 1.212 + ("parentId" in aLivemarkInfo && aLivemarkInfo.parentId < 1) || 1.213 + !("index" in aLivemarkInfo) || aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX || 1.214 + !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) || 1.215 + (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) || 1.216 + (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) { 1.217 + throw Cr.NS_ERROR_INVALID_ARG; 1.218 + } 1.219 + 1.220 + if (aLivemarkCallback) { 1.221 + Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + 1.222 + "Please use the returned promise instead.", 1.223 + "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); 1.224 + } 1.225 + 1.226 + // The addition is done synchronously due to the fact importExport service 1.227 + // and JSON backups require that. The notification is async though. 1.228 + // Once bookmarks are async, this may be properly fixed. 1.229 + let deferred = Promise.defer(); 1.230 + let addLivemarkEx = null; 1.231 + let livemark = null; 1.232 + try { 1.233 + // Disallow adding a livemark inside another livemark. 1.234 + if (aLivemarkInfo.parentId in this._livemarks) { 1.235 + throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG); 1.236 + } 1.237 + 1.238 + // Don't pass unexpected input data to the livemark constructor. 1.239 + livemark = new Livemark({ title: aLivemarkInfo.title 1.240 + , parentId: aLivemarkInfo.parentId 1.241 + , index: aLivemarkInfo.index 1.242 + , feedURI: aLivemarkInfo.feedURI 1.243 + , siteURI: aLivemarkInfo.siteURI 1.244 + , guid: aLivemarkInfo.guid 1.245 + , lastModified: aLivemarkInfo.lastModified 1.246 + }); 1.247 + if (this._itemAdded && this._itemAdded.id == livemark.id) { 1.248 + livemark.index = this._itemAdded.index; 1.249 + livemark.guid = this._itemAdded.guid; 1.250 + if (!aLivemarkInfo.lastModified) { 1.251 + livemark.lastModified = this._itemAdded.lastModified; 1.252 + } 1.253 + } 1.254 + 1.255 + // Updating the cache even if it has not yet been populated doesn't 1.256 + // matter since it will just be overwritten. 1.257 + this._livemarks[livemark.id] = livemark; 1.258 + this._guids[livemark.guid] = livemark.id; 1.259 + } 1.260 + catch (ex) { 1.261 + addLivemarkEx = ex; 1.262 + livemark = null; 1.263 + } 1.264 + finally { 1.265 + this._onCacheReady( () => { 1.266 + if (addLivemarkEx) { 1.267 + if (aLivemarkCallback) { 1.268 + try { 1.269 + aLivemarkCallback.onCompletion(addLivemarkEx.result, livemark); 1.270 + } 1.271 + catch(ex2) { } 1.272 + } else { 1.273 + deferred.reject(addLivemarkEx); 1.274 + } 1.275 + } 1.276 + else { 1.277 + if (aLivemarkCallback) { 1.278 + try { 1.279 + aLivemarkCallback.onCompletion(Cr.NS_OK, livemark); 1.280 + } 1.281 + catch(ex2) { } 1.282 + } else { 1.283 + deferred.resolve(livemark); 1.284 + } 1.285 + } 1.286 + }); 1.287 + } 1.288 + 1.289 + return aLivemarkCallback ? null : deferred.promise; 1.290 + }, 1.291 + 1.292 + removeLivemark: function LS_removeLivemark(aLivemarkInfo, aLivemarkCallback) 1.293 + { 1.294 + if (!aLivemarkInfo) { 1.295 + throw Cr.NS_ERROR_INVALID_ARG; 1.296 + } 1.297 + 1.298 + // Accept either a guid or an id. 1.299 + let id = aLivemarkInfo.guid || aLivemarkInfo.id; 1.300 + if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) || 1.301 + ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) || 1.302 + !id) { 1.303 + throw Cr.NS_ERROR_INVALID_ARG; 1.304 + } 1.305 + 1.306 + if (aLivemarkCallback) { 1.307 + Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + 1.308 + "Please use the returned promise instead.", 1.309 + "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); 1.310 + } 1.311 + 1.312 + // Convert the guid to an id. 1.313 + if (id in this._guids) { 1.314 + id = this._guids[id]; 1.315 + } 1.316 + 1.317 + let deferred = Promise.defer(); 1.318 + let removeLivemarkEx = null; 1.319 + try { 1.320 + if (!(id in this._livemarks)) { 1.321 + throw new Components.Exception("", Cr.NS_ERROR_INVALID_ARG); 1.322 + } 1.323 + this._livemarks[id].remove(); 1.324 + } 1.325 + catch (ex) { 1.326 + removeLivemarkEx = ex; 1.327 + } 1.328 + finally { 1.329 + this._onCacheReady( () => { 1.330 + if (removeLivemarkEx) { 1.331 + if (aLivemarkCallback) { 1.332 + try { 1.333 + aLivemarkCallback.onCompletion(removeLivemarkEx.result, null); 1.334 + } 1.335 + catch(ex2) { } 1.336 + } else { 1.337 + deferred.reject(removeLivemarkEx); 1.338 + } 1.339 + } 1.340 + else { 1.341 + if (aLivemarkCallback) { 1.342 + try { 1.343 + aLivemarkCallback.onCompletion(Cr.NS_OK, null); 1.344 + } 1.345 + catch(ex2) { } 1.346 + } else { 1.347 + deferred.resolve(); 1.348 + } 1.349 + } 1.350 + }); 1.351 + } 1.352 + 1.353 + return aLivemarkCallback ? null : deferred.promise; 1.354 + }, 1.355 + 1.356 + _reloaded: [], 1.357 + _reloadNextLivemark: function LS__reloadNextLivemark() 1.358 + { 1.359 + this._reloading = false; 1.360 + // Find first livemark to be reloaded. 1.361 + for (let id in this._livemarks) { 1.362 + if (this._reloaded.indexOf(id) == -1) { 1.363 + this._reloaded.push(id); 1.364 + this._livemarks[id].reload(this._forceUpdate); 1.365 + this._startReloadTimer(); 1.366 + break; 1.367 + } 1.368 + } 1.369 + }, 1.370 + 1.371 + reloadLivemarks: function LS_reloadLivemarks(aForceUpdate) 1.372 + { 1.373 + // Check if there's a currently running reload, to save some useless work. 1.374 + let notWorthRestarting = 1.375 + this._forceUpdate || // We're already forceUpdating. 1.376 + !aForceUpdate; // The caller didn't request a forced update. 1.377 + if (this._reloading && notWorthRestarting) { 1.378 + // Ignore this call. 1.379 + return; 1.380 + } 1.381 + 1.382 + this._onCacheReady( () => { 1.383 + this._forceUpdate = !!aForceUpdate; 1.384 + this._reloaded = []; 1.385 + // Livemarks reloads happen on a timer, and are delayed for performance 1.386 + // reasons. 1.387 + this._startReloadTimer(); 1.388 + }); 1.389 + }, 1.390 + 1.391 + getLivemark: function LS_getLivemark(aLivemarkInfo, aLivemarkCallback) 1.392 + { 1.393 + if (!aLivemarkInfo) { 1.394 + throw Cr.NS_ERROR_INVALID_ARG; 1.395 + } 1.396 + // Accept either a guid or an id. 1.397 + let id = aLivemarkInfo.guid || aLivemarkInfo.id; 1.398 + if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) || 1.399 + ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) || 1.400 + !id) { 1.401 + throw Cr.NS_ERROR_INVALID_ARG; 1.402 + } 1.403 + 1.404 + if (aLivemarkCallback) { 1.405 + Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " + 1.406 + "Please use the returned promise instead.", 1.407 + "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm"); 1.408 + } 1.409 + 1.410 + let deferred = Promise.defer(); 1.411 + this._onCacheReady( () => { 1.412 + // Convert the guid to an id. 1.413 + if (id in this._guids) { 1.414 + id = this._guids[id]; 1.415 + } 1.416 + if (id in this._livemarks) { 1.417 + if (aLivemarkCallback) { 1.418 + try { 1.419 + aLivemarkCallback.onCompletion(Cr.NS_OK, this._livemarks[id]); 1.420 + } catch (ex) {} 1.421 + } else { 1.422 + deferred.resolve(this._livemarks[id]); 1.423 + } 1.424 + } 1.425 + else { 1.426 + if (aLivemarkCallback) { 1.427 + try { 1.428 + aLivemarkCallback.onCompletion(Cr.NS_ERROR_INVALID_ARG, null); 1.429 + } catch (ex) { } 1.430 + } else { 1.431 + deferred.reject(Components.Exception("", Cr.NS_ERROR_INVALID_ARG)); 1.432 + } 1.433 + } 1.434 + }); 1.435 + 1.436 + return aLivemarkCallback ? null : deferred.promise; 1.437 + }, 1.438 + 1.439 + ////////////////////////////////////////////////////////////////////////////// 1.440 + //// nsINavBookmarkObserver 1.441 + 1.442 + onBeginUpdateBatch: function () {}, 1.443 + onEndUpdateBatch: function () {}, 1.444 + onItemVisited: function () {}, 1.445 + 1.446 + _itemAdded: null, 1.447 + onItemAdded: function LS_onItemAdded(aItemId, aParentId, aIndex, aItemType, 1.448 + aURI, aTitle, aDateAdded, aGUID) 1.449 + { 1.450 + if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) { 1.451 + this._itemAdded = { id: aItemId 1.452 + , guid: aGUID 1.453 + , index: aIndex 1.454 + , lastModified: aDateAdded 1.455 + }; 1.456 + } 1.457 + }, 1.458 + 1.459 + onItemChanged: function LS_onItemChanged(aItemId, aProperty, aIsAnno, aValue, 1.460 + aLastModified, aItemType) 1.461 + { 1.462 + if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) { 1.463 + if (this._itemAdded && this._itemAdded.id == aItemId) { 1.464 + this._itemAdded.lastModified = aLastModified; 1.465 + } 1.466 + if (aItemId in this._livemarks) { 1.467 + if (aProperty == "title") { 1.468 + this._livemarks[aItemId].title = aValue; 1.469 + } 1.470 + this._livemarks[aItemId].lastModified = aLastModified; 1.471 + } 1.472 + } 1.473 + }, 1.474 + 1.475 + onItemMoved: function LS_onItemMoved(aItemId, aOldParentId, aOldIndex, 1.476 + aNewParentId, aNewIndex, aItemType) 1.477 + { 1.478 + if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER && 1.479 + aItemId in this._livemarks) { 1.480 + this._livemarks[aItemId].parentId = aNewParentId; 1.481 + this._livemarks[aItemId].index = aNewIndex; 1.482 + } 1.483 + }, 1.484 + 1.485 + onItemRemoved: function LS_onItemRemoved(aItemId, aParentId, aIndex, 1.486 + aItemType, aURI, aGUID) 1.487 + { 1.488 + if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER && 1.489 + aItemId in this._livemarks) { 1.490 + this._livemarks[aItemId].terminate(); 1.491 + delete this._livemarks[aItemId]; 1.492 + delete this._guids[aGUID]; 1.493 + } 1.494 + }, 1.495 + 1.496 + ////////////////////////////////////////////////////////////////////////////// 1.497 + //// nsINavHistoryObserver 1.498 + 1.499 + onBeginUpdateBatch: function () {}, 1.500 + onEndUpdateBatch: function () {}, 1.501 + onPageChanged: function () {}, 1.502 + onTitleChanged: function () {}, 1.503 + onDeleteVisits: function () {}, 1.504 + onClearHistory: function () {}, 1.505 + 1.506 + onDeleteURI: function PS_onDeleteURI(aURI) { 1.507 + for each (let livemark in this._livemarks) { 1.508 + livemark.updateURIVisitedStatus(aURI, false); 1.509 + } 1.510 + }, 1.511 + 1.512 + onVisit: function PS_onVisit(aURI) { 1.513 + for each (let livemark in this._livemarks) { 1.514 + livemark.updateURIVisitedStatus(aURI, true); 1.515 + } 1.516 + }, 1.517 + 1.518 + ////////////////////////////////////////////////////////////////////////////// 1.519 + //// nsISupports 1.520 + 1.521 + classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"), 1.522 + 1.523 + _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService), 1.524 + 1.525 + QueryInterface: XPCOMUtils.generateQI([ 1.526 + Ci.mozIAsyncLivemarks 1.527 + , Ci.nsINavBookmarkObserver 1.528 + , Ci.nsINavHistoryObserver 1.529 + , Ci.nsIObserver 1.530 + , Ci.nsISupportsWeakReference 1.531 + ]) 1.532 +}; 1.533 + 1.534 +//////////////////////////////////////////////////////////////////////////////// 1.535 +//// Livemark 1.536 + 1.537 +/** 1.538 + * Object used internally to represent a livemark. 1.539 + * 1.540 + * @param aLivemarkInfo 1.541 + * Object containing information on the livemark. If the livemark is 1.542 + * not included in the object, a new livemark will be created. 1.543 + * 1.544 + * @note terminate() must be invoked before getting rid of this object. 1.545 + */ 1.546 +function Livemark(aLivemarkInfo) 1.547 +{ 1.548 + this.title = aLivemarkInfo.title; 1.549 + this.parentId = aLivemarkInfo.parentId; 1.550 + this.index = aLivemarkInfo.index; 1.551 + 1.552 + this._status = Ci.mozILivemark.STATUS_READY; 1.553 + 1.554 + // Hash of resultObservers, hashed by container. 1.555 + this._resultObservers = new Map(); 1.556 + // This keeps a list of the containers used as keys in the map, since 1.557 + // it's not iterable. In future may use an iterable Map. 1.558 + this._resultObserversList = []; 1.559 + 1.560 + // Sorted array of objects representing livemark children in the form 1.561 + // { uri, title, visited }. 1.562 + this._children = []; 1.563 + 1.564 + // Keeps a separate array of nodes for each requesting container, hashed by 1.565 + // the container itself. 1.566 + this._nodes = new Map(); 1.567 + 1.568 + this._guid = ""; 1.569 + this._lastModified = 0; 1.570 + 1.571 + this.loadGroup = null; 1.572 + this.feedURI = null; 1.573 + this.siteURI = null; 1.574 + this.expireTime = 0; 1.575 + 1.576 + if (aLivemarkInfo.id) { 1.577 + // This request comes from the cache. 1.578 + this.id = aLivemarkInfo.id; 1.579 + this.guid = aLivemarkInfo.guid; 1.580 + this.feedURI = aLivemarkInfo.feedURI; 1.581 + this.siteURI = aLivemarkInfo.siteURI; 1.582 + this.lastModified = aLivemarkInfo.lastModified; 1.583 + } 1.584 + else { 1.585 + // Create a new livemark. 1.586 + this.id = PlacesUtils.bookmarks.createFolder(aLivemarkInfo.parentId, 1.587 + aLivemarkInfo.title, 1.588 + aLivemarkInfo.index, 1.589 + aLivemarkInfo.guid); 1.590 + PlacesUtils.bookmarks.setFolderReadonly(this.id, true); 1.591 + this.writeFeedURI(aLivemarkInfo.feedURI); 1.592 + if (aLivemarkInfo.siteURI) { 1.593 + this.writeSiteURI(aLivemarkInfo.siteURI); 1.594 + } 1.595 + // Last modified time must be the last change. 1.596 + if (aLivemarkInfo.lastModified) { 1.597 + this.lastModified = aLivemarkInfo.lastModified; 1.598 + PlacesUtils.bookmarks.setItemLastModified(this.id, this.lastModified); 1.599 + } 1.600 + } 1.601 +} 1.602 + 1.603 +Livemark.prototype = { 1.604 + get status() this._status, 1.605 + set status(val) { 1.606 + if (this._status != val) { 1.607 + this._status = val; 1.608 + this._invalidateRegisteredContainers(); 1.609 + } 1.610 + return this._status; 1.611 + }, 1.612 + 1.613 + /** 1.614 + * Sets an annotation on the bookmarks folder id representing the livemark. 1.615 + * 1.616 + * @param aAnnoName 1.617 + * Name of the annotation. 1.618 + * @param aValue 1.619 + * Value of the annotation. 1.620 + * @return The annotation value. 1.621 + * @throws If the folder is invalid. 1.622 + */ 1.623 + _setAnno: function LM__setAnno(aAnnoName, aValue) 1.624 + { 1.625 + PlacesUtils.annotations 1.626 + .setItemAnnotation(this.id, aAnnoName, aValue, 0, 1.627 + PlacesUtils.annotations.EXPIRE_NEVER); 1.628 + }, 1.629 + 1.630 + writeFeedURI: function LM_writeFeedURI(aFeedURI) 1.631 + { 1.632 + this._setAnno(PlacesUtils.LMANNO_FEEDURI, aFeedURI.spec); 1.633 + this.feedURI = aFeedURI; 1.634 + }, 1.635 + 1.636 + writeSiteURI: function LM_writeSiteURI(aSiteURI) 1.637 + { 1.638 + if (!aSiteURI) { 1.639 + PlacesUtils.annotations.removeItemAnnotation(this.id, 1.640 + PlacesUtils.LMANNO_SITEURI) 1.641 + this.siteURI = null; 1.642 + return; 1.643 + } 1.644 + 1.645 + // Security check the site URI against the feed URI principal. 1.646 + let feedPrincipal = secMan.getSimpleCodebasePrincipal(this.feedURI); 1.647 + try { 1.648 + secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI, SEC_FLAGS); 1.649 + } 1.650 + catch (ex) { 1.651 + return; 1.652 + } 1.653 + 1.654 + this._setAnno(PlacesUtils.LMANNO_SITEURI, aSiteURI.spec) 1.655 + this.siteURI = aSiteURI; 1.656 + }, 1.657 + 1.658 + set guid(aGUID) { 1.659 + this._guid = aGUID; 1.660 + return aGUID; 1.661 + }, 1.662 + get guid() this._guid, 1.663 + 1.664 + set lastModified(aLastModified) { 1.665 + this._lastModified = aLastModified; 1.666 + return aLastModified; 1.667 + }, 1.668 + get lastModified() this._lastModified, 1.669 + 1.670 + /** 1.671 + * Tries to updates the livemark if needed. 1.672 + * The update process is asynchronous. 1.673 + * 1.674 + * @param [optional] aForceUpdate 1.675 + * If true will try to update the livemark even if its contents have 1.676 + * not yet expired. 1.677 + */ 1.678 + updateChildren: function LM_updateChildren(aForceUpdate) 1.679 + { 1.680 + // Check if the livemark is already updating. 1.681 + if (this.status == Ci.mozILivemark.STATUS_LOADING) 1.682 + return; 1.683 + 1.684 + // Check the TTL/expiration on this, to check if there is no need to update 1.685 + // this livemark. 1.686 + if (!aForceUpdate && this.children.length && this.expireTime > Date.now()) 1.687 + return; 1.688 + 1.689 + this.status = Ci.mozILivemark.STATUS_LOADING; 1.690 + 1.691 + // Setting the status notifies observers that may remove the livemark. 1.692 + if (this._terminated) 1.693 + return; 1.694 + 1.695 + try { 1.696 + // Create a load group for the request. This will allow us to 1.697 + // automatically keep track of redirects, so we can always 1.698 + // cancel the channel. 1.699 + let loadgroup = Cc["@mozilla.org/network/load-group;1"]. 1.700 + createInstance(Ci.nsILoadGroup); 1.701 + let channel = NetUtil.newChannel(this.feedURI.spec). 1.702 + QueryInterface(Ci.nsIHttpChannel); 1.703 + channel.loadGroup = loadgroup; 1.704 + channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND | 1.705 + Ci.nsIRequest.LOAD_BYPASS_CACHE; 1.706 + channel.requestMethod = "GET"; 1.707 + channel.setRequestHeader("X-Moz", "livebookmarks", false); 1.708 + 1.709 + // Stream the result to the feed parser with this listener 1.710 + let listener = new LivemarkLoadListener(this); 1.711 + channel.notificationCallbacks = listener; 1.712 + channel.asyncOpen(listener, null); 1.713 + 1.714 + this.loadGroup = loadgroup; 1.715 + } 1.716 + catch (ex) { 1.717 + this.status = Ci.mozILivemark.STATUS_FAILED; 1.718 + } 1.719 + }, 1.720 + 1.721 + reload: function LM_reload(aForceUpdate) 1.722 + { 1.723 + this.updateChildren(aForceUpdate); 1.724 + }, 1.725 + 1.726 + remove: function LM_remove() { 1.727 + PlacesUtils.bookmarks.removeItem(this.id); 1.728 + }, 1.729 + 1.730 + get children() this._children, 1.731 + set children(val) { 1.732 + this._children = val; 1.733 + 1.734 + // Discard the previous cached nodes, new ones should be generated. 1.735 + for (let i = 0; i < this._resultObserversList.length; i++) { 1.736 + let container = this._resultObserversList[i]; 1.737 + this._nodes.delete(container); 1.738 + } 1.739 + 1.740 + // Update visited status for each entry. 1.741 + for (let i = 0; i < this._children.length; i++) { 1.742 + let child = this._children[i]; 1.743 + asyncHistory.isURIVisited(child.uri, 1.744 + (function(aURI, aIsVisited) { 1.745 + this.updateURIVisitedStatus(aURI, aIsVisited); 1.746 + }).bind(this)); 1.747 + } 1.748 + 1.749 + return this._children; 1.750 + }, 1.751 + 1.752 + _isURIVisited: function LM__isURIVisited(aURI) { 1.753 + for (let i = 0; i < this.children.length; i++) { 1.754 + if (this.children[i].uri.equals(aURI)) { 1.755 + return this.children[i].visited; 1.756 + } 1.757 + } 1.758 + }, 1.759 + 1.760 + getNodesForContainer: function LM_getNodesForContainer(aContainerNode) 1.761 + { 1.762 + if (this._nodes.has(aContainerNode)) { 1.763 + return this._nodes.get(aContainerNode); 1.764 + } 1.765 + 1.766 + let livemark = this; 1.767 + let nodes = []; 1.768 + let now = Date.now() * 1000; 1.769 + for (let i = 0; i < this._children.length; i++) { 1.770 + let child = this._children[i]; 1.771 + let node = { 1.772 + // The QueryInterface is needed cause aContainerNode is a jsval. 1.773 + // This is required to avoid issues with scriptable wrappers that would 1.774 + // not allow the view to correctly set expandos. 1.775 + get parent() 1.776 + aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode), 1.777 + get parentResult() this.parent.parentResult, 1.778 + get uri() child.uri.spec, 1.779 + get type() Ci.nsINavHistoryResultNode.RESULT_TYPE_URI, 1.780 + get title() child.title, 1.781 + get accessCount() 1.782 + Number(livemark._isURIVisited(NetUtil.newURI(this.uri))), 1.783 + get time() 0, 1.784 + get icon() "", 1.785 + get indentLevel() this.parent.indentLevel + 1, 1.786 + get bookmarkIndex() -1, 1.787 + get itemId() -1, 1.788 + get dateAdded() now + i, 1.789 + get lastModified() now + i, 1.790 + get tags() 1.791 + PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", "), 1.792 + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode]) 1.793 + }; 1.794 + nodes.push(node); 1.795 + } 1.796 + this._nodes.set(aContainerNode, nodes); 1.797 + return nodes; 1.798 + }, 1.799 + 1.800 + registerForUpdates: function LM_registerForUpdates(aContainerNode, 1.801 + aResultObserver) 1.802 + { 1.803 + this._resultObservers.set(aContainerNode, aResultObserver); 1.804 + this._resultObserversList.push(aContainerNode); 1.805 + }, 1.806 + 1.807 + unregisterForUpdates: function LM_unregisterForUpdates(aContainerNode) 1.808 + { 1.809 + this._resultObservers.delete(aContainerNode); 1.810 + let index = this._resultObserversList.indexOf(aContainerNode); 1.811 + this._resultObserversList.splice(index, 1); 1.812 + 1.813 + this._nodes.delete(aContainerNode); 1.814 + }, 1.815 + 1.816 + _invalidateRegisteredContainers: function LM__invalidateRegisteredContainers() 1.817 + { 1.818 + for (let i = 0; i < this._resultObserversList.length; i++) { 1.819 + let container = this._resultObserversList[i]; 1.820 + let observer = this._resultObservers.get(container); 1.821 + observer.invalidateContainer(container); 1.822 + } 1.823 + }, 1.824 + 1.825 + updateURIVisitedStatus: 1.826 + function LM_updateURIVisitedStatus(aURI, aVisitedStatus) 1.827 + { 1.828 + for (let i = 0; i < this.children.length; i++) { 1.829 + if (this.children[i].uri.equals(aURI)) { 1.830 + this.children[i].visited = aVisitedStatus; 1.831 + } 1.832 + } 1.833 + 1.834 + for (let i = 0; i < this._resultObserversList.length; i++) { 1.835 + let container = this._resultObserversList[i]; 1.836 + let observer = this._resultObservers.get(container); 1.837 + if (this._nodes.has(container)) { 1.838 + let nodes = this._nodes.get(container); 1.839 + for (let j = 0; j < nodes.length; j++) { 1.840 + let node = nodes[j]; 1.841 + if (node.uri == aURI.spec) { 1.842 + Services.tm.mainThread.dispatch((function () { 1.843 + observer.nodeHistoryDetailsChanged(node, 0, aVisitedStatus); 1.844 + }).bind(this), Ci.nsIThread.DISPATCH_NORMAL); 1.845 + } 1.846 + } 1.847 + } 1.848 + } 1.849 + }, 1.850 + 1.851 + /** 1.852 + * Terminates the livemark entry, cancelling any ongoing load. 1.853 + * Must be invoked before destroying the entry. 1.854 + */ 1.855 + terminate: function LM_terminate() 1.856 + { 1.857 + // Avoid handling any updateChildren request from now on. 1.858 + this._terminated = true; 1.859 + // Clear the list before aborting, since abort() would try to set the 1.860 + // status and notify about it, but that's not really useful at this point. 1.861 + this._resultObserversList = []; 1.862 + this.abort(); 1.863 + }, 1.864 + 1.865 + /** 1.866 + * Aborts the livemark loading if needed. 1.867 + */ 1.868 + abort: function LM_abort() 1.869 + { 1.870 + this.status = Ci.mozILivemark.STATUS_FAILED; 1.871 + if (this.loadGroup) { 1.872 + this.loadGroup.cancel(Cr.NS_BINDING_ABORTED); 1.873 + this.loadGroup = null; 1.874 + } 1.875 + }, 1.876 + 1.877 + QueryInterface: XPCOMUtils.generateQI([ 1.878 + Ci.mozILivemark 1.879 + ]) 1.880 +} 1.881 + 1.882 +//////////////////////////////////////////////////////////////////////////////// 1.883 +//// LivemarkLoadListener 1.884 + 1.885 +/** 1.886 + * Object used internally to handle loading a livemark's contents. 1.887 + * 1.888 + * @param aLivemark 1.889 + * The Livemark that is loading. 1.890 + */ 1.891 +function LivemarkLoadListener(aLivemark) 1.892 +{ 1.893 + this._livemark = aLivemark; 1.894 + this._processor = null; 1.895 + this._isAborted = false; 1.896 + this._ttl = EXPIRE_TIME_MS; 1.897 +} 1.898 + 1.899 +LivemarkLoadListener.prototype = { 1.900 + abort: function LLL_abort(aException) 1.901 + { 1.902 + if (!this._isAborted) { 1.903 + this._isAborted = true; 1.904 + this._livemark.abort(); 1.905 + this._setResourceTTL(ONERROR_EXPIRE_TIME_MS); 1.906 + } 1.907 + }, 1.908 + 1.909 + // nsIFeedResultListener 1.910 + handleResult: function LLL_handleResult(aResult) 1.911 + { 1.912 + if (this._isAborted) { 1.913 + return; 1.914 + } 1.915 + 1.916 + try { 1.917 + // We need this to make sure the item links are safe 1.918 + let feedPrincipal = 1.919 + secMan.getSimpleCodebasePrincipal(this._livemark.feedURI); 1.920 + 1.921 + // Enforce well-formedness because the existing code does 1.922 + if (!aResult || !aResult.doc || aResult.bozo) { 1.923 + throw new Components.Exception("", Cr.NS_ERROR_FAILURE); 1.924 + } 1.925 + 1.926 + let feed = aResult.doc.QueryInterface(Ci.nsIFeed); 1.927 + let siteURI = this._livemark.siteURI; 1.928 + if (feed.link && (!siteURI || !feed.link.equals(siteURI))) { 1.929 + siteURI = feed.link; 1.930 + this._livemark.writeSiteURI(siteURI); 1.931 + } 1.932 + 1.933 + // Insert feed items. 1.934 + let livemarkChildren = []; 1.935 + for (let i = 0; i < feed.items.length; ++i) { 1.936 + let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry); 1.937 + let uri = entry.link || siteURI; 1.938 + if (!uri) { 1.939 + continue; 1.940 + } 1.941 + 1.942 + try { 1.943 + secMan.checkLoadURIWithPrincipal(feedPrincipal, uri, SEC_FLAGS); 1.944 + } 1.945 + catch(ex) { 1.946 + continue; 1.947 + } 1.948 + 1.949 + let title = entry.title ? entry.title.plainText() : ""; 1.950 + livemarkChildren.push({ uri: uri, title: title, visited: false }); 1.951 + } 1.952 + 1.953 + this._livemark.children = livemarkChildren; 1.954 + } 1.955 + catch (ex) { 1.956 + this.abort(ex); 1.957 + } 1.958 + finally { 1.959 + this._processor.listener = null; 1.960 + this._processor = null; 1.961 + } 1.962 + }, 1.963 + 1.964 + onDataAvailable: function LLL_onDataAvailable(aRequest, aContext, 1.965 + aInputStream, aSourceOffset, 1.966 + aCount) 1.967 + { 1.968 + if (this._processor) { 1.969 + this._processor.onDataAvailable(aRequest, aContext, aInputStream, 1.970 + aSourceOffset, aCount); 1.971 + } 1.972 + }, 1.973 + 1.974 + onStartRequest: function LLL_onStartRequest(aRequest, aContext) 1.975 + { 1.976 + if (this._isAborted) { 1.977 + throw Cr.NS_ERROR_UNEXPECTED; 1.978 + } 1.979 + 1.980 + let channel = aRequest.QueryInterface(Ci.nsIChannel); 1.981 + try { 1.982 + // Parse feed data as it comes in 1.983 + this._processor = Cc["@mozilla.org/feed-processor;1"]. 1.984 + createInstance(Ci.nsIFeedProcessor); 1.985 + this._processor.listener = this; 1.986 + this._processor.parseAsync(null, channel.URI); 1.987 + this._processor.onStartRequest(aRequest, aContext); 1.988 + } 1.989 + catch (ex) { 1.990 + Components.utils.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec); 1.991 + this.abort(ex); 1.992 + } 1.993 + }, 1.994 + 1.995 + onStopRequest: function LLL_onStopRequest(aRequest, aContext, aStatus) 1.996 + { 1.997 + if (!Components.isSuccessCode(aStatus)) { 1.998 + this.abort(); 1.999 + return; 1.1000 + } 1.1001 + 1.1002 + // Set an expiration on the livemark, to reloading the data in future. 1.1003 + try { 1.1004 + if (this._processor) { 1.1005 + this._processor.onStopRequest(aRequest, aContext, aStatus); 1.1006 + } 1.1007 + 1.1008 + // Calculate a new ttl 1.1009 + let channel = aRequest.QueryInterface(Ci.nsICachingChannel); 1.1010 + if (channel) { 1.1011 + let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry); 1.1012 + if (entryInfo) { 1.1013 + // nsICacheEntry returns value as seconds. 1.1014 + let expireTime = entryInfo.expirationTime * 1000; 1.1015 + let nowTime = Date.now(); 1.1016 + // Note, expireTime can be 0, see bug 383538. 1.1017 + if (expireTime > nowTime) { 1.1018 + this._setResourceTTL(Math.max((expireTime - nowTime), 1.1019 + EXPIRE_TIME_MS)); 1.1020 + return; 1.1021 + } 1.1022 + } 1.1023 + } 1.1024 + this._setResourceTTL(EXPIRE_TIME_MS); 1.1025 + } 1.1026 + catch (ex) { 1.1027 + this.abort(ex); 1.1028 + } 1.1029 + finally { 1.1030 + if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) { 1.1031 + this._livemark.status = Ci.mozILivemark.STATUS_READY; 1.1032 + } 1.1033 + this._livemark.locked = false; 1.1034 + this._livemark.loadGroup = null; 1.1035 + } 1.1036 + }, 1.1037 + 1.1038 + _setResourceTTL: function LLL__setResourceTTL(aMilliseconds) 1.1039 + { 1.1040 + this._livemark.expireTime = Date.now() + aMilliseconds; 1.1041 + }, 1.1042 + 1.1043 + // nsIInterfaceRequestor 1.1044 + getInterface: function LLL_getInterface(aIID) 1.1045 + { 1.1046 + return this.QueryInterface(aIID); 1.1047 + }, 1.1048 + 1.1049 + // nsISupports 1.1050 + QueryInterface: XPCOMUtils.generateQI([ 1.1051 + Ci.nsIFeedResultListener 1.1052 + , Ci.nsIStreamListener 1.1053 + , Ci.nsIRequestObserver 1.1054 + , Ci.nsIInterfaceRequestor 1.1055 + ]) 1.1056 +} 1.1057 + 1.1058 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);