mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 12:37:37 +00:00
7e20285e70
The -*- file variable lines -*- establish per-file settings that Emacs will pick up. This patch makes the following changes to those lines (and touches nothing else): - Never set the buffer's mode. Years ago, Emacs did not have a good JavaScript mode, so it made sense to use Java or C++ mode in .js files. However, Emacs has had js-mode for years now; it's perfectly serviceable, and is available and enabled by default in all major Emacs packagings. Selecting a mode in the -*- file variable line -*- is almost always the wrong thing to do anyway. It overrides Emacs's default choice, which is (now) reasonable; and even worse, it overrides settings the user might have made in their '.emacs' file for that file extension. It's only useful when there's something specific about that particular file that makes a particular mode appropriate. - Correctly propagate settings that establish the correct indentation level for this file: c-basic-offset and js2-basic-offset should be js-indent-level. Whatever value they're given should be preserved; different parts of our tree use different indentation styles. - We don't use tabs in Mozilla JS code. Always set indent-tabs-mode: nil. Remove tab-width: settings, at least in files that don't contain tab characters. - Remove js2-mode settings that belong in the user's .emacs file, like js2-skip-preprocessor-directives.
970 lines
28 KiB
JavaScript
970 lines
28 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const CURRENT_SCHEMA_VERSION = 23;
|
|
|
|
const NS_APP_USER_PROFILE_50_DIR = "ProfD";
|
|
const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
|
|
|
|
// Shortcuts to transitions type.
|
|
const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
|
|
const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
|
|
const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK;
|
|
const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED;
|
|
const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK;
|
|
const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT;
|
|
const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY;
|
|
const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD;
|
|
|
|
const TITLE_LENGTH_MAX = 4096;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
|
"resource://gre/modules/FileUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
|
"resource://gre/modules/NetUtil.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
|
"resource://gre/modules/Promise.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
|
"resource://gre/modules/Services.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
|
"resource://gre/modules/Task.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
|
|
"resource://gre/modules/BookmarkJSONUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
|
|
"resource://gre/modules/BookmarkHTMLUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
|
|
"resource://gre/modules/PlacesBackups.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
|
|
"resource://gre/modules/PlacesTransactions.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
|
"resource://gre/modules/osfile.jsm");
|
|
|
|
// This imports various other objects in addition to PlacesUtils.
|
|
Cu.import("resource://gre/modules/PlacesUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() {
|
|
return NetUtil.newURI(
|
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
|
|
"AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==");
|
|
});
|
|
|
|
function LOG(aMsg) {
|
|
aMsg = ("*** PLACES TESTS: " + aMsg);
|
|
Services.console.logStringMessage(aMsg);
|
|
print(aMsg);
|
|
}
|
|
|
|
let gTestDir = do_get_cwd();
|
|
|
|
// Initialize profile.
|
|
let gProfD = do_get_profile();
|
|
|
|
// Remove any old database.
|
|
clearDB();
|
|
|
|
/**
|
|
* Shortcut to create a nsIURI.
|
|
*
|
|
* @param aSpec
|
|
* URLString of the uri.
|
|
*/
|
|
function uri(aSpec) NetUtil.newURI(aSpec);
|
|
|
|
|
|
/**
|
|
* Gets the database connection. If the Places connection is invalid it will
|
|
* try to create a new connection.
|
|
*
|
|
* @param [optional] aForceNewConnection
|
|
* Forces creation of a new connection to the database. When a
|
|
* connection is asyncClosed it cannot anymore schedule async statements,
|
|
* though connectionReady will keep returning true (Bug 726990).
|
|
*
|
|
* @return The database connection or null if unable to get one.
|
|
*/
|
|
let gDBConn;
|
|
function DBConn(aForceNewConnection) {
|
|
if (!aForceNewConnection) {
|
|
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
|
|
.DBConnection;
|
|
if (db.connectionReady)
|
|
return db;
|
|
}
|
|
|
|
// If the Places database connection has been closed, create a new connection.
|
|
if (!gDBConn || aForceNewConnection) {
|
|
let file = Services.dirsvc.get('ProfD', Ci.nsIFile);
|
|
file.append("places.sqlite");
|
|
let dbConn = gDBConn = Services.storage.openDatabase(file);
|
|
|
|
// Be sure to cleanly close this connection.
|
|
Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) {
|
|
Services.obs.removeObserver(DBCloseCallback, aTopic);
|
|
dbConn.asyncClose();
|
|
}, "profile-before-change", false);
|
|
}
|
|
|
|
return gDBConn.connectionReady ? gDBConn : null;
|
|
};
|
|
|
|
/**
|
|
* Reads data from the provided inputstream.
|
|
*
|
|
* @return an array of bytes.
|
|
*/
|
|
function readInputStreamData(aStream) {
|
|
let bistream = Cc["@mozilla.org/binaryinputstream;1"].
|
|
createInstance(Ci.nsIBinaryInputStream);
|
|
try {
|
|
bistream.setInputStream(aStream);
|
|
let expectedData = [];
|
|
let avail;
|
|
while ((avail = bistream.available())) {
|
|
expectedData = expectedData.concat(bistream.readByteArray(avail));
|
|
}
|
|
return expectedData;
|
|
} finally {
|
|
bistream.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads the data from the specified nsIFile.
|
|
*
|
|
* @param aFile
|
|
* The nsIFile to read from.
|
|
* @return an array of bytes.
|
|
*/
|
|
function readFileData(aFile) {
|
|
let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
|
|
createInstance(Ci.nsIFileInputStream);
|
|
// init the stream as RD_ONLY, -1 == default permissions.
|
|
inputStream.init(aFile, 0x01, -1, null);
|
|
|
|
// Check the returned size versus the expected size.
|
|
let size = inputStream.available();
|
|
let bytes = readInputStreamData(inputStream);
|
|
if (size != bytes.length) {
|
|
throw "Didn't read expected number of bytes";
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* Reads the data from the named file, verifying the expected file length.
|
|
*
|
|
* @param aFileName
|
|
* This file should be located in the same folder as the test.
|
|
* @param aExpectedLength
|
|
* Expected length of the file.
|
|
*
|
|
* @return The array of bytes read from the file.
|
|
*/
|
|
function readFileOfLength(aFileName, aExpectedLength) {
|
|
let data = readFileData(do_get_file(aFileName));
|
|
do_check_eq(data.length, aExpectedLength);
|
|
return data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the base64-encoded version of the given string. This function is
|
|
* similar to window.btoa, but is available to xpcshell tests also.
|
|
*
|
|
* @param aString
|
|
* Each character in this string corresponds to a byte, and must be a
|
|
* code point in the range 0-255.
|
|
*
|
|
* @return The base64-encoded string.
|
|
*/
|
|
function base64EncodeString(aString) {
|
|
var stream = Cc["@mozilla.org/io/string-input-stream;1"]
|
|
.createInstance(Ci.nsIStringInputStream);
|
|
stream.setData(aString, aString.length);
|
|
var encoder = Cc["@mozilla.org/scriptablebase64encoder;1"]
|
|
.createInstance(Ci.nsIScriptableBase64Encoder);
|
|
return encoder.encodeToString(stream, aString.length);
|
|
}
|
|
|
|
|
|
/**
|
|
* Compares two arrays, and returns true if they are equal.
|
|
*
|
|
* @param aArray1
|
|
* First array to compare.
|
|
* @param aArray2
|
|
* Second array to compare.
|
|
*/
|
|
function compareArrays(aArray1, aArray2) {
|
|
if (aArray1.length != aArray2.length) {
|
|
print("compareArrays: array lengths differ\n");
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < aArray1.length; i++) {
|
|
if (aArray1[i] != aArray2[i]) {
|
|
print("compareArrays: arrays differ at index " + i + ": " +
|
|
"(" + aArray1[i] + ") != (" + aArray2[i] +")\n");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes a previously created sqlite file from the profile folder.
|
|
*/
|
|
function clearDB() {
|
|
try {
|
|
let file = Services.dirsvc.get('ProfD', Ci.nsIFile);
|
|
file.append("places.sqlite");
|
|
if (file.exists())
|
|
file.remove(false);
|
|
} catch(ex) { dump("Exception: " + ex); }
|
|
}
|
|
|
|
|
|
/**
|
|
* Dumps the rows of a table out to the console.
|
|
*
|
|
* @param aName
|
|
* The name of the table or view to output.
|
|
*/
|
|
function dump_table(aName)
|
|
{
|
|
let stmt = DBConn().createStatement("SELECT * FROM " + aName);
|
|
|
|
print("\n*** Printing data from " + aName);
|
|
let count = 0;
|
|
while (stmt.executeStep()) {
|
|
let columns = stmt.numEntries;
|
|
|
|
if (count == 0) {
|
|
// Print the column names.
|
|
for (let i = 0; i < columns; i++)
|
|
dump(stmt.getColumnName(i) + "\t");
|
|
dump("\n");
|
|
}
|
|
|
|
// Print the rows.
|
|
for (let i = 0; i < columns; i++) {
|
|
switch (stmt.getTypeOfIndex(i)) {
|
|
case Ci.mozIStorageValueArray.VALUE_TYPE_NULL:
|
|
dump("NULL\t");
|
|
break;
|
|
case Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER:
|
|
dump(stmt.getInt64(i) + "\t");
|
|
break;
|
|
case Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT:
|
|
dump(stmt.getDouble(i) + "\t");
|
|
break;
|
|
case Ci.mozIStorageValueArray.VALUE_TYPE_TEXT:
|
|
dump(stmt.getString(i) + "\t");
|
|
break;
|
|
}
|
|
}
|
|
dump("\n");
|
|
|
|
count++;
|
|
}
|
|
print("*** There were a total of " + count + " rows of data.\n");
|
|
|
|
stmt.finalize();
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks if an address is found in the database.
|
|
* @param aURI
|
|
* nsIURI or address to look for.
|
|
* @return place id of the page or 0 if not found
|
|
*/
|
|
function page_in_database(aURI)
|
|
{
|
|
let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
|
|
let stmt = DBConn().createStatement(
|
|
"SELECT id FROM moz_places WHERE url = :url"
|
|
);
|
|
stmt.params.url = url;
|
|
try {
|
|
if (!stmt.executeStep())
|
|
return 0;
|
|
return stmt.getInt64(0);
|
|
}
|
|
finally {
|
|
stmt.finalize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks how many visits exist for a specified page.
|
|
* @param aURI
|
|
* nsIURI or address to look for.
|
|
* @return number of visits found.
|
|
*/
|
|
function visits_in_database(aURI)
|
|
{
|
|
let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
|
|
let stmt = DBConn().createStatement(
|
|
"SELECT count(*) FROM moz_historyvisits v "
|
|
+ "JOIN moz_places h ON h.id = v.place_id "
|
|
+ "WHERE url = :url"
|
|
);
|
|
stmt.params.url = url;
|
|
try {
|
|
if (!stmt.executeStep())
|
|
return 0;
|
|
return stmt.getInt64(0);
|
|
}
|
|
finally {
|
|
stmt.finalize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes all bookmarks and checks for correct cleanup
|
|
*/
|
|
function remove_all_bookmarks() {
|
|
let PU = PlacesUtils;
|
|
// Clear all bookmarks
|
|
PU.bookmarks.removeFolderChildren(PU.bookmarks.bookmarksMenuFolder);
|
|
PU.bookmarks.removeFolderChildren(PU.bookmarks.toolbarFolder);
|
|
PU.bookmarks.removeFolderChildren(PU.bookmarks.unfiledBookmarksFolder);
|
|
// Check for correct cleanup
|
|
check_no_bookmarks();
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks that we don't have any bookmark
|
|
*/
|
|
function check_no_bookmarks() {
|
|
let query = PlacesUtils.history.getNewQuery();
|
|
let folders = [
|
|
PlacesUtils.bookmarks.toolbarFolder,
|
|
PlacesUtils.bookmarks.bookmarksMenuFolder,
|
|
PlacesUtils.bookmarks.unfiledBookmarksFolder,
|
|
];
|
|
query.setFolders(folders, 3);
|
|
let options = PlacesUtils.history.getNewQueryOptions();
|
|
options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
|
|
let root = PlacesUtils.history.executeQuery(query, options).root;
|
|
root.containerOpen = true;
|
|
if (root.childCount != 0)
|
|
do_throw("Unable to remove all bookmarks");
|
|
root.containerOpen = false;
|
|
}
|
|
|
|
/**
|
|
* Allows waiting for an observer notification once.
|
|
*
|
|
* @param aTopic
|
|
* Notification topic to observe.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves The array [aSubject, aData] from the observed notification.
|
|
* @rejects Never.
|
|
*/
|
|
function promiseTopicObserved(aTopic)
|
|
{
|
|
let deferred = Promise.defer();
|
|
|
|
Services.obs.addObserver(
|
|
function PTO_observe(aSubject, aTopic, aData) {
|
|
Services.obs.removeObserver(PTO_observe, aTopic);
|
|
deferred.resolve([aSubject, aData]);
|
|
}, aTopic, false);
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
/**
|
|
* Clears history asynchronously.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When history has been cleared.
|
|
* @rejects Never.
|
|
*/
|
|
function promiseClearHistory() {
|
|
let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
|
|
do_execute_soon(function() PlacesUtils.bhistory.removeAllPages());
|
|
return promise;
|
|
}
|
|
|
|
|
|
/**
|
|
* Simulates a Places shutdown.
|
|
*/
|
|
function shutdownPlaces(aKeepAliveConnection)
|
|
{
|
|
let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver);
|
|
hs.observe(null, "profile-change-teardown", null);
|
|
hs.observe(null, "profile-before-change", null);
|
|
}
|
|
|
|
const FILENAME_BOOKMARKS_HTML = "bookmarks.html";
|
|
let (backup_date = new Date().toLocaleFormat("%Y-%m-%d")) {
|
|
const FILENAME_BOOKMARKS_JSON = "bookmarks-" + backup_date + ".json";
|
|
}
|
|
|
|
/**
|
|
* Creates a bookmarks.html file in the profile folder from a given source file.
|
|
*
|
|
* @param aFilename
|
|
* Name of the file to copy to the profile folder. This file must
|
|
* exist in the directory that contains the test files.
|
|
*
|
|
* @return nsIFile object for the file.
|
|
*/
|
|
function create_bookmarks_html(aFilename) {
|
|
if (!aFilename)
|
|
do_throw("you must pass a filename to create_bookmarks_html function");
|
|
remove_bookmarks_html();
|
|
let bookmarksHTMLFile = gTestDir.clone();
|
|
bookmarksHTMLFile.append(aFilename);
|
|
do_check_true(bookmarksHTMLFile.exists());
|
|
bookmarksHTMLFile.copyTo(gProfD, FILENAME_BOOKMARKS_HTML);
|
|
let profileBookmarksHTMLFile = gProfD.clone();
|
|
profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
|
|
do_check_true(profileBookmarksHTMLFile.exists());
|
|
return profileBookmarksHTMLFile;
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove bookmarks.html file from the profile folder.
|
|
*/
|
|
function remove_bookmarks_html() {
|
|
let profileBookmarksHTMLFile = gProfD.clone();
|
|
profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
|
|
if (profileBookmarksHTMLFile.exists()) {
|
|
profileBookmarksHTMLFile.remove(false);
|
|
do_check_false(profileBookmarksHTMLFile.exists());
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Check bookmarks.html file exists in the profile folder.
|
|
*
|
|
* @return nsIFile object for the file.
|
|
*/
|
|
function check_bookmarks_html() {
|
|
let profileBookmarksHTMLFile = gProfD.clone();
|
|
profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
|
|
do_check_true(profileBookmarksHTMLFile.exists());
|
|
return profileBookmarksHTMLFile;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a JSON backup in the profile folder folder from a given source file.
|
|
*
|
|
* @param aFilename
|
|
* Name of the file to copy to the profile folder. This file must
|
|
* exist in the directory that contains the test files.
|
|
*
|
|
* @return nsIFile object for the file.
|
|
*/
|
|
function create_JSON_backup(aFilename) {
|
|
if (!aFilename)
|
|
do_throw("you must pass a filename to create_JSON_backup function");
|
|
let bookmarksBackupDir = gProfD.clone();
|
|
bookmarksBackupDir.append("bookmarkbackups");
|
|
if (!bookmarksBackupDir.exists()) {
|
|
bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
|
|
do_check_true(bookmarksBackupDir.exists());
|
|
}
|
|
let profileBookmarksJSONFile = bookmarksBackupDir.clone();
|
|
profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
|
|
if (profileBookmarksJSONFile.exists()) {
|
|
profileBookmarksJSONFile.remove();
|
|
}
|
|
let bookmarksJSONFile = gTestDir.clone();
|
|
bookmarksJSONFile.append(aFilename);
|
|
do_check_true(bookmarksJSONFile.exists());
|
|
bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON);
|
|
profileBookmarksJSONFile = bookmarksBackupDir.clone();
|
|
profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
|
|
do_check_true(profileBookmarksJSONFile.exists());
|
|
return profileBookmarksJSONFile;
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove bookmarksbackup dir and all backups from the profile folder.
|
|
*/
|
|
function remove_all_JSON_backups() {
|
|
let bookmarksBackupDir = gProfD.clone();
|
|
bookmarksBackupDir.append("bookmarkbackups");
|
|
if (bookmarksBackupDir.exists()) {
|
|
bookmarksBackupDir.remove(true);
|
|
do_check_false(bookmarksBackupDir.exists());
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Check a JSON backup file for today exists in the profile folder.
|
|
*
|
|
* @param aIsAutomaticBackup The boolean indicates whether it's an automatic
|
|
* backup.
|
|
* @return nsIFile object for the file.
|
|
*/
|
|
function check_JSON_backup(aIsAutomaticBackup) {
|
|
let profileBookmarksJSONFile;
|
|
if (aIsAutomaticBackup) {
|
|
let bookmarksBackupDir = gProfD.clone();
|
|
bookmarksBackupDir.append("bookmarkbackups");
|
|
let files = bookmarksBackupDir.directoryEntries;
|
|
let backup_date = new Date().toLocaleFormat("%Y-%m-%d");
|
|
while (files.hasMoreElements()) {
|
|
let entry = files.getNext().QueryInterface(Ci.nsIFile);
|
|
if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
|
|
profileBookmarksJSONFile = entry;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
profileBookmarksJSONFile = gProfD.clone();
|
|
profileBookmarksJSONFile.append("bookmarkbackups");
|
|
profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
|
|
}
|
|
do_check_true(profileBookmarksJSONFile.exists());
|
|
return profileBookmarksJSONFile;
|
|
}
|
|
|
|
/**
|
|
* Returns the frecency of a url.
|
|
*
|
|
* @param aURI
|
|
* The URI or spec to get frecency for.
|
|
* @return the frecency value.
|
|
*/
|
|
function frecencyForUrl(aURI)
|
|
{
|
|
let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
|
|
let stmt = DBConn().createStatement(
|
|
"SELECT frecency FROM moz_places WHERE url = ?1"
|
|
);
|
|
stmt.bindByIndex(0, url);
|
|
try {
|
|
if (!stmt.executeStep()) {
|
|
throw new Error("No result for frecency.");
|
|
}
|
|
return stmt.getInt32(0);
|
|
} finally {
|
|
stmt.finalize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the hidden status of a url.
|
|
*
|
|
* @param aURI
|
|
* The URI or spec to get hidden for.
|
|
* @return @return true if the url is hidden, false otherwise.
|
|
*/
|
|
function isUrlHidden(aURI)
|
|
{
|
|
let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
|
|
let stmt = DBConn().createStatement(
|
|
"SELECT hidden FROM moz_places WHERE url = ?1"
|
|
);
|
|
stmt.bindByIndex(0, url);
|
|
if (!stmt.executeStep())
|
|
throw new Error("No result for hidden.");
|
|
let hidden = stmt.getInt32(0);
|
|
stmt.finalize();
|
|
|
|
return !!hidden;
|
|
}
|
|
|
|
/**
|
|
* Compares two times in usecs, considering eventual platform timers skews.
|
|
*
|
|
* @param aTimeBefore
|
|
* The older time in usecs.
|
|
* @param aTimeAfter
|
|
* The newer time in usecs.
|
|
* @return true if times are ordered, false otherwise.
|
|
*/
|
|
function is_time_ordered(before, after) {
|
|
// Windows has an estimated 16ms timers precision, since Date.now() and
|
|
// PR_Now() use different code atm, the results can be unordered by this
|
|
// amount of time. See bug 558745 and bug 557406.
|
|
let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
|
|
// Just to be safe we consider 20ms.
|
|
let skew = isWindows ? 20000000 : 0;
|
|
return after - before > -skew;
|
|
}
|
|
|
|
/**
|
|
* Waits for all pending async statements on the default connection.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When all pending async statements finished.
|
|
* @rejects Never.
|
|
*
|
|
* @note The result is achieved by asynchronously executing a query requiring
|
|
* a write lock. Since all statements on the same connection are
|
|
* serialized, the end of this write operation means that all writes are
|
|
* complete. Note that WAL makes so that writers don't block readers, but
|
|
* this is a problem only across different connections.
|
|
*/
|
|
function promiseAsyncUpdates()
|
|
{
|
|
let deferred = Promise.defer();
|
|
|
|
let db = DBConn();
|
|
let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
|
|
begin.executeAsync();
|
|
begin.finalize();
|
|
|
|
let commit = db.createAsyncStatement("COMMIT");
|
|
commit.executeAsync({
|
|
handleResult: function () {},
|
|
handleError: function () {},
|
|
handleCompletion: function(aReason)
|
|
{
|
|
deferred.resolve();
|
|
}
|
|
});
|
|
commit.finalize();
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
/**
|
|
* Shutdowns Places, invoking the callback when the connection has been closed.
|
|
*
|
|
* @param aCallback
|
|
* Function to be called when done.
|
|
*/
|
|
function waitForConnectionClosed(aCallback)
|
|
{
|
|
Services.obs.addObserver(function WFCCCallback() {
|
|
Services.obs.removeObserver(WFCCCallback, "places-connection-closed");
|
|
aCallback();
|
|
}, "places-connection-closed", false);
|
|
shutdownPlaces();
|
|
}
|
|
|
|
/**
|
|
* Tests if a given guid is valid for use in Places or not.
|
|
*
|
|
* @param aGuid
|
|
* The guid to test.
|
|
* @param [optional] aStack
|
|
* The stack frame used to report the error.
|
|
*/
|
|
function do_check_valid_places_guid(aGuid,
|
|
aStack)
|
|
{
|
|
if (!aStack) {
|
|
aStack = Components.stack.caller;
|
|
}
|
|
do_check_true(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), aStack);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the guid for a given uri.
|
|
*
|
|
* @param aURI
|
|
* The uri to check.
|
|
* @param [optional] aStack
|
|
* The stack frame used to report the error.
|
|
* @return the associated the guid.
|
|
*/
|
|
function do_get_guid_for_uri(aURI,
|
|
aStack)
|
|
{
|
|
if (!aStack) {
|
|
aStack = Components.stack.caller;
|
|
}
|
|
let stmt = DBConn().createStatement(
|
|
"SELECT guid "
|
|
+ "FROM moz_places "
|
|
+ "WHERE url = :url "
|
|
);
|
|
stmt.params.url = aURI.spec;
|
|
do_check_true(stmt.executeStep(), aStack);
|
|
let guid = stmt.row.guid;
|
|
stmt.finalize();
|
|
do_check_valid_places_guid(guid, aStack);
|
|
return guid;
|
|
}
|
|
|
|
/**
|
|
* Tests that a guid was set in moz_places for a given uri.
|
|
*
|
|
* @param aURI
|
|
* The uri to check.
|
|
* @param [optional] aGUID
|
|
* The expected guid in the database.
|
|
*/
|
|
function do_check_guid_for_uri(aURI,
|
|
aGUID)
|
|
{
|
|
let caller = Components.stack.caller;
|
|
let guid = do_get_guid_for_uri(aURI, caller);
|
|
if (aGUID) {
|
|
do_check_valid_places_guid(aGUID, caller);
|
|
do_check_eq(guid, aGUID, caller);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the guid for a given bookmark.
|
|
*
|
|
* @param aId
|
|
* The bookmark id to check.
|
|
* @param [optional] aStack
|
|
* The stack frame used to report the error.
|
|
* @return the associated the guid.
|
|
*/
|
|
function do_get_guid_for_bookmark(aId,
|
|
aStack)
|
|
{
|
|
if (!aStack) {
|
|
aStack = Components.stack.caller;
|
|
}
|
|
let stmt = DBConn().createStatement(
|
|
"SELECT guid "
|
|
+ "FROM moz_bookmarks "
|
|
+ "WHERE id = :item_id "
|
|
);
|
|
stmt.params.item_id = aId;
|
|
do_check_true(stmt.executeStep(), aStack);
|
|
let guid = stmt.row.guid;
|
|
stmt.finalize();
|
|
do_check_valid_places_guid(guid, aStack);
|
|
return guid;
|
|
}
|
|
|
|
/**
|
|
* Tests that a guid was set in moz_places for a given bookmark.
|
|
*
|
|
* @param aId
|
|
* The bookmark id to check.
|
|
* @param [optional] aGUID
|
|
* The expected guid in the database.
|
|
*/
|
|
function do_check_guid_for_bookmark(aId,
|
|
aGUID)
|
|
{
|
|
let caller = Components.stack.caller;
|
|
let guid = do_get_guid_for_bookmark(aId, caller);
|
|
if (aGUID) {
|
|
do_check_valid_places_guid(aGUID, caller);
|
|
do_check_eq(guid, aGUID, caller);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logs info to the console in the standard way (includes the filename).
|
|
*
|
|
* @param aMessage
|
|
* The message to log to the console.
|
|
*/
|
|
function do_log_info(aMessage)
|
|
{
|
|
print("TEST-INFO | " + _TEST_FILE + " | " + aMessage);
|
|
}
|
|
|
|
/**
|
|
* Compares 2 arrays returning whether they contains the same elements.
|
|
*
|
|
* @param a1
|
|
* First array to compare.
|
|
* @param a2
|
|
* Second array to compare.
|
|
* @param [optional] sorted
|
|
* Whether the comparison should take in count position of the elements.
|
|
* @return true if the arrays contain the same elements, false otherwise.
|
|
*/
|
|
function do_compare_arrays(a1, a2, sorted)
|
|
{
|
|
if (a1.length != a2.length)
|
|
return false;
|
|
|
|
if (sorted) {
|
|
return a1.every(function (e, i) e == a2[i]);
|
|
}
|
|
else {
|
|
return a1.filter(function (e) a2.indexOf(e) == -1).length == 0 &&
|
|
a2.filter(function (e) a1.indexOf(e) == -1).length == 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generic nsINavBookmarkObserver that doesn't implement anything, but provides
|
|
* dummy methods to prevent errors about an object not having a certain method.
|
|
*/
|
|
function NavBookmarkObserver() {}
|
|
|
|
NavBookmarkObserver.prototype = {
|
|
onBeginUpdateBatch: function () {},
|
|
onEndUpdateBatch: function () {},
|
|
onItemAdded: function () {},
|
|
onItemRemoved: function () {},
|
|
onItemChanged: function () {},
|
|
onItemVisited: function () {},
|
|
onItemMoved: function () {},
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsINavBookmarkObserver,
|
|
])
|
|
};
|
|
|
|
/**
|
|
* Generic nsINavHistoryObserver that doesn't implement anything, but provides
|
|
* dummy methods to prevent errors about an object not having a certain method.
|
|
*/
|
|
function NavHistoryObserver() {}
|
|
|
|
NavHistoryObserver.prototype = {
|
|
onBeginUpdateBatch: function () {},
|
|
onEndUpdateBatch: function () {},
|
|
onVisit: function () {},
|
|
onTitleChanged: function () {},
|
|
onDeleteURI: function () {},
|
|
onClearHistory: function () {},
|
|
onPageChanged: function () {},
|
|
onDeleteVisits: function () {},
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsINavHistoryObserver,
|
|
])
|
|
};
|
|
|
|
/**
|
|
* Generic nsINavHistoryResultObserver that doesn't implement anything, but
|
|
* provides dummy methods to prevent errors about an object not having a certain
|
|
* method.
|
|
*/
|
|
function NavHistoryResultObserver() {}
|
|
|
|
NavHistoryResultObserver.prototype = {
|
|
batching: function () {},
|
|
containerStateChanged: function () {},
|
|
invalidateContainer: function () {},
|
|
nodeAnnotationChanged: function () {},
|
|
nodeDateAddedChanged: function () {},
|
|
nodeHistoryDetailsChanged: function () {},
|
|
nodeIconChanged: function () {},
|
|
nodeInserted: function () {},
|
|
nodeKeywordChanged: function () {},
|
|
nodeLastModifiedChanged: function () {},
|
|
nodeMoved: function () {},
|
|
nodeRemoved: function () {},
|
|
nodeTagsChanged: function () {},
|
|
nodeTitleChanged: function () {},
|
|
nodeURIChanged: function () {},
|
|
sortingChanged: function () {},
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsINavHistoryResultObserver,
|
|
])
|
|
};
|
|
|
|
/**
|
|
* Asynchronously adds visits to a page.
|
|
*
|
|
* @param aPlaceInfo
|
|
* Can be an nsIURI, in such a case a single LINK visit will be added.
|
|
* Otherwise can be an object describing the visit to add, or an array
|
|
* of these objects:
|
|
* { uri: nsIURI of the page,
|
|
* transition: one of the TRANSITION_* from nsINavHistoryService,
|
|
* [optional] title: title of the page,
|
|
* [optional] visitDate: visit date in microseconds from the epoch
|
|
* [optional] referrer: nsIURI of the referrer for this visit
|
|
* }
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When all visits have been added successfully.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
function promiseAddVisits(aPlaceInfo)
|
|
{
|
|
let deferred = Promise.defer();
|
|
let places = [];
|
|
if (aPlaceInfo instanceof Ci.nsIURI) {
|
|
places.push({ uri: aPlaceInfo });
|
|
}
|
|
else if (Array.isArray(aPlaceInfo)) {
|
|
places = places.concat(aPlaceInfo);
|
|
} else {
|
|
places.push(aPlaceInfo)
|
|
}
|
|
|
|
// Create mozIVisitInfo for each entry.
|
|
let now = Date.now();
|
|
for (let i = 0; i < places.length; i++) {
|
|
if (!places[i].title) {
|
|
places[i].title = "test visit for " + places[i].uri.spec;
|
|
}
|
|
places[i].visits = [{
|
|
transitionType: places[i].transition === undefined ? TRANSITION_LINK
|
|
: places[i].transition,
|
|
visitDate: places[i].visitDate || (now++) * 1000,
|
|
referrerURI: places[i].referrer
|
|
}];
|
|
}
|
|
|
|
PlacesUtils.asyncHistory.updatePlaces(
|
|
places,
|
|
{
|
|
handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
|
|
let ex = new Components.Exception("Unexpected error in adding visits.",
|
|
aResultCode);
|
|
deferred.reject(ex);
|
|
},
|
|
handleResult: function () {},
|
|
handleCompletion: function UP_handleCompletion() {
|
|
deferred.resolve();
|
|
}
|
|
}
|
|
);
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
/**
|
|
* Asynchronously check a url is visited.
|
|
*
|
|
* @param aURI The URI.
|
|
* @return {Promise}
|
|
* @resolves When the check has been added successfully.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
function promiseIsURIVisited(aURI) {
|
|
let deferred = Promise.defer();
|
|
|
|
PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
|
|
deferred.resolve(aIsVisited);
|
|
});
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
/**
|
|
* Asynchronously set the favicon associated with a page.
|
|
* @param aPageURI
|
|
* The page's URI
|
|
* @param aIconURI
|
|
* The URI of the favicon to be set.
|
|
*/
|
|
function promiseSetIconForPage(aPageURI, aIconURI) {
|
|
let deferred = Promise.defer();
|
|
PlacesUtils.favicons.setAndFetchFaviconForPage(
|
|
aPageURI, aIconURI, true,
|
|
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
|
|
() => { deferred.resolve(); });
|
|
return deferred.promise;
|
|
}
|