1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/tests/head_common.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,954 @@ 1.4 +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +const CURRENT_SCHEMA_VERSION = 23; 1.10 + 1.11 +const NS_APP_USER_PROFILE_50_DIR = "ProfD"; 1.12 +const NS_APP_PROFILE_DIR_STARTUP = "ProfDS"; 1.13 + 1.14 +// Shortcuts to transitions type. 1.15 +const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK; 1.16 +const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED; 1.17 +const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK; 1.18 +const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED; 1.19 +const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK; 1.20 +const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT; 1.21 +const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY; 1.22 +const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD; 1.23 + 1.24 +const TITLE_LENGTH_MAX = 4096; 1.25 + 1.26 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.27 + 1.28 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", 1.29 + "resource://gre/modules/FileUtils.jsm"); 1.30 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.31 + "resource://gre/modules/NetUtil.jsm"); 1.32 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.33 + "resource://gre/modules/Promise.jsm"); 1.34 +XPCOMUtils.defineLazyModuleGetter(this, "Services", 1.35 + "resource://gre/modules/Services.jsm"); 1.36 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.37 + "resource://gre/modules/Task.jsm"); 1.38 +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", 1.39 + "resource://gre/modules/BookmarkJSONUtils.jsm"); 1.40 +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils", 1.41 + "resource://gre/modules/BookmarkHTMLUtils.jsm"); 1.42 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", 1.43 + "resource://gre/modules/PlacesBackups.jsm"); 1.44 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions", 1.45 + "resource://gre/modules/PlacesTransactions.jsm"); 1.46 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.47 + "resource://gre/modules/osfile.jsm"); 1.48 + 1.49 +// This imports various other objects in addition to PlacesUtils. 1.50 +Cu.import("resource://gre/modules/PlacesUtils.jsm"); 1.51 + 1.52 +XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() { 1.53 + return NetUtil.newURI( 1.54 + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" + 1.55 + "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="); 1.56 +}); 1.57 + 1.58 +function LOG(aMsg) { 1.59 + aMsg = ("*** PLACES TESTS: " + aMsg); 1.60 + Services.console.logStringMessage(aMsg); 1.61 + print(aMsg); 1.62 +} 1.63 + 1.64 +let gTestDir = do_get_cwd(); 1.65 + 1.66 +// Initialize profile. 1.67 +let gProfD = do_get_profile(); 1.68 + 1.69 +// Remove any old database. 1.70 +clearDB(); 1.71 + 1.72 +/** 1.73 + * Shortcut to create a nsIURI. 1.74 + * 1.75 + * @param aSpec 1.76 + * URLString of the uri. 1.77 + */ 1.78 +function uri(aSpec) NetUtil.newURI(aSpec); 1.79 + 1.80 + 1.81 +/** 1.82 + * Gets the database connection. If the Places connection is invalid it will 1.83 + * try to create a new connection. 1.84 + * 1.85 + * @param [optional] aForceNewConnection 1.86 + * Forces creation of a new connection to the database. When a 1.87 + * connection is asyncClosed it cannot anymore schedule async statements, 1.88 + * though connectionReady will keep returning true (Bug 726990). 1.89 + * 1.90 + * @return The database connection or null if unable to get one. 1.91 + */ 1.92 +let gDBConn; 1.93 +function DBConn(aForceNewConnection) { 1.94 + if (!aForceNewConnection) { 1.95 + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) 1.96 + .DBConnection; 1.97 + if (db.connectionReady) 1.98 + return db; 1.99 + } 1.100 + 1.101 + // If the Places database connection has been closed, create a new connection. 1.102 + if (!gDBConn || aForceNewConnection) { 1.103 + let file = Services.dirsvc.get('ProfD', Ci.nsIFile); 1.104 + file.append("places.sqlite"); 1.105 + let dbConn = gDBConn = Services.storage.openDatabase(file); 1.106 + 1.107 + // Be sure to cleanly close this connection. 1.108 + Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) { 1.109 + Services.obs.removeObserver(DBCloseCallback, aTopic); 1.110 + dbConn.asyncClose(); 1.111 + }, "profile-before-change", false); 1.112 + } 1.113 + 1.114 + return gDBConn.connectionReady ? gDBConn : null; 1.115 +}; 1.116 + 1.117 +/** 1.118 + * Reads data from the provided inputstream. 1.119 + * 1.120 + * @return an array of bytes. 1.121 + */ 1.122 +function readInputStreamData(aStream) { 1.123 + let bistream = Cc["@mozilla.org/binaryinputstream;1"]. 1.124 + createInstance(Ci.nsIBinaryInputStream); 1.125 + try { 1.126 + bistream.setInputStream(aStream); 1.127 + let expectedData = []; 1.128 + let avail; 1.129 + while ((avail = bistream.available())) { 1.130 + expectedData = expectedData.concat(bistream.readByteArray(avail)); 1.131 + } 1.132 + return expectedData; 1.133 + } finally { 1.134 + bistream.close(); 1.135 + } 1.136 +} 1.137 + 1.138 +/** 1.139 + * Reads the data from the specified nsIFile. 1.140 + * 1.141 + * @param aFile 1.142 + * The nsIFile to read from. 1.143 + * @return an array of bytes. 1.144 + */ 1.145 +function readFileData(aFile) { 1.146 + let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]. 1.147 + createInstance(Ci.nsIFileInputStream); 1.148 + // init the stream as RD_ONLY, -1 == default permissions. 1.149 + inputStream.init(aFile, 0x01, -1, null); 1.150 + 1.151 + // Check the returned size versus the expected size. 1.152 + let size = inputStream.available(); 1.153 + let bytes = readInputStreamData(inputStream); 1.154 + if (size != bytes.length) { 1.155 + throw "Didn't read expected number of bytes"; 1.156 + } 1.157 + return bytes; 1.158 +} 1.159 + 1.160 +/** 1.161 + * Reads the data from the named file, verifying the expected file length. 1.162 + * 1.163 + * @param aFileName 1.164 + * This file should be located in the same folder as the test. 1.165 + * @param aExpectedLength 1.166 + * Expected length of the file. 1.167 + * 1.168 + * @return The array of bytes read from the file. 1.169 + */ 1.170 +function readFileOfLength(aFileName, aExpectedLength) { 1.171 + let data = readFileData(do_get_file(aFileName)); 1.172 + do_check_eq(data.length, aExpectedLength); 1.173 + return data; 1.174 +} 1.175 + 1.176 + 1.177 +/** 1.178 + * Returns the base64-encoded version of the given string. This function is 1.179 + * similar to window.btoa, but is available to xpcshell tests also. 1.180 + * 1.181 + * @param aString 1.182 + * Each character in this string corresponds to a byte, and must be a 1.183 + * code point in the range 0-255. 1.184 + * 1.185 + * @return The base64-encoded string. 1.186 + */ 1.187 +function base64EncodeString(aString) { 1.188 + var stream = Cc["@mozilla.org/io/string-input-stream;1"] 1.189 + .createInstance(Ci.nsIStringInputStream); 1.190 + stream.setData(aString, aString.length); 1.191 + var encoder = Cc["@mozilla.org/scriptablebase64encoder;1"] 1.192 + .createInstance(Ci.nsIScriptableBase64Encoder); 1.193 + return encoder.encodeToString(stream, aString.length); 1.194 +} 1.195 + 1.196 + 1.197 +/** 1.198 + * Compares two arrays, and returns true if they are equal. 1.199 + * 1.200 + * @param aArray1 1.201 + * First array to compare. 1.202 + * @param aArray2 1.203 + * Second array to compare. 1.204 + */ 1.205 +function compareArrays(aArray1, aArray2) { 1.206 + if (aArray1.length != aArray2.length) { 1.207 + print("compareArrays: array lengths differ\n"); 1.208 + return false; 1.209 + } 1.210 + 1.211 + for (let i = 0; i < aArray1.length; i++) { 1.212 + if (aArray1[i] != aArray2[i]) { 1.213 + print("compareArrays: arrays differ at index " + i + ": " + 1.214 + "(" + aArray1[i] + ") != (" + aArray2[i] +")\n"); 1.215 + return false; 1.216 + } 1.217 + } 1.218 + 1.219 + return true; 1.220 +} 1.221 + 1.222 + 1.223 +/** 1.224 + * Deletes a previously created sqlite file from the profile folder. 1.225 + */ 1.226 +function clearDB() { 1.227 + try { 1.228 + let file = Services.dirsvc.get('ProfD', Ci.nsIFile); 1.229 + file.append("places.sqlite"); 1.230 + if (file.exists()) 1.231 + file.remove(false); 1.232 + } catch(ex) { dump("Exception: " + ex); } 1.233 +} 1.234 + 1.235 + 1.236 +/** 1.237 + * Dumps the rows of a table out to the console. 1.238 + * 1.239 + * @param aName 1.240 + * The name of the table or view to output. 1.241 + */ 1.242 +function dump_table(aName) 1.243 +{ 1.244 + let stmt = DBConn().createStatement("SELECT * FROM " + aName); 1.245 + 1.246 + print("\n*** Printing data from " + aName); 1.247 + let count = 0; 1.248 + while (stmt.executeStep()) { 1.249 + let columns = stmt.numEntries; 1.250 + 1.251 + if (count == 0) { 1.252 + // Print the column names. 1.253 + for (let i = 0; i < columns; i++) 1.254 + dump(stmt.getColumnName(i) + "\t"); 1.255 + dump("\n"); 1.256 + } 1.257 + 1.258 + // Print the rows. 1.259 + for (let i = 0; i < columns; i++) { 1.260 + switch (stmt.getTypeOfIndex(i)) { 1.261 + case Ci.mozIStorageValueArray.VALUE_TYPE_NULL: 1.262 + dump("NULL\t"); 1.263 + break; 1.264 + case Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER: 1.265 + dump(stmt.getInt64(i) + "\t"); 1.266 + break; 1.267 + case Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT: 1.268 + dump(stmt.getDouble(i) + "\t"); 1.269 + break; 1.270 + case Ci.mozIStorageValueArray.VALUE_TYPE_TEXT: 1.271 + dump(stmt.getString(i) + "\t"); 1.272 + break; 1.273 + } 1.274 + } 1.275 + dump("\n"); 1.276 + 1.277 + count++; 1.278 + } 1.279 + print("*** There were a total of " + count + " rows of data.\n"); 1.280 + 1.281 + stmt.finalize(); 1.282 +} 1.283 + 1.284 + 1.285 +/** 1.286 + * Checks if an address is found in the database. 1.287 + * @param aURI 1.288 + * nsIURI or address to look for. 1.289 + * @return place id of the page or 0 if not found 1.290 + */ 1.291 +function page_in_database(aURI) 1.292 +{ 1.293 + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; 1.294 + let stmt = DBConn().createStatement( 1.295 + "SELECT id FROM moz_places WHERE url = :url" 1.296 + ); 1.297 + stmt.params.url = url; 1.298 + try { 1.299 + if (!stmt.executeStep()) 1.300 + return 0; 1.301 + return stmt.getInt64(0); 1.302 + } 1.303 + finally { 1.304 + stmt.finalize(); 1.305 + } 1.306 +} 1.307 + 1.308 +/** 1.309 + * Checks how many visits exist for a specified page. 1.310 + * @param aURI 1.311 + * nsIURI or address to look for. 1.312 + * @return number of visits found. 1.313 + */ 1.314 +function visits_in_database(aURI) 1.315 +{ 1.316 + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; 1.317 + let stmt = DBConn().createStatement( 1.318 + "SELECT count(*) FROM moz_historyvisits v " 1.319 + + "JOIN moz_places h ON h.id = v.place_id " 1.320 + + "WHERE url = :url" 1.321 + ); 1.322 + stmt.params.url = url; 1.323 + try { 1.324 + if (!stmt.executeStep()) 1.325 + return 0; 1.326 + return stmt.getInt64(0); 1.327 + } 1.328 + finally { 1.329 + stmt.finalize(); 1.330 + } 1.331 +} 1.332 + 1.333 +/** 1.334 + * Removes all bookmarks and checks for correct cleanup 1.335 + */ 1.336 +function remove_all_bookmarks() { 1.337 + let PU = PlacesUtils; 1.338 + // Clear all bookmarks 1.339 + PU.bookmarks.removeFolderChildren(PU.bookmarks.bookmarksMenuFolder); 1.340 + PU.bookmarks.removeFolderChildren(PU.bookmarks.toolbarFolder); 1.341 + PU.bookmarks.removeFolderChildren(PU.bookmarks.unfiledBookmarksFolder); 1.342 + // Check for correct cleanup 1.343 + check_no_bookmarks(); 1.344 +} 1.345 + 1.346 + 1.347 +/** 1.348 + * Checks that we don't have any bookmark 1.349 + */ 1.350 +function check_no_bookmarks() { 1.351 + let query = PlacesUtils.history.getNewQuery(); 1.352 + let folders = [ 1.353 + PlacesUtils.bookmarks.toolbarFolder, 1.354 + PlacesUtils.bookmarks.bookmarksMenuFolder, 1.355 + PlacesUtils.bookmarks.unfiledBookmarksFolder, 1.356 + ]; 1.357 + query.setFolders(folders, 3); 1.358 + let options = PlacesUtils.history.getNewQueryOptions(); 1.359 + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS; 1.360 + let root = PlacesUtils.history.executeQuery(query, options).root; 1.361 + root.containerOpen = true; 1.362 + if (root.childCount != 0) 1.363 + do_throw("Unable to remove all bookmarks"); 1.364 + root.containerOpen = false; 1.365 +} 1.366 + 1.367 +/** 1.368 + * Allows waiting for an observer notification once. 1.369 + * 1.370 + * @param aTopic 1.371 + * Notification topic to observe. 1.372 + * 1.373 + * @return {Promise} 1.374 + * @resolves The array [aSubject, aData] from the observed notification. 1.375 + * @rejects Never. 1.376 + */ 1.377 +function promiseTopicObserved(aTopic) 1.378 +{ 1.379 + let deferred = Promise.defer(); 1.380 + 1.381 + Services.obs.addObserver( 1.382 + function PTO_observe(aSubject, aTopic, aData) { 1.383 + Services.obs.removeObserver(PTO_observe, aTopic); 1.384 + deferred.resolve([aSubject, aData]); 1.385 + }, aTopic, false); 1.386 + 1.387 + return deferred.promise; 1.388 +} 1.389 + 1.390 +/** 1.391 + * Clears history asynchronously. 1.392 + * 1.393 + * @return {Promise} 1.394 + * @resolves When history has been cleared. 1.395 + * @rejects Never. 1.396 + */ 1.397 +function promiseClearHistory() { 1.398 + let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED); 1.399 + do_execute_soon(function() PlacesUtils.bhistory.removeAllPages()); 1.400 + return promise; 1.401 +} 1.402 + 1.403 + 1.404 +/** 1.405 + * Simulates a Places shutdown. 1.406 + */ 1.407 +function shutdownPlaces(aKeepAliveConnection) 1.408 +{ 1.409 + let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver); 1.410 + hs.observe(null, "profile-change-teardown", null); 1.411 + hs.observe(null, "profile-before-change", null); 1.412 +} 1.413 + 1.414 +const FILENAME_BOOKMARKS_HTML = "bookmarks.html"; 1.415 +let (backup_date = new Date().toLocaleFormat("%Y-%m-%d")) { 1.416 + const FILENAME_BOOKMARKS_JSON = "bookmarks-" + backup_date + ".json"; 1.417 +} 1.418 + 1.419 +/** 1.420 + * Creates a bookmarks.html file in the profile folder from a given source file. 1.421 + * 1.422 + * @param aFilename 1.423 + * Name of the file to copy to the profile folder. This file must 1.424 + * exist in the directory that contains the test files. 1.425 + * 1.426 + * @return nsIFile object for the file. 1.427 + */ 1.428 +function create_bookmarks_html(aFilename) { 1.429 + if (!aFilename) 1.430 + do_throw("you must pass a filename to create_bookmarks_html function"); 1.431 + remove_bookmarks_html(); 1.432 + let bookmarksHTMLFile = gTestDir.clone(); 1.433 + bookmarksHTMLFile.append(aFilename); 1.434 + do_check_true(bookmarksHTMLFile.exists()); 1.435 + bookmarksHTMLFile.copyTo(gProfD, FILENAME_BOOKMARKS_HTML); 1.436 + let profileBookmarksHTMLFile = gProfD.clone(); 1.437 + profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); 1.438 + do_check_true(profileBookmarksHTMLFile.exists()); 1.439 + return profileBookmarksHTMLFile; 1.440 +} 1.441 + 1.442 + 1.443 +/** 1.444 + * Remove bookmarks.html file from the profile folder. 1.445 + */ 1.446 +function remove_bookmarks_html() { 1.447 + let profileBookmarksHTMLFile = gProfD.clone(); 1.448 + profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); 1.449 + if (profileBookmarksHTMLFile.exists()) { 1.450 + profileBookmarksHTMLFile.remove(false); 1.451 + do_check_false(profileBookmarksHTMLFile.exists()); 1.452 + } 1.453 +} 1.454 + 1.455 + 1.456 +/** 1.457 + * Check bookmarks.html file exists in the profile folder. 1.458 + * 1.459 + * @return nsIFile object for the file. 1.460 + */ 1.461 +function check_bookmarks_html() { 1.462 + let profileBookmarksHTMLFile = gProfD.clone(); 1.463 + profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML); 1.464 + do_check_true(profileBookmarksHTMLFile.exists()); 1.465 + return profileBookmarksHTMLFile; 1.466 +} 1.467 + 1.468 + 1.469 +/** 1.470 + * Creates a JSON backup in the profile folder folder from a given source file. 1.471 + * 1.472 + * @param aFilename 1.473 + * Name of the file to copy to the profile folder. This file must 1.474 + * exist in the directory that contains the test files. 1.475 + * 1.476 + * @return nsIFile object for the file. 1.477 + */ 1.478 +function create_JSON_backup(aFilename) { 1.479 + if (!aFilename) 1.480 + do_throw("you must pass a filename to create_JSON_backup function"); 1.481 + let bookmarksBackupDir = gProfD.clone(); 1.482 + bookmarksBackupDir.append("bookmarkbackups"); 1.483 + if (!bookmarksBackupDir.exists()) { 1.484 + bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); 1.485 + do_check_true(bookmarksBackupDir.exists()); 1.486 + } 1.487 + let profileBookmarksJSONFile = bookmarksBackupDir.clone(); 1.488 + profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); 1.489 + if (profileBookmarksJSONFile.exists()) { 1.490 + profileBookmarksJSONFile.remove(); 1.491 + } 1.492 + let bookmarksJSONFile = gTestDir.clone(); 1.493 + bookmarksJSONFile.append(aFilename); 1.494 + do_check_true(bookmarksJSONFile.exists()); 1.495 + bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON); 1.496 + profileBookmarksJSONFile = bookmarksBackupDir.clone(); 1.497 + profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); 1.498 + do_check_true(profileBookmarksJSONFile.exists()); 1.499 + return profileBookmarksJSONFile; 1.500 +} 1.501 + 1.502 + 1.503 +/** 1.504 + * Remove bookmarksbackup dir and all backups from the profile folder. 1.505 + */ 1.506 +function remove_all_JSON_backups() { 1.507 + let bookmarksBackupDir = gProfD.clone(); 1.508 + bookmarksBackupDir.append("bookmarkbackups"); 1.509 + if (bookmarksBackupDir.exists()) { 1.510 + bookmarksBackupDir.remove(true); 1.511 + do_check_false(bookmarksBackupDir.exists()); 1.512 + } 1.513 +} 1.514 + 1.515 + 1.516 +/** 1.517 + * Check a JSON backup file for today exists in the profile folder. 1.518 + * 1.519 + * @param aIsAutomaticBackup The boolean indicates whether it's an automatic 1.520 + * backup. 1.521 + * @return nsIFile object for the file. 1.522 + */ 1.523 +function check_JSON_backup(aIsAutomaticBackup) { 1.524 + let profileBookmarksJSONFile; 1.525 + if (aIsAutomaticBackup) { 1.526 + let bookmarksBackupDir = gProfD.clone(); 1.527 + bookmarksBackupDir.append("bookmarkbackups"); 1.528 + let files = bookmarksBackupDir.directoryEntries; 1.529 + let backup_date = new Date().toLocaleFormat("%Y-%m-%d"); 1.530 + while (files.hasMoreElements()) { 1.531 + let entry = files.getNext().QueryInterface(Ci.nsIFile); 1.532 + if (PlacesBackups.filenamesRegex.test(entry.leafName)) { 1.533 + profileBookmarksJSONFile = entry; 1.534 + break; 1.535 + } 1.536 + } 1.537 + } else { 1.538 + profileBookmarksJSONFile = gProfD.clone(); 1.539 + profileBookmarksJSONFile.append("bookmarkbackups"); 1.540 + profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON); 1.541 + } 1.542 + do_check_true(profileBookmarksJSONFile.exists()); 1.543 + return profileBookmarksJSONFile; 1.544 +} 1.545 + 1.546 +/** 1.547 + * Returns the frecency of a url. 1.548 + * 1.549 + * @param aURI 1.550 + * The URI or spec to get frecency for. 1.551 + * @return the frecency value. 1.552 + */ 1.553 +function frecencyForUrl(aURI) 1.554 +{ 1.555 + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; 1.556 + let stmt = DBConn().createStatement( 1.557 + "SELECT frecency FROM moz_places WHERE url = ?1" 1.558 + ); 1.559 + stmt.bindByIndex(0, url); 1.560 + try { 1.561 + if (!stmt.executeStep()) { 1.562 + throw new Error("No result for frecency."); 1.563 + } 1.564 + return stmt.getInt32(0); 1.565 + } finally { 1.566 + stmt.finalize(); 1.567 + } 1.568 +} 1.569 + 1.570 +/** 1.571 + * Returns the hidden status of a url. 1.572 + * 1.573 + * @param aURI 1.574 + * The URI or spec to get hidden for. 1.575 + * @return @return true if the url is hidden, false otherwise. 1.576 + */ 1.577 +function isUrlHidden(aURI) 1.578 +{ 1.579 + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; 1.580 + let stmt = DBConn().createStatement( 1.581 + "SELECT hidden FROM moz_places WHERE url = ?1" 1.582 + ); 1.583 + stmt.bindByIndex(0, url); 1.584 + if (!stmt.executeStep()) 1.585 + throw new Error("No result for hidden."); 1.586 + let hidden = stmt.getInt32(0); 1.587 + stmt.finalize(); 1.588 + 1.589 + return !!hidden; 1.590 +} 1.591 + 1.592 +/** 1.593 + * Compares two times in usecs, considering eventual platform timers skews. 1.594 + * 1.595 + * @param aTimeBefore 1.596 + * The older time in usecs. 1.597 + * @param aTimeAfter 1.598 + * The newer time in usecs. 1.599 + * @return true if times are ordered, false otherwise. 1.600 + */ 1.601 +function is_time_ordered(before, after) { 1.602 + // Windows has an estimated 16ms timers precision, since Date.now() and 1.603 + // PR_Now() use different code atm, the results can be unordered by this 1.604 + // amount of time. See bug 558745 and bug 557406. 1.605 + let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc); 1.606 + // Just to be safe we consider 20ms. 1.607 + let skew = isWindows ? 20000000 : 0; 1.608 + return after - before > -skew; 1.609 +} 1.610 + 1.611 +/** 1.612 + * Waits for all pending async statements on the default connection. 1.613 + * 1.614 + * @return {Promise} 1.615 + * @resolves When all pending async statements finished. 1.616 + * @rejects Never. 1.617 + * 1.618 + * @note The result is achieved by asynchronously executing a query requiring 1.619 + * a write lock. Since all statements on the same connection are 1.620 + * serialized, the end of this write operation means that all writes are 1.621 + * complete. Note that WAL makes so that writers don't block readers, but 1.622 + * this is a problem only across different connections. 1.623 + */ 1.624 +function promiseAsyncUpdates() 1.625 +{ 1.626 + let deferred = Promise.defer(); 1.627 + 1.628 + let db = DBConn(); 1.629 + let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); 1.630 + begin.executeAsync(); 1.631 + begin.finalize(); 1.632 + 1.633 + let commit = db.createAsyncStatement("COMMIT"); 1.634 + commit.executeAsync({ 1.635 + handleResult: function () {}, 1.636 + handleError: function () {}, 1.637 + handleCompletion: function(aReason) 1.638 + { 1.639 + deferred.resolve(); 1.640 + } 1.641 + }); 1.642 + commit.finalize(); 1.643 + 1.644 + return deferred.promise; 1.645 +} 1.646 + 1.647 +/** 1.648 + * Shutdowns Places, invoking the callback when the connection has been closed. 1.649 + * 1.650 + * @param aCallback 1.651 + * Function to be called when done. 1.652 + */ 1.653 +function waitForConnectionClosed(aCallback) 1.654 +{ 1.655 + Services.obs.addObserver(function WFCCCallback() { 1.656 + Services.obs.removeObserver(WFCCCallback, "places-connection-closed"); 1.657 + aCallback(); 1.658 + }, "places-connection-closed", false); 1.659 + shutdownPlaces(); 1.660 +} 1.661 + 1.662 +/** 1.663 + * Tests if a given guid is valid for use in Places or not. 1.664 + * 1.665 + * @param aGuid 1.666 + * The guid to test. 1.667 + * @param [optional] aStack 1.668 + * The stack frame used to report the error. 1.669 + */ 1.670 +function do_check_valid_places_guid(aGuid, 1.671 + aStack) 1.672 +{ 1.673 + if (!aStack) { 1.674 + aStack = Components.stack.caller; 1.675 + } 1.676 + do_check_true(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), aStack); 1.677 +} 1.678 + 1.679 +/** 1.680 + * Retrieves the guid for a given uri. 1.681 + * 1.682 + * @param aURI 1.683 + * The uri to check. 1.684 + * @param [optional] aStack 1.685 + * The stack frame used to report the error. 1.686 + * @return the associated the guid. 1.687 + */ 1.688 +function do_get_guid_for_uri(aURI, 1.689 + aStack) 1.690 +{ 1.691 + if (!aStack) { 1.692 + aStack = Components.stack.caller; 1.693 + } 1.694 + let stmt = DBConn().createStatement( 1.695 + "SELECT guid " 1.696 + + "FROM moz_places " 1.697 + + "WHERE url = :url " 1.698 + ); 1.699 + stmt.params.url = aURI.spec; 1.700 + do_check_true(stmt.executeStep(), aStack); 1.701 + let guid = stmt.row.guid; 1.702 + stmt.finalize(); 1.703 + do_check_valid_places_guid(guid, aStack); 1.704 + return guid; 1.705 +} 1.706 + 1.707 +/** 1.708 + * Tests that a guid was set in moz_places for a given uri. 1.709 + * 1.710 + * @param aURI 1.711 + * The uri to check. 1.712 + * @param [optional] aGUID 1.713 + * The expected guid in the database. 1.714 + */ 1.715 +function do_check_guid_for_uri(aURI, 1.716 + aGUID) 1.717 +{ 1.718 + let caller = Components.stack.caller; 1.719 + let guid = do_get_guid_for_uri(aURI, caller); 1.720 + if (aGUID) { 1.721 + do_check_valid_places_guid(aGUID, caller); 1.722 + do_check_eq(guid, aGUID, caller); 1.723 + } 1.724 +} 1.725 + 1.726 +/** 1.727 + * Retrieves the guid for a given bookmark. 1.728 + * 1.729 + * @param aId 1.730 + * The bookmark id to check. 1.731 + * @param [optional] aStack 1.732 + * The stack frame used to report the error. 1.733 + * @return the associated the guid. 1.734 + */ 1.735 +function do_get_guid_for_bookmark(aId, 1.736 + aStack) 1.737 +{ 1.738 + if (!aStack) { 1.739 + aStack = Components.stack.caller; 1.740 + } 1.741 + let stmt = DBConn().createStatement( 1.742 + "SELECT guid " 1.743 + + "FROM moz_bookmarks " 1.744 + + "WHERE id = :item_id " 1.745 + ); 1.746 + stmt.params.item_id = aId; 1.747 + do_check_true(stmt.executeStep(), aStack); 1.748 + let guid = stmt.row.guid; 1.749 + stmt.finalize(); 1.750 + do_check_valid_places_guid(guid, aStack); 1.751 + return guid; 1.752 +} 1.753 + 1.754 +/** 1.755 + * Tests that a guid was set in moz_places for a given bookmark. 1.756 + * 1.757 + * @param aId 1.758 + * The bookmark id to check. 1.759 + * @param [optional] aGUID 1.760 + * The expected guid in the database. 1.761 + */ 1.762 +function do_check_guid_for_bookmark(aId, 1.763 + aGUID) 1.764 +{ 1.765 + let caller = Components.stack.caller; 1.766 + let guid = do_get_guid_for_bookmark(aId, caller); 1.767 + if (aGUID) { 1.768 + do_check_valid_places_guid(aGUID, caller); 1.769 + do_check_eq(guid, aGUID, caller); 1.770 + } 1.771 +} 1.772 + 1.773 +/** 1.774 + * Logs info to the console in the standard way (includes the filename). 1.775 + * 1.776 + * @param aMessage 1.777 + * The message to log to the console. 1.778 + */ 1.779 +function do_log_info(aMessage) 1.780 +{ 1.781 + print("TEST-INFO | " + _TEST_FILE + " | " + aMessage); 1.782 +} 1.783 + 1.784 +/** 1.785 + * Compares 2 arrays returning whether they contains the same elements. 1.786 + * 1.787 + * @param a1 1.788 + * First array to compare. 1.789 + * @param a2 1.790 + * Second array to compare. 1.791 + * @param [optional] sorted 1.792 + * Whether the comparison should take in count position of the elements. 1.793 + * @return true if the arrays contain the same elements, false otherwise. 1.794 + */ 1.795 +function do_compare_arrays(a1, a2, sorted) 1.796 +{ 1.797 + if (a1.length != a2.length) 1.798 + return false; 1.799 + 1.800 + if (sorted) { 1.801 + return a1.every(function (e, i) e == a2[i]); 1.802 + } 1.803 + else { 1.804 + return a1.filter(function (e) a2.indexOf(e) == -1).length == 0 && 1.805 + a2.filter(function (e) a1.indexOf(e) == -1).length == 0; 1.806 + } 1.807 +} 1.808 + 1.809 +/** 1.810 + * Generic nsINavBookmarkObserver that doesn't implement anything, but provides 1.811 + * dummy methods to prevent errors about an object not having a certain method. 1.812 + */ 1.813 +function NavBookmarkObserver() {} 1.814 + 1.815 +NavBookmarkObserver.prototype = { 1.816 + onBeginUpdateBatch: function () {}, 1.817 + onEndUpdateBatch: function () {}, 1.818 + onItemAdded: function () {}, 1.819 + onItemRemoved: function () {}, 1.820 + onItemChanged: function () {}, 1.821 + onItemVisited: function () {}, 1.822 + onItemMoved: function () {}, 1.823 + QueryInterface: XPCOMUtils.generateQI([ 1.824 + Ci.nsINavBookmarkObserver, 1.825 + ]) 1.826 +}; 1.827 + 1.828 +/** 1.829 + * Generic nsINavHistoryObserver that doesn't implement anything, but provides 1.830 + * dummy methods to prevent errors about an object not having a certain method. 1.831 + */ 1.832 +function NavHistoryObserver() {} 1.833 + 1.834 +NavHistoryObserver.prototype = { 1.835 + onBeginUpdateBatch: function () {}, 1.836 + onEndUpdateBatch: function () {}, 1.837 + onVisit: function () {}, 1.838 + onTitleChanged: function () {}, 1.839 + onDeleteURI: function () {}, 1.840 + onClearHistory: function () {}, 1.841 + onPageChanged: function () {}, 1.842 + onDeleteVisits: function () {}, 1.843 + QueryInterface: XPCOMUtils.generateQI([ 1.844 + Ci.nsINavHistoryObserver, 1.845 + ]) 1.846 +}; 1.847 + 1.848 +/** 1.849 + * Generic nsINavHistoryResultObserver that doesn't implement anything, but 1.850 + * provides dummy methods to prevent errors about an object not having a certain 1.851 + * method. 1.852 + */ 1.853 +function NavHistoryResultObserver() {} 1.854 + 1.855 +NavHistoryResultObserver.prototype = { 1.856 + batching: function () {}, 1.857 + containerStateChanged: function () {}, 1.858 + invalidateContainer: function () {}, 1.859 + nodeAnnotationChanged: function () {}, 1.860 + nodeDateAddedChanged: function () {}, 1.861 + nodeHistoryDetailsChanged: function () {}, 1.862 + nodeIconChanged: function () {}, 1.863 + nodeInserted: function () {}, 1.864 + nodeKeywordChanged: function () {}, 1.865 + nodeLastModifiedChanged: function () {}, 1.866 + nodeMoved: function () {}, 1.867 + nodeRemoved: function () {}, 1.868 + nodeTagsChanged: function () {}, 1.869 + nodeTitleChanged: function () {}, 1.870 + nodeURIChanged: function () {}, 1.871 + sortingChanged: function () {}, 1.872 + QueryInterface: XPCOMUtils.generateQI([ 1.873 + Ci.nsINavHistoryResultObserver, 1.874 + ]) 1.875 +}; 1.876 + 1.877 +/** 1.878 + * Asynchronously adds visits to a page. 1.879 + * 1.880 + * @param aPlaceInfo 1.881 + * Can be an nsIURI, in such a case a single LINK visit will be added. 1.882 + * Otherwise can be an object describing the visit to add, or an array 1.883 + * of these objects: 1.884 + * { uri: nsIURI of the page, 1.885 + * transition: one of the TRANSITION_* from nsINavHistoryService, 1.886 + * [optional] title: title of the page, 1.887 + * [optional] visitDate: visit date in microseconds from the epoch 1.888 + * [optional] referrer: nsIURI of the referrer for this visit 1.889 + * } 1.890 + * 1.891 + * @return {Promise} 1.892 + * @resolves When all visits have been added successfully. 1.893 + * @rejects JavaScript exception. 1.894 + */ 1.895 +function promiseAddVisits(aPlaceInfo) 1.896 +{ 1.897 + let deferred = Promise.defer(); 1.898 + let places = []; 1.899 + if (aPlaceInfo instanceof Ci.nsIURI) { 1.900 + places.push({ uri: aPlaceInfo }); 1.901 + } 1.902 + else if (Array.isArray(aPlaceInfo)) { 1.903 + places = places.concat(aPlaceInfo); 1.904 + } else { 1.905 + places.push(aPlaceInfo) 1.906 + } 1.907 + 1.908 + // Create mozIVisitInfo for each entry. 1.909 + let now = Date.now(); 1.910 + for (let i = 0; i < places.length; i++) { 1.911 + if (!places[i].title) { 1.912 + places[i].title = "test visit for " + places[i].uri.spec; 1.913 + } 1.914 + places[i].visits = [{ 1.915 + transitionType: places[i].transition === undefined ? TRANSITION_LINK 1.916 + : places[i].transition, 1.917 + visitDate: places[i].visitDate || (now++) * 1000, 1.918 + referrerURI: places[i].referrer 1.919 + }]; 1.920 + } 1.921 + 1.922 + PlacesUtils.asyncHistory.updatePlaces( 1.923 + places, 1.924 + { 1.925 + handleError: function AAV_handleError(aResultCode, aPlaceInfo) { 1.926 + let ex = new Components.Exception("Unexpected error in adding visits.", 1.927 + aResultCode); 1.928 + deferred.reject(ex); 1.929 + }, 1.930 + handleResult: function () {}, 1.931 + handleCompletion: function UP_handleCompletion() { 1.932 + deferred.resolve(); 1.933 + } 1.934 + } 1.935 + ); 1.936 + 1.937 + return deferred.promise; 1.938 +} 1.939 + 1.940 +/** 1.941 + * Asynchronously check a url is visited. 1.942 + * 1.943 + * @param aURI The URI. 1.944 + * @return {Promise} 1.945 + * @resolves When the check has been added successfully. 1.946 + * @rejects JavaScript exception. 1.947 + */ 1.948 +function promiseIsURIVisited(aURI) { 1.949 + let deferred = Promise.defer(); 1.950 + 1.951 + PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { 1.952 + deferred.resolve(aIsVisited); 1.953 + }); 1.954 + 1.955 + return deferred.promise; 1.956 +} 1.957 +